all repos — honk @ 5331434ed6c91a0a1beec10408b9069270b8a67b

my fork of honk

honk.go (view raw)

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