all repos — honk @ 687789837e21cf8354f325551bbfc5f3b26a8c5a

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