all repos — honk @ 6efd4cb5d646e926f486e660ec1e59120fb64bfe

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