all repos — honk @ 12e1deac71cc9338bc2adb90740bd86685f09391

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