all repos — honk @ 405e1c153dcf38cd85a1df0b611f0c3afb2355a9

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