all repos — honk @ ecde389a3ea23f4a45f1a3ab36d0ceaae3989f0b

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