all repos — honk @ f0517822b4985b269f9a8e108cfbda6dfc620831

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