all repos — honk @ d0c3466096be24942914dd6fdc502bb360ce633e

my fork of honk

honk.go (view raw)

   1//
   2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
   3//
   4// Permission to use, copy, modify, and distribute this software for any
   5// purpose with or without fee is hereby granted, provided that the above
   6// copyright notice and this permission notice appear in all copies.
   7//
   8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
   9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15
  16package main
  17
  18import (
  19	"bytes"
  20	"database/sql"
  21	"fmt"
  22	"html"
  23	"html/template"
  24	"io"
  25	"log"
  26	notrand "math/rand"
  27	"net/http"
  28	"net/url"
  29	"os"
  30	"sort"
  31	"strconv"
  32	"strings"
  33	"time"
  34
  35	"github.com/gorilla/mux"
  36	"humungus.tedunangst.com/r/webs/htfilter"
  37	"humungus.tedunangst.com/r/webs/image"
  38	"humungus.tedunangst.com/r/webs/junk"
  39	"humungus.tedunangst.com/r/webs/login"
  40	"humungus.tedunangst.com/r/webs/rss"
  41	"humungus.tedunangst.com/r/webs/templates"
  42)
  43
  44type WhatAbout struct {
  45	ID      int64
  46	Name    string
  47	Display string
  48	About   string
  49	Key     string
  50	URL     string
  51}
  52
  53type Honk struct {
  54	ID       int64
  55	UserID   int64
  56	Username string
  57	What     string
  58	Honker   string
  59	Handle   string
  60	Oonker   string
  61	XID      string
  62	RID      string
  63	Date     time.Time
  64	URL      string
  65	Noise    string
  66	Precis   string
  67	Convoy   string
  68	Audience []string
  69	Public   bool
  70	Whofore  int64
  71	HTML     template.HTML
  72	Donks    []*Donk
  73}
  74
  75type Donk struct {
  76	FileID  int64
  77	XID     string
  78	Name    string
  79	URL     string
  80	Media   string
  81	Local   bool
  82	Content []byte
  83}
  84
  85type Honker struct {
  86	ID     int64
  87	UserID int64
  88	Name   string
  89	XID    string
  90	Flavor string
  91	Combos []string
  92}
  93
  94var serverName string
  95var iconName = "icon.png"
  96var serverMsg = "Things happen."
  97
  98var readviews *templates.Template
  99
 100func getInfo(r *http.Request) map[string]interface{} {
 101	templinfo := make(map[string]interface{})
 102	templinfo["StyleParam"] = getstyleparam("views/style.css")
 103	templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
 104	templinfo["ServerName"] = serverName
 105	templinfo["IconName"] = iconName
 106	templinfo["UserInfo"] = login.GetUserInfo(r)
 107	return templinfo
 108}
 109
 110func homepage(w http.ResponseWriter, r *http.Request) {
 111	templinfo := getInfo(r)
 112	u := login.GetUserInfo(r)
 113	var honks []*Honk
 114	if u != nil {
 115		if r.URL.Path == "/atme" {
 116			honks = gethonksforme(u.UserID)
 117		} else {
 118			honks = gethonksforuser(u.UserID)
 119			honks = osmosis(honks, u.UserID)
 120		}
 121		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 122	} else {
 123		honks = getpublichonks()
 124	}
 125
 126	reverbolate(honks)
 127
 128	templinfo["Honks"] = honks
 129	templinfo["ShowRSS"] = true
 130	templinfo["ServerMessage"] = serverMsg
 131	if u == nil {
 132		w.Header().Set("Cache-Control", "max-age=60")
 133	} else {
 134		w.Header().Set("Cache-Control", "max-age=0")
 135	}
 136	err := readviews.Execute(w, "honkpage.html", templinfo)
 137	if err != nil {
 138		log.Print(err)
 139	}
 140}
 141
 142func showrss(w http.ResponseWriter, r *http.Request) {
 143	name := mux.Vars(r)["name"]
 144
 145	var honks []*Honk
 146	if name != "" {
 147		honks = gethonksbyuser(name, false)
 148	} else {
 149		honks = getpublichonks()
 150	}
 151	reverbolate(honks)
 152
 153	home := fmt.Sprintf("https://%s/", serverName)
 154	base := home
 155	if name != "" {
 156		home += "u/" + name
 157		name += " "
 158	}
 159	feed := rss.Feed{
 160		Title:       name + "honk",
 161		Link:        home,
 162		Description: name + "honk rss",
 163		Image: &rss.Image{
 164			URL:   base + "icon.png",
 165			Title: name + "honk rss",
 166			Link:  home,
 167		},
 168	}
 169	var modtime time.Time
 170	for _, honk := range honks {
 171		desc := string(honk.HTML)
 172		for _, d := range honk.Donks {
 173			desc += fmt.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
 174				d.URL, html.EscapeString(d.Name))
 175		}
 176
 177		feed.Items = append(feed.Items, &rss.Item{
 178			Title:       fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
 179			Description: rss.CData{desc},
 180			Link:        honk.URL,
 181			PubDate:     honk.Date.Format(time.RFC1123),
 182			Guid:        &rss.Guid{IsPermaLink: true, Value: honk.URL},
 183		})
 184		if honk.Date.After(modtime) {
 185			modtime = honk.Date
 186		}
 187	}
 188	w.Header().Set("Cache-Control", "max-age=300")
 189	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 190
 191	err := feed.Write(w)
 192	if err != nil {
 193		log.Printf("error writing rss: %s", err)
 194	}
 195}
 196
 197func butwhatabout(name string) (*WhatAbout, error) {
 198	row := stmtWhatAbout.QueryRow(name)
 199	var user WhatAbout
 200	err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key)
 201	user.URL = fmt.Sprintf("https://%s/u/%s", serverName, user.Name)
 202	return &user, err
 203}
 204
 205func crappola(j junk.Junk) bool {
 206	t, _ := j.GetString("type")
 207	a, _ := j.GetString("actor")
 208	o, _ := j.GetString("object")
 209	if t == "Delete" && a == o {
 210		log.Printf("crappola from %s", a)
 211		return true
 212	}
 213	return false
 214}
 215
 216func ping(user *WhatAbout, who string) {
 217	box, err := getboxes(who)
 218	if err != nil {
 219		log.Printf("no inbox for ping: %s", err)
 220		return
 221	}
 222	j := junk.New()
 223	j["@context"] = itiswhatitis
 224	j["type"] = "Ping"
 225	j["id"] = user.URL + "/ping/" + xfiltrate()
 226	j["actor"] = user.URL
 227	j["to"] = who
 228	keyname, key := ziggy(user.Name)
 229	err = PostJunk(keyname, key, box.In, j)
 230	if err != nil {
 231		log.Printf("can't send ping: %s", err)
 232		return
 233	}
 234	log.Printf("sent ping to %s: %s", who, j["id"])
 235}
 236
 237func pong(user *WhatAbout, who string, obj string) {
 238	box, err := getboxes(who)
 239	if err != nil {
 240		log.Printf("no inbox for pong %s : %s", who, err)
 241		return
 242	}
 243	j := junk.New()
 244	j["@context"] = itiswhatitis
 245	j["type"] = "Pong"
 246	j["id"] = user.URL + "/pong/" + xfiltrate()
 247	j["actor"] = user.URL
 248	j["to"] = who
 249	j["object"] = obj
 250	keyname, key := ziggy(user.Name)
 251	err = PostJunk(keyname, key, box.In, j)
 252	if err != nil {
 253		log.Printf("can't send pong: %s", err)
 254		return
 255	}
 256}
 257
 258func inbox(w http.ResponseWriter, r *http.Request) {
 259	name := mux.Vars(r)["name"]
 260	user, err := butwhatabout(name)
 261	if err != nil {
 262		http.NotFound(w, r)
 263		return
 264	}
 265	var buf bytes.Buffer
 266	io.Copy(&buf, r.Body)
 267	payload := buf.Bytes()
 268	j, err := junk.Read(bytes.NewReader(payload))
 269	if err != nil {
 270		log.Printf("bad payload: %s", err)
 271		io.WriteString(os.Stdout, "bad payload\n")
 272		os.Stdout.Write(payload)
 273		io.WriteString(os.Stdout, "\n")
 274		return
 275	}
 276	if crappola(j) {
 277		return
 278	}
 279	keyname, err := zag(r, payload)
 280	if err != nil {
 281		log.Printf("inbox message failed signature: %s", err)
 282		if keyname != "" {
 283			keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
 284		}
 285		if err != nil {
 286			return
 287		}
 288	}
 289	what, _ := j.GetString("type")
 290	if what == "Like" {
 291		return
 292	}
 293	who, _ := j.GetString("actor")
 294	origin := keymatch(keyname, who)
 295	if origin == "" {
 296		log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 297		return
 298	}
 299	objid, _ := j.GetString("id")
 300	if thoudostbitethythumb(user.ID, []string{who}, objid) {
 301		log.Printf("ignoring thumb sucker %s", who)
 302		return
 303	}
 304	switch what {
 305	case "Ping":
 306		obj, _ := j.GetString("id")
 307		log.Printf("ping from %s: %s", who, obj)
 308		pong(user, who, obj)
 309	case "Pong":
 310		obj, _ := j.GetString("object")
 311		log.Printf("pong from %s: %s", who, obj)
 312	case "Follow":
 313		obj, _ := j.GetString("object")
 314		if obj == user.URL {
 315			log.Printf("updating honker follow: %s", who)
 316			stmtSaveDub.Exec(user.ID, who, who, "dub")
 317			go rubadubdub(user, j)
 318		} else {
 319			log.Printf("can't follow %s", obj)
 320		}
 321	case "Accept":
 322		log.Printf("updating honker accept: %s", who)
 323		_, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
 324		if err != nil {
 325			log.Printf("error updating honker: %s", err)
 326			return
 327		}
 328	case "Update":
 329		obj, ok := j.GetMap("object")
 330		if ok {
 331			what, _ := obj.GetString("type")
 332			switch what {
 333			case "Person":
 334				return
 335			}
 336		}
 337		log.Printf("unknown Update activity")
 338		fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 339		j.Write(fd)
 340		io.WriteString(fd, "\n")
 341		fd.Close()
 342
 343	case "Undo":
 344		obj, ok := j.GetMap("object")
 345		if !ok {
 346			log.Printf("unknown undo no object")
 347		} else {
 348			what, _ := obj.GetString("type")
 349			switch what {
 350			case "Follow":
 351				log.Printf("updating honker undo: %s", who)
 352				_, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
 353				if err != nil {
 354					log.Printf("error updating honker: %s", err)
 355					return
 356				}
 357			case "Like":
 358			case "Announce":
 359			default:
 360				log.Printf("unknown undo: %s", what)
 361			}
 362		}
 363	default:
 364		go consumeactivity(user, j, origin)
 365	}
 366}
 367
 368func ximport(w http.ResponseWriter, r *http.Request) {
 369	xid := r.FormValue("xid")
 370	j, err := GetJunk(xid)
 371	if err != nil {
 372		log.Printf("error getting external object: %s", err)
 373		return
 374	}
 375	u := login.GetUserInfo(r)
 376	user, _ := butwhatabout(u.Username)
 377	xonk := xonkxonk(user, j, originate(xid))
 378	convoy := ""
 379	if xonk != nil {
 380		convoy = xonk.Convoy
 381		savexonk(user, xonk)
 382	}
 383	http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
 384}
 385
 386func xzone(w http.ResponseWriter, r *http.Request) {
 387	u := login.GetUserInfo(r)
 388	var honkers []string
 389	rows, err := stmtRecentHonkers.Query(u.UserID)
 390	if err != nil {
 391		log.Printf("query err: %s", err)
 392		return
 393	}
 394	for rows.Next() {
 395		var s string
 396		rows.Scan(&s)
 397		honkers = append(honkers, s)
 398	}
 399
 400	templinfo := getInfo(r)
 401	templinfo["XCSRF"] = login.GetCSRF("ximport", r)
 402	templinfo["Honkers"] = honkers
 403	err = readviews.Execute(w, "xzone.html", templinfo)
 404	if err != nil {
 405		log.Print(err)
 406	}
 407}
 408func outbox(w http.ResponseWriter, r *http.Request) {
 409	name := mux.Vars(r)["name"]
 410	user, err := butwhatabout(name)
 411	if err != nil {
 412		http.NotFound(w, r)
 413		return
 414	}
 415	honks := gethonksbyuser(name, false)
 416
 417	var jonks []map[string]interface{}
 418	for _, h := range honks {
 419		j, _ := jonkjonk(user, h)
 420		jonks = append(jonks, j)
 421	}
 422
 423	j := junk.New()
 424	j["@context"] = itiswhatitis
 425	j["id"] = user.URL + "/outbox"
 426	j["type"] = "OrderedCollection"
 427	j["totalItems"] = len(jonks)
 428	j["orderedItems"] = jonks
 429
 430	w.Header().Set("Cache-Control", "max-age=60")
 431	w.Header().Set("Content-Type", theonetruename)
 432	j.Write(w)
 433}
 434
 435func emptiness(w http.ResponseWriter, r *http.Request) {
 436	name := mux.Vars(r)["name"]
 437	user, err := butwhatabout(name)
 438	if err != nil {
 439		http.NotFound(w, r)
 440		return
 441	}
 442	colname := "/followers"
 443	if strings.HasSuffix(r.URL.Path, "/following") {
 444		colname = "/following"
 445	}
 446	j := junk.New()
 447	j["@context"] = itiswhatitis
 448	j["id"] = user.URL + colname
 449	j["type"] = "OrderedCollection"
 450	j["totalItems"] = 0
 451	j["orderedItems"] = []interface{}{}
 452
 453	w.Header().Set("Cache-Control", "max-age=60")
 454	w.Header().Set("Content-Type", theonetruename)
 455	j.Write(w)
 456}
 457
 458func showuser(w http.ResponseWriter, r *http.Request) {
 459	name := mux.Vars(r)["name"]
 460	user, err := butwhatabout(name)
 461	if err != nil {
 462		http.NotFound(w, r)
 463		return
 464	}
 465	if friendorfoe(r.Header.Get("Accept")) {
 466		j := asjonker(user)
 467		w.Header().Set("Cache-Control", "max-age=600")
 468		w.Header().Set("Content-Type", theonetruename)
 469		j.Write(w)
 470		return
 471	}
 472	u := login.GetUserInfo(r)
 473	honks := gethonksbyuser(name, u != nil && u.Username == name)
 474	honkpage(w, r, u, user, honks, "")
 475}
 476
 477func showhonker(w http.ResponseWriter, r *http.Request) {
 478	u := login.GetUserInfo(r)
 479	name := mux.Vars(r)["name"]
 480	var honks []*Honk
 481	if name == "" {
 482		name = r.FormValue("xid")
 483		honks = gethonksbyxonker(u.UserID, name)
 484	} else {
 485		honks = gethonksbyhonker(u.UserID, name)
 486	}
 487	honkpage(w, r, u, nil, honks, "honks by honker: "+name)
 488}
 489
 490func showcombo(w http.ResponseWriter, r *http.Request) {
 491	name := mux.Vars(r)["name"]
 492	u := login.GetUserInfo(r)
 493	honks := gethonksbycombo(u.UserID, name)
 494	honkpage(w, r, u, nil, honks, "honks by combo: "+name)
 495}
 496func showconvoy(w http.ResponseWriter, r *http.Request) {
 497	c := r.FormValue("c")
 498	var userid int64 = -1
 499	u := login.GetUserInfo(r)
 500	if u != nil {
 501		userid = u.UserID
 502	}
 503	honks := gethonksbyconvoy(userid, c)
 504	honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
 505}
 506
 507func showhonk(w http.ResponseWriter, r *http.Request) {
 508	name := mux.Vars(r)["name"]
 509	user, err := butwhatabout(name)
 510	if err != nil {
 511		http.NotFound(w, r)
 512		return
 513	}
 514	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 515	h := getxonk(user.ID, xid)
 516	if h == nil || !h.Public {
 517		http.NotFound(w, r)
 518		return
 519	}
 520	if friendorfoe(r.Header.Get("Accept")) {
 521		donksforhonks([]*Honk{h})
 522		_, j := jonkjonk(user, h)
 523		j["@context"] = itiswhatitis
 524		w.Header().Set("Cache-Control", "max-age=3600")
 525		w.Header().Set("Content-Type", theonetruename)
 526		j.Write(w)
 527		return
 528	}
 529	honks := gethonksbyconvoy(-1, h.Convoy)
 530	u := login.GetUserInfo(r)
 531	honkpage(w, r, u, nil, honks, "one honk maybe more")
 532}
 533
 534func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 535	honks []*Honk, infomsg string) {
 536	reverbolate(honks)
 537	templinfo := getInfo(r)
 538	if u != nil {
 539		honks = osmosis(honks, u.UserID)
 540		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 541	}
 542	if u == nil {
 543		w.Header().Set("Cache-Control", "max-age=60")
 544	}
 545	if user != nil {
 546		filt := htfilter.New()
 547		templinfo["Name"] = user.Name
 548		whatabout := user.About
 549		whatabout = obfusbreak(user.About)
 550		templinfo["WhatAbout"], _ = filt.String(whatabout)
 551	}
 552	templinfo["Honks"] = honks
 553	templinfo["ServerMessage"] = infomsg
 554	err := readviews.Execute(w, "honkpage.html", templinfo)
 555	if err != nil {
 556		log.Print(err)
 557	}
 558}
 559
 560func saveuser(w http.ResponseWriter, r *http.Request) {
 561	whatabout := r.FormValue("whatabout")
 562	u := login.GetUserInfo(r)
 563	db := opendatabase()
 564	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 565	if err != nil {
 566		log.Printf("error bouting what: %s", err)
 567	}
 568
 569	http.Redirect(w, r, "/account", http.StatusSeeOther)
 570}
 571
 572func gethonkers(userid int64) []*Honker {
 573	rows, err := stmtHonkers.Query(userid)
 574	if err != nil {
 575		log.Printf("error querying honkers: %s", err)
 576		return nil
 577	}
 578	defer rows.Close()
 579	var honkers []*Honker
 580	for rows.Next() {
 581		var f Honker
 582		var combos string
 583		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 584		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 585		if err != nil {
 586			log.Printf("error scanning honker: %s", err)
 587			return nil
 588		}
 589		honkers = append(honkers, &f)
 590	}
 591	return honkers
 592}
 593
 594func getdubs(userid int64) []*Honker {
 595	rows, err := stmtDubbers.Query(userid)
 596	if err != nil {
 597		log.Printf("error querying dubs: %s", err)
 598		return nil
 599	}
 600	defer rows.Close()
 601	var honkers []*Honker
 602	for rows.Next() {
 603		var f Honker
 604		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 605		if err != nil {
 606			log.Printf("error scanning honker: %s", err)
 607			return nil
 608		}
 609		honkers = append(honkers, &f)
 610	}
 611	return honkers
 612}
 613
 614func allusers() []login.UserInfo {
 615	var users []login.UserInfo
 616	rows, _ := opendatabase().Query("select userid, username from users")
 617	defer rows.Close()
 618	for rows.Next() {
 619		var u login.UserInfo
 620		rows.Scan(&u.UserID, &u.Username)
 621		users = append(users, u)
 622	}
 623	return users
 624}
 625
 626func getxonk(userid int64, xid string) *Honk {
 627	h := new(Honk)
 628	var dt, aud string
 629	row := stmtOneXonk.QueryRow(userid, xid)
 630	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 631		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 632	if err != nil {
 633		if err != sql.ErrNoRows {
 634			log.Printf("error scanning xonk: %s", err)
 635		}
 636		return nil
 637	}
 638	h.Date, _ = time.Parse(dbtimeformat, dt)
 639	h.Audience = strings.Split(aud, " ")
 640	h.Public = !keepitquiet(h.Audience)
 641	return h
 642}
 643
 644func getpublichonks() []*Honk {
 645	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 646	rows, err := stmtPublicHonks.Query(dt)
 647	return getsomehonks(rows, err)
 648}
 649func gethonksbyuser(name string, includeprivate bool) []*Honk {
 650	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 651	whofore := 2
 652	if includeprivate {
 653		whofore = 3
 654	}
 655	rows, err := stmtUserHonks.Query(whofore, name, dt)
 656	return getsomehonks(rows, err)
 657}
 658func gethonksforuser(userid int64) []*Honk {
 659	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 660	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 661	return getsomehonks(rows, err)
 662}
 663func gethonksforme(userid int64) []*Honk {
 664	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 665	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 666	return getsomehonks(rows, err)
 667}
 668func gethonksbyhonker(userid int64, honker string) []*Honk {
 669	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 670	return getsomehonks(rows, err)
 671}
 672func gethonksbyxonker(userid int64, xonker string) []*Honk {
 673	rows, err := stmtHonksByXonker.Query(userid, xonker, userid)
 674	return getsomehonks(rows, err)
 675}
 676func gethonksbycombo(userid int64, combo string) []*Honk {
 677	combo = "% " + combo + " %"
 678	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 679	return getsomehonks(rows, err)
 680}
 681func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 682	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 683	honks := getsomehonks(rows, err)
 684	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 685		honks[i], honks[j] = honks[j], honks[i]
 686	}
 687	return honks
 688}
 689
 690func getsomehonks(rows *sql.Rows, err error) []*Honk {
 691	if err != nil {
 692		log.Printf("error querying honks: %s", err)
 693		return nil
 694	}
 695	defer rows.Close()
 696	var honks []*Honk
 697	for rows.Next() {
 698		var h Honk
 699		var dt, aud string
 700		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 701			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 702		if err != nil {
 703			log.Printf("error scanning honks: %s", err)
 704			return nil
 705		}
 706		h.Date, _ = time.Parse(dbtimeformat, dt)
 707		h.Audience = strings.Split(aud, " ")
 708		h.Public = !keepitquiet(h.Audience)
 709		honks = append(honks, &h)
 710	}
 711	rows.Close()
 712	donksforhonks(honks)
 713	return honks
 714}
 715
 716func donksforhonks(honks []*Honk) {
 717	db := opendatabase()
 718	var ids []string
 719	hmap := make(map[int64]*Honk)
 720	for _, h := range honks {
 721		ids = append(ids, fmt.Sprintf("%d", h.ID))
 722		hmap[h.ID] = h
 723	}
 724	q := fmt.Sprintf("select honkid, donks.fileid, xid, name, url, media, local from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
 725	rows, err := db.Query(q)
 726	if err != nil {
 727		log.Printf("error querying donks: %s", err)
 728		return
 729	}
 730	defer rows.Close()
 731	for rows.Next() {
 732		var hid int64
 733		var d Donk
 734		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 735		if err != nil {
 736			log.Printf("error scanning donk: %s", err)
 737			continue
 738		}
 739		h := hmap[hid]
 740		h.Donks = append(h.Donks, &d)
 741	}
 742}
 743
 744func savebonk(w http.ResponseWriter, r *http.Request) {
 745	xid := r.FormValue("xid")
 746	userinfo := login.GetUserInfo(r)
 747	user, _ := butwhatabout(userinfo.Username)
 748
 749	log.Printf("bonking %s", xid)
 750
 751	xonk := getxonk(userinfo.UserID, xid)
 752	if xonk == nil {
 753		return
 754	}
 755	if !xonk.Public {
 756		return
 757	}
 758	donksforhonks([]*Honk{xonk})
 759
 760	dt := time.Now().UTC()
 761	bonk := Honk{
 762		UserID:   userinfo.UserID,
 763		Username: userinfo.Username,
 764		What:     "bonk",
 765		Honker:   user.URL,
 766		XID:      xonk.XID,
 767		Date:     dt,
 768		Donks:    xonk.Donks,
 769		Audience: []string{thewholeworld},
 770		Public:   true,
 771	}
 772
 773	oonker := xonk.Oonker
 774	if oonker == "" {
 775		oonker = xonk.Honker
 776	}
 777	aud := strings.Join(bonk.Audience, " ")
 778	whofore := 2
 779	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 780		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 781		xonk.Precis, oonker)
 782	if err != nil {
 783		log.Printf("error saving bonk: %s", err)
 784		return
 785	}
 786	bonk.ID, _ = res.LastInsertId()
 787	for _, d := range bonk.Donks {
 788		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 789		if err != nil {
 790			log.Printf("err saving donk: %s", err)
 791			return
 792		}
 793	}
 794
 795	go honkworldwide(user, &bonk)
 796}
 797
 798func zonkit(w http.ResponseWriter, r *http.Request) {
 799	wherefore := r.FormValue("wherefore")
 800	var what string
 801	switch wherefore {
 802	case "this honk":
 803		what = r.FormValue("honk")
 804		wherefore = "zonk"
 805	case "this honker":
 806		what = r.FormValue("honker")
 807		wherefore = "zonker"
 808	case "this convoy":
 809		what = r.FormValue("convoy")
 810		wherefore = "zonvoy"
 811	}
 812	if what == "" {
 813		return
 814	}
 815
 816	log.Printf("zonking %s %s", wherefore, what)
 817	userinfo := login.GetUserInfo(r)
 818	if wherefore == "zonk" {
 819		xonk := getxonk(userinfo.UserID, what)
 820		if xonk != nil {
 821			stmtZonkDonks.Exec(xonk.ID)
 822			stmtZonkIt.Exec(userinfo.UserID, what)
 823			if xonk.Whofore == 2 || xonk.Whofore == 3 {
 824				zonk := Honk{
 825					What:     "zonk",
 826					XID:      xonk.XID,
 827					Date:     time.Now().UTC(),
 828					Audience: oneofakind(xonk.Audience),
 829				}
 830
 831				user, _ := butwhatabout(userinfo.Username)
 832				log.Printf("announcing deleted honk: %s", what)
 833				go honkworldwide(user, &zonk)
 834			}
 835		}
 836	} else {
 837		_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 838		if err != nil {
 839			log.Printf("error saving zonker: %s", err)
 840			return
 841		}
 842	}
 843}
 844
 845func savehonk(w http.ResponseWriter, r *http.Request) {
 846	rid := r.FormValue("rid")
 847	noise := r.FormValue("noise")
 848
 849	userinfo := login.GetUserInfo(r)
 850	user, _ := butwhatabout(userinfo.Username)
 851
 852	dt := time.Now().UTC()
 853	xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
 854	what := "honk"
 855	if rid != "" {
 856		what = "tonk"
 857	}
 858	honk := Honk{
 859		UserID:   userinfo.UserID,
 860		Username: userinfo.Username,
 861		What:     "honk",
 862		Honker:   user.URL,
 863		XID:      xid,
 864		Date:     dt,
 865	}
 866	if strings.HasPrefix(noise, "DZ:") {
 867		idx := strings.Index(noise, "\n")
 868		if idx == -1 {
 869			honk.Precis = noise
 870			noise = ""
 871		} else {
 872			honk.Precis = noise[:idx]
 873			noise = noise[idx+1:]
 874		}
 875	}
 876	noise = hooterize(noise)
 877	noise = strings.TrimSpace(noise)
 878	honk.Precis = strings.TrimSpace(honk.Precis)
 879
 880	var convoy string
 881	if rid != "" {
 882		xonk := getxonk(userinfo.UserID, rid)
 883		if xonk != nil {
 884			if xonk.Public {
 885				honk.Audience = append(honk.Audience, xonk.Audience...)
 886			}
 887			convoy = xonk.Convoy
 888		} else {
 889			xonkaud, c := whosthere(rid)
 890			honk.Audience = append(honk.Audience, xonkaud...)
 891			convoy = c
 892		}
 893		for i, a := range honk.Audience {
 894			if a == thewholeworld {
 895				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
 896				break
 897			}
 898		}
 899		honk.RID = rid
 900	} else {
 901		honk.Audience = []string{thewholeworld}
 902	}
 903	if noise != "" && noise[0] == '@' {
 904		honk.Audience = append(grapevine(noise), honk.Audience...)
 905	} else {
 906		honk.Audience = append(honk.Audience, grapevine(noise)...)
 907	}
 908	if convoy == "" {
 909		convoy = "data:,electrichonkytonk-" + xfiltrate()
 910	}
 911	butnottooloud(honk.Audience)
 912	honk.Audience = oneofakind(honk.Audience)
 913	if len(honk.Audience) == 0 {
 914		log.Printf("honk to nowhere")
 915		return
 916	}
 917	honk.Public = !keepitquiet(honk.Audience)
 918	noise = obfusbreak(noise)
 919	honk.Noise = noise
 920	honk.Convoy = convoy
 921
 922	file, filehdr, err := r.FormFile("donk")
 923	if err == nil {
 924		var buf bytes.Buffer
 925		io.Copy(&buf, file)
 926		file.Close()
 927		data := buf.Bytes()
 928		xid := xfiltrate()
 929		var media, name string
 930		img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
 931		if err == nil {
 932			data = img.Data
 933			format := img.Format
 934			media = "image/" + format
 935			if format == "jpeg" {
 936				format = "jpg"
 937			}
 938			name = xid + "." + format
 939			xid = name
 940		} else {
 941			maxsize := 100000
 942			if len(data) > maxsize {
 943				log.Printf("bad image: %s too much text: %d", err, len(data))
 944				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 945				return
 946			}
 947			for i := 0; i < len(data); i++ {
 948				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 949					log.Printf("bad image: %s not text: %d", err, data[i])
 950					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 951					return
 952				}
 953			}
 954			media = "text/plain"
 955			name = filehdr.Filename
 956			if name == "" {
 957				name = xid + ".txt"
 958			}
 959			xid += ".txt"
 960		}
 961		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 962		res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
 963		if err != nil {
 964			log.Printf("unable to save image: %s", err)
 965			return
 966		}
 967		var d Donk
 968		d.FileID, _ = res.LastInsertId()
 969		d.XID = name
 970		d.Name = name
 971		d.Media = media
 972		d.URL = url
 973		d.Local = true
 974		honk.Donks = append(honk.Donks, &d)
 975	}
 976	herd := herdofemus(honk.Noise)
 977	for _, e := range herd {
 978		donk := savedonk(e.ID, e.Name, "image/png", true)
 979		if donk != nil {
 980			donk.Name = e.Name
 981			honk.Donks = append(honk.Donks, donk)
 982		}
 983	}
 984	honk.Donks = append(honk.Donks, memetics(honk.Noise)...)
 985
 986	aud := strings.Join(honk.Audience, " ")
 987	whofore := 2
 988	if !honk.Public {
 989		whofore = 3
 990	}
 991	if r.FormValue("preview") == "preview" {
 992		honks := []*Honk{&honk}
 993		reverbolate(honks)
 994		templinfo := getInfo(r)
 995		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 996		templinfo["Honks"] = honks
 997		templinfo["Noise"] = r.FormValue("noise")
 998		templinfo["ServerMessage"] = "honk preview"
 999		err := readviews.Execute(w, "honkpage.html", templinfo)
1000		if err != nil {
1001			log.Print(err)
1002		}
1003		return
1004	}
1005	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1006		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
1007	if err != nil {
1008		log.Printf("error saving honk: %s", err)
1009		return
1010	}
1011	honk.ID, _ = res.LastInsertId()
1012	for _, d := range honk.Donks {
1013		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1014		if err != nil {
1015			log.Printf("err saving donk: %s", err)
1016			return
1017		}
1018	}
1019
1020	go honkworldwide(user, &honk)
1021
1022	http.Redirect(w, r, "/", http.StatusSeeOther)
1023}
1024
1025func showhonkers(w http.ResponseWriter, r *http.Request) {
1026	userinfo := login.GetUserInfo(r)
1027	templinfo := getInfo(r)
1028	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1029	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1030	err := readviews.Execute(w, "honkers.html", templinfo)
1031	if err != nil {
1032		log.Print(err)
1033	}
1034}
1035
1036func showcombos(w http.ResponseWriter, r *http.Request) {
1037	userinfo := login.GetUserInfo(r)
1038	templinfo := getInfo(r)
1039	honkers := gethonkers(userinfo.UserID)
1040	var combos []string
1041	for _, h := range honkers {
1042		combos = append(combos, h.Combos...)
1043	}
1044	for i, c := range combos {
1045		if c == "-" {
1046			combos[i] = ""
1047		}
1048	}
1049	combos = oneofakind(combos)
1050	sort.Strings(combos)
1051	templinfo["Combos"] = combos
1052	err := readviews.Execute(w, "combos.html", templinfo)
1053	if err != nil {
1054		log.Print(err)
1055	}
1056}
1057
1058func savehonker(w http.ResponseWriter, r *http.Request) {
1059	u := login.GetUserInfo(r)
1060	name := r.FormValue("name")
1061	url := r.FormValue("url")
1062	peep := r.FormValue("peep")
1063	combos := r.FormValue("combos")
1064	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1065
1066	if honkerid > 0 {
1067		goodbye := r.FormValue("goodbye")
1068		if goodbye == "F" {
1069			db := opendatabase()
1070			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1071				honkerid, u.UserID)
1072			var xid string
1073			err := row.Scan(&xid)
1074			if err != nil {
1075				log.Printf("can't get honker xid: %s", err)
1076				return
1077			}
1078			log.Printf("unsubscribing from %s", xid)
1079			user, _ := butwhatabout(u.Username)
1080			go itakeitallback(user, xid)
1081			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1082			if err != nil {
1083				log.Printf("error updating honker: %s", err)
1084				return
1085			}
1086
1087			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1088			return
1089		}
1090		combos = " " + strings.TrimSpace(combos) + " "
1091		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1092		if err != nil {
1093			log.Printf("update honker err: %s", err)
1094			return
1095		}
1096		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1097	}
1098
1099	flavor := "presub"
1100	if peep == "peep" {
1101		flavor = "peep"
1102	}
1103	url = investigate(url)
1104	if url == "" {
1105		return
1106	}
1107	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1108	if err != nil {
1109		log.Print(err)
1110		return
1111	}
1112	if flavor == "presub" {
1113		user, _ := butwhatabout(u.Username)
1114		go subsub(user, url)
1115	}
1116	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1117}
1118
1119type Zonker struct {
1120	ID        int64
1121	Name      string
1122	Wherefore string
1123}
1124
1125func zonkzone(w http.ResponseWriter, r *http.Request) {
1126	db := opendatabase()
1127	userinfo := login.GetUserInfo(r)
1128	rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1129	if err != nil {
1130		log.Printf("err: %s", err)
1131		return
1132	}
1133	var zonkers []Zonker
1134	for rows.Next() {
1135		var z Zonker
1136		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1137		zonkers = append(zonkers, z)
1138	}
1139	sort.Slice(zonkers, func(i, j int) bool {
1140		w1 := zonkers[i].Wherefore
1141		w2 := zonkers[j].Wherefore
1142		if w1 == w2 {
1143			return zonkers[i].Name < zonkers[j].Name
1144		}
1145		if w1 == "zonvoy" {
1146			w1 = "zzzzzzz"
1147		}
1148		if w2 == "zonvoy" {
1149			w2 = "zzzzzzz"
1150		}
1151		return w1 < w2
1152	})
1153
1154	templinfo := getInfo(r)
1155	templinfo["Zonkers"] = zonkers
1156	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1157	err = readviews.Execute(w, "zonkers.html", templinfo)
1158	if err != nil {
1159		log.Print(err)
1160	}
1161}
1162
1163func zonkzonk(w http.ResponseWriter, r *http.Request) {
1164	userinfo := login.GetUserInfo(r)
1165	itsok := r.FormValue("itsok")
1166	if itsok == "iforgiveyou" {
1167		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1168		db := opendatabase()
1169		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1170			userinfo.UserID, zonkerid)
1171		bitethethumbs()
1172		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1173		return
1174	}
1175	wherefore := r.FormValue("wherefore")
1176	name := r.FormValue("name")
1177	if name == "" {
1178		return
1179	}
1180	switch wherefore {
1181	case "zonker":
1182	case "zurl":
1183	case "zonvoy":
1184	case "zword":
1185	default:
1186		return
1187	}
1188	db := opendatabase()
1189	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1190		userinfo.UserID, name, wherefore)
1191	if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1192		bitethethumbs()
1193	}
1194
1195	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1196}
1197
1198func accountpage(w http.ResponseWriter, r *http.Request) {
1199	u := login.GetUserInfo(r)
1200	user, _ := butwhatabout(u.Username)
1201	templinfo := getInfo(r)
1202	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1203	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1204	templinfo["WhatAbout"] = user.About
1205	err := readviews.Execute(w, "account.html", templinfo)
1206	if err != nil {
1207		log.Print(err)
1208	}
1209}
1210
1211func dochpass(w http.ResponseWriter, r *http.Request) {
1212	err := login.ChangePassword(w, r)
1213	if err != nil {
1214		log.Printf("error changing password: %s", err)
1215	}
1216	http.Redirect(w, r, "/account", http.StatusSeeOther)
1217}
1218
1219func fingerlicker(w http.ResponseWriter, r *http.Request) {
1220	orig := r.FormValue("resource")
1221
1222	log.Printf("finger lick: %s", orig)
1223
1224	if strings.HasPrefix(orig, "acct:") {
1225		orig = orig[5:]
1226	}
1227
1228	name := orig
1229	idx := strings.LastIndexByte(name, '/')
1230	if idx != -1 {
1231		name = name[idx+1:]
1232		if "https://"+serverName+"/u/"+name != orig {
1233			log.Printf("foreign request rejected")
1234			name = ""
1235		}
1236	} else {
1237		idx = strings.IndexByte(name, '@')
1238		if idx != -1 {
1239			name = name[:idx]
1240			if name+"@"+serverName != orig {
1241				log.Printf("foreign request rejected")
1242				name = ""
1243			}
1244		}
1245	}
1246	user, err := butwhatabout(name)
1247	if err != nil {
1248		http.NotFound(w, r)
1249		return
1250	}
1251
1252	j := junk.New()
1253	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1254	j["aliases"] = []string{user.URL}
1255	var links []map[string]interface{}
1256	l := junk.New()
1257	l["rel"] = "self"
1258	l["type"] = `application/activity+json`
1259	l["href"] = user.URL
1260	links = append(links, l)
1261	j["links"] = links
1262
1263	w.Header().Set("Cache-Control", "max-age=3600")
1264	w.Header().Set("Content-Type", "application/jrd+json")
1265	j.Write(w)
1266}
1267
1268func somedays() string {
1269	secs := 432000 + notrand.Int63n(432000)
1270	return fmt.Sprintf("%d", secs)
1271}
1272
1273func avatate(w http.ResponseWriter, r *http.Request) {
1274	n := r.FormValue("a")
1275	a := avatar(n)
1276	w.Header().Set("Cache-Control", "max-age="+somedays())
1277	w.Write(a)
1278}
1279
1280func servecss(w http.ResponseWriter, r *http.Request) {
1281	w.Header().Set("Cache-Control", "max-age=7776000")
1282	http.ServeFile(w, r, "views"+r.URL.Path)
1283}
1284func servehtml(w http.ResponseWriter, r *http.Request) {
1285	templinfo := getInfo(r)
1286	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1287	if err != nil {
1288		log.Print(err)
1289	}
1290}
1291func serveemu(w http.ResponseWriter, r *http.Request) {
1292	xid := mux.Vars(r)["xid"]
1293	w.Header().Set("Cache-Control", "max-age="+somedays())
1294	http.ServeFile(w, r, "emus/"+xid)
1295}
1296func servememe(w http.ResponseWriter, r *http.Request) {
1297	xid := mux.Vars(r)["xid"]
1298	w.Header().Set("Cache-Control", "max-age="+somedays())
1299	http.ServeFile(w, r, "memes/"+xid)
1300}
1301
1302func servefile(w http.ResponseWriter, r *http.Request) {
1303	xid := mux.Vars(r)["xid"]
1304	row := stmtFileData.QueryRow(xid)
1305	var media string
1306	var data []byte
1307	err := row.Scan(&media, &data)
1308	if err != nil {
1309		log.Printf("error loading file: %s", err)
1310		http.NotFound(w, r)
1311		return
1312	}
1313	w.Header().Set("Content-Type", media)
1314	w.Header().Set("X-Content-Type-Options", "nosniff")
1315	w.Header().Set("Cache-Control", "max-age="+somedays())
1316	w.Write(data)
1317}
1318
1319func serve() {
1320	db := opendatabase()
1321	login.Init(db)
1322
1323	listener, err := openListener()
1324	if err != nil {
1325		log.Fatal(err)
1326	}
1327	go redeliverator()
1328
1329	debug := false
1330	getconfig("debug", &debug)
1331	readviews = templates.Load(debug,
1332		"views/honkpage.html",
1333		"views/honkers.html",
1334		"views/zonkers.html",
1335		"views/combos.html",
1336		"views/honkform.html",
1337		"views/honk.html",
1338		"views/account.html",
1339		"views/about.html",
1340		"views/login.html",
1341		"views/xzone.html",
1342		"views/header.html",
1343	)
1344	if !debug {
1345		s := "views/style.css"
1346		savedstyleparams[s] = getstyleparam(s)
1347		s = "views/local.css"
1348		savedstyleparams[s] = getstyleparam(s)
1349	}
1350
1351	bitethethumbs()
1352
1353	mux := mux.NewRouter()
1354	mux.Use(login.Checker)
1355
1356	posters := mux.Methods("POST").Subrouter()
1357	getters := mux.Methods("GET").Subrouter()
1358
1359	getters.HandleFunc("/", homepage)
1360	getters.HandleFunc("/rss", showrss)
1361	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1362	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1363	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1364	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1365	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1366	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1367	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1368	getters.HandleFunc("/a", avatate)
1369	getters.HandleFunc("/t", showconvoy)
1370	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1371	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1372	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1373	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1374
1375	getters.HandleFunc("/style.css", servecss)
1376	getters.HandleFunc("/local.css", servecss)
1377	getters.HandleFunc("/about", servehtml)
1378	getters.HandleFunc("/login", servehtml)
1379	posters.HandleFunc("/dologin", login.LoginFunc)
1380	getters.HandleFunc("/logout", login.LogoutFunc)
1381
1382	loggedin := mux.NewRoute().Subrouter()
1383	loggedin.Use(login.Required)
1384	loggedin.HandleFunc("/account", accountpage)
1385	loggedin.HandleFunc("/chpass", dochpass)
1386	loggedin.HandleFunc("/atme", homepage)
1387	loggedin.HandleFunc("/zonkzone", zonkzone)
1388	loggedin.HandleFunc("/xzone", xzone)
1389	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1390	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1391	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1392	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1393	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1394	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1395	loggedin.HandleFunc("/honkers", showhonkers)
1396	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1397	loggedin.HandleFunc("/h", showhonker)
1398	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1399	loggedin.HandleFunc("/c", showcombos)
1400	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1401
1402	err = http.Serve(listener, mux)
1403	if err != nil {
1404		log.Fatal(err)
1405	}
1406}
1407
1408func cleanupdb(days int) {
1409	db := opendatabase()
1410	expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1411	doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3))", expdate)
1412	doordie(db, "delete from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3)", expdate)
1413	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1414}
1415
1416func reducedb(honker string) {
1417	db := opendatabase()
1418	expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1419	doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1420	doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1421	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1422}
1423
1424var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1425var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1426var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1427var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1428var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1429var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1430var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1431var stmtRecentHonkers, stmtGetXonker, stmtSaveXonker *sql.Stmt
1432
1433func preparetodie(db *sql.DB, s string) *sql.Stmt {
1434	stmt, err := db.Prepare(s)
1435	if err != nil {
1436		log.Fatalf("error %s: %s", err, s)
1437	}
1438	return stmt
1439}
1440
1441func prepareStatements(db *sql.DB) {
1442	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and (flavor = 'sub' or flavor = 'peep' or flavor = 'unsub') order by name")
1443	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1444	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1445	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1446	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1447	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1448
1449	selecthonks := "select honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, convoy, whofore from honks join users on honks.userid = users.userid "
1450	limit := " order by honkid desc limit 250"
1451	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1452	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1453	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1454	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1455	stmtHonksForUser = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and honker in (select xid from honkers where userid = ? and flavor = 'sub' and combos not like '% - %')"+butnotthose+limit)
1456	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1457	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1458	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and honker = ?"+butnotthose+limit)
1459	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1460	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1461
1462	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1463	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1464	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1465	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1466	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1467	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1468	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1469	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1470	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1471	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1472	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1473	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1474	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1475	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1476	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1477	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1478	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1479	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1480	stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? order by honkid desc limit 100")
1481}
1482
1483func ElaborateUnitTests() {
1484}
1485
1486func main() {
1487	var err error
1488	cmd := "run"
1489	if len(os.Args) > 1 {
1490		cmd = os.Args[1]
1491	}
1492	switch cmd {
1493	case "init":
1494		initdb()
1495	case "upgrade":
1496		upgradedb()
1497	}
1498	db := opendatabase()
1499	dbversion := 0
1500	getconfig("dbversion", &dbversion)
1501	if dbversion != myVersion {
1502		log.Fatal("incorrect database version. run upgrade.")
1503	}
1504	getconfig("servermsg", &serverMsg)
1505	getconfig("servername", &serverName)
1506	prepareStatements(db)
1507	switch cmd {
1508	case "adduser":
1509		adduser()
1510	case "cleanup":
1511		days := 30
1512		if len(os.Args) > 2 {
1513			days, err = strconv.Atoi(os.Args[2])
1514			if err != nil {
1515				log.Fatal(err)
1516			}
1517		}
1518		cleanupdb(days)
1519	case "reduce":
1520		if len(os.Args) < 3 {
1521			log.Fatal("need a honker name")
1522		}
1523		reducedb(os.Args[2])
1524	case "ping":
1525		if len(os.Args) < 4 {
1526			fmt.Printf("usage: honk ping from to\n")
1527			return
1528		}
1529		name := os.Args[2]
1530		targ := os.Args[3]
1531		user, err := butwhatabout(name)
1532		if err != nil {
1533			log.Printf("unknown user")
1534			return
1535		}
1536		ping(user, targ)
1537	case "peep":
1538		peeppeep()
1539	case "run":
1540		serve()
1541	case "test":
1542		ElaborateUnitTests()
1543	default:
1544		log.Fatal("unknown command")
1545	}
1546}