all repos — honk @ 025dc2ebd055e599219745e9b3666d6f596675ee

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