all repos — honk @ 85aa13c31a3299ecf4d046b2a0475ba5176057f5

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