all repos — honk @ f03619373c3f096875c7d7b2274865d9994707ad

my fork of honk

honk.go (view raw)

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