all repos — honk @ 2928641390c96b82fc1a934377f352cf68b637c5

my fork of honk

honk.go (view raw)

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