all repos — honk @ 79d9b15b8387a9f537c5fbbd928c748aa6f8f145

my fork of honk

honk.go (view raw)

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