all repos — honk @ b4634c4b64a43da9896cdafc1ad076e65f638ba6

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