all repos — honk @ 1ae28c75a94d118c92afa0da7e01e915ab99b2bf

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	if thoudostbitethythumb(user.ID, who) {
 324		log.Printf("ignoring thumb sucker %s", who)
 325		return
 326	}
 327	fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 328	WriteJunk(fd, j)
 329	io.WriteString(fd, "\n")
 330	fd.Close()
 331	switch what {
 332	case "Ping":
 333		obj, _ := jsongetstring(j, "id")
 334		log.Printf("ping from %s: %s", who, obj)
 335		pong(user, who, obj)
 336	case "Pong":
 337		obj, _ := jsongetstring(j, "object")
 338		log.Printf("pong from %s: %s", who, obj)
 339	case "Follow":
 340		obj, _ := jsongetstring(j, "object")
 341		if obj == user.URL {
 342			log.Printf("updating honker follow: %s", who)
 343			rubadubdub(user, j)
 344		} else {
 345			log.Printf("can't follow %s", obj)
 346		}
 347	case "Accept":
 348		db := opendatabase()
 349		log.Printf("updating honker accept: %s", who)
 350		db.Exec("update honkers set flavor = 'sub' where xid = ? and flavor = 'presub'", who)
 351	case "Undo":
 352		obj, ok := jsongetmap(j, "object")
 353		if !ok {
 354			log.Printf("unknown undo no object")
 355		} else {
 356			what, _ := jsongetstring(obj, "type")
 357			switch what {
 358			case "Follow":
 359				log.Printf("updating honker undo: %s", who)
 360				db := opendatabase()
 361				db.Exec("update honkers set flavor = 'undub' where xid = ? and flavor = 'dub'", who)
 362			case "Like":
 363			default:
 364				log.Printf("unknown undo: %s", what)
 365			}
 366		}
 367	default:
 368		xonk := xonkxonk(user, j)
 369		if needxonk(user, xonk) {
 370			xonk.UserID = user.ID
 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	xid := r.FormValue("xid")
 793
 794	log.Printf("zonking %s", xid)
 795	userinfo := login.GetUserInfo(r)
 796	stmtZonkIt.Exec(userinfo.UserID, xid)
 797}
 798
 799func savehonk(w http.ResponseWriter, r *http.Request) {
 800	rid := r.FormValue("rid")
 801	noise := r.FormValue("noise")
 802
 803	userinfo := login.GetUserInfo(r)
 804
 805	dt := time.Now().UTC()
 806	xid := xfiltrate()
 807	what := "honk"
 808	if rid != "" {
 809		what = "tonk"
 810	}
 811	honk := Honk{
 812		UserID:   userinfo.UserID,
 813		Username: userinfo.Username,
 814		What:     "honk",
 815		XID:      xid,
 816		Date:     dt,
 817	}
 818	if noise[0] == '@' {
 819		honk.Audience = append(grapevine(noise), thewholeworld)
 820	} else {
 821		honk.Audience = prepend(thewholeworld, grapevine(noise))
 822	}
 823	var convoy string
 824	if rid != "" {
 825		xonk := getxonk("", rid)
 826		if xonk != nil {
 827			if xonk.Honker == "" {
 828				rid = "https://" + serverName + "/u/" + xonk.Username + "/h/" + rid
 829			}
 830			honk.Audience = append(honk.Audience, xonk.Audience...)
 831			convoy = xonk.Convoy
 832		} else {
 833			xonkaud, c := whosthere(rid)
 834			honk.Audience = append(honk.Audience, xonkaud...)
 835			convoy = c
 836		}
 837		honk.RID = rid
 838	}
 839	if convoy == "" {
 840		convoy = "data:,electrichonkytonk-" + xfiltrate()
 841	}
 842	honk.Audience = oneofakind(honk.Audience)
 843	noise = obfusbreak(noise)
 844	honk.Noise = noise
 845	honk.Convoy = convoy
 846
 847	file, filehdr, err := r.FormFile("donk")
 848	if err == nil {
 849		var buf bytes.Buffer
 850		io.Copy(&buf, file)
 851		file.Close()
 852		data := buf.Bytes()
 853		xid := xfiltrate()
 854		var media, name string
 855		img, format, err := image.Decode(&buf)
 856		if err == nil {
 857			data, format, err = vacuumwrap(img, format)
 858			if err != nil {
 859				log.Printf("can't vacuum image: %s", err)
 860				return
 861			}
 862			media = "image/" + format
 863			if format == "jpeg" {
 864				format = "jpg"
 865			}
 866			name = xid + "." + format
 867			xid = name
 868		} else {
 869			maxsize := 100000
 870			if len(data) > maxsize {
 871				log.Printf("bad image: %s too much text: %d", err, len(data))
 872				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 873				return
 874			}
 875			for i := 0; i < len(data); i++ {
 876				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 877					log.Printf("bad image: %s not text: %d", err, data[i])
 878					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 879					return
 880				}
 881			}
 882			media = "text/plain"
 883			name = filehdr.Filename
 884			if name == "" {
 885				name = xid + ".txt"
 886			}
 887			xid += ".txt"
 888		}
 889		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 890		res, err := stmtSaveFile.Exec(xid, name, url, media, data)
 891		if err != nil {
 892			log.Printf("unable to save image: %s", err)
 893			return
 894		}
 895		var d Donk
 896		d.FileID, _ = res.LastInsertId()
 897		d.XID = name
 898		d.Name = name
 899		d.Media = media
 900		d.URL = url
 901		honk.Donks = append(honk.Donks, &d)
 902	}
 903	herd := herdofemus(honk.Noise)
 904	for _, e := range herd {
 905		donk := savedonk(e.ID, e.Name, "image/png")
 906		if donk != nil {
 907			donk.Name = e.Name
 908			honk.Donks = append(honk.Donks, donk)
 909		}
 910	}
 911
 912	user, _ := butwhatabout(userinfo.Username)
 913
 914	aud := strings.Join(honk.Audience, " ")
 915	whofore := 0
 916	if strings.Contains(aud, user.URL) {
 917		whofore = 1
 918	}
 919	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
 920		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore)
 921	if err != nil {
 922		log.Printf("error saving honk: %s", err)
 923		return
 924	}
 925	honk.ID, _ = res.LastInsertId()
 926	for _, d := range honk.Donks {
 927		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 928		if err != nil {
 929			log.Printf("err saving donk: %s", err)
 930			return
 931		}
 932	}
 933
 934	go honkworldwide(user, &honk)
 935
 936	http.Redirect(w, r, "/", http.StatusSeeOther)
 937}
 938
 939func viewhonkers(w http.ResponseWriter, r *http.Request) {
 940	userinfo := login.GetUserInfo(r)
 941	templinfo := getInfo(r)
 942	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 943	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
 944	err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
 945	if err != nil {
 946		log.Print(err)
 947	}
 948}
 949
 950var handfull = make(map[string]string)
 951var handlock sync.Mutex
 952
 953func gofish(name string) string {
 954	if name[0] == '@' {
 955		name = name[1:]
 956	}
 957	m := strings.Split(name, "@")
 958	if len(m) != 2 {
 959		log.Printf("bad fish name: %s", name)
 960		return ""
 961	}
 962	handlock.Lock()
 963	ref, ok := handfull[name]
 964	handlock.Unlock()
 965	if ok {
 966		return ref
 967	}
 968	j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
 969	handlock.Lock()
 970	defer handlock.Unlock()
 971	if err != nil {
 972		log.Printf("failed to go fish %s: %s", name, err)
 973		handfull[name] = ""
 974		return ""
 975	}
 976	links, _ := jsongetarray(j, "links")
 977	for _, l := range links {
 978		href, _ := jsongetstring(l, "href")
 979		rel, _ := jsongetstring(l, "rel")
 980		t, _ := jsongetstring(l, "type")
 981		if rel == "self" && friendorfoe(t) {
 982			handfull[name] = href
 983			return href
 984		}
 985	}
 986	handfull[name] = ""
 987	return ""
 988}
 989
 990func savehonker(w http.ResponseWriter, r *http.Request) {
 991	u := login.GetUserInfo(r)
 992	name := r.FormValue("name")
 993	url := r.FormValue("url")
 994	peep := r.FormValue("peep")
 995	combos := r.FormValue("combos")
 996	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
 997
 998	if honkerid > 0 {
 999		combos = " " + strings.TrimSpace(combos) + " "
1000		_, err := stmtUpdateHonker.Exec(combos, honkerid, u.UserID)
1001		if err != nil {
1002			log.Printf("update honker err: %s", err)
1003			return
1004		}
1005		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1006	}
1007
1008	flavor := "presub"
1009	if peep == "peep" {
1010		flavor = "peep"
1011	}
1012	if url == "" {
1013		return
1014	}
1015	if url[0] == '@' {
1016		url = gofish(url)
1017	}
1018	if url == "" {
1019		return
1020	}
1021	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1022	if err != nil {
1023		log.Print(err)
1024		return
1025	}
1026	if flavor == "presub" {
1027		user, _ := butwhatabout(u.Username)
1028		go subsub(user, url)
1029	}
1030	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1031}
1032
1033type Zonker struct {
1034	Name      string
1035	Wherefore string
1036}
1037
1038func killzone(w http.ResponseWriter, r *http.Request) {
1039	db := opendatabase()
1040	userinfo := login.GetUserInfo(r)
1041	rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
1042	if err != nil {
1043		log.Printf("err: %s", err)
1044		return
1045	}
1046	var zonkers []Zonker
1047	for rows.Next() {
1048		var z Zonker
1049		rows.Scan(&z.Name, &z.Wherefore)
1050		zonkers = append(zonkers, z)
1051	}
1052	templinfo := getInfo(r)
1053	templinfo["Zonkers"] = zonkers
1054	templinfo["KillCSRF"] = login.GetCSRF("killitwithfire", r)
1055	err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
1056	if err != nil {
1057		log.Print(err)
1058	}
1059}
1060
1061func killitwithfire(w http.ResponseWriter, r *http.Request) {
1062	userinfo := login.GetUserInfo(r)
1063	wherefore := r.FormValue("wherefore")
1064	name := r.FormValue("name")
1065	if name == "" {
1066		return
1067	}
1068	switch wherefore {
1069	case "zonker":
1070	case "zurl":
1071	case "zonvoy":
1072	default:
1073		return
1074	}
1075	db := opendatabase()
1076	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1077		userinfo.UserID, name, wherefore)
1078
1079	http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1080}
1081
1082func somedays() string {
1083	secs := 432000 + notrand.Int63n(432000)
1084	return fmt.Sprintf("%d", secs)
1085}
1086
1087func avatate(w http.ResponseWriter, r *http.Request) {
1088	n := r.FormValue("a")
1089	a := avatar(n)
1090	w.Header().Set("Cache-Control", "max-age="+somedays())
1091	w.Write(a)
1092}
1093
1094func servecss(w http.ResponseWriter, r *http.Request) {
1095	w.Header().Set("Cache-Control", "max-age=7776000")
1096	http.ServeFile(w, r, "views"+r.URL.Path)
1097}
1098func servehtml(w http.ResponseWriter, r *http.Request) {
1099	templinfo := getInfo(r)
1100	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1101	if err != nil {
1102		log.Print(err)
1103	}
1104}
1105func serveemu(w http.ResponseWriter, r *http.Request) {
1106	xid := mux.Vars(r)["xid"]
1107	w.Header().Set("Cache-Control", "max-age="+somedays())
1108	http.ServeFile(w, r, "emus/"+xid)
1109}
1110
1111func servefile(w http.ResponseWriter, r *http.Request) {
1112	xid := mux.Vars(r)["xid"]
1113	row := stmtFileData.QueryRow(xid)
1114	var media string
1115	var data []byte
1116	err := row.Scan(&media, &data)
1117	if err != nil {
1118		log.Printf("error loading file: %s", err)
1119		http.NotFound(w, r)
1120		return
1121	}
1122	w.Header().Set("Content-Type", media)
1123	w.Header().Set("X-Content-Type-Options", "nosniff")
1124	w.Header().Set("Cache-Control", "max-age="+somedays())
1125	w.Write(data)
1126}
1127
1128func serve() {
1129	db := opendatabase()
1130	login.Init(db)
1131
1132	listener, err := openListener()
1133	if err != nil {
1134		log.Fatal(err)
1135	}
1136	go redeliverator()
1137
1138	debug := false
1139	getconfig("debug", &debug)
1140	readviews = ParseTemplates(debug,
1141		"views/honkpage.html",
1142		"views/honkers.html",
1143		"views/zonkers.html",
1144		"views/honkform.html",
1145		"views/honk.html",
1146		"views/login.html",
1147		"views/header.html",
1148	)
1149	if !debug {
1150		s := "views/style.css"
1151		savedstyleparams[s] = getstyleparam(s)
1152		s = "views/local.css"
1153		savedstyleparams[s] = getstyleparam(s)
1154	}
1155
1156	mux := mux.NewRouter()
1157	mux.Use(login.Checker)
1158
1159	posters := mux.Methods("POST").Subrouter()
1160	getters := mux.Methods("GET").Subrouter()
1161
1162	getters.HandleFunc("/", homepage)
1163	getters.HandleFunc("/rss", showrss)
1164	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1165	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1166	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1167	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1168	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1169	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1170	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1171	getters.HandleFunc("/a", avatate)
1172	getters.HandleFunc("/t", viewconvoy)
1173	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1174	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1175	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1176
1177	getters.HandleFunc("/style.css", servecss)
1178	getters.HandleFunc("/local.css", servecss)
1179	getters.HandleFunc("/login", servehtml)
1180	posters.HandleFunc("/dologin", login.LoginFunc)
1181	getters.HandleFunc("/logout", login.LogoutFunc)
1182
1183	loggedin := mux.NewRoute().Subrouter()
1184	loggedin.Use(login.Required)
1185	loggedin.HandleFunc("/atme", homepage)
1186	loggedin.HandleFunc("/killzone", killzone)
1187	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1188	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1189	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1190	loggedin.Handle("/killitwithfire", login.CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1191	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1192	loggedin.HandleFunc("/honkers", viewhonkers)
1193	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1194	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", viewcombo)
1195	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1196
1197	err = http.Serve(listener, mux)
1198	if err != nil {
1199		log.Fatal(err)
1200	}
1201}
1202
1203var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateHonker *sql.Stmt
1204var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1205var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1206var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1207var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1208var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1209var stmtHasHonker, stmtThumbBiter, stmtZonkIt *sql.Stmt
1210
1211func preparetodie(db *sql.DB, s string) *sql.Stmt {
1212	stmt, err := db.Prepare(s)
1213	if err != nil {
1214		log.Fatalf("error %s: %s", err, s)
1215	}
1216	return stmt
1217}
1218
1219func prepareStatements(db *sql.DB) {
1220	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1221	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1222	stmtUpdateHonker = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1223	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1224	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1225
1226	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 "
1227	limit := " order by honkid desc limit "
1228	stmtOneXonk = preparetodie(db, selecthonks+"where xid = ?")
1229	stmtPublicHonks = preparetodie(db, selecthonks+"where honker = ''"+limit+"50")
1230	stmtUserHonks = preparetodie(db, selecthonks+"where honker = '' and username = ?"+limit+"50")
1231	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")
1232	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")
1233	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+limit+"50")
1234	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+limit+"50")
1235	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or honker = '') and convoy = ?"+limit+"50")
1236
1237	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1238	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1239	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1240	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1241	stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ? and userid = ?")
1242	stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1243	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1244	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1245	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1246	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1247	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1248	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1249	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1250	stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1251	stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1252}
1253
1254func ElaborateUnitTests() {
1255}
1256
1257func finishusersetup() error {
1258	db := opendatabase()
1259	k, err := rsa.GenerateKey(rand.Reader, 2048)
1260	if err != nil {
1261		return err
1262	}
1263	pubkey, err := zem(&k.PublicKey)
1264	if err != nil {
1265		return err
1266	}
1267	seckey, err := zem(k)
1268	if err != nil {
1269		return err
1270	}
1271	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1272	if err != nil {
1273		return err
1274	}
1275	return nil
1276}
1277
1278func main() {
1279	cmd := "run"
1280	if len(os.Args) > 1 {
1281		cmd = os.Args[1]
1282	}
1283	switch cmd {
1284	case "init":
1285		initdb()
1286	case "upgrade":
1287		upgradedb()
1288	}
1289	db := opendatabase()
1290	dbversion := 0
1291	getconfig("dbversion", &dbversion)
1292	if dbversion != myVersion {
1293		log.Fatal("incorrect database version. run upgrade.")
1294	}
1295	getconfig("servername", &serverName)
1296	prepareStatements(db)
1297	switch cmd {
1298	case "ping":
1299		if len(os.Args) < 4 {
1300			fmt.Printf("usage: honk ping from to\n")
1301			return
1302		}
1303		name := os.Args[2]
1304		targ := os.Args[3]
1305		user, err := butwhatabout(name)
1306		if err != nil {
1307			log.Printf("unknown user")
1308			return
1309		}
1310		ping(user, targ)
1311	case "peep":
1312		peeppeep()
1313	case "run":
1314		serve()
1315	case "test":
1316		ElaborateUnitTests()
1317	default:
1318		log.Fatal("unknown command")
1319	}
1320}