all repos — honk @ 418d9fb9a10e445a0fa7c641e9a6b59278ab1a32

my fork of honk

honk.go (view raw)

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