all repos — honk @ e9f403ede51a92ec14b409ddc4e7063e0a299c7a

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