all repos — honk @ 87595f390c3672470635ac11537bfd4555cc1a36

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