all repos — honk @ afcec984663d7cf9c39ce88413a5f93942570a26

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