all repos — honk @ 47cd70806cb551c6e73b12d68932be65ed56f6ca

my fork of honk

honk.go (view raw)

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