all repos — honk @ 5dbcd597504c85e9e01d26ef4f21e32d6b67f5e6

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