all repos — honk @ e0cc48fd6d5ce43fdced313e4ec943bfb9c9bd95

my fork of honk

honk.go (view raw)

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