all repos — honk @ 2c0310a318c981a3bd698c2f1f915419e655872f

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		honk.Audience = append(honk.Audience, xonk.Audience...)
 872	}
 873	honk.Audience = oneofakind(honk.Audience)
 874	noise = obfusbreak(noise)
 875	honk.Noise = noise
 876
 877	file, _, err := r.FormFile("donk")
 878	if err == nil {
 879		var buf bytes.Buffer
 880		io.Copy(&buf, file)
 881		file.Close()
 882		data := buf.Bytes()
 883		img, format, err := image.Decode(&buf)
 884		if err != nil {
 885			log.Printf("bad image: %s", err)
 886			return
 887		}
 888		data, format, err = vacuumwrap(img, format)
 889		if err != nil {
 890			log.Printf("can't vacuum image: %s", err)
 891			return
 892		}
 893		name := xfiltrate()
 894		media := "image/" + format
 895		if format == "jpeg" {
 896			format = "jpg"
 897		}
 898		name = name + "." + format
 899		url := fmt.Sprintf("https://%s/d/%s", serverName, name)
 900		res, err := stmtSaveFile.Exec(name, name, url, media, data)
 901		if err != nil {
 902			log.Printf("unable to save image: %s", err)
 903			return
 904		}
 905		var d Donk
 906		d.FileID, _ = res.LastInsertId()
 907		d.XID = name
 908		d.Name = name
 909		d.Media = media
 910		d.URL = url
 911		honk.Donks = append(honk.Donks, &d)
 912	}
 913
 914	aud := strings.Join(honk.Audience, " ")
 915	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, "", xid, rid,
 916		dt.Format(dbtimeformat), "", aud, noise)
 917	if err != nil {
 918		log.Printf("error saving honk: %s", err)
 919		return
 920	}
 921	honk.ID, _ = res.LastInsertId()
 922	for _, d := range honk.Donks {
 923		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
 924		if err != nil {
 925			log.Printf("err saving donk: %s", err)
 926			return
 927		}
 928	}
 929
 930	user, _ := butwhatabout(userinfo.Username)
 931
 932	go honkworldwide(user, &honk)
 933
 934	http.Redirect(w, r, "/", http.StatusSeeOther)
 935}
 936
 937func showhonkers(w http.ResponseWriter, r *http.Request) {
 938	userinfo := GetUserInfo(r)
 939	templinfo := getInfo(r)
 940	templinfo["Honkers"] = gethonkers(userinfo.UserID)
 941	templinfo["HonkerCSRF"] = GetCSRF("savehonker", r)
 942	err := readviews.ExecuteTemplate(w, "honkers.html", templinfo)
 943	if err != nil {
 944		log.Print(err)
 945	}
 946}
 947
 948var handfull = make(map[string]string)
 949var handlock sync.Mutex
 950
 951func gofish(name string) string {
 952	if name[0] == '@' {
 953		name = name[1:]
 954	}
 955	m := strings.Split(name, "@")
 956	if len(m) != 2 {
 957		log.Printf("bad far name: %s", name)
 958		return ""
 959	}
 960	handlock.Lock()
 961	defer handlock.Unlock()
 962	ref, ok := handfull[name]
 963	if ok {
 964		return ref
 965	}
 966	j, err := GetJunk(fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name))
 967	if err != nil {
 968		log.Printf("failed to get far name: %s", err)
 969		handfull[name] = ""
 970		return ""
 971	}
 972	links, _ := jsongetarray(j, "links")
 973	for _, l := range links {
 974		href, _ := jsongetstring(l, "href")
 975		rel, _ := jsongetstring(l, "rel")
 976		t, _ := jsongetstring(l, "type")
 977		if rel == "self" && friendorfoe(t) {
 978			handfull[name] = href
 979			return href
 980		}
 981	}
 982	handfull[name] = ""
 983	return ""
 984}
 985
 986func savehonker(w http.ResponseWriter, r *http.Request) {
 987	name := r.FormValue("name")
 988	url := r.FormValue("url")
 989	peep := r.FormValue("peep")
 990	flavor := "presub"
 991	if peep == "peep" {
 992		flavor = "peep"
 993	}
 994
 995	if url == "" {
 996		return
 997	}
 998	if url[0] == '@' {
 999		url = gofish(url)
1000	}
1001	if url == "" {
1002		return
1003	}
1004
1005	u := GetUserInfo(r)
1006	db := opendatabase()
1007	_, err := db.Exec("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)",
1008		u.UserID, name, url, flavor)
1009	if err != nil {
1010		log.Print(err)
1011	}
1012	if flavor == "presub" {
1013		user, _ := butwhatabout(u.Username)
1014		go subsub(user, url)
1015	}
1016	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1017}
1018
1019func avatate(w http.ResponseWriter, r *http.Request) {
1020	n := r.FormValue("a")
1021	a := avatar(n)
1022	w.Header().Set("Cache-Control", "max-age=76000")
1023	w.Write(a)
1024}
1025
1026func servecss(w http.ResponseWriter, r *http.Request) {
1027	w.Header().Set("Cache-Control", "max-age=7776000")
1028	http.ServeFile(w, r, "views"+r.URL.Path)
1029}
1030func servehtml(w http.ResponseWriter, r *http.Request) {
1031	templinfo := getInfo(r)
1032	err := readviews.ExecuteTemplate(w, r.URL.Path[1:]+".html", templinfo)
1033	if err != nil {
1034		log.Print(err)
1035	}
1036}
1037
1038func servefile(w http.ResponseWriter, r *http.Request) {
1039	xid := mux.Vars(r)["xid"]
1040	row := stmtFileData.QueryRow(xid)
1041	var data []byte
1042	err := row.Scan(&data)
1043	if err != nil {
1044		log.Printf("error loading file: %s", err)
1045		http.NotFound(w, r)
1046		return
1047	}
1048	w.Header().Set("Cache-Control", "max-age=432000")
1049	w.Write(data)
1050}
1051
1052func serve() {
1053	db := opendatabase()
1054	LoginInit(db)
1055
1056	getconfig("servername", &serverName)
1057	listener, err := openListener()
1058	if err != nil {
1059		log.Fatal(err)
1060	}
1061	debug := false
1062	getconfig("debug", &debug)
1063	readviews = ParseTemplates(debug,
1064		"views/homepage.html",
1065		"views/honkpage.html",
1066		"views/honkers.html",
1067		"views/honkform.html",
1068		"views/honk.html",
1069		"views/login.html",
1070		"views/header.html",
1071	)
1072	if !debug {
1073		savedstyleparam = getstyleparam()
1074	}
1075
1076	mux := mux.NewRouter()
1077	mux.Use(LoginChecker)
1078
1079	posters := mux.Methods("POST").Subrouter()
1080	getters := mux.Methods("GET").Subrouter()
1081
1082	getters.HandleFunc("/", homepage)
1083	getters.HandleFunc("/rss", showrss)
1084	getters.HandleFunc("/u/{name:[[:alnum:]]+}", viewuser)
1085	getters.HandleFunc("/u/{name:[[:alnum:]]+}/h/{xid:[[:alnum:]]+}", viewhonk)
1086	getters.HandleFunc("/u/{name:[[:alnum:]]+}/rss", showrss)
1087	posters.HandleFunc("/u/{name:[[:alnum:]]+}/inbox", inbox)
1088	getters.HandleFunc("/u/{name:[[:alnum:]]+}/outbox", outbox)
1089	getters.HandleFunc("/a", avatate)
1090	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1091	getters.HandleFunc("/h/{name:[[:alnum:]]+}", viewhonker)
1092	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1093
1094	getters.HandleFunc("/style.css", servecss)
1095	getters.HandleFunc("/login", servehtml)
1096	posters.HandleFunc("/dologin", dologin)
1097	getters.HandleFunc("/logout", dologout)
1098
1099	loggedin := mux.NewRoute().Subrouter()
1100	loggedin.Use(LoginRequired)
1101	loggedin.Handle("/honk", CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1102	loggedin.Handle("/bonk", CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1103	loggedin.Handle("/saveuser", CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1104	loggedin.HandleFunc("/honkers", showhonkers)
1105	loggedin.Handle("/savehonker", CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1106
1107	err = http.Serve(listener, mux)
1108	if err != nil {
1109		log.Fatal(err)
1110	}
1111}
1112
1113var stmtHonkers, stmtDubbers, stmtOneHonk, stmtOneXonk, stmtHonks, stmtUserHonks *sql.Stmt
1114var stmtHonksForUser, stmtDeleteHonk, stmtSaveDub *sql.Stmt
1115var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1116var stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1117
1118func prepareStatements(db *sql.DB) {
1119	var err error
1120	stmtHonkers, err = db.Prepare("select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'sub' or flavor = 'peep'")
1121	if err != nil {
1122		log.Fatal(err)
1123	}
1124	stmtDubbers, err = db.Prepare("select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1125	if err != nil {
1126		log.Fatal(err)
1127	}
1128	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")
1129	if err != nil {
1130		log.Fatal(err)
1131	}
1132	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 = ?")
1133	if err != nil {
1134		log.Fatal(err)
1135	}
1136	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")
1137	if err != nil {
1138		log.Fatal(err)
1139	}
1140	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")
1141	if err != nil {
1142		log.Fatal(err)
1143	}
1144	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")
1145	if err != nil {
1146		log.Fatal(err)
1147	}
1148	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")
1149	if err != nil {
1150		log.Fatal(err)
1151	}
1152	stmtSaveHonk, err = db.Prepare("insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
1153	if err != nil {
1154		log.Fatal(err)
1155	}
1156	stmtFileData, err = db.Prepare("select content from files where xid = ?")
1157	if err != nil {
1158		log.Fatal(err)
1159	}
1160	stmtFindXonk, err = db.Prepare("select honkid from honks where userid = ? and xid = ? and what = ?")
1161	if err != nil {
1162		log.Fatal(err)
1163	}
1164	stmtSaveDonk, err = db.Prepare("insert into donks (honkid, fileid) values (?, ?)")
1165	if err != nil {
1166		log.Fatal(err)
1167	}
1168	stmtDeleteHonk, err = db.Prepare("update honks set what = 'zonk' where xid = ? and honker = ?")
1169	if err != nil {
1170		log.Fatal(err)
1171	}
1172	stmtFindFile, err = db.Prepare("select fileid from files where url = ?")
1173	if err != nil {
1174		log.Fatal(err)
1175	}
1176	stmtSaveFile, err = db.Prepare("insert into files (xid, name, url, media, content) values (?, ?, ?, ?, ?)")
1177	if err != nil {
1178		log.Fatal(err)
1179	}
1180	stmtWhatAbout, err = db.Prepare("select userid, username, displayname, about, pubkey from users where username = ?")
1181	if err != nil {
1182		log.Fatal(err)
1183	}
1184	stmtSaveDub, err = db.Prepare("insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1185	if err != nil {
1186		log.Fatal(err)
1187	}
1188}
1189
1190func ElaborateUnitTests() {
1191}
1192
1193func finishusersetup() error {
1194	db := opendatabase()
1195	k, err := rsa.GenerateKey(rand.Reader, 2048)
1196	if err != nil {
1197		return err
1198	}
1199	pubkey, err := zem(&k.PublicKey)
1200	if err != nil {
1201		return err
1202	}
1203	seckey, err := zem(k)
1204	if err != nil {
1205		return err
1206	}
1207	_, err = db.Exec("update users set displayname = username, about = ?, pubkey = ?, seckey = ? where userid = 1", "what about me?", pubkey, seckey)
1208	if err != nil {
1209		return err
1210	}
1211	return nil
1212}
1213
1214func main() {
1215	cmd := "run"
1216	if len(os.Args) > 1 {
1217		cmd = os.Args[1]
1218	}
1219	if cmd != "init" {
1220		db := opendatabase()
1221		prepareStatements(db)
1222	}
1223	switch cmd {
1224	case "peep":
1225		peeppeep()
1226	case "init":
1227		initdb()
1228	case "run":
1229		serve()
1230	case "test":
1231		ElaborateUnitTests()
1232	default:
1233		log.Fatal("unknown command")
1234	}
1235}