all repos — honk @ cddb36be49b98ad95472413e9f9c5b1bd48e2f9e

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	herd := herdofemus(honk.Noise)
1002	for _, e := range herd {
1003		donk := savedonk(e.ID, e.Name, "image/png")
1004		if donk != nil {
1005			honk.Donks = append(honk.Donks, donk)
1006		}
1007	}
1008
1009	aud := strings.Join(honk.Audience, " ")
1010	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
1011		dt.Format(dbtimeformat), "", aud, noise)
1012	if err != nil {
1013		log.Printf("error saving honk: %s", err)
1014		return
1015	}
1016	honk.ID, _ = res.LastInsertId()
1017	for _, d := range honk.Donks {
1018		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1019		if err != nil {
1020			log.Printf("err saving donk: %s", err)
1021			return
1022		}
1023	}
1024
1025	user, _ := butwhatabout(userinfo.Username)
1026
1027	go honkworldwide(user, &honk)
1028
1029	http.Redirect(w, r, "/", http.StatusSeeOther)
1030}
1031
1032func showhonkers(w http.ResponseWriter, r *http.Request) {
1033	userinfo := GetUserInfo(r)
1034	templinfo := getInfo(r)
1035	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1036	templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
1037	err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
1038	if err != nil {
1039		log.Print(err)
1040	}
1041}
1042
1043var handfull = make(map[string]string)
1044var handlock sync.Mutex
1045
1046func gofish(name string) string {
1047	if name[0] == '@' {
1048		name = name[1:]
1049	}
1050	m := strings.Split(name, "@")
1051	if len(m) != 2 {
1052		log.Printf("bad far name: %s", name)
1053		return ""
1054	}
1055	handlock.Lock()
1056	defer handlock.Unlock()
1057	ref, ok := handfull[name]
1058	if ok {
1059		return ref
1060	}
1061	j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
1062	if err != nil {
1063		log.Printf("failed to get far name: %s", err)
1064		handfull[name] = ""
1065		return ""
1066	}
1067	links, _ := jsongetarray(j, "links")
1068	for _, l := range links {
1069		href, _ := jsongetstring(l, "href")
1070		rel, _ := jsongetstring(l, "rel")
1071		t, _ := jsongetstring(l, "type")
1072		if rel == "self" && friendorfoe(t) {
1073			handfull[name] = href
1074			return href
1075		}
1076	}
1077	handfull[name] = ""
1078	return ""
1079}
1080
1081func savehonker(w http.ResponseWriter, r *http.Request) {
1082	name := r.FormValue("name")
1083	url := r.FormValue("url")
1084	peep := r.FormValue("peep")
1085	flavor := "presub"
1086	if peep == "peep" {
1087		flavor = "peep"
1088	}
1089
1090	if url == "" {
1091		return
1092	}
1093	if url[0] == '@' {
1094		url = gofish(url)
1095	}
1096	if url == "" {
1097		return
1098	}
1099
1100	u := GetUserInfo(r)
1101	db := opendatabase()
1102	_, err := db.Exec("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)",
1103		u.UserID, name, url, flavor)
1104	if err != nil {
1105		log.Print(err)
1106	}
1107	if flavor == "presub" {
1108		user, _ := butwhatabout(u.Username)
1109		go subsub(user, url)
1110	}
1111	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1112}
1113
1114func avatate(w http.ResponseWriter, r *http.Request) {
1115	n := r.FormValue("a")
1116	a := avatar(n)
1117	w.Header().Set("Cache-Control", "max-age=432000")
1118	w.Write(a)
1119}
1120
1121func servecss(w http.ResponseWriter, r *http.Request) {
1122	w.Header().Set("Cache-Control", "max-age=7776000")
1123	http.ServeFile(w, r, "views"+r.URL.Path)
1124}
1125func servehtml(w http.ResponseWriter, r *http.Request) {
1126	templinfo := getInfo(r)
1127	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1128	if err != nil {
1129		log.Print(err)
1130	}
1131}
1132func serveemu(w http.ResponseWriter, r *http.Request) {
1133	xid := mux.Vars(r)["xid"]
1134	w.Header().Set("Cache-Control", "max-age=432000")
1135	http.ServeFile(w, r, "emus/"+xid)
1136}
1137
1138func servefile(w http.ResponseWriter, r *http.Request) {
1139	xid := mux.Vars(r)["xid"]
1140	row := stmtFileData.QueryRow(xid)
1141	var data []byte
1142	err := row.Scan(&data)
1143	if err != nil {
1144		log.Printf("error loading file: %s", err)
1145		http.NotFound(w, r)
1146		return
1147	}
1148	w.Header().Set("Cache-Control", "max-age=432000")
1149	w.Write(data)
1150}
1151
1152func serve() {
1153	db := opendatabase()
1154	LoginInit(db)
1155
1156	listener, err := openListener()
1157	if err != nil {
1158		log.Fatal(err)
1159	}
1160	debug := false
1161	getconfig("debug", &debug)
1162	readviews = ParseTemplates(debug,
1163		"views/homepage.html",
1164		"views/honkpage.html",
1165		"views/honkers.html",
1166		"views/honkform.html",
1167		"views/honk.html",
1168		"views/login.html",
1169		"views/header.html",
1170	)
1171	if !debug {
1172		s := "views/style.css"
1173		savedstyleparams[s] = getstyleparam(s)
1174		s = "views/local.css"
1175		savedstyleparams[s] = getstyleparam(s)
1176	}
1177
1178	mux := mux.NewRouter()
1179	mux.Use(LoginChecker)
1180
1181	posters := mux.Methods("POST").Subrouter()
1182	getters := mux.Methods("GET").Subrouter()
1183
1184	getters.HandleFunc("/", homepage)
1185	getters.HandleFunc("/rss", showrss)
1186	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1187	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1188	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1189	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1190	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1191	getters.HandleFunc("/a", avatate)
1192	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1193	getters.HandleFunc("/emu/{xid:[[:alnum:]_.]+}", serveemu)
1194	getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1195	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1196
1197	getters.HandleFunc("/style.css", servecss)
1198	getters.HandleFunc("/local.css", servecss)
1199	getters.HandleFunc("/login", servehtml)
1200	posters.HandleFunc("/dologin", dologin)
1201	getters.HandleFunc("/logout", dologout)
1202
1203	loggedin := mux.NewRoute().Subrouter()
1204	loggedin.Use(LoginRequired)
1205	loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1206	loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1207	loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1208	loggedin.HandleFunc("/honkers", showhonkers)
1209	loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1210
1211	err = http.Serve(listener, mux)
1212	if err != nil {
1213		log.Fatal(err)
1214	}
1215}
1216
1217var stmtHonkers, stmtDubbers, stmtOneHonk, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1218var stmtHonksForUser, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1219var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1220var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1221
1222func prepareStatements(db *sql.DB) {
1223	var err error
1224	stmtHonkers, err = db.Prepare("select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1225	if err != nil {
1226		log.Fatal(err)
1227	}
1228	stmtDubbers, err = db.Prepare("select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1229	if err != nil {
1230		log.Fatal(err)
1231	}
1232	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")
1233	if err != nil {
1234		log.Fatal(err)
1235	}
1236	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 = ?")
1237	if err != nil {
1238		log.Fatal(err)
1239	}
1240	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")
1241	if err != nil {
1242		log.Fatal(err)
1243	}
1244	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")
1245	if err != nil {
1246		log.Fatal(err)
1247	}
1248	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")
1249	if err != nil {
1250		log.Fatal(err)
1251	}
1252	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")
1253	if err != nil {
1254		log.Fatal(err)
1255	}
1256	stmtSaveHonk, err = db.Prepare("insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
1257	if err != nil {
1258		log.Fatal(err)
1259	}
1260	stmtFileData, err = db.Prepare("select content from files where xid = ?")
1261	if err != nil {
1262		log.Fatal(err)
1263	}
1264	stmtFindXonk, err = db.Prepare("select honkid from honks where userid = ? and xid = ? and what = ?")
1265	if err != nil {
1266		log.Fatal(err)
1267	}
1268	stmtSaveDonk, err = db.Prepare("insert into donks (honkid, fileid) values (?, ?)")
1269	if err != nil {
1270		log.Fatal(err)
1271	}
1272	stmtDeleteHonk, err = db.Prepare("update honks set what = 'zonk' where xid = ? and honker = ?")
1273	if err != nil {
1274		log.Fatal(err)
1275	}
1276	stmtFindFile, err = db.Prepare("select fileid from files where url = ?")
1277	if err != nil {
1278		log.Fatal(err)
1279	}
1280	stmtSaveFile, err = db.Prepare("insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1281	if err != nil {
1282		log.Fatal(err)
1283	}
1284	stmtWhatAbout, err = db.Prepare("select userid, username, displayname, about, pubkey from users where username = ?")
1285	if err != nil {
1286		log.Fatal(err)
1287	}
1288	stmtSaveDub, err = db.Prepare("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1289	if err != nil {
1290		log.Fatal(err)
1291	}
1292}
1293
1294func ElaborateUnitTests() {
1295}
1296
1297func finishusersetup() error {
1298	db := opendatabase()
1299	k, err := rsa.GenerateKey(rand.Reader, 2048)
1300	if err != nil {
1301		return err
1302	}
1303	pubkey, err := zem(&k.PublicKey)
1304	if err != nil {
1305		return err
1306	}
1307	seckey, err := zem(k)
1308	if err != nil {
1309		return err
1310	}
1311	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1312	if err != nil {
1313		return err
1314	}
1315	return nil
1316}
1317
1318func main() {
1319	cmd := "run"
1320	if len(os.Args) > 1 {
1321		cmd = os.Args[1]
1322	}
1323	if cmd != "init" {
1324		db := opendatabase()
1325		prepareStatements(db)
1326		getconfig("servername", &serverName)
1327	}
1328	switch cmd {
1329	case "ping":
1330		if len(os.Args) < 4 {
1331			fmt.Printf("usage: honk ping from to\n")
1332			return
1333		}
1334		name := os.Args[2]
1335		targ := os.Args[3]
1336		user, err := butwhatabout(name)
1337		if err != nil {
1338			log.Printf("unknown user")
1339			return
1340		}
1341		ping(user, targ)
1342	case "peep":
1343		peeppeep()
1344	case "init":
1345		initdb()
1346	case "run":
1347		serve()
1348	case "test":
1349		ElaborateUnitTests()
1350	default:
1351		log.Fatal("unknown command")
1352	}
1353}