all repos — honk @ 98663d9505df390e6a90cfcaedcff22e3be6699e

my fork of honk

honk.go (view raw)

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