all repos — honk @ 2554baee873ab0d70f7b141870cf328b27562152

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 := xonk.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	honk := Honk{
 727		UserID:   userinfo.UserID,
 728		Username: userinfo.Username,
 729		What:     "honk",
 730		XID:      xid,
 731		RID:      rid,
 732		Date:     dt,
 733	}
 734	if noise[0] == '@' {
 735		honk.Audience = append(grapevine(noise), thewholeworld)
 736	} else {
 737		honk.Audience = prepend(thewholeworld, grapevine(noise))
 738	}
 739	var convoy string
 740	if rid != "" {
 741		xonk := getxonk("", rid)
 742		if xonk != nil {
 743			honk.Audience = append(honk.Audience, xonk.Audience...)
 744			convoy = xonk.Convoy
 745		} else {
 746			xonkaud, c := whosthere(rid)
 747			honk.Audience = append(honk.Audience, xonkaud...)
 748			convoy = c
 749		}
 750	}
 751	if convoy == "" {
 752		convoy = "data:,electrichonkytonk-" + xfiltrate()
 753	}
 754	honk.Audience = oneofakind(honk.Audience)
 755	noise = obfusbreak(noise)
 756	honk.Noise = noise
 757	honk.Convoy = convoy
 758
 759	file, filehdr, err := r.FormFile("donk")
 760	if err == nil {
 761		var buf bytes.Buffer
 762		io.Copy(&buf, file)
 763		file.Close()
 764		data := buf.Bytes()
 765		xid := xfiltrate()
 766		var media, name string
 767		img, format, err := image.Decode(&buf)
 768		if err == nil {
 769			data, format, err = vacuumwrap(img, format)
 770			if err != nil {
 771				log.Printf("can't vacuum image: %s", err)
 772				return
 773			}
 774			media = "image/" + format
 775			if format == "jpeg" {
 776				format = "jpg"
 777			}
 778			name = xid + "." + format
 779			xid = name
 780		} else {
 781			maxsize := 100000
 782			if len(data) > maxsize {
 783				log.Printf("bad image: %s too much text: %d", err, len(data))
 784				http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 785				return
 786			}
 787			for i := 0; i < len(data); i++ {
 788				if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
 789					log.Printf("bad image: %s not text: %d", err, data[i])
 790					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
 791					return
 792				}
 793			}
 794			media = "text/plain"
 795			name = filehdr.Filename
 796			if name == "" {
 797				name = xid + ".txt"
 798			}
 799			xid += ".txt"
 800		}
 801		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
 802		res, err := stmtSaveFile.Exec(xid, name, url, media, data)
 803		if err != nil {
 804			log.Printf("unable to save image: %s", err)
 805			return
 806		}
 807		var d Donk
 808		d.FileID, _ = res.LastInsertId()
 809		d.XID = name
 810		d.Name = name
 811		d.Media = media
 812		d.URL = url
 813		honk.Donks = append(honk.Donks, &d)
 814	}
 815	herd := herdofemus(honk.Noise)
 816	for _, e := range herd {
 817		donk := savedonk(e.ID, e.Name, "image/png")
 818		if donk != nil {
 819			donk.Name = e.Name
 820			honk.Donks = append(honk.Donks, donk)
 821		}
 822	}
 823
 824	aud := strings.Join(honk.Audience, " ")
 825	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
 826		dt.Format(dbtimeformat), "", aud, noise, convoy)
 827	if err != nil {
 828		log.Printf("error saving honk: %s", err)
 829		return
 830	}
 831	honk.ID, _ = res.LastInsertId()
 832	for _, d := range honk.Donks {
 833		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 834		if err != nil {
 835			log.Printf("err saving donk: %s", err)
 836			return
 837		}
 838	}
 839
 840	user, _ := butwhatabout(userinfo.Username)
 841
 842	go honkworldwide(user, &honk)
 843
 844	http.Redirect(w, r, "/", http.StatusSeeOther)
 845}
 846
 847func showhonkers(w http.ResponseWriter, r *http.Request) {
 848	userinfo := GetUserInfo(r)
 849	templinfo := getInfo(r)
 850	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 851	templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
 852	err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
 853	if err != nil {
 854		log.Print(err)
 855	}
 856}
 857
 858var handfull = make(map[string]string)
 859var handlock sync.Mutex
 860
 861func gofish(name string) string {
 862	if name[0] == '@' {
 863		name = name[1:]
 864	}
 865	m := strings.Split(name, "@")
 866	if len(m) != 2 {
 867		log.Printf("bad fish name: %s", name)
 868		return ""
 869	}
 870	handlock.Lock()
 871	ref, ok := handfull[name]
 872	handlock.Unlock()
 873	if ok {
 874		return ref
 875	}
 876	j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
 877	handlock.Lock()
 878	defer handlock.Unlock()
 879	if err != nil {
 880		log.Printf("failed to go fish %s: %s", name, err)
 881		handfull[name] = ""
 882		return ""
 883	}
 884	links, _ := jsongetarray(j, "links")
 885	for _, l := range links {
 886		href, _ := jsongetstring(l, "href")
 887		rel, _ := jsongetstring(l, "rel")
 888		t, _ := jsongetstring(l, "type")
 889		if rel == "self" && friendorfoe(t) {
 890			handfull[name] = href
 891			return href
 892		}
 893	}
 894	handfull[name] = ""
 895	return ""
 896}
 897
 898func savehonker(w http.ResponseWriter, r *http.Request) {
 899	name := r.FormValue("name")
 900	url := r.FormValue("url")
 901	peep := r.FormValue("peep")
 902	flavor := "presub"
 903	if peep == "peep" {
 904		flavor = "peep"
 905	}
 906
 907	if url == "" {
 908		return
 909	}
 910	if url[0] == '@' {
 911		url = gofish(url)
 912	}
 913	if url == "" {
 914		return
 915	}
 916
 917	u := GetUserInfo(r)
 918	db := opendatabase()
 919	_, err := db.Exec("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)",
 920		u.UserID, name, url, flavor)
 921	if err != nil {
 922		log.Print(err)
 923	}
 924	if flavor == "presub" {
 925		user, _ := butwhatabout(u.Username)
 926		go subsub(user, url)
 927	}
 928	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
 929}
 930
 931func somedays() string {
 932	secs := 432000 + notrand.Int63n(432000)
 933	return fmt.Sprintf("%d", secs)
 934}
 935
 936func avatate(w http.ResponseWriter, r *http.Request) {
 937	n := r.FormValue("a")
 938	a := avatar(n)
 939	w.Header().Set("Cache-Control", "max-age="+somedays())
 940	w.Write(a)
 941}
 942
 943func servecss(w http.ResponseWriter, r *http.Request) {
 944	w.Header().Set("Cache-Control", "max-age=7776000")
 945	http.ServeFile(w, r, "views"+r.URL.Path)
 946}
 947func servehtml(w http.ResponseWriter, r *http.Request) {
 948	templinfo := getInfo(r)
 949	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
 950	if err != nil {
 951		log.Print(err)
 952	}
 953}
 954func serveemu(w http.ResponseWriter, r *http.Request) {
 955	xid := mux.Vars(r)["xid"]
 956	w.Header().Set("Cache-Control", "max-age="+somedays())
 957	http.ServeFile(w, r, "emus/"+xid)
 958}
 959
 960func servefile(w http.ResponseWriter, r *http.Request) {
 961	xid := mux.Vars(r)["xid"]
 962	row := stmtFileData.QueryRow(xid)
 963	var media string
 964	var data []byte
 965	err := row.Scan(&media, &data)
 966	if err != nil {
 967		log.Printf("error loading file: %s", err)
 968		http.NotFound(w, r)
 969		return
 970	}
 971	w.Header().Set("Content-Type", media)
 972	w.Header().Set("X-Content-Type-Options", "nosniff")
 973	w.Header().Set("Cache-Control", "max-age="+somedays())
 974	w.Write(data)
 975}
 976
 977func serve() {
 978	db := opendatabase()
 979	LoginInit(db)
 980
 981	listener, err := openListener()
 982	if err != nil {
 983		log.Fatal(err)
 984	}
 985	go redeliverator()
 986
 987	debug := false
 988	getconfig("debug", &debug)
 989	readviews = ParseTemplates(debug,
 990		"views/homepage.html",
 991		"views/honkpage.html",
 992		"views/honkers.html",
 993		"views/honkform.html",
 994		"views/honk.html",
 995		"views/login.html",
 996		"views/header.html",
 997	)
 998	if !debug {
 999		s := "views/style.css"
1000		savedstyleparams[s] = getstyleparam(s)
1001		s = "views/local.css"
1002		savedstyleparams[s] = getstyleparam(s)
1003	}
1004
1005	mux := mux.NewRouter()
1006	mux.Use(LoginChecker)
1007
1008	posters := mux.Methods("POST").Subrouter()
1009	getters := mux.Methods("GET").Subrouter()
1010
1011	getters.HandleFunc("/", homepage)
1012	getters.HandleFunc("/rss", showrss)
1013	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1014	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1015	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1016	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1017	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1018	getters.HandleFunc("/a", avatate)
1019	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1020	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1021	getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1022	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1023
1024	getters.HandleFunc("/style.css", servecss)
1025	getters.HandleFunc("/local.css", servecss)
1026	getters.HandleFunc("/login", servehtml)
1027	posters.HandleFunc("/dologin", dologin)
1028	getters.HandleFunc("/logout", dologout)
1029
1030	loggedin := mux.NewRoute().Subrouter()
1031	loggedin.Use(LoginRequired)
1032	loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1033	loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1034	loggedin.Handle("/zonkit", CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1035	loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1036	loggedin.HandleFunc("/honkers", showhonkers)
1037	loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1038
1039	err = http.Serve(listener, mux)
1040	if err != nil {
1041		log.Fatal(err)
1042	}
1043}
1044
1045var stmtHonkers, stmtDubbers, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1046var stmtHonksForUser, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1047var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1048var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1049var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1050var stmtZonkIt *sql.Stmt
1051
1052func preparetodie(db *sql.DB, s string) *sql.Stmt {
1053	stmt, err := db.Prepare(s)
1054	if err != nil {
1055		log.Fatalf("error %s: %s", err, s)
1056	}
1057	return stmt
1058}
1059
1060func prepareStatements(db *sql.DB) {
1061	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1062	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1063	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 = ?")
1064	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")
1065	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")
1066	stmtHonksForUser = preparetodie(db, "select honkid, honks.userid, users.username, what, honker, xid, rid, dt, url, audience, noise, convoy from honks join users on honks.userid = users.userid where honks.userid = ? and dt > ? and convoy not in (select name from zonkers where wherefore = 'zonvoy' order by zonkerid desc limit 100) order by honkid desc limit 250")
1067	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")
1068	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1069	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1070	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1071	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1072	stmtDeleteHonk = preparetodie(db, "update honks set what = 'zonk' where xid = ? and honker = ?")
1073	stmtFindFile = preparetodie(db, "select fileid from files where url = ?")
1074	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1075	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey from users where username = ?")
1076	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1077	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1078	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1079	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1080	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1081	stmtZonkIt = preparetodie(db, "update honks set what = 'zonk' where userid = ? and xid = ?")
1082}
1083
1084func ElaborateUnitTests() {
1085}
1086
1087func finishusersetup() error {
1088	db := opendatabase()
1089	k, err := rsa.GenerateKey(rand.Reader, 2048)
1090	if err != nil {
1091		return err
1092	}
1093	pubkey, err := zem(&k.PublicKey)
1094	if err != nil {
1095		return err
1096	}
1097	seckey, err := zem(k)
1098	if err != nil {
1099		return err
1100	}
1101	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1102	if err != nil {
1103		return err
1104	}
1105	return nil
1106}
1107
1108func main() {
1109	cmd := "run"
1110	if len(os.Args) > 1 {
1111		cmd = os.Args[1]
1112	}
1113	switch cmd {
1114	case "init":
1115		initdb()
1116	case "upgrade":
1117		upgradedb()
1118	}
1119	db := opendatabase()
1120	dbversion := 0
1121	getconfig("dbversion", &dbversion)
1122	if dbversion != myVersion {
1123		log.Fatal("incorrect database version. run upgrade.")
1124	}
1125	getconfig("servername", &serverName)
1126	prepareStatements(db)
1127	switch cmd {
1128	case "ping":
1129		if len(os.Args) < 4 {
1130			fmt.Printf("usage: honk ping from to\n")
1131			return
1132		}
1133		name := os.Args[2]
1134		targ := os.Args[3]
1135		user, err := butwhatabout(name)
1136		if err != nil {
1137			log.Printf("unknown user")
1138			return
1139		}
1140		ping(user, targ)
1141	case "peep":
1142		peeppeep()
1143	case "run":
1144		serve()
1145	case "test":
1146		ElaborateUnitTests()
1147	default:
1148		log.Fatal("unknown command")
1149	}
1150}