all repos — honk @ 97d112aad61b5c35e0a18963319c78405d9bb606

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	"database/sql"
  21	"fmt"
  22	"html"
  23	"html/template"
  24	"io"
  25	"log"
  26	notrand "math/rand"
  27	"net/http"
  28	"net/url"
  29	"os"
  30	"sort"
  31	"strconv"
  32	"strings"
  33	"time"
  34
  35	"github.com/gorilla/mux"
  36	"humungus.tedunangst.com/r/webs/htfilter"
  37	"humungus.tedunangst.com/r/webs/httpsig"
  38	"humungus.tedunangst.com/r/webs/image"
  39	"humungus.tedunangst.com/r/webs/junk"
  40	"humungus.tedunangst.com/r/webs/login"
  41	"humungus.tedunangst.com/r/webs/rss"
  42	"humungus.tedunangst.com/r/webs/templates"
  43)
  44
  45type WhatAbout struct {
  46	ID        int64
  47	Name      string
  48	Display   string
  49	About     string
  50	Key       string
  51	URL       string
  52	SkinnyCSS bool
  53}
  54
  55type Honk struct {
  56	ID       int64
  57	UserID   int64
  58	Username string
  59	What     string
  60	Honker   string
  61	Handle   string
  62	Oonker   string
  63	Oondle   string
  64	XID      string
  65	RID      string
  66	Date     time.Time
  67	URL      string
  68	Noise    string
  69	Precis   string
  70	Convoy   string
  71	Audience []string
  72	Public   bool
  73	Whofore  int64
  74	Replies  []*Honk
  75	Flags    int64
  76	HTML     template.HTML
  77	Style    string
  78	Open     string
  79	Donks    []*Donk
  80	Onts     []string
  81}
  82
  83const (
  84	flagIsAcked  = 1
  85	flagIsBonked = 2
  86)
  87
  88func (honk *Honk) IsAcked() bool {
  89	return honk.Flags&flagIsAcked != 0
  90}
  91
  92func (honk *Honk) IsBonked() bool {
  93	return honk.Flags&flagIsBonked != 0
  94}
  95
  96type Donk struct {
  97	FileID  int64
  98	XID     string
  99	Name    string
 100	URL     string
 101	Media   string
 102	Local   bool
 103	Content []byte
 104}
 105
 106type Honker struct {
 107	ID     int64
 108	UserID int64
 109	Name   string
 110	XID    string
 111	Handle string
 112	Flavor string
 113	Combos []string
 114}
 115
 116var serverName string
 117var iconName = "icon.png"
 118var serverMsg = "Things happen."
 119
 120var userSep = "u"
 121var honkSep = "h"
 122
 123var readviews *templates.Template
 124
 125func getuserstyle(u *login.UserInfo) template.CSS {
 126	if u == nil {
 127		return ""
 128	}
 129	user, _ := butwhatabout(u.Username)
 130	if user.SkinnyCSS {
 131		return "main { max-width: 700px; }"
 132	}
 133	return ""
 134}
 135
 136func getInfo(r *http.Request) map[string]interface{} {
 137	u := login.GetUserInfo(r)
 138	templinfo := make(map[string]interface{})
 139	templinfo["StyleParam"] = getstyleparam("views/style.css")
 140	templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
 141	templinfo["UserStyle"] = getuserstyle(u)
 142	templinfo["ServerName"] = serverName
 143	templinfo["IconName"] = iconName
 144	templinfo["UserInfo"] = u
 145	templinfo["UserSep"] = userSep
 146	return templinfo
 147}
 148
 149var donotfedafterdark = make(map[string]bool)
 150
 151func stealthed(r *http.Request) bool {
 152	addr := r.Header.Get("X-Forwarded-For")
 153	fake := donotfedafterdark[addr]
 154	if fake {
 155		log.Printf("faking 404 for %s", addr)
 156	}
 157	return fake
 158}
 159
 160func homepage(w http.ResponseWriter, r *http.Request) {
 161	templinfo := getInfo(r)
 162	u := login.GetUserInfo(r)
 163	var honks []*Honk
 164	var userid int64 = -1
 165	if r.URL.Path == "/front" || u == nil {
 166		honks = getpublichonks()
 167	} else {
 168		userid = u.UserID
 169		if r.URL.Path == "/atme" {
 170			honks = gethonksforme(userid)
 171		} else {
 172			honks = gethonksforuser(userid)
 173			honks = osmosis(honks, userid)
 174			if len(honks) > 0 {
 175				templinfo["TopXID"] = honks[0].XID
 176			}
 177		}
 178		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 179	}
 180
 181	tname := "honkpage.html"
 182	if topxid := r.FormValue("topxid"); topxid != "" {
 183		for i, h := range honks {
 184			if h.XID == topxid {
 185				honks = honks[0:i]
 186				break
 187			}
 188		}
 189		log.Printf("topxid %d frags", len(honks))
 190		tname = "honkfrags.html"
 191	}
 192
 193	reverbolate(userid, honks)
 194
 195	templinfo["Honks"] = honks
 196	templinfo["ShowRSS"] = true
 197	templinfo["ServerMessage"] = serverMsg
 198	if u == nil {
 199		w.Header().Set("Cache-Control", "max-age=60")
 200	} else {
 201		w.Header().Set("Cache-Control", "max-age=0")
 202	}
 203	w.Header().Set("Content-Type", "text/html; charset=utf-8")
 204
 205	err := readviews.Execute(w, tname, templinfo)
 206	if err != nil {
 207		log.Print(err)
 208	}
 209}
 210
 211func showfunzone(w http.ResponseWriter, r *http.Request) {
 212	var emunames, memenames []string
 213	dir, err := os.Open("emus")
 214	if err == nil {
 215		emunames, _ = dir.Readdirnames(0)
 216		dir.Close()
 217	}
 218	for i, e := range emunames {
 219		if len(e) > 4 {
 220			emunames[i] = e[:len(e)-4]
 221		}
 222	}
 223	dir, err = os.Open("memes")
 224	if err == nil {
 225		memenames, _ = dir.Readdirnames(0)
 226		dir.Close()
 227	}
 228	templinfo := getInfo(r)
 229	templinfo["Emus"] = emunames
 230	templinfo["Memes"] = memenames
 231	err = readviews.Execute(w, "funzone.html", templinfo)
 232	if err != nil {
 233		log.Print(err)
 234	}
 235
 236}
 237
 238func showrss(w http.ResponseWriter, r *http.Request) {
 239	name := mux.Vars(r)["name"]
 240
 241	var honks []*Honk
 242	if name != "" {
 243		honks = gethonksbyuser(name, false)
 244	} else {
 245		honks = getpublichonks()
 246	}
 247	reverbolate(-1, honks)
 248
 249	home := fmt.Sprintf("https://%s/", serverName)
 250	base := home
 251	if name != "" {
 252		home += "u/" + name
 253		name += " "
 254	}
 255	feed := rss.Feed{
 256		Title:       name + "honk",
 257		Link:        home,
 258		Description: name + "honk rss",
 259		Image: &rss.Image{
 260			URL:   base + "icon.png",
 261			Title: name + "honk rss",
 262			Link:  home,
 263		},
 264	}
 265	var modtime time.Time
 266	for _, honk := range honks {
 267		if !firstclass(honk) {
 268			continue
 269		}
 270		desc := string(honk.HTML)
 271		for _, d := range honk.Donks {
 272			desc += fmt.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
 273				d.URL, html.EscapeString(d.Name))
 274		}
 275
 276		feed.Items = append(feed.Items, &rss.Item{
 277			Title:       fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
 278			Description: rss.CData{desc},
 279			Link:        honk.URL,
 280			PubDate:     honk.Date.Format(time.RFC1123),
 281			Guid:        &rss.Guid{IsPermaLink: true, Value: honk.URL},
 282		})
 283		if honk.Date.After(modtime) {
 284			modtime = honk.Date
 285		}
 286	}
 287	w.Header().Set("Cache-Control", "max-age=300")
 288	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 289
 290	err := feed.Write(w)
 291	if err != nil {
 292		log.Printf("error writing rss: %s", err)
 293	}
 294}
 295
 296func butwhatabout(name string) (*WhatAbout, error) {
 297	row := stmtWhatAbout.QueryRow(name)
 298	var user WhatAbout
 299	var options string
 300	err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key, &options)
 301	user.URL = fmt.Sprintf("https://%s/%s/%s", serverName, userSep, user.Name)
 302	user.SkinnyCSS = strings.Contains(options, " skinny ")
 303	return &user, err
 304}
 305
 306func crappola(j junk.Junk) bool {
 307	t, _ := j.GetString("type")
 308	a, _ := j.GetString("actor")
 309	o, _ := j.GetString("object")
 310	if t == "Delete" && a == o {
 311		log.Printf("crappola from %s", a)
 312		return true
 313	}
 314	return false
 315}
 316
 317func ping(user *WhatAbout, who string) {
 318	box, err := getboxes(who)
 319	if err != nil {
 320		log.Printf("no inbox for ping: %s", err)
 321		return
 322	}
 323	j := junk.New()
 324	j["@context"] = itiswhatitis
 325	j["type"] = "Ping"
 326	j["id"] = user.URL + "/ping/" + xfiltrate()
 327	j["actor"] = user.URL
 328	j["to"] = who
 329	keyname, key := ziggy(user.Name)
 330	err = PostJunk(keyname, key, box.In, j)
 331	if err != nil {
 332		log.Printf("can't send ping: %s", err)
 333		return
 334	}
 335	log.Printf("sent ping to %s: %s", who, j["id"])
 336}
 337
 338func pong(user *WhatAbout, who string, obj string) {
 339	box, err := getboxes(who)
 340	if err != nil {
 341		log.Printf("no inbox for pong %s : %s", who, err)
 342		return
 343	}
 344	j := junk.New()
 345	j["@context"] = itiswhatitis
 346	j["type"] = "Pong"
 347	j["id"] = user.URL + "/pong/" + xfiltrate()
 348	j["actor"] = user.URL
 349	j["to"] = who
 350	j["object"] = obj
 351	keyname, key := ziggy(user.Name)
 352	err = PostJunk(keyname, key, box.In, j)
 353	if err != nil {
 354		log.Printf("can't send pong: %s", err)
 355		return
 356	}
 357}
 358
 359func inbox(w http.ResponseWriter, r *http.Request) {
 360	name := mux.Vars(r)["name"]
 361	user, err := butwhatabout(name)
 362	if err != nil {
 363		http.NotFound(w, r)
 364		return
 365	}
 366	var buf bytes.Buffer
 367	io.Copy(&buf, r.Body)
 368	payload := buf.Bytes()
 369	j, err := junk.Read(bytes.NewReader(payload))
 370	if err != nil {
 371		log.Printf("bad payload: %s", err)
 372		io.WriteString(os.Stdout, "bad payload\n")
 373		os.Stdout.Write(payload)
 374		io.WriteString(os.Stdout, "\n")
 375		return
 376	}
 377	if crappola(j) {
 378		return
 379	}
 380	keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
 381	if err != nil {
 382		log.Printf("inbox message failed signature: %s", err)
 383		if keyname != "" {
 384			keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
 385			if err != nil {
 386				log.Printf("still failed: %s", err)
 387			}
 388		}
 389		if err != nil {
 390			return
 391		}
 392	}
 393	what, _ := j.GetString("type")
 394	if what == "Like" {
 395		return
 396	}
 397	who, _ := j.GetString("actor")
 398	origin := keymatch(keyname, who)
 399	if origin == "" {
 400		log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 401		return
 402	}
 403	objid, _ := j.GetString("id")
 404	if thoudostbitethythumb(user.ID, []string{who}, objid) {
 405		log.Printf("ignoring thumb sucker %s", who)
 406		return
 407	}
 408	switch what {
 409	case "Ping":
 410		obj, _ := j.GetString("id")
 411		log.Printf("ping from %s: %s", who, obj)
 412		pong(user, who, obj)
 413	case "Pong":
 414		obj, _ := j.GetString("object")
 415		log.Printf("pong from %s: %s", who, obj)
 416	case "Follow":
 417		obj, _ := j.GetString("object")
 418		if obj == user.URL {
 419			log.Printf("updating honker follow: %s", who)
 420			stmtSaveDub.Exec(user.ID, who, who, "dub")
 421			go rubadubdub(user, j)
 422		} else {
 423			log.Printf("can't follow %s", obj)
 424		}
 425	case "Accept":
 426		log.Printf("updating honker accept: %s", who)
 427		_, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
 428		if err != nil {
 429			log.Printf("error updating honker: %s", err)
 430			return
 431		}
 432	case "Update":
 433		obj, ok := j.GetMap("object")
 434		if ok {
 435			what, _ := obj.GetString("type")
 436			switch what {
 437			case "Person":
 438				return
 439			case "Question":
 440				return
 441			}
 442		}
 443		log.Printf("unknown Update activity")
 444		fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 445		j.Write(fd)
 446		io.WriteString(fd, "\n")
 447		fd.Close()
 448
 449	case "Undo":
 450		obj, ok := j.GetMap("object")
 451		if !ok {
 452			log.Printf("unknown undo no object")
 453		} else {
 454			what, _ := obj.GetString("type")
 455			switch what {
 456			case "Follow":
 457				log.Printf("updating honker undo: %s", who)
 458				_, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
 459				if err != nil {
 460					log.Printf("error updating honker: %s", err)
 461					return
 462				}
 463			case "Announce":
 464				fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 465				j.Write(fd)
 466				io.WriteString(fd, "\n")
 467				fd.Close()
 468				log.Printf("an announcement has been undone")
 469				xid, _ := obj.GetString("object")
 470				log.Printf("undo announce: %s", xid)
 471			case "Like":
 472			default:
 473				log.Printf("unknown undo: %s", what)
 474			}
 475		}
 476	default:
 477		go consumeactivity(user, j, origin)
 478	}
 479}
 480
 481func ximport(w http.ResponseWriter, r *http.Request) {
 482	xid := r.FormValue("xid")
 483	p := investigate(xid)
 484	if p != nil {
 485		xid = p.XID
 486	}
 487	j, err := GetJunk(xid)
 488	if err != nil {
 489		http.Error(w, "error getting external object", http.StatusInternalServerError)
 490		log.Printf("error getting external object: %s", err)
 491		return
 492	}
 493	log.Printf("importing %s", xid)
 494	u := login.GetUserInfo(r)
 495	user, _ := butwhatabout(u.Username)
 496
 497	what, _ := j.GetString("type")
 498	if isactor(what) {
 499		outbox, _ := j.GetString("outbox")
 500		gimmexonks(user, outbox)
 501		http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
 502		return
 503	}
 504	xonk := xonkxonk(user, j, originate(xid))
 505	convoy := ""
 506	if xonk != nil {
 507		convoy = xonk.Convoy
 508		savexonk(user, xonk)
 509	}
 510	http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
 511}
 512
 513func xzone(w http.ResponseWriter, r *http.Request) {
 514	u := login.GetUserInfo(r)
 515	rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID)
 516	if err != nil {
 517		log.Printf("query err: %s", err)
 518		return
 519	}
 520	defer rows.Close()
 521	var honkers []Honker
 522	for rows.Next() {
 523		var xid string
 524		rows.Scan(&xid)
 525		honkers = append(honkers, Honker{XID: xid})
 526	}
 527	rows.Close()
 528	for i, _ := range honkers {
 529		_, honkers[i].Handle = handles(honkers[i].XID)
 530	}
 531	templinfo := getInfo(r)
 532	templinfo["XCSRF"] = login.GetCSRF("ximport", r)
 533	templinfo["Honkers"] = honkers
 534	err = readviews.Execute(w, "xzone.html", templinfo)
 535	if err != nil {
 536		log.Print(err)
 537	}
 538}
 539
 540func outbox(w http.ResponseWriter, r *http.Request) {
 541	name := mux.Vars(r)["name"]
 542	user, err := butwhatabout(name)
 543	if err != nil {
 544		http.NotFound(w, r)
 545		return
 546	}
 547	if stealthed(r) {
 548		http.NotFound(w, r)
 549		return
 550	}
 551
 552	honks := gethonksbyuser(name, false)
 553
 554	var jonks []junk.Junk
 555	for _, h := range honks {
 556		j, _ := jonkjonk(user, h)
 557		jonks = append(jonks, j)
 558	}
 559
 560	j := junk.New()
 561	j["@context"] = itiswhatitis
 562	j["id"] = user.URL + "/outbox"
 563	j["type"] = "OrderedCollection"
 564	j["totalItems"] = len(jonks)
 565	j["orderedItems"] = jonks
 566
 567	w.Header().Set("Content-Type", theonetruename)
 568	j.Write(w)
 569}
 570
 571func emptiness(w http.ResponseWriter, r *http.Request) {
 572	name := mux.Vars(r)["name"]
 573	user, err := butwhatabout(name)
 574	if err != nil {
 575		http.NotFound(w, r)
 576		return
 577	}
 578	colname := "/followers"
 579	if strings.HasSuffix(r.URL.Path, "/following") {
 580		colname = "/following"
 581	}
 582	j := junk.New()
 583	j["@context"] = itiswhatitis
 584	j["id"] = user.URL + colname
 585	j["type"] = "OrderedCollection"
 586	j["totalItems"] = 0
 587	j["orderedItems"] = []junk.Junk{}
 588
 589	w.Header().Set("Content-Type", theonetruename)
 590	j.Write(w)
 591}
 592
 593func showuser(w http.ResponseWriter, r *http.Request) {
 594	name := mux.Vars(r)["name"]
 595	user, err := butwhatabout(name)
 596	if err != nil {
 597		log.Printf("user not found %s: %s", name, err)
 598		http.NotFound(w, r)
 599		return
 600	}
 601	if friendorfoe(r.Header.Get("Accept")) {
 602		j := asjonker(user)
 603		w.Header().Set("Content-Type", theonetruename)
 604		j.Write(w)
 605		return
 606	}
 607	u := login.GetUserInfo(r)
 608	honks := gethonksbyuser(name, u != nil && u.Username == name)
 609	honkpage(w, r, u, user, honks, "")
 610}
 611
 612func showhonker(w http.ResponseWriter, r *http.Request) {
 613	u := login.GetUserInfo(r)
 614	name := mux.Vars(r)["name"]
 615	var honks []*Honk
 616	if name == "" {
 617		name = r.FormValue("xid")
 618		honks = gethonksbyxonker(u.UserID, name)
 619	} else {
 620		honks = gethonksbyhonker(u.UserID, name)
 621	}
 622	name = html.EscapeString(name)
 623	msg := fmt.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>`, name, name)
 624	honkpage(w, r, u, nil, honks, template.HTML(msg))
 625}
 626
 627func showcombo(w http.ResponseWriter, r *http.Request) {
 628	name := mux.Vars(r)["name"]
 629	u := login.GetUserInfo(r)
 630	honks := gethonksbycombo(u.UserID, name)
 631	honks = osmosis(honks, u.UserID)
 632	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by combo: "+name)))
 633}
 634func showconvoy(w http.ResponseWriter, r *http.Request) {
 635	c := r.FormValue("c")
 636	u := login.GetUserInfo(r)
 637	honks := gethonksbyconvoy(u.UserID, c)
 638	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks in convoy: "+c)))
 639}
 640func showontology(w http.ResponseWriter, r *http.Request) {
 641	name := mux.Vars(r)["name"]
 642	u := login.GetUserInfo(r)
 643	var userid int64 = -1
 644	if u != nil {
 645		userid = u.UserID
 646	}
 647	honks := gethonksbyontology(userid, "#"+name)
 648	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by ontology: "+name)))
 649}
 650
 651func thelistingoftheontologies(w http.ResponseWriter, r *http.Request) {
 652	rows, err := stmtSelectOnts.Query()
 653	if err != nil {
 654		log.Printf("selection error: %s", err)
 655		return
 656	}
 657	var onts [][]string
 658	for rows.Next() {
 659		var o string
 660		err := rows.Scan(&o)
 661		if err != nil {
 662			log.Printf("error scanning ont: %s", err)
 663			continue
 664		}
 665		onts = append(onts, []string{o, o[1:]})
 666	}
 667	templinfo := getInfo(r)
 668	templinfo["Onts"] = onts
 669	err = readviews.Execute(w, "onts.html", templinfo)
 670	if err != nil {
 671		log.Print(err)
 672	}
 673}
 674
 675func showhonk(w http.ResponseWriter, r *http.Request) {
 676	name := mux.Vars(r)["name"]
 677	user, err := butwhatabout(name)
 678	if err != nil {
 679		http.NotFound(w, r)
 680		return
 681	}
 682	if stealthed(r) {
 683		http.NotFound(w, r)
 684		return
 685	}
 686
 687	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 688	honk := getxonk(user.ID, xid)
 689	if honk == nil {
 690		http.NotFound(w, r)
 691		return
 692	}
 693	u := login.GetUserInfo(r)
 694	if u != nil && u.UserID != user.ID {
 695		u = nil
 696	}
 697	if !honk.Public {
 698		if u == nil {
 699			http.NotFound(w, r)
 700			return
 701
 702		}
 703		honkpage(w, r, u, nil, []*Honk{honk}, "one honk maybe more")
 704		return
 705	}
 706	rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy)
 707	if friendorfoe(r.Header.Get("Accept")) {
 708		for _, h := range rawhonks {
 709			if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) {
 710				honk.Replies = append(honk.Replies, h)
 711			}
 712		}
 713		donksforhonks([]*Honk{honk})
 714		_, j := jonkjonk(user, honk)
 715		j["@context"] = itiswhatitis
 716		w.Header().Set("Content-Type", theonetruename)
 717		j.Write(w)
 718		return
 719	}
 720	var honks []*Honk
 721	for _, h := range rawhonks {
 722		if h.Public && (h.Whofore == 2 || h.IsAcked()) {
 723			honks = append(honks, h)
 724		}
 725	}
 726
 727	honkpage(w, r, u, nil, honks, "one honk maybe more")
 728}
 729
 730func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 731	honks []*Honk, infomsg template.HTML) {
 732	templinfo := getInfo(r)
 733	var userid int64 = -1
 734	if u != nil {
 735		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 736		userid = u.UserID
 737	}
 738	if u == nil {
 739		w.Header().Set("Cache-Control", "max-age=60")
 740	}
 741	reverbolate(userid, honks)
 742	if user != nil {
 743		filt := htfilter.New()
 744		templinfo["Name"] = user.Name
 745		whatabout := user.About
 746		whatabout = obfusbreak(user.About)
 747		templinfo["WhatAbout"], _ = filt.String(whatabout)
 748	}
 749	templinfo["Honks"] = honks
 750	templinfo["ServerMessage"] = infomsg
 751	err := readviews.Execute(w, "honkpage.html", templinfo)
 752	if err != nil {
 753		log.Print(err)
 754	}
 755}
 756
 757func saveuser(w http.ResponseWriter, r *http.Request) {
 758	whatabout := r.FormValue("whatabout")
 759	u := login.GetUserInfo(r)
 760	db := opendatabase()
 761	options := ""
 762	if r.FormValue("skinny") == "skinny" {
 763		options += " skinny "
 764	}
 765	_, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
 766	if err != nil {
 767		log.Printf("error bouting what: %s", err)
 768	}
 769
 770	http.Redirect(w, r, "/account", http.StatusSeeOther)
 771}
 772
 773func gethonkers(userid int64) []*Honker {
 774	rows, err := stmtHonkers.Query(userid)
 775	if err != nil {
 776		log.Printf("error querying honkers: %s", err)
 777		return nil
 778	}
 779	defer rows.Close()
 780	var honkers []*Honker
 781	for rows.Next() {
 782		var f Honker
 783		var combos string
 784		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 785		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 786		if err != nil {
 787			log.Printf("error scanning honker: %s", err)
 788			return nil
 789		}
 790		honkers = append(honkers, &f)
 791	}
 792	return honkers
 793}
 794
 795func getdubs(userid int64) []*Honker {
 796	rows, err := stmtDubbers.Query(userid)
 797	if err != nil {
 798		log.Printf("error querying dubs: %s", err)
 799		return nil
 800	}
 801	defer rows.Close()
 802	var honkers []*Honker
 803	for rows.Next() {
 804		var f Honker
 805		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 806		if err != nil {
 807			log.Printf("error scanning honker: %s", err)
 808			return nil
 809		}
 810		honkers = append(honkers, &f)
 811	}
 812	return honkers
 813}
 814
 815func allusers() []login.UserInfo {
 816	var users []login.UserInfo
 817	rows, _ := opendatabase().Query("select userid, username from users")
 818	defer rows.Close()
 819	for rows.Next() {
 820		var u login.UserInfo
 821		rows.Scan(&u.UserID, &u.Username)
 822		users = append(users, u)
 823	}
 824	return users
 825}
 826
 827func getxonk(userid int64, xid string) *Honk {
 828	h := new(Honk)
 829	var dt, aud string
 830	row := stmtOneXonk.QueryRow(userid, xid)
 831	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 832		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags)
 833	if err != nil {
 834		if err != sql.ErrNoRows {
 835			log.Printf("error scanning xonk: %s", err)
 836		}
 837		return nil
 838	}
 839	h.Date, _ = time.Parse(dbtimeformat, dt)
 840	h.Audience = strings.Split(aud, " ")
 841	h.Public = !keepitquiet(h.Audience)
 842	return h
 843}
 844
 845func getbonk(userid int64, xid string) *Honk {
 846	h := new(Honk)
 847	var dt, aud string
 848	row := stmtOneXonk.QueryRow(userid, xid)
 849	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 850		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags)
 851	if err != nil {
 852		if err != sql.ErrNoRows {
 853			log.Printf("error scanning xonk: %s", err)
 854		}
 855		return nil
 856	}
 857	h.Date, _ = time.Parse(dbtimeformat, dt)
 858	h.Audience = strings.Split(aud, " ")
 859	h.Public = !keepitquiet(h.Audience)
 860	return h
 861}
 862
 863func getpublichonks() []*Honk {
 864	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 865	rows, err := stmtPublicHonks.Query(dt)
 866	return getsomehonks(rows, err)
 867}
 868func gethonksbyuser(name string, includeprivate bool) []*Honk {
 869	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 870	whofore := 2
 871	if includeprivate {
 872		whofore = 3
 873	}
 874	rows, err := stmtUserHonks.Query(whofore, name, dt)
 875	return getsomehonks(rows, err)
 876}
 877func gethonksforuser(userid int64) []*Honk {
 878	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 879	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 880	return getsomehonks(rows, err)
 881}
 882func gethonksforme(userid int64) []*Honk {
 883	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 884	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 885	return getsomehonks(rows, err)
 886}
 887func gethonksbyhonker(userid int64, honker string) []*Honk {
 888	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 889	return getsomehonks(rows, err)
 890}
 891func gethonksbyxonker(userid int64, xonker string) []*Honk {
 892	rows, err := stmtHonksByXonker.Query(userid, xonker, xonker, userid)
 893	return getsomehonks(rows, err)
 894}
 895func gethonksbycombo(userid int64, combo string) []*Honk {
 896	combo = "% " + combo + " %"
 897	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 898	return getsomehonks(rows, err)
 899}
 900func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 901	rows, err := stmtHonksByConvoy.Query(userid, userid, convoy)
 902	honks := getsomehonks(rows, err)
 903	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 904		honks[i], honks[j] = honks[j], honks[i]
 905	}
 906	return honks
 907}
 908func gethonksbyontology(userid int64, name string) []*Honk {
 909	rows, err := stmtHonksByOntology.Query(name, userid, userid)
 910	honks := getsomehonks(rows, err)
 911	return honks
 912}
 913
 914func getsomehonks(rows *sql.Rows, err error) []*Honk {
 915	if err != nil {
 916		log.Printf("error querying honks: %s", err)
 917		return nil
 918	}
 919	defer rows.Close()
 920	var honks []*Honk
 921	for rows.Next() {
 922		var h Honk
 923		var dt, aud string
 924		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 925			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore, &h.Flags)
 926		if err != nil {
 927			log.Printf("error scanning honks: %s", err)
 928			return nil
 929		}
 930		h.Date, _ = time.Parse(dbtimeformat, dt)
 931		h.Audience = strings.Split(aud, " ")
 932		h.Public = !keepitquiet(h.Audience)
 933		honks = append(honks, &h)
 934	}
 935	rows.Close()
 936	donksforhonks(honks)
 937	return honks
 938}
 939
 940func donksforhonks(honks []*Honk) {
 941	db := opendatabase()
 942	var ids []string
 943	hmap := make(map[int64]*Honk)
 944	for _, h := range honks {
 945		ids = append(ids, fmt.Sprintf("%d", h.ID))
 946		hmap[h.ID] = h
 947	}
 948	q := fmt.Sprintf("select honkid, donks.fileid, xid, name, url, media, local from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
 949	rows, err := db.Query(q)
 950	if err != nil {
 951		log.Printf("error querying donks: %s", err)
 952		return
 953	}
 954	defer rows.Close()
 955	for rows.Next() {
 956		var hid int64
 957		var d Donk
 958		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 959		if err != nil {
 960			log.Printf("error scanning donk: %s", err)
 961			continue
 962		}
 963		h := hmap[hid]
 964		h.Donks = append(h.Donks, &d)
 965	}
 966}
 967
 968func savebonk(w http.ResponseWriter, r *http.Request) {
 969	xid := r.FormValue("xid")
 970	userinfo := login.GetUserInfo(r)
 971	user, _ := butwhatabout(userinfo.Username)
 972
 973	log.Printf("bonking %s", xid)
 974
 975	xonk := getxonk(userinfo.UserID, xid)
 976	if xonk == nil {
 977		return
 978	}
 979	if !xonk.Public {
 980		return
 981	}
 982	donksforhonks([]*Honk{xonk})
 983
 984	_, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.ID)
 985	if err != nil {
 986		log.Printf("error acking bonk: %s", err)
 987	}
 988
 989	oonker := xonk.Oonker
 990	if oonker == "" {
 991		oonker = xonk.Honker
 992	}
 993	dt := time.Now().UTC()
 994	bonk := Honk{
 995		UserID:   userinfo.UserID,
 996		Username: userinfo.Username,
 997		What:     "bonk",
 998		Honker:   user.URL,
 999		XID:      xonk.XID,
1000		Date:     dt,
1001		Donks:    xonk.Donks,
1002		Convoy:   xonk.Convoy,
1003		Audience: []string{thewholeworld, oonker},
1004		Public:   true,
1005	}
1006
1007	aud := strings.Join(bonk.Audience, " ")
1008	whofore := 2
1009	onts := ontologies(xonk.Noise)
1010	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
1011		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
1012		xonk.Precis, oonker, 0, strings.Join(onts, " "))
1013	if err != nil {
1014		log.Printf("error saving bonk: %s", err)
1015		return
1016	}
1017	bonk.ID, _ = res.LastInsertId()
1018	for _, d := range bonk.Donks {
1019		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
1020		if err != nil {
1021			log.Printf("err saving donk: %s", err)
1022			return
1023		}
1024	}
1025	for _, o := range onts {
1026		_, err = stmtSaveOnts.Exec(strings.ToLower(o), bonk.ID)
1027		if err != nil {
1028			log.Printf("error saving ont: %s", err)
1029		}
1030	}
1031
1032	go honkworldwide(user, &bonk)
1033}
1034
1035func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string) {
1036	zonk := Honk{
1037		What:     what,
1038		XID:      xonk.XID,
1039		Date:     time.Now().UTC(),
1040		Audience: oneofakind(xonk.Audience),
1041	}
1042	zonk.Public = !keepitquiet(zonk.Audience)
1043
1044	log.Printf("announcing %sed honk: %s", what, xonk.XID)
1045	go honkworldwide(user, &zonk)
1046}
1047
1048func zonkit(w http.ResponseWriter, r *http.Request) {
1049	wherefore := r.FormValue("wherefore")
1050	what := r.FormValue("what")
1051	userinfo := login.GetUserInfo(r)
1052	user, _ := butwhatabout(userinfo.Username)
1053
1054	if wherefore == "ack" {
1055		xonk := getxonk(userinfo.UserID, what)
1056		if xonk != nil {
1057			_, err := stmtUpdateFlags.Exec(flagIsAcked, xonk.ID)
1058			if err != nil {
1059				log.Printf("error acking: %s", err)
1060			}
1061			sendzonkofsorts(xonk, user, "ack")
1062		}
1063		return
1064	}
1065
1066	if wherefore == "deack" {
1067		xonk := getxonk(userinfo.UserID, what)
1068		if xonk != nil {
1069			_, err := stmtClearFlags.Exec(flagIsAcked, xonk.ID)
1070			if err != nil {
1071				log.Printf("error deacking: %s", err)
1072			}
1073			sendzonkofsorts(xonk, user, "deack")
1074		}
1075		return
1076	}
1077
1078	if wherefore == "unbonk" {
1079		xonk := getbonk(userinfo.UserID, what)
1080		if xonk != nil {
1081			_, err := stmtClearFlags.Exec(flagIsBonked, xonk.ID)
1082			if err != nil {
1083				log.Printf("error unbonking: %s", err)
1084			}
1085			sendzonkofsorts(xonk, user, "unbonk")
1086		}
1087		return
1088	}
1089
1090	log.Printf("zonking %s %s", wherefore, what)
1091	if wherefore == "zonk" {
1092		xonk := getxonk(userinfo.UserID, what)
1093		if xonk != nil {
1094			_, err := stmtZonkDonks.Exec(xonk.ID)
1095			if err != nil {
1096				log.Printf("error zonking: %s", err)
1097			}
1098			_, err = stmtZonkIt.Exec(userinfo.UserID, what)
1099			if err != nil {
1100				log.Printf("error zonking: %s", err)
1101			}
1102			if xonk.Whofore == 2 || xonk.Whofore == 3 {
1103				sendzonkofsorts(xonk, user, "zonk")
1104			}
1105		}
1106	}
1107	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
1108	if err != nil {
1109		log.Printf("error saving zonker: %s", err)
1110		return
1111	}
1112}
1113
1114func savehonk(w http.ResponseWriter, r *http.Request) {
1115	rid := r.FormValue("rid")
1116	noise := r.FormValue("noise")
1117
1118	userinfo := login.GetUserInfo(r)
1119	user, _ := butwhatabout(userinfo.Username)
1120
1121	dt := time.Now().UTC()
1122	xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
1123	what := "honk"
1124	if rid != "" {
1125		what = "tonk"
1126	}
1127	honk := Honk{
1128		UserID:   userinfo.UserID,
1129		Username: userinfo.Username,
1130		What:     "honk",
1131		Honker:   user.URL,
1132		XID:      xid,
1133		Date:     dt,
1134	}
1135	if strings.HasPrefix(noise, "DZ:") {
1136		idx := strings.Index(noise, "\n")
1137		if idx == -1 {
1138			honk.Precis = noise
1139			noise = ""
1140		} else {
1141			honk.Precis = noise[:idx]
1142			noise = noise[idx+1:]
1143		}
1144	}
1145	noise = hooterize(noise)
1146	noise = strings.TrimSpace(noise)
1147	honk.Precis = strings.TrimSpace(honk.Precis)
1148
1149	var convoy string
1150	if rid != "" {
1151		xonk := getxonk(userinfo.UserID, rid)
1152		if xonk != nil {
1153			if xonk.Public {
1154				honk.Audience = append(honk.Audience, xonk.Audience...)
1155			}
1156			convoy = xonk.Convoy
1157		} else {
1158			xonkaud, c := whosthere(rid)
1159			honk.Audience = append(honk.Audience, xonkaud...)
1160			convoy = c
1161		}
1162		for i, a := range honk.Audience {
1163			if a == thewholeworld {
1164				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1165				break
1166			}
1167		}
1168		honk.RID = rid
1169	} else {
1170		honk.Audience = []string{thewholeworld}
1171	}
1172	if noise != "" && noise[0] == '@' {
1173		honk.Audience = append(grapevine(noise), honk.Audience...)
1174	} else {
1175		honk.Audience = append(honk.Audience, grapevine(noise)...)
1176	}
1177	if convoy == "" {
1178		convoy = "data:,electrichonkytonk-" + xfiltrate()
1179	}
1180	butnottooloud(honk.Audience)
1181	honk.Audience = oneofakind(honk.Audience)
1182	if len(honk.Audience) == 0 {
1183		log.Printf("honk to nowhere")
1184		http.Error(w, "honk to nowhere...", http.StatusNotFound)
1185		return
1186	}
1187	honk.Public = !keepitquiet(honk.Audience)
1188	noise = obfusbreak(noise)
1189	honk.Noise = noise
1190	honk.Convoy = convoy
1191
1192	donkxid := r.FormValue("donkxid")
1193	if donkxid == "" {
1194		file, filehdr, err := r.FormFile("donk")
1195		if err == nil {
1196			var buf bytes.Buffer
1197			io.Copy(&buf, file)
1198			file.Close()
1199			data := buf.Bytes()
1200			xid := xfiltrate()
1201			var media, name string
1202			img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1203			if err == nil {
1204				data = img.Data
1205				format := img.Format
1206				media = "image/" + format
1207				if format == "jpeg" {
1208					format = "jpg"
1209				}
1210				name = xid + "." + format
1211				xid = name
1212			} else {
1213				maxsize := 100000
1214				if len(data) > maxsize {
1215					log.Printf("bad image: %s too much text: %d", err, len(data))
1216					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1217					return
1218				}
1219				for i := 0; i < len(data); i++ {
1220					if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1221						log.Printf("bad image: %s not text: %d", err, data[i])
1222						http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1223						return
1224					}
1225				}
1226				media = "text/plain"
1227				name = filehdr.Filename
1228				if name == "" {
1229					name = xid + ".txt"
1230				}
1231				xid += ".txt"
1232			}
1233			url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1234			res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1235			if err != nil {
1236				log.Printf("unable to save image: %s", err)
1237				return
1238			}
1239			var d Donk
1240			d.FileID, _ = res.LastInsertId()
1241			d.XID = name
1242			d.Name = name
1243			d.Media = media
1244			d.URL = url
1245			d.Local = true
1246			honk.Donks = append(honk.Donks, &d)
1247			donkxid = d.XID
1248		}
1249	} else {
1250		xid := donkxid
1251		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1252		var donk Donk
1253		row := stmtFindFile.QueryRow(url)
1254		err := row.Scan(&donk.FileID)
1255		if err == nil {
1256			donk.XID = xid
1257			donk.Local = true
1258			donk.URL = url
1259			honk.Donks = append(honk.Donks, &donk)
1260		} else {
1261			log.Printf("can't find file: %s", xid)
1262		}
1263	}
1264	herd := herdofemus(honk.Noise)
1265	for _, e := range herd {
1266		donk := savedonk(e.ID, e.Name, "image/png", true)
1267		if donk != nil {
1268			donk.Name = e.Name
1269			honk.Donks = append(honk.Donks, donk)
1270		}
1271	}
1272	memetize(&honk)
1273
1274	aud := strings.Join(honk.Audience, " ")
1275	whofore := 2
1276	if !honk.Public {
1277		whofore = 3
1278	}
1279	if r.FormValue("preview") == "preview" {
1280		honks := []*Honk{&honk}
1281		reverbolate(userinfo.UserID, honks)
1282		templinfo := getInfo(r)
1283		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1284		templinfo["Honks"] = honks
1285		templinfo["InReplyTo"] = r.FormValue("rid")
1286		templinfo["Noise"] = r.FormValue("noise")
1287		templinfo["SavedFile"] = donkxid
1288		templinfo["ServerMessage"] = "honk preview"
1289		err := readviews.Execute(w, "honkpage.html", templinfo)
1290		if err != nil {
1291			log.Print(err)
1292		}
1293		return
1294	}
1295	onts := ontologies(honk.Noise)
1296	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1297		dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html",
1298		honk.Precis, honk.Oonker, 0, strings.Join(onts, " "))
1299	if err != nil {
1300		log.Printf("error saving honk: %s", err)
1301		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1302		return
1303	}
1304	honk.ID, _ = res.LastInsertId()
1305	for _, d := range honk.Donks {
1306		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1307		if err != nil {
1308			log.Printf("err saving donk: %s", err)
1309			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1310			return
1311		}
1312	}
1313	for _, o := range onts {
1314		_, err = stmtSaveOnts.Exec(strings.ToLower(o), honk.ID)
1315		if err != nil {
1316			log.Printf("error saving ont: %s", err)
1317		}
1318	}
1319
1320	go honkworldwide(user, &honk)
1321
1322	http.Redirect(w, r, xid, http.StatusSeeOther)
1323}
1324
1325func showhonkers(w http.ResponseWriter, r *http.Request) {
1326	userinfo := login.GetUserInfo(r)
1327	templinfo := getInfo(r)
1328	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1329	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1330	err := readviews.Execute(w, "honkers.html", templinfo)
1331	if err != nil {
1332		log.Print(err)
1333	}
1334}
1335
1336func showcombos(w http.ResponseWriter, r *http.Request) {
1337	userinfo := login.GetUserInfo(r)
1338	templinfo := getInfo(r)
1339	honkers := gethonkers(userinfo.UserID)
1340	var combos []string
1341	for _, h := range honkers {
1342		combos = append(combos, h.Combos...)
1343	}
1344	for i, c := range combos {
1345		if c == "-" {
1346			combos[i] = ""
1347		}
1348	}
1349	combos = oneofakind(combos)
1350	sort.Strings(combos)
1351	templinfo["Combos"] = combos
1352	err := readviews.Execute(w, "combos.html", templinfo)
1353	if err != nil {
1354		log.Print(err)
1355	}
1356}
1357
1358func savehonker(w http.ResponseWriter, r *http.Request) {
1359	u := login.GetUserInfo(r)
1360	name := r.FormValue("name")
1361	url := r.FormValue("url")
1362	peep := r.FormValue("peep")
1363	combos := r.FormValue("combos")
1364	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1365
1366	if honkerid > 0 {
1367		goodbye := r.FormValue("goodbye")
1368		if goodbye == "F" {
1369			db := opendatabase()
1370			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1371				honkerid, u.UserID)
1372			var xid string
1373			err := row.Scan(&xid)
1374			if err != nil {
1375				log.Printf("can't get honker xid: %s", err)
1376				return
1377			}
1378			log.Printf("unsubscribing from %s", xid)
1379			user, _ := butwhatabout(u.Username)
1380			go itakeitallback(user, xid)
1381			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1382			if err != nil {
1383				log.Printf("error updating honker: %s", err)
1384				return
1385			}
1386
1387			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1388			return
1389		}
1390		combos = " " + strings.TrimSpace(combos) + " "
1391		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1392		if err != nil {
1393			log.Printf("update honker err: %s", err)
1394			return
1395		}
1396		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1397	}
1398
1399	flavor := "presub"
1400	if peep == "peep" {
1401		flavor = "peep"
1402	}
1403	p := investigate(url)
1404	if p == nil {
1405		log.Printf("failed to investigate honker")
1406		return
1407	}
1408	url = p.XID
1409	if name == "" {
1410		name = p.Handle
1411	}
1412	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1413	if err != nil {
1414		log.Print(err)
1415		return
1416	}
1417	if flavor == "presub" {
1418		user, _ := butwhatabout(u.Username)
1419		go subsub(user, url)
1420	}
1421	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1422}
1423
1424type Zonker struct {
1425	ID        int64
1426	Name      string
1427	Wherefore string
1428}
1429
1430func zonkzone(w http.ResponseWriter, r *http.Request) {
1431	userinfo := login.GetUserInfo(r)
1432	rows, err := stmtGetZonkers.Query(userinfo.UserID)
1433	if err != nil {
1434		log.Printf("err: %s", err)
1435		return
1436	}
1437	defer rows.Close()
1438	var zonkers []Zonker
1439	for rows.Next() {
1440		var z Zonker
1441		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1442		zonkers = append(zonkers, z)
1443	}
1444	sort.Slice(zonkers, func(i, j int) bool {
1445		w1 := zonkers[i].Wherefore
1446		w2 := zonkers[j].Wherefore
1447		if w1 == w2 {
1448			return zonkers[i].Name < zonkers[j].Name
1449		}
1450		if w1 == "zonvoy" {
1451			w1 = "zzzzzzz"
1452		}
1453		if w2 == "zonvoy" {
1454			w2 = "zzzzzzz"
1455		}
1456		return w1 < w2
1457	})
1458
1459	templinfo := getInfo(r)
1460	templinfo["Zonkers"] = zonkers
1461	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1462	err = readviews.Execute(w, "zonkers.html", templinfo)
1463	if err != nil {
1464		log.Print(err)
1465	}
1466}
1467
1468func zonkzonk(w http.ResponseWriter, r *http.Request) {
1469	userinfo := login.GetUserInfo(r)
1470	itsok := r.FormValue("itsok")
1471	if itsok == "iforgiveyou" {
1472		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1473		db := opendatabase()
1474		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1475			userinfo.UserID, zonkerid)
1476		bitethethumbs()
1477		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1478		return
1479	}
1480	wherefore := r.FormValue("wherefore")
1481	name := r.FormValue("name")
1482	if name == "" {
1483		return
1484	}
1485	switch wherefore {
1486	case "zonker":
1487	case "zomain":
1488	case "zonvoy":
1489	case "zord":
1490	case "zilence":
1491	default:
1492		return
1493	}
1494	db := opendatabase()
1495	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1496		userinfo.UserID, name, wherefore)
1497	if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1498		bitethethumbs()
1499	}
1500
1501	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1502}
1503
1504func accountpage(w http.ResponseWriter, r *http.Request) {
1505	u := login.GetUserInfo(r)
1506	user, _ := butwhatabout(u.Username)
1507	templinfo := getInfo(r)
1508	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1509	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1510	templinfo["User"] = user
1511	err := readviews.Execute(w, "account.html", templinfo)
1512	if err != nil {
1513		log.Print(err)
1514	}
1515}
1516
1517func dochpass(w http.ResponseWriter, r *http.Request) {
1518	err := login.ChangePassword(w, r)
1519	if err != nil {
1520		log.Printf("error changing password: %s", err)
1521	}
1522	http.Redirect(w, r, "/account", http.StatusSeeOther)
1523}
1524
1525func fingerlicker(w http.ResponseWriter, r *http.Request) {
1526	orig := r.FormValue("resource")
1527
1528	log.Printf("finger lick: %s", orig)
1529
1530	if strings.HasPrefix(orig, "acct:") {
1531		orig = orig[5:]
1532	}
1533
1534	name := orig
1535	idx := strings.LastIndexByte(name, '/')
1536	if idx != -1 {
1537		name = name[idx+1:]
1538		if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
1539			log.Printf("foreign request rejected")
1540			name = ""
1541		}
1542	} else {
1543		idx = strings.IndexByte(name, '@')
1544		if idx != -1 {
1545			name = name[:idx]
1546			if name+"@"+serverName != orig {
1547				log.Printf("foreign request rejected")
1548				name = ""
1549			}
1550		}
1551	}
1552	user, err := butwhatabout(name)
1553	if err != nil {
1554		http.NotFound(w, r)
1555		return
1556	}
1557
1558	j := junk.New()
1559	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1560	j["aliases"] = []string{user.URL}
1561	var links []junk.Junk
1562	l := junk.New()
1563	l["rel"] = "self"
1564	l["type"] = `application/activity+json`
1565	l["href"] = user.URL
1566	links = append(links, l)
1567	j["links"] = links
1568
1569	w.Header().Set("Cache-Control", "max-age=3600")
1570	w.Header().Set("Content-Type", "application/jrd+json")
1571	j.Write(w)
1572}
1573
1574func somedays() string {
1575	secs := 432000 + notrand.Int63n(432000)
1576	return fmt.Sprintf("%d", secs)
1577}
1578
1579func avatate(w http.ResponseWriter, r *http.Request) {
1580	n := r.FormValue("a")
1581	a := avatar(n)
1582	w.Header().Set("Cache-Control", "max-age="+somedays())
1583	w.Write(a)
1584}
1585
1586func servecss(w http.ResponseWriter, r *http.Request) {
1587	w.Header().Set("Cache-Control", "max-age=7776000")
1588	http.ServeFile(w, r, "views"+r.URL.Path)
1589}
1590func servehtml(w http.ResponseWriter, r *http.Request) {
1591	templinfo := getInfo(r)
1592	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1593	if err != nil {
1594		log.Print(err)
1595	}
1596}
1597func serveemu(w http.ResponseWriter, r *http.Request) {
1598	xid := mux.Vars(r)["xid"]
1599	w.Header().Set("Cache-Control", "max-age="+somedays())
1600	http.ServeFile(w, r, "emus/"+xid)
1601}
1602func servememe(w http.ResponseWriter, r *http.Request) {
1603	xid := mux.Vars(r)["xid"]
1604	w.Header().Set("Cache-Control", "max-age="+somedays())
1605	http.ServeFile(w, r, "memes/"+xid)
1606}
1607
1608func servefile(w http.ResponseWriter, r *http.Request) {
1609	xid := mux.Vars(r)["xid"]
1610	row := stmtFileData.QueryRow(xid)
1611	var media string
1612	var data []byte
1613	err := row.Scan(&media, &data)
1614	if err != nil {
1615		log.Printf("error loading file: %s", err)
1616		http.NotFound(w, r)
1617		return
1618	}
1619	w.Header().Set("Content-Type", media)
1620	w.Header().Set("X-Content-Type-Options", "nosniff")
1621	w.Header().Set("Cache-Control", "max-age="+somedays())
1622	w.Write(data)
1623}
1624
1625func nomoroboto(w http.ResponseWriter, r *http.Request) {
1626	io.WriteString(w, "User-agent: *\n")
1627	io.WriteString(w, "Disallow: /a\n")
1628	io.WriteString(w, "Disallow: /d\n")
1629	io.WriteString(w, "Disallow: /meme\n")
1630	for _, u := range allusers() {
1631		fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
1632	}
1633}
1634
1635func serve() {
1636	db := opendatabase()
1637	login.Init(db)
1638
1639	listener, err := openListener()
1640	if err != nil {
1641		log.Fatal(err)
1642	}
1643	go redeliverator()
1644
1645	debug := false
1646	getconfig("debug", &debug)
1647	readviews = templates.Load(debug,
1648		"views/honkpage.html",
1649		"views/honkfrags.html",
1650		"views/honkers.html",
1651		"views/zonkers.html",
1652		"views/combos.html",
1653		"views/honkform.html",
1654		"views/honk.html",
1655		"views/account.html",
1656		"views/about.html",
1657		"views/funzone.html",
1658		"views/login.html",
1659		"views/xzone.html",
1660		"views/header.html",
1661		"views/onts.html",
1662	)
1663	if !debug {
1664		s := "views/style.css"
1665		savedstyleparams[s] = getstyleparam(s)
1666		s = "views/local.css"
1667		savedstyleparams[s] = getstyleparam(s)
1668	}
1669
1670	bitethethumbs()
1671
1672	mux := mux.NewRouter()
1673	mux.Use(login.Checker)
1674
1675	posters := mux.Methods("POST").Subrouter()
1676	getters := mux.Methods("GET").Subrouter()
1677
1678	getters.HandleFunc("/", homepage)
1679	getters.HandleFunc("/front", homepage)
1680	getters.HandleFunc("/robots.txt", nomoroboto)
1681	getters.HandleFunc("/rss", showrss)
1682	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}", showuser)
1683	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/"+honkSep+"/{xid:[[:alnum:]]+}", showhonk)
1684	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/rss", showrss)
1685	posters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/inbox", inbox)
1686	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/outbox", outbox)
1687	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/followers", emptiness)
1688	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/following", emptiness)
1689	getters.HandleFunc("/a", avatate)
1690	getters.HandleFunc("/o", thelistingoftheontologies)
1691	getters.HandleFunc("/o/{name:[a-z0-9-]+}", showontology)
1692	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1693	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1694	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1695	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1696
1697	getters.HandleFunc("/style.css", servecss)
1698	getters.HandleFunc("/local.css", servecss)
1699	getters.HandleFunc("/about", servehtml)
1700	getters.HandleFunc("/login", servehtml)
1701	posters.HandleFunc("/dologin", login.LoginFunc)
1702	getters.HandleFunc("/logout", login.LogoutFunc)
1703
1704	loggedin := mux.NewRoute().Subrouter()
1705	loggedin.Use(login.Required)
1706	loggedin.HandleFunc("/account", accountpage)
1707	loggedin.HandleFunc("/funzone", showfunzone)
1708	loggedin.HandleFunc("/chpass", dochpass)
1709	loggedin.HandleFunc("/atme", homepage)
1710	loggedin.HandleFunc("/zonkzone", zonkzone)
1711	loggedin.HandleFunc("/xzone", xzone)
1712	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1713	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1714	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1715	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1716	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1717	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1718	loggedin.HandleFunc("/honkers", showhonkers)
1719	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1720	loggedin.HandleFunc("/h", showhonker)
1721	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1722	loggedin.HandleFunc("/c", showcombos)
1723	loggedin.HandleFunc("/t", showconvoy)
1724	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1725
1726	err = http.Serve(listener, mux)
1727	if err != nil {
1728		log.Fatal(err)
1729	}
1730}
1731
1732func cleanupdb(arg string) {
1733	db := opendatabase()
1734	days, err := strconv.Atoi(arg)
1735	if err != nil {
1736		honker := arg
1737		expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1738		doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1739		doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1740	} else {
1741		expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1742		doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3))", expdate)
1743		doordie(db, "delete from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3)", expdate)
1744	}
1745	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1746	for _, u := range allusers() {
1747		doordie(db, "delete from zonkers where userid = ? and wherefore = 'zonvoy' and zonkerid < (select zonkerid from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 1 offset 200)", u.UserID, u.UserID)
1748	}
1749}
1750
1751var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1752var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1753var stmtHonksByOntology, stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1754var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1755var stmtOneBonk, stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1756var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1757var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1758var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1759var stmtSelectOnts, stmtSaveOnts, stmtUpdateFlags, stmtClearFlags *sql.Stmt
1760
1761func preparetodie(db *sql.DB, s string) *sql.Stmt {
1762	stmt, err := db.Prepare(s)
1763	if err != nil {
1764		log.Fatalf("error %s: %s", err, s)
1765	}
1766	return stmt
1767}
1768
1769func prepareStatements(db *sql.DB) {
1770	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and (flavor = 'sub' or flavor = 'peep' or flavor = 'unsub') order by name")
1771	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1772	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1773	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1774	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1775	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1776
1777	selecthonks := "select honks.honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, convoy, whofore, flags from honks join users on honks.userid = users.userid "
1778	limit := " order by honks.honkid desc limit 250"
1779	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1780	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1781	stmtOneBonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ? and what = 'bonk' and whofore = 2")
1782	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1783	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1784	stmtHonksForUser = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and honker in (select xid from honkers where userid = ? and flavor = 'sub' and combos not like '% - %')"+butnotthose+limit)
1785	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1786	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on (honkers.xid = honks.honker or honkers.xid = honks.oonker) where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1787	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1788	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1789	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1790	stmtHonksByOntology = preparetodie(db, selecthonks+"join onts on honks.honkid = onts.honkid where onts.ontology = ? and (honks.userid = ? or (? = -1 and honks.whofore = 2))"+limit)
1791
1792	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags, onts) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1793	stmtSaveOnts = preparetodie(db, "insert into onts (ontology, honkid) values (?, ?)")
1794	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1795	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1796	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1797	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1798	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1799	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1800	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1801	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1802	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1803	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1804	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1805	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1806	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1807	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1808	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1809	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1810	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1811	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1812	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1813	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1814	stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? and honker not in (select xid from honkers where userid = ? and flavor = 'sub') order by honkid desc limit 100")
1815	stmtUpdateFlags = preparetodie(db, "update honks set flags = flags | ? where honkid = ?")
1816	stmtClearFlags = preparetodie(db, "update honks set flags = flags & ~ ? where honkid = ?")
1817	stmtSelectOnts = preparetodie(db, "select distinct(ontology) from onts join honks on onts.honkid = honks.honkid where honks.whofore = 2")
1818}
1819
1820func ElaborateUnitTests() {
1821}
1822
1823func main() {
1824	cmd := "run"
1825	if len(os.Args) > 1 {
1826		cmd = os.Args[1]
1827	}
1828	switch cmd {
1829	case "init":
1830		initdb()
1831	case "upgrade":
1832		upgradedb()
1833	}
1834	db := opendatabase()
1835	dbversion := 0
1836	getconfig("dbversion", &dbversion)
1837	if dbversion != myVersion {
1838		log.Fatal("incorrect database version. run upgrade.")
1839	}
1840	getconfig("servermsg", &serverMsg)
1841	getconfig("servername", &serverName)
1842	getconfig("usersep", &userSep)
1843	getconfig("honksep", &honkSep)
1844	getconfig("dnf", &donotfedafterdark)
1845	prepareStatements(db)
1846	switch cmd {
1847	case "adduser":
1848		adduser()
1849	case "cleanup":
1850		arg := "30"
1851		if len(os.Args) > 2 {
1852			arg = os.Args[2]
1853		}
1854		cleanupdb(arg)
1855	case "ping":
1856		if len(os.Args) < 4 {
1857			fmt.Printf("usage: honk ping from to\n")
1858			return
1859		}
1860		name := os.Args[2]
1861		targ := os.Args[3]
1862		user, err := butwhatabout(name)
1863		if err != nil {
1864			log.Printf("unknown user")
1865			return
1866		}
1867		ping(user, targ)
1868	case "peep":
1869		peeppeep()
1870	case "run":
1871		serve()
1872	case "test":
1873		ElaborateUnitTests()
1874	default:
1875		log.Fatal("unknown command")
1876	}
1877}