all repos — honk @ e445336534e0b42be9523e086679175494da3686

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		log.Printf("updating honker follow: %s", who)
 341		rubadubdub(user, j)
 342	case "Accept":
 343		db := opendatabase()
 344		log.Printf("updating honker accept: %s", who)
 345		db.Exec("update honkers set flavor = 'sub' where xid = ? and flavor = 'presub'", who)
 346	case "Undo":
 347		obj, ok := jsongetmap(j, "object")
 348		if !ok {
 349			log.Printf("unknown undo no object")
 350		} else {
 351			what, _ := jsongetstring(obj, "type")
 352			switch what {
 353			case "Follow":
 354				log.Printf("updating honker undo: %s", who)
 355				db := opendatabase()
 356				db.Exec("update honkers set flavor = 'undub' where xid = ? and flavor = 'dub'", who)
 357			case "Like":
 358			default:
 359				log.Printf("unknown undo: %s", what)
 360			}
 361		}
 362	default:
 363		xonk := xonkxonk(j)
 364		if xonk != nil && needxonk(user, xonk) {
 365			xonk.UserID = user.ID
 366			savexonk(user, xonk)
 367		}
 368	}
 369}
 370
 371func outbox(w http.ResponseWriter, r *http.Request) {
 372	name := mux.Vars(r)["name"]
 373	user, err := butwhatabout(name)
 374	if err != nil {
 375		http.NotFound(w, r)
 376		return
 377	}
 378	honks := gethonksbyuser(name)
 379
 380	var jonks []map[string]interface{}
 381	for _, h := range honks {
 382		j, _ := jonkjonk(user, h)
 383		jonks = append(jonks, j)
 384	}
 385
 386	j := NewJunk()
 387	j["@context"] = itiswhatitis
 388	j["id"] = user.URL + "/outbox"
 389	j["type"] = "OrderedCollection"
 390	j["totalItems"] = len(jonks)
 391	j["orderedItems"] = jonks
 392
 393	w.Header().Set("Cache-Control", "max-age=60")
 394	w.Header().Set("Content-Type", theonetruename)
 395	WriteJunk(w, j)
 396}
 397
 398func emptiness(w http.ResponseWriter, r *http.Request) {
 399	name := mux.Vars(r)["name"]
 400	user, err := butwhatabout(name)
 401	if err != nil {
 402		http.NotFound(w, r)
 403		return
 404	}
 405	colname := "/followers"
 406	if strings.HasSuffix(r.URL.Path, "/following") {
 407		colname = "/following"
 408	}
 409	j := NewJunk()
 410	j["@context"] = itiswhatitis
 411	j["id"] = user.URL + colname
 412	j["type"] = "OrderedCollection"
 413	j["totalItems"] = 0
 414	j["orderedItems"] = []interface{}{}
 415
 416	w.Header().Set("Cache-Control", "max-age=60")
 417	w.Header().Set("Content-Type", theonetruename)
 418	WriteJunk(w, j)
 419}
 420
 421func viewuser(w http.ResponseWriter, r *http.Request) {
 422	name := mux.Vars(r)["name"]
 423	user, err := butwhatabout(name)
 424	if err != nil {
 425		http.NotFound(w, r)
 426		return
 427	}
 428	if friendorfoe(r.Header.Get("Accept")) {
 429		j := asjonker(user)
 430		w.Header().Set("Cache-Control", "max-age=600")
 431		w.Header().Set("Content-Type", theonetruename)
 432		WriteJunk(w, j)
 433		return
 434	}
 435	honks := gethonksbyuser(name)
 436	u := login.GetUserInfo(r)
 437	honkpage(w, r, u, user, honks, "")
 438}
 439
 440func viewhonker(w http.ResponseWriter, r *http.Request) {
 441	name := mux.Vars(r)["name"]
 442	u := login.GetUserInfo(r)
 443	honks := gethonksbyhonker(u.UserID, name)
 444	honkpage(w, r, u, nil, honks, "honks by honker: "+name)
 445}
 446
 447func viewcombo(w http.ResponseWriter, r *http.Request) {
 448	name := mux.Vars(r)["name"]
 449	u := login.GetUserInfo(r)
 450	honks := gethonksbycombo(u.UserID, name)
 451	honkpage(w, r, u, nil, honks, "honks by combo: "+name)
 452}
 453func viewconvoy(w http.ResponseWriter, r *http.Request) {
 454	c := r.FormValue("c")
 455	var userid int64 = -1
 456	u := login.GetUserInfo(r)
 457	if u != nil {
 458		userid = u.UserID
 459	}
 460	honks := gethonksbyconvoy(userid, c)
 461	honkpage(w, r, u, nil, honks, "honks in convoy: "+c)
 462}
 463
 464func fingerlicker(w http.ResponseWriter, r *http.Request) {
 465	orig := r.FormValue("resource")
 466
 467	log.Printf("finger lick: %s", orig)
 468
 469	if strings.HasPrefix(orig, "acct:") {
 470		orig = orig[5:]
 471	}
 472
 473	name := orig
 474	idx := strings.LastIndexByte(name, '/')
 475	if idx != -1 {
 476		name = name[idx+1:]
 477		if "https://"+serverName+"/u/"+name != orig {
 478			log.Printf("foreign request rejected")
 479			name = ""
 480		}
 481	} else {
 482		idx = strings.IndexByte(name, '@')
 483		if idx != -1 {
 484			name = name[:idx]
 485			if name+"@"+serverName != orig {
 486				log.Printf("foreign request rejected")
 487				name = ""
 488			}
 489		}
 490	}
 491	user, err := butwhatabout(name)
 492	if err != nil {
 493		http.NotFound(w, r)
 494		return
 495	}
 496
 497	j := NewJunk()
 498	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
 499	j["aliases"] = []string{user.URL}
 500	var links []map[string]interface{}
 501	l := NewJunk()
 502	l["rel"] = "self"
 503	l["type"] = `application/activity+json`
 504	l["href"] = user.URL
 505	links = append(links, l)
 506	j["links"] = links
 507
 508	w.Header().Set("Cache-Control", "max-age=3600")
 509	w.Header().Set("Content-Type", "application/jrd+json")
 510	WriteJunk(w, j)
 511}
 512
 513func viewhonk(w http.ResponseWriter, r *http.Request) {
 514	name := mux.Vars(r)["name"]
 515	xid := mux.Vars(r)["xid"]
 516	user, err := butwhatabout(name)
 517	if err != nil {
 518		http.NotFound(w, r)
 519		return
 520	}
 521	h := getxonk(name, xid)
 522	if h == nil {
 523		http.NotFound(w, r)
 524		return
 525	}
 526	if friendorfoe(r.Header.Get("Accept")) {
 527		_, j := jonkjonk(user, h)
 528		j["@context"] = itiswhatitis
 529		w.Header().Set("Cache-Control", "max-age=3600")
 530		w.Header().Set("Content-Type", theonetruename)
 531		WriteJunk(w, j)
 532		return
 533	}
 534	u := login.GetUserInfo(r)
 535	honkpage(w, r, u, nil, []*Honk{h}, "one honk")
 536}
 537
 538func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 539	honks []*Honk, infomsg string) {
 540	reverbolate(honks)
 541	templinfo := getInfo(r)
 542	if u != nil {
 543		if user != nil && u.Username == user.Name {
 544			templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
 545		}
 546		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 547	}
 548	if u == nil {
 549		w.Header().Set("Cache-Control", "max-age=60")
 550	}
 551	if user != nil {
 552		templinfo["Name"] = user.Name
 553		whatabout := user.About
 554		templinfo["RawWhatAbout"] = whatabout
 555		whatabout = obfusbreak(whatabout)
 556		templinfo["WhatAbout"] = cleanstring(whatabout)
 557	}
 558	templinfo["Honks"] = honks
 559	templinfo["ServerMessage"] = infomsg
 560	err := readviews.ExecuteTemplate(w, "honkpage.html", templinfo)
 561	if err != nil {
 562		log.Print(err)
 563	}
 564}
 565
 566func saveuser(w http.ResponseWriter, r *http.Request) {
 567	whatabout := r.FormValue("whatabout")
 568	u := login.GetUserInfo(r)
 569	db := opendatabase()
 570	_, err := db.Exec("update users set about = ? where username = ?", whatabout, u.Username)
 571	if err != nil {
 572		log.Printf("error bouting what: %s", err)
 573	}
 574
 575	http.Redirect(w, r, "/u/"+u.Username, http.StatusSeeOther)
 576}
 577
 578func gethonkers(userid int64) []*Honker {
 579	rows, err := stmtHonkers.Query(userid)
 580	if err != nil {
 581		log.Printf("error querying honkers: %s", err)
 582		return nil
 583	}
 584	defer rows.Close()
 585	var honkers []*Honker
 586	for rows.Next() {
 587		var f Honker
 588		var combos string
 589		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 590		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 591		if err != nil {
 592			log.Printf("error scanning honker: %s", err)
 593			return nil
 594		}
 595		honkers = append(honkers, &f)
 596	}
 597	return honkers
 598}
 599
 600func getdubs(userid int64) []*Honker {
 601	rows, err := stmtDubbers.Query(userid)
 602	if err != nil {
 603		log.Printf("error querying dubs: %s", err)
 604		return nil
 605	}
 606	defer rows.Close()
 607	var honkers []*Honker
 608	for rows.Next() {
 609		var f Honker
 610		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 611		if err != nil {
 612			log.Printf("error scanning honker: %s", err)
 613			return nil
 614		}
 615		honkers = append(honkers, &f)
 616	}
 617	return honkers
 618}
 619
 620func getxonk(name, xid string) *Honk {
 621	var h Honk
 622	var dt, aud string
 623	row := stmtOneXonk.QueryRow(xid)
 624	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
 625		&dt, &h.URL, &aud, &h.Noise, &h.Convoy)
 626	if err != nil {
 627		if err != sql.ErrNoRows {
 628			log.Printf("error scanning xonk: %s", err)
 629		}
 630		return nil
 631	}
 632	if name != "" && h.Username != name {
 633		log.Printf("user xonk mismatch")
 634		return nil
 635	}
 636	h.Date, _ = time.Parse(dbtimeformat, dt)
 637	h.Audience = strings.Split(aud, " ")
 638	donksforhonks([]*Honk{&h})
 639	return &h
 640}
 641
 642func getpublichonks() []*Honk {
 643	rows, err := stmtPublicHonks.Query()
 644	return getsomehonks(rows, err)
 645}
 646func gethonksbyuser(name string) []*Honk {
 647	rows, err := stmtUserHonks.Query(name)
 648	return getsomehonks(rows, err)
 649}
 650func gethonksforuser(userid int64) []*Honk {
 651	dt := time.Now().UTC().Add(-2 * 24 * time.Hour)
 652	rows, err := stmtHonksForUser.Query(userid, dt.Format(dbtimeformat), userid)
 653	return getsomehonks(rows, err)
 654}
 655func gethonksforme(userid int64) []*Honk {
 656	dt := time.Now().UTC().Add(-4 * 24 * time.Hour)
 657	rows, err := stmtHonksForMe.Query(userid, dt.Format(dbtimeformat), userid)
 658	return getsomehonks(rows, err)
 659}
 660func gethonksbyhonker(userid int64, honker string) []*Honk {
 661	rows, err := stmtHonksByHonker.Query(userid, honker)
 662	return getsomehonks(rows, err)
 663}
 664func gethonksbycombo(userid int64, combo string) []*Honk {
 665	combo = "% " + combo + " %"
 666	rows, err := stmtHonksByCombo.Query(userid, combo)
 667	return getsomehonks(rows, err)
 668}
 669func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 670	rows, err := stmtHonksByConvoy.Query(userid, convoy)
 671	return getsomehonks(rows, err)
 672}
 673
 674func getsomehonks(rows *sql.Rows, err error) []*Honk {
 675	if err != nil {
 676		log.Printf("error querying honks: %s", err)
 677		return nil
 678	}
 679	defer rows.Close()
 680	var honks []*Honk
 681	for rows.Next() {
 682		var h Honk
 683		var dt, aud string
 684		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.XID, &h.RID,
 685			&dt, &h.URL, &aud, &h.Noise, &h.Convoy)
 686		if err != nil {
 687			log.Printf("error scanning honks: %s", err)
 688			return nil
 689		}
 690		h.Date, _ = time.Parse(dbtimeformat, dt)
 691		h.Audience = strings.Split(aud, " ")
 692		honks = append(honks, &h)
 693	}
 694	rows.Close()
 695	donksforhonks(honks)
 696	return honks
 697}
 698
 699func donksforhonks(honks []*Honk) {
 700	db := opendatabase()
 701	var ids []string
 702	hmap := make(map[int64]*Honk)
 703	for _, h := range honks {
 704		if h.What == "zonk" {
 705			continue
 706		}
 707		ids = append(ids, fmt.Sprintf("%d", h.ID))
 708		hmap[h.ID] = h
 709	}
 710	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, ","))
 711	rows, err := db.Query(q)
 712	if err != nil {
 713		log.Printf("error querying donks: %s", err)
 714		return
 715	}
 716	defer rows.Close()
 717	for rows.Next() {
 718		var hid int64
 719		var d Donk
 720		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media)
 721		if err != nil {
 722			log.Printf("error scanning donk: %s", err)
 723			continue
 724		}
 725		h := hmap[hid]
 726		h.Donks = append(h.Donks, &d)
 727	}
 728}
 729
 730func savebonk(w http.ResponseWriter, r *http.Request) {
 731	xid := r.FormValue("xid")
 732
 733	log.Printf("bonking %s", xid)
 734
 735	xonk := getxonk("", xid)
 736	if xonk == nil {
 737		return
 738	}
 739	if xonk.Honker == "" {
 740		xonk.XID = fmt.Sprintf("https://%s/u/%s/h/%s", serverName, xonk.Username, xonk.XID)
 741	}
 742	convoy := xonk.Convoy
 743
 744	userinfo := login.GetUserInfo(r)
 745
 746	dt := time.Now().UTC()
 747	bonk := Honk{
 748		UserID:   userinfo.UserID,
 749		Username: userinfo.Username,
 750		Honker:   xonk.Honker,
 751		What:     "bonk",
 752		XID:      xonk.XID,
 753		Date:     dt,
 754		Noise:    xonk.Noise,
 755		Convoy:   convoy,
 756		Donks:    xonk.Donks,
 757		Audience: oneofakind(prepend(thewholeworld, xonk.Audience)),
 758	}
 759
 760	user, _ := butwhatabout(userinfo.Username)
 761
 762	aud := strings.Join(bonk.Audience, " ")
 763	whofore := 0
 764	if strings.Contains(aud, user.URL) {
 765		whofore = 1
 766	}
 767	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", "", xid, "",
 768		dt.Format(dbtimeformat), "", aud, bonk.Noise, bonk.Convoy, whofore)
 769	if err != nil {
 770		log.Printf("error saving bonk: %s", err)
 771		return
 772	}
 773	bonk.ID, _ = res.LastInsertId()
 774	for _, d := range bonk.Donks {
 775		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 776		if err != nil {
 777			log.Printf("err saving donk: %s", err)
 778			return
 779		}
 780	}
 781
 782	go honkworldwide(user, &bonk)
 783
 784}
 785
 786func zonkit(w http.ResponseWriter, r *http.Request) {
 787	xid := r.FormValue("xid")
 788
 789	log.Printf("zonking %s", xid)
 790	userinfo := login.GetUserInfo(r)
 791	stmtZonkIt.Exec(userinfo.UserID, xid)
 792}
 793
 794func savehonk(w http.ResponseWriter, r *http.Request) {
 795	rid := r.FormValue("rid")
 796	noise := r.FormValue("noise")
 797
 798	userinfo := login.GetUserInfo(r)
 799
 800	dt := time.Now().UTC()
 801	xid := xfiltrate()
 802	what := "honk"
 803	if rid != "" {
 804		what = "tonk"
 805	}
 806	honk := Honk{
 807		UserID:   userinfo.UserID,
 808		Username: userinfo.Username,
 809		What:     "honk",
 810		XID:      xid,
 811		RID:      rid,
 812		Date:     dt,
 813	}
 814	if noise[0] == '@' {
 815		honk.Audience = append(grapevine(noise), thewholeworld)
 816	} else {
 817		honk.Audience = prepend(thewholeworld, grapevine(noise))
 818	}
 819	var convoy string
 820	if rid != "" {
 821		xonk := getxonk("", rid)
 822		if xonk != nil {
 823			honk.Audience = append(honk.Audience, xonk.Audience...)
 824			convoy = xonk.Convoy
 825		} else {
 826			xonkaud, c := whosthere(rid)
 827			honk.Audience = append(honk.Audience, xonkaud...)
 828			convoy = c
 829		}
 830	}
 831	if convoy == "" {
 832		convoy = "data:,electrichonkytonk-" + xfiltrate()
 833	}
 834	honk.Audience = oneofakind(honk.Audience)
 835	noise = obfusbreak(noise)
 836	honk.Noise = noise
 837	honk.Convoy = convoy
 838
 839	file, filehdr, err := r.FormFile("donk")
 840	if err == nil {
 841		var buf bytes.Buffer
 842		io.Copy(&buf, file)
 843		file.Close()
 844		data := buf.Bytes()
 845		xid := xfiltrate()
 846		var media, name string
 847		img, format, err := image.Decode(&buf)
 848		if err == nil {
 849			data, format, err = vacuumwrap(img, format)
 850			if err != nil {
 851				log.Printf("can't vacuum image: %s", err)
 852				return
 853			}
 854			media = "image/" + format
 855			if format == "jpeg" {
 856				format = "jpg"
 857			}
 858			name = xid + "." + format
 859			xid = name
 860		} else {
 861			maxsize := 100000
 862			if len(data) > maxsize {
 863				log.Printf("bad image: %s too much text: %d", err, len(data))
 864				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 865				return
 866			}
 867			for i := 0; i < len(data); i++ {
 868				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 869					log.Printf("bad image: %s not text: %d", err, data[i])
 870					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 871					return
 872				}
 873			}
 874			media = "text/plain"
 875			name = filehdr.Filename
 876			if name == "" {
 877				name = xid + ".txt"
 878			}
 879			xid += ".txt"
 880		}
 881		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 882		res, err := stmtSaveFile.Exec(xid, name, url, media, data)
 883		if err != nil {
 884			log.Printf("unable to save image: %s", err)
 885			return
 886		}
 887		var d Donk
 888		d.FileID, _ = res.LastInsertId()
 889		d.XID = name
 890		d.Name = name
 891		d.Media = media
 892		d.URL = url
 893		honk.Donks = append(honk.Donks, &d)
 894	}
 895	herd := herdofemus(honk.Noise)
 896	for _, e := range herd {
 897		donk := savedonk(e.ID, e.Name, "image/png")
 898		if donk != nil {
 899			donk.Name = e.Name
 900			honk.Donks = append(honk.Donks, donk)
 901		}
 902	}
 903
 904	user, _ := butwhatabout(userinfo.Username)
 905
 906	aud := strings.Join(honk.Audience, " ")
 907	whofore := 0
 908	if strings.Contains(aud, user.URL) {
 909		whofore = 1
 910	}
 911	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
 912		dt.Format(dbtimeformat), "", aud, noise, convoy, whofore)
 913	if err != nil {
 914		log.Printf("error saving honk: %s", err)
 915		return
 916	}
 917	honk.ID, _ = res.LastInsertId()
 918	for _, d := range honk.Donks {
 919		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 920		if err != nil {
 921			log.Printf("err saving donk: %s", err)
 922			return
 923		}
 924	}
 925
 926	go honkworldwide(user, &honk)
 927
 928	http.Redirect(w, r, "/", http.StatusSeeOther)
 929}
 930
 931func viewhonkers(w http.ResponseWriter, r *http.Request) {
 932	userinfo := login.GetUserInfo(r)
 933	templinfo := getInfo(r)
 934	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 935	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
 936	err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
 937	if err != nil {
 938		log.Print(err)
 939	}
 940}
 941
 942var handfull = make(map[string]string)
 943var handlock sync.Mutex
 944
 945func gofish(name string) string {
 946	if name[0] == '@' {
 947		name = name[1:]
 948	}
 949	m := strings.Split(name, "@")
 950	if len(m) != 2 {
 951		log.Printf("bad fish name: %s", name)
 952		return ""
 953	}
 954	handlock.Lock()
 955	ref, ok := handfull[name]
 956	handlock.Unlock()
 957	if ok {
 958		return ref
 959	}
 960	j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
 961	handlock.Lock()
 962	defer handlock.Unlock()
 963	if err != nil {
 964		log.Printf("failed to go fish %s: %s", name, err)
 965		handfull[name] = ""
 966		return ""
 967	}
 968	links, _ := jsongetarray(j, "links")
 969	for _, l := range links {
 970		href, _ := jsongetstring(l, "href")
 971		rel, _ := jsongetstring(l, "rel")
 972		t, _ := jsongetstring(l, "type")
 973		if rel == "self" && friendorfoe(t) {
 974			handfull[name] = href
 975			return href
 976		}
 977	}
 978	handfull[name] = ""
 979	return ""
 980}
 981
 982func savehonker(w http.ResponseWriter, r *http.Request) {
 983	u := login.GetUserInfo(r)
 984	name := r.FormValue("name")
 985	url := r.FormValue("url")
 986	peep := r.FormValue("peep")
 987	combos := r.FormValue("combos")
 988	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
 989
 990	if honkerid > 0 {
 991		combos = " " + strings.TrimSpace(combos) + " "
 992		_, err := stmtUpdateHonker.Exec(combos, honkerid, u.UserID)
 993		if err != nil {
 994			log.Printf("update honker err: %s", err)
 995			return
 996		}
 997		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
 998	}
 999
1000	flavor := "presub"
1001	if peep == "peep" {
1002		flavor = "peep"
1003	}
1004	if url == "" {
1005		return
1006	}
1007	if url[0] == '@' {
1008		url = gofish(url)
1009	}
1010	if url == "" {
1011		return
1012	}
1013	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1014	if err != nil {
1015		log.Print(err)
1016		return
1017	}
1018	if flavor == "presub" {
1019		user, _ := butwhatabout(u.Username)
1020		go subsub(user, url)
1021	}
1022	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1023}
1024
1025type Zonker struct {
1026	Name      string
1027	Wherefore string
1028}
1029
1030func killzone(w http.ResponseWriter, r *http.Request) {
1031	db := opendatabase()
1032	userinfo := login.GetUserInfo(r)
1033	rows, err := db.Query("select name, wherefore from zonkers where userid = ?", userinfo.UserID)
1034	if err != nil {
1035		log.Printf("err: %s", err)
1036		return
1037	}
1038	var zonkers []Zonker
1039	for rows.Next() {
1040		var z Zonker
1041		rows.Scan(&z.Name, &z.Wherefore)
1042		zonkers = append(zonkers, z)
1043	}
1044	templinfo := getInfo(r)
1045	templinfo["Zonkers"] = zonkers
1046	templinfo["KillCSRF"] = login.GetCSRF("killitwithfire", r)
1047	err = readviews.ExecuteTemplate(w, "zonkers.html", templinfo)
1048	if err != nil {
1049		log.Print(err)
1050	}
1051}
1052
1053func killitwithfire(w http.ResponseWriter, r *http.Request) {
1054	userinfo := login.GetUserInfo(r)
1055	wherefore := r.FormValue("wherefore")
1056	name := r.FormValue("name")
1057	if name == "" {
1058		return
1059	}
1060	switch wherefore {
1061	case "zonker":
1062	case "zurl":
1063	case "zonvoy":
1064	default:
1065		return
1066	}
1067	db := opendatabase()
1068	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1069		userinfo.UserID, name, wherefore)
1070
1071	http.Redirect(w, r, "/killzone", http.StatusSeeOther)
1072}
1073
1074func somedays() string {
1075	secs := 432000 + notrand.Int63n(432000)
1076	return fmt.Sprintf("%d", secs)
1077}
1078
1079func avatate(w http.ResponseWriter, r *http.Request) {
1080	n := r.FormValue("a")
1081	a := avatar(n)
1082	w.Header().Set("Cache-Control", "max-age="+somedays())
1083	w.Write(a)
1084}
1085
1086func servecss(w http.ResponseWriter, r *http.Request) {
1087	w.Header().Set("Cache-Control", "max-age=7776000")
1088	http.ServeFile(w, r, "views"+r.URL.Path)
1089}
1090func servehtml(w http.ResponseWriter, r *http.Request) {
1091	templinfo := getInfo(r)
1092	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1093	if err != nil {
1094		log.Print(err)
1095	}
1096}
1097func serveemu(w http.ResponseWriter, r *http.Request) {
1098	xid := mux.Vars(r)["xid"]
1099	w.Header().Set("Cache-Control", "max-age="+somedays())
1100	http.ServeFile(w, r, "emus/"+xid)
1101}
1102
1103func servefile(w http.ResponseWriter, r *http.Request) {
1104	xid := mux.Vars(r)["xid"]
1105	row := stmtFileData.QueryRow(xid)
1106	var media string
1107	var data []byte
1108	err := row.Scan(&media, &data)
1109	if err != nil {
1110		log.Printf("error loading file: %s", err)
1111		http.NotFound(w, r)
1112		return
1113	}
1114	w.Header().Set("Content-Type", media)
1115	w.Header().Set("X-Content-Type-Options", "nosniff")
1116	w.Header().Set("Cache-Control", "max-age="+somedays())
1117	w.Write(data)
1118}
1119
1120func serve() {
1121	db := opendatabase()
1122	login.Init(db)
1123
1124	listener, err := openListener()
1125	if err != nil {
1126		log.Fatal(err)
1127	}
1128	go redeliverator()
1129
1130	debug := false
1131	getconfig("debug", &debug)
1132	readviews = ParseTemplates(debug,
1133		"views/honkpage.html",
1134		"views/honkers.html",
1135		"views/zonkers.html",
1136		"views/honkform.html",
1137		"views/honk.html",
1138		"views/login.html",
1139		"views/header.html",
1140	)
1141	if !debug {
1142		s := "views/style.css"
1143		savedstyleparams[s] = getstyleparam(s)
1144		s = "views/local.css"
1145		savedstyleparams[s] = getstyleparam(s)
1146	}
1147
1148	mux := mux.NewRouter()
1149	mux.Use(login.Checker)
1150
1151	posters := mux.Methods("POST").Subrouter()
1152	getters := mux.Methods("GET").Subrouter()
1153
1154	getters.HandleFunc("/", homepage)
1155	getters.HandleFunc("/rss", showrss)
1156	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1157	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1158	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1159	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1160	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1161	getters.HandleFunc("/u/{name:[[:alnum:]]+}/followers", emptiness)
1162	getters.HandleFunc("/u/{name:[[:alnum:]]+}/following", emptiness)
1163	getters.HandleFunc("/a", avatate)
1164	getters.HandleFunc("/t", viewconvoy)
1165	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1166	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1167	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1168
1169	getters.HandleFunc("/style.css", servecss)
1170	getters.HandleFunc("/local.css", servecss)
1171	getters.HandleFunc("/login", servehtml)
1172	posters.HandleFunc("/dologin", login.LoginFunc)
1173	getters.HandleFunc("/logout", login.LogoutFunc)
1174
1175	loggedin := mux.NewRoute().Subrouter()
1176	loggedin.Use(login.Required)
1177	loggedin.HandleFunc("/atme", homepage)
1178	loggedin.HandleFunc("/killzone", killzone)
1179	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1180	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1181	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1182	loggedin.Handle("/killitwithfire", login.CSRFWrap("killitwithfire", http.HandlerFunc(killitwithfire)))
1183	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1184	loggedin.HandleFunc("/honkers", viewhonkers)
1185	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1186	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", viewcombo)
1187	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1188
1189	err = http.Serve(listener, mux)
1190	if err != nil {
1191		log.Fatal(err)
1192	}
1193}
1194
1195var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateHonker *sql.Stmt
1196var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1197var stmtHonksForUser, stmtHonksForMe, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1198var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1199var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1200var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1201var stmtHasHonker, stmtThumbBiter, stmtZonkIt *sql.Stmt
1202
1203func preparetodie(db *sql.DB, s string) *sql.Stmt {
1204	stmt, err := db.Prepare(s)
1205	if err != nil {
1206		log.Fatalf("error %s: %s", err, s)
1207	}
1208	return stmt
1209}
1210
1211func prepareStatements(db *sql.DB) {
1212	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1213	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1214	stmtUpdateHonker = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1215	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1216	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1217
1218	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 "
1219	limit := " order by honkid desc limit "
1220	stmtOneXonk = preparetodie(db, selecthonks+"where xid = ?")
1221	stmtPublicHonks = preparetodie(db, selecthonks+"where honker = ''"+limit+"50")
1222	stmtUserHonks = preparetodie(db, selecthonks+"where honker = '' and username = ?"+limit+"50")
1223	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")
1224	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")
1225	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+limit+"50")
1226	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+limit+"50")
1227	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or honker = '') and convoy = ?"+limit+"50")
1228
1229	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1230	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1231	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1232	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1233	stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ? and userid = ?")
1234	stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1235	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1236	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1237	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1238	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1239	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1240	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1241	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1242	stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1243	stmtThumbBiter = preparetodie(db, "select zonkerid from zonkers where ((name = ? and wherefore = 'zonker') or (name = ? and wherefore = 'zurl')) and userid = ?")
1244}
1245
1246func ElaborateUnitTests() {
1247}
1248
1249func finishusersetup() error {
1250	db := opendatabase()
1251	k, err := rsa.GenerateKey(rand.Reader, 2048)
1252	if err != nil {
1253		return err
1254	}
1255	pubkey, err := zem(&k.PublicKey)
1256	if err != nil {
1257		return err
1258	}
1259	seckey, err := zem(k)
1260	if err != nil {
1261		return err
1262	}
1263	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1264	if err != nil {
1265		return err
1266	}
1267	return nil
1268}
1269
1270func main() {
1271	cmd := "run"
1272	if len(os.Args) > 1 {
1273		cmd = os.Args[1]
1274	}
1275	switch cmd {
1276	case "init":
1277		initdb()
1278	case "upgrade":
1279		upgradedb()
1280	}
1281	db := opendatabase()
1282	dbversion := 0
1283	getconfig("dbversion", &dbversion)
1284	if dbversion != myVersion {
1285		log.Fatal("incorrect database version. run upgrade.")
1286	}
1287	getconfig("servername", &serverName)
1288	prepareStatements(db)
1289	switch cmd {
1290	case "ping":
1291		if len(os.Args) < 4 {
1292			fmt.Printf("usage: honk ping from to\n")
1293			return
1294		}
1295		name := os.Args[2]
1296		targ := os.Args[3]
1297		user, err := butwhatabout(name)
1298		if err != nil {
1299			log.Printf("unknown user")
1300			return
1301		}
1302		ping(user, targ)
1303	case "peep":
1304		peeppeep()
1305	case "run":
1306		serve()
1307	case "test":
1308		ElaborateUnitTests()
1309	default:
1310		log.Fatal("unknown command")
1311	}
1312}