all repos — honk @ e594b995656218c32baa1a3265a6b82351647a4f

my fork of honk

honk.go (view raw)

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