all repos — honk @ v0.2.4

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