all repos — honk @ 98d43d7a370a4302641cd69fcfa0a885c69fd0c7

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	"crypto/rand"
  21	"crypto/rsa"
  22	"database/sql"
  23	"fmt"
  24	"html"
  25	"html/template"
  26	"image"
  27	_ "image/gif"
  28	_ "image/jpeg"
  29	_ "image/png"
  30	"io"
  31	"log"
  32	notrand "math/rand"
  33	"net/http"
  34	"os"
  35	"sort"
  36	"strconv"
  37	"strings"
  38	"sync"
  39	"time"
  40
  41	"github.com/gorilla/mux"
  42)
  43
  44type UserInfo struct {
  45	UserID   int64
  46	Username string
  47}
  48
  49type WhatAbout struct {
  50	ID      int64
  51	Name    string
  52	Display string
  53	About   string
  54	Key     string
  55	URL     string
  56}
  57
  58type Honk struct {
  59	ID       int64
  60	UserID   int64
  61	Username string
  62	What     string
  63	Honker   string
  64	XID      string
  65	RID      string
  66	Date     time.Time
  67	URL      string
  68	Noise    string
  69	Convoy   string
  70	Audience []string
  71	HTML     template.HTML
  72	Donks    []*Donk
  73}
  74
  75type Donk struct {
  76	FileID  int64
  77	XID     string
  78	Name    string
  79	URL     string
  80	Media   string
  81	Content []byte
  82}
  83
  84type Honker struct {
  85	ID     int64
  86	UserID int64
  87	Name   string
  88	XID    string
  89	Flavor string
  90	Combos []string
  91}
  92
  93var serverName string
  94var iconName = "icon.png"
  95
  96var readviews *Template
  97
  98func getInfo(r *http.Request) map[string]interface{} {
  99	templinfo := make(map[string]interface{})
 100	templinfo["StyleParam"] = getstyleparam("views/style.css")
 101	templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
 102	templinfo["ServerName"] = serverName
 103	templinfo["IconName"] = iconName
 104	templinfo["UserInfo"] = GetUserInfo(r)
 105	templinfo["LogoutCSRF"] = GetCSRF("logout", r)
 106	return templinfo
 107}
 108
 109func homepage(w http.ResponseWriter, r *http.Request) {
 110	templinfo := getInfo(r)
 111	u := GetUserInfo(r)
 112	var honks []*Honk
 113	if u != nil {
 114		if r.URL.Path == "/atme" {
 115			honks = gethonksforme(u.UserID)
 116		} else {
 117			honks = gethonksforuser(u.UserID)
 118		}
 119		templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
 120	} else {
 121		honks = getpublichonks()
 122	}
 123	sort.Slice(honks, func(i, j int) bool {
 124		return honks[i].Date.After(honks[j].Date)
 125	})
 126
 127	var modtime time.Time
 128	if len(honks) > 0 {
 129		modtime = honks[0].Date
 130	}
 131	debug := false
 132	getconfig("debug", &debug)
 133	imh := r.Header.Get("If-Modified-Since")
 134	if !debug && imh != "" && !modtime.IsZero() {
 135		ifmod, err := time.Parse(http.TimeFormat, imh)
 136		if err == nil && !modtime.After(ifmod) {
 137			w.WriteHeader(http.StatusNotModified)
 138			return
 139		}
 140	}
 141	reverbolate(honks)
 142
 143	msg := "Things happen."
 144	getconfig("servermsg", &msg)
 145	templinfo["Honks"] = honks
 146	templinfo["ShowRSS"] = true
 147	templinfo["ServerMessage"] = msg
 148	if u == nil {
 149		w.Header().Set("Cache-Control", "max-age=60")
 150	} else {
 151		w.Header().Set("Cache-Control", "max-age=0")
 152	}
 153	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 154	err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
 155	if err != nil {
 156		log.Print(err)
 157	}
 158}
 159
 160func showrss(w http.ResponseWriter, r *http.Request) {
 161	name := mux.Vars(r)["name"]
 162
 163	var honks []*Honk
 164	if name != "" {
 165		honks = gethonksbyuser(name)
 166	} else {
 167		honks = getpublichonks()
 168	}
 169	sort.Slice(honks, func(i, j int) bool {
 170		return honks[i].Date.After(honks[j].Date)
 171	})
 172	reverbolate(honks)
 173
 174	home := fmt.Sprintf("https://%s/", serverName)
 175	base := home
 176	if name != "" {
 177		home += "u/" + name
 178		name += " "
 179	}
 180	feed := RssFeed{
 181		Title:       name + "honk",
 182		Link:        home,
 183		Description: name + "honk rss",
 184		FeedImage: &RssFeedImage{
 185			URL:   base + "icon.png",
 186			Title: name + "honk rss",
 187			Link:  home,
 188		},
 189	}
 190	var modtime time.Time
 191	past := time.Now().UTC().Add(-3 * 24 * time.Hour)
 192	for _, honk := range honks {
 193		if honk.Date.Before(past) {
 194			break
 195		}
 196		desc := string(honk.HTML)
 197		for _, d := range honk.Donks {
 198			desc += fmt.Sprintf(`<p><a href="%sd/%s">Attachment: %s</a>`,
 199				base, d.XID, html.EscapeString(d.Name))
 200		}
 201
 202		feed.Items = append(feed.Items, &RssItem{
 203			Title:       fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
 204			Description: RssCData{desc},
 205			Link:        honk.URL,
 206			PubDate:     honk.Date.Format(time.RFC1123),
 207		})
 208		if honk.Date.After(modtime) {
 209			modtime = honk.Date
 210		}
 211	}
 212	w.Header().Set("Cache-Control", "max-age=300")
 213	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 214
 215	err := feed.Write(w)
 216	if err != nil {
 217		log.Printf("error writing rss: %s", err)
 218	}
 219}
 220
 221func butwhatabout(name string) (*WhatAbout, error) {
 222	row := stmtWhatAbout.QueryRow(name)
 223	var user WhatAbout
 224	err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key)
 225	user.URL = fmt.Sprintf("https://%s/u/%s", serverName, user.Name)
 226	return &user, err
 227}
 228
 229func crappola(j map[string]interface{}) bool {
 230	t, _ := jsongetstring(j, "type")
 231	a, _ := jsongetstring(j, "actor")
 232	o, _ := jsongetstring(j, "object")
 233	if t == "Delete" && a == o {
 234		log.Printf("crappola from %s", a)
 235		return true
 236	}
 237	return false
 238}
 239
 240func ping(user *WhatAbout, who string) {
 241	box, err := getboxes(who)
 242	if err != nil {
 243		log.Printf("no inbox for ping: %s", err)
 244		return
 245	}
 246	j := NewJunk()
 247	j["@context"] = itiswhatitis
 248	j["type"] = "Ping"
 249	j["id"] = user.URL + "/ping/" + xfiltrate()
 250	j["actor"] = user.URL
 251	j["to"] = who
 252	keyname, key := ziggy(user.Name)
 253	err = PostJunk(keyname, key, box.In, j)
 254	if err != nil {
 255		log.Printf("can't send ping: %s", err)
 256		return
 257	}
 258	log.Printf("sent ping to %s: %s", who, j["id"])
 259}
 260
 261func pong(user *WhatAbout, who string, obj string) {
 262	box, err := getboxes(who)
 263	if err != nil {
 264		log.Printf("no inbox for pong %s : %s", who, err)
 265		return
 266	}
 267	j := NewJunk()
 268	j["@context"] = itiswhatitis
 269	j["type"] = "Pong"
 270	j["id"] = user.URL + "/pong/" + xfiltrate()
 271	j["actor"] = user.URL
 272	j["to"] = who
 273	j["object"] = obj
 274	keyname, key := ziggy(user.Name)
 275	err = PostJunk(keyname, key, box.In, j)
 276	if err != nil {
 277		log.Printf("can't send pong: %s", err)
 278		return
 279	}
 280}
 281
 282func inbox(w http.ResponseWriter, r *http.Request) {
 283	name := mux.Vars(r)["name"]
 284	user, err := butwhatabout(name)
 285	if err != nil {
 286		http.NotFound(w, r)
 287		return
 288	}
 289	var buf bytes.Buffer
 290	io.Copy(&buf, r.Body)
 291	payload := buf.Bytes()
 292	j, err := ReadJunk(bytes.NewReader(payload))
 293	if err != nil {
 294		log.Printf("bad payload: %s", err)
 295		io.WriteString(os.Stdout, "bad payload\n")
 296		os.Stdout.Write(payload)
 297		io.WriteString(os.Stdout, "\n")
 298		return
 299	}
 300	if crappola(j) {
 301		return
 302	}
 303	keyname, err := zag(r, payload)
 304	if err != nil {
 305		log.Printf("inbox message failed signature: %s", err)
 306		if keyname != "" {
 307			keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
 308		}
 309		if err != nil {
 310			fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 311			io.WriteString(fd, "bad signature:\n")
 312			WriteJunk(fd, j)
 313			io.WriteString(fd, "\n")
 314			fd.Close()
 315			return
 316		}
 317	}
 318	what, _ := jsongetstring(j, "type")
 319	if what == "Like" {
 320		return
 321	}
 322	who, _ := jsongetstring(j, "actor")
 323	if !keymatch(keyname, who, user.ID) {
 324		log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 325		return
 326	}
 327	if thoudostbitethythumb(user.ID, who) {
 328		log.Printf("ignoring thumb sucker %s", who)
 329		return
 330	}
 331	fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 332	WriteJunk(fd, j)
 333	io.WriteString(fd, "\n")
 334	fd.Close()
 335	switch what {
 336	case "Ping":
 337		obj, _ := jsongetstring(j, "id")
 338		log.Printf("ping from %s: %s", who, obj)
 339		pong(user, who, obj)
 340	case "Pong":
 341		obj, _ := jsongetstring(j, "object")
 342		log.Printf("pong from %s: %s", who, obj)
 343	case "Follow":
 344		log.Printf("updating honker follow: %s", who)
 345		rubadubdub(user, j)
 346	case "Accept":
 347		db := opendatabase()
 348		log.Printf("updating honker accept: %s", who)
 349		db.Exec("update honkers set flavor = 'sub' where xid = ? and flavor = 'presub'", who)
 350	case "Undo":
 351		obj, ok := jsongetmap(j, "object")
 352		if !ok {
 353			log.Printf("unknown undo no object")
 354		} else {
 355			what, _ := jsongetstring(obj, "type")
 356			switch what {
 357			case "Follow":
 358				log.Printf("updating honker undo: %s", who)
 359				db := opendatabase()
 360				db.Exec("update honkers set flavor = 'undub' where xid = ? and flavor = 'dub'", who)
 361			case "Like":
 362			default:
 363				log.Printf("unknown undo: %s", what)
 364			}
 365		}
 366	default:
 367		xonk := xonkxonk(j)
 368		if xonk != nil && needxonk(user, xonk) {
 369			xonk.UserID = user.ID
 370			savexonk(user, xonk)
 371		}
 372	}
 373}
 374
 375func outbox(w http.ResponseWriter, r *http.Request) {
 376	name := mux.Vars(r)["name"]
 377	user, err := butwhatabout(name)
 378	if err != nil {
 379		http.NotFound(w, r)
 380		return
 381	}
 382	honks := gethonksbyuser(name)
 383
 384	var jonks []map[string]interface{}
 385	for _, h := range honks {
 386		j, _ := jonkjonk(user, h)
 387		jonks = append(jonks, j)
 388	}
 389
 390	j := NewJunk()
 391	j["@context"] = itiswhatitis
 392	j["id"] = user.URL + "/outbox"
 393	j["type"] = "OrderedCollection"
 394	j["totalItems"] = len(jonks)
 395	j["orderedItems"] = jonks
 396
 397	w.Header().Set("Cache-Control", "max-age=60")
 398	w.Header().Set("Content-Type", theonetruename)
 399	WriteJunk(w, j)
 400}
 401
 402func viewuser(w http.ResponseWriter, r *http.Request) {
 403	name := mux.Vars(r)["name"]
 404	user, err := butwhatabout(name)
 405	if err != nil {
 406		http.NotFound(w, r)
 407		return
 408	}
 409	if friendorfoe(r.Header.Get("Accept")) {
 410		j := asjonker(user)
 411		w.Header().Set("Cache-Control", "max-age=600")
 412		w.Header().Set("Content-Type", theonetruename)
 413		WriteJunk(w, j)
 414		return
 415	}
 416	honks := gethonksbyuser(name)
 417	u := GetUserInfo(r)
 418	honkpage(w, r, u, user, honks, "")
 419}
 420
 421func viewhonker(w http.ResponseWriter, r *http.Request) {
 422	name := mux.Vars(r)["name"]
 423	u := GetUserInfo(r)
 424	honks := gethonksbyhonker(u.UserID, name)
 425	honkpage(w, r, u, nil, honks, "honks by honker: " + name)
 426}
 427
 428func viewcombo(w http.ResponseWriter, r *http.Request) {
 429	name := mux.Vars(r)["name"]
 430	u := GetUserInfo(r)
 431	honks := gethonksbycombo(u.UserID, name)
 432	honkpage(w, r, u, nil, honks, "honks by combo: " + name)
 433}
 434func viewconvoy(w http.ResponseWriter, r *http.Request) {
 435	c := r.FormValue("c")
 436	var userid int64 = -1
 437	u := GetUserInfo(r)
 438	if u != nil {
 439		userid = u.UserID
 440	}
 441	honks := gethonksbyconvoy(userid, c)
 442	honkpage(w, r, u, nil, honks, "honks in convoy: " + c)
 443}
 444
 445func fingerlicker(w http.ResponseWriter, r *http.Request) {
 446	orig := r.FormValue("resource")
 447
 448	log.Printf("finger lick: %s", orig)
 449
 450	if strings.HasPrefix(orig, "acct:") {
 451		orig = orig[5:]
 452	}
 453
 454	name := orig
 455	idx := strings.LastIndexByte(name, '/')
 456	if idx != -1 {
 457		name = name[idx+1:]
 458		if "https://"+serverName+"/u/"+name != orig {
 459			log.Printf("foreign request rejected")
 460			name = ""
 461		}
 462	} else {
 463		idx = strings.IndexByte(name, '@')
 464		if idx != -1 {
 465			name = name[:idx]
 466			if name+"@"+serverName != orig {
 467				log.Printf("foreign request rejected")
 468				name = ""
 469			}
 470		}
 471	}
 472	user, err := butwhatabout(name)
 473	if err != nil {
 474		http.NotFound(w, r)
 475		return
 476	}
 477
 478	j := NewJunk()
 479	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
 480	j["aliases"] = []string{user.URL}
 481	var links []map[string]interface{}
 482	l := NewJunk()
 483	l["rel"] = "self"
 484	l["type"] = `application/activity+json`
 485	l["href"] = user.URL
 486	links = append(links, l)
 487	j["links"] = links
 488
 489	w.Header().Set("Cache-Control", "max-age=3600")
 490	w.Header().Set("Content-Type", "application/jrd+json")
 491	WriteJunk(w, j)
 492}
 493
 494func viewhonk(w http.ResponseWriter, r *http.Request) {
 495	name := mux.Vars(r)["name"]
 496	xid := mux.Vars(r)["xid"]
 497	user, err := butwhatabout(name)
 498	if err != nil {
 499		http.NotFound(w, r)
 500		return
 501	}
 502	h := getxonk(name, xid)
 503	if h == nil {
 504		http.NotFound(w, r)
 505		return
 506	}
 507	if friendorfoe(r.Header.Get("Accept")) {
 508		_, j := jonkjonk(user, h)
 509		j["@context"] = itiswhatitis
 510		w.Header().Set("Cache-Control", "max-age=3600")
 511		w.Header().Set("Content-Type", theonetruename)
 512		WriteJunk(w, j)
 513		return
 514	}
 515	u := GetUserInfo(r)
 516	honkpage(w, r, u, nil, []*Honk{h}, "one honk")
 517}
 518
 519func honkpage(w http.ResponseWriter, r *http.Request, u *UserInfo, user *WhatAbout, honks []*Honk, infomsg string) {
 520	reverbolate(honks)
 521	templinfo := getInfo(r)
 522	if u != nil {
 523		if user != nil && u.Username == user.Name {
 524			templinfo["UserCSRF"] = GetCSRF("saveuser", r)
 525		}
 526		templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
 527	}
 528	if u == nil {
 529		w.Header().Set("Cache-Control", "max-age=60")
 530	}
 531	if user != nil {
 532		templinfo["Name"] = user.Name
 533		whatabout := user.About
 534		templinfo["RawWhatAbout"] = whatabout
 535		whatabout = obfusbreak(whatabout)
 536		templinfo["WhatAbout"] = cleanstring(whatabout)
 537	}
 538	templinfo["Honks"] = honks
 539	templinfo["ServerMessage"] = infomsg
 540	err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
 541	if err != nil {
 542		log.Print(err)
 543	}
 544}
 545
 546func saveuser(w http.ResponseWriter, r *http.Request) {
 547	whatabout := r.FormValue("whatabout")
 548	u := GetUserInfo(r)
 549	db := opendatabase()
 550	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 551	if err != nil {
 552		log.Printf("error bouting what: %s", err)
 553	}
 554
 555	http.Redirect(w, r, "/u/"+u.Username, http.StatusSeeOther)
 556}
 557
 558func gethonkers(userid int64) []*Honker {
 559	rows, err := stmtHonkers.Query(userid)
 560	if err != nil {
 561		log.Printf("error querying honkers: %s", err)
 562		return nil
 563	}
 564	defer rows.Close()
 565	var honkers []*Honker
 566	for rows.Next() {
 567		var f Honker
 568		var combos string
 569		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 570		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 571		if err != nil {
 572			log.Printf("error scanning honker: %s", err)
 573			return nil
 574		}
 575		honkers = append(honkers, &f)
 576	}
 577	return honkers
 578}
 579
 580func getdubs(userid int64) []*Honker {
 581	rows, err := stmtDubbers.Query(userid)
 582	if err != nil {
 583		log.Printf("error querying dubs: %s", err)
 584		return nil
 585	}
 586	defer rows.Close()
 587	var honkers []*Honker
 588	for rows.Next() {
 589		var f Honker
 590		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 591		if err != nil {
 592			log.Printf("error scanning honker: %s", err)
 593			return nil
 594		}
 595		honkers = append(honkers, &f)
 596	}
 597	return honkers
 598}
 599
 600func getxonk(name, xid string) *Honk {
 601	var h Honk
 602	var dt, aud string
 603	row := stmtOneXonk.QueryRow(xid)
 604	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
 605		&dt, &h.URL, &aud, &h.Noise, &h.Convoy)
 606	if err != nil {
 607		if err != sql.ErrNoRows {
 608			log.Printf("error scanning xonk: %s", err)
 609		}
 610		return nil
 611	}
 612	if name != "" && h.Username != name {
 613		log.Printf("user xonk mismatch")
 614		return nil
 615	}
 616	h.Date, _ = time.Parse(dbtimeformat, dt)
 617	h.Audience = strings.Split(aud, " ")
 618	donksforhonks([]*Honk{&h})
 619	return &h
 620}
 621
 622func getpublichonks() []*Honk {
 623	rows, err := stmtPublicHonks.Query()
 624	return getsomehonks(rows, err)
 625}
 626func gethonksbyuser(name string) []*Honk {
 627	rows, err := stmtUserHonks.Query(name)
 628	return getsomehonks(rows, err)
 629}
 630func gethonksforuser(userid int64) []*Honk {
 631	dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
 632	rows, err := stmtHonksForUser.Query(userid, dt.Format(dbtimeformat), userid)
 633	return getsomehonks(rows, err)
 634}
 635func gethonksforme(userid int64) []*Honk {
 636	dt := time.Now().UTC().Add(-4 * 24 * time.Hour)
 637	rows, err := stmtHonksForMe.Query(userid, dt.Format(dbtimeformat), userid)
 638	return getsomehonks(rows, err)
 639}
 640func gethonksbyhonker(userid int64, honker string) []*Honk {
 641	rows, err := stmtHonksByHonker.Query(userid, honker)
 642	return getsomehonks(rows, err)
 643}
 644func gethonksbycombo(userid int64, combo string) []*Honk {
 645	combo = "% " + combo + " %"
 646	rows, err := stmtHonksByCombo.Query(userid, combo)
 647	return getsomehonks(rows, err)
 648}
 649func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 650	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 651	return getsomehonks(rows, err)
 652}
 653
 654func getsomehonks(rows *sql.Rows, err error) []*Honk {
 655	if err != nil {
 656		log.Printf("error querying honks: %s", err)
 657		return nil
 658	}
 659	defer rows.Close()
 660	var honks []*Honk
 661	for rows.Next() {
 662		var h Honk
 663		var dt, aud string
 664		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
 665			&dt, &h.URL, &aud, &h.Noise, &h.Convoy)
 666		if err != nil {
 667			log.Printf("error scanning honks: %s", err)
 668			return nil
 669		}
 670		h.Date, _ = time.Parse(dbtimeformat, dt)
 671		h.Audience = strings.Split(aud, " ")
 672		honks = append(honks, &h)
 673	}
 674	rows.Close()
 675	donksforhonks(honks)
 676	return honks
 677}
 678
 679func donksforhonks(honks []*Honk) {
 680	db := opendatabase()
 681	var ids []string
 682	hmap := make(map[int64]*Honk)
 683	for _, h := range honks {
 684		if h.What == "zonk" {
 685			continue
 686		}
 687		ids = append(ids, fmt.Sprintf("%d", h.ID))
 688		hmap[h.ID] = h
 689	}
 690	q := fmt.Sprintf("select honkid, donks.fileid, xid, name, url, media from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
 691	rows, err := db.Query(q)
 692	if err != nil {
 693		log.Printf("error querying donks: %s", err)
 694		return
 695	}
 696	defer rows.Close()
 697	for rows.Next() {
 698		var hid int64
 699		var d Donk
 700		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
 701		if err != nil {
 702			log.Printf("error scanning donk: %s", err)
 703			continue
 704		}
 705		h := hmap[hid]
 706		h.Donks = append(h.Donks, &d)
 707	}
 708}
 709
 710func savebonk(w http.ResponseWriter, r *http.Request) {
 711	xid := r.FormValue("xid")
 712
 713	log.Printf("bonking %s", xid)
 714
 715	xonk := getxonk("", xid)
 716	if xonk == nil {
 717		return
 718	}
 719	if xonk.Honker == "" {
 720		xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
 721	}
 722	convoy := xonk.Convoy
 723
 724	userinfo := GetUserInfo(r)
 725
 726	dt := time.Now().UTC()
 727	bonk := Honk{
 728		UserID:   userinfo.UserID,
 729		Username: userinfo.Username,
 730		Honker:   xonk.Honker,
 731		What:     "bonk",
 732		XID:      xonk.XID,
 733		Date:     dt,
 734		Noise:    xonk.Noise,
 735		Convoy:   convoy,
 736		Donks:    xonk.Donks,
 737		Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
 738	}
 739
 740	user, _ := butwhatabout(userinfo.Username)
 741
 742	aud := strings.Join(bonk.Audience, " ")
 743	whofore := 0
 744	if strings.Contains(aud, user.URL) {
 745		whofore = 1
 746	}
 747	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
 748		dt.Format(dbtimeformat), "", aud, bonk.Noise, bonk.Convoy, whofore)
 749	if err != nil {
 750		log.Printf("error saving bonk: %s", err)
 751		return
 752	}
 753	bonk.ID, _ = res.LastInsertId()
 754	for _, d := range bonk.Donks {
 755		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 756		if err != nil {
 757			log.Printf("err saving donk: %s", err)
 758			return
 759		}
 760	}
 761
 762	go honkworldwide(user, &bonk)
 763
 764}
 765
 766func zonkit(w http.ResponseWriter, r *http.Request) {
 767	xid := r.FormValue("xid")
 768
 769	log.Printf("zonking %s", xid)
 770	userinfo := GetUserInfo(r)
 771	stmtZonkIt.Exec(userinfo.UserID, xid)
 772}
 773
 774func savehonk(w http.ResponseWriter, r *http.Request) {
 775	rid := r.FormValue("rid")
 776	noise := r.FormValue("noise")
 777
 778	userinfo := GetUserInfo(r)
 779
 780	dt := time.Now().UTC()
 781	xid := xfiltrate()
 782	what := "honk"
 783	if rid != "" {
 784		what = "tonk"
 785	}
 786	honk := Honk{
 787		UserID:   userinfo.UserID,
 788		Username: userinfo.Username,
 789		What:     "honk",
 790		XID:      xid,
 791		RID:      rid,
 792		Date:     dt,
 793	}
 794	if noise[0] == '@' {
 795		honk.Audience = append(grapevine(noise), thewholeworld)
 796	} else {
 797		honk.Audience = prepend(thewholeworld, grapevine(noise))
 798	}
 799	var convoy string
 800	if rid != "" {
 801		xonk := getxonk("", rid)
 802		if xonk != nil {
 803			honk.Audience = append(honk.Audience, xonk.Audience...)
 804			convoy = xonk.Convoy
 805		} else {
 806			xonkaud, c := whosthere(rid)
 807			honk.Audience = append(honk.Audience, xonkaud...)
 808			convoy = c
 809		}
 810	}
 811	if convoy == "" {
 812		convoy = "data:,electrichonkytonk-" + xfiltrate()
 813	}
 814	honk.Audience = oneofakind(honk.Audience)
 815	noise = obfusbreak(noise)
 816	honk.Noise = noise
 817	honk.Convoy = convoy
 818
 819	file, filehdr, err := r.FormFile("donk")
 820	if err == nil {
 821		var buf bytes.Buffer
 822		io.Copy(&buf, file)
 823		file.Close()
 824		data := buf.Bytes()
 825		xid := xfiltrate()
 826		var media, name string
 827		img, format, err := image.Decode(&buf)
 828		if err == nil {
 829			data, format, err = vacuumwrap(img, format)
 830			if err != nil {
 831				log.Printf("can't vacuum image: %s", err)
 832				return
 833			}
 834			media = "image/" + format
 835			if format == "jpeg" {
 836				format = "jpg"
 837			}
 838			name = xid + "." + format
 839			xid = name
 840		} else {
 841			maxsize := 100000
 842			if len(data) > maxsize {
 843				log.Printf("bad image: %s too much text: %d", err, len(data))
 844				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 845				return
 846			}
 847			for i := 0; i < len(data); i++ {
 848				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 849					log.Printf("bad image: %s not text: %d", err, data[i])
 850					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 851					return
 852				}
 853			}
 854			media = "text/plain"
 855			name = filehdr.Filename
 856			if name == "" {
 857				name = xid + ".txt"
 858			}
 859			xid += ".txt"
 860		}
 861		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 862		res, err := stmtSaveFile.Exec(xid, name, url, media, data)
 863		if err != nil {
 864			log.Printf("unable to save image: %s", err)
 865			return
 866		}
 867		var d Donk
 868		d.FileID, _ = res.LastInsertId()
 869		d.XID = name
 870		d.Name = name
 871		d.Media = media
 872		d.URL = url
 873		honk.Donks = append(honk.Donks, &d)
 874	}
 875	herd := herdofemus(honk.Noise)
 876	for _, e := range herd {
 877		donk := savedonk(e.ID, e.Name, "image/png")
 878		if donk != nil {
 879			donk.Name = e.Name
 880			honk.Donks = append(honk.Donks, donk)
 881		}
 882	}
 883
 884	user, _ := butwhatabout(userinfo.Username)
 885
 886	aud := strings.Join(honk.Audience, " ")
 887	whofore := 0
 888	if strings.Contains(aud, user.URL) {
 889		whofore = 1
 890	}
 891	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
 892		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore)
 893	if err != nil {
 894		log.Printf("error saving honk: %s", err)
 895		return
 896	}
 897	honk.ID, _ = res.LastInsertId()
 898	for _, d := range honk.Donks {
 899		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 900		if err != nil {
 901			log.Printf("err saving donk: %s", err)
 902			return
 903		}
 904	}
 905
 906	go honkworldwide(user, &honk)
 907
 908	http.Redirect(w, r, "/", http.StatusSeeOther)
 909}
 910
 911func viewhonkers(w http.ResponseWriter, r *http.Request) {
 912	userinfo := GetUserInfo(r)
 913	templinfo := getInfo(r)
 914	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 915	templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
 916	err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
 917	if err != nil {
 918		log.Print(err)
 919	}
 920}
 921
 922var handfull = make(map[string]string)
 923var handlock sync.Mutex
 924
 925func gofish(name string) string {
 926	if name[0] == '@' {
 927		name = name[1:]
 928	}
 929	m := strings.Split(name, "@")
 930	if len(m) != 2 {
 931		log.Printf("bad fish name: %s", name)
 932		return ""
 933	}
 934	handlock.Lock()
 935	ref, ok := handfull[name]
 936	handlock.Unlock()
 937	if ok {
 938		return ref
 939	}
 940	j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
 941	handlock.Lock()
 942	defer handlock.Unlock()
 943	if err != nil {
 944		log.Printf("failed to go fish %s: %s", name, err)
 945		handfull[name] = ""
 946		return ""
 947	}
 948	links, _ := jsongetarray(j, "links")
 949	for _, l := range links {
 950		href, _ := jsongetstring(l, "href")
 951		rel, _ := jsongetstring(l, "rel")
 952		t, _ := jsongetstring(l, "type")
 953		if rel == "self" && friendorfoe(t) {
 954			handfull[name] = href
 955			return href
 956		}
 957	}
 958	handfull[name] = ""
 959	return ""
 960}
 961
 962func savehonker(w http.ResponseWriter, r *http.Request) {
 963	u := GetUserInfo(r)
 964	name := r.FormValue("name")
 965	url := r.FormValue("url")
 966	peep := r.FormValue("peep")
 967	combos := r.FormValue("combos")
 968	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
 969
 970	if honkerid > 0 {
 971		combos = " " + strings.TrimSpace(combos) + " "
 972		_, err := stmtUpdateHonker.Exec(combos, honkerid, u.UserID)
 973		if err != nil {
 974			log.Printf("update honker err: %s", err)
 975			return
 976		}
 977		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
 978	}
 979
 980	flavor := "presub"
 981	if peep == "peep" {
 982		flavor = "peep"
 983	}
 984	if url == "" {
 985		return
 986	}
 987	if url[0] == '@' {
 988		url = gofish(url)
 989	}
 990	if url == "" {
 991		return
 992	}
 993	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
 994	if err != nil {
 995		log.Print(err)
 996		return
 997	}
 998	if flavor == "presub" {
 999		user, _ := butwhatabout(u.Username)
1000		go subsub(user, url)
1001	}
1002	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1003}
1004
1005type Zonker struct {
1006	Name      string
1007	Wherefore string
1008}
1009
1010func killzone(w http.ResponseWriter, r *http.Request) {
1011	db := opendatabase()
1012	userinfo := GetUserInfo(r)
1013	rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
1014	if err != nil {
1015		log.Printf("err: %s", err)
1016		return
1017	}
1018	var zonkers []Zonker
1019	for rows.Next() {
1020		var z Zonker
1021		rows.Scan(&z.Name, &z.Wherefore)
1022		zonkers = append(zonkers, z)
1023	}
1024	templinfo := getInfo(r)
1025	templinfo["Zonkers"] = zonkers
1026	templinfo["KillCSRF"] = GetCSRF("killitwithfire", r)
1027	err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
1028	if err != nil {
1029		log.Print(err)
1030	}
1031}
1032
1033func killitwithfire(w http.ResponseWriter, r *http.Request) {
1034	userinfo := GetUserInfo(r)
1035	wherefore := r.FormValue("wherefore")
1036	name := r.FormValue("name")
1037	if name == "" {
1038		return
1039	}
1040	switch wherefore {
1041	case "zonker":
1042	case "zurl":
1043	case "zonvoy":
1044	default:
1045		return
1046	}
1047	db := opendatabase()
1048	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1049		userinfo.UserID, name, wherefore)
1050
1051	http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1052}
1053
1054func somedays() string {
1055	secs := 432000 + notrand.Int63n(432000)
1056	return fmt.Sprintf("%d", secs)
1057}
1058
1059func avatate(w http.ResponseWriter, r *http.Request) {
1060	n := r.FormValue("a")
1061	a := avatar(n)
1062	w.Header().Set("Cache-Control", "max-age="+somedays())
1063	w.Write(a)
1064}
1065
1066func servecss(w http.ResponseWriter, r *http.Request) {
1067	w.Header().Set("Cache-Control", "max-age=7776000")
1068	http.ServeFile(w, r, "views"+r.URL.Path)
1069}
1070func servehtml(w http.ResponseWriter, r *http.Request) {
1071	templinfo := getInfo(r)
1072	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1073	if err != nil {
1074		log.Print(err)
1075	}
1076}
1077func serveemu(w http.ResponseWriter, r *http.Request) {
1078	xid := mux.Vars(r)["xid"]
1079	w.Header().Set("Cache-Control", "max-age="+somedays())
1080	http.ServeFile(w, r, "emus/"+xid)
1081}
1082
1083func servefile(w http.ResponseWriter, r *http.Request) {
1084	xid := mux.Vars(r)["xid"]
1085	row := stmtFileData.QueryRow(xid)
1086	var media string
1087	var data []byte
1088	err := row.Scan(&media, &data)
1089	if err != nil {
1090		log.Printf("error loading file: %s", err)
1091		http.NotFound(w, r)
1092		return
1093	}
1094	w.Header().Set("Content-Type", media)
1095	w.Header().Set("X-Content-Type-Options", "nosniff")
1096	w.Header().Set("Cache-Control", "max-age="+somedays())
1097	w.Write(data)
1098}
1099
1100func serve() {
1101	db := opendatabase()
1102	LoginInit(db)
1103
1104	listener, err := openListener()
1105	if err != nil {
1106		log.Fatal(err)
1107	}
1108	go redeliverator()
1109
1110	debug := false
1111	getconfig("debug", &debug)
1112	readviews = ParseTemplates(debug,
1113		"views/honkpage.html",
1114		"views/honkers.html",
1115		"views/zonkers.html",
1116		"views/honkform.html",
1117		"views/honk.html",
1118		"views/login.html",
1119		"views/header.html",
1120	)
1121	if !debug {
1122		s := "views/style.css"
1123		savedstyleparams[s] = getstyleparam(s)
1124		s = "views/local.css"
1125		savedstyleparams[s] = getstyleparam(s)
1126	}
1127
1128	mux := mux.NewRouter()
1129	mux.Use(LoginChecker)
1130
1131	posters := mux.Methods("POST").Subrouter()
1132	getters := mux.Methods("GET").Subrouter()
1133
1134	getters.HandleFunc("/", homepage)
1135	getters.HandleFunc("/rss", showrss)
1136	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1137	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1138	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1139	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1140	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1141	getters.HandleFunc("/a", avatate)
1142	getters.HandleFunc("/t", viewconvoy)
1143	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1144	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1145	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1146
1147	getters.HandleFunc("/style.css", servecss)
1148	getters.HandleFunc("/local.css", servecss)
1149	getters.HandleFunc("/login", servehtml)
1150	posters.HandleFunc("/dologin", dologin)
1151	getters.HandleFunc("/logout", dologout)
1152
1153	loggedin := mux.NewRoute().Subrouter()
1154	loggedin.Use(LoginRequired)
1155	loggedin.HandleFunc("/atme", homepage)
1156	loggedin.HandleFunc("/killzone", killzone)
1157	loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1158	loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1159	loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1160	loggedin.Handle("/killitwithfire", CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1161	loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1162	loggedin.HandleFunc("/honkers", viewhonkers)
1163	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1164	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", viewcombo)
1165	loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1166
1167	err = http.Serve(listener, mux)
1168	if err != nil {
1169		log.Fatal(err)
1170	}
1171}
1172
1173var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateHonker *sql.Stmt
1174var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1175var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1176var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1177var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1178var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1179var stmtHasHonker, stmtThumbBiter, stmtZonkIt *sql.Stmt
1180
1181func preparetodie(db *sql.DB, s string) *sql.Stmt {
1182	stmt, err := db.Prepare(s)
1183	if err != nil {
1184		log.Fatalf("error %s: %s", err, s)
1185	}
1186	return stmt
1187}
1188
1189func prepareStatements(db *sql.DB) {
1190	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1191	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1192	stmtUpdateHonker = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1193	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1194	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1195
1196	selecthonks := "select honkid, honks.userid, username, what, honker, honks.xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid "
1197	limit := " order by honkid desc limit "
1198	stmtOneXonk = preparetodie(db, selecthonks+"where xid = ?")
1199	stmtPublicHonks = preparetodie(db, selecthonks+"where honker = ''"+limit+"50")
1200	stmtUserHonks = preparetodie(db, selecthonks+"where honker = '' and username = ?"+limit+"50")
1201	stmtHonksForUser = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"+limit+"250")
1202	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1 and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"+limit+"150")
1203	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+limit+"50")
1204	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+limit+"50")
1205	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or honker = '') and convoy = ?"+limit+"50")
1206
1207	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1208	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1209	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1210	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1211	stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1212	stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1213	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1214	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1215	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1216	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1217	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1218	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1219	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1220	stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1221	stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1222}
1223
1224func ElaborateUnitTests() {
1225}
1226
1227func finishusersetup() error {
1228	db := opendatabase()
1229	k, err := rsa.GenerateKey(rand.Reader, 2048)
1230	if err != nil {
1231		return err
1232	}
1233	pubkey, err := zem(&k.PublicKey)
1234	if err != nil {
1235		return err
1236	}
1237	seckey, err := zem(k)
1238	if err != nil {
1239		return err
1240	}
1241	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1242	if err != nil {
1243		return err
1244	}
1245	return nil
1246}
1247
1248func main() {
1249	cmd := "run"
1250	if len(os.Args) > 1 {
1251		cmd = os.Args[1]
1252	}
1253	switch cmd {
1254	case "init":
1255		initdb()
1256	case "upgrade":
1257		upgradedb()
1258	}
1259	db := opendatabase()
1260	dbversion := 0
1261	getconfig("dbversion", &dbversion)
1262	if dbversion != myVersion {
1263		log.Fatal("incorrect database version. run upgrade.")
1264	}
1265	getconfig("servername", &serverName)
1266	prepareStatements(db)
1267	switch cmd {
1268	case "ping":
1269		if len(os.Args) < 4 {
1270			fmt.Printf("usage: honk ping from to\n")
1271			return
1272		}
1273		name := os.Args[2]
1274		targ := os.Args[3]
1275		user, err := butwhatabout(name)
1276		if err != nil {
1277			log.Printf("unknown user")
1278			return
1279		}
1280		ping(user, targ)
1281	case "peep":
1282		peeppeep()
1283	case "run":
1284		serve()
1285	case "test":
1286		ElaborateUnitTests()
1287	default:
1288		log.Fatal("unknown command")
1289	}
1290}