all repos — honk @ e44896aaa4dbfea7659dffb88ae3748c40f4e10f

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="%sd/%s">Attachment: %s</a>`,
 187				base, d.XID, 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	}
 718
 719	oonker := xonk.Oonker
 720	if oonker == "" {
 721		oonker = xonk.Honker
 722	}
 723	aud := strings.Join(bonk.Audience, " ")
 724	whofore := 2
 725	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 726		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 727		xonk.Precis, oonker)
 728	if err != nil {
 729		log.Printf("error saving bonk: %s", err)
 730		return
 731	}
 732	bonk.ID, _ = res.LastInsertId()
 733	for _, d := range bonk.Donks {
 734		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 735		if err != nil {
 736			log.Printf("err saving donk: %s", err)
 737			return
 738		}
 739	}
 740
 741	go honkworldwide(user, &bonk)
 742}
 743
 744func zonkit(w http.ResponseWriter, r *http.Request) {
 745	wherefore := r.FormValue("wherefore")
 746	var what string
 747	switch wherefore {
 748	case "this honk":
 749		what = r.FormValue("honk")
 750		wherefore = "zonk"
 751	case "this honker":
 752		what = r.FormValue("honker")
 753		wherefore = "zonker"
 754	case "this convoy":
 755		what = r.FormValue("convoy")
 756		wherefore = "zonvoy"
 757	}
 758	if what == "" {
 759		return
 760	}
 761
 762	log.Printf("zonking %s %s", wherefore, what)
 763	userinfo := login.GetUserInfo(r)
 764	if wherefore == "zonk" {
 765		xonk := getxonk(userinfo.UserID, what)
 766		if xonk != nil {
 767			stmtZonkDonks.Exec(xonk.ID)
 768			stmtZonkIt.Exec(userinfo.UserID, what)
 769			if xonk.Whofore == 2 || xonk.Whofore == 3 {
 770				zonk := Honk{
 771					What:     "zonk",
 772					XID:      xonk.XID,
 773					Date:     time.Now().UTC(),
 774					Audience: oneofakind(xonk.Audience),
 775				}
 776
 777				user, _ := butwhatabout(userinfo.Username)
 778				log.Printf("announcing deleted honk: %s", what)
 779				go honkworldwide(user, &zonk)
 780			}
 781		}
 782	} else {
 783		_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 784		if err != nil {
 785			log.Printf("error saving zonker: %s", err)
 786			return
 787		}
 788	}
 789}
 790
 791func savehonk(w http.ResponseWriter, r *http.Request) {
 792	rid := r.FormValue("rid")
 793	noise := r.FormValue("noise")
 794
 795	userinfo := login.GetUserInfo(r)
 796	user, _ := butwhatabout(userinfo.Username)
 797
 798	dt := time.Now().UTC()
 799	xid := fmt.Sprintf("https://%s/u/%s/h/%s", serverName, userinfo.Username, xfiltrate())
 800	what := "honk"
 801	if rid != "" {
 802		what = "tonk"
 803	}
 804	honk := Honk{
 805		UserID:   userinfo.UserID,
 806		Username: userinfo.Username,
 807		What:     "honk",
 808		Honker:   user.URL,
 809		XID:      xid,
 810		Date:     dt,
 811	}
 812	if strings.HasPrefix(noise, "DZ:") {
 813		idx := strings.Index(noise, "\n")
 814		if idx == -1 {
 815			honk.Precis = noise
 816			noise = ""
 817		} else {
 818			honk.Precis = noise[:idx]
 819			noise = noise[idx+1:]
 820		}
 821	}
 822	noise = strings.TrimSpace(noise)
 823	honk.Precis = strings.TrimSpace(honk.Precis)
 824
 825	var convoy string
 826	if rid != "" {
 827		xonk := getxonk(userinfo.UserID, rid)
 828		if xonk != nil {
 829			honk.Audience = append(honk.Audience, xonk.Audience...)
 830			convoy = xonk.Convoy
 831		} else {
 832			xonkaud, c := whosthere(rid)
 833			honk.Audience = append(honk.Audience, xonkaud...)
 834			convoy = c
 835		}
 836		for i, a := range honk.Audience {
 837			if a == thewholeworld {
 838				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
 839				break
 840			}
 841		}
 842		honk.RID = rid
 843	} else {
 844		honk.Audience = []string{thewholeworld}
 845	}
 846	if noise != "" && noise[0] == '@' {
 847		honk.Audience = append(grapevine(noise), honk.Audience...)
 848	} else {
 849		honk.Audience = append(honk.Audience, grapevine(noise)...)
 850	}
 851	if convoy == "" {
 852		convoy = "data:,electrichonkytonk-" + xfiltrate()
 853	}
 854	butnottooloud(honk.Audience)
 855	honk.Audience = oneofakind(honk.Audience)
 856	honk.Public = !keepitquiet(honk.Audience)
 857	noise = obfusbreak(noise)
 858	honk.Noise = noise
 859	honk.Convoy = convoy
 860
 861	file, filehdr, err := r.FormFile("donk")
 862	if err == nil {
 863		var buf bytes.Buffer
 864		io.Copy(&buf, file)
 865		file.Close()
 866		data := buf.Bytes()
 867		xid := xfiltrate()
 868		var media, name string
 869		img, err := image.Vacuum(&buf)
 870		if err == nil {
 871			data = img.Data
 872			format := img.Format
 873			media = "image/" + format
 874			if format == "jpeg" {
 875				format = "jpg"
 876			}
 877			name = xid + "." + format
 878			xid = name
 879		} else {
 880			maxsize := 100000
 881			if len(data) > maxsize {
 882				log.Printf("bad image: %s too much text: %d", err, len(data))
 883				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 884				return
 885			}
 886			for i := 0; i < len(data); i++ {
 887				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 888					log.Printf("bad image: %s not text: %d", err, data[i])
 889					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 890					return
 891				}
 892			}
 893			media = "text/plain"
 894			name = filehdr.Filename
 895			if name == "" {
 896				name = xid + ".txt"
 897			}
 898			xid += ".txt"
 899		}
 900		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 901		res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
 902		if err != nil {
 903			log.Printf("unable to save image: %s", err)
 904			return
 905		}
 906		var d Donk
 907		d.FileID, _ = res.LastInsertId()
 908		d.XID = name
 909		d.Name = name
 910		d.Media = media
 911		d.URL = url
 912		honk.Donks = append(honk.Donks, &d)
 913	}
 914	herd := herdofemus(honk.Noise)
 915	for _, e := range herd {
 916		donk := savedonk(e.ID, e.Name, "image/png", true)
 917		if donk != nil {
 918			donk.Name = e.Name
 919			honk.Donks = append(honk.Donks, donk)
 920		}
 921	}
 922
 923	aud := strings.Join(honk.Audience, " ")
 924	whofore := 2
 925	if !honk.Public {
 926		whofore = 3
 927	}
 928	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
 929		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
 930	if err != nil {
 931		log.Printf("error saving honk: %s", err)
 932		return
 933	}
 934	honk.ID, _ = res.LastInsertId()
 935	for _, d := range honk.Donks {
 936		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 937		if err != nil {
 938			log.Printf("err saving donk: %s", err)
 939			return
 940		}
 941	}
 942
 943	go honkworldwide(user, &honk)
 944
 945	http.Redirect(w, r, "/", http.StatusSeeOther)
 946}
 947
 948func showhonkers(w http.ResponseWriter, r *http.Request) {
 949	userinfo := login.GetUserInfo(r)
 950	templinfo := getInfo(r)
 951	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 952	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
 953	err := readviews.Execute(w, "honkers.html", templinfo)
 954	if err != nil {
 955		log.Print(err)
 956	}
 957}
 958
 959func showcombos(w http.ResponseWriter, r *http.Request) {
 960	userinfo := login.GetUserInfo(r)
 961	templinfo := getInfo(r)
 962	honkers := gethonkers(userinfo.UserID)
 963	var combos []string
 964	for _, h := range honkers {
 965		combos = append(combos, h.Combos...)
 966	}
 967	for i, c := range combos {
 968		if c == "-" {
 969			combos[i] = ""
 970		}
 971	}
 972	combos = oneofakind(combos)
 973	sort.Strings(combos)
 974	templinfo["Combos"] = combos
 975	err := readviews.Execute(w, "combos.html", templinfo)
 976	if err != nil {
 977		log.Print(err)
 978	}
 979}
 980
 981func savehonker(w http.ResponseWriter, r *http.Request) {
 982	u := login.GetUserInfo(r)
 983	name := r.FormValue("name")
 984	url := r.FormValue("url")
 985	peep := r.FormValue("peep")
 986	combos := r.FormValue("combos")
 987	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
 988
 989	if honkerid > 0 {
 990		goodbye := r.FormValue("goodbye")
 991		if goodbye == "F" {
 992			db := opendatabase()
 993			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
 994				honkerid, u.UserID)
 995			var xid string
 996			err := row.Scan(&xid)
 997			if err != nil {
 998				log.Printf("can't get honker xid: %s", err)
 999				return
1000			}
1001			log.Printf("unsubscribing from %s", xid)
1002			user, _ := butwhatabout(u.Username)
1003			err = itakeitallback(user, xid)
1004			if err != nil {
1005				log.Printf("can't take it back: %s", err)
1006			} else {
1007				_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1008				if err != nil {
1009					log.Printf("error updating honker: %s", err)
1010					return
1011				}
1012			}
1013
1014			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1015			return
1016		}
1017		combos = " " + strings.TrimSpace(combos) + " "
1018		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1019		if err != nil {
1020			log.Printf("update honker err: %s", err)
1021			return
1022		}
1023		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1024	}
1025
1026	flavor := "presub"
1027	if peep == "peep" {
1028		flavor = "peep"
1029	}
1030	if url == "" {
1031		return
1032	}
1033	if url[0] == '@' {
1034		url = gofish(url)
1035	}
1036	if url == "" {
1037		return
1038	}
1039	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1040	if err != nil {
1041		log.Print(err)
1042		return
1043	}
1044	if flavor == "presub" {
1045		user, _ := butwhatabout(u.Username)
1046		go subsub(user, url)
1047	}
1048	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1049}
1050
1051type Zonker struct {
1052	ID        int64
1053	Name      string
1054	Wherefore string
1055}
1056
1057func killzone(w http.ResponseWriter, r *http.Request) {
1058	db := opendatabase()
1059	userinfo := login.GetUserInfo(r)
1060	rows, err := db.Query("select zonkerid, name, wherefore from zonkers where userid = ?", userinfo.UserID)
1061	if err != nil {
1062		log.Printf("err: %s", err)
1063		return
1064	}
1065	var zonkers []Zonker
1066	for rows.Next() {
1067		var z Zonker
1068		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1069		zonkers = append(zonkers, z)
1070	}
1071	templinfo := getInfo(r)
1072	templinfo["Zonkers"] = zonkers
1073	templinfo["KillCSRF"] = login.GetCSRF("killitwithfire", r)
1074	err = readviews.Execute(w, "zonkers.html", templinfo)
1075	if err != nil {
1076		log.Print(err)
1077	}
1078}
1079
1080func killitwithfire(w http.ResponseWriter, r *http.Request) {
1081	userinfo := login.GetUserInfo(r)
1082	itsok := r.FormValue("itsok")
1083	if itsok == "iforgiveyou" {
1084		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1085		db := opendatabase()
1086		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1087			userinfo.UserID, zonkerid)
1088		bitethethumbs()
1089		http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1090		return
1091	}
1092	wherefore := r.FormValue("wherefore")
1093	name := r.FormValue("name")
1094	if name == "" {
1095		return
1096	}
1097	switch wherefore {
1098	case "zonker":
1099	case "zurl":
1100	case "zonvoy":
1101	case "zword":
1102	default:
1103		return
1104	}
1105	db := opendatabase()
1106	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1107		userinfo.UserID, name, wherefore)
1108	if wherefore == "zonker" || wherefore == "zurl" || wherefore == "zword" {
1109		bitethethumbs()
1110	}
1111
1112	http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1113}
1114
1115func accountpage(w http.ResponseWriter, r *http.Request) {
1116	u := login.GetUserInfo(r)
1117	user, _ := butwhatabout(u.Username)
1118	templinfo := getInfo(r)
1119	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1120	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1121	templinfo["WhatAbout"] = user.About
1122	err := readviews.Execute(w, "account.html", templinfo)
1123	if err != nil {
1124		log.Print(err)
1125	}
1126}
1127
1128func dochpass(w http.ResponseWriter, r *http.Request) {
1129	err := login.ChangePassword(w, r)
1130	if err != nil {
1131		log.Printf("error changing password: %s", err)
1132	}
1133	http.Redirect(w, r, "/account", http.StatusSeeOther)
1134}
1135
1136func fingerlicker(w http.ResponseWriter, r *http.Request) {
1137	orig := r.FormValue("resource")
1138
1139	log.Printf("finger lick: %s", orig)
1140
1141	if strings.HasPrefix(orig, "acct:") {
1142		orig = orig[5:]
1143	}
1144
1145	name := orig
1146	idx := strings.LastIndexByte(name, '/')
1147	if idx != -1 {
1148		name = name[idx+1:]
1149		if "https://"+serverName+"/u/"+name != orig {
1150			log.Printf("foreign request rejected")
1151			name = ""
1152		}
1153	} else {
1154		idx = strings.IndexByte(name, '@')
1155		if idx != -1 {
1156			name = name[:idx]
1157			if name+"@"+serverName != orig {
1158				log.Printf("foreign request rejected")
1159				name = ""
1160			}
1161		}
1162	}
1163	user, err := butwhatabout(name)
1164	if err != nil {
1165		http.NotFound(w, r)
1166		return
1167	}
1168
1169	j := NewJunk()
1170	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1171	j["aliases"] = []string{user.URL}
1172	var links []map[string]interface{}
1173	l := NewJunk()
1174	l["rel"] = "self"
1175	l["type"] = `application/activity+json`
1176	l["href"] = user.URL
1177	links = append(links, l)
1178	j["links"] = links
1179
1180	w.Header().Set("Cache-Control", "max-age=3600")
1181	w.Header().Set("Content-Type", "application/jrd+json")
1182	WriteJunk(w, j)
1183}
1184
1185func somedays() string {
1186	secs := 432000 + notrand.Int63n(432000)
1187	return fmt.Sprintf("%d", secs)
1188}
1189
1190func avatate(w http.ResponseWriter, r *http.Request) {
1191	n := r.FormValue("a")
1192	a := avatar(n)
1193	w.Header().Set("Cache-Control", "max-age="+somedays())
1194	w.Write(a)
1195}
1196
1197func servecss(w http.ResponseWriter, r *http.Request) {
1198	w.Header().Set("Cache-Control", "max-age=7776000")
1199	http.ServeFile(w, r, "views"+r.URL.Path)
1200}
1201func servehtml(w http.ResponseWriter, r *http.Request) {
1202	templinfo := getInfo(r)
1203	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1204	if err != nil {
1205		log.Print(err)
1206	}
1207}
1208func serveemu(w http.ResponseWriter, r *http.Request) {
1209	xid := mux.Vars(r)["xid"]
1210	w.Header().Set("Cache-Control", "max-age="+somedays())
1211	http.ServeFile(w, r, "emus/"+xid)
1212}
1213
1214func servefile(w http.ResponseWriter, r *http.Request) {
1215	xid := mux.Vars(r)["xid"]
1216	row := stmtFileData.QueryRow(xid)
1217	var media string
1218	var data []byte
1219	err := row.Scan(&media, &data)
1220	if err != nil {
1221		log.Printf("error loading file: %s", err)
1222		http.NotFound(w, r)
1223		return
1224	}
1225	w.Header().Set("Content-Type", media)
1226	w.Header().Set("X-Content-Type-Options", "nosniff")
1227	w.Header().Set("Cache-Control", "max-age="+somedays())
1228	w.Write(data)
1229}
1230
1231func serve() {
1232	db := opendatabase()
1233	login.Init(db)
1234
1235	listener, err := openListener()
1236	if err != nil {
1237		log.Fatal(err)
1238	}
1239	go redeliverator()
1240
1241	debug := false
1242	getconfig("debug", &debug)
1243	readviews = templates.Load(debug,
1244		"views/honkpage.html",
1245		"views/honkers.html",
1246		"views/zonkers.html",
1247		"views/combos.html",
1248		"views/honkform.html",
1249		"views/honk.html",
1250		"views/account.html",
1251		"views/login.html",
1252		"views/header.html",
1253	)
1254	if !debug {
1255		s := "views/style.css"
1256		savedstyleparams[s] = getstyleparam(s)
1257		s = "views/local.css"
1258		savedstyleparams[s] = getstyleparam(s)
1259	}
1260
1261	bitethethumbs()
1262
1263	mux := mux.NewRouter()
1264	mux.Use(login.Checker)
1265
1266	posters := mux.Methods("POST").Subrouter()
1267	getters := mux.Methods("GET").Subrouter()
1268
1269	getters.HandleFunc("/", homepage)
1270	getters.HandleFunc("/rss", showrss)
1271	getters.HandleFunc("/u/{name:[[:alnum:]]+}", showuser)
1272	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", showhonk)
1273	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1274	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1275	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1276	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1277	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1278	getters.HandleFunc("/a", avatate)
1279	getters.HandleFunc("/t", showconvoy)
1280	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1281	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1282	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1283
1284	getters.HandleFunc("/style.css", servecss)
1285	getters.HandleFunc("/local.css", servecss)
1286	getters.HandleFunc("/login", servehtml)
1287	posters.HandleFunc("/dologin", login.LoginFunc)
1288	getters.HandleFunc("/logout", login.LogoutFunc)
1289
1290	loggedin := mux.NewRoute().Subrouter()
1291	loggedin.Use(login.Required)
1292	loggedin.HandleFunc("/account", accountpage)
1293	loggedin.HandleFunc("/chpass", dochpass)
1294	loggedin.HandleFunc("/atme", homepage)
1295	loggedin.HandleFunc("/killzone", killzone)
1296	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1297	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1298	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1299	loggedin.Handle("/killitwithfire", login.CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1300	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1301	loggedin.HandleFunc("/honkers", showhonkers)
1302	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1303	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1304	loggedin.HandleFunc("/c", showcombos)
1305	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1306
1307	err = http.Serve(listener, mux)
1308	if err != nil {
1309		log.Fatal(err)
1310	}
1311}
1312
1313func cleanupdb() {
1314	db := opendatabase()
1315	rows, _ := db.Query("select userid, name from zonkers where wherefore = 'zonvoy'")
1316	deadthreads := make(map[int64][]string)
1317	for rows.Next() {
1318		var userid int64
1319		var name string
1320		rows.Scan(&userid, &name)
1321		deadthreads[userid] = append(deadthreads[userid], name)
1322	}
1323	rows.Close()
1324	for userid, threads := range deadthreads {
1325		for _, t := range threads {
1326			doordie(db, "delete from donks where honkid in (select honkid from honks where userid = ? and convoy = ?)", userid, t)
1327			doordie(db, "delete from honks where userid = ? and convoy = ?", userid, t)
1328		}
1329	}
1330	expdate := time.Now().UTC().Add(-30 * 24 * time.Hour).Format(dbtimeformat)
1331	doordie(db, "update files set content = '', local = 0 where length(content) > 20000 and fileid in (select fileid from donks join honks on donks.honkid = honks.honkid where honks.dt < ? and whofore = 0) and fileid not in (select fileid from donks join honks on donks.honkid = honks.honkid where whofore = 2 or whofore = 3)", expdate)
1332	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1333}
1334
1335var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1336var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1337var stmtHonksForUser, stmtHonksForMe, stmtSaveDub *sql.Stmt
1338var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1339var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1340var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1341var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1342var stmtGetBoxes, stmtSaveBoxes *sql.Stmt
1343
1344func preparetodie(db *sql.DB, s string) *sql.Stmt {
1345	stmt, err := db.Prepare(s)
1346	if err != nil {
1347		log.Fatalf("error %s: %s", err, s)
1348	}
1349	return stmt
1350}
1351
1352func prepareStatements(db *sql.DB) {
1353	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")
1354	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1355	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1356	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1357	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1358	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1359
1360	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 "
1361	limit := " order by honkid desc limit 250"
1362	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1363	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1364	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1365	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1366	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)
1367	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1368	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1369	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1370	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or whofore = 2) and convoy = ?"+limit)
1371
1372	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1373	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1374	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1375	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1376	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1377	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1378	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1379	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1380	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1381	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1382	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1383	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1384	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1385	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1386	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zurl' or wherefore = 'zword')")
1387	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1388	stmtGetBoxes = preparetodie(db, "select ibox, obox, sbox from xonkers where xid = ?")
1389	stmtSaveBoxes = preparetodie(db, "insert into xonkers (xid, ibox, obox, sbox, pubkey) values (?, ?, ?, ?, ?)")
1390}
1391
1392func ElaborateUnitTests() {
1393}
1394
1395func main() {
1396	cmd := "run"
1397	if len(os.Args) > 1 {
1398		cmd = os.Args[1]
1399	}
1400	switch cmd {
1401	case "init":
1402		initdb()
1403	case "upgrade":
1404		upgradedb()
1405	}
1406	db := opendatabase()
1407	dbversion := 0
1408	getconfig("dbversion", &dbversion)
1409	if dbversion != myVersion {
1410		log.Fatal("incorrect database version. run upgrade.")
1411	}
1412	getconfig("servername", &serverName)
1413	prepareStatements(db)
1414	switch cmd {
1415	case "adduser":
1416		adduser()
1417	case "cleanup":
1418		cleanupdb()
1419	case "ping":
1420		if len(os.Args) < 4 {
1421			fmt.Printf("usage: honk ping from to\n")
1422			return
1423		}
1424		name := os.Args[2]
1425		targ := os.Args[3]
1426		user, err := butwhatabout(name)
1427		if err != nil {
1428			log.Printf("unknown user")
1429			return
1430		}
1431		ping(user, targ)
1432	case "peep":
1433		peeppeep()
1434	case "run":
1435		serve()
1436	case "test":
1437		ElaborateUnitTests()
1438	default:
1439		log.Fatal("unknown command")
1440	}
1441}