all repos — honk @ e8a291302f6a296a1e377dea15ad14ca76e51413

my fork of honk

honk.go (view raw)

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