all repos — honk @ a8984000538dd76de12f4ea99eb4c5a0db7067c5

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