all repos — honk @ v0.1.0

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