all repos — honk @ 5f108eceda00a5c93368d6218beab5b2c157441c

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	"database/sql"
  21	"fmt"
  22	"html"
  23	"html/template"
  24	"io"
  25	"log"
  26	notrand "math/rand"
  27	"net/http"
  28	"net/url"
  29	"os"
  30	"sort"
  31	"strconv"
  32	"strings"
  33	"time"
  34
  35	"github.com/gorilla/mux"
  36	"humungus.tedunangst.com/r/webs/htfilter"
  37	"humungus.tedunangst.com/r/webs/httpsig"
  38	"humungus.tedunangst.com/r/webs/image"
  39	"humungus.tedunangst.com/r/webs/junk"
  40	"humungus.tedunangst.com/r/webs/login"
  41	"humungus.tedunangst.com/r/webs/rss"
  42	"humungus.tedunangst.com/r/webs/templates"
  43)
  44
  45type WhatAbout struct {
  46	ID        int64
  47	Name      string
  48	Display   string
  49	About     string
  50	Key       string
  51	URL       string
  52	SkinnyCSS bool
  53}
  54
  55type Honk struct {
  56	ID       int64
  57	UserID   int64
  58	Username string
  59	What     string
  60	Honker   string
  61	Handle   string
  62	Oonker   string
  63	Oondle   string
  64	XID      string
  65	RID      string
  66	Date     time.Time
  67	URL      string
  68	Noise    string
  69	Precis   string
  70	Convoy   string
  71	Audience []string
  72	Public   bool
  73	Whofore  int64
  74	HTML     template.HTML
  75	Style    string
  76	Open     string
  77	Donks    []*Donk
  78}
  79
  80type Donk struct {
  81	FileID  int64
  82	XID     string
  83	Name    string
  84	URL     string
  85	Media   string
  86	Local   bool
  87	Content []byte
  88}
  89
  90type Honker struct {
  91	ID     int64
  92	UserID int64
  93	Name   string
  94	XID    string
  95	Flavor string
  96	Combos []string
  97}
  98
  99var serverName string
 100var iconName = "icon.png"
 101var serverMsg = "Things happen."
 102
 103var userSep = "u"
 104var honkSep = "h"
 105
 106var readviews *templates.Template
 107
 108func getuserstyle(u *login.UserInfo) template.CSS {
 109	if u == nil {
 110		return ""
 111	}
 112	user, _ := butwhatabout(u.Username)
 113	if user.SkinnyCSS {
 114		return "main { max-width: 700px; }"
 115	}
 116	return ""
 117}
 118
 119func getInfo(r *http.Request) map[string]interface{} {
 120	u := login.GetUserInfo(r)
 121	templinfo := make(map[string]interface{})
 122	templinfo["StyleParam"] = getstyleparam("views/style.css")
 123	templinfo["LocalStyleParam"] = getstyleparam("views/local.css")
 124	templinfo["UserStyle"] = getuserstyle(u)
 125	templinfo["ServerName"] = serverName
 126	templinfo["IconName"] = iconName
 127	templinfo["UserInfo"] = u
 128	templinfo["UserSep"] = userSep
 129	return templinfo
 130}
 131
 132var donotfedafterdark = make(map[string]bool)
 133
 134func stealthed(r *http.Request) bool {
 135	addr := r.Header.Get("X-Forwarded-For")
 136	fake := donotfedafterdark[addr]
 137	if fake {
 138		log.Printf("faking 404 for %s", addr)
 139	}
 140	return fake
 141}
 142
 143func homepage(w http.ResponseWriter, r *http.Request) {
 144	templinfo := getInfo(r)
 145	u := login.GetUserInfo(r)
 146	var honks []*Honk
 147	var userid int64 = -1
 148	if r.URL.Path == "/front" || u == nil {
 149		honks = getpublichonks()
 150	} else {
 151		userid = u.UserID
 152		if r.URL.Path == "/atme" {
 153			honks = gethonksforme(userid)
 154		} else {
 155			honks = gethonksforuser(userid)
 156			honks = osmosis(honks, userid)
 157		}
 158		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 159	}
 160
 161	reverbolate(userid, honks)
 162
 163	templinfo["Honks"] = honks
 164	templinfo["ShowRSS"] = true
 165	templinfo["ServerMessage"] = serverMsg
 166	if u == nil {
 167		w.Header().Set("Cache-Control", "max-age=60")
 168	} else {
 169		w.Header().Set("Cache-Control", "max-age=0")
 170	}
 171	err := readviews.Execute(w, "honkpage.html", templinfo)
 172	if err != nil {
 173		log.Print(err)
 174	}
 175}
 176
 177func showfunzone(w http.ResponseWriter, r *http.Request) {
 178	var emunames, memenames []string
 179	dir, err := os.Open("emus")
 180	if err == nil {
 181		emunames, _ = dir.Readdirnames(0)
 182		dir.Close()
 183	}
 184	for i, e := range emunames {
 185		if len(e) > 4 {
 186			emunames[i] = e[:len(e)-4]
 187		}
 188	}
 189	dir, err = os.Open("memes")
 190	if err == nil {
 191		memenames, _ = dir.Readdirnames(0)
 192		dir.Close()
 193	}
 194	templinfo := getInfo(r)
 195	templinfo["Emus"] = emunames
 196	templinfo["Memes"] = memenames
 197	err = readviews.Execute(w, "funzone.html", templinfo)
 198	if err != nil {
 199		log.Print(err)
 200	}
 201
 202}
 203
 204func showrss(w http.ResponseWriter, r *http.Request) {
 205	name := mux.Vars(r)["name"]
 206
 207	var honks []*Honk
 208	if name != "" {
 209		honks = gethonksbyuser(name, false)
 210	} else {
 211		honks = getpublichonks()
 212	}
 213	reverbolate(-1, honks)
 214
 215	home := fmt.Sprintf("https://%s/", serverName)
 216	base := home
 217	if name != "" {
 218		home += "u/" + name
 219		name += " "
 220	}
 221	feed := rss.Feed{
 222		Title:       name + "honk",
 223		Link:        home,
 224		Description: name + "honk rss",
 225		Image: &rss.Image{
 226			URL:   base + "icon.png",
 227			Title: name + "honk rss",
 228			Link:  home,
 229		},
 230	}
 231	var modtime time.Time
 232	for _, honk := range honks {
 233		desc := string(honk.HTML)
 234		for _, d := range honk.Donks {
 235			desc += fmt.Sprintf(`<p><a href="%s">Attachment: %s</a>`,
 236				d.URL, html.EscapeString(d.Name))
 237		}
 238
 239		feed.Items = append(feed.Items, &rss.Item{
 240			Title:       fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID),
 241			Description: rss.CData{desc},
 242			Link:        honk.URL,
 243			PubDate:     honk.Date.Format(time.RFC1123),
 244			Guid:        &rss.Guid{IsPermaLink: true, Value: honk.URL},
 245		})
 246		if honk.Date.After(modtime) {
 247			modtime = honk.Date
 248		}
 249	}
 250	w.Header().Set("Cache-Control", "max-age=300")
 251	w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat))
 252
 253	err := feed.Write(w)
 254	if err != nil {
 255		log.Printf("error writing rss: %s", err)
 256	}
 257}
 258
 259func butwhatabout(name string) (*WhatAbout, error) {
 260	row := stmtWhatAbout.QueryRow(name)
 261	var user WhatAbout
 262	var options string
 263	err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key, &options)
 264	user.URL = fmt.Sprintf("https://%s/%s/%s", serverName, userSep, user.Name)
 265	user.SkinnyCSS = strings.Contains(options, " skinny ")
 266	return &user, err
 267}
 268
 269func crappola(j junk.Junk) bool {
 270	t, _ := j.GetString("type")
 271	a, _ := j.GetString("actor")
 272	o, _ := j.GetString("object")
 273	if t == "Delete" && a == o {
 274		log.Printf("crappola from %s", a)
 275		return true
 276	}
 277	return false
 278}
 279
 280func ping(user *WhatAbout, who string) {
 281	box, err := getboxes(who)
 282	if err != nil {
 283		log.Printf("no inbox for ping: %s", err)
 284		return
 285	}
 286	j := junk.New()
 287	j["@context"] = itiswhatitis
 288	j["type"] = "Ping"
 289	j["id"] = user.URL + "/ping/" + xfiltrate()
 290	j["actor"] = user.URL
 291	j["to"] = who
 292	keyname, key := ziggy(user.Name)
 293	err = PostJunk(keyname, key, box.In, j)
 294	if err != nil {
 295		log.Printf("can't send ping: %s", err)
 296		return
 297	}
 298	log.Printf("sent ping to %s: %s", who, j["id"])
 299}
 300
 301func pong(user *WhatAbout, who string, obj string) {
 302	box, err := getboxes(who)
 303	if err != nil {
 304		log.Printf("no inbox for pong %s : %s", who, err)
 305		return
 306	}
 307	j := junk.New()
 308	j["@context"] = itiswhatitis
 309	j["type"] = "Pong"
 310	j["id"] = user.URL + "/pong/" + xfiltrate()
 311	j["actor"] = user.URL
 312	j["to"] = who
 313	j["object"] = obj
 314	keyname, key := ziggy(user.Name)
 315	err = PostJunk(keyname, key, box.In, j)
 316	if err != nil {
 317		log.Printf("can't send pong: %s", err)
 318		return
 319	}
 320}
 321
 322func inbox(w http.ResponseWriter, r *http.Request) {
 323	name := mux.Vars(r)["name"]
 324	user, err := butwhatabout(name)
 325	if err != nil {
 326		http.NotFound(w, r)
 327		return
 328	}
 329	var buf bytes.Buffer
 330	io.Copy(&buf, r.Body)
 331	payload := buf.Bytes()
 332	j, err := junk.Read(bytes.NewReader(payload))
 333	if err != nil {
 334		log.Printf("bad payload: %s", err)
 335		io.WriteString(os.Stdout, "bad payload\n")
 336		os.Stdout.Write(payload)
 337		io.WriteString(os.Stdout, "\n")
 338		return
 339	}
 340	if crappola(j) {
 341		return
 342	}
 343	keyname, err := httpsig.VerifyRequest(r, payload, zaggy)
 344	if err != nil {
 345		log.Printf("inbox message failed signature: %s", err)
 346		if keyname != "" {
 347			keyname, err = makeitworksomehowwithoutregardforkeycontinuity(keyname, r, payload)
 348			if err != nil {
 349				log.Printf("still failed: %s", err)
 350			}
 351		}
 352		if err != nil {
 353			return
 354		}
 355	}
 356	what, _ := j.GetString("type")
 357	if what == "Like" {
 358		return
 359	}
 360	who, _ := j.GetString("actor")
 361	origin := keymatch(keyname, who)
 362	if origin == "" {
 363		log.Printf("keyname actor mismatch: %s <> %s", keyname, who)
 364		return
 365	}
 366	objid, _ := j.GetString("id")
 367	if thoudostbitethythumb(user.ID, []string{who}, objid) {
 368		log.Printf("ignoring thumb sucker %s", who)
 369		return
 370	}
 371	switch what {
 372	case "Ping":
 373		obj, _ := j.GetString("id")
 374		log.Printf("ping from %s: %s", who, obj)
 375		pong(user, who, obj)
 376	case "Pong":
 377		obj, _ := j.GetString("object")
 378		log.Printf("pong from %s: %s", who, obj)
 379	case "Follow":
 380		obj, _ := j.GetString("object")
 381		if obj == user.URL {
 382			log.Printf("updating honker follow: %s", who)
 383			stmtSaveDub.Exec(user.ID, who, who, "dub")
 384			go rubadubdub(user, j)
 385		} else {
 386			log.Printf("can't follow %s", obj)
 387		}
 388	case "Accept":
 389		log.Printf("updating honker accept: %s", who)
 390		_, err = stmtUpdateFlavor.Exec("sub", user.ID, who, "presub")
 391		if err != nil {
 392			log.Printf("error updating honker: %s", err)
 393			return
 394		}
 395	case "Update":
 396		obj, ok := j.GetMap("object")
 397		if ok {
 398			what, _ := obj.GetString("type")
 399			switch what {
 400			case "Person":
 401				return
 402			case "Question":
 403				return
 404			}
 405		}
 406		log.Printf("unknown Update activity")
 407		fd, _ := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 408		j.Write(fd)
 409		io.WriteString(fd, "\n")
 410		fd.Close()
 411
 412	case "Undo":
 413		obj, ok := j.GetMap("object")
 414		if !ok {
 415			log.Printf("unknown undo no object")
 416		} else {
 417			what, _ := obj.GetString("type")
 418			switch what {
 419			case "Follow":
 420				log.Printf("updating honker undo: %s", who)
 421				_, err = stmtUpdateFlavor.Exec("undub", user.ID, who, "dub")
 422				if err != nil {
 423					log.Printf("error updating honker: %s", err)
 424					return
 425				}
 426			case "Like":
 427			case "Announce":
 428			default:
 429				log.Printf("unknown undo: %s", what)
 430			}
 431		}
 432	default:
 433		go consumeactivity(user, j, origin)
 434	}
 435}
 436
 437func ximport(w http.ResponseWriter, r *http.Request) {
 438	xid := r.FormValue("xid")
 439	x := investigate(xid)
 440	if x != "" {
 441		xid = x
 442	}
 443	j, err := GetJunk(xid)
 444	if err != nil {
 445		http.Error(w, "error getting external object", http.StatusInternalServerError)
 446		log.Printf("error getting external object: %s", err)
 447		return
 448	}
 449	log.Printf("importing %s", xid)
 450	u := login.GetUserInfo(r)
 451	user, _ := butwhatabout(u.Username)
 452
 453	what, _ := j.GetString("type")
 454	if isactor(what) {
 455		outbox, _ := j.GetString("outbox")
 456		gimmexonks(user, outbox)
 457		http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther)
 458		return
 459	}
 460	xonk := xonkxonk(user, j, originate(xid))
 461	convoy := ""
 462	if xonk != nil {
 463		convoy = xonk.Convoy
 464		savexonk(user, xonk)
 465	}
 466	http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther)
 467}
 468
 469func xzone(w http.ResponseWriter, r *http.Request) {
 470	u := login.GetUserInfo(r)
 471	var honkers []string
 472	rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID)
 473	if err != nil {
 474		log.Printf("query err: %s", err)
 475		return
 476	}
 477	defer rows.Close()
 478	for rows.Next() {
 479		var s string
 480		rows.Scan(&s)
 481		honkers = append(honkers, s)
 482	}
 483
 484	templinfo := getInfo(r)
 485	templinfo["XCSRF"] = login.GetCSRF("ximport", r)
 486	templinfo["Honkers"] = honkers
 487	err = readviews.Execute(w, "xzone.html", templinfo)
 488	if err != nil {
 489		log.Print(err)
 490	}
 491}
 492
 493func outbox(w http.ResponseWriter, r *http.Request) {
 494	name := mux.Vars(r)["name"]
 495	user, err := butwhatabout(name)
 496	if err != nil {
 497		http.NotFound(w, r)
 498		return
 499	}
 500	if stealthed(r) {
 501		http.NotFound(w, r)
 502		return
 503	}
 504
 505	honks := gethonksbyuser(name, false)
 506
 507	var jonks []junk.Junk
 508	for _, h := range honks {
 509		if bloat_iscounter(h) {
 510			continue
 511		}
 512		j, _ := jonkjonk(user, h)
 513		jonks = append(jonks, j)
 514	}
 515
 516	j := junk.New()
 517	j["@context"] = itiswhatitis
 518	j["id"] = user.URL + "/outbox"
 519	j["type"] = "OrderedCollection"
 520	j["totalItems"] = len(jonks)
 521	j["orderedItems"] = jonks
 522
 523	w.Header().Set("Content-Type", theonetruename)
 524	j.Write(w)
 525}
 526
 527func emptiness(w http.ResponseWriter, r *http.Request) {
 528	name := mux.Vars(r)["name"]
 529	user, err := butwhatabout(name)
 530	if err != nil {
 531		http.NotFound(w, r)
 532		return
 533	}
 534	colname := "/followers"
 535	if strings.HasSuffix(r.URL.Path, "/following") {
 536		colname = "/following"
 537	}
 538	j := junk.New()
 539	j["@context"] = itiswhatitis
 540	j["id"] = user.URL + colname
 541	j["type"] = "OrderedCollection"
 542	j["totalItems"] = 0
 543	j["orderedItems"] = []junk.Junk{}
 544
 545	w.Header().Set("Content-Type", theonetruename)
 546	j.Write(w)
 547}
 548
 549func showuser(w http.ResponseWriter, r *http.Request) {
 550	name := mux.Vars(r)["name"]
 551	user, err := butwhatabout(name)
 552	if err != nil {
 553		log.Printf("user not found %s: %s", name, err)
 554		http.NotFound(w, r)
 555		return
 556	}
 557	if friendorfoe(r.Header.Get("Accept")) {
 558		j := asjonker(user)
 559		w.Header().Set("Content-Type", theonetruename)
 560		j.Write(w)
 561		return
 562	}
 563	u := login.GetUserInfo(r)
 564	honks := gethonksbyuser(name, u != nil && u.Username == name)
 565	honkpage(w, r, u, user, honks, "")
 566}
 567
 568func showhonker(w http.ResponseWriter, r *http.Request) {
 569	u := login.GetUserInfo(r)
 570	name := mux.Vars(r)["name"]
 571	var honks []*Honk
 572	if name == "" {
 573		name = r.FormValue("xid")
 574		honks = gethonksbyxonker(u.UserID, name)
 575	} else {
 576		honks = gethonksbyhonker(u.UserID, name)
 577	}
 578	name = html.EscapeString(name)
 579	msg := fmt.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>`, name, name)
 580	honkpage(w, r, u, nil, honks, template.HTML(msg))
 581}
 582
 583func showcombo(w http.ResponseWriter, r *http.Request) {
 584	name := mux.Vars(r)["name"]
 585	u := login.GetUserInfo(r)
 586	honks := gethonksbycombo(u.UserID, name)
 587	honks = osmosis(honks, u.UserID)
 588	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks by combo: "+name)))
 589}
 590func showconvoy(w http.ResponseWriter, r *http.Request) {
 591	c := r.FormValue("c")
 592	u := login.GetUserInfo(r)
 593	honks := gethonksbyconvoy(u.UserID, c)
 594	honkpage(w, r, u, nil, honks, template.HTML(html.EscapeString("honks in convoy: "+c)))
 595}
 596
 597func showhonk(w http.ResponseWriter, r *http.Request) {
 598	name := mux.Vars(r)["name"]
 599	user, err := butwhatabout(name)
 600	if err != nil {
 601		http.NotFound(w, r)
 602		return
 603	}
 604	if stealthed(r) {
 605		http.NotFound(w, r)
 606		return
 607	}
 608
 609	xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path)
 610	h := getxonk(user.ID, xid)
 611	if h == nil {
 612		http.NotFound(w, r)
 613		return
 614	}
 615	u := login.GetUserInfo(r)
 616	if u != nil && u.UserID != user.ID {
 617		u = nil
 618	}
 619	if !h.Public {
 620		if u == nil {
 621			http.NotFound(w, r)
 622			return
 623
 624		}
 625		honkpage(w, r, u, nil, []*Honk{h}, "one honk maybe more")
 626		return
 627	}
 628	if friendorfoe(r.Header.Get("Accept")) {
 629		donksforhonks([]*Honk{h})
 630		if bloat_iscounter(h) {
 631			bloat_counterfixhonk(h)
 632		}
 633		_, j := jonkjonk(user, h)
 634		j["@context"] = itiswhatitis
 635		w.Header().Set("Content-Type", theonetruename)
 636		j.Write(w)
 637		return
 638	}
 639	honks := gethonksbyconvoy(-1, h.Convoy)
 640	honkpage(w, r, u, nil, honks, "one honk maybe more")
 641}
 642
 643func honkpage(w http.ResponseWriter, r *http.Request, u *login.UserInfo, user *WhatAbout,
 644	honks []*Honk, infomsg template.HTML) {
 645	templinfo := getInfo(r)
 646	var userid int64 = -1
 647	if u != nil {
 648		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
 649		userid = u.UserID
 650	}
 651	if u == nil {
 652		w.Header().Set("Cache-Control", "max-age=60")
 653	}
 654	reverbolate(userid, honks)
 655	if user != nil {
 656		filt := htfilter.New()
 657		templinfo["Name"] = user.Name
 658		whatabout := user.About
 659		whatabout = obfusbreak(user.About)
 660		templinfo["WhatAbout"], _ = filt.String(whatabout)
 661	}
 662	templinfo["Honks"] = honks
 663	templinfo["ServerMessage"] = infomsg
 664	err := readviews.Execute(w, "honkpage.html", templinfo)
 665	if err != nil {
 666		log.Print(err)
 667	}
 668}
 669
 670func saveuser(w http.ResponseWriter, r *http.Request) {
 671	whatabout := r.FormValue("whatabout")
 672	u := login.GetUserInfo(r)
 673	db := opendatabase()
 674	options := ""
 675	if r.FormValue("skinny") == "skinny" {
 676		options += " skinny "
 677	}
 678	_, err := db.Exec("update users set about = ?, options = ? where username = ?", whatabout, options, u.Username)
 679	if err != nil {
 680		log.Printf("error bouting what: %s", err)
 681	}
 682
 683	http.Redirect(w, r, "/account", http.StatusSeeOther)
 684}
 685
 686func gethonkers(userid int64) []*Honker {
 687	rows, err := stmtHonkers.Query(userid)
 688	if err != nil {
 689		log.Printf("error querying honkers: %s", err)
 690		return nil
 691	}
 692	defer rows.Close()
 693	var honkers []*Honker
 694	for rows.Next() {
 695		var f Honker
 696		var combos string
 697		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor, &combos)
 698		f.Combos = strings.Split(strings.TrimSpace(combos), " ")
 699		if err != nil {
 700			log.Printf("error scanning honker: %s", err)
 701			return nil
 702		}
 703		honkers = append(honkers, &f)
 704	}
 705	return honkers
 706}
 707
 708func getdubs(userid int64) []*Honker {
 709	rows, err := stmtDubbers.Query(userid)
 710	if err != nil {
 711		log.Printf("error querying dubs: %s", err)
 712		return nil
 713	}
 714	defer rows.Close()
 715	var honkers []*Honker
 716	for rows.Next() {
 717		var f Honker
 718		err = rows.Scan(&f.ID, &f.UserID, &f.Name, &f.XID, &f.Flavor)
 719		if err != nil {
 720			log.Printf("error scanning honker: %s", err)
 721			return nil
 722		}
 723		honkers = append(honkers, &f)
 724	}
 725	return honkers
 726}
 727
 728func allusers() []login.UserInfo {
 729	var users []login.UserInfo
 730	rows, _ := opendatabase().Query("select userid, username from users")
 731	defer rows.Close()
 732	for rows.Next() {
 733		var u login.UserInfo
 734		rows.Scan(&u.UserID, &u.Username)
 735		users = append(users, u)
 736	}
 737	return users
 738}
 739
 740func getxonk(userid int64, xid string) *Honk {
 741	h := new(Honk)
 742	var dt, aud string
 743	row := stmtOneXonk.QueryRow(userid, xid)
 744	err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID,
 745		&dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 746	if err != nil {
 747		if err != sql.ErrNoRows {
 748			log.Printf("error scanning xonk: %s", err)
 749		}
 750		return nil
 751	}
 752	h.Date, _ = time.Parse(dbtimeformat, dt)
 753	h.Audience = strings.Split(aud, " ")
 754	h.Public = !keepitquiet(h.Audience)
 755	return h
 756}
 757
 758func getpublichonks() []*Honk {
 759	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 760	rows, err := stmtPublicHonks.Query(dt)
 761	return getsomehonks(rows, err)
 762}
 763func gethonksbyuser(name string, includeprivate bool) []*Honk {
 764	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 765	whofore := 2
 766	if includeprivate {
 767		whofore = 3
 768	}
 769	rows, err := stmtUserHonks.Query(whofore, name, dt)
 770	return getsomehonks(rows, err)
 771}
 772func gethonksforuser(userid int64) []*Honk {
 773	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 774	rows, err := stmtHonksForUser.Query(userid, dt, userid, userid)
 775	return getsomehonks(rows, err)
 776}
 777func gethonksforme(userid int64) []*Honk {
 778	dt := time.Now().UTC().Add(-7 * 24 * time.Hour).Format(dbtimeformat)
 779	rows, err := stmtHonksForMe.Query(userid, dt, userid)
 780	return getsomehonks(rows, err)
 781}
 782func gethonksbyhonker(userid int64, honker string) []*Honk {
 783	rows, err := stmtHonksByHonker.Query(userid, honker, userid)
 784	return getsomehonks(rows, err)
 785}
 786func gethonksbyxonker(userid int64, xonker string) []*Honk {
 787	rows, err := stmtHonksByXonker.Query(userid, xonker, xonker, userid)
 788	return getsomehonks(rows, err)
 789}
 790func gethonksbycombo(userid int64, combo string) []*Honk {
 791	combo = "% " + combo + " %"
 792	rows, err := stmtHonksByCombo.Query(userid, combo, userid)
 793	return getsomehonks(rows, err)
 794}
 795func gethonksbyconvoy(userid int64, convoy string) []*Honk {
 796	rows, err := stmtHonksByConvoy.Query(userid, userid, convoy)
 797	honks := getsomehonks(rows, err)
 798	for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 {
 799		honks[i], honks[j] = honks[j], honks[i]
 800	}
 801	return honks
 802}
 803
 804func getsomehonks(rows *sql.Rows, err error) []*Honk {
 805	if err != nil {
 806		log.Printf("error querying honks: %s", err)
 807		return nil
 808	}
 809	defer rows.Close()
 810	var honks []*Honk
 811	for rows.Next() {
 812		var h Honk
 813		var dt, aud string
 814		err = rows.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker,
 815			&h.XID, &h.RID, &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Convoy, &h.Whofore)
 816		if err != nil {
 817			log.Printf("error scanning honks: %s", err)
 818			return nil
 819		}
 820		h.Date, _ = time.Parse(dbtimeformat, dt)
 821		h.Audience = strings.Split(aud, " ")
 822		h.Public = !keepitquiet(h.Audience)
 823		honks = append(honks, &h)
 824	}
 825	rows.Close()
 826	donksforhonks(honks)
 827	return honks
 828}
 829
 830func donksforhonks(honks []*Honk) {
 831	db := opendatabase()
 832	var ids []string
 833	hmap := make(map[int64]*Honk)
 834	for _, h := range honks {
 835		ids = append(ids, fmt.Sprintf("%d", h.ID))
 836		hmap[h.ID] = h
 837	}
 838	q := fmt.Sprintf("select honkid, donks.fileid, xid, name, url, media, local from donks join files on donks.fileid = files.fileid where honkid in (%s)", strings.Join(ids, ","))
 839	rows, err := db.Query(q)
 840	if err != nil {
 841		log.Printf("error querying donks: %s", err)
 842		return
 843	}
 844	defer rows.Close()
 845	for rows.Next() {
 846		var hid int64
 847		var d Donk
 848		err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.URL, &d.Media, &d.Local)
 849		if err != nil {
 850			log.Printf("error scanning donk: %s", err)
 851			continue
 852		}
 853		h := hmap[hid]
 854		h.Donks = append(h.Donks, &d)
 855	}
 856}
 857
 858func savebonk(w http.ResponseWriter, r *http.Request) {
 859	xid := r.FormValue("xid")
 860	userinfo := login.GetUserInfo(r)
 861	user, _ := butwhatabout(userinfo.Username)
 862
 863	log.Printf("bonking %s", xid)
 864
 865	xonk := getxonk(userinfo.UserID, xid)
 866	if xonk == nil {
 867		return
 868	}
 869	if !xonk.Public {
 870		return
 871	}
 872	donksforhonks([]*Honk{xonk})
 873
 874	oonker := xonk.Oonker
 875	if oonker == "" {
 876		oonker = xonk.Honker
 877	}
 878	dt := time.Now().UTC()
 879	bonk := Honk{
 880		UserID:   userinfo.UserID,
 881		Username: userinfo.Username,
 882		What:     "bonk",
 883		Honker:   user.URL,
 884		XID:      xonk.XID,
 885		Date:     dt,
 886		Donks:    xonk.Donks,
 887		Convoy:   xonk.Convoy,
 888		Audience: []string{oonker, thewholeworld},
 889		Public:   true,
 890	}
 891
 892	aud := strings.Join(bonk.Audience, " ")
 893	whofore := 2
 894	res, err := stmtSaveHonk.Exec(userinfo.UserID, "bonk", bonk.Honker, xid, "",
 895		dt.Format(dbtimeformat), "", aud, xonk.Noise, xonk.Convoy, whofore, "html",
 896		xonk.Precis, oonker)
 897	if err != nil {
 898		log.Printf("error saving bonk: %s", err)
 899		return
 900	}
 901	bonk.ID, _ = res.LastInsertId()
 902	for _, d := range bonk.Donks {
 903		_, err = stmtSaveDonk.Exec(bonk.ID, d.FileID)
 904		if err != nil {
 905			log.Printf("err saving donk: %s", err)
 906			return
 907		}
 908	}
 909
 910	go honkworldwide(user, &bonk)
 911}
 912
 913func zonkit(w http.ResponseWriter, r *http.Request) {
 914	wherefore := r.FormValue("wherefore")
 915	what := r.FormValue("what")
 916	switch wherefore {
 917	case "zonk":
 918	case "zonvoy":
 919	}
 920
 921	log.Printf("zonking %s %s", wherefore, what)
 922	userinfo := login.GetUserInfo(r)
 923	if wherefore == "zonk" {
 924		xonk := getxonk(userinfo.UserID, what)
 925		if xonk != nil {
 926			stmtZonkDonks.Exec(xonk.ID)
 927			stmtZonkIt.Exec(userinfo.UserID, what)
 928			if xonk.Whofore == 2 || xonk.Whofore == 3 {
 929				zonk := Honk{
 930					What:     "zonk",
 931					XID:      xonk.XID,
 932					Date:     time.Now().UTC(),
 933					Audience: oneofakind(xonk.Audience),
 934				}
 935				zonk.Public = !keepitquiet(zonk.Audience)
 936
 937				user, _ := butwhatabout(userinfo.Username)
 938				log.Printf("announcing deleted honk: %s", what)
 939				go honkworldwide(user, &zonk)
 940			}
 941		}
 942	}
 943	_, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore)
 944	if err != nil {
 945		log.Printf("error saving zonker: %s", err)
 946		return
 947	}
 948}
 949
 950func savehonk(w http.ResponseWriter, r *http.Request) {
 951	rid := r.FormValue("rid")
 952	noise := r.FormValue("noise")
 953
 954	userinfo := login.GetUserInfo(r)
 955	user, _ := butwhatabout(userinfo.Username)
 956
 957	dt := time.Now().UTC()
 958	xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate())
 959	what := "honk"
 960	if rid != "" {
 961		what = "tonk"
 962	}
 963	honk := Honk{
 964		UserID:   userinfo.UserID,
 965		Username: userinfo.Username,
 966		What:     "honk",
 967		Honker:   user.URL,
 968		XID:      xid,
 969		Date:     dt,
 970	}
 971	if strings.HasPrefix(noise, "DZ:") {
 972		idx := strings.Index(noise, "\n")
 973		if idx == -1 {
 974			honk.Precis = noise
 975			noise = ""
 976		} else {
 977			honk.Precis = noise[:idx]
 978			noise = noise[idx+1:]
 979		}
 980	}
 981	noise = hooterize(noise)
 982	noise = strings.TrimSpace(noise)
 983	honk.Precis = strings.TrimSpace(honk.Precis)
 984
 985	var convoy string
 986	if rid != "" {
 987		xonk := getxonk(userinfo.UserID, rid)
 988		if xonk != nil {
 989			if xonk.Public {
 990				honk.Audience = append(honk.Audience, xonk.Audience...)
 991			}
 992			convoy = xonk.Convoy
 993		} else {
 994			xonkaud, c := whosthere(rid)
 995			honk.Audience = append(honk.Audience, xonkaud...)
 996			convoy = c
 997		}
 998		for i, a := range honk.Audience {
 999			if a == thewholeworld {
1000				honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0]
1001				break
1002			}
1003		}
1004		honk.RID = rid
1005	} else {
1006		honk.Audience = []string{thewholeworld}
1007	}
1008	if noise != "" && noise[0] == '@' {
1009		honk.Audience = append(grapevine(noise), honk.Audience...)
1010	} else {
1011		honk.Audience = append(honk.Audience, grapevine(noise)...)
1012	}
1013	if convoy == "" {
1014		convoy = "data:,electrichonkytonk-" + xfiltrate()
1015	}
1016	butnottooloud(honk.Audience)
1017	honk.Audience = oneofakind(honk.Audience)
1018	if len(honk.Audience) == 0 {
1019		log.Printf("honk to nowhere")
1020		http.Error(w, "honk to nowhere...", http.StatusNotFound)
1021		return
1022	}
1023	honk.Public = !keepitquiet(honk.Audience)
1024	noise = obfusbreak(noise)
1025	honk.Noise = noise
1026	honk.Convoy = convoy
1027
1028	donkxid := r.FormValue("donkxid")
1029	if donkxid == "" {
1030		file, filehdr, err := r.FormFile("donk")
1031		if err == nil {
1032			var buf bytes.Buffer
1033			io.Copy(&buf, file)
1034			file.Close()
1035			data := buf.Bytes()
1036			xid := xfiltrate()
1037			var media, name string
1038			img, err := image.Vacuum(&buf, image.Params{MaxWidth: 2048, MaxHeight: 2048})
1039			if err == nil {
1040				data = img.Data
1041				format := img.Format
1042				media = "image/" + format
1043				if format == "jpeg" {
1044					format = "jpg"
1045				}
1046				name = xid + "." + format
1047				xid = name
1048			} else {
1049				maxsize := 100000
1050				if len(data) > maxsize {
1051					log.Printf("bad image: %s too much text: %d", err, len(data))
1052					http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1053					return
1054				}
1055				for i := 0; i < len(data); i++ {
1056					if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' {
1057						log.Printf("bad image: %s not text: %d", err, data[i])
1058						http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType)
1059						return
1060					}
1061				}
1062				media = "text/plain"
1063				name = filehdr.Filename
1064				if name == "" {
1065					name = xid + ".txt"
1066				}
1067				xid += ".txt"
1068			}
1069			url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1070			res, err := stmtSaveFile.Exec(xid, name, url, media, 1, data)
1071			if err != nil {
1072				log.Printf("unable to save image: %s", err)
1073				return
1074			}
1075			var d Donk
1076			d.FileID, _ = res.LastInsertId()
1077			d.XID = name
1078			d.Name = name
1079			d.Media = media
1080			d.URL = url
1081			d.Local = true
1082			honk.Donks = append(honk.Donks, &d)
1083			donkxid = d.XID
1084		}
1085	} else {
1086		xid := donkxid
1087		url := fmt.Sprintf("https://%s/d/%s", serverName, xid)
1088		var donk Donk
1089		row := stmtFindFile.QueryRow(url)
1090		err := row.Scan(&donk.FileID)
1091		if err == nil {
1092			donk.XID = xid
1093			donk.Local = true
1094			donk.URL = url
1095			honk.Donks = append(honk.Donks, &donk)
1096		} else {
1097			log.Printf("can't find file: %s", xid)
1098		}
1099	}
1100	herd := herdofemus(honk.Noise)
1101	for _, e := range herd {
1102		donk := savedonk(e.ID, e.Name, "image/png", true)
1103		if donk != nil {
1104			donk.Name = e.Name
1105			honk.Donks = append(honk.Donks, donk)
1106		}
1107	}
1108	memetize(&honk)
1109
1110	aud := strings.Join(honk.Audience, " ")
1111	whofore := 2
1112	if !honk.Public {
1113		whofore = 3
1114	}
1115	if r.FormValue("preview") == "preview" {
1116		honks := []*Honk{&honk}
1117		reverbolate(userinfo.UserID, honks)
1118		templinfo := getInfo(r)
1119		templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r)
1120		templinfo["Honks"] = honks
1121		templinfo["InReplyTo"] = r.FormValue("rid")
1122		templinfo["Noise"] = r.FormValue("noise")
1123		templinfo["SavedFile"] = donkxid
1124		templinfo["ServerMessage"] = "honk preview"
1125		err := readviews.Execute(w, "honkpage.html", templinfo)
1126		if err != nil {
1127			log.Print(err)
1128		}
1129		return
1130	}
1131	res, err := stmtSaveHonk.Exec(userinfo.UserID, what, honk.Honker, xid, rid,
1132		dt.Format(dbtimeformat), "", aud, honk.Noise, convoy, whofore, "html", honk.Precis, honk.Oonker)
1133	if err != nil {
1134		log.Printf("error saving honk: %s", err)
1135		http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1136		return
1137	}
1138	honk.ID, _ = res.LastInsertId()
1139	for _, d := range honk.Donks {
1140		_, err = stmtSaveDonk.Exec(honk.ID, d.FileID)
1141		if err != nil {
1142			log.Printf("err saving donk: %s", err)
1143			http.Error(w, "something bad happened while saving", http.StatusInternalServerError)
1144			return
1145		}
1146	}
1147
1148	if bloat_iscounter(&honk) {
1149		go bloat_counterannounce(user, &honk)
1150	} else {
1151		go honkworldwide(user, &honk)
1152	}
1153
1154	http.Redirect(w, r, xid, http.StatusSeeOther)
1155}
1156
1157func showhonkers(w http.ResponseWriter, r *http.Request) {
1158	userinfo := login.GetUserInfo(r)
1159	templinfo := getInfo(r)
1160	templinfo["Honkers"] = gethonkers(userinfo.UserID)
1161	templinfo["HonkerCSRF"] = login.GetCSRF("savehonker", r)
1162	err := readviews.Execute(w, "honkers.html", templinfo)
1163	if err != nil {
1164		log.Print(err)
1165	}
1166}
1167
1168func showcombos(w http.ResponseWriter, r *http.Request) {
1169	userinfo := login.GetUserInfo(r)
1170	templinfo := getInfo(r)
1171	honkers := gethonkers(userinfo.UserID)
1172	var combos []string
1173	for _, h := range honkers {
1174		combos = append(combos, h.Combos...)
1175	}
1176	for i, c := range combos {
1177		if c == "-" {
1178			combos[i] = ""
1179		}
1180	}
1181	combos = oneofakind(combos)
1182	sort.Strings(combos)
1183	templinfo["Combos"] = combos
1184	err := readviews.Execute(w, "combos.html", templinfo)
1185	if err != nil {
1186		log.Print(err)
1187	}
1188}
1189
1190func savehonker(w http.ResponseWriter, r *http.Request) {
1191	u := login.GetUserInfo(r)
1192	name := r.FormValue("name")
1193	url := r.FormValue("url")
1194	peep := r.FormValue("peep")
1195	combos := r.FormValue("combos")
1196	honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0)
1197
1198	if honkerid > 0 {
1199		goodbye := r.FormValue("goodbye")
1200		if goodbye == "F" {
1201			db := opendatabase()
1202			row := db.QueryRow("select xid from honkers where honkerid = ? and userid = ?",
1203				honkerid, u.UserID)
1204			var xid string
1205			err := row.Scan(&xid)
1206			if err != nil {
1207				log.Printf("can't get honker xid: %s", err)
1208				return
1209			}
1210			log.Printf("unsubscribing from %s", xid)
1211			user, _ := butwhatabout(u.Username)
1212			go itakeitallback(user, xid)
1213			_, err = stmtUpdateFlavor.Exec("unsub", u.UserID, xid, "sub")
1214			if err != nil {
1215				log.Printf("error updating honker: %s", err)
1216				return
1217			}
1218
1219			http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1220			return
1221		}
1222		combos = " " + strings.TrimSpace(combos) + " "
1223		_, err := stmtUpdateCombos.Exec(combos, honkerid, u.UserID)
1224		if err != nil {
1225			log.Printf("update honker err: %s", err)
1226			return
1227		}
1228		http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1229	}
1230
1231	flavor := "presub"
1232	if peep == "peep" {
1233		flavor = "peep"
1234	}
1235	url = investigate(url)
1236	if url == "" {
1237		return
1238	}
1239	_, err := stmtSaveHonker.Exec(u.UserID, name, url, flavor, combos)
1240	if err != nil {
1241		log.Print(err)
1242		return
1243	}
1244	if flavor == "presub" {
1245		user, _ := butwhatabout(u.Username)
1246		go subsub(user, url)
1247	}
1248	http.Redirect(w, r, "/honkers", http.StatusSeeOther)
1249}
1250
1251type Zonker struct {
1252	ID        int64
1253	Name      string
1254	Wherefore string
1255}
1256
1257func zonkzone(w http.ResponseWriter, r *http.Request) {
1258	userinfo := login.GetUserInfo(r)
1259	rows, err := stmtGetZonkers.Query(userinfo.UserID)
1260	if err != nil {
1261		log.Printf("err: %s", err)
1262		return
1263	}
1264	defer rows.Close()
1265	var zonkers []Zonker
1266	for rows.Next() {
1267		var z Zonker
1268		rows.Scan(&z.ID, &z.Name, &z.Wherefore)
1269		zonkers = append(zonkers, z)
1270	}
1271	sort.Slice(zonkers, func(i, j int) bool {
1272		w1 := zonkers[i].Wherefore
1273		w2 := zonkers[j].Wherefore
1274		if w1 == w2 {
1275			return zonkers[i].Name < zonkers[j].Name
1276		}
1277		if w1 == "zonvoy" {
1278			w1 = "zzzzzzz"
1279		}
1280		if w2 == "zonvoy" {
1281			w2 = "zzzzzzz"
1282		}
1283		return w1 < w2
1284	})
1285
1286	templinfo := getInfo(r)
1287	templinfo["Zonkers"] = zonkers
1288	templinfo["ZonkCSRF"] = login.GetCSRF("zonkzonk", r)
1289	err = readviews.Execute(w, "zonkers.html", templinfo)
1290	if err != nil {
1291		log.Print(err)
1292	}
1293}
1294
1295func zonkzonk(w http.ResponseWriter, r *http.Request) {
1296	userinfo := login.GetUserInfo(r)
1297	itsok := r.FormValue("itsok")
1298	if itsok == "iforgiveyou" {
1299		zonkerid, _ := strconv.ParseInt(r.FormValue("zonkerid"), 10, 0)
1300		db := opendatabase()
1301		db.Exec("delete from zonkers where userid = ? and zonkerid = ?",
1302			userinfo.UserID, zonkerid)
1303		bitethethumbs()
1304		http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1305		return
1306	}
1307	wherefore := r.FormValue("wherefore")
1308	name := r.FormValue("name")
1309	if name == "" {
1310		return
1311	}
1312	switch wherefore {
1313	case "zonker":
1314	case "zomain":
1315	case "zonvoy":
1316	case "zord":
1317	case "zilence":
1318	default:
1319		return
1320	}
1321	db := opendatabase()
1322	db.Exec("insert into zonkers (userid, name, wherefore) values (?, ?, ?)",
1323		userinfo.UserID, name, wherefore)
1324	if wherefore == "zonker" || wherefore == "zomain" || wherefore == "zord" || wherefore == "zilence" {
1325		bitethethumbs()
1326	}
1327
1328	http.Redirect(w, r, "/zonkzone", http.StatusSeeOther)
1329}
1330
1331func accountpage(w http.ResponseWriter, r *http.Request) {
1332	u := login.GetUserInfo(r)
1333	user, _ := butwhatabout(u.Username)
1334	templinfo := getInfo(r)
1335	templinfo["UserCSRF"] = login.GetCSRF("saveuser", r)
1336	templinfo["LogoutCSRF"] = login.GetCSRF("logout", r)
1337	templinfo["User"] = user
1338	err := readviews.Execute(w, "account.html", templinfo)
1339	if err != nil {
1340		log.Print(err)
1341	}
1342}
1343
1344func dochpass(w http.ResponseWriter, r *http.Request) {
1345	err := login.ChangePassword(w, r)
1346	if err != nil {
1347		log.Printf("error changing password: %s", err)
1348	}
1349	http.Redirect(w, r, "/account", http.StatusSeeOther)
1350}
1351
1352func fingerlicker(w http.ResponseWriter, r *http.Request) {
1353	orig := r.FormValue("resource")
1354
1355	log.Printf("finger lick: %s", orig)
1356
1357	if strings.HasPrefix(orig, "acct:") {
1358		orig = orig[5:]
1359	}
1360
1361	name := orig
1362	idx := strings.LastIndexByte(name, '/')
1363	if idx != -1 {
1364		name = name[idx+1:]
1365		if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig {
1366			log.Printf("foreign request rejected")
1367			name = ""
1368		}
1369	} else {
1370		idx = strings.IndexByte(name, '@')
1371		if idx != -1 {
1372			name = name[:idx]
1373			if name+"@"+serverName != orig {
1374				log.Printf("foreign request rejected")
1375				name = ""
1376			}
1377		}
1378	}
1379	user, err := butwhatabout(name)
1380	if err != nil {
1381		http.NotFound(w, r)
1382		return
1383	}
1384
1385	j := junk.New()
1386	j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, serverName)
1387	j["aliases"] = []string{user.URL}
1388	var links []junk.Junk
1389	l := junk.New()
1390	l["rel"] = "self"
1391	l["type"] = `application/activity+json`
1392	l["href"] = user.URL
1393	links = append(links, l)
1394	j["links"] = links
1395
1396	w.Header().Set("Cache-Control", "max-age=3600")
1397	w.Header().Set("Content-Type", "application/jrd+json")
1398	j.Write(w)
1399}
1400
1401func somedays() string {
1402	secs := 432000 + notrand.Int63n(432000)
1403	return fmt.Sprintf("%d", secs)
1404}
1405
1406func avatate(w http.ResponseWriter, r *http.Request) {
1407	n := r.FormValue("a")
1408	a := avatar(n)
1409	w.Header().Set("Cache-Control", "max-age="+somedays())
1410	w.Write(a)
1411}
1412
1413func servecss(w http.ResponseWriter, r *http.Request) {
1414	w.Header().Set("Cache-Control", "max-age=7776000")
1415	http.ServeFile(w, r, "views"+r.URL.Path)
1416}
1417func servehtml(w http.ResponseWriter, r *http.Request) {
1418	templinfo := getInfo(r)
1419	err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo)
1420	if err != nil {
1421		log.Print(err)
1422	}
1423}
1424func serveemu(w http.ResponseWriter, r *http.Request) {
1425	xid := mux.Vars(r)["xid"]
1426	w.Header().Set("Cache-Control", "max-age="+somedays())
1427	http.ServeFile(w, r, "emus/"+xid)
1428}
1429func servememe(w http.ResponseWriter, r *http.Request) {
1430	xid := mux.Vars(r)["xid"]
1431	w.Header().Set("Cache-Control", "max-age="+somedays())
1432	http.ServeFile(w, r, "memes/"+xid)
1433}
1434
1435func servefile(w http.ResponseWriter, r *http.Request) {
1436	xid := mux.Vars(r)["xid"]
1437	row := stmtFileData.QueryRow(xid)
1438	var media string
1439	var data []byte
1440	err := row.Scan(&media, &data)
1441	if err != nil {
1442		log.Printf("error loading file: %s", err)
1443		http.NotFound(w, r)
1444		return
1445	}
1446	w.Header().Set("Content-Type", media)
1447	w.Header().Set("X-Content-Type-Options", "nosniff")
1448	w.Header().Set("Cache-Control", "max-age="+somedays())
1449	w.Write(data)
1450}
1451
1452func nomoroboto(w http.ResponseWriter, r *http.Request) {
1453	io.WriteString(w, "User-agent: *\n")
1454	io.WriteString(w, "Disallow: /a\n")
1455	io.WriteString(w, "Disallow: /d\n")
1456	io.WriteString(w, "Disallow: /meme\n")
1457	for _, u := range allusers() {
1458		fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep)
1459	}
1460}
1461
1462func serve() {
1463	db := opendatabase()
1464	login.Init(db)
1465
1466	listener, err := openListener()
1467	if err != nil {
1468		log.Fatal(err)
1469	}
1470	go redeliverator()
1471
1472	debug := false
1473	getconfig("debug", &debug)
1474	readviews = templates.Load(debug,
1475		"views/honkpage.html",
1476		"views/honkers.html",
1477		"views/zonkers.html",
1478		"views/combos.html",
1479		"views/honkform.html",
1480		"views/honk.html",
1481		"views/account.html",
1482		"views/about.html",
1483		"views/funzone.html",
1484		"views/login.html",
1485		"views/xzone.html",
1486		"views/header.html",
1487	)
1488	if !debug {
1489		s := "views/style.css"
1490		savedstyleparams[s] = getstyleparam(s)
1491		s = "views/local.css"
1492		savedstyleparams[s] = getstyleparam(s)
1493	}
1494
1495	bitethethumbs()
1496
1497	mux := mux.NewRouter()
1498	mux.Use(login.Checker)
1499
1500	posters := mux.Methods("POST").Subrouter()
1501	getters := mux.Methods("GET").Subrouter()
1502
1503	getters.HandleFunc("/", homepage)
1504	getters.HandleFunc("/front", homepage)
1505	getters.HandleFunc("/robots.txt", nomoroboto)
1506	getters.HandleFunc("/rss", showrss)
1507	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}", showuser)
1508	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/"+honkSep+"/{xid:[[:alnum:]]+}", showhonk)
1509	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/rss", showrss)
1510	posters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/inbox", inbox)
1511	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/outbox", outbox)
1512	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/followers", emptiness)
1513	getters.HandleFunc("/"+userSep+"/{name:[[:alnum:]]+}/following", emptiness)
1514	getters.HandleFunc("/a", avatate)
1515	getters.HandleFunc("/d/{xid:[[:alnum:].]+}", servefile)
1516	getters.HandleFunc("/emu/{xid:[[:alnum:]_.-]+}", serveemu)
1517	getters.HandleFunc("/meme/{xid:[[:alnum:]_.-]+}", servememe)
1518	getters.HandleFunc("/.well-known/webfinger", fingerlicker)
1519
1520	getters.HandleFunc("/style.css", servecss)
1521	getters.HandleFunc("/local.css", servecss)
1522	getters.HandleFunc("/about", servehtml)
1523	getters.HandleFunc("/login", servehtml)
1524	posters.HandleFunc("/dologin", login.LoginFunc)
1525	getters.HandleFunc("/logout", login.LogoutFunc)
1526
1527	loggedin := mux.NewRoute().Subrouter()
1528	loggedin.Use(login.Required)
1529	loggedin.HandleFunc("/account", accountpage)
1530	loggedin.HandleFunc("/funzone", showfunzone)
1531	loggedin.HandleFunc("/chpass", dochpass)
1532	loggedin.HandleFunc("/atme", homepage)
1533	loggedin.HandleFunc("/zonkzone", zonkzone)
1534	loggedin.HandleFunc("/xzone", xzone)
1535	loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(savehonk)))
1536	loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(savebonk)))
1537	loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit)))
1538	loggedin.Handle("/zonkzonk", login.CSRFWrap("zonkzonk", http.HandlerFunc(zonkzonk)))
1539	loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser)))
1540	loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport)))
1541	loggedin.HandleFunc("/honkers", showhonkers)
1542	loggedin.HandleFunc("/h/{name:[[:alnum:]]+}", showhonker)
1543	loggedin.HandleFunc("/h", showhonker)
1544	loggedin.HandleFunc("/c/{name:[[:alnum:]]+}", showcombo)
1545	loggedin.HandleFunc("/c", showcombos)
1546	loggedin.HandleFunc("/t", showconvoy)
1547	loggedin.Handle("/savehonker", login.CSRFWrap("savehonker", http.HandlerFunc(savehonker)))
1548
1549	err = http.Serve(listener, mux)
1550	if err != nil {
1551		log.Fatal(err)
1552	}
1553}
1554
1555func cleanupdb(arg string) {
1556	db := opendatabase()
1557	days, err := strconv.Atoi(arg)
1558	if err != nil {
1559		honker := arg
1560		expdate := time.Now().UTC().Add(-3 * 24 * time.Hour).Format(dbtimeformat)
1561		doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and honker = ?)", expdate, honker)
1562		doordie(db, "delete from honks where dt < ? and whofore = 0 and honker = ?", expdate, honker)
1563	} else {
1564		expdate := time.Now().UTC().Add(-time.Duration(days) * 24 * time.Hour).Format(dbtimeformat)
1565		doordie(db, "delete from donks where honkid in (select honkid from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3))", expdate)
1566		doordie(db, "delete from honks where dt < ? and whofore = 0 and convoy not in (select convoy from honks where whofore = 2 or whofore = 3)", expdate)
1567	}
1568	doordie(db, "delete from files where fileid not in (select fileid from donks)")
1569	for _, u := range allusers() {
1570		doordie(db, "delete from zonkers where userid = ? and wherefore = 'zonvoy' and zonkerid < (select zonkerid from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 1 offset 200)", u.UserID, u.UserID)
1571	}
1572}
1573
1574var stmtHonkers, stmtDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateCombos *sql.Stmt
1575var stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt
1576var stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt
1577var stmtHonksByHonker, stmtSaveHonk, stmtFileData, stmtWhatAbout *sql.Stmt
1578var stmtFindZonk, stmtFindXonk, stmtSaveDonk, stmtFindFile, stmtSaveFile *sql.Stmt
1579var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover *sql.Stmt
1580var stmtHasHonker, stmtThumbBiters, stmtZonkIt, stmtZonkDonks, stmtSaveZonker *sql.Stmt
1581var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker *sql.Stmt
1582
1583func preparetodie(db *sql.DB, s string) *sql.Stmt {
1584	stmt, err := db.Prepare(s)
1585	if err != nil {
1586		log.Fatalf("error %s: %s", err, s)
1587	}
1588	return stmt
1589}
1590
1591func prepareStatements(db *sql.DB) {
1592	stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos from honkers where userid = ? and (flavor = 'sub' or flavor = 'peep' or flavor = 'unsub') order by name")
1593	stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos) values (?, ?, ?, ?, ?)")
1594	stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ? where userid = ? and xid = ? and flavor = ?")
1595	stmtUpdateCombos = preparetodie(db, "update honkers set combos = ? where honkerid = ? and userid = ?")
1596	stmtHasHonker = preparetodie(db, "select honkerid from honkers where xid = ? and userid = ?")
1597	stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'")
1598
1599	selecthonks := "select honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, convoy, whofore from honks join users on honks.userid = users.userid "
1600	limit := " order by honkid desc limit 250"
1601	butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)"
1602	stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?")
1603	stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+limit)
1604	stmtUserHonks = preparetodie(db, selecthonks+"where (whofore = 2 or whofore = ?) and username = ? and dt > ?"+limit)
1605	stmtHonksForUser = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and honker in (select xid from honkers where userid = ? and flavor = 'sub' and combos not like '% - %')"+butnotthose+limit)
1606	stmtHonksForMe = preparetodie(db, selecthonks+"where honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit)
1607	stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.name = ?"+butnotthose+limit)
1608	stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit)
1609	stmtHonksByCombo = preparetodie(db, selecthonks+"join honkers on honkers.xid = honks.honker where honks.userid = ? and honkers.combos like ?"+butnotthose+limit)
1610	stmtHonksByConvoy = preparetodie(db, selecthonks+"where (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit)
1611
1612	stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
1613	stmtFileData = preparetodie(db, "select media, content from files where xid = ?")
1614	stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
1615	stmtSaveDonk = preparetodie(db, "insert into donks (honkid, fileid) values (?, ?)")
1616	stmtZonkIt = preparetodie(db, "delete from honks where userid = ? and xid = ?")
1617	stmtZonkDonks = preparetodie(db, "delete from donks where honkid = ?")
1618	stmtFindFile = preparetodie(db, "select fileid from files where url = ? and local = 1")
1619	stmtSaveFile = preparetodie(db, "insert into files (xid, name, url, media, local, content) values (?, ?, ?, ?, ?, ?)")
1620	stmtWhatAbout = preparetodie(db, "select userid, username, displayname, about, pubkey, options from users where username = ?")
1621	stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor) values (?, ?, ?, ?)")
1622	stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, username, rcpt, msg) values (?, ?, ?, ?, ?)")
1623	stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers")
1624	stmtLoadDoover = preparetodie(db, "select tries, username, rcpt, msg from doovers where dooverid = ?")
1625	stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?")
1626	stmtThumbBiters = preparetodie(db, "select userid, name, wherefore from zonkers where (wherefore = 'zonker' or wherefore = 'zomain' or wherefore = 'zord' or wherefore = 'zilence')")
1627	stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'")
1628	stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'")
1629	stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)")
1630	stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?")
1631	stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor) values (?, ?, ?)")
1632	stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ?")
1633	stmtRecentHonkers = preparetodie(db, "select distinct(honker) from honks where userid = ? and honker not in (select xid from honkers where userid = ? and flavor = 'sub') order by honkid desc limit 100")
1634}
1635
1636func ElaborateUnitTests() {
1637}
1638
1639func main() {
1640	cmd := "run"
1641	if len(os.Args) > 1 {
1642		cmd = os.Args[1]
1643	}
1644	switch cmd {
1645	case "init":
1646		initdb()
1647	case "upgrade":
1648		upgradedb()
1649	}
1650	db := opendatabase()
1651	dbversion := 0
1652	getconfig("dbversion", &dbversion)
1653	if dbversion != myVersion {
1654		log.Fatal("incorrect database version. run upgrade.")
1655	}
1656	getconfig("servermsg", &serverMsg)
1657	getconfig("servername", &serverName)
1658	getconfig("usersep", &userSep)
1659	getconfig("honksep", &honkSep)
1660	getconfig("dnf", &donotfedafterdark)
1661	prepareStatements(db)
1662	switch cmd {
1663	case "adduser":
1664		adduser()
1665	case "cleanup":
1666		arg := "30"
1667		if len(os.Args) > 2 {
1668			arg = os.Args[2]
1669		}
1670		cleanupdb(arg)
1671	case "ping":
1672		if len(os.Args) < 4 {
1673			fmt.Printf("usage: honk ping from to\n")
1674			return
1675		}
1676		name := os.Args[2]
1677		targ := os.Args[3]
1678		user, err := butwhatabout(name)
1679		if err != nil {
1680			log.Printf("unknown user")
1681			return
1682		}
1683		ping(user, targ)
1684	case "peep":
1685		peeppeep()
1686	case "run":
1687		serve()
1688	case "test":
1689		ElaborateUnitTests()
1690	default:
1691		log.Fatal("unknown command")
1692	}
1693}