all repos — honk @ 7ff15d164af060074fcf73ae3c54c516cf516fef

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