all repos — honk @ f409d2694b6afa51206b3bb7196c4310f16f5478

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