all repos — honk @ b93adc318d2266c3e43dbf776142b20d1eae5b18

my fork of honk

honk.go (view raw)

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