all repos — honk @ f2f53fc8bd0ce8a3f0324057cc76105a4d479964

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