all repos — honk @ 5919024da1616a2634b0a2bb121d21076f604350

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