all repos — honk @ c2429397f835803ebcf6797ff21728c341b2c50a

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