all repos — honk @ 962443422e5009397c2b3ce2cbdcccb3d51a02db

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