all repos — honk @ fd700453c2a28d7b4df6e971a956404ea7ae6bab

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