all repos — honk @ 683d6da2fd61c7acecbf040421275464c1fc7858

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