all repos — honk @ f1504e795c65fdaa724445e9ec793962975c224b

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	templinfo := getInfo(r)
 388	templinfo["XCSRF"] = login.GetCSRF("ximport", r)
 389	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
 390	if err != nil {
 391		log.Print(err)
 392	}
 393}
 394func outbox(w http.ResponseWriter, r *http.Request) {
 395	name := mux.Vars(r)["name"]
 396	user, err := butwhatabout(name)
 397	if err != nil {
 398		http.NotFound(w, r)
 399		return
 400	}
 401	honks := gethonksbyuser(name, false)
 402
 403	var jonks []map[string]interface{}
 404	for _, h := range honks {
 405		j, _ := jonkjonk(user, h)
 406		jonks = append(jonks, j)
 407	}
 408
 409	j := junk.New()
 410	j["@context"] = itiswhatitis
 411	j["id"] = user.URL + "/outbox"
 412	j["type"] = "OrderedCollection"
 413	j["totalItems"] = len(jonks)
 414	j["orderedItems"] = jonks
 415
 416	w.Header().Set("Cache-Control", "max-age=60")
 417	w.Header().Set("Content-Type", theonetruename)
 418	j.Write(w)
 419}
 420
 421func emptiness(w http.ResponseWriter, r *http.Request) {
 422	name := mux.Vars(r)["name"]
 423	user, err := butwhatabout(name)
 424	if err != nil {
 425		http.NotFound(w, r)
 426		return
 427	}
 428	colname := "/followers"
 429	if strings.HasSuffix(r.URL.Path, "/following") {
 430		colname = "/following"
 431	}
 432	j := junk.New()
 433	j["@context"] = itiswhatitis
 434	j["id"] = user.URL + colname
 435	j["type"] = "OrderedCollection"
 436	j["totalItems"] = 0
 437	j["orderedItems"] = []interface{}{}
 438
 439	w.Header().Set("Cache-Control", "max-age=60")
 440	w.Header().Set("Content-Type", theonetruename)
 441	j.Write(w)
 442}
 443
 444func showuser(w http.ResponseWriter, r *http.Request) {
 445	name := mux.Vars(r)["name"]
 446	user, err := butwhatabout(name)
 447	if err != nil {
 448		http.NotFound(w, r)
 449		return
 450	}
 451	if friendorfoe(r.Header.Get("Accept")) {
 452		j := asjonker(user)
 453		w.Header().Set("Cache-Control", "max-age=600")
 454		w.Header().Set("Content-Type", theonetruename)
 455		j.Write(w)
 456		return
 457	}
 458	u := login.GetUserInfo(r)
 459	honks := gethonksbyuser(name, u != nil && u.Username == name)
 460	honkpage(w, r, u, user, honks, "")
 461}
 462
 463func showhonker(w http.ResponseWriter, r *http.Request) {
 464	name := mux.Vars(r)["name"]
 465	u := login.GetUserInfo(r)
 466	honks := gethonksbyhonker(u.UserID, name)
 467	honkpage(w, r, u, nil, honks, "honks by honker: "+name)
 468}
 469
 470func showcombo(w http.ResponseWriter, r *http.Request) {
 471	name := mux.Vars(r)["name"]
 472	u := login.GetUserInfo(r)
 473	honks := gethonksbycombo(u.UserID, name)
 474	honkpage(w, r, u, nil, honks, "honks by combo: "+name)
 475}
 476func showconvoy(w http.ResponseWriter, r *http.Request) {
 477	c := r.FormValue("c")
 478	var userid int64 = -1
 479	u := login.GetUserInfo(r)
 480	if u != nil {
 481		userid = u.UserID
 482	}
 483	honks := gethonksbyconvoy(userid, c)
 484	honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
 485}
 486
 487func showhonk(w http.ResponseWriter, r *http.Request) {
 488	name := mux.Vars(r)["name"]
 489	user, err := butwhatabout(name)
 490	if err != nil {
 491		http.NotFound(w, r)
 492		return
 493	}
 494	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 495	h := getxonk(user.ID, xid)
 496	if h == nil || !h.Public {
 497		http.NotFound(w, r)
 498		return
 499	}
 500	if friendorfoe(r.Header.Get("Accept")) {
 501		donksforhonks([]*Honk{h})
 502		_, j := jonkjonk(user, h)
 503		j["@context"] = itiswhatitis
 504		w.Header().Set("Cache-Control", "max-age=3600")
 505		w.Header().Set("Content-Type", theonetruename)
 506		j.Write(w)
 507		return
 508	}
 509	honks := gethonksbyconvoy(-1, h.Convoy)
 510	u := login.GetUserInfo(r)
 511	honkpage(w, r, u, nil, honks, "one honk maybe more")
 512}
 513
 514func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 515	honks []*Honk, infomsg string) {
 516	reverbolate(honks)
 517	templinfo := getInfo(r)
 518	if u != nil {
 519		honks = osmosis(honks, u.UserID)
 520		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 521	}
 522	if u == nil {
 523		w.Header().Set("Cache-Control", "max-age=60")
 524	}
 525	if user != nil {
 526		filt := htfilter.New()
 527		templinfo["Name"] = user.Name
 528		whatabout := user.About
 529		whatabout = obfusbreak(user.About)
 530		templinfo["WhatAbout"], _ = filt.String(whatabout)
 531	}
 532	templinfo["Honks"] = honks
 533	templinfo["ServerMessage"] = infomsg
 534	err := readviews.Execute(w, "honkpage.html", templinfo)
 535	if err != nil {
 536		log.Print(err)
 537	}
 538}
 539
 540func saveuser(w http.ResponseWriter, r *http.Request) {
 541	whatabout := r.FormValue("whatabout")
 542	u := login.GetUserInfo(r)
 543	db := opendatabase()
 544	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 545	if err != nil {
 546		log.Printf("error bouting what: %s", err)
 547	}
 548
 549	http.Redirect(w, r, "/account", http.StatusSeeOther)
 550}
 551
 552func gethonkers(userid int64) []*Honker {
 553	rows, err := stmtHonkers.Query(userid)
 554	if err != nil {
 555		log.Printf("error querying honkers: %s", err)
 556		return nil
 557	}
 558	defer rows.Close()
 559	var honkers []*Honker
 560	for rows.Next() {
 561		var f Honker
 562		var combos string
 563		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 564		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 565		if err != nil {
 566			log.Printf("error scanning honker: %s", err)
 567			return nil
 568		}
 569		honkers = append(honkers, &f)
 570	}
 571	return honkers
 572}
 573
 574func getdubs(userid int64) []*Honker {
 575	rows, err := stmtDubbers.Query(userid)
 576	if err != nil {
 577		log.Printf("error querying dubs: %s", err)
 578		return nil
 579	}
 580	defer rows.Close()
 581	var honkers []*Honker
 582	for rows.Next() {
 583		var f Honker
 584		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 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 allusers() []login.UserInfo {
 595	var users []login.UserInfo
 596	rows, _ := opendatabase().Query("select userid, username from users")
 597	defer rows.Close()
 598	for rows.Next() {
 599		var u login.UserInfo
 600		rows.Scan(&u.UserID, &u.Username)
 601		users = append(users, u)
 602	}
 603	return users
 604}
 605
 606func getxonk(userid int64, xid string) *Honk {
 607	h := new(Honk)
 608	var dt, aud string
 609	row := stmtOneXonk.QueryRow(userid, xid)
 610	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 611		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 612	if err != nil {
 613		if err != sql.ErrNoRows {
 614			log.Printf("error scanning xonk: %s", err)
 615		}
 616		return nil
 617	}
 618	h.Date, _ = time.Parse(dbtimeformat, dt)
 619	h.Audience = strings.Split(aud, " ")
 620	h.Public = !keepitquiet(h.Audience)
 621	return h
 622}
 623
 624func getpublichonks() []*Honk {
 625	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 626	rows, err := stmtPublicHonks.Query(dt)
 627	return getsomehonks(rows, err)
 628}
 629func gethonksbyuser(name string, includeprivate bool) []*Honk {
 630	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 631	whofore := 2
 632	if includeprivate {
 633		whofore = 3
 634	}
 635	rows, err := stmtUserHonks.Query(whofore, name, dt)
 636	return getsomehonks(rows, err)
 637}
 638func gethonksforuser(userid int64) []*Honk {
 639	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 640	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 641	return getsomehonks(rows, err)
 642}
 643func gethonksforme(userid int64) []*Honk {
 644	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 645	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 646	return getsomehonks(rows, err)
 647}
 648func gethonksbyhonker(userid int64, honker string) []*Honk {
 649	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 650	return getsomehonks(rows, err)
 651}
 652func gethonksbycombo(userid int64, combo string) []*Honk {
 653	combo = "% " + combo + " %"
 654	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 655	return getsomehonks(rows, err)
 656}
 657func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 658	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 659	honks := getsomehonks(rows, err)
 660	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 661		honks[i], honks[j] = honks[j], honks[i]
 662	}
 663	return honks
 664}
 665
 666func getsomehonks(rows *sql.Rows, err error) []*Honk {
 667	if err != nil {
 668		log.Printf("error querying honks: %s", err)
 669		return nil
 670	}
 671	defer rows.Close()
 672	var honks []*Honk
 673	for rows.Next() {
 674		var h Honk
 675		var dt, aud string
 676		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 677			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 678		if err != nil {
 679			log.Printf("error scanning honks: %s", err)
 680			return nil
 681		}
 682		h.Date, _ = time.Parse(dbtimeformat, dt)
 683		h.Audience = strings.Split(aud, " ")
 684		h.Public = !keepitquiet(h.Audience)
 685		honks = append(honks, &h)
 686	}
 687	rows.Close()
 688	donksforhonks(honks)
 689	return honks
 690}
 691
 692func donksforhonks(honks []*Honk) {
 693	db := opendatabase()
 694	var ids []string
 695	hmap := make(map[int64]*Honk)
 696	for _, h := range honks {
 697		ids = append(ids, fmt.Sprintf("%d", h.ID))
 698		hmap[h.ID] = h
 699	}
 700	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, ","))
 701	rows, err := db.Query(q)
 702	if err != nil {
 703		log.Printf("error querying donks: %s", err)
 704		return
 705	}
 706	defer rows.Close()
 707	for rows.Next() {
 708		var hid int64
 709		var d Donk
 710		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 711		if err != nil {
 712			log.Printf("error scanning donk: %s", err)
 713			continue
 714		}
 715		h := hmap[hid]
 716		h.Donks = append(h.Donks, &d)
 717	}
 718}
 719
 720func savebonk(w http.ResponseWriter, r *http.Request) {
 721	xid := r.FormValue("xid")
 722	userinfo := login.GetUserInfo(r)
 723	user, _ := butwhatabout(userinfo.Username)
 724
 725	log.Printf("bonking %s", xid)
 726
 727	xonk := getxonk(userinfo.UserID, xid)
 728	if xonk == nil {
 729		return
 730	}
 731	if !xonk.Public {
 732		return
 733	}
 734	donksforhonks([]*Honk{xonk})
 735
 736	dt := time.Now().UTC()
 737	bonk := Honk{
 738		UserID:   userinfo.UserID,
 739		Username: userinfo.Username,
 740		What:     "bonk",
 741		Honker:   user.URL,
 742		XID:      xonk.XID,
 743		Date:     dt,
 744		Donks:    xonk.Donks,
 745		Audience: []string{thewholeworld},
 746		Public:   true,
 747	}
 748
 749	oonker := xonk.Oonker
 750	if oonker == "" {
 751		oonker = xonk.Honker
 752	}
 753	aud := strings.Join(bonk.Audience, " ")
 754	whofore := 2
 755	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 756		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 757		xonk.Precis, oonker)
 758	if err != nil {
 759		log.Printf("error saving bonk: %s", err)
 760		return
 761	}
 762	bonk.ID, _ = res.LastInsertId()
 763	for _, d := range bonk.Donks {
 764		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 765		if err != nil {
 766			log.Printf("err saving donk: %s", err)
 767			return
 768		}
 769	}
 770
 771	go honkworldwide(user, &bonk)
 772}
 773
 774func zonkit(w http.ResponseWriter, r *http.Request) {
 775	wherefore := r.FormValue("wherefore")
 776	var what string
 777	switch wherefore {
 778	case "this honk":
 779		what = r.FormValue("honk")
 780		wherefore = "zonk"
 781	case "this honker":
 782		what = r.FormValue("honker")
 783		wherefore = "zonker"
 784	case "this convoy":
 785		what = r.FormValue("convoy")
 786		wherefore = "zonvoy"
 787	}
 788	if what == "" {
 789		return
 790	}
 791
 792	log.Printf("zonking %s %s", wherefore, what)
 793	userinfo := login.GetUserInfo(r)
 794	if wherefore == "zonk" {
 795		xonk := getxonk(userinfo.UserID, what)
 796		if xonk != nil {
 797			stmtZonkDonks.Exec(xonk.ID)
 798			stmtZonkIt.Exec(userinfo.UserID, what)
 799			if xonk.Whofore == 2 || xonk.Whofore == 3 {
 800				zonk := Honk{
 801					What:     "zonk",
 802					XID:      xonk.XID,
 803					Date:     time.Now().UTC(),
 804					Audience: oneofakind(xonk.Audience),
 805				}
 806
 807				user, _ := butwhatabout(userinfo.Username)
 808				log.Printf("announcing deleted honk: %s", what)
 809				go honkworldwide(user, &zonk)
 810			}
 811		}
 812	} else {
 813		_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 814		if err != nil {
 815			log.Printf("error saving zonker: %s", err)
 816			return
 817		}
 818	}
 819}
 820
 821func savehonk(w http.ResponseWriter, r *http.Request) {
 822	rid := r.FormValue("rid")
 823	noise := r.FormValue("noise")
 824
 825	userinfo := login.GetUserInfo(r)
 826	user, _ := butwhatabout(userinfo.Username)
 827
 828	dt := time.Now().UTC()
 829	xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
 830	what := "honk"
 831	if rid != "" {
 832		what = "tonk"
 833	}
 834	honk := Honk{
 835		UserID:   userinfo.UserID,
 836		Username: userinfo.Username,
 837		What:     "honk",
 838		Honker:   user.URL,
 839		XID:      xid,
 840		Date:     dt,
 841	}
 842	if strings.HasPrefix(noise, "DZ:") {
 843		idx := strings.Index(noise, "\n")
 844		if idx == -1 {
 845			honk.Precis = noise
 846			noise = ""
 847		} else {
 848			honk.Precis = noise[:idx]
 849			noise = noise[idx+1:]
 850		}
 851	}
 852	noise = hooterize(noise)
 853	noise = strings.TrimSpace(noise)
 854	honk.Precis = strings.TrimSpace(honk.Precis)
 855
 856	var convoy string
 857	if rid != "" {
 858		xonk := getxonk(userinfo.UserID, rid)
 859		if xonk != nil {
 860			if xonk.Public {
 861				honk.Audience = append(honk.Audience, xonk.Audience...)
 862			}
 863			convoy = xonk.Convoy
 864		} else {
 865			xonkaud, c := whosthere(rid)
 866			honk.Audience = append(honk.Audience, xonkaud...)
 867			convoy = c
 868		}
 869		for i, a := range honk.Audience {
 870			if a == thewholeworld {
 871				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
 872				break
 873			}
 874		}
 875		honk.RID = rid
 876	} else {
 877		honk.Audience = []string{thewholeworld}
 878	}
 879	if noise != "" && noise[0] == '@' {
 880		honk.Audience = append(grapevine(noise), honk.Audience...)
 881	} else {
 882		honk.Audience = append(honk.Audience, grapevine(noise)...)
 883	}
 884	if convoy == "" {
 885		convoy = "data:,electrichonkytonk-" + xfiltrate()
 886	}
 887	butnottooloud(honk.Audience)
 888	honk.Audience = oneofakind(honk.Audience)
 889	if len(honk.Audience) == 0 {
 890		log.Printf("honk to nowhere")
 891		return
 892	}
 893	honk.Public = !keepitquiet(honk.Audience)
 894	noise = obfusbreak(noise)
 895	honk.Noise = noise
 896	honk.Convoy = convoy
 897
 898	file, filehdr, err := r.FormFile("donk")
 899	if err == nil {
 900		var buf bytes.Buffer
 901		io.Copy(&buf, file)
 902		file.Close()
 903		data := buf.Bytes()
 904		xid := xfiltrate()
 905		var media, name string
 906		img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
 907		if err == nil {
 908			data = img.Data
 909			format := img.Format
 910			media = "image/" + format
 911			if format == "jpeg" {
 912				format = "jpg"
 913			}
 914			name = xid + "." + format
 915			xid = name
 916		} else {
 917			maxsize := 100000
 918			if len(data) > maxsize {
 919				log.Printf("bad image: %s too much text: %d", err, len(data))
 920				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 921				return
 922			}
 923			for i := 0; i < len(data); i++ {
 924				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 925					log.Printf("bad image: %s not text: %d", err, data[i])
 926					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 927					return
 928				}
 929			}
 930			media = "text/plain"
 931			name = filehdr.Filename
 932			if name == "" {
 933				name = xid + ".txt"
 934			}
 935			xid += ".txt"
 936		}
 937		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 938		res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
 939		if err != nil {
 940			log.Printf("unable to save image: %s", err)
 941			return
 942		}
 943		var d Donk
 944		d.FileID, _ = res.LastInsertId()
 945		d.XID = name
 946		d.Name = name
 947		d.Media = media
 948		d.URL = url
 949		d.Local = true
 950		honk.Donks = append(honk.Donks, &d)
 951	}
 952	herd := herdofemus(honk.Noise)
 953	for _, e := range herd {
 954		donk := savedonk(e.ID, e.Name, "image/png", true)
 955		if donk != nil {
 956			donk.Name = e.Name
 957			honk.Donks = append(honk.Donks, donk)
 958		}
 959	}
 960	honk.Donks = append(honk.Donks, memetics(honk.Noise)...)
 961
 962	aud := strings.Join(honk.Audience, " ")
 963	whofore := 2
 964	if !honk.Public {
 965		whofore = 3
 966	}
 967	if r.FormValue("preview") == "preview" {
 968		honks := []*Honk{ &honk }
 969		reverbolate(honks)
 970		templinfo := getInfo(r)
 971		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 972		templinfo["Honks"] = honks
 973		templinfo["Noise"] = r.FormValue("noise")
 974		templinfo["ServerMessage"] = "honk preview"
 975		err := readviews.Execute(w, "honkpage.html", templinfo)
 976		if err != nil {
 977			log.Print(err)
 978		}
 979		return
 980	}
 981	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
 982		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
 983	if err != nil {
 984		log.Printf("error saving honk: %s", err)
 985		return
 986	}
 987	honk.ID, _ = res.LastInsertId()
 988	for _, d := range honk.Donks {
 989		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 990		if err != nil {
 991			log.Printf("err saving donk: %s", err)
 992			return
 993		}
 994	}
 995
 996	go honkworldwide(user, &honk)
 997
 998	http.Redirect(w, r, "/", http.StatusSeeOther)
 999}
1000
1001func showhonkers(w http.ResponseWriter, r *http.Request) {
1002	userinfo := login.GetUserInfo(r)
1003	templinfo := getInfo(r)
1004	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1005	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1006	err := readviews.Execute(w, "honkers.html", templinfo)
1007	if err != nil {
1008		log.Print(err)
1009	}
1010}
1011
1012func showcombos(w http.ResponseWriter, r *http.Request) {
1013	userinfo := login.GetUserInfo(r)
1014	templinfo := getInfo(r)
1015	honkers := gethonkers(userinfo.UserID)
1016	var combos []string
1017	for _, h := range honkers {
1018		combos = append(combos, h.Combos...)
1019	}
1020	for i, c := range combos {
1021		if c == "-" {
1022			combos[i] = ""
1023		}
1024	}
1025	combos = oneofakind(combos)
1026	sort.Strings(combos)
1027	templinfo["Combos"] = combos
1028	err := readviews.Execute(w, "combos.html", templinfo)
1029	if err != nil {
1030		log.Print(err)
1031	}
1032}
1033
1034func savehonker(w http.ResponseWriter, r *http.Request) {
1035	u := login.GetUserInfo(r)
1036	name := r.FormValue("name")
1037	url := r.FormValue("url")
1038	peep := r.FormValue("peep")
1039	combos := r.FormValue("combos")
1040	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1041
1042	if honkerid > 0 {
1043		goodbye := r.FormValue("goodbye")
1044		if goodbye == "F" {
1045			db := opendatabase()
1046			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1047				honkerid, u.UserID)
1048			var xid string
1049			err := row.Scan(&xid)
1050			if err != nil {
1051				log.Printf("can't get honker xid: %s", err)
1052				return
1053			}
1054			log.Printf("unsubscribing from %s", xid)
1055			user, _ := butwhatabout(u.Username)
1056			go itakeitallback(user, xid)
1057			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1058			if err != nil {
1059				log.Printf("error updating honker: %s", err)
1060				return
1061			}
1062
1063			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1064			return
1065		}
1066		combos = " " + strings.TrimSpace(combos) + " "
1067		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1068		if err != nil {
1069			log.Printf("update honker err: %s", err)
1070			return
1071		}
1072		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1073	}
1074
1075	flavor := "presub"
1076	if peep == "peep" {
1077		flavor = "peep"
1078	}
1079	url = investigate(url)
1080	if url == "" {
1081		return
1082	}
1083	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1084	if err != nil {
1085		log.Print(err)
1086		return
1087	}
1088	if flavor == "presub" {
1089		user, _ := butwhatabout(u.Username)
1090		go subsub(user, url)
1091	}
1092	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1093}
1094
1095type Zonker struct {
1096	ID        int64
1097	Name      string
1098	Wherefore string
1099}
1100
1101func zonkzone(w http.ResponseWriter, r *http.Request) {
1102	db := opendatabase()
1103	userinfo := login.GetUserInfo(r)
1104	rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1105	if err != nil {
1106		log.Printf("err: %s", err)
1107		return
1108	}
1109	var zonkers []Zonker
1110	for rows.Next() {
1111		var z Zonker
1112		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1113		zonkers = append(zonkers, z)
1114	}
1115	templinfo := getInfo(r)
1116	templinfo["Zonkers"] = zonkers
1117	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1118	err = readviews.Execute(w, "zonkers.html", templinfo)
1119	if err != nil {
1120		log.Print(err)
1121	}
1122}
1123
1124func zonkzonk(w http.ResponseWriter, r *http.Request) {
1125	userinfo := login.GetUserInfo(r)
1126	itsok := r.FormValue("itsok")
1127	if itsok == "iforgiveyou" {
1128		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1129		db := opendatabase()
1130		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1131			userinfo.UserID, zonkerid)
1132		bitethethumbs()
1133		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1134		return
1135	}
1136	wherefore := r.FormValue("wherefore")
1137	name := r.FormValue("name")
1138	if name == "" {
1139		return
1140	}
1141	switch wherefore {
1142	case "zonker":
1143	case "zurl":
1144	case "zonvoy":
1145	case "zword":
1146	default:
1147		return
1148	}
1149	db := opendatabase()
1150	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1151		userinfo.UserID, name, wherefore)
1152	if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1153		bitethethumbs()
1154	}
1155
1156	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1157}
1158
1159func accountpage(w http.ResponseWriter, r *http.Request) {
1160	u := login.GetUserInfo(r)
1161	user, _ := butwhatabout(u.Username)
1162	templinfo := getInfo(r)
1163	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1164	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1165	templinfo["WhatAbout"] = user.About
1166	err := readviews.Execute(w, "account.html", templinfo)
1167	if err != nil {
1168		log.Print(err)
1169	}
1170}
1171
1172func dochpass(w http.ResponseWriter, r *http.Request) {
1173	err := login.ChangePassword(w, r)
1174	if err != nil {
1175		log.Printf("error changing password: %s", err)
1176	}
1177	http.Redirect(w, r, "/account", http.StatusSeeOther)
1178}
1179
1180func fingerlicker(w http.ResponseWriter, r *http.Request) {
1181	orig := r.FormValue("resource")
1182
1183	log.Printf("finger lick: %s", orig)
1184
1185	if strings.HasPrefix(orig, "acct:") {
1186		orig = orig[5:]
1187	}
1188
1189	name := orig
1190	idx := strings.LastIndexByte(name, '/')
1191	if idx != -1 {
1192		name = name[idx+1:]
1193		if "https://"+serverName+"/u/"+name != orig {
1194			log.Printf("foreign request rejected")
1195			name = ""
1196		}
1197	} else {
1198		idx = strings.IndexByte(name, '@')
1199		if idx != -1 {
1200			name = name[:idx]
1201			if name+"@"+serverName != orig {
1202				log.Printf("foreign request rejected")
1203				name = ""
1204			}
1205		}
1206	}
1207	user, err := butwhatabout(name)
1208	if err != nil {
1209		http.NotFound(w, r)
1210		return
1211	}
1212
1213	j := junk.New()
1214	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1215	j["aliases"] = []string{user.URL}
1216	var links []map[string]interface{}
1217	l := junk.New()
1218	l["rel"] = "self"
1219	l["type"] = `application/activity+json`
1220	l["href"] = user.URL
1221	links = append(links, l)
1222	j["links"] = links
1223
1224	w.Header().Set("Cache-Control", "max-age=3600")
1225	w.Header().Set("Content-Type", "application/jrd+json")
1226	j.Write(w)
1227}
1228
1229func somedays() string {
1230	secs := 432000 + notrand.Int63n(432000)
1231	return fmt.Sprintf("%d", secs)
1232}
1233
1234func avatate(w http.ResponseWriter, r *http.Request) {
1235	n := r.FormValue("a")
1236	a := avatar(n)
1237	w.Header().Set("Cache-Control", "max-age="+somedays())
1238	w.Write(a)
1239}
1240
1241func servecss(w http.ResponseWriter, r *http.Request) {
1242	w.Header().Set("Cache-Control", "max-age=7776000")
1243	http.ServeFile(w, r, "views"+r.URL.Path)
1244}
1245func servehtml(w http.ResponseWriter, r *http.Request) {
1246	templinfo := getInfo(r)
1247	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1248	if err != nil {
1249		log.Print(err)
1250	}
1251}
1252func serveemu(w http.ResponseWriter, r *http.Request) {
1253	xid := mux.Vars(r)["xid"]
1254	w.Header().Set("Cache-Control", "max-age="+somedays())
1255	http.ServeFile(w, r, "emus/"+xid)
1256}
1257func servememe(w http.ResponseWriter, r *http.Request) {
1258	xid := mux.Vars(r)["xid"]
1259	w.Header().Set("Cache-Control", "max-age="+somedays())
1260	http.ServeFile(w, r, "memes/"+xid)
1261}
1262
1263func servefile(w http.ResponseWriter, r *http.Request) {
1264	xid := mux.Vars(r)["xid"]
1265	row := stmtFileData.QueryRow(xid)
1266	var media string
1267	var data []byte
1268	err := row.Scan(&media, &data)
1269	if err != nil {
1270		log.Printf("error loading file: %s", err)
1271		http.NotFound(w, r)
1272		return
1273	}
1274	w.Header().Set("Content-Type", media)
1275	w.Header().Set("X-Content-Type-Options", "nosniff")
1276	w.Header().Set("Cache-Control", "max-age="+somedays())
1277	w.Write(data)
1278}
1279
1280func serve() {
1281	db := opendatabase()
1282	login.Init(db)
1283
1284	listener, err := openListener()
1285	if err != nil {
1286		log.Fatal(err)
1287	}
1288	go redeliverator()
1289
1290	debug := false
1291	getconfig("debug", &debug)
1292	readviews = templates.Load(debug,
1293		"views/honkpage.html",
1294		"views/honkers.html",
1295		"views/zonkers.html",
1296		"views/combos.html",
1297		"views/honkform.html",
1298		"views/honk.html",
1299		"views/account.html",
1300		"views/about.html",
1301		"views/login.html",
1302		"views/xzone.html",
1303		"views/header.html",
1304	)
1305	if !debug {
1306		s := "views/style.css"
1307		savedstyleparams[s] = getstyleparam(s)
1308		s = "views/local.css"
1309		savedstyleparams[s] = getstyleparam(s)
1310	}
1311
1312	bitethethumbs()
1313
1314	mux := mux.NewRouter()
1315	mux.Use(login.Checker)
1316
1317	posters := mux.Methods("POST").Subrouter()
1318	getters := mux.Methods("GET").Subrouter()
1319
1320	getters.HandleFunc("/", homepage)
1321	getters.HandleFunc("/rss", showrss)
1322	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1323	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1324	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1325	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1326	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1327	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1328	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1329	getters.HandleFunc("/a", avatate)
1330	getters.HandleFunc("/t", showconvoy)
1331	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1332	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1333	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1334	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1335
1336	getters.HandleFunc("/style.css", servecss)
1337	getters.HandleFunc("/local.css", servecss)
1338	getters.HandleFunc("/about", servehtml)
1339	getters.HandleFunc("/login", servehtml)
1340	posters.HandleFunc("/dologin", login.LoginFunc)
1341	getters.HandleFunc("/logout", login.LogoutFunc)
1342
1343	loggedin := mux.NewRoute().Subrouter()
1344	loggedin.Use(login.Required)
1345	loggedin.HandleFunc("/account", accountpage)
1346	loggedin.HandleFunc("/chpass", dochpass)
1347	loggedin.HandleFunc("/atme", homepage)
1348	loggedin.HandleFunc("/zonkzone", zonkzone)
1349	loggedin.HandleFunc("/xzone", xzone)
1350	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1351	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1352	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1353	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1354	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1355	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1356	loggedin.HandleFunc("/honkers", showhonkers)
1357	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1358	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1359	loggedin.HandleFunc("/c", showcombos)
1360	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1361
1362	err = http.Serve(listener, mux)
1363	if err != nil {
1364		log.Fatal(err)
1365	}
1366}
1367
1368func cleanupdb(days int) {
1369	db := opendatabase()
1370	expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1371	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)
1372	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)
1373	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1374}
1375
1376func reducedb(honker string) {
1377	db := opendatabase()
1378	expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1379	doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1380	doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1381	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1382}
1383
1384var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1385var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1386var stmtHonksForUser, stmtHonksForMe, stmtSaveDub *sql.Stmt
1387var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1388var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1389var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1390var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1391var stmtGetXonker, stmtSaveXonker *sql.Stmt
1392
1393func preparetodie(db *sql.DB, s string) *sql.Stmt {
1394	stmt, err := db.Prepare(s)
1395	if err != nil {
1396		log.Fatalf("error %s: %s", err, s)
1397	}
1398	return stmt
1399}
1400
1401func prepareStatements(db *sql.DB) {
1402	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")
1403	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1404	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1405	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1406	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1407	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1408
1409	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 "
1410	limit := " order by honkid desc limit 250"
1411	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1412	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1413	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1414	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1415	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)
1416	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1417	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1418	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1419	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1420
1421	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1422	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1423	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1424	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1425	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1426	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1427	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1428	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1429	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1430	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1431	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1432	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1433	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1434	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1435	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1436	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1437	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1438	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1439}
1440
1441func ElaborateUnitTests() {
1442}
1443
1444func main() {
1445	var err error
1446	cmd := "run"
1447	if len(os.Args) > 1 {
1448		cmd = os.Args[1]
1449	}
1450	switch cmd {
1451	case "init":
1452		initdb()
1453	case "upgrade":
1454		upgradedb()
1455	}
1456	db := opendatabase()
1457	dbversion := 0
1458	getconfig("dbversion", &dbversion)
1459	if dbversion != myVersion {
1460		log.Fatal("incorrect database version. run upgrade.")
1461	}
1462	getconfig("servermsg", &serverMsg)
1463	getconfig("servername", &serverName)
1464	prepareStatements(db)
1465	switch cmd {
1466	case "adduser":
1467		adduser()
1468	case "cleanup":
1469		days := 30
1470		if len(os.Args) > 2 {
1471			days, err = strconv.Atoi(os.Args[2])
1472			if err != nil {
1473				log.Fatal(err)
1474			}
1475		}
1476		cleanupdb(days)
1477	case "reduce":
1478		if len(os.Args) < 3 {
1479			log.Fatal("need a honker name")
1480		}
1481		reducedb(os.Args[2])
1482	case "ping":
1483		if len(os.Args) < 4 {
1484			fmt.Printf("usage: honk ping from to\n")
1485			return
1486		}
1487		name := os.Args[2]
1488		targ := os.Args[3]
1489		user, err := butwhatabout(name)
1490		if err != nil {
1491			log.Printf("unknown user")
1492			return
1493		}
1494		ping(user, targ)
1495	case "peep":
1496		peeppeep()
1497	case "run":
1498		serve()
1499	case "test":
1500		ElaborateUnitTests()
1501	default:
1502		log.Fatal("unknown command")
1503	}
1504}