all repos — honk @ d76e8ec4c624636dcace9dba7ab31d301da48644

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		if err != sql.ErrNoRows {
 572			log.Printf("error scanning xonk: %s", err)
 573		}
 574		return nil
 575	}
 576	if name != "" && h.Username != name {
 577		log.Printf("user xonk mismatch")
 578		return nil
 579	}
 580	h.Date, _ = time.Parse(dbtimeformat, dt)
 581	h.Audience = strings.Split(aud, " ")
 582	donksforhonks([]*Honk{&h})
 583	return &h
 584}
 585
 586func gethonks() []*Honk {
 587	rows, err := stmtHonks.Query()
 588	return getsomehonks(rows, err)
 589}
 590func gethonksbyuser(name string) []*Honk {
 591	rows, err := stmtUserHonks.Query(name)
 592	return getsomehonks(rows, err)
 593}
 594func gethonksforuser(userid int64) []*Honk {
 595	dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
 596	rows, err := stmtHonksForUser.Query(userid, dt.Format(dbtimeformat), userid)
 597	return getsomehonks(rows, err)
 598}
 599func gethonksbyhonker(userid int64, honker string) []*Honk {
 600	rows, err := stmtHonksByHonker.Query(userid, honker)
 601	return getsomehonks(rows, err)
 602}
 603
 604func getsomehonks(rows *sql.Rows, err error) []*Honk {
 605	if err != nil {
 606		log.Printf("error querying honks: %s", err)
 607		return nil
 608	}
 609	defer rows.Close()
 610	var honks []*Honk
 611	for rows.Next() {
 612		var h Honk
 613		var dt, aud string
 614		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
 615			&dt, &h.URL, &aud, &h.Noise, &h.Convoy)
 616		if err != nil {
 617			log.Printf("error scanning honks: %s", err)
 618			return nil
 619		}
 620		h.Date, _ = time.Parse(dbtimeformat, dt)
 621		h.Audience = strings.Split(aud, " ")
 622		honks = append(honks, &h)
 623	}
 624	rows.Close()
 625	donksforhonks(honks)
 626	return honks
 627}
 628
 629func donksforhonks(honks []*Honk) {
 630	db := opendatabase()
 631	var ids []string
 632	hmap := make(map[int64]*Honk)
 633	for _, h := range honks {
 634		if h.What == "zonk" {
 635			continue
 636		}
 637		ids = append(ids, fmt.Sprintf("%d", h.ID))
 638		hmap[h.ID] = h
 639	}
 640	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, ","))
 641	rows, err := db.Query(q)
 642	if err != nil {
 643		log.Printf("error querying donks: %s", err)
 644		return
 645	}
 646	defer rows.Close()
 647	for rows.Next() {
 648		var hid int64
 649		var d Donk
 650		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
 651		if err != nil {
 652			log.Printf("error scanning donk: %s", err)
 653			continue
 654		}
 655		h := hmap[hid]
 656		h.Donks = append(h.Donks, &d)
 657	}
 658}
 659
 660func savebonk(w http.ResponseWriter, r *http.Request) {
 661	xid := r.FormValue("xid")
 662
 663	log.Printf("bonking %s", xid)
 664
 665	xonk := getxonk("", xid)
 666	if xonk == nil {
 667		return
 668	}
 669	if xonk.Honker == "" {
 670		xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
 671	}
 672	convoy := xonk.Convoy
 673
 674	userinfo := GetUserInfo(r)
 675
 676	dt := time.Now().UTC()
 677	bonk := Honk{
 678		UserID:   userinfo.UserID,
 679		Username: userinfo.Username,
 680		Honker:   xonk.Honker,
 681		What:     "bonk",
 682		XID:      xonk.XID,
 683		Date:     dt,
 684		Noise:    xonk.Noise,
 685		Convoy:   convoy,
 686		Donks:    xonk.Donks,
 687		Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
 688	}
 689
 690	aud := strings.Join(bonk.Audience, " ")
 691	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
 692		dt.Format(dbtimeformat), "", aud, bonk.Noise, bonk.Convoy)
 693	if err != nil {
 694		log.Printf("error saving bonk: %s", err)
 695		return
 696	}
 697	bonk.ID, _ = res.LastInsertId()
 698	for _, d := range bonk.Donks {
 699		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 700		if err != nil {
 701			log.Printf("err saving donk: %s", err)
 702			return
 703		}
 704	}
 705
 706	user, _ := butwhatabout(userinfo.Username)
 707
 708	go honkworldwide(user, &bonk)
 709
 710}
 711
 712func zonkit(w http.ResponseWriter, r *http.Request) {
 713	xid := r.FormValue("xid")
 714
 715	log.Printf("zonking %s", xid)
 716	userinfo := GetUserInfo(r)
 717	stmtZonkIt.Exec(userinfo.UserID, xid)
 718}
 719
 720func savehonk(w http.ResponseWriter, r *http.Request) {
 721	rid := r.FormValue("rid")
 722	noise := r.FormValue("noise")
 723
 724	userinfo := GetUserInfo(r)
 725
 726	dt := time.Now().UTC()
 727	xid := xfiltrate()
 728	what := "honk"
 729	if rid != "" {
 730		what = "tonk"
 731	}
 732	honk := Honk{
 733		UserID:   userinfo.UserID,
 734		Username: userinfo.Username,
 735		What:     "honk",
 736		XID:      xid,
 737		RID:      rid,
 738		Date:     dt,
 739	}
 740	if noise[0] == '@' {
 741		honk.Audience = append(grapevine(noise), thewholeworld)
 742	} else {
 743		honk.Audience = prepend(thewholeworld, grapevine(noise))
 744	}
 745	var convoy string
 746	if rid != "" {
 747		xonk := getxonk("", rid)
 748		if xonk != nil {
 749			honk.Audience = append(honk.Audience, xonk.Audience...)
 750			convoy = xonk.Convoy
 751		} else {
 752			xonkaud, c := whosthere(rid)
 753			honk.Audience = append(honk.Audience, xonkaud...)
 754			convoy = c
 755		}
 756	}
 757	if convoy == "" {
 758		convoy = "data:,electrichonkytonk-" + xfiltrate()
 759	}
 760	honk.Audience = oneofakind(honk.Audience)
 761	noise = obfusbreak(noise)
 762	honk.Noise = noise
 763	honk.Convoy = convoy
 764
 765	file, filehdr, err := r.FormFile("donk")
 766	if err == nil {
 767		var buf bytes.Buffer
 768		io.Copy(&buf, file)
 769		file.Close()
 770		data := buf.Bytes()
 771		xid := xfiltrate()
 772		var media, name string
 773		img, format, err := image.Decode(&buf)
 774		if err == nil {
 775			data, format, err = vacuumwrap(img, format)
 776			if err != nil {
 777				log.Printf("can't vacuum image: %s", err)
 778				return
 779			}
 780			media = "image/" + format
 781			if format == "jpeg" {
 782				format = "jpg"
 783			}
 784			name = xid + "." + format
 785			xid = name
 786		} else {
 787			maxsize := 100000
 788			if len(data) > maxsize {
 789				log.Printf("bad image: %s too much text: %d", err, len(data))
 790				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 791				return
 792			}
 793			for i := 0; i < len(data); i++ {
 794				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 795					log.Printf("bad image: %s not text: %d", err, data[i])
 796					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 797					return
 798				}
 799			}
 800			media = "text/plain"
 801			name = filehdr.Filename
 802			if name == "" {
 803				name = xid + ".txt"
 804			}
 805			xid += ".txt"
 806		}
 807		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 808		res, err := stmtSaveFile.Exec(xid, name, url, media, data)
 809		if err != nil {
 810			log.Printf("unable to save image: %s", err)
 811			return
 812		}
 813		var d Donk
 814		d.FileID, _ = res.LastInsertId()
 815		d.XID = name
 816		d.Name = name
 817		d.Media = media
 818		d.URL = url
 819		honk.Donks = append(honk.Donks, &d)
 820	}
 821	herd := herdofemus(honk.Noise)
 822	for _, e := range herd {
 823		donk := savedonk(e.ID, e.Name, "image/png")
 824		if donk != nil {
 825			donk.Name = e.Name
 826			honk.Donks = append(honk.Donks, donk)
 827		}
 828	}
 829
 830	aud := strings.Join(honk.Audience, " ")
 831	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
 832		dt.Format(dbtimeformat), "", aud, noise, convoy)
 833	if err != nil {
 834		log.Printf("error saving honk: %s", err)
 835		return
 836	}
 837	honk.ID, _ = res.LastInsertId()
 838	for _, d := range honk.Donks {
 839		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 840		if err != nil {
 841			log.Printf("err saving donk: %s", err)
 842			return
 843		}
 844	}
 845
 846	user, _ := butwhatabout(userinfo.Username)
 847
 848	go honkworldwide(user, &honk)
 849
 850	http.Redirect(w, r, "/", http.StatusSeeOther)
 851}
 852
 853func showhonkers(w http.ResponseWriter, r *http.Request) {
 854	userinfo := GetUserInfo(r)
 855	templinfo := getInfo(r)
 856	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 857	templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
 858	err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
 859	if err != nil {
 860		log.Print(err)
 861	}
 862}
 863
 864var handfull = make(map[string]string)
 865var handlock sync.Mutex
 866
 867func gofish(name string) string {
 868	if name[0] == '@' {
 869		name = name[1:]
 870	}
 871	m := strings.Split(name, "@")
 872	if len(m) != 2 {
 873		log.Printf("bad fish name: %s", name)
 874		return ""
 875	}
 876	handlock.Lock()
 877	ref, ok := handfull[name]
 878	handlock.Unlock()
 879	if ok {
 880		return ref
 881	}
 882	j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
 883	handlock.Lock()
 884	defer handlock.Unlock()
 885	if err != nil {
 886		log.Printf("failed to go fish %s: %s", name, err)
 887		handfull[name] = ""
 888		return ""
 889	}
 890	links, _ := jsongetarray(j, "links")
 891	for _, l := range links {
 892		href, _ := jsongetstring(l, "href")
 893		rel, _ := jsongetstring(l, "rel")
 894		t, _ := jsongetstring(l, "type")
 895		if rel == "self" && friendorfoe(t) {
 896			handfull[name] = href
 897			return href
 898		}
 899	}
 900	handfull[name] = ""
 901	return ""
 902}
 903
 904func savehonker(w http.ResponseWriter, r *http.Request) {
 905	name := r.FormValue("name")
 906	url := r.FormValue("url")
 907	peep := r.FormValue("peep")
 908	flavor := "presub"
 909	if peep == "peep" {
 910		flavor = "peep"
 911	}
 912
 913	if url == "" {
 914		return
 915	}
 916	if url[0] == '@' {
 917		url = gofish(url)
 918	}
 919	if url == "" {
 920		return
 921	}
 922
 923	u := GetUserInfo(r)
 924	db := opendatabase()
 925	_, err := db.Exec("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)",
 926		u.UserID, name, url, flavor)
 927	if err != nil {
 928		log.Print(err)
 929	}
 930	if flavor == "presub" {
 931		user, _ := butwhatabout(u.Username)
 932		go subsub(user, url)
 933	}
 934	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
 935}
 936
 937func somedays() string {
 938	secs := 432000 + notrand.Int63n(432000)
 939	return fmt.Sprintf("%d", secs)
 940}
 941
 942func avatate(w http.ResponseWriter, r *http.Request) {
 943	n := r.FormValue("a")
 944	a := avatar(n)
 945	w.Header().Set("Cache-Control", "max-age="+somedays())
 946	w.Write(a)
 947}
 948
 949func servecss(w http.ResponseWriter, r *http.Request) {
 950	w.Header().Set("Cache-Control", "max-age=7776000")
 951	http.ServeFile(w, r, "views"+r.URL.Path)
 952}
 953func servehtml(w http.ResponseWriter, r *http.Request) {
 954	templinfo := getInfo(r)
 955	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
 956	if err != nil {
 957		log.Print(err)
 958	}
 959}
 960func serveemu(w http.ResponseWriter, r *http.Request) {
 961	xid := mux.Vars(r)["xid"]
 962	w.Header().Set("Cache-Control", "max-age="+somedays())
 963	http.ServeFile(w, r, "emus/"+xid)
 964}
 965
 966func servefile(w http.ResponseWriter, r *http.Request) {
 967	xid := mux.Vars(r)["xid"]
 968	row := stmtFileData.QueryRow(xid)
 969	var media string
 970	var data []byte
 971	err := row.Scan(&media, &data)
 972	if err != nil {
 973		log.Printf("error loading file: %s", err)
 974		http.NotFound(w, r)
 975		return
 976	}
 977	w.Header().Set("Content-Type", media)
 978	w.Header().Set("X-Content-Type-Options", "nosniff")
 979	w.Header().Set("Cache-Control", "max-age="+somedays())
 980	w.Write(data)
 981}
 982
 983func serve() {
 984	db := opendatabase()
 985	LoginInit(db)
 986
 987	listener, err := openListener()
 988	if err != nil {
 989		log.Fatal(err)
 990	}
 991	go redeliverator()
 992
 993	debug := false
 994	getconfig("debug", &debug)
 995	readviews = ParseTemplates(debug,
 996		"views/homepage.html",
 997		"views/honkpage.html",
 998		"views/honkers.html",
 999		"views/honkform.html",
1000		"views/honk.html",
1001		"views/login.html",
1002		"views/header.html",
1003	)
1004	if !debug {
1005		s := "views/style.css"
1006		savedstyleparams[s] = getstyleparam(s)
1007		s = "views/local.css"
1008		savedstyleparams[s] = getstyleparam(s)
1009	}
1010
1011	mux := mux.NewRouter()
1012	mux.Use(LoginChecker)
1013
1014	posters := mux.Methods("POST").Subrouter()
1015	getters := mux.Methods("GET").Subrouter()
1016
1017	getters.HandleFunc("/", homepage)
1018	getters.HandleFunc("/rss", showrss)
1019	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1020	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1021	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1022	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1023	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1024	getters.HandleFunc("/a", avatate)
1025	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1026	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1027	getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1028	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1029
1030	getters.HandleFunc("/style.css", servecss)
1031	getters.HandleFunc("/local.css", servecss)
1032	getters.HandleFunc("/login", servehtml)
1033	posters.HandleFunc("/dologin", dologin)
1034	getters.HandleFunc("/logout", dologout)
1035
1036	loggedin := mux.NewRoute().Subrouter()
1037	loggedin.Use(LoginRequired)
1038	loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1039	loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1040	loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1041	loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1042	loggedin.HandleFunc("/honkers", showhonkers)
1043	loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1044
1045	err = http.Serve(listener, mux)
1046	if err != nil {
1047		log.Fatal(err)
1048	}
1049}
1050
1051var stmtHonkers, stmtDubbers, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1052var stmtHonksForUser, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1053var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1054var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1055var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1056var stmtThumbBiter, stmtZonkIt *sql.Stmt
1057
1058func preparetodie(db *sql.DB, s string) *sql.Stmt {
1059	stmt, err := db.Prepare(s)
1060	if err != nil {
1061		log.Fatalf("error %s: %s", err, s)
1062	}
1063	return stmt
1064}
1065
1066func prepareStatements(db *sql.DB) {
1067	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1068	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1069	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 = ?")
1070	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")
1071	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")
1072	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")
1073	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")
1074	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1075	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1076	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1077	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1078	stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1079	stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1080	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1081	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1082	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1083	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1084	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1085	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1086	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1087	stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1088	stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1089}
1090
1091func ElaborateUnitTests() {
1092}
1093
1094func finishusersetup() error {
1095	db := opendatabase()
1096	k, err := rsa.GenerateKey(rand.Reader, 2048)
1097	if err != nil {
1098		return err
1099	}
1100	pubkey, err := zem(&k.PublicKey)
1101	if err != nil {
1102		return err
1103	}
1104	seckey, err := zem(k)
1105	if err != nil {
1106		return err
1107	}
1108	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1109	if err != nil {
1110		return err
1111	}
1112	return nil
1113}
1114
1115func main() {
1116	cmd := "run"
1117	if len(os.Args) > 1 {
1118		cmd = os.Args[1]
1119	}
1120	switch cmd {
1121	case "init":
1122		initdb()
1123	case "upgrade":
1124		upgradedb()
1125	}
1126	db := opendatabase()
1127	dbversion := 0
1128	getconfig("dbversion", &dbversion)
1129	if dbversion != myVersion {
1130		log.Fatal("incorrect database version. run upgrade.")
1131	}
1132	getconfig("servername", &serverName)
1133	prepareStatements(db)
1134	switch cmd {
1135	case "ping":
1136		if len(os.Args) < 4 {
1137			fmt.Printf("usage: honk ping from to\n")
1138			return
1139		}
1140		name := os.Args[2]
1141		targ := os.Args[3]
1142		user, err := butwhatabout(name)
1143		if err != nil {
1144			log.Printf("unknown user")
1145			return
1146		}
1147		ping(user, targ)
1148	case "peep":
1149		peeppeep()
1150	case "run":
1151		serve()
1152	case "test":
1153		ElaborateUnitTests()
1154	default:
1155		log.Fatal("unknown command")
1156	}
1157}