all repos — honk @ 7fb85a75f618a25acc66c89cf7d131cfaf75509a

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