all repos — honk @ 434f07afe46ccf141f52674b895f11036c2093a7

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