all repos — honk @ 567e7587f639721053a090a619180f92d5de701f

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
 961type Zonker struct {
 962	Name string
 963	Wherefore string
 964}
 965
 966func killzone(w http.ResponseWriter, r *http.Request) {
 967	db := opendatabase()
 968	userinfo := GetUserInfo(r)
 969	rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
 970	if err != nil {
 971		log.Printf("err: %s", err)
 972		return
 973	}
 974	var zonkers []Zonker
 975	for rows.Next() {
 976		var z Zonker
 977		rows.Scan(&z.Name, &z.Wherefore)
 978		zonkers = append(zonkers, z)
 979	}
 980	templinfo := getInfo(r)
 981	templinfo["Zonkers"] = zonkers
 982	templinfo["KillCSRF"] = GetCSRF("killitwithfire", r)
 983	err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
 984	if err != nil {
 985		log.Print(err)
 986	}
 987}
 988
 989func killitwithfire(w http.ResponseWriter, r *http.Request) {
 990	userinfo := GetUserInfo(r)
 991	wherefore := r.FormValue("wherefore")
 992	name := r.FormValue("name")
 993	if name == "" {
 994		return
 995	}
 996	switch wherefore {
 997	case "zonker":
 998	case "zurl":
 999	case "zonvoy":
1000	default:
1001		return
1002	}
1003	db := opendatabase()
1004	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1005		userinfo.UserID, name, wherefore)
1006
1007	http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1008}
1009
1010func somedays() string {
1011	secs := 432000 + notrand.Int63n(432000)
1012	return fmt.Sprintf("%d", secs)
1013}
1014
1015func avatate(w http.ResponseWriter, r *http.Request) {
1016	n := r.FormValue("a")
1017	a := avatar(n)
1018	w.Header().Set("Cache-Control", "max-age="+somedays())
1019	w.Write(a)
1020}
1021
1022func servecss(w http.ResponseWriter, r *http.Request) {
1023	w.Header().Set("Cache-Control", "max-age=7776000")
1024	http.ServeFile(w, r, "views"+r.URL.Path)
1025}
1026func servehtml(w http.ResponseWriter, r *http.Request) {
1027	templinfo := getInfo(r)
1028	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1029	if err != nil {
1030		log.Print(err)
1031	}
1032}
1033func serveemu(w http.ResponseWriter, r *http.Request) {
1034	xid := mux.Vars(r)["xid"]
1035	w.Header().Set("Cache-Control", "max-age="+somedays())
1036	http.ServeFile(w, r, "emus/"+xid)
1037}
1038
1039func servefile(w http.ResponseWriter, r *http.Request) {
1040	xid := mux.Vars(r)["xid"]
1041	row := stmtFileData.QueryRow(xid)
1042	var media string
1043	var data []byte
1044	err := row.Scan(&media, &data)
1045	if err != nil {
1046		log.Printf("error loading file: %s", err)
1047		http.NotFound(w, r)
1048		return
1049	}
1050	w.Header().Set("Content-Type", media)
1051	w.Header().Set("X-Content-Type-Options", "nosniff")
1052	w.Header().Set("Cache-Control", "max-age="+somedays())
1053	w.Write(data)
1054}
1055
1056func serve() {
1057	db := opendatabase()
1058	LoginInit(db)
1059
1060	listener, err := openListener()
1061	if err != nil {
1062		log.Fatal(err)
1063	}
1064	go redeliverator()
1065
1066	debug := false
1067	getconfig("debug", &debug)
1068	readviews = ParseTemplates(debug,
1069		"views/homepage.html",
1070		"views/honkpage.html",
1071		"views/honkers.html",
1072		"views/zonkers.html",
1073		"views/honkform.html",
1074		"views/honk.html",
1075		"views/login.html",
1076		"views/header.html",
1077	)
1078	if !debug {
1079		s := "views/style.css"
1080		savedstyleparams[s] = getstyleparam(s)
1081		s = "views/local.css"
1082		savedstyleparams[s] = getstyleparam(s)
1083	}
1084
1085	mux := mux.NewRouter()
1086	mux.Use(LoginChecker)
1087
1088	posters := mux.Methods("POST").Subrouter()
1089	getters := mux.Methods("GET").Subrouter()
1090
1091	getters.HandleFunc("/", homepage)
1092	getters.Handle("/atme", LoginRequired(http.HandlerFunc(homepage)))
1093	getters.Handle("/killzone", LoginRequired(http.HandlerFunc(killzone)))
1094	getters.HandleFunc("/rss", showrss)
1095	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1096	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1097	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1098	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1099	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1100	getters.HandleFunc("/a", avatate)
1101	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1102	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1103	getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1104	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1105
1106	getters.HandleFunc("/style.css", servecss)
1107	getters.HandleFunc("/local.css", servecss)
1108	getters.HandleFunc("/login", servehtml)
1109	posters.HandleFunc("/dologin", dologin)
1110	getters.HandleFunc("/logout", dologout)
1111
1112	loggedin := mux.NewRoute().Subrouter()
1113	loggedin.Use(LoginRequired)
1114	loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1115	loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1116	loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1117	loggedin.Handle("/killitwithfire", CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1118	loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1119	loggedin.HandleFunc("/honkers", showhonkers)
1120	loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1121
1122	err = http.Serve(listener, mux)
1123	if err != nil {
1124		log.Fatal(err)
1125	}
1126}
1127
1128var stmtHonkers, stmtDubbers, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1129var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1130var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1131var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1132var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1133var stmtThumbBiter, stmtZonkIt *sql.Stmt
1134
1135func preparetodie(db *sql.DB, s string) *sql.Stmt {
1136	stmt, err := db.Prepare(s)
1137	if err != nil {
1138		log.Fatalf("error %s: %s", err, s)
1139	}
1140	return stmt
1141}
1142
1143func prepareStatements(db *sql.DB) {
1144	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1145	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1146	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 = ?")
1147	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")
1148	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")
1149	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")
1150	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")
1151	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")
1152	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1153	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1154	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1155	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1156	stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1157	stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1158	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1159	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1160	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1161	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1162	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1163	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1164	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1165	stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1166	stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1167}
1168
1169func ElaborateUnitTests() {
1170}
1171
1172func finishusersetup() error {
1173	db := opendatabase()
1174	k, err := rsa.GenerateKey(rand.Reader, 2048)
1175	if err != nil {
1176		return err
1177	}
1178	pubkey, err := zem(&k.PublicKey)
1179	if err != nil {
1180		return err
1181	}
1182	seckey, err := zem(k)
1183	if err != nil {
1184		return err
1185	}
1186	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1187	if err != nil {
1188		return err
1189	}
1190	return nil
1191}
1192
1193func main() {
1194	cmd := "run"
1195	if len(os.Args) > 1 {
1196		cmd = os.Args[1]
1197	}
1198	switch cmd {
1199	case "init":
1200		initdb()
1201	case "upgrade":
1202		upgradedb()
1203	}
1204	db := opendatabase()
1205	dbversion := 0
1206	getconfig("dbversion", &dbversion)
1207	if dbversion != myVersion {
1208		log.Fatal("incorrect database version. run upgrade.")
1209	}
1210	getconfig("servername", &serverName)
1211	prepareStatements(db)
1212	switch cmd {
1213	case "ping":
1214		if len(os.Args) < 4 {
1215			fmt.Printf("usage: honk ping from to\n")
1216			return
1217		}
1218		name := os.Args[2]
1219		targ := os.Args[3]
1220		user, err := butwhatabout(name)
1221		if err != nil {
1222			log.Printf("unknown user")
1223			return
1224		}
1225		ping(user, targ)
1226	case "peep":
1227		peeppeep()
1228	case "run":
1229		serve()
1230	case "test":
1231		ElaborateUnitTests()
1232	default:
1233		log.Fatal("unknown command")
1234	}
1235}