all repos — honk @ v0.2.0

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