all repos — honk @ cb90c3d285f215b52945c2bd0b93086c4e2660b7

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 "Like":
 464			case "Announce":
 465			default:
 466				log.Printf("unknown undo: %s", what)
 467			}
 468		}
 469	default:
 470		go consumeactivity(user, j, origin)
 471	}
 472}
 473
 474func ximport(w http.ResponseWriter, r *http.Request) {
 475	xid := r.FormValue("xid")
 476	p := investigate(xid)
 477	if p != nil {
 478		xid = p.XID
 479	}
 480	j, err := GetJunk(xid)
 481	if err != nil {
 482		http.Error(w, "error getting external object", http.StatusInternalServerError)
 483		log.Printf("error getting external object: %s", err)
 484		return
 485	}
 486	log.Printf("importing %s", xid)
 487	u := login.GetUserInfo(r)
 488	user, _ := butwhatabout(u.Username)
 489
 490	what, _ := j.GetString("type")
 491	if isactor(what) {
 492		outbox, _ := j.GetString("outbox")
 493		gimmexonks(user, outbox)
 494		http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
 495		return
 496	}
 497	xonk := xonkxonk(user, j, originate(xid))
 498	convoy := ""
 499	if xonk != nil {
 500		convoy = xonk.Convoy
 501		savexonk(user, xonk)
 502	}
 503	http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
 504}
 505
 506func xzone(w http.ResponseWriter, r *http.Request) {
 507	u := login.GetUserInfo(r)
 508	rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID)
 509	if err != nil {
 510		log.Printf("query err: %s", err)
 511		return
 512	}
 513	defer rows.Close()
 514	var honkers []Honker
 515	for rows.Next() {
 516		var xid string
 517		rows.Scan(&xid)
 518		honkers = append(honkers, Honker{XID: xid})
 519	}
 520	rows.Close()
 521	for i, _ := range honkers {
 522		_, honkers[i].Handle = handles(honkers[i].XID)
 523	}
 524	templinfo := getInfo(r)
 525	templinfo["XCSRF"] = login.GetCSRF("ximport", r)
 526	templinfo["Honkers"] = honkers
 527	err = readviews.Execute(w, "xzone.html", templinfo)
 528	if err != nil {
 529		log.Print(err)
 530	}
 531}
 532
 533func outbox(w http.ResponseWriter, r *http.Request) {
 534	name := mux.Vars(r)["name"]
 535	user, err := butwhatabout(name)
 536	if err != nil {
 537		http.NotFound(w, r)
 538		return
 539	}
 540	if stealthed(r) {
 541		http.NotFound(w, r)
 542		return
 543	}
 544
 545	honks := gethonksbyuser(name, false)
 546
 547	var jonks []junk.Junk
 548	for _, h := range honks {
 549		j, _ := jonkjonk(user, h)
 550		jonks = append(jonks, j)
 551	}
 552
 553	j := junk.New()
 554	j["@context"] = itiswhatitis
 555	j["id"] = user.URL + "/outbox"
 556	j["type"] = "OrderedCollection"
 557	j["totalItems"] = len(jonks)
 558	j["orderedItems"] = jonks
 559
 560	w.Header().Set("Content-Type", theonetruename)
 561	j.Write(w)
 562}
 563
 564func emptiness(w http.ResponseWriter, r *http.Request) {
 565	name := mux.Vars(r)["name"]
 566	user, err := butwhatabout(name)
 567	if err != nil {
 568		http.NotFound(w, r)
 569		return
 570	}
 571	colname := "/followers"
 572	if strings.HasSuffix(r.URL.Path, "/following") {
 573		colname = "/following"
 574	}
 575	j := junk.New()
 576	j["@context"] = itiswhatitis
 577	j["id"] = user.URL + colname
 578	j["type"] = "OrderedCollection"
 579	j["totalItems"] = 0
 580	j["orderedItems"] = []junk.Junk{}
 581
 582	w.Header().Set("Content-Type", theonetruename)
 583	j.Write(w)
 584}
 585
 586func showuser(w http.ResponseWriter, r *http.Request) {
 587	name := mux.Vars(r)["name"]
 588	user, err := butwhatabout(name)
 589	if err != nil {
 590		log.Printf("user not found %s: %s", name, err)
 591		http.NotFound(w, r)
 592		return
 593	}
 594	if friendorfoe(r.Header.Get("Accept")) {
 595		j := asjonker(user)
 596		w.Header().Set("Content-Type", theonetruename)
 597		j.Write(w)
 598		return
 599	}
 600	u := login.GetUserInfo(r)
 601	honks := gethonksbyuser(name, u != nil && u.Username == name)
 602	honkpage(w, r, u, user, honks, "")
 603}
 604
 605func showhonker(w http.ResponseWriter, r *http.Request) {
 606	u := login.GetUserInfo(r)
 607	name := mux.Vars(r)["name"]
 608	var honks []*Honk
 609	if name == "" {
 610		name = r.FormValue("xid")
 611		honks = gethonksbyxonker(u.UserID, name)
 612	} else {
 613		honks = gethonksbyhonker(u.UserID, name)
 614	}
 615	name = html.EscapeString(name)
 616	msg := fmt.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>`, name, name)
 617	honkpage(w, r, u, nil, honks, template.HTML(msg))
 618}
 619
 620func showcombo(w http.ResponseWriter, r *http.Request) {
 621	name := mux.Vars(r)["name"]
 622	u := login.GetUserInfo(r)
 623	honks := gethonksbycombo(u.UserID, name)
 624	honks = osmosis(honks, u.UserID)
 625	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by combo: "+name)))
 626}
 627func showconvoy(w http.ResponseWriter, r *http.Request) {
 628	c := r.FormValue("c")
 629	u := login.GetUserInfo(r)
 630	honks := gethonksbyconvoy(u.UserID, c)
 631	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks in convoy: "+c)))
 632}
 633func showontology(w http.ResponseWriter, r *http.Request) {
 634	name := mux.Vars(r)["name"]
 635	u := login.GetUserInfo(r)
 636	honks := gethonksbyontology("#" + name)
 637	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by ontology: "+name)))
 638}
 639
 640func showhonk(w http.ResponseWriter, r *http.Request) {
 641	name := mux.Vars(r)["name"]
 642	user, err := butwhatabout(name)
 643	if err != nil {
 644		http.NotFound(w, r)
 645		return
 646	}
 647	if stealthed(r) {
 648		http.NotFound(w, r)
 649		return
 650	}
 651
 652	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 653	honk := getxonk(user.ID, xid)
 654	if honk == nil {
 655		http.NotFound(w, r)
 656		return
 657	}
 658	u := login.GetUserInfo(r)
 659	if u != nil && u.UserID != user.ID {
 660		u = nil
 661	}
 662	if !honk.Public {
 663		if u == nil {
 664			http.NotFound(w, r)
 665			return
 666
 667		}
 668		honkpage(w, r, u, nil, []*Honk{honk}, "one honk maybe more")
 669		return
 670	}
 671	rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy)
 672	if friendorfoe(r.Header.Get("Accept")) {
 673		for _, h := range rawhonks {
 674			if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
 675				honk.Replies = append(honk.Replies, h)
 676			}
 677		}
 678		donksforhonks([]*Honk{honk})
 679		_, j := jonkjonk(user, honk)
 680		j["@context"] = itiswhatitis
 681		w.Header().Set("Content-Type", theonetruename)
 682		j.Write(w)
 683		return
 684	}
 685	var honks []*Honk
 686	for _, h := range rawhonks {
 687		if h.Public && (h.Whofore == 2 || h.IsAcked()) {
 688			honks = append(honks, h)
 689		}
 690	}
 691
 692	honkpage(w, r, u, nil, honks, "one honk maybe more")
 693}
 694
 695func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 696	honks []*Honk, infomsg template.HTML) {
 697	templinfo := getInfo(r)
 698	var userid int64 = -1
 699	if u != nil {
 700		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 701		userid = u.UserID
 702	}
 703	if u == nil {
 704		w.Header().Set("Cache-Control", "max-age=60")
 705	}
 706	reverbolate(userid, honks)
 707	if user != nil {
 708		filt := htfilter.New()
 709		templinfo["Name"] = user.Name
 710		whatabout := user.About
 711		whatabout = obfusbreak(user.About)
 712		templinfo["WhatAbout"], _ = filt.String(whatabout)
 713	}
 714	templinfo["Honks"] = honks
 715	templinfo["ServerMessage"] = infomsg
 716	err := readviews.Execute(w, "honkpage.html", templinfo)
 717	if err != nil {
 718		log.Print(err)
 719	}
 720}
 721
 722func saveuser(w http.ResponseWriter, r *http.Request) {
 723	whatabout := r.FormValue("whatabout")
 724	u := login.GetUserInfo(r)
 725	db := opendatabase()
 726	options := ""
 727	if r.FormValue("skinny") == "skinny" {
 728		options += " skinny "
 729	}
 730	_, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
 731	if err != nil {
 732		log.Printf("error bouting what: %s", err)
 733	}
 734
 735	http.Redirect(w, r, "/account", http.StatusSeeOther)
 736}
 737
 738func gethonkers(userid int64) []*Honker {
 739	rows, err := stmtHonkers.Query(userid)
 740	if err != nil {
 741		log.Printf("error querying honkers: %s", err)
 742		return nil
 743	}
 744	defer rows.Close()
 745	var honkers []*Honker
 746	for rows.Next() {
 747		var f Honker
 748		var combos string
 749		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 750		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 751		if err != nil {
 752			log.Printf("error scanning honker: %s", err)
 753			return nil
 754		}
 755		honkers = append(honkers, &f)
 756	}
 757	return honkers
 758}
 759
 760func getdubs(userid int64) []*Honker {
 761	rows, err := stmtDubbers.Query(userid)
 762	if err != nil {
 763		log.Printf("error querying dubs: %s", err)
 764		return nil
 765	}
 766	defer rows.Close()
 767	var honkers []*Honker
 768	for rows.Next() {
 769		var f Honker
 770		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 771		if err != nil {
 772			log.Printf("error scanning honker: %s", err)
 773			return nil
 774		}
 775		honkers = append(honkers, &f)
 776	}
 777	return honkers
 778}
 779
 780func allusers() []login.UserInfo {
 781	var users []login.UserInfo
 782	rows, _ := opendatabase().Query("select userid, username from users")
 783	defer rows.Close()
 784	for rows.Next() {
 785		var u login.UserInfo
 786		rows.Scan(&u.UserID, &u.Username)
 787		users = append(users, u)
 788	}
 789	return users
 790}
 791
 792func getxonk(userid int64, xid string) *Honk {
 793	h := new(Honk)
 794	var dt, aud string
 795	row := stmtOneXonk.QueryRow(userid, xid)
 796	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 797		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags)
 798	if err != nil {
 799		if err != sql.ErrNoRows {
 800			log.Printf("error scanning xonk: %s", err)
 801		}
 802		return nil
 803	}
 804	h.Date, _ = time.Parse(dbtimeformat, dt)
 805	h.Audience = strings.Split(aud, " ")
 806	h.Public = !keepitquiet(h.Audience)
 807	return h
 808}
 809
 810func getpublichonks() []*Honk {
 811	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 812	rows, err := stmtPublicHonks.Query(dt)
 813	return getsomehonks(rows, err)
 814}
 815func gethonksbyuser(name string, includeprivate bool) []*Honk {
 816	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 817	whofore := 2
 818	if includeprivate {
 819		whofore = 3
 820	}
 821	rows, err := stmtUserHonks.Query(whofore, name, dt)
 822	return getsomehonks(rows, err)
 823}
 824func gethonksforuser(userid int64) []*Honk {
 825	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 826	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 827	return getsomehonks(rows, err)
 828}
 829func gethonksforme(userid int64) []*Honk {
 830	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 831	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 832	return getsomehonks(rows, err)
 833}
 834func gethonksbyhonker(userid int64, honker string) []*Honk {
 835	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 836	return getsomehonks(rows, err)
 837}
 838func gethonksbyxonker(userid int64, xonker string) []*Honk {
 839	rows, err := stmtHonksByXonker.Query(userid, xonker, xonker, userid)
 840	return getsomehonks(rows, err)
 841}
 842func gethonksbycombo(userid int64, combo string) []*Honk {
 843	combo = "% " + combo + " %"
 844	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 845	return getsomehonks(rows, err)
 846}
 847func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 848	rows, err := stmtHonksByConvoy.Query(userid, userid, convoy)
 849	honks := getsomehonks(rows, err)
 850	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 851		honks[i], honks[j] = honks[j], honks[i]
 852	}
 853	return honks
 854}
 855func gethonksbyontology(name string) []*Honk {
 856	rows, err := stmtHonksByOntology.Query(name)
 857	honks := getsomehonks(rows, err)
 858	return honks
 859}
 860
 861func getsomehonks(rows *sql.Rows, err error) []*Honk {
 862	if err != nil {
 863		log.Printf("error querying honks: %s", err)
 864		return nil
 865	}
 866	defer rows.Close()
 867	var honks []*Honk
 868	for rows.Next() {
 869		var h Honk
 870		var dt, aud string
 871		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 872			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags)
 873		if err != nil {
 874			log.Printf("error scanning honks: %s", err)
 875			return nil
 876		}
 877		h.Date, _ = time.Parse(dbtimeformat, dt)
 878		h.Audience = strings.Split(aud, " ")
 879		h.Public = !keepitquiet(h.Audience)
 880		honks = append(honks, &h)
 881	}
 882	rows.Close()
 883	donksforhonks(honks)
 884	return honks
 885}
 886
 887func donksforhonks(honks []*Honk) {
 888	db := opendatabase()
 889	var ids []string
 890	hmap := make(map[int64]*Honk)
 891	for _, h := range honks {
 892		ids = append(ids, fmt.Sprintf("%d", h.ID))
 893		hmap[h.ID] = h
 894	}
 895	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, ","))
 896	rows, err := db.Query(q)
 897	if err != nil {
 898		log.Printf("error querying donks: %s", err)
 899		return
 900	}
 901	defer rows.Close()
 902	for rows.Next() {
 903		var hid int64
 904		var d Donk
 905		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 906		if err != nil {
 907			log.Printf("error scanning donk: %s", err)
 908			continue
 909		}
 910		h := hmap[hid]
 911		h.Donks = append(h.Donks, &d)
 912	}
 913}
 914
 915func savebonk(w http.ResponseWriter, r *http.Request) {
 916	xid := r.FormValue("xid")
 917	userinfo := login.GetUserInfo(r)
 918	user, _ := butwhatabout(userinfo.Username)
 919
 920	log.Printf("bonking %s", xid)
 921
 922	xonk := getxonk(userinfo.UserID, xid)
 923	if xonk == nil {
 924		return
 925	}
 926	if !xonk.Public {
 927		return
 928	}
 929	donksforhonks([]*Honk{xonk})
 930
 931	_, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.XID, userinfo.UserID)
 932	if err != nil {
 933		log.Printf("error acking bonk: %s", err)
 934	}
 935
 936	oonker := xonk.Oonker
 937	if oonker == "" {
 938		oonker = xonk.Honker
 939	}
 940	dt := time.Now().UTC()
 941	bonk := Honk{
 942		UserID:   userinfo.UserID,
 943		Username: userinfo.Username,
 944		What:     "bonk",
 945		Honker:   user.URL,
 946		XID:      xonk.XID,
 947		Date:     dt,
 948		Donks:    xonk.Donks,
 949		Convoy:   xonk.Convoy,
 950		Audience: []string{thewholeworld, oonker},
 951		Public:   true,
 952	}
 953
 954	aud := strings.Join(bonk.Audience, " ")
 955	whofore := 2
 956	onts := ontologies(xonk.Noise)
 957	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 958		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 959		xonk.Precis, oonker, 0, strings.Join(onts, " "))
 960	if err != nil {
 961		log.Printf("error saving bonk: %s", err)
 962		return
 963	}
 964	bonk.ID, _ = res.LastInsertId()
 965	for _, d := range bonk.Donks {
 966		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 967		if err != nil {
 968			log.Printf("err saving donk: %s", err)
 969			return
 970		}
 971	}
 972	for _, o := range onts {
 973		_, err = stmtSaveOnts.Exec(strings.ToLower(o), bonk.ID)
 974		if err != nil {
 975			log.Printf("error saving ont: %s", err)
 976		}
 977	}
 978
 979	go honkworldwide(user, &bonk)
 980}
 981
 982func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string) {
 983	zonk := Honk{
 984		What:     what,
 985		XID:      xonk.XID,
 986		Date:     time.Now().UTC(),
 987		Audience: oneofakind(xonk.Audience),
 988	}
 989	zonk.Public = !keepitquiet(zonk.Audience)
 990
 991	log.Printf("announcing %sed honk: %s", what, xonk.XID)
 992	go honkworldwide(user, &zonk)
 993}
 994
 995func zonkit(w http.ResponseWriter, r *http.Request) {
 996	wherefore := r.FormValue("wherefore")
 997	what := r.FormValue("what")
 998	userinfo := login.GetUserInfo(r)
 999	user, _ := butwhatabout(userinfo.Username)
1000
1001	if wherefore == "ack" {
1002		_, err := stmtUpdateFlags.Exec(flagIsAcked, what, userinfo.UserID)
1003		if err != nil {
1004			log.Printf("error acking: %s", err)
1005		}
1006		xonk := getxonk(userinfo.UserID, what)
1007		if xonk != nil {
1008			sendzonkofsorts(xonk, user, "ack")
1009		}
1010		return
1011	}
1012
1013	if wherefore == "deack" {
1014		_, err := stmtClearFlags.Exec(flagIsAcked, what, userinfo.UserID)
1015		if err != nil {
1016			log.Printf("error deacking: %s", err)
1017		}
1018		xonk := getxonk(userinfo.UserID, what)
1019		if xonk != nil {
1020			sendzonkofsorts(xonk, user, "deack")
1021		}
1022		return
1023	}
1024
1025	if wherefore == "unbonk" {
1026		// todo
1027		return
1028	}
1029
1030	log.Printf("zonking %s %s", wherefore, what)
1031	if wherefore == "zonk" {
1032		xonk := getxonk(userinfo.UserID, what)
1033		if xonk != nil {
1034			_, err := stmtZonkDonks.Exec(xonk.ID)
1035			if err != nil {
1036				log.Printf("error zonking: %s", err)
1037			}
1038			_, err = stmtZonkIt.Exec(userinfo.UserID, what)
1039			if err != nil {
1040				log.Printf("error zonking: %s", err)
1041			}
1042			if xonk.Whofore == 2 || xonk.Whofore == 3 {
1043				sendzonkofsorts(xonk, user, "zonk")
1044			}
1045		}
1046	}
1047	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
1048	if err != nil {
1049		log.Printf("error saving zonker: %s", err)
1050		return
1051	}
1052}
1053
1054func savehonk(w http.ResponseWriter, r *http.Request) {
1055	rid := r.FormValue("rid")
1056	noise := r.FormValue("noise")
1057
1058	userinfo := login.GetUserInfo(r)
1059	user, _ := butwhatabout(userinfo.Username)
1060
1061	dt := time.Now().UTC()
1062	xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
1063	what := "honk"
1064	if rid != "" {
1065		what = "tonk"
1066	}
1067	honk := Honk{
1068		UserID:   userinfo.UserID,
1069		Username: userinfo.Username,
1070		What:     "honk",
1071		Honker:   user.URL,
1072		XID:      xid,
1073		Date:     dt,
1074	}
1075	if strings.HasPrefix(noise, "DZ:") {
1076		idx := strings.Index(noise, "\n")
1077		if idx == -1 {
1078			honk.Precis = noise
1079			noise = ""
1080		} else {
1081			honk.Precis = noise[:idx]
1082			noise = noise[idx+1:]
1083		}
1084	}
1085	noise = hooterize(noise)
1086	noise = strings.TrimSpace(noise)
1087	honk.Precis = strings.TrimSpace(honk.Precis)
1088
1089	var convoy string
1090	if rid != "" {
1091		xonk := getxonk(userinfo.UserID, rid)
1092		if xonk != nil {
1093			if xonk.Public {
1094				honk.Audience = append(honk.Audience, xonk.Audience...)
1095			}
1096			convoy = xonk.Convoy
1097		} else {
1098			xonkaud, c := whosthere(rid)
1099			honk.Audience = append(honk.Audience, xonkaud...)
1100			convoy = c
1101		}
1102		for i, a := range honk.Audience {
1103			if a == thewholeworld {
1104				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1105				break
1106			}
1107		}
1108		honk.RID = rid
1109	} else {
1110		honk.Audience = []string{thewholeworld}
1111	}
1112	if noise != "" && noise[0] == '@' {
1113		honk.Audience = append(grapevine(noise), honk.Audience...)
1114	} else {
1115		honk.Audience = append(honk.Audience, grapevine(noise)...)
1116	}
1117	if convoy == "" {
1118		convoy = "data:,electrichonkytonk-" + xfiltrate()
1119	}
1120	butnottooloud(honk.Audience)
1121	honk.Audience = oneofakind(honk.Audience)
1122	if len(honk.Audience) == 0 {
1123		log.Printf("honk to nowhere")
1124		http.Error(w, "honk to nowhere...", http.StatusNotFound)
1125		return
1126	}
1127	honk.Public = !keepitquiet(honk.Audience)
1128	noise = obfusbreak(noise)
1129	honk.Noise = noise
1130	honk.Convoy = convoy
1131
1132	donkxid := r.FormValue("donkxid")
1133	if donkxid == "" {
1134		file, filehdr, err := r.FormFile("donk")
1135		if err == nil {
1136			var buf bytes.Buffer
1137			io.Copy(&buf, file)
1138			file.Close()
1139			data := buf.Bytes()
1140			xid := xfiltrate()
1141			var media, name string
1142			img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1143			if err == nil {
1144				data = img.Data
1145				format := img.Format
1146				media = "image/" + format
1147				if format == "jpeg" {
1148					format = "jpg"
1149				}
1150				name = xid + "." + format
1151				xid = name
1152			} else {
1153				maxsize := 100000
1154				if len(data) > maxsize {
1155					log.Printf("bad image: %s too much text: %d", err, len(data))
1156					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1157					return
1158				}
1159				for i := 0; i < len(data); i++ {
1160					if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1161						log.Printf("bad image: %s not text: %d", err, data[i])
1162						http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1163						return
1164					}
1165				}
1166				media = "text/plain"
1167				name = filehdr.Filename
1168				if name == "" {
1169					name = xid + ".txt"
1170				}
1171				xid += ".txt"
1172			}
1173			url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1174			res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1175			if err != nil {
1176				log.Printf("unable to save image: %s", err)
1177				return
1178			}
1179			var d Donk
1180			d.FileID, _ = res.LastInsertId()
1181			d.XID = name
1182			d.Name = name
1183			d.Media = media
1184			d.URL = url
1185			d.Local = true
1186			honk.Donks = append(honk.Donks, &d)
1187			donkxid = d.XID
1188		}
1189	} else {
1190		xid := donkxid
1191		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1192		var donk Donk
1193		row := stmtFindFile.QueryRow(url)
1194		err := row.Scan(&donk.FileID)
1195		if err == nil {
1196			donk.XID = xid
1197			donk.Local = true
1198			donk.URL = url
1199			honk.Donks = append(honk.Donks, &donk)
1200		} else {
1201			log.Printf("can't find file: %s", xid)
1202		}
1203	}
1204	herd := herdofemus(honk.Noise)
1205	for _, e := range herd {
1206		donk := savedonk(e.ID, e.Name, "image/png", true)
1207		if donk != nil {
1208			donk.Name = e.Name
1209			honk.Donks = append(honk.Donks, donk)
1210		}
1211	}
1212	memetize(&honk)
1213
1214	aud := strings.Join(honk.Audience, " ")
1215	whofore := 2
1216	if !honk.Public {
1217		whofore = 3
1218	}
1219	if r.FormValue("preview") == "preview" {
1220		honks := []*Honk{&honk}
1221		reverbolate(userinfo.UserID, honks)
1222		templinfo := getInfo(r)
1223		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1224		templinfo["Honks"] = honks
1225		templinfo["InReplyTo"] = r.FormValue("rid")
1226		templinfo["Noise"] = r.FormValue("noise")
1227		templinfo["SavedFile"] = donkxid
1228		templinfo["ServerMessage"] = "honk preview"
1229		err := readviews.Execute(w, "honkpage.html", templinfo)
1230		if err != nil {
1231			log.Print(err)
1232		}
1233		return
1234	}
1235	onts := ontologies(honk.Noise)
1236	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1237		dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html",
1238		honk.Precis, honk.Oonker, 0, strings.Join(onts, " "))
1239	if err != nil {
1240		log.Printf("error saving honk: %s", err)
1241		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1242		return
1243	}
1244	honk.ID, _ = res.LastInsertId()
1245	for _, d := range honk.Donks {
1246		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1247		if err != nil {
1248			log.Printf("err saving donk: %s", err)
1249			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1250			return
1251		}
1252	}
1253	for _, o := range onts {
1254		_, err = stmtSaveOnts.Exec(strings.ToLower(o), honk.ID)
1255		if err != nil {
1256			log.Printf("error saving ont: %s", err)
1257		}
1258	}
1259
1260	go honkworldwide(user, &honk)
1261
1262	http.Redirect(w, r, xid, http.StatusSeeOther)
1263}
1264
1265func showhonkers(w http.ResponseWriter, r *http.Request) {
1266	userinfo := login.GetUserInfo(r)
1267	templinfo := getInfo(r)
1268	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1269	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1270	err := readviews.Execute(w, "honkers.html", templinfo)
1271	if err != nil {
1272		log.Print(err)
1273	}
1274}
1275
1276func showcombos(w http.ResponseWriter, r *http.Request) {
1277	userinfo := login.GetUserInfo(r)
1278	templinfo := getInfo(r)
1279	honkers := gethonkers(userinfo.UserID)
1280	var combos []string
1281	for _, h := range honkers {
1282		combos = append(combos, h.Combos...)
1283	}
1284	for i, c := range combos {
1285		if c == "-" {
1286			combos[i] = ""
1287		}
1288	}
1289	combos = oneofakind(combos)
1290	sort.Strings(combos)
1291	templinfo["Combos"] = combos
1292	err := readviews.Execute(w, "combos.html", templinfo)
1293	if err != nil {
1294		log.Print(err)
1295	}
1296}
1297
1298func savehonker(w http.ResponseWriter, r *http.Request) {
1299	u := login.GetUserInfo(r)
1300	name := r.FormValue("name")
1301	url := r.FormValue("url")
1302	peep := r.FormValue("peep")
1303	combos := r.FormValue("combos")
1304	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1305
1306	if honkerid > 0 {
1307		goodbye := r.FormValue("goodbye")
1308		if goodbye == "F" {
1309			db := opendatabase()
1310			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1311				honkerid, u.UserID)
1312			var xid string
1313			err := row.Scan(&xid)
1314			if err != nil {
1315				log.Printf("can't get honker xid: %s", err)
1316				return
1317			}
1318			log.Printf("unsubscribing from %s", xid)
1319			user, _ := butwhatabout(u.Username)
1320			go itakeitallback(user, xid)
1321			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1322			if err != nil {
1323				log.Printf("error updating honker: %s", err)
1324				return
1325			}
1326
1327			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1328			return
1329		}
1330		combos = " " + strings.TrimSpace(combos) + " "
1331		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1332		if err != nil {
1333			log.Printf("update honker err: %s", err)
1334			return
1335		}
1336		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1337	}
1338
1339	flavor := "presub"
1340	if peep == "peep" {
1341		flavor = "peep"
1342	}
1343	p := investigate(url)
1344	if p == nil {
1345		log.Printf("failed to investigate honker")
1346		return
1347	}
1348	url = p.XID
1349	if name == "" {
1350		name = p.Handle
1351	}
1352	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1353	if err != nil {
1354		log.Print(err)
1355		return
1356	}
1357	if flavor == "presub" {
1358		user, _ := butwhatabout(u.Username)
1359		go subsub(user, url)
1360	}
1361	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1362}
1363
1364type Zonker struct {
1365	ID        int64
1366	Name      string
1367	Wherefore string
1368}
1369
1370func zonkzone(w http.ResponseWriter, r *http.Request) {
1371	userinfo := login.GetUserInfo(r)
1372	rows, err := stmtGetZonkers.Query(userinfo.UserID)
1373	if err != nil {
1374		log.Printf("err: %s", err)
1375		return
1376	}
1377	defer rows.Close()
1378	var zonkers []Zonker
1379	for rows.Next() {
1380		var z Zonker
1381		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1382		zonkers = append(zonkers, z)
1383	}
1384	sort.Slice(zonkers, func(i, j int) bool {
1385		w1 := zonkers[i].Wherefore
1386		w2 := zonkers[j].Wherefore
1387		if w1 == w2 {
1388			return zonkers[i].Name < zonkers[j].Name
1389		}
1390		if w1 == "zonvoy" {
1391			w1 = "zzzzzzz"
1392		}
1393		if w2 == "zonvoy" {
1394			w2 = "zzzzzzz"
1395		}
1396		return w1 < w2
1397	})
1398
1399	templinfo := getInfo(r)
1400	templinfo["Zonkers"] = zonkers
1401	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1402	err = readviews.Execute(w, "zonkers.html", templinfo)
1403	if err != nil {
1404		log.Print(err)
1405	}
1406}
1407
1408func zonkzonk(w http.ResponseWriter, r *http.Request) {
1409	userinfo := login.GetUserInfo(r)
1410	itsok := r.FormValue("itsok")
1411	if itsok == "iforgiveyou" {
1412		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1413		db := opendatabase()
1414		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1415			userinfo.UserID, zonkerid)
1416		bitethethumbs()
1417		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1418		return
1419	}
1420	wherefore := r.FormValue("wherefore")
1421	name := r.FormValue("name")
1422	if name == "" {
1423		return
1424	}
1425	switch wherefore {
1426	case "zonker":
1427	case "zomain":
1428	case "zonvoy":
1429	case "zord":
1430	case "zilence":
1431	default:
1432		return
1433	}
1434	db := opendatabase()
1435	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1436		userinfo.UserID, name, wherefore)
1437	if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1438		bitethethumbs()
1439	}
1440
1441	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1442}
1443
1444func accountpage(w http.ResponseWriter, r *http.Request) {
1445	u := login.GetUserInfo(r)
1446	user, _ := butwhatabout(u.Username)
1447	templinfo := getInfo(r)
1448	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1449	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1450	templinfo["User"] = user
1451	err := readviews.Execute(w, "account.html", templinfo)
1452	if err != nil {
1453		log.Print(err)
1454	}
1455}
1456
1457func dochpass(w http.ResponseWriter, r *http.Request) {
1458	err := login.ChangePassword(w, r)
1459	if err != nil {
1460		log.Printf("error changing password: %s", err)
1461	}
1462	http.Redirect(w, r, "/account", http.StatusSeeOther)
1463}
1464
1465func fingerlicker(w http.ResponseWriter, r *http.Request) {
1466	orig := r.FormValue("resource")
1467
1468	log.Printf("finger lick: %s", orig)
1469
1470	if strings.HasPrefix(orig, "acct:") {
1471		orig = orig[5:]
1472	}
1473
1474	name := orig
1475	idx := strings.LastIndexByte(name, '/')
1476	if idx != -1 {
1477		name = name[idx+1:]
1478		if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
1479			log.Printf("foreign request rejected")
1480			name = ""
1481		}
1482	} else {
1483		idx = strings.IndexByte(name, '@')
1484		if idx != -1 {
1485			name = name[:idx]
1486			if name+"@"+serverName != orig {
1487				log.Printf("foreign request rejected")
1488				name = ""
1489			}
1490		}
1491	}
1492	user, err := butwhatabout(name)
1493	if err != nil {
1494		http.NotFound(w, r)
1495		return
1496	}
1497
1498	j := junk.New()
1499	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1500	j["aliases"] = []string{user.URL}
1501	var links []junk.Junk
1502	l := junk.New()
1503	l["rel"] = "self"
1504	l["type"] = `application/activity+json`
1505	l["href"] = user.URL
1506	links = append(links, l)
1507	j["links"] = links
1508
1509	w.Header().Set("Cache-Control", "max-age=3600")
1510	w.Header().Set("Content-Type", "application/jrd+json")
1511	j.Write(w)
1512}
1513
1514func somedays() string {
1515	secs := 432000 + notrand.Int63n(432000)
1516	return fmt.Sprintf("%d", secs)
1517}
1518
1519func avatate(w http.ResponseWriter, r *http.Request) {
1520	n := r.FormValue("a")
1521	a := avatar(n)
1522	w.Header().Set("Cache-Control", "max-age="+somedays())
1523	w.Write(a)
1524}
1525
1526func servecss(w http.ResponseWriter, r *http.Request) {
1527	w.Header().Set("Cache-Control", "max-age=7776000")
1528	http.ServeFile(w, r, "views"+r.URL.Path)
1529}
1530func servehtml(w http.ResponseWriter, r *http.Request) {
1531	templinfo := getInfo(r)
1532	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1533	if err != nil {
1534		log.Print(err)
1535	}
1536}
1537func serveemu(w http.ResponseWriter, r *http.Request) {
1538	xid := mux.Vars(r)["xid"]
1539	w.Header().Set("Cache-Control", "max-age="+somedays())
1540	http.ServeFile(w, r, "emus/"+xid)
1541}
1542func servememe(w http.ResponseWriter, r *http.Request) {
1543	xid := mux.Vars(r)["xid"]
1544	w.Header().Set("Cache-Control", "max-age="+somedays())
1545	http.ServeFile(w, r, "memes/"+xid)
1546}
1547
1548func servefile(w http.ResponseWriter, r *http.Request) {
1549	xid := mux.Vars(r)["xid"]
1550	row := stmtFileData.QueryRow(xid)
1551	var media string
1552	var data []byte
1553	err := row.Scan(&media, &data)
1554	if err != nil {
1555		log.Printf("error loading file: %s", err)
1556		http.NotFound(w, r)
1557		return
1558	}
1559	w.Header().Set("Content-Type", media)
1560	w.Header().Set("X-Content-Type-Options", "nosniff")
1561	w.Header().Set("Cache-Control", "max-age="+somedays())
1562	w.Write(data)
1563}
1564
1565func nomoroboto(w http.ResponseWriter, r *http.Request) {
1566	io.WriteString(w, "User-agent: *\n")
1567	io.WriteString(w, "Disallow: /a\n")
1568	io.WriteString(w, "Disallow: /d\n")
1569	io.WriteString(w, "Disallow: /meme\n")
1570	for _, u := range allusers() {
1571		fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
1572	}
1573}
1574
1575func serve() {
1576	db := opendatabase()
1577	login.Init(db)
1578
1579	listener, err := openListener()
1580	if err != nil {
1581		log.Fatal(err)
1582	}
1583	go redeliverator()
1584
1585	debug := false
1586	getconfig("debug", &debug)
1587	readviews = templates.Load(debug,
1588		"views/honkpage.html",
1589		"views/honkfrags.html",
1590		"views/honkers.html",
1591		"views/zonkers.html",
1592		"views/combos.html",
1593		"views/honkform.html",
1594		"views/honk.html",
1595		"views/account.html",
1596		"views/about.html",
1597		"views/funzone.html",
1598		"views/login.html",
1599		"views/xzone.html",
1600		"views/header.html",
1601	)
1602	if !debug {
1603		s := "views/style.css"
1604		savedstyleparams[s] = getstyleparam(s)
1605		s = "views/local.css"
1606		savedstyleparams[s] = getstyleparam(s)
1607	}
1608
1609	bitethethumbs()
1610
1611	mux := mux.NewRouter()
1612	mux.Use(login.Checker)
1613
1614	posters := mux.Methods("POST").Subrouter()
1615	getters := mux.Methods("GET").Subrouter()
1616
1617	getters.HandleFunc("/", homepage)
1618	getters.HandleFunc("/front", homepage)
1619	getters.HandleFunc("/robots.txt", nomoroboto)
1620	getters.HandleFunc("/rss", showrss)
1621	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}", showuser)
1622	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/"+honkSep+"/{xid:[[:alnum:]]+}", showhonk)
1623	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/rss", showrss)
1624	posters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/inbox", inbox)
1625	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/outbox", outbox)
1626	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/followers", emptiness)
1627	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/following", emptiness)
1628	getters.HandleFunc("/a", avatate)
1629	getters.HandleFunc("/o/{name:[a-z0-9-]+}", showontology)
1630	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1631	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1632	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1633	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1634
1635	getters.HandleFunc("/style.css", servecss)
1636	getters.HandleFunc("/local.css", servecss)
1637	getters.HandleFunc("/about", servehtml)
1638	getters.HandleFunc("/login", servehtml)
1639	posters.HandleFunc("/dologin", login.LoginFunc)
1640	getters.HandleFunc("/logout", login.LogoutFunc)
1641
1642	loggedin := mux.NewRoute().Subrouter()
1643	loggedin.Use(login.Required)
1644	loggedin.HandleFunc("/account", accountpage)
1645	loggedin.HandleFunc("/funzone", showfunzone)
1646	loggedin.HandleFunc("/chpass", dochpass)
1647	loggedin.HandleFunc("/atme", homepage)
1648	loggedin.HandleFunc("/zonkzone", zonkzone)
1649	loggedin.HandleFunc("/xzone", xzone)
1650	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1651	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1652	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1653	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1654	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1655	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1656	loggedin.HandleFunc("/honkers", showhonkers)
1657	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1658	loggedin.HandleFunc("/h", showhonker)
1659	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1660	loggedin.HandleFunc("/c", showcombos)
1661	loggedin.HandleFunc("/t", showconvoy)
1662	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1663
1664	err = http.Serve(listener, mux)
1665	if err != nil {
1666		log.Fatal(err)
1667	}
1668}
1669
1670func cleanupdb(arg string) {
1671	db := opendatabase()
1672	days, err := strconv.Atoi(arg)
1673	if err != nil {
1674		honker := arg
1675		expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1676		doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1677		doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1678	} else {
1679		expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1680		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)
1681		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)
1682	}
1683	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1684	for _, u := range allusers() {
1685		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)
1686	}
1687}
1688
1689var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1690var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1691var stmtHonksByOntology, stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1692var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1693var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1694var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1695var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1696var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1697var stmtSaveOnts, stmtUpdateFlags, stmtClearFlags *sql.Stmt
1698
1699func preparetodie(db *sql.DB, s string) *sql.Stmt {
1700	stmt, err := db.Prepare(s)
1701	if err != nil {
1702		log.Fatalf("error %s: %s", err, s)
1703	}
1704	return stmt
1705}
1706
1707func prepareStatements(db *sql.DB) {
1708	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")
1709	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1710	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1711	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1712	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1713	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1714
1715	selecthonks := "select honks.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 "
1716	limit := " order by honks.honkid desc limit 250"
1717	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1718	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1719	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1720	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1721	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)
1722	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1723	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on (honkers.xid = honks.honker or honkers.xid = honks.oonker) where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1724	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1725	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1726	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1727	stmtHonksByOntology = preparetodie(db, selecthonks+"join onts on honks.honkid = onts.honkid where onts.ontology = ? and honks.whofore = 2"+limit)
1728
1729	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags, onts) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1730	stmtSaveOnts = preparetodie(db, "insert into onts (ontology, honkid), values (?, ?)")
1731	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1732	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1733	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1734	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1735	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1736	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1737	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1738	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1739	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1740	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1741	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1742	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1743	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1744	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1745	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1746	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1747	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1748	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1749	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1750	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1751	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")
1752	stmtUpdateFlags = preparetodie(db, "update honks set flags = flags | ? where xid = ? and userid = ?")
1753	stmtClearFlags = preparetodie(db, "update honks set flags = flags & ~ ? where xid = ? and userid = ?")
1754}
1755
1756func ElaborateUnitTests() {
1757}
1758
1759func main() {
1760	cmd := "run"
1761	if len(os.Args) > 1 {
1762		cmd = os.Args[1]
1763	}
1764	switch cmd {
1765	case "init":
1766		initdb()
1767	case "upgrade":
1768		upgradedb()
1769	}
1770	db := opendatabase()
1771	dbversion := 0
1772	getconfig("dbversion", &dbversion)
1773	if dbversion != myVersion {
1774		log.Fatal("incorrect database version. run upgrade.")
1775	}
1776	getconfig("servermsg", &serverMsg)
1777	getconfig("servername", &serverName)
1778	getconfig("usersep", &userSep)
1779	getconfig("honksep", &honkSep)
1780	getconfig("dnf", &donotfedafterdark)
1781	prepareStatements(db)
1782	switch cmd {
1783	case "adduser":
1784		adduser()
1785	case "cleanup":
1786		arg := "30"
1787		if len(os.Args) > 2 {
1788			arg = os.Args[2]
1789		}
1790		cleanupdb(arg)
1791	case "ping":
1792		if len(os.Args) < 4 {
1793			fmt.Printf("usage: honk ping from to\n")
1794			return
1795		}
1796		name := os.Args[2]
1797		targ := os.Args[3]
1798		user, err := butwhatabout(name)
1799		if err != nil {
1800			log.Printf("unknown user")
1801			return
1802		}
1803		ping(user, targ)
1804	case "peep":
1805		peeppeep()
1806	case "run":
1807		serve()
1808	case "test":
1809		ElaborateUnitTests()
1810	default:
1811		log.Fatal("unknown command")
1812	}
1813}