all repos — honk @ 0bda578a55ec88bada36f87992552900d6358ce1

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	case "Like":
 337		break
 338	default:
 339		xonk := xonkxonk(j)
 340		if xonk != nil && needxonk(user, xonk) {
 341			xonk.UserID = user.ID
 342			savexonk(xonk)
 343		}
 344	}
 345}
 346
 347func outbox(w http.ResponseWriter, r *http.Request) {
 348	name := mux.Vars(r)["name"]
 349	user, err := butwhatabout(name)
 350	if err != nil {
 351		http.NotFound(w, r)
 352		return
 353	}
 354	honks := gethonksbyuser(name)
 355
 356	var jonks []map[string]interface{}
 357	for _, h := range honks {
 358		j, _ := jonkjonk(user, h)
 359		jonks = append(jonks, j)
 360	}
 361
 362	j := NewJunk()
 363	j["@context"] = itiswhatitis
 364	j["id"] = user.URL + "/outbox"
 365	j["type"] = "OrderedCollection"
 366	j["totalItems"] = len(jonks)
 367	j["orderedItems"] = jonks
 368
 369	w.Header().Set("Content-Type", theonetruename)
 370	WriteJunk(w, j)
 371}
 372
 373func viewuser(w http.ResponseWriter, r *http.Request) {
 374	name := mux.Vars(r)["name"]
 375	user, err := butwhatabout(name)
 376	if err != nil {
 377		http.NotFound(w, r)
 378		return
 379	}
 380	if friendorfoe(r.Header.Get("Accept")) {
 381		j := asjonker(user)
 382		w.Header().Set("Content-Type", theonetruename)
 383		WriteJunk(w, j)
 384		return
 385	}
 386	honks := gethonksbyuser(name)
 387	u := GetUserInfo(r)
 388	honkpage(w, r, u, user, honks)
 389}
 390
 391func viewhonker(w http.ResponseWriter, r *http.Request) {
 392	name := mux.Vars(r)["name"]
 393	u := GetUserInfo(r)
 394	honks := gethonksbyhonker(u.UserID, name)
 395	honkpage(w, r, nil, nil, honks)
 396}
 397
 398func fingerlicker(w http.ResponseWriter, r *http.Request) {
 399	orig := r.FormValue("resource")
 400
 401	log.Printf("finger lick: %s", orig)
 402
 403	if strings.HasPrefix(orig, "acct:") {
 404		orig = orig[5:]
 405	}
 406
 407	name := orig
 408	idx := strings.LastIndexByte(name, '/')
 409	if idx != -1 {
 410		name = name[idx+1:]
 411		if "https://"+serverName+"/u/"+name != orig {
 412			log.Printf("foreign request rejected")
 413			name = ""
 414		}
 415	} else {
 416		idx = strings.IndexByte(name, '@')
 417		if idx != -1 {
 418			name = name[:idx]
 419			if name+"@"+serverName != orig {
 420				log.Printf("foreign request rejected")
 421				name = ""
 422			}
 423		}
 424	}
 425	user, err := butwhatabout(name)
 426	if err != nil {
 427		http.NotFound(w, r)
 428		return
 429	}
 430
 431	j := NewJunk()
 432	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
 433	j["aliases"] = []string{user.URL}
 434	var links []map[string]interface{}
 435	l := NewJunk()
 436	l["rel"] = "self"
 437	l["type"] = `application/activity+json`
 438	l["href"] = user.URL
 439	links = append(links, l)
 440	j["links"] = links
 441
 442	w.Header().Set("Content-Type", "application/jrd+json")
 443	WriteJunk(w, j)
 444}
 445
 446func viewhonk(w http.ResponseWriter, r *http.Request) {
 447	name := mux.Vars(r)["name"]
 448	xid := mux.Vars(r)["xid"]
 449	user, err := butwhatabout(name)
 450	if err != nil {
 451		http.NotFound(w, r)
 452		return
 453	}
 454	h := getxonk(name, xid)
 455	if h == nil {
 456		http.NotFound(w, r)
 457		return
 458	}
 459	if friendorfoe(r.Header.Get("Accept")) {
 460		_, j := jonkjonk(user, h)
 461		j["@context"] = itiswhatitis
 462		w.Header().Set("Content-Type", theonetruename)
 463		WriteJunk(w, j)
 464		return
 465	}
 466	honkpage(w, r, nil, nil, []*Honk{h})
 467}
 468
 469func honkpage(w http.ResponseWriter, r *http.Request, u *UserInfo, user *WhatAbout, honks []*Honk) {
 470	reverbolate(honks)
 471	templinfo := getInfo(r)
 472	if u != nil && u.Username == user.Name {
 473		templinfo["UserCSRF"] = GetCSRF("saveuser", r)
 474		templinfo["HonkCSRF"] = GetCSRF("honkhonk", r)
 475	}
 476	if user != nil {
 477		templinfo["Name"] = user.Name
 478		whatabout := user.About
 479		templinfo["RawWhatAbout"] = whatabout
 480		whatabout = obfusbreak(whatabout)
 481		templinfo["WhatAbout"] = cleanstring(whatabout)
 482	}
 483	templinfo["Honks"] = honks
 484	err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
 485	if err != nil {
 486		log.Print(err)
 487	}
 488}
 489
 490func saveuser(w http.ResponseWriter, r *http.Request) {
 491	whatabout := r.FormValue("whatabout")
 492	u := GetUserInfo(r)
 493	db := opendatabase()
 494	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 495	if err != nil {
 496		log.Printf("error bouting what: %s", err)
 497	}
 498
 499	http.Redirect(w, r, "/u/"+u.Username, http.StatusSeeOther)
 500}
 501
 502func gethonkers(userid int64) []*Honker {
 503	rows, err := stmtHonkers.Query(userid)
 504	if err != nil {
 505		log.Printf("error querying honkers: %s", err)
 506		return nil
 507	}
 508	defer rows.Close()
 509	var honkers []*Honker
 510	for rows.Next() {
 511		var f Honker
 512		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 513		if err != nil {
 514			log.Printf("error scanning honker: %s", err)
 515			return nil
 516		}
 517		honkers = append(honkers, &f)
 518	}
 519	return honkers
 520}
 521
 522func getdubs(userid int64) []*Honker {
 523	rows, err := stmtDubbers.Query(userid)
 524	if err != nil {
 525		log.Printf("error querying dubs: %s", err)
 526		return nil
 527	}
 528	defer rows.Close()
 529	var honkers []*Honker
 530	for rows.Next() {
 531		var f Honker
 532		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 533		if err != nil {
 534			log.Printf("error scanning honker: %s", err)
 535			return nil
 536		}
 537		honkers = append(honkers, &f)
 538	}
 539	return honkers
 540}
 541
 542func getxonk(name, xid string) *Honk {
 543	var h Honk
 544	var dt, aud string
 545	row := stmtOneXonk.QueryRow(xid)
 546	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
 547		&dt, &h.URL, &aud, &h.Noise)
 548	if err != nil {
 549		log.Printf("error scanning xonk: %s", err)
 550		return nil
 551	}
 552	if name != "" && h.Username != name {
 553		log.Printf("user xonk mismatch")
 554		return nil
 555	}
 556	h.Date, _ = time.Parse(dbtimeformat, dt)
 557	h.Audience = strings.Split(aud, " ")
 558	donksforhonks([]*Honk{&h})
 559	return &h
 560}
 561
 562func gethonks() []*Honk {
 563	rows, err := stmtHonks.Query()
 564	return getsomehonks(rows, err)
 565}
 566func gethonksbyuser(name string) []*Honk {
 567	rows, err := stmtUserHonks.Query(name)
 568	return getsomehonks(rows, err)
 569}
 570func gethonksforuser(userid int64) []*Honk {
 571	dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
 572	rows, err := stmtHonksForUser.Query(userid, dt.Format(dbtimeformat))
 573	return getsomehonks(rows, err)
 574}
 575func gethonksbyhonker(userid int64, honker string) []*Honk {
 576	rows, err := stmtHonksByHonker.Query(userid, honker)
 577	return getsomehonks(rows, err)
 578}
 579
 580func getsomehonks(rows *sql.Rows, err error) []*Honk {
 581	if err != nil {
 582		log.Printf("error querying honks: %s", err)
 583		return nil
 584	}
 585	defer rows.Close()
 586	var honks []*Honk
 587	for rows.Next() {
 588		var h Honk
 589		var dt, aud string
 590		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
 591			&dt, &h.URL, &aud, &h.Noise)
 592		if err != nil {
 593			log.Printf("error scanning honks: %s", err)
 594			return nil
 595		}
 596		h.Date, _ = time.Parse(dbtimeformat, dt)
 597		h.Audience = strings.Split(aud, " ")
 598		honks = append(honks, &h)
 599	}
 600	rows.Close()
 601	donksforhonks(honks)
 602	return honks
 603}
 604
 605func donksforhonks(honks []*Honk) {
 606	db := opendatabase()
 607	var ids []string
 608	hmap := make(map[int64]*Honk)
 609	for _, h := range honks {
 610		ids = append(ids, fmt.Sprintf("%d", h.ID))
 611		hmap[h.ID] = h
 612	}
 613	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, ","))
 614	rows, err := db.Query(q)
 615	if err != nil {
 616		log.Printf("error querying donks: %s", err)
 617		return
 618	}
 619	defer rows.Close()
 620	for rows.Next() {
 621		var hid int64
 622		var d Donk
 623		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
 624		if err != nil {
 625			log.Printf("error scanning donk: %s", err)
 626			continue
 627		}
 628		h := hmap[hid]
 629		h.Donks = append(h.Donks, &d)
 630	}
 631}
 632
 633func savebonk(w http.ResponseWriter, r *http.Request) {
 634	xid := r.FormValue("xid")
 635
 636	log.Printf("bonking %s", xid)
 637
 638	xonk := getxonk("", xid)
 639	if xonk == nil {
 640		return
 641	}
 642	if xonk.Honker == "" {
 643		xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
 644	}
 645
 646	userinfo := GetUserInfo(r)
 647
 648	dt := time.Now().UTC()
 649	bonk := Honk{
 650		UserID:   userinfo.UserID,
 651		Username: userinfo.Username,
 652		Honker:   xonk.Honker,
 653		What:     "bonk",
 654		XID:      xonk.XID,
 655		Date:     dt,
 656		Noise:    xonk.Noise,
 657		Donks:    xonk.Donks,
 658		Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
 659	}
 660
 661	aud := strings.Join(bonk.Audience, " ")
 662	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
 663		dt.Format(dbtimeformat), "", aud, bonk.Noise)
 664	if err != nil {
 665		log.Printf("error saving bonk: %s", err)
 666		return
 667	}
 668	bonk.ID, _ = res.LastInsertId()
 669	for _, d := range bonk.Donks {
 670		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 671		if err != nil {
 672			log.Printf("err saving donk: %s", err)
 673			return
 674		}
 675	}
 676
 677	user, _ := butwhatabout(userinfo.Username)
 678
 679	go honkworldwide(user, &bonk)
 680
 681}
 682
 683func savehonk(w http.ResponseWriter, r *http.Request) {
 684	rid := r.FormValue("rid")
 685	noise := r.FormValue("noise")
 686
 687	userinfo := GetUserInfo(r)
 688
 689	dt := time.Now().UTC()
 690	xid := xfiltrate()
 691	if xid == "" {
 692		return
 693	}
 694	what := "honk"
 695	if rid != "" {
 696		what = "tonk"
 697	}
 698	honk := Honk{
 699		UserID:   userinfo.UserID,
 700		Username: userinfo.Username,
 701		What:     "honk",
 702		XID:      xid,
 703		RID:      rid,
 704		Date:     dt,
 705	}
 706	if noise[0] == '@' {
 707		honk.Audience = append(grapevine(noise), thewholeworld)
 708	} else {
 709		honk.Audience = append([]string{thewholeworld}, grapevine(noise)...)
 710	}
 711	if rid != "" {
 712		xonk := getxonk("", rid)
 713		if xonk != nil {
 714			honk.Audience = append(honk.Audience, xonk.Audience...)
 715		} else {
 716			xonkaud := whosthere(rid)
 717			honk.Audience = append(honk.Audience, xonkaud...)
 718		}
 719	}
 720	honk.Audience = oneofakind(honk.Audience)
 721	noise = obfusbreak(noise)
 722	honk.Noise = noise
 723
 724	file, _, err := r.FormFile("donk")
 725	if err == nil {
 726		var buf bytes.Buffer
 727		io.Copy(&buf, file)
 728		file.Close()
 729		data := buf.Bytes()
 730		img, format, err := image.Decode(&buf)
 731		if err != nil {
 732			log.Printf("bad image: %s", err)
 733			return
 734		}
 735		data, format, err = vacuumwrap(img, format)
 736		if err != nil {
 737			log.Printf("can't vacuum image: %s", err)
 738			return
 739		}
 740		name := xfiltrate()
 741		media := "image/" + format
 742		if format == "jpeg" {
 743			format = "jpg"
 744		}
 745		name = name + "." + format
 746		url := fmt.Sprintf("https://%s/d/%s", serverName, name)
 747		res, err := stmtSaveFile.Exec(name, name, url, media, data)
 748		if err != nil {
 749			log.Printf("unable to save image: %s", err)
 750			return
 751		}
 752		var d Donk
 753		d.FileID, _ = res.LastInsertId()
 754		d.XID = name
 755		d.Name = name
 756		d.Media = media
 757		d.URL = url
 758		honk.Donks = append(honk.Donks, &d)
 759	}
 760	herd := herdofemus(honk.Noise)
 761	for _, e := range herd {
 762		donk := savedonk(e.ID, e.Name, "image/png")
 763		if donk != nil {
 764			honk.Donks = append(honk.Donks, donk)
 765		}
 766	}
 767
 768	aud := strings.Join(honk.Audience, " ")
 769	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
 770		dt.Format(dbtimeformat), "", aud, noise)
 771	if err != nil {
 772		log.Printf("error saving honk: %s", err)
 773		return
 774	}
 775	honk.ID, _ = res.LastInsertId()
 776	for _, d := range honk.Donks {
 777		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 778		if err != nil {
 779			log.Printf("err saving donk: %s", err)
 780			return
 781		}
 782	}
 783
 784	user, _ := butwhatabout(userinfo.Username)
 785
 786	go honkworldwide(user, &honk)
 787
 788	http.Redirect(w, r, "/", http.StatusSeeOther)
 789}
 790
 791func showhonkers(w http.ResponseWriter, r *http.Request) {
 792	userinfo := GetUserInfo(r)
 793	templinfo := getInfo(r)
 794	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 795	templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
 796	err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
 797	if err != nil {
 798		log.Print(err)
 799	}
 800}
 801
 802var handfull = make(map[string]string)
 803var handlock sync.Mutex
 804
 805func gofish(name string) string {
 806	if name[0] == '@' {
 807		name = name[1:]
 808	}
 809	m := strings.Split(name, "@")
 810	if len(m) != 2 {
 811		log.Printf("bad fish name: %s", name)
 812		return ""
 813	}
 814	handlock.Lock()
 815	defer handlock.Unlock()
 816	ref, ok := handfull[name]
 817	if ok {
 818		return ref
 819	}
 820	j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
 821	if err != nil {
 822		log.Printf("failed to go fish %s: %s", name, err)
 823		handfull[name] = ""
 824		return ""
 825	}
 826	links, _ := jsongetarray(j, "links")
 827	for _, l := range links {
 828		href, _ := jsongetstring(l, "href")
 829		rel, _ := jsongetstring(l, "rel")
 830		t, _ := jsongetstring(l, "type")
 831		if rel == "self" && friendorfoe(t) {
 832			handfull[name] = href
 833			return href
 834		}
 835	}
 836	handfull[name] = ""
 837	return ""
 838}
 839
 840func savehonker(w http.ResponseWriter, r *http.Request) {
 841	name := r.FormValue("name")
 842	url := r.FormValue("url")
 843	peep := r.FormValue("peep")
 844	flavor := "presub"
 845	if peep == "peep" {
 846		flavor = "peep"
 847	}
 848
 849	if url == "" {
 850		return
 851	}
 852	if url[0] == '@' {
 853		url = gofish(url)
 854	}
 855	if url == "" {
 856		return
 857	}
 858
 859	u := GetUserInfo(r)
 860	db := opendatabase()
 861	_, err := db.Exec("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)",
 862		u.UserID, name, url, flavor)
 863	if err != nil {
 864		log.Print(err)
 865	}
 866	if flavor == "presub" {
 867		user, _ := butwhatabout(u.Username)
 868		go subsub(user, url)
 869	}
 870	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
 871}
 872
 873func avatate(w http.ResponseWriter, r *http.Request) {
 874	n := r.FormValue("a")
 875	a := avatar(n)
 876	w.Header().Set("Cache-Control", "max-age=432000")
 877	w.Write(a)
 878}
 879
 880func servecss(w http.ResponseWriter, r *http.Request) {
 881	w.Header().Set("Cache-Control", "max-age=7776000")
 882	http.ServeFile(w, r, "views"+r.URL.Path)
 883}
 884func servehtml(w http.ResponseWriter, r *http.Request) {
 885	templinfo := getInfo(r)
 886	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
 887	if err != nil {
 888		log.Print(err)
 889	}
 890}
 891func serveemu(w http.ResponseWriter, r *http.Request) {
 892	xid := mux.Vars(r)["xid"]
 893	w.Header().Set("Cache-Control", "max-age=432000")
 894	http.ServeFile(w, r, "emus/"+xid)
 895}
 896
 897func servefile(w http.ResponseWriter, r *http.Request) {
 898	xid := mux.Vars(r)["xid"]
 899	row := stmtFileData.QueryRow(xid)
 900	var data []byte
 901	err := row.Scan(&data)
 902	if err != nil {
 903		log.Printf("error loading file: %s", err)
 904		http.NotFound(w, r)
 905		return
 906	}
 907	w.Header().Set("Cache-Control", "max-age=432000")
 908	w.Write(data)
 909}
 910
 911func serve() {
 912	db := opendatabase()
 913	LoginInit(db)
 914
 915	listener, err := openListener()
 916	if err != nil {
 917		log.Fatal(err)
 918	}
 919	go redeliverator()
 920
 921	debug := false
 922	getconfig("debug", &debug)
 923	readviews = ParseTemplates(debug,
 924		"views/homepage.html",
 925		"views/honkpage.html",
 926		"views/honkers.html",
 927		"views/honkform.html",
 928		"views/honk.html",
 929		"views/login.html",
 930		"views/header.html",
 931	)
 932	if !debug {
 933		s := "views/style.css"
 934		savedstyleparams[s] = getstyleparam(s)
 935		s = "views/local.css"
 936		savedstyleparams[s] = getstyleparam(s)
 937	}
 938
 939	mux := mux.NewRouter()
 940	mux.Use(LoginChecker)
 941
 942	posters := mux.Methods("POST").Subrouter()
 943	getters := mux.Methods("GET").Subrouter()
 944
 945	getters.HandleFunc("/", homepage)
 946	getters.HandleFunc("/rss", showrss)
 947	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
 948	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
 949	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
 950	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
 951	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
 952	getters.HandleFunc("/a", avatate)
 953	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
 954	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
 955	getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
 956	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
 957
 958	getters.HandleFunc("/style.css", servecss)
 959	getters.HandleFunc("/local.css", servecss)
 960	getters.HandleFunc("/login", servehtml)
 961	posters.HandleFunc("/dologin", dologin)
 962	getters.HandleFunc("/logout", dologout)
 963
 964	loggedin := mux.NewRoute().Subrouter()
 965	loggedin.Use(LoginRequired)
 966	loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
 967	loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
 968	loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
 969	loggedin.HandleFunc("/honkers", showhonkers)
 970	loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
 971
 972	err = http.Serve(listener, mux)
 973	if err != nil {
 974		log.Fatal(err)
 975	}
 976}
 977
 978var stmtHonkers, stmtDubbers, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
 979var stmtHonksForUser, stmtDeleteHonk, stmtSaveDub *sql.Stmt
 980var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
 981var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
 982var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
 983
 984func preparetodie(db *sql.DB, s string) *sql.Stmt {
 985	stmt, err := db.Prepare(s)
 986	if err != nil {
 987		log.Fatal(err)
 988	}
 989	return stmt
 990}
 991
 992func prepareStatements(db *sql.DB) {
 993	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
 994	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
 995	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 = ?")
 996	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")
 997	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")
 998	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")
 999	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")
1000	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
1001	stmtFileData = preparetodie(db, "select content from files where xid = ?")
1002	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1003	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1004	stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1005	stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1006	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1007	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1008	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1009	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1010	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1011	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1012	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1013}
1014
1015func ElaborateUnitTests() {
1016}
1017
1018func finishusersetup() error {
1019	db := opendatabase()
1020	k, err := rsa.GenerateKey(rand.Reader, 2048)
1021	if err != nil {
1022		return err
1023	}
1024	pubkey, err := zem(&k.PublicKey)
1025	if err != nil {
1026		return err
1027	}
1028	seckey, err := zem(k)
1029	if err != nil {
1030		return err
1031	}
1032	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1033	if err != nil {
1034		return err
1035	}
1036	return nil
1037}
1038
1039func main() {
1040	cmd := "run"
1041	if len(os.Args) > 1 {
1042		cmd = os.Args[1]
1043	}
1044	switch cmd {
1045	case "init":
1046		initdb()
1047	case "upgrade":
1048		upgradedb()
1049	}
1050	db := opendatabase()
1051	dbversion := 0
1052	getconfig("dbversion", &dbversion)
1053	if dbversion != myVersion {
1054		log.Fatal("incorrect database version. run upgrade.")
1055	}
1056	getconfig("servername", &serverName)
1057	prepareStatements(db)
1058	switch cmd {
1059	case "ping":
1060		if len(os.Args) < 4 {
1061			fmt.Printf("usage: honk ping from to\n")
1062			return
1063		}
1064		name := os.Args[2]
1065		targ := os.Args[3]
1066		user, err := butwhatabout(name)
1067		if err != nil {
1068			log.Printf("unknown user")
1069			return
1070		}
1071		ping(user, targ)
1072	case "peep":
1073		peeppeep()
1074	case "run":
1075		serve()
1076	case "test":
1077		ElaborateUnitTests()
1078	default:
1079		log.Fatal("unknown command")
1080	}
1081}