all repos — honk @ 19d2f9ff85fdfff0e462a4bf3e61a781dde9c92f

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