all repos — honk @ 4a0a0768c71c2bdcee020fd35f47142071c5d410

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