all repos — honk @ 45069601a4bb8c966a5b04774562fb62b79b518c

my fork of honk

import
Anirudh Oppiliappan x@icyphox.sh
Tue, 08 Nov 2022 11:22:47 +0530
commit

45069601a4bb8c966a5b04774562fb62b79b518c

A .gitignore

@@ -0,0 +1,7 @@

+*.db +*db-* +*.out +.hg +memes +emus +honk
A .hgignore

@@ -0,0 +1,4 @@

+.*\.db +memes +emus +honk
A .hgtags

@@ -0,0 +1,38 @@

+473c3dd6df6f1294362d9042ae5583a6e30d48c9 v0.1.0 +14d4623234ca623e9ae35a64d3f87c7d50e2b42f v0.1.1 +96d2e1dc6664d71ba67d896c32d8b50094305e0f v0.1.2 +a50adf10726dac300afeba9cfacb6db185157311 v0.2.0 +2c62e21731b375f81c57f00c709244ae8fc65d76 v0.2.1 +eba8ccf45b80e6f9523c55e55d937f10f9262cb1 v0.2.2 +4a5816b79e8787dea21c0addcd8ba62a4eaec318 v0.2.3 +7b38c7500ce067001c14b844eaf99f85887324da v0.2.4 +9e95200c9763966f3c8d13fd93771e3a68ac77aa v0.3.0 +12e113bb60481f2429ce053286e2e512520bd4ef v0.4.0 +e57c6026b9033af5649b31cce06b566206809367 v0.5.0 +57603c1049f68945c40e3c8e5c4486e367daade4 v0.6.0 +907c626de523fa2b055306a9385dfd569cf03f56 v0.7.0 +0baeee9aed87624499931ac5cf44f2ebdfd2ceb0 v0.7.1 +b1ec4c9c189d13d8c6dba0ab7e75e4533abebff2 v0.7.2 +efaae027527fad86cb584995305ee8dd2456a5d8 v0.7.3 +4f0ac04432d5cc18eaab139e8d3408219ca16ead v0.7.4 +65ea769c65bb0a5a9ae204537b5c1bff6261dec4 v0.7.5 +0f150eed70d5f92e69249f427b5bb921d8f0703e v0.7.6 +b2278292cce136cd9f5b7cd2ba7be04f887ab700 v0.7.7 +cc2ec3c8bf656f337919efddffd43634f1d9144c v0.8.0 +8e85621f4e62da9d6caf946ce65be90e6f49434e v0.8.1 +b140f7a3216b820aa13f982e45ff42781d7a8f4a v0.8.2 +b140f7a3216b820aa13f982e45ff42781d7a8f4a v0.8.2 +8a2a90379bf60d425fec114ff88f5fd9806a4965 v0.8.2 +808ef90260d5d81db1ec98fb8894588a3ac7b369 v0.8.3 +3ada67b721e7e4a478d0effacde14f36dc16e1de v0.8.4 +2e9969df06ddab8fa07999e91437dda28ec058ae v0.8.5 +3e41549dbc90f1d2ad246e4af72db9343021bc98 v0.8.6 +8e5c85fbcf02e61c9a2d12415f83d16f8afadba4 v0.9.0 +5218aee560879398288e009d8a426749bd172f40 v0.9.1 +a2f6f7bdfb6ea8cac68acdb952b2eed8a585749d v0.9.2 +dac64bc6a93cedeb6ae618cba8f8647af96d2ece v0.9.3 +28b92eaba37140a8a84a871d3f007b46fe66acb7 v0.9.4 +3ece33fb77800c027ecfd3b5100881732d68c9bb v0.9.5 +6a522536238fe25b6d048543f52ed406ccf720b2 v0.9.6 +bc1bcfb9c0cc86b3c63325b07e13a36b9d4500f0 v0.9.7 +916cefdc24363b6e7e193dbde82632c17f58adfd v0.9.8
A LICENSE

@@ -0,0 +1,24 @@

+The license for honk and components is generally ISC or compatible. + +Individual source files are licensed per license at the top. + +Distributed components and dependenices in the vendor directory should have +compatible licenses. + +Files without explicit licenses and the conglomeration as a whole is subject +to the license below. + +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +
A Makefile

@@ -0,0 +1,14 @@

+ +all: honk + +honk: .preflightcheck schema.sql *.go go.mod + go build -mod=`ls -d vendor 2> /dev/null` -o honk + +.preflightcheck: preflight.sh + @sh ./preflight.sh + +clean: + rm -f honk + +test: + go test
A README

@@ -0,0 +1,72 @@

+honk + +-- features + +Take control of your honks and join the federation. +An ActivityPub server with minimal setup and support costs. +Spend more time using the software and less time operating it. + +No attention mining. +No likes, no faves, no polls, no stars, no claps, no counts. + +Purple color scheme. Custom emus. Memes too. +Avatars automatically assigned by the NSA. + +The button to submit a new honk says "it's gonna be honked". + +The honk mission is to work well if it's what you want. +This does not imply the goal is to be what you want. + +-- build + +It should be sufficient to type make after unpacking a release. +You'll need a go compiler version 1.16 or later. And libsqlite3. + +Even on a fast machine, building from source can take several seconds. + +Development sources: hg clone https://humungus.tedunangst.com/r/honk + +-- setup + +honk expects to be fronted by a TLS terminating reverse proxy. + +First, create the database. This will ask four questions. +./honk init +username: (the username you want) +password: (the password you want) +listenaddr: (tcp or unix: 127.0.0.1:31337, /var/www/honk.sock, etc.) +servername: (public DNS name: honk.example.com) + +Then run honk. +./honk + +-- upgrade + +old-honk backup `date +backup-%F` +./honk upgrade +./honk + +-- documentation + +There is a more complete incomplete manual. This is just the README. + +-- guidelines + +One honk per day, or call it an "eighth-tenth" honk. +If your honk frequency changes, so will the number of honks. + +The honk should be short, but not so short that you cannot identify it. + +The honk is an animal sign of respect and should be accompanied by a +friendly greeting or a nod. + +The honk should be done from a seat and in a safe area. + +It is considered rude to make noise in a place of business. + +The honk may be made on public property only when the person doing +the honk has the permission of the owner of that property. + +-- disclaimer + +Do not use honk to contact emergency services.
A activity.go

@@ -0,0 +1,1935 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "bytes" + "context" + "crypto/tls" + "database/sql" + "fmt" + "html" + "io" + notrand "math/rand" + "net/http" + "net/url" + "os" + "strings" + "time" + + "humungus.tedunangst.com/r/webs/cache" + "humungus.tedunangst.com/r/webs/gate" + "humungus.tedunangst.com/r/webs/httpsig" + "humungus.tedunangst.com/r/webs/junk" + "humungus.tedunangst.com/r/webs/templates" +) + +var theonetruename = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` +var thefakename = `application/activity+json` +var falsenames = []string{ + `application/ld+json`, + `application/activity+json`, +} +var itiswhatitis = "https://www.w3.org/ns/activitystreams" +var thewholeworld = "https://www.w3.org/ns/activitystreams#Public" + +var fastTimeout time.Duration = 5 +var slowTimeout time.Duration = 30 + +func friendorfoe(ct string) bool { + ct = strings.ToLower(ct) + for _, at := range falsenames { + if strings.HasPrefix(ct, at) { + return true + } + } + return false +} + +var develClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, +} + +func PostJunk(keyname string, key httpsig.PrivateKey, url string, j junk.Junk) error { + return PostMsg(keyname, key, url, j.ToBytes()) +} + +func PostMsg(keyname string, key httpsig.PrivateKey, url string, msg []byte) error { + client := http.DefaultClient + if develMode { + client = develClient + } + req, err := http.NewRequest("POST", url, bytes.NewReader(msg)) + if err != nil { + return err + } + req.Header.Set("User-Agent", "honksnonk/5.0; "+serverName) + req.Header.Set("Content-Type", theonetruename) + httpsig.SignRequest(keyname, key, req, msg) + ctx, cancel := context.WithTimeout(context.Background(), 2*slowTimeout*time.Second) + defer cancel() + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return err + } + resp.Body.Close() + switch resp.StatusCode { + case 200: + case 201: + case 202: + default: + return fmt.Errorf("http post status: %d", resp.StatusCode) + } + ilog.Printf("successful post: %s %d", url, resp.StatusCode) + return nil +} + +func GetJunk(userid int64, url string) (junk.Junk, error) { + return GetJunkTimeout(userid, url, slowTimeout*time.Second) +} + +func GetJunkFast(userid int64, url string) (junk.Junk, error) { + return GetJunkTimeout(userid, url, fastTimeout*time.Second) +} + +func GetJunkHardMode(userid int64, url string) (junk.Junk, error) { + j, err := GetJunk(userid, url) + if err != nil { + emsg := err.Error() + if emsg == "http get status: 502" || strings.Contains(emsg, "timeout") { + ilog.Printf("trying again after error: %s", emsg) + time.Sleep(time.Duration(60+notrand.Int63n(60)) * time.Second) + j, err = GetJunk(userid, url) + if err != nil { + ilog.Printf("still couldn't get it") + } else { + ilog.Printf("retry success!") + } + } + } + return j, err +} + +var flightdeck = gate.NewSerializer() + +var signGets = true + +func junkGet(userid int64, url string, args junk.GetArgs) (junk.Junk, error) { + client := http.DefaultClient + if args.Client != nil { + client = args.Client + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + if args.Accept != "" { + req.Header.Set("Accept", args.Accept) + } + if args.Agent != "" { + req.Header.Set("User-Agent", args.Agent) + } + if signGets { + var ki *KeyInfo + ok := ziggies.Get(userid, &ki) + if ok { + httpsig.SignRequest(ki.keyname, ki.seckey, req, nil) + } + } + if args.Timeout != 0 { + ctx, cancel := context.WithTimeout(context.Background(), args.Timeout) + defer cancel() + req = req.WithContext(ctx) + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("http get status: %d", resp.StatusCode) + } + return junk.Read(resp.Body) +} + +func GetJunkTimeout(userid int64, url string, timeout time.Duration) (junk.Junk, error) { + client := http.DefaultClient + if develMode { + client = develClient + } + fn := func() (interface{}, error) { + at := thefakename + if strings.Contains(url, ".well-known/webfinger?resource") { + at = "application/jrd+json" + } + j, err := junkGet(userid, url, junk.GetArgs{ + Accept: at, + Agent: "honksnonk/5.0; " + serverName, + Timeout: timeout, + Client: client, + }) + return j, err + } + + ji, err := flightdeck.Call(url, fn) + if err != nil { + return nil, err + } + j := ji.(junk.Junk) + return j, nil +} + +func fetchsome(url string) ([]byte, error) { + client := http.DefaultClient + if develMode { + client = develClient + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + ilog.Printf("error fetching %s: %s", url, err) + return nil, err + } + req.Header.Set("User-Agent", "honksnonk/5.0; "+serverName) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + ilog.Printf("error fetching %s: %s", url, err) + return nil, err + } + defer resp.Body.Close() + switch resp.StatusCode { + case 200: + case 201: + case 202: + default: + return nil, fmt.Errorf("http get not 200: %d %s", resp.StatusCode, url) + } + var buf bytes.Buffer + limiter := io.LimitReader(resp.Body, 10*1024*1024) + io.Copy(&buf, limiter) + return buf.Bytes(), nil +} + +func savedonk(url string, name, desc, media string, localize bool) *Donk { + if url == "" { + return nil + } + if donk := finddonk(url); donk != nil { + return donk + } + ilog.Printf("saving donk: %s", url) + data := []byte{} + if localize { + fn := func() (interface{}, error) { + return fetchsome(url) + } + ii, err := flightdeck.Call(url, fn) + if err != nil { + ilog.Printf("error fetching donk: %s", err) + localize = false + goto saveit + } + data = ii.([]byte) + + if len(data) == 10*1024*1024 { + ilog.Printf("truncation likely") + } + if strings.HasPrefix(media, "image") { + img, err := shrinkit(data) + if err != nil { + ilog.Printf("unable to decode image: %s", err) + localize = false + data = []byte{} + goto saveit + } + data = img.Data + media = "image/" + img.Format + } else if media == "application/pdf" { + if len(data) > 1000000 { + ilog.Printf("not saving large pdf") + localize = false + data = []byte{} + } + } else if len(data) > 100000 { + ilog.Printf("not saving large attachment") + localize = false + data = []byte{} + } + } +saveit: + fileid, err := savefile(name, desc, url, media, localize, data) + if err != nil { + elog.Printf("error saving file %s: %s", url, err) + return nil + } + donk := new(Donk) + donk.FileID = fileid + return donk +} + +func iszonked(userid int64, xid string) bool { + var id int64 + row := stmtFindZonk.QueryRow(userid, xid) + err := row.Scan(&id) + if err == nil { + return true + } + if err != sql.ErrNoRows { + ilog.Printf("error querying zonk: %s", err) + } + return false +} + +func needxonk(user *WhatAbout, x *Honk) bool { + if rejectxonk(x) { + return false + } + return needxonkid(user, x.XID) +} +func needbonkid(user *WhatAbout, xid string) bool { + return needxonkidX(user, xid, true) +} +func needxonkid(user *WhatAbout, xid string) bool { + return needxonkidX(user, xid, false) +} +func needxonkidX(user *WhatAbout, xid string, isannounce bool) bool { + if !strings.HasPrefix(xid, "https://") { + return false + } + if strings.HasPrefix(xid, user.URL+"/") { + return false + } + if rejectorigin(user.ID, xid, isannounce) { + ilog.Printf("rejecting origin: %s", xid) + return false + } + if iszonked(user.ID, xid) { + ilog.Printf("already zonked: %s", xid) + return false + } + var id int64 + row := stmtFindXonk.QueryRow(user.ID, xid) + err := row.Scan(&id) + if err == nil { + return false + } + if err != sql.ErrNoRows { + ilog.Printf("error querying xonk: %s", err) + } + return true +} + +func eradicatexonk(userid int64, xid string) { + xonk := getxonk(userid, xid) + if xonk != nil { + deletehonk(xonk.ID) + } + _, err := stmtSaveZonker.Exec(userid, xid, "zonk") + if err != nil { + elog.Printf("error eradicating: %s", err) + } +} + +func savexonk(x *Honk) { + ilog.Printf("saving xonk: %s", x.XID) + go handles(x.Honker) + go handles(x.Oonker) + savehonk(x) +} + +type Box struct { + In string + Out string + Shared string +} + +var boxofboxes = cache.New(cache.Options{Filler: func(ident string) (*Box, bool) { + var info string + row := stmtGetXonker.QueryRow(ident, "boxes") + err := row.Scan(&info) + if err != nil { + dlog.Printf("need to get boxes for %s", ident) + var j junk.Junk + j, err = GetJunk(serverUID, ident) + if err != nil { + dlog.Printf("error getting boxes: %s", err) + return nil, false + } + allinjest(originate(ident), j) + row = stmtGetXonker.QueryRow(ident, "boxes") + err = row.Scan(&info) + } + if err == nil { + m := strings.Split(info, " ") + b := &Box{In: m[0], Out: m[1], Shared: m[2]} + return b, true + } + return nil, false +}}) + +func gimmexonks(user *WhatAbout, outbox string) { + dlog.Printf("getting outbox: %s", outbox) + j, err := GetJunk(user.ID, outbox) + if err != nil { + ilog.Printf("error getting outbox: %s", err) + return + } + t, _ := j.GetString("type") + origin := originate(outbox) + if t == "OrderedCollection" { + items, _ := j.GetArray("orderedItems") + if items == nil { + items, _ = j.GetArray("items") + } + if items == nil { + obj, ok := j.GetMap("first") + if ok { + items, _ = obj.GetArray("orderedItems") + } else { + page1, ok := j.GetString("first") + if ok { + j, err = GetJunk(user.ID, page1) + if err != nil { + ilog.Printf("error gettings page1: %s", err) + return + } + items, _ = j.GetArray("orderedItems") + } + } + } + if len(items) > 20 { + items = items[0:20] + } + for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 { + items[i], items[j] = items[j], items[i] + } + for _, item := range items { + obj, ok := item.(junk.Junk) + if ok { + xonksaver(user, obj, origin) + continue + } + xid, ok := item.(string) + if ok { + if !needxonkid(user, xid) { + continue + } + obj, err = GetJunk(user.ID, xid) + if err != nil { + ilog.Printf("error getting item: %s", err) + continue + } + xonksaver(user, obj, originate(xid)) + } + } + } +} + +func newphone(a []string, obj junk.Junk) []string { + for _, addr := range []string{"to", "cc", "attributedTo"} { + who, _ := obj.GetString(addr) + if who != "" { + a = append(a, who) + } + whos, _ := obj.GetArray(addr) + for _, w := range whos { + who, _ := w.(string) + if who != "" { + a = append(a, who) + } + } + } + return a +} + +func extractattrto(obj junk.Junk) string { + who, _ := obj.GetString("attributedTo") + if who != "" { + return who + } + o, ok := obj.GetMap("attributedTo") + if ok { + id, ok := o.GetString("id") + if ok { + return id + } + } + arr, _ := obj.GetArray("attributedTo") + for _, a := range arr { + o, ok := a.(junk.Junk) + if ok { + t, _ := o.GetString("type") + id, _ := o.GetString("id") + if t == "Person" || t == "" { + return id + } + } + s, ok := a.(string) + if ok { + return s + } + } + return "" +} + +func firstofmany(obj junk.Junk, key string) string { + if val, _ := obj.GetString(key); val != "" { + return val + } + if arr, _ := obj.GetArray(key); len(arr) > 0 { + val, ok := arr[0].(string) + if ok { + return val + } + } + return "" +} + +func xonksaver(user *WhatAbout, item junk.Junk, origin string) *Honk { + depth := 0 + maxdepth := 10 + currenttid := "" + goingup := 0 + var xonkxonkfn func(item junk.Junk, origin string, isUpdate bool) *Honk + + saveonemore := func(xid string) { + dlog.Printf("getting onemore: %s", xid) + if depth >= maxdepth { + ilog.Printf("in too deep") + return + } + obj, err := GetJunkHardMode(user.ID, xid) + if err != nil { + ilog.Printf("error getting onemore: %s: %s", xid, err) + return + } + depth++ + xonkxonkfn(obj, originate(xid), false) + depth-- + } + + xonkxonkfn = func(item junk.Junk, origin string, isUpdate bool) *Honk { + id, _ := item.GetString("id") + what := firstofmany(item, "type") + dt, ok := item.GetString("published") + if !ok { + dt = time.Now().Format(time.RFC3339) + } + + var err error + var xid, rid, url, convoy string + var replies []string + var obj junk.Junk + switch what { + case "Delete": + obj, ok = item.GetMap("object") + if ok { + xid, _ = obj.GetString("id") + } else { + xid, _ = item.GetString("object") + } + if xid == "" { + return nil + } + if originate(xid) != origin { + ilog.Printf("forged delete: %s", xid) + return nil + } + ilog.Printf("eradicating %s", xid) + eradicatexonk(user.ID, xid) + return nil + case "Remove": + xid, _ = item.GetString("object") + targ, _ := obj.GetString("target") + ilog.Printf("remove %s from %s", obj, targ) + return nil + case "Tombstone": + xid, _ = item.GetString("id") + if xid == "" { + return nil + } + if originate(xid) != origin { + ilog.Printf("forged delete: %s", xid) + return nil + } + ilog.Printf("eradicating %s", xid) + eradicatexonk(user.ID, xid) + return nil + case "Announce": + obj, ok = item.GetMap("object") + if ok { + xid, _ = obj.GetString("id") + } else { + xid, _ = item.GetString("object") + } + if !needbonkid(user, xid) { + return nil + } + dlog.Printf("getting bonk: %s", xid) + obj, err = GetJunkHardMode(user.ID, xid) + if err != nil { + ilog.Printf("error getting bonk: %s: %s", xid, err) + } + origin = originate(xid) + what = "bonk" + case "Update": + isUpdate = true + fallthrough + case "Create": + obj, ok = item.GetMap("object") + if !ok { + xid, _ = item.GetString("object") + dlog.Printf("getting created honk: %s", xid) + if originate(xid) != origin { + ilog.Printf("out of bounds %s not from %s", xid, origin) + return nil + } + obj, err = GetJunkHardMode(user.ID, xid) + if err != nil { + ilog.Printf("error getting creation: %s", err) + } + } + if obj == nil { + ilog.Printf("no object for creation %s", id) + return nil + } + return xonkxonkfn(obj, origin, isUpdate) + case "Read": + xid, ok = item.GetString("object") + if ok { + if !needxonkid(user, xid) { + dlog.Printf("don't need read obj: %s", xid) + return nil + } + obj, err = GetJunkHardMode(user.ID, xid) + if err != nil { + ilog.Printf("error getting read: %s", err) + return nil + } + return xonkxonkfn(obj, originate(xid), false) + } + return nil + case "Add": + xid, ok = item.GetString("object") + if ok { + // check target... + if !needxonkid(user, xid) { + dlog.Printf("don't need added obj: %s", xid) + return nil + } + obj, err = GetJunkHardMode(user.ID, xid) + if err != nil { + ilog.Printf("error getting add: %s", err) + return nil + } + return xonkxonkfn(obj, originate(xid), false) + } + return nil + case "Move": + obj = item + what = "move" + case "GuessWord": // dealt with below + fallthrough + case "Audio": + fallthrough + case "Image": + fallthrough + case "Video": + fallthrough + case "Question": + fallthrough + case "Note": + fallthrough + case "Article": + fallthrough + case "Page": + obj = item + what = "honk" + case "Event": + obj = item + what = "event" + case "ChatMessage": + obj = item + what = "chonk" + default: + ilog.Printf("unknown activity: %s", what) + dumpactivity(item) + return nil + } + + if obj != nil { + xid, _ = obj.GetString("id") + } + + if xid == "" { + ilog.Printf("don't know what xid is") + item.Write(ilog.Writer()) + return nil + } + if originate(xid) != origin { + ilog.Printf("original sin: %s not from %s", xid, origin) + item.Write(ilog.Writer()) + return nil + } + + var xonk Honk + // early init + xonk.XID = xid + xonk.UserID = user.ID + xonk.Honker, _ = item.GetString("actor") + if xonk.Honker == "" { + xonk.Honker, _ = item.GetString("attributedTo") + } + if obj != nil { + if xonk.Honker == "" { + xonk.Honker = extractattrto(obj) + } + xonk.Oonker = extractattrto(obj) + if xonk.Oonker == xonk.Honker { + xonk.Oonker = "" + } + xonk.Audience = newphone(nil, obj) + } + xonk.Audience = append(xonk.Audience, xonk.Honker) + xonk.Audience = oneofakind(xonk.Audience) + xonk.Public = loudandproud(xonk.Audience) + + var mentions []Mention + if obj != nil { + ot, _ := obj.GetString("type") + url, _ = obj.GetString("url") + if dt2, ok := obj.GetString("published"); ok { + dt = dt2 + } + content, _ := obj.GetString("content") + if !strings.HasPrefix(content, "<p>") { + content = "<p>" + content + } + precis, _ := obj.GetString("summary") + if name, ok := obj.GetString("name"); ok { + if precis != "" { + content = precis + "<p>" + content + } + precis = html.EscapeString(name) + } + if sens, _ := obj["sensitive"].(bool); sens && precis == "" { + precis = "unspecified horror" + } + rid, ok = obj.GetString("inReplyTo") + if !ok { + if robj, ok := obj.GetMap("inReplyTo"); ok { + rid, _ = robj.GetString("id") + } + } + convoy, _ = obj.GetString("context") + if convoy == "" { + convoy, _ = obj.GetString("conversation") + } + if ot == "Question" { + if what == "honk" { + what = "qonk" + } + content += "<ul>" + ans, _ := obj.GetArray("oneOf") + for _, ai := range ans { + a, ok := ai.(junk.Junk) + if !ok { + continue + } + as, _ := a.GetString("name") + content += "<li>" + as + } + ans, _ = obj.GetArray("anyOf") + for _, ai := range ans { + a, ok := ai.(junk.Junk) + if !ok { + continue + } + as, _ := a.GetString("name") + content += "<li>" + as + } + content += "</ul>" + } + if ot == "Move" { + targ, _ := obj.GetString("target") + content += string(templates.Sprintf(`<p>Moved to <a href="%s">%s</a>`, targ, targ)) + } + if ot == "GuessWord" { + what = "wonk" + content, _ = obj.GetString("content") + xonk.Wonkles, _ = obj.GetString("wordlist") + go savewonkles(xonk.Wonkles) + } + if what == "honk" && rid != "" { + what = "tonk" + } + if len(content) > 90001 { + ilog.Printf("content too long. truncating") + content = content[:90001] + } + + xonk.Noise = content + xonk.Precis = precis + if rejectxonk(&xonk) { + dlog.Printf("fast reject: %s", xid) + return nil + } + + numatts := 0 + procatt := func(att junk.Junk) { + at, _ := att.GetString("type") + mt, _ := att.GetString("mediaType") + u, ok := att.GetString("url") + if !ok { + if ua, ok := att.GetArray("url"); ok && len(ua) > 0 { + u, ok = ua[0].(string) + if !ok { + if uu, ok := ua[0].(junk.Junk); ok { + u, _ = uu.GetString("href") + if mt == "" { + mt, _ = uu.GetString("mediaType") + } + } + } + } else if uu, ok := att.GetMap("url"); ok { + u, _ = uu.GetString("href") + if mt == "" { + mt, _ = uu.GetString("mediaType") + } + } + } + name, _ := att.GetString("name") + desc, _ := att.GetString("summary") + desc = html.UnescapeString(desc) + if desc == "" { + desc = name + } + localize := false + if numatts > 4 { + ilog.Printf("excessive attachment: %s", at) + } else if at == "Document" || at == "Image" { + mt = strings.ToLower(mt) + dlog.Printf("attachment: %s %s", mt, u) + if mt == "text/plain" || mt == "application/pdf" || + strings.HasPrefix(mt, "image") { + localize = true + } + } else { + ilog.Printf("unknown attachment: %s", at) + } + if skipMedia(&xonk) { + localize = false + } + donk := savedonk(u, name, desc, mt, localize) + if donk != nil { + xonk.Donks = append(xonk.Donks, donk) + } + numatts++ + } + atts, _ := obj.GetArray("attachment") + for _, atti := range atts { + att, ok := atti.(junk.Junk) + if !ok { + ilog.Printf("attachment that wasn't map?") + continue + } + procatt(att) + } + if att, ok := obj.GetMap("attachment"); ok { + procatt(att) + } + tags, _ := obj.GetArray("tag") + for _, tagi := range tags { + tag, ok := tagi.(junk.Junk) + if !ok { + continue + } + tt, _ := tag.GetString("type") + name, _ := tag.GetString("name") + desc, _ := tag.GetString("summary") + desc = html.UnescapeString(desc) + if desc == "" { + desc = name + } + if tt == "Emoji" { + icon, _ := tag.GetMap("icon") + mt, _ := icon.GetString("mediaType") + if mt == "" { + mt = "image/png" + } + u, _ := icon.GetString("url") + donk := savedonk(u, name, desc, mt, true) + if donk != nil { + xonk.Donks = append(xonk.Donks, donk) + } + } + if tt == "Hashtag" { + if name == "" || name == "#" { + // skip it + } else { + if name[0] != '#' { + name = "#" + name + } + xonk.Onts = append(xonk.Onts, name) + } + } + if tt == "Place" { + p := new(Place) + p.Name = name + p.Latitude, _ = tag.GetNumber("latitude") + p.Longitude, _ = tag.GetNumber("longitude") + p.Url, _ = tag.GetString("url") + xonk.Place = p + } + if tt == "Mention" { + var m Mention + m.Who, _ = tag.GetString("name") + m.Where, _ = tag.GetString("href") + mentions = append(mentions, m) + } + } + if starttime, ok := obj.GetString("startTime"); ok { + if start, err := time.Parse(time.RFC3339, starttime); err == nil { + t := new(Time) + t.StartTime = start + endtime, _ := obj.GetString("endTime") + t.EndTime, _ = time.Parse(time.RFC3339, endtime) + dura, _ := obj.GetString("duration") + if strings.HasPrefix(dura, "PT") { + dura = strings.ToLower(dura[2:]) + d, _ := time.ParseDuration(dura) + t.Duration = Duration(d) + } + xonk.Time = t + } + } + if loca, ok := obj.GetMap("location"); ok { + if tt, _ := loca.GetString("type"); tt == "Place" { + p := new(Place) + p.Name, _ = loca.GetString("name") + p.Latitude, _ = loca.GetNumber("latitude") + p.Longitude, _ = loca.GetNumber("longitude") + p.Url, _ = loca.GetString("url") + xonk.Place = p + } + } + + xonk.Onts = oneofakind(xonk.Onts) + replyobj, ok := obj.GetMap("replies") + if ok { + items, ok := replyobj.GetArray("items") + if !ok { + first, ok := replyobj.GetMap("first") + if ok { + items, _ = first.GetArray("items") + } + } + for _, repl := range items { + s, ok := repl.(string) + if ok { + replies = append(replies, s) + } + } + } + + } + + if currenttid == "" { + currenttid = convoy + } + + // init xonk + xonk.What = what + xonk.RID = rid + xonk.Date, _ = time.Parse(time.RFC3339, dt) + xonk.URL = url + xonk.Format = "html" + xonk.Convoy = convoy + xonk.Mentions = mentions + for _, m := range mentions { + if m.Where == user.URL { + xonk.Whofore = 1 + } + } + imaginate(&xonk) + + if what == "chonk" { + ch := Chonk{ + UserID: xonk.UserID, + XID: xid, + Who: xonk.Honker, + Target: xonk.Honker, + Date: xonk.Date, + Noise: xonk.Noise, + Format: xonk.Format, + Donks: xonk.Donks, + } + savechonk(&ch) + return nil + } + + if isUpdate { + dlog.Printf("something has changed! %s", xonk.XID) + prev := getxonk(user.ID, xonk.XID) + if prev == nil { + ilog.Printf("didn't find old version for update: %s", xonk.XID) + isUpdate = false + } else { + xonk.ID = prev.ID + updatehonk(&xonk) + } + } + if !isUpdate && needxonk(user, &xonk) { + if rid != "" && xonk.Public { + if needxonkid(user, rid) { + goingup++ + saveonemore(rid) + goingup-- + } + if convoy == "" { + xx := getxonk(user.ID, rid) + if xx != nil { + convoy = xx.Convoy + } + } + } + if convoy == "" { + convoy = currenttid + } + if convoy == "" { + convoy = "data:,missing-" + xfiltrate() + currenttid = convoy + } + xonk.Convoy = convoy + savexonk(&xonk) + } + if goingup == 0 { + for _, replid := range replies { + if needxonkid(user, replid) { + dlog.Printf("missing a reply: %s", replid) + saveonemore(replid) + } + } + } + return &xonk + } + + return xonkxonkfn(item, origin, false) +} + +func dumpactivity(item junk.Junk) { + fd, err := os.OpenFile("savedinbox.json", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + elog.Printf("error opening inbox! %s", err) + return + } + defer fd.Close() + item.Write(fd) + io.WriteString(fd, "\n") +} + +func rubadubdub(user *WhatAbout, req junk.Junk) { + actor, _ := req.GetString("actor") + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = user.URL + "/dub/" + xfiltrate() + j["type"] = "Accept" + j["actor"] = user.URL + j["to"] = actor + j["published"] = time.Now().UTC().Format(time.RFC3339) + j["object"] = req + + deliverate(0, user.ID, actor, j.ToBytes(), true) +} + +func itakeitallback(user *WhatAbout, xid string, owner string, folxid string) { + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = user.URL + "/unsub/" + folxid + j["type"] = "Undo" + j["actor"] = user.URL + j["to"] = owner + f := junk.New() + f["id"] = user.URL + "/sub/" + folxid + f["type"] = "Follow" + f["actor"] = user.URL + f["to"] = owner + f["object"] = xid + j["object"] = f + j["published"] = time.Now().UTC().Format(time.RFC3339) + + deliverate(0, user.ID, owner, j.ToBytes(), true) +} + +func subsub(user *WhatAbout, xid string, owner string, folxid string) { + if xid == "" { + ilog.Printf("can't subscribe to empty") + return + } + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = user.URL + "/sub/" + folxid + j["type"] = "Follow" + j["actor"] = user.URL + j["to"] = owner + j["object"] = xid + j["published"] = time.Now().UTC().Format(time.RFC3339) + + deliverate(0, user.ID, owner, j.ToBytes(), true) +} + +func activatedonks(donks []*Donk) []junk.Junk { + var atts []junk.Junk + for _, d := range donks { + if re_emus.MatchString(d.Name) { + continue + } + jd := junk.New() + jd["mediaType"] = d.Media + jd["name"] = d.Name + jd["summary"] = html.EscapeString(d.Desc) + jd["type"] = "Document" + jd["url"] = d.URL + atts = append(atts, jd) + } + return atts +} + +// returns activity, object +func jonkjonk(user *WhatAbout, h *Honk) (junk.Junk, junk.Junk) { + dt := h.Date.Format(time.RFC3339) + var jo junk.Junk + j := junk.New() + j["id"] = user.URL + "/" + h.What + "/" + shortxid(h.XID) + j["actor"] = user.URL + j["published"] = dt + j["to"] = h.Audience[0] + if len(h.Audience) > 1 { + j["cc"] = h.Audience[1:] + } + + switch h.What { + case "update": + fallthrough + case "tonk": + fallthrough + case "event": + fallthrough + case "wonk": + fallthrough + case "honk": + j["type"] = "Create" + jo = junk.New() + jo["id"] = h.XID + jo["type"] = "Note" + if h.What == "event" { + jo["type"] = "Event" + } else if h.What == "wonk" { + jo["type"] = "GuessWord" + } + if h.What == "update" { + j["type"] = "Update" + jo["updated"] = dt + } + jo["published"] = dt + jo["url"] = h.XID + jo["attributedTo"] = user.URL + if h.RID != "" { + jo["inReplyTo"] = h.RID + } + if h.Convoy != "" { + jo["context"] = h.Convoy + jo["conversation"] = h.Convoy + } + jo["to"] = h.Audience[0] + if len(h.Audience) > 1 { + jo["cc"] = h.Audience[1:] + } + if !h.Public { + jo["directMessage"] = true + } + translate(h) + redoimages(h) + if h.Precis != "" { + jo["sensitive"] = true + } + + var replies []string + for _, reply := range h.Replies { + replies = append(replies, reply.XID) + } + if len(replies) > 0 { + jr := junk.New() + jr["type"] = "Collection" + jr["totalItems"] = len(replies) + jr["items"] = replies + jo["replies"] = jr + } + + var tags []junk.Junk + for _, m := range h.Mentions { + t := junk.New() + t["type"] = "Mention" + t["name"] = m.Who + t["href"] = m.Where + tags = append(tags, t) + } + for _, o := range h.Onts { + t := junk.New() + t["type"] = "Hashtag" + o = strings.ToLower(o) + t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:]) + t["name"] = o + tags = append(tags, t) + } + for _, e := range herdofemus(h.Noise) { + t := junk.New() + t["id"] = e.ID + t["type"] = "Emoji" + t["name"] = e.Name + i := junk.New() + i["type"] = "Image" + i["mediaType"] = e.Type + i["url"] = e.ID + t["icon"] = i + tags = append(tags, t) + } + for _, e := range fixupflags(h) { + t := junk.New() + t["id"] = e.ID + t["type"] = "Emoji" + t["name"] = e.Name + i := junk.New() + i["type"] = "Image" + i["mediaType"] = "image/png" + i["url"] = e.ID + t["icon"] = i + tags = append(tags, t) + } + if len(tags) > 0 { + jo["tag"] = tags + } + if p := h.Place; p != nil { + t := junk.New() + t["type"] = "Place" + if p.Name != "" { + t["name"] = p.Name + } + if p.Latitude != 0 { + t["latitude"] = p.Latitude + } + if p.Longitude != 0 { + t["longitude"] = p.Longitude + } + if p.Url != "" { + t["url"] = p.Url + } + jo["location"] = t + } + if t := h.Time; t != nil { + jo["startTime"] = t.StartTime.Format(time.RFC3339) + if t.Duration != 0 { + jo["duration"] = "PT" + strings.ToUpper(t.Duration.String()) + } + } + if w := h.Wonkles; w != "" { + jo["wordlist"] = w + } + atts := activatedonks(h.Donks) + if len(atts) > 0 { + jo["attachment"] = atts + } + jo["summary"] = html.EscapeString(h.Precis) + jo["content"] = h.Noise + j["object"] = jo + case "bonk": + j["type"] = "Announce" + if h.Convoy != "" { + j["context"] = h.Convoy + } + j["object"] = h.XID + case "unbonk": + b := junk.New() + b["id"] = user.URL + "/" + "bonk" + "/" + shortxid(h.XID) + b["type"] = "Announce" + b["actor"] = user.URL + if h.Convoy != "" { + b["context"] = h.Convoy + } + b["object"] = h.XID + j["type"] = "Undo" + j["object"] = b + case "zonk": + j["type"] = "Delete" + j["object"] = h.XID + case "ack": + j["type"] = "Read" + j["object"] = h.XID + if h.Convoy != "" { + j["context"] = h.Convoy + } + case "react": + j["type"] = "EmojiReact" + j["object"] = h.XID + if h.Convoy != "" { + j["context"] = h.Convoy + } + j["content"] = h.Noise + case "deack": + b := junk.New() + b["id"] = user.URL + "/" + "ack" + "/" + shortxid(h.XID) + b["type"] = "Read" + b["actor"] = user.URL + b["object"] = h.XID + if h.Convoy != "" { + b["context"] = h.Convoy + } + j["type"] = "Undo" + j["object"] = b + } + + return j, jo +} + +var oldjonks = cache.New(cache.Options{Filler: func(xid string) ([]byte, bool) { + row := stmtAnyXonk.QueryRow(xid) + honk := scanhonk(row) + if honk == nil || !honk.Public { + return nil, true + } + user, _ := butwhatabout(honk.Username) + rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0) + reversehonks(rawhonks) + for _, h := range rawhonks { + if h.RID == honk.XID && h.Public && (h.Whofore == 2 || h.IsAcked()) { + honk.Replies = append(honk.Replies, h) + } + } + donksforhonks([]*Honk{honk}) + _, j := jonkjonk(user, honk) + j["@context"] = itiswhatitis + + return j.ToBytes(), true +}, Limit: 128}) + +func gimmejonk(xid string) ([]byte, bool) { + var j []byte + ok := oldjonks.Get(xid, &j) + return j, ok +} + +func boxuprcpts(user *WhatAbout, addresses []string, useshared bool) map[string]bool { + rcpts := make(map[string]bool) + for _, a := range addresses { + if a == "" || a == thewholeworld || a == user.URL || strings.HasSuffix(a, "/followers") { + continue + } + if a[0] == '%' { + rcpts[a] = true + continue + } + var box *Box + ok := boxofboxes.Get(a, &box) + if ok && useshared && box.Shared != "" { + rcpts["%"+box.Shared] = true + } else { + rcpts[a] = true + } + } + return rcpts +} + +func chonkifymsg(user *WhatAbout, ch *Chonk) []byte { + dt := ch.Date.Format(time.RFC3339) + aud := []string{ch.Target} + + jo := junk.New() + jo["id"] = ch.XID + jo["type"] = "ChatMessage" + jo["published"] = dt + jo["attributedTo"] = user.URL + jo["to"] = aud + jo["content"] = ch.HTML + atts := activatedonks(ch.Donks) + if len(atts) > 0 { + jo["attachment"] = atts + } + var tags []junk.Junk + for _, e := range herdofemus(ch.Noise) { + t := junk.New() + t["id"] = e.ID + t["type"] = "Emoji" + t["name"] = e.Name + i := junk.New() + i["type"] = "Image" + i["mediaType"] = e.Type + i["url"] = e.ID + t["icon"] = i + tags = append(tags, t) + } + if len(tags) > 0 { + jo["tag"] = tags + } + + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = user.URL + "/" + "honk" + "/" + shortxid(ch.XID) + j["type"] = "Create" + j["actor"] = user.URL + j["published"] = dt + j["to"] = aud + j["object"] = jo + + return j.ToBytes() +} + +func sendchonk(user *WhatAbout, ch *Chonk) { + msg := chonkifymsg(user, ch) + + rcpts := make(map[string]bool) + rcpts[ch.Target] = true + for a := range rcpts { + go deliverate(0, user.ID, a, msg, true) + } +} + +func honkworldwide(user *WhatAbout, honk *Honk) { + jonk, _ := jonkjonk(user, honk) + jonk["@context"] = itiswhatitis + msg := jonk.ToBytes() + + rcpts := boxuprcpts(user, honk.Audience, honk.Public) + + if honk.Public { + for _, h := range getdubs(user.ID) { + if h.XID == user.URL { + continue + } + var box *Box + ok := boxofboxes.Get(h.XID, &box) + if ok && box.Shared != "" { + rcpts["%"+box.Shared] = true + } else { + rcpts[h.XID] = true + } + } + for _, f := range getbacktracks(honk.XID) { + if f[0] == '%' { + rcpts[f] = true + } else { + var box *Box + ok := boxofboxes.Get(f, &box) + if ok && box.Shared != "" { + rcpts["%"+box.Shared] = true + } else { + rcpts[f] = true + } + } + } + } + for a := range rcpts { + go deliverate(0, user.ID, a, msg, doesitmatter(honk.What)) + } + if honk.Public && len(honk.Onts) > 0 { + collectiveaction(honk) + } +} + +func doesitmatter(what string) bool { + switch what { + case "ack": + return false + case "react": + return false + case "deack": + return false + } + return true +} + +func collectiveaction(honk *Honk) { + user := getserveruser() + for _, ont := range honk.Onts { + dubs := getnameddubs(serverUID, ont) + if len(dubs) == 0 { + continue + } + j := junk.New() + j["@context"] = itiswhatitis + j["type"] = "Add" + j["id"] = user.URL + "/add/" + shortxid(ont+honk.XID) + j["actor"] = user.URL + j["object"] = honk.XID + j["target"] = fmt.Sprintf("https://%s/o/%s", serverName, ont[1:]) + rcpts := make(map[string]bool) + for _, dub := range dubs { + var box *Box + ok := boxofboxes.Get(dub.XID, &box) + if ok && box.Shared != "" { + rcpts["%"+box.Shared] = true + } else { + rcpts[dub.XID] = true + } + } + msg := j.ToBytes() + for a := range rcpts { + go deliverate(0, user.ID, a, msg, false) + } + } +} + +func junkuser(user *WhatAbout) junk.Junk { + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = user.URL + j["inbox"] = user.URL + "/inbox" + j["outbox"] = user.URL + "/outbox" + j["name"] = user.Display + j["preferredUsername"] = user.Name + j["summary"] = user.HTAbout + var tags []junk.Junk + for _, o := range user.Onts { + t := junk.New() + t["type"] = "Hashtag" + o = strings.ToLower(o) + t["href"] = fmt.Sprintf("https://%s/o/%s", serverName, o[1:]) + t["name"] = o + tags = append(tags, t) + } + if len(tags) > 0 { + j["tag"] = tags + } + + if user.ID > 0 { + j["type"] = "Person" + j["url"] = user.URL + j["followers"] = user.URL + "/followers" + j["following"] = user.URL + "/following" + a := junk.New() + a["type"] = "Image" + a["mediaType"] = "image/png" + if ava := user.Options.Avatar; ava != "" { + a["url"] = ava + } else { + u := fmt.Sprintf("https://%s/a?a=%s", serverName, url.QueryEscape(user.URL)) + if user.Options.Avahex { + u += "&hex=1" + } + a["url"] = u + } + j["icon"] = a + if ban := user.Options.Banner; ban != "" { + a := junk.New() + a["type"] = "Image" + a["mediaType"] = "image/jpg" + a["url"] = ban + j["image"] = a + } + } else { + j["type"] = "Service" + } + k := junk.New() + k["id"] = user.URL + "#key" + k["owner"] = user.URL + k["publicKeyPem"] = user.Key + j["publicKey"] = k + + return j +} + +var oldjonkers = cache.New(cache.Options{Filler: func(name string) ([]byte, bool) { + user, err := butwhatabout(name) + if err != nil { + return nil, false + } + var buf bytes.Buffer + j := junkuser(user) + j.Write(&buf) + return buf.Bytes(), true +}, Duration: 1 * time.Minute}) + +func asjonker(name string) ([]byte, bool) { + var j []byte + ok := oldjonkers.Get(name, &j) + return j, ok +} + +var handfull = cache.New(cache.Options{Filler: func(name string) (string, bool) { + m := strings.Split(name, "@") + if len(m) != 2 { + dlog.Printf("bad fish name: %s", name) + return "", true + } + var href string + row := stmtGetXonker.QueryRow(name, "fishname") + err := row.Scan(&href) + if err == nil { + return href, true + } + dlog.Printf("fishing for %s", name) + j, err := GetJunkFast(serverUID, fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s", m[1], name)) + if err != nil { + ilog.Printf("failed to go fish %s: %s", name, err) + return "", true + } + links, _ := j.GetArray("links") + for _, li := range links { + l, ok := li.(junk.Junk) + if !ok { + continue + } + href, _ := l.GetString("href") + rel, _ := l.GetString("rel") + t, _ := l.GetString("type") + if rel == "self" && friendorfoe(t) { + when := time.Now().UTC().Format(dbtimeformat) + _, err := stmtSaveXonker.Exec(name, href, "fishname", when) + if err != nil { + elog.Printf("error saving fishname: %s", err) + } + return href, true + } + } + return href, true +}, Duration: 1 * time.Minute}) + +func gofish(name string) string { + if name[0] == '@' { + name = name[1:] + } + var href string + handfull.Get(name, &href) + return href +} + +func investigate(name string) (*SomeThing, error) { + if name == "" { + return nil, fmt.Errorf("no name") + } + if name[0] == '@' { + name = gofish(name) + } + if name == "" { + return nil, fmt.Errorf("no name") + } + obj, err := GetJunkFast(serverUID, name) + if err != nil { + return nil, err + } + allinjest(originate(name), obj) + return somethingabout(obj) +} + +func somethingabout(obj junk.Junk) (*SomeThing, error) { + info := new(SomeThing) + t, _ := obj.GetString("type") + switch t { + case "Person": + fallthrough + case "Organization": + fallthrough + case "Application": + fallthrough + case "Service": + info.What = SomeActor + case "OrderedCollection": + fallthrough + case "Collection": + info.What = SomeCollection + default: + return nil, fmt.Errorf("unknown object type") + } + info.XID, _ = obj.GetString("id") + info.Name, _ = obj.GetString("preferredUsername") + if info.Name == "" { + info.Name, _ = obj.GetString("name") + } + info.Owner, _ = obj.GetString("attributedTo") + if info.Owner == "" { + info.Owner = info.XID + } + return info, nil +} + +func allinjest(origin string, obj junk.Junk) { + keyobj, ok := obj.GetMap("publicKey") + if ok { + ingestpubkey(origin, keyobj) + } + ingestboxes(origin, obj) + ingesthandle(origin, obj) +} + +func ingestpubkey(origin string, obj junk.Junk) { + keyobj, ok := obj.GetMap("publicKey") + if ok { + obj = keyobj + } + keyname, ok := obj.GetString("id") + var data string + row := stmtGetXonker.QueryRow(keyname, "pubkey") + err := row.Scan(&data) + if err == nil { + return + } + if !ok || origin != originate(keyname) { + ilog.Printf("bad key origin %s <> %s", origin, keyname) + return + } + dlog.Printf("ingesting a needed pubkey: %s", keyname) + owner, ok := obj.GetString("owner") + if !ok { + ilog.Printf("error finding %s pubkey owner", keyname) + return + } + data, ok = obj.GetString("publicKeyPem") + if !ok { + ilog.Printf("error finding %s pubkey", keyname) + return + } + if originate(owner) != origin { + ilog.Printf("bad key owner: %s <> %s", owner, origin) + return + } + _, _, err = httpsig.DecodeKey(data) + if err != nil { + ilog.Printf("error decoding %s pubkey: %s", keyname, err) + return + } + when := time.Now().UTC().Format(dbtimeformat) + _, err = stmtSaveXonker.Exec(keyname, data, "pubkey", when) + if err != nil { + elog.Printf("error saving key: %s", err) + } +} + +func ingestboxes(origin string, obj junk.Junk) { + ident, _ := obj.GetString("id") + if ident == "" { + return + } + if originate(ident) != origin { + return + } + var info string + row := stmtGetXonker.QueryRow(ident, "boxes") + err := row.Scan(&info) + if err == nil { + return + } + dlog.Printf("ingesting boxes: %s", ident) + inbox, _ := obj.GetString("inbox") + outbox, _ := obj.GetString("outbox") + sbox, _ := obj.GetString("endpoints", "sharedInbox") + if inbox != "" { + when := time.Now().UTC().Format(dbtimeformat) + m := strings.Join([]string{inbox, outbox, sbox}, " ") + _, err = stmtSaveXonker.Exec(ident, m, "boxes", when) + if err != nil { + elog.Printf("error saving boxes: %s", err) + } + } +} + +func ingesthandle(origin string, obj junk.Junk) { + xid, _ := obj.GetString("id") + if xid == "" { + return + } + if originate(xid) != origin { + return + } + var handle string + row := stmtGetXonker.QueryRow(xid, "handle") + err := row.Scan(&handle) + if err == nil { + return + } + handle, _ = obj.GetString("preferredUsername") + if handle != "" { + when := time.Now().UTC().Format(dbtimeformat) + _, err = stmtSaveXonker.Exec(xid, handle, "handle", when) + if err != nil { + elog.Printf("error saving handle: %s", err) + } + } +} + +func updateMe(username string) { + var user *WhatAbout + somenamedusers.Get(username, &user) + dt := time.Now().UTC().Format(time.RFC3339) + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = fmt.Sprintf("%s/upme/%s/%d", user.URL, user.Name, time.Now().Unix()) + j["actor"] = user.URL + j["published"] = dt + j["to"] = thewholeworld + j["type"] = "Update" + j["object"] = junkuser(user) + + msg := j.ToBytes() + + rcpts := make(map[string]bool) + for _, f := range getdubs(user.ID) { + if f.XID == user.URL { + continue + } + var box *Box + boxofboxes.Get(f.XID, &box) + if box != nil && box.Shared != "" { + rcpts["%"+box.Shared] = true + } else { + rcpts[f.XID] = true + } + } + for a := range rcpts { + go deliverate(0, user.ID, a, msg, false) + } +} + +func followme(user *WhatAbout, who string, name string, j junk.Junk) { + folxid, _ := j.GetString("id") + + ilog.Printf("updating honker follow: %s %s", who, folxid) + + var x string + db := opendatabase() + row := db.QueryRow("select xid from honkers where name = ? and xid = ? and userid = ? and flavor in ('dub', 'undub')", name, who, user.ID) + err := row.Scan(&x) + if err != sql.ErrNoRows { + ilog.Printf("duplicate follow request: %s", who) + _, err = stmtUpdateFlavor.Exec("dub", folxid, user.ID, name, who, "undub") + if err != nil { + elog.Printf("error updating honker: %s", err) + } + } else { + stmtSaveDub.Exec(user.ID, name, who, "dub", folxid) + } + go rubadubdub(user, j) +} + +func unfollowme(user *WhatAbout, who string, name string, j junk.Junk) { + var folxid string + if who == "" { + folxid, _ = j.GetString("object") + + db := opendatabase() + row := db.QueryRow("select xid, name from honkers where userid = ? and folxid = ? and flavor in ('dub', 'undub')", user.ID, folxid) + err := row.Scan(&who, &name) + if err != nil { + if err != sql.ErrNoRows { + elog.Printf("error scanning honker: %s", err) + } + return + } + } + + ilog.Printf("updating honker undo: %s %s", who, folxid) + _, err := stmtUpdateFlavor.Exec("undub", folxid, user.ID, name, who, "dub") + if err != nil { + elog.Printf("error updating honker: %s", err) + return + } +} + +func followyou(user *WhatAbout, honkerid int64) { + var url, owner string + db := opendatabase() + row := db.QueryRow("select xid, owner from honkers where honkerid = ? and userid = ? and flavor in ('unsub', 'peep', 'presub', 'sub')", + honkerid, user.ID) + err := row.Scan(&url, &owner) + if err != nil { + elog.Printf("can't get honker xid: %s", err) + return + } + folxid := xfiltrate() + ilog.Printf("subscribing to %s", url) + _, err = db.Exec("update honkers set flavor = ?, folxid = ? where honkerid = ?", "presub", folxid, honkerid) + if err != nil { + elog.Printf("error updating honker: %s", err) + return + } + go subsub(user, url, owner, folxid) + +} +func unfollowyou(user *WhatAbout, honkerid int64) { + db := opendatabase() + row := db.QueryRow("select xid, owner, folxid from honkers where honkerid = ? and userid = ? and flavor in ('sub')", + honkerid, user.ID) + var url, owner, folxid string + err := row.Scan(&url, &owner, &folxid) + if err != nil { + elog.Printf("can't get honker xid: %s", err) + return + } + ilog.Printf("unsubscribing from %s", url) + _, err = db.Exec("update honkers set flavor = ? where honkerid = ?", "unsub", honkerid) + if err != nil { + elog.Printf("error updating honker: %s", err) + return + } + go itakeitallback(user, url, owner, folxid) +} + +func followyou2(user *WhatAbout, j junk.Junk) { + who, _ := j.GetString("actor") + + ilog.Printf("updating honker accept: %s", who) + db := opendatabase() + row := db.QueryRow("select name, folxid from honkers where userid = ? and xid = ? and flavor in ('presub')", + user.ID, who) + var name, folxid string + err := row.Scan(&name, &folxid) + if err != nil { + elog.Printf("can't get honker name: %s", err) + return + } + _, err = stmtUpdateFlavor.Exec("sub", folxid, user.ID, name, who, "presub") + if err != nil { + elog.Printf("error updating honker: %s", err) + return + } +} + +func nofollowyou2(user *WhatAbout, j junk.Junk) { + who, _ := j.GetString("actor") + + ilog.Printf("updating honker reject: %s", who) + db := opendatabase() + row := db.QueryRow("select name, folxid from honkers where userid = ? and xid = ? and flavor in ('presub', 'sub')", + user.ID, who) + var name, folxid string + err := row.Scan(&name, &folxid) + if err != nil { + elog.Printf("can't get honker name: %s", err) + return + } + _, err = stmtUpdateFlavor.Exec("unsub", folxid, user.ID, name, who, "presub") + _, err = stmtUpdateFlavor.Exec("unsub", folxid, user.ID, name, who, "sub") + if err != nil { + elog.Printf("error updating honker: %s", err) + return + } +}
A admin.go

@@ -0,0 +1,299 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +/* +#include <termios.h> +void +clearecho(struct termios *tio) +{ + tio->c_lflag = tio->c_lflag & ~(ECHO|ICANON); +} +*/ +import "C" +import ( + "bufio" + "fmt" + "os" + "os/signal" + "strings" + + "humungus.tedunangst.com/r/webs/log" +) + +func adminscreen() { + log.Init(log.Options{Progname: "honk", Alllogname: "null"}) + stdout := bufio.NewWriter(os.Stdout) + esc := "\x1b" + smcup := esc + "[?1049h" + rmcup := esc + "[?1049l" + + var avatarColors string + getconfig("avatarcolors", &avatarColors) + loadLingo() + + type adminfield struct { + name string + label string + text string + oneline bool + } + + messages := []*adminfield{ + { + name: "servermsg", + label: "server", + text: string(serverMsg), + }, + { + name: "aboutmsg", + label: "about", + text: string(aboutMsg), + }, + { + name: "loginmsg", + label: "login", + text: string(loginMsg), + }, + { + name: "avatarcolors", + label: "avatar colors (4 RGBA hex numbers)", + text: string(avatarColors), + oneline: true, + }, + } + for _, l := range []string{"honked", "bonked", "honked back", "qonked", "evented"} { + messages = append(messages, &adminfield{ + name: "lingo-" + strings.ReplaceAll(l, " ", ""), + label: "lingo for " + l, + text: relingo[l], + oneline: true, + }) + } + cursel := 0 + + hidecursor := func() { + stdout.WriteString(esc + "[?25l") + } + showcursor := func() { + stdout.WriteString(esc + "[?12;25h") + } + movecursor := func(x, y int) { + stdout.WriteString(fmt.Sprintf(esc+"[%d;%dH", y, x)) + } + moveleft := func() { + stdout.WriteString(esc + "[1D") + } + clearscreen := func() { + stdout.WriteString(esc + "[2J") + } + //clearline := func() { stdout.WriteString(esc + "[2K") } + colorfn := func(code int) func(string) string { + return func(s string) string { + return fmt.Sprintf(esc+"[%dm"+"%s"+esc+"[0m", code, s) + } + } + reverse := colorfn(7) + magenta := colorfn(35) + readchar := func() byte { + var buf [1]byte + os.Stdin.Read(buf[:]) + c := buf[0] + return c + } + + savedtio := new(C.struct_termios) + C.tcgetattr(1, savedtio) + restore := func() { + stdout.WriteString(rmcup) + showcursor() + stdout.Flush() + C.tcsetattr(1, C.TCSAFLUSH, savedtio) + } + defer restore() + go func() { + sig := make(chan os.Signal) + signal.Notify(sig, os.Interrupt) + <-sig + restore() + os.Exit(0) + }() + + init := func() { + tio := new(C.struct_termios) + C.tcgetattr(1, tio) + C.clearecho(tio) + C.tcsetattr(1, C.TCSADRAIN, tio) + + hidecursor() + stdout.WriteString(smcup) + clearscreen() + movecursor(1, 1) + stdout.Flush() + } + + editing := false + + linecount := func(s string) int { + lines := 1 + for i := range s { + if s[i] == '\n' { + lines++ + } + } + return lines + } + + msglineno := func(idx int) int { + off := 1 + if idx == -1 { + return off + } + for i, m := range messages { + off += 1 + if i == idx { + return off + } + if !m.oneline { + off += 1 + off += linecount(m.text) + } + } + off += 2 + return off + } + + forscreen := func(s string) string { + return strings.Replace(s, "\n", "\n ", -1) + } + + drawmessage := func(idx int) { + line := msglineno(idx) + movecursor(4, line) + label := messages[idx].label + if idx == cursel { + label = reverse(label) + } + label = magenta(label) + text := forscreen(messages[idx].text) + if messages[idx].oneline { + stdout.WriteString(fmt.Sprintf("%s\t %s", label, text)) + } else { + stdout.WriteString(fmt.Sprintf("%s\n %s", label, text)) + } + } + + drawscreen := func() { + clearscreen() + movecursor(4, msglineno(-1)) + stdout.WriteString(magenta(serverName + " admin panel")) + for i := range messages { + if !editing || i != cursel { + drawmessage(i) + } + } + movecursor(4, msglineno(len(messages))) + dir := "j/k to move - q to quit - enter to edit" + if editing { + dir = "esc to end" + } + stdout.WriteString(magenta(dir)) + if editing { + drawmessage(cursel) + } + stdout.Flush() + } + + selectnext := func() { + if cursel < len(messages)-1 { + movecursor(4, msglineno(cursel)) + stdout.WriteString(magenta(messages[cursel].label)) + cursel++ + movecursor(4, msglineno(cursel)) + stdout.WriteString(reverse(magenta(messages[cursel].label))) + stdout.Flush() + } + } + selectprev := func() { + if cursel > 0 { + movecursor(4, msglineno(cursel)) + stdout.WriteString(magenta(messages[cursel].label)) + cursel-- + movecursor(4, msglineno(cursel)) + stdout.WriteString(reverse(magenta(messages[cursel].label))) + stdout.Flush() + } + } + editsel := func() { + editing = true + showcursor() + drawscreen() + m := messages[cursel] + loop: + for { + c := readchar() + switch c { + case '\x1b': + break loop + case '\n': + if m.oneline { + break loop + } + m.text += "\n" + drawscreen() + case 127: + if len(m.text) > 0 { + last := m.text[len(m.text)-1] + m.text = m.text[:len(m.text)-1] + if last == '\n' { + drawscreen() + } else { + moveleft() + stdout.WriteString(" ") + moveleft() + } + } + default: + m.text += string(c) + stdout.WriteString(string(c)) + } + stdout.Flush() + } + editing = false + setconfig(m.name, m.text) + hidecursor() + drawscreen() + } + + init() + drawscreen() + + for { + c := readchar() + switch c { + case 'q': + return + case 'j': + selectnext() + case 'k': + selectprev() + case '\n': + editsel() + default: + + } + } +}
A avatar.go

@@ -0,0 +1,205 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "bufio" + "bytes" + "crypto/sha512" + "fmt" + "image" + "image/png" + "net/http" + "regexp" + "strconv" + "strings" + + "github.com/gorilla/mux" +) + +var avatarcolors = [4][4]byte{ + {16, 0, 48, 255}, + {48, 0, 96, 255}, + {72, 0, 144, 255}, + {96, 0, 192, 255}, +} + +func loadAvatarColors() { + var colors string + getconfig("avatarcolors", &colors) + if colors == "" { + return + } + r := bufio.NewReader(strings.NewReader(colors)) + for i := 0; i < 4; i++ { + l, _ := r.ReadString(' ') + for l == " " { + l, _ = r.ReadString(' ') + } + l = strings.Trim(l, "# \n") + if len(l) == 6 { + l = l + "ff" + } + c, err := strconv.ParseUint(l, 16, 32) + if err != nil { + elog.Printf("error reading avatar color %d: %s", i, err) + continue + } + avatarcolors[i][0] = byte(c >> 24 & 0xff) + avatarcolors[i][1] = byte(c >> 16 & 0xff) + avatarcolors[i][2] = byte(c >> 8 & 0xff) + avatarcolors[i][3] = byte(c >> 0 & 0xff) + } +} + +func genAvatar(name string, hex bool) []byte { + h := sha512.New() + h.Write([]byte(name)) + s := h.Sum(nil) + img := image.NewNRGBA(image.Rect(0, 0, 64, 64)) + for i := 0; i < 64; i++ { + for j := 0; j < 64; j++ { + p := i*img.Stride + j*4 + if hex { + tan := 0.577 + if i < 32 { + if j < 17-int(float64(i)*tan) || j > 46+int(float64(i)*tan) { + img.Pix[p+0] = 0 + img.Pix[p+1] = 0 + img.Pix[p+2] = 0 + img.Pix[p+3] = 255 + continue + } + } else { + if j < 17-int(float64(64-i)*tan) || j > 46+int(float64(64-i)*tan) { + img.Pix[p+0] = 0 + img.Pix[p+1] = 0 + img.Pix[p+2] = 0 + img.Pix[p+3] = 255 + continue + + } + } + } + xx := i/16*16 + j/16 + x := s[xx] + if x < 64 { + img.Pix[p+0] = avatarcolors[0][0] + img.Pix[p+1] = avatarcolors[0][1] + img.Pix[p+2] = avatarcolors[0][2] + img.Pix[p+3] = avatarcolors[0][3] + } else if x < 128 { + img.Pix[p+0] = avatarcolors[1][0] + img.Pix[p+1] = avatarcolors[1][1] + img.Pix[p+2] = avatarcolors[1][2] + img.Pix[p+3] = avatarcolors[1][3] + } else if x < 192 { + img.Pix[p+0] = avatarcolors[2][0] + img.Pix[p+1] = avatarcolors[2][1] + img.Pix[p+2] = avatarcolors[2][2] + img.Pix[p+3] = avatarcolors[2][3] + } else { + img.Pix[p+0] = avatarcolors[3][0] + img.Pix[p+1] = avatarcolors[3][1] + img.Pix[p+2] = avatarcolors[3][2] + img.Pix[p+3] = avatarcolors[3][3] + } + } + } + var buf bytes.Buffer + png.Encode(&buf, img) + return buf.Bytes() +} + +func showflag(writer http.ResponseWriter, req *http.Request) { + code := mux.Vars(req)["code"] + colors := strings.Split(code, ",") + numcolors := len(colors) + vert := false + if colors[0] == "vert" { + vert = true + colors = colors[1:] + numcolors-- + if numcolors == 0 { + http.Error(writer, "bad flag", 400) + return + } + } + pixels := make([][4]byte, numcolors) + for i := 0; i < numcolors; i++ { + hex := colors[i] + if len(hex) == 3 { + hex = fmt.Sprintf("%c%c%c%c%c%c", + hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]) + } + c, _ := strconv.ParseUint(hex, 16, 32) + r := byte(c >> 16 & 0xff) + g := byte(c >> 8 & 0xff) + b := byte(c >> 0 & 0xff) + pixels[i][0] = r + pixels[i][1] = g + pixels[i][2] = b + pixels[i][3] = 255 + } + + h := 128 + w := h * 3 / 2 + img := image.NewRGBA(image.Rect(0, 0, w, h)) + if vert { + for j := 0; j < w; j++ { + pix := pixels[j*numcolors/w][:] + for i := 0; i < h; i++ { + p := i*img.Stride + j*4 + copy(img.Pix[p:], pix) + } + } + } else { + for i := 0; i < h; i++ { + pix := pixels[i*numcolors/h][:] + for j := 0; j < w; j++ { + p := i*img.Stride + j*4 + copy(img.Pix[p:], pix) + } + } + } + + writer.Header().Set("Cache-Control", "max-age="+somedays()) + png.Encode(writer, img) +} + +var re_flags = regexp.MustCompile("flag:[[:alnum:],]+") + +func fixupflags(h *Honk) []Emu { + var emus []Emu + count := 0 + h.Noise = re_flags.ReplaceAllStringFunc(h.Noise, func(m string) string { + count++ + var e Emu + e.Name = fmt.Sprintf(":flag%d:", count) + e.ID = fmt.Sprintf("https://%s/flag/%s", serverName, m[5:]) + emus = append(emus, e) + return e.Name + }) + return emus +} + +func renderflags(h *Honk) { + h.Noise = re_flags.ReplaceAllStringFunc(h.Noise, func(m string) string { + code := m[5:] + src := fmt.Sprintf("https://%s/flag/%s", serverName, code) + return fmt.Sprintf(`<img class="emu" title="%s" src="%s">`, "flag", src) + }) +}
A backend.go

@@ -0,0 +1,132 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "bytes" + "net" + "net/rpc" + "os" + "os/exec" + + "humungus.tedunangst.com/r/webs/gate" + "humungus.tedunangst.com/r/webs/image" +) + +type Shrinker struct { +} + +type ShrinkerArgs struct { + Buf []byte + Params image.Params +} + +type ShrinkerResult struct { + Image *image.Image +} + +var shrinkgate = gate.NewLimiter(4) + +func (s *Shrinker) Shrink(args *ShrinkerArgs, res *ShrinkerResult) error { + shrinkgate.Start() + defer shrinkgate.Finish() + img, err := image.Vacuum(bytes.NewReader(args.Buf), args.Params) + if err != nil { + return err + } + res.Image = img + return nil +} + +func backendSockname() string { + return dataDir + "/backend.sock" +} + +func shrinkit(data []byte) (*image.Image, error) { + cl, err := rpc.Dial("unix", backendSockname()) + if err != nil { + return nil, err + } + defer cl.Close() + var res ShrinkerResult + err = cl.Call("Shrinker.Shrink", &ShrinkerArgs{ + Buf: data, + Params: image.Params{LimitSize: 4200 * 4200, MaxWidth: 2048, MaxHeight: 2048}, + }, &res) + if err != nil { + return nil, err + } + return res.Image, nil +} + +var backendhooks []func() + +func orphancheck() { + var b [1]byte + os.Stdin.Read(b[:]) + dlog.Printf("backend shutting down") + os.Exit(0) +} + +func backendServer() { + dlog.Printf("backend server running") + go orphancheck() + shrinker := new(Shrinker) + srv := rpc.NewServer() + err := srv.Register(shrinker) + if err != nil { + elog.Panicf("unable to register shrinker: %s", err) + } + + sockname := backendSockname() + err = os.Remove(sockname) + if err != nil && !os.IsNotExist(err) { + elog.Panicf("unable to unlink socket: %s", err) + } + + lis, err := net.Listen("unix", sockname) + if err != nil { + elog.Panicf("unable to register shrinker: %s", err) + } + err = setLimits() + if err != nil { + elog.Printf("error setting backend limits: %s", err) + } + for _, h := range backendhooks { + h() + } + srv.Accept(lis) +} + +func runBackendServer() { + r, w, err := os.Pipe() + if err != nil { + elog.Panicf("can't pipe: %s", err) + } + proc := exec.Command(os.Args[0], reexecArgs("backend")...) + proc.Stdout = os.Stdout + proc.Stderr = os.Stderr + proc.Stdin = r + err = proc.Start() + if err != nil { + elog.Panicf("can't exec backend: %s", err) + } + go func() { + proc.Wait() + elog.Printf("lost the backend: %s", err) + w.Close() + }() +}
A backupdb.go

@@ -0,0 +1,196 @@

+package main + +import ( + "database/sql" + "fmt" + "os" + "time" + + "strings" +) + +func qordie(db *sql.DB, s string, args ...interface{}) *sql.Rows { + rows, err := db.Query(s, args...) + if err != nil { + elog.Fatalf("can't query %s: %s", s, err) + } + return rows +} + +func scanordie(rows *sql.Rows, args ...interface{}) { + err := rows.Scan(args...) + if err != nil { + elog.Fatalf("can't scan: %s", err) + } +} + +func svalbard(dirname string) { + err := os.Mkdir(dirname, 0700) + if err != nil && !os.IsExist(err) { + elog.Fatalf("can't create directory: %s", dirname) + } + now := time.Now().Unix() + backupdbname := fmt.Sprintf("%s/honk-%d.db", dirname, now) + backup, err := sql.Open("sqlite3", backupdbname) + if err != nil { + elog.Fatalf("can't open backup database") + } + for _, line := range strings.Split(sqlSchema, ";") { + _, err = backup.Exec(line) + if err != nil { + elog.Fatal(err) + return + } + } + tx, err := backup.Begin() + if err != nil { + elog.Fatal(err) + } + orig := opendatabase() + rows := qordie(orig, "select userid, username, hash, displayname, about, pubkey, seckey, options from users") + for rows.Next() { + var userid int64 + var username, hash, displayname, about, pubkey, seckey, options string + scanordie(rows, &userid, &username, &hash, &displayname, &about, &pubkey, &seckey, &options) + doordie(tx, "insert into users (userid, username, hash, displayname, about, pubkey, seckey, options) values (?, ?, ?, ?, ?, ?, ?, ?)", userid, username, hash, displayname, about, pubkey, seckey, options) + } + rows.Close() + + rows = qordie(orig, "select honkerid, userid, name, xid, flavor, combos, owner, meta, folxid from honkers") + for rows.Next() { + var honkerid, userid int64 + var name, xid, flavor, combos, owner, meta, folxid string + scanordie(rows, &honkerid, &userid, &name, &xid, &flavor, &combos, &owner, &meta, &folxid) + doordie(tx, "insert into honkers (honkerid, userid, name, xid, flavor, combos, owner, meta, folxid) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", honkerid, userid, name, xid, flavor, combos, owner, meta, folxid) + } + rows.Close() + + rows = qordie(orig, "select convoy from honks where flags & 4 or whofore = 2 or whofore = 3") + convoys := make(map[string]bool) + for rows.Next() { + var convoy string + scanordie(rows, &convoy) + convoys[convoy] = true + } + rows.Close() + + honkids := make(map[int64]bool) + for c := range convoys { + rows = qordie(orig, "select honkid, userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags from honks where convoy = ?", c) + for rows.Next() { + var honkid, userid int64 + var what, honker, xid, rid, dt, url, audience, noise, convoy string + var whofore int64 + var format, precis, oonker string + var flags int64 + scanordie(rows, &honkid, &userid, &what, &honker, &xid, &rid, &dt, &url, &audience, &noise, &convoy, &whofore, &format, &precis, &oonker, &flags) + honkids[honkid] = true + doordie(tx, "insert into honks (honkid, userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", honkid, userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags) + } + rows.Close() + } + fileids := make(map[int64]bool) + for h := range honkids { + rows = qordie(orig, "select honkid, chonkid, fileid from donks where honkid = ?", h) + for rows.Next() { + var honkid, chonkid, fileid int64 + scanordie(rows, &honkid, &chonkid, &fileid) + fileids[fileid] = true + doordie(tx, "insert into donks (honkid, chonkid, fileid) values (?, ?, ?)", honkid, chonkid, fileid) + } + rows.Close() + rows = qordie(orig, "select ontology, honkid from onts where honkid = ?", h) + for rows.Next() { + var ontology string + var honkid int64 + scanordie(rows, &ontology, &honkid) + doordie(tx, "insert into onts (ontology, honkid) values (?, ?)", ontology, honkid) + } + rows.Close() + rows = qordie(orig, "select honkid, genus, json from honkmeta where honkid = ?", h) + for rows.Next() { + var honkid int64 + var genus, json string + scanordie(rows, &honkid, &genus, &json) + doordie(tx, "insert into honkmeta (honkid, genus, json) values (?, ?, ?)", honkid, genus, json) + } + rows.Close() + } + chonkids := make(map[int64]bool) + rows = qordie(orig, "select chonkid, userid, xid, who, target, dt, noise, format from chonks") + for rows.Next() { + var chonkid, userid int64 + var xid, who, target, dt, noise, format string + scanordie(rows, &chonkid, &userid, &xid, &who, &target, &dt, &noise, &format) + chonkids[chonkid] = true + doordie(tx, "insert into chonks (chonkid, userid, xid, who, target, dt, noise, format) values (?, ?, ?, ?, ?, ?, ?, ?)", chonkid, userid, xid, who, target, dt, noise, format) + } + rows.Close() + for c := range chonkids { + rows = qordie(orig, "select honkid, chonkid, fileid from donks where chonkid = ?", c) + for rows.Next() { + var honkid, chonkid, fileid int64 + scanordie(rows, &honkid, &chonkid, &fileid) + fileids[fileid] = true + doordie(tx, "insert into donks (honkid, chonkid, fileid) values (?, ?, ?)", honkid, chonkid, fileid) + } + rows.Close() + } + filexids := make(map[string]bool) + for f := range fileids { + rows = qordie(orig, "select fileid, xid, name, description, url, media, local from filemeta where fileid = ?", f) + for rows.Next() { + var fileid int64 + var xid, name, description, url, media string + var local int64 + scanordie(rows, &fileid, &xid, &name, &description, &url, &media, &local) + filexids[xid] = true + doordie(tx, "insert into filemeta (fileid, xid, name, description, url, media, local) values (?, ?, ?, ?, ?, ?, ?)", fileid, xid, name, description, url, media, local) + } + rows.Close() + } + + rows = qordie(orig, "select key, value from config") + for rows.Next() { + var key string + var value interface{} + scanordie(rows, &key, &value) + doordie(tx, "insert into config (key, value) values (?, ?)", key, value) + } + + err = tx.Commit() + if err != nil { + elog.Fatalf("can't commit backp: %s", err) + } + backup.Close() + + backupblobname := fmt.Sprintf("%s/blob-%d.db", dirname, now) + blob, err := sql.Open("sqlite3", backupblobname) + if err != nil { + elog.Fatalf("can't open backup blob database") + } + doordie(blob, "create table filedata (xid text, media text, hash text, content blob)") + doordie(blob, "create index idx_filexid on filedata(xid)") + doordie(blob, "create index idx_filehash on filedata(hash)") + tx, err = blob.Begin() + if err != nil { + elog.Fatalf("can't start transaction: %s", err) + } + origblob := openblobdb() + for x := range filexids { + rows = qordie(origblob, "select xid, media, hash, content from filedata where xid = ?", x) + for rows.Next() { + var xid, media, hash string + var content sql.RawBytes + scanordie(rows, &xid, &media, &hash, &content) + doordie(tx, "insert into filedata (xid, media, hash, content) values (?, ?, ?, ?)", xid, media, hash, content) + } + rows.Close() + } + + err = tx.Commit() + if err != nil { + elog.Fatalf("can't commit blobs: %s", err) + } + blob.Close() +}
A bloat.go

@@ -0,0 +1,67 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "net/http" + "strings" + + "humungus.tedunangst.com/r/webs/junk" +) + +func servewonkles(w http.ResponseWriter, r *http.Request) { + url := r.FormValue("w") + dlog.Printf("getting wordlist: %s", url) + wonkles := getxonker(url, "wonkles") + if wonkles == "" { + wonkles = savewonkles(url) + if wonkles == "" { + http.NotFound(w, r) + return + } + } + var words []string + for _, l := range strings.Split(wonkles, "\n") { + words = append(words, l) + } + if !develMode { + w.Header().Set("Cache-Control", "max-age=7776000") + } + + j := junk.New() + j["wordlist"] = words + j.Write(w) +} + +func savewonkles(url string) string { + w := getxonker(url, "wonkles") + if w != "" { + return w + } + ilog.Printf("fetching wonkles: %s", url) + res, err := fetchsome(url) + if err != nil { + ilog.Printf("error fetching wonkles: %s", err) + return "" + } + w = getxonker(url, "wonkles") + if w != "" { + return w + } + w = string(res) + savexonker(url, w, "wonkles", "") + return w +}
A database.go

@@ -0,0 +1,1197 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "bytes" + "crypto/sha512" + "database/sql" + _ "embed" + "encoding/json" + "fmt" + "html/template" + "sort" + "strconv" + "strings" + "sync" + "time" + + "humungus.tedunangst.com/r/webs/cache" + "humungus.tedunangst.com/r/webs/httpsig" + "humungus.tedunangst.com/r/webs/login" + "humungus.tedunangst.com/r/webs/mz" +) + +//go:embed schema.sql +var sqlSchema string + +func userfromrow(row *sql.Row) (*WhatAbout, error) { + user := new(WhatAbout) + var seckey, options string + err := row.Scan(&user.ID, &user.Name, &user.Display, &user.About, &user.Key, &seckey, &options) + if err == nil { + user.SecKey, _, err = httpsig.DecodeKey(seckey) + } + if err != nil { + return nil, err + } + if user.ID > 0 { + user.URL = fmt.Sprintf("https://%s/%s/%s", serverName, userSep, user.Name) + err = unjsonify(options, &user.Options) + if err != nil { + elog.Printf("error processing user options: %s", err) + } + } else { + user.URL = fmt.Sprintf("https://%s/%s", serverName, user.Name) + } + if user.Options.Reaction == "" { + user.Options.Reaction = "none" + } + + return user, nil +} + +var somenamedusers = cache.New(cache.Options{Filler: func(name string) (*WhatAbout, bool) { + row := stmtUserByName.QueryRow(name) + user, err := userfromrow(row) + if err != nil { + return nil, false + } + var marker mz.Marker + marker.HashLinker = ontoreplacer + marker.AtLinker = attoreplacer + user.HTAbout = template.HTML(marker.Mark(user.About)) + user.Onts = marker.HashTags + return user, true +}}) + +var somenumberedusers = cache.New(cache.Options{Filler: func(userid int64) (*WhatAbout, bool) { + row := stmtUserByNumber.QueryRow(userid) + user, err := userfromrow(row) + if err != nil { + return nil, false + } + // don't touch attoreplacer, which introduces a loop + // finger -> getjunk -> keys -> users + return user, true +}}) + +func getserveruser() *WhatAbout { + var user *WhatAbout + ok := somenumberedusers.Get(serverUID, &user) + if !ok { + elog.Panicf("lost server user") + } + return user +} + +func butwhatabout(name string) (*WhatAbout, error) { + var user *WhatAbout + ok := somenamedusers.Get(name, &user) + if !ok { + return nil, fmt.Errorf("no user: %s", name) + } + return user, nil +} + +var honkerinvalidator cache.Invalidator + +func gethonkers(userid int64) []*Honker { + rows, err := stmtHonkers.Query(userid) + if err != nil { + elog.Printf("error querying honkers: %s", err) + return nil + } + defer rows.Close() + var honkers []*Honker + for rows.Next() { + h := new(Honker) + var combos, meta string + err = rows.Scan(&h.ID, &h.UserID, &h.Name, &h.XID, &h.Flavor, &combos, &meta) + if err == nil { + err = unjsonify(meta, &h.Meta) + } + if err != nil { + elog.Printf("error scanning honker: %s", err) + continue + } + h.Combos = strings.Split(strings.TrimSpace(combos), " ") + honkers = append(honkers, h) + } + return honkers +} + +func getdubs(userid int64) []*Honker { + rows, err := stmtDubbers.Query(userid) + return dubsfromrows(rows, err) +} + +func getnameddubs(userid int64, name string) []*Honker { + rows, err := stmtNamedDubbers.Query(userid, name) + return dubsfromrows(rows, err) +} + +func dubsfromrows(rows *sql.Rows, err error) []*Honker { + if err != nil { + elog.Printf("error querying dubs: %s", err) + return nil + } + defer rows.Close() + var honkers []*Honker + for rows.Next() { + h := new(Honker) + err = rows.Scan(&h.ID, &h.UserID, &h.Name, &h.XID, &h.Flavor) + if err != nil { + elog.Printf("error scanning honker: %s", err) + return nil + } + honkers = append(honkers, h) + } + return honkers +} + +func allusers() []login.UserInfo { + var users []login.UserInfo + rows, _ := opendatabase().Query("select userid, username from users where userid > 0") + defer rows.Close() + for rows.Next() { + var u login.UserInfo + rows.Scan(&u.UserID, &u.Username) + users = append(users, u) + } + return users +} + +func getxonk(userid int64, xid string) *Honk { + row := stmtOneXonk.QueryRow(userid, xid) + return scanhonk(row) +} + +func getbonk(userid int64, xid string) *Honk { + row := stmtOneBonk.QueryRow(userid, xid) + return scanhonk(row) +} + +func getpublichonks() []*Honk { + dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + rows, err := stmtPublicHonks.Query(dt, 100) + return getsomehonks(rows, err) +} +func geteventhonks(userid int64) []*Honk { + rows, err := stmtEventHonks.Query(userid, 25) + honks := getsomehonks(rows, err) + sort.Slice(honks, func(i, j int) bool { + var t1, t2 time.Time + if honks[i].Time == nil { + t1 = honks[i].Date + } else { + t1 = honks[i].Time.StartTime + } + if honks[j].Time == nil { + t2 = honks[j].Date + } else { + t2 = honks[j].Time.StartTime + } + return t1.After(t2) + }) + now := time.Now().Add(-24 * time.Hour) + for i, h := range honks { + t := h.Date + if tm := h.Time; tm != nil { + t = tm.StartTime + } + if t.Before(now) { + honks = honks[:i] + break + } + } + reversehonks(honks) + return honks +} +func gethonksbyuser(name string, includeprivate bool, wanted int64) []*Honk { + dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + limit := 50 + whofore := 2 + if includeprivate { + whofore = 3 + } + rows, err := stmtUserHonks.Query(wanted, whofore, name, dt, limit) + return getsomehonks(rows, err) +} +func gethonksforuser(userid int64, wanted int64) []*Honk { + dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + rows, err := stmtHonksForUser.Query(wanted, userid, dt, userid, userid) + return getsomehonks(rows, err) +} +func gethonksforuserfirstclass(userid int64, wanted int64) []*Honk { + dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + rows, err := stmtHonksForUserFirstClass.Query(wanted, userid, dt, userid, userid) + return getsomehonks(rows, err) +} + +func gethonksforme(userid int64, wanted int64) []*Honk { + dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + rows, err := stmtHonksForMe.Query(wanted, userid, dt, userid) + return getsomehonks(rows, err) +} +func gethonksfromlongago(userid int64, wanted int64) []*Honk { + now := time.Now() + var honks []*Honk + for i := 1; i <= 3; i++ { + dt := time.Date(now.Year()-i, now.Month(), now.Day(), now.Hour(), now.Minute(), + now.Second(), 0, now.Location()) + dt1 := dt.Add(-36 * time.Hour).UTC().Format(dbtimeformat) + dt2 := dt.Add(12 * time.Hour).UTC().Format(dbtimeformat) + rows, err := stmtHonksFromLongAgo.Query(wanted, userid, dt1, dt2, userid) + honks = append(honks, getsomehonks(rows, err)...) + } + return honks +} +func getsavedhonks(userid int64, wanted int64) []*Honk { + rows, err := stmtHonksISaved.Query(wanted, userid) + return getsomehonks(rows, err) +} +func gethonksbyhonker(userid int64, honker string, wanted int64) []*Honk { + rows, err := stmtHonksByHonker.Query(wanted, userid, honker, userid) + return getsomehonks(rows, err) +} +func gethonksbyxonker(userid int64, xonker string, wanted int64) []*Honk { + rows, err := stmtHonksByXonker.Query(wanted, userid, xonker, xonker, userid) + return getsomehonks(rows, err) +} +func gethonksbycombo(userid int64, combo string, wanted int64) []*Honk { + combo = "% " + combo + " %" + rows, err := stmtHonksByCombo.Query(wanted, userid, userid, combo, userid, wanted, userid, combo, userid) + return getsomehonks(rows, err) +} +func gethonksbyconvoy(userid int64, convoy string, wanted int64) []*Honk { + rows, err := stmtHonksByConvoy.Query(wanted, userid, userid, convoy) + honks := getsomehonks(rows, err) + return honks +} +func gethonksbysearch(userid int64, q string, wanted int64) []*Honk { + var queries []string + var params []interface{} + queries = append(queries, "honks.honkid > ?") + params = append(params, wanted) + queries = append(queries, "honks.userid = ?") + params = append(params, userid) + + terms := strings.Split(q, " ") + for _, t := range terms { + if t == "" { + continue + } + negate := " " + if t[0] == '-' { + t = t[1:] + negate = " not " + } + if t == "" { + continue + } + if strings.HasPrefix(t, "site:") { + site := t[5:] + site = "%" + site + "%" + queries = append(queries, "xid"+negate+"like ?") + params = append(params, site) + continue + } + if strings.HasPrefix(t, "honker:") { + honker := t[7:] + xid := fullname(honker, userid) + if xid != "" { + honker = xid + } + queries = append(queries, negate+"(honks.honker = ? or honks.oonker = ?)") + params = append(params, honker) + params = append(params, honker) + continue + } + t = "%" + t + "%" + queries = append(queries, "noise"+negate+"like ?") + params = append(params, t) + } + + selecthonks := "select honks.honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, format, convoy, whofore, flags from honks join users on honks.userid = users.userid " + where := "where " + strings.Join(queries, " and ") + butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)" + limit := " order by honks.honkid desc limit 250" + params = append(params, userid) + rows, err := opendatabase().Query(selecthonks+where+butnotthose+limit, params...) + honks := getsomehonks(rows, err) + return honks +} +func gethonksbyontology(userid int64, name string, wanted int64) []*Honk { + rows, err := stmtHonksByOntology.Query(wanted, name, userid, userid) + honks := getsomehonks(rows, err) + return honks +} + +func reversehonks(honks []*Honk) { + for i, j := 0, len(honks)-1; i < j; i, j = i+1, j-1 { + honks[i], honks[j] = honks[j], honks[i] + } +} + +func getsomehonks(rows *sql.Rows, err error) []*Honk { + if err != nil { + elog.Printf("error querying honks: %s", err) + return nil + } + defer rows.Close() + var honks []*Honk + for rows.Next() { + h := scanhonk(rows) + if h != nil { + honks = append(honks, h) + } + } + rows.Close() + donksforhonks(honks) + return honks +} + +type RowLike interface { + Scan(dest ...interface{}) error +} + +func scanhonk(row RowLike) *Honk { + h := new(Honk) + var dt, aud string + err := row.Scan(&h.ID, &h.UserID, &h.Username, &h.What, &h.Honker, &h.Oonker, &h.XID, &h.RID, + &dt, &h.URL, &aud, &h.Noise, &h.Precis, &h.Format, &h.Convoy, &h.Whofore, &h.Flags) + if err != nil { + if err != sql.ErrNoRows { + elog.Printf("error scanning honk: %s", err) + } + return nil + } + h.Date, _ = time.Parse(dbtimeformat, dt) + h.Audience = strings.Split(aud, " ") + h.Public = loudandproud(h.Audience) + return h +} + +func donksforhonks(honks []*Honk) { + db := opendatabase() + var ids []string + hmap := make(map[int64]*Honk) + for _, h := range honks { + ids = append(ids, fmt.Sprintf("%d", h.ID)) + hmap[h.ID] = h + } + idset := strings.Join(ids, ",") + // grab donks + q := fmt.Sprintf("select honkid, donks.fileid, xid, name, description, url, media, local from donks join filemeta on donks.fileid = filemeta.fileid where honkid in (%s)", idset) + rows, err := db.Query(q) + if err != nil { + elog.Printf("error querying donks: %s", err) + return + } + defer rows.Close() + for rows.Next() { + var hid int64 + d := new(Donk) + err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.Desc, &d.URL, &d.Media, &d.Local) + if err != nil { + elog.Printf("error scanning donk: %s", err) + continue + } + d.External = !strings.HasPrefix(d.URL, serverPrefix) + h := hmap[hid] + h.Donks = append(h.Donks, d) + } + rows.Close() + + // grab onts + q = fmt.Sprintf("select honkid, ontology from onts where honkid in (%s)", idset) + rows, err = db.Query(q) + if err != nil { + elog.Printf("error querying onts: %s", err) + return + } + defer rows.Close() + for rows.Next() { + var hid int64 + var o string + err = rows.Scan(&hid, &o) + if err != nil { + elog.Printf("error scanning donk: %s", err) + continue + } + h := hmap[hid] + h.Onts = append(h.Onts, o) + } + rows.Close() + + // grab meta + q = fmt.Sprintf("select honkid, genus, json from honkmeta where honkid in (%s)", idset) + rows, err = db.Query(q) + if err != nil { + elog.Printf("error querying honkmeta: %s", err) + return + } + defer rows.Close() + for rows.Next() { + var hid int64 + var genus, j string + err = rows.Scan(&hid, &genus, &j) + if err != nil { + elog.Printf("error scanning honkmeta: %s", err) + continue + } + h := hmap[hid] + switch genus { + case "place": + p := new(Place) + err = unjsonify(j, p) + if err != nil { + elog.Printf("error parsing place: %s", err) + continue + } + h.Place = p + case "time": + t := new(Time) + err = unjsonify(j, t) + if err != nil { + elog.Printf("error parsing time: %s", err) + continue + } + h.Time = t + case "mentions": + err = unjsonify(j, &h.Mentions) + if err != nil { + elog.Printf("error parsing mentions: %s", err) + continue + } + case "badonks": + err = unjsonify(j, &h.Badonks) + if err != nil { + elog.Printf("error parsing badonks: %s", err) + continue + } + case "wonkles": + h.Wonkles = j + case "guesses": + h.Guesses = template.HTML(j) + case "oldrev": + default: + elog.Printf("unknown meta genus: %s", genus) + } + } + rows.Close() +} + +func donksforchonks(chonks []*Chonk) { + db := opendatabase() + var ids []string + chmap := make(map[int64]*Chonk) + for _, ch := range chonks { + ids = append(ids, fmt.Sprintf("%d", ch.ID)) + chmap[ch.ID] = ch + } + idset := strings.Join(ids, ",") + // grab donks + q := fmt.Sprintf("select chonkid, donks.fileid, xid, name, description, url, media, local from donks join filemeta on donks.fileid = filemeta.fileid where chonkid in (%s)", idset) + rows, err := db.Query(q) + if err != nil { + elog.Printf("error querying donks: %s", err) + return + } + defer rows.Close() + for rows.Next() { + var chid int64 + d := new(Donk) + err = rows.Scan(&chid, &d.FileID, &d.XID, &d.Name, &d.Desc, &d.URL, &d.Media, &d.Local) + if err != nil { + elog.Printf("error scanning donk: %s", err) + continue + } + ch := chmap[chid] + ch.Donks = append(ch.Donks, d) + } +} + +func savefile(name string, desc string, url string, media string, local bool, data []byte) (int64, error) { + fileid, _, err := savefileandxid(name, desc, url, media, local, data) + return fileid, err +} + +func hashfiledata(data []byte) string { + h := sha512.New512_256() + h.Write(data) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func savefileandxid(name string, desc string, url string, media string, local bool, data []byte) (int64, string, error) { + var xid string + if local { + hash := hashfiledata(data) + row := stmtCheckFileData.QueryRow(hash) + err := row.Scan(&xid) + if err == sql.ErrNoRows { + xid = xfiltrate() + switch media { + case "image/png": + xid += ".png" + case "image/jpeg": + xid += ".jpg" + case "application/pdf": + xid += ".pdf" + case "text/plain": + xid += ".txt" + } + _, err = stmtSaveFileData.Exec(xid, media, hash, data) + if err != nil { + return 0, "", err + } + } else if err != nil { + elog.Printf("error checking file hash: %s", err) + return 0, "", err + } + if url == "" { + url = fmt.Sprintf("https://%s/d/%s", serverName, xid) + } + } + + res, err := stmtSaveFile.Exec(xid, name, desc, url, media, local) + if err != nil { + return 0, "", err + } + fileid, _ := res.LastInsertId() + return fileid, xid, nil +} + +func finddonk(url string) *Donk { + donk := new(Donk) + row := stmtFindFile.QueryRow(url) + err := row.Scan(&donk.FileID, &donk.XID) + if err == nil { + return donk + } + if err != sql.ErrNoRows { + elog.Printf("error finding file: %s", err) + } + return nil +} + +func savechonk(ch *Chonk) error { + dt := ch.Date.UTC().Format(dbtimeformat) + db := opendatabase() + tx, err := db.Begin() + if err != nil { + elog.Printf("can't begin tx: %s", err) + return err + } + + res, err := tx.Stmt(stmtSaveChonk).Exec(ch.UserID, ch.XID, ch.Who, ch.Target, dt, ch.Noise, ch.Format) + if err == nil { + ch.ID, _ = res.LastInsertId() + for _, d := range ch.Donks { + _, err := tx.Stmt(stmtSaveDonk).Exec(-1, ch.ID, d.FileID) + if err != nil { + elog.Printf("error saving donk: %s", err) + break + } + } + chatplusone(tx, ch.UserID) + err = tx.Commit() + } else { + tx.Rollback() + } + return err +} + +func chatplusone(tx *sql.Tx, userid int64) { + var user *WhatAbout + ok := somenumberedusers.Get(userid, &user) + if !ok { + return + } + options := user.Options + options.ChatCount += 1 + j, err := jsonify(options) + if err == nil { + _, err = tx.Exec("update users set options = ? where username = ?", j, user.Name) + } + if err != nil { + elog.Printf("error plussing chat: %s", err) + } + somenamedusers.Clear(user.Name) + somenumberedusers.Clear(user.ID) +} + +func chatnewnone(userid int64) { + var user *WhatAbout + ok := somenumberedusers.Get(userid, &user) + if !ok || user.Options.ChatCount == 0 { + return + } + options := user.Options + options.ChatCount = 0 + j, err := jsonify(options) + if err == nil { + db := opendatabase() + _, err = db.Exec("update users set options = ? where username = ?", j, user.Name) + } + if err != nil { + elog.Printf("error noneing chat: %s", err) + } + somenamedusers.Clear(user.Name) + somenumberedusers.Clear(user.ID) +} + +func meplusone(tx *sql.Tx, userid int64) { + var user *WhatAbout + ok := somenumberedusers.Get(userid, &user) + if !ok { + return + } + options := user.Options + options.MeCount += 1 + j, err := jsonify(options) + if err == nil { + _, err = tx.Exec("update users set options = ? where username = ?", j, user.Name) + } + if err != nil { + elog.Printf("error plussing me: %s", err) + } + somenamedusers.Clear(user.Name) + somenumberedusers.Clear(user.ID) +} + +func menewnone(userid int64) { + var user *WhatAbout + ok := somenumberedusers.Get(userid, &user) + if !ok || user.Options.MeCount == 0 { + return + } + options := user.Options + options.MeCount = 0 + j, err := jsonify(options) + if err == nil { + db := opendatabase() + _, err = db.Exec("update users set options = ? where username = ?", j, user.Name) + } + if err != nil { + elog.Printf("error noneing me: %s", err) + } + somenamedusers.Clear(user.Name) + somenumberedusers.Clear(user.ID) +} + +func loadchatter(userid int64) []*Chatter { + duedt := time.Now().Add(-3 * 24 * time.Hour).UTC().Format(dbtimeformat) + rows, err := stmtLoadChonks.Query(userid, duedt) + if err != nil { + elog.Printf("error loading chonks: %s", err) + return nil + } + defer rows.Close() + chonks := make(map[string][]*Chonk) + var allchonks []*Chonk + for rows.Next() { + ch := new(Chonk) + var dt string + err = rows.Scan(&ch.ID, &ch.UserID, &ch.XID, &ch.Who, &ch.Target, &dt, &ch.Noise, &ch.Format) + if err != nil { + elog.Printf("error scanning chonk: %s", err) + continue + } + ch.Date, _ = time.Parse(dbtimeformat, dt) + chonks[ch.Target] = append(chonks[ch.Target], ch) + allchonks = append(allchonks, ch) + } + donksforchonks(allchonks) + rows.Close() + rows, err = stmtGetChatters.Query(userid) + if err != nil { + elog.Printf("error getting chatters: %s", err) + return nil + } + for rows.Next() { + var target string + err = rows.Scan(&target) + if err != nil { + elog.Printf("error scanning chatter: %s", target) + continue + } + if _, ok := chonks[target]; !ok { + chonks[target] = []*Chonk{} + + } + } + var chatter []*Chatter + for target, chonks := range chonks { + chatter = append(chatter, &Chatter{ + Target: target, + Chonks: chonks, + }) + } + sort.Slice(chatter, func(i, j int) bool { + a, b := chatter[i], chatter[j] + if len(a.Chonks) == 0 || len(b.Chonks) == 0 { + if len(a.Chonks) == len(b.Chonks) { + return a.Target < b.Target + } + return len(a.Chonks) > len(b.Chonks) + } + return a.Chonks[len(a.Chonks)-1].Date.After(b.Chonks[len(b.Chonks)-1].Date) + }) + + return chatter +} + +func savehonk(h *Honk) error { + dt := h.Date.UTC().Format(dbtimeformat) + aud := strings.Join(h.Audience, " ") + + db := opendatabase() + tx, err := db.Begin() + if err != nil { + elog.Printf("can't begin tx: %s", err) + return err + } + + res, err := tx.Stmt(stmtSaveHonk).Exec(h.UserID, h.What, h.Honker, h.XID, h.RID, dt, h.URL, + aud, h.Noise, h.Convoy, h.Whofore, h.Format, h.Precis, + h.Oonker, h.Flags) + if err == nil { + h.ID, _ = res.LastInsertId() + err = saveextras(tx, h) + } + if err == nil { + if h.Whofore == 1 { + meplusone(tx, h.UserID) + } + err = tx.Commit() + } else { + tx.Rollback() + } + if err != nil { + elog.Printf("error saving honk: %s", err) + } + honkhonkline() + return err +} + +func updatehonk(h *Honk) error { + old := getxonk(h.UserID, h.XID) + oldrev := OldRevision{Precis: old.Precis, Noise: old.Noise} + dt := h.Date.UTC().Format(dbtimeformat) + + db := opendatabase() + tx, err := db.Begin() + if err != nil { + elog.Printf("can't begin tx: %s", err) + return err + } + + err = deleteextras(tx, h.ID, false) + if err == nil { + _, err = tx.Stmt(stmtUpdateHonk).Exec(h.Precis, h.Noise, h.Format, h.Whofore, dt, h.ID) + } + if err == nil { + err = saveextras(tx, h) + } + if err == nil { + var j string + j, err = jsonify(&oldrev) + if err == nil { + _, err = tx.Stmt(stmtSaveMeta).Exec(old.ID, "oldrev", j) + } + if err != nil { + elog.Printf("error saving oldrev: %s", err) + } + } + if err == nil { + err = tx.Commit() + } else { + tx.Rollback() + } + if err != nil { + elog.Printf("error updating honk %d: %s", h.ID, err) + } + return err +} + +func deletehonk(honkid int64) error { + db := opendatabase() + tx, err := db.Begin() + if err != nil { + elog.Printf("can't begin tx: %s", err) + return err + } + + err = deleteextras(tx, honkid, true) + if err == nil { + _, err = tx.Stmt(stmtDeleteHonk).Exec(honkid) + } + if err == nil { + err = tx.Commit() + } else { + tx.Rollback() + } + if err != nil { + elog.Printf("error deleting honk %d: %s", honkid, err) + } + return err +} + +func saveextras(tx *sql.Tx, h *Honk) error { + for _, d := range h.Donks { + _, err := tx.Stmt(stmtSaveDonk).Exec(h.ID, -1, d.FileID) + if err != nil { + elog.Printf("error saving donk: %s", err) + return err + } + } + for _, o := range h.Onts { + _, err := tx.Stmt(stmtSaveOnt).Exec(strings.ToLower(o), h.ID) + if err != nil { + elog.Printf("error saving ont: %s", err) + return err + } + } + if p := h.Place; p != nil { + j, err := jsonify(p) + if err == nil { + _, err = tx.Stmt(stmtSaveMeta).Exec(h.ID, "place", j) + } + if err != nil { + elog.Printf("error saving place: %s", err) + return err + } + } + if t := h.Time; t != nil { + j, err := jsonify(t) + if err == nil { + _, err = tx.Stmt(stmtSaveMeta).Exec(h.ID, "time", j) + } + if err != nil { + elog.Printf("error saving time: %s", err) + return err + } + } + if m := h.Mentions; len(m) > 0 { + j, err := jsonify(m) + if err == nil { + _, err = tx.Stmt(stmtSaveMeta).Exec(h.ID, "mentions", j) + } + if err != nil { + elog.Printf("error saving mentions: %s", err) + return err + } + } + if w := h.Wonkles; w != "" { + _, err := tx.Stmt(stmtSaveMeta).Exec(h.ID, "wonkles", w) + if err != nil { + elog.Printf("error saving wonkles: %s", err) + return err + } + } + if g := h.Guesses; g != "" { + _, err := tx.Stmt(stmtSaveMeta).Exec(h.ID, "guesses", g) + if err != nil { + elog.Printf("error saving guesses: %s", err) + return err + } + } + return nil +} + +var baxonker sync.Mutex + +func addreaction(user *WhatAbout, xid string, who, react string) { + baxonker.Lock() + defer baxonker.Unlock() + h := getxonk(user.ID, xid) + if h == nil { + return + } + h.Badonks = append(h.Badonks, Badonk{Who: who, What: react}) + j, _ := jsonify(h.Badonks) + db := opendatabase() + tx, _ := db.Begin() + _, _ = tx.Stmt(stmtDeleteOneMeta).Exec(h.ID, "badonks") + _, _ = tx.Stmt(stmtSaveMeta).Exec(h.ID, "badonks", j) + tx.Commit() +} + +func deleteextras(tx *sql.Tx, honkid int64, everything bool) error { + _, err := tx.Stmt(stmtDeleteDonks).Exec(honkid) + if err != nil { + return err + } + _, err = tx.Stmt(stmtDeleteOnts).Exec(honkid) + if err != nil { + return err + } + if everything { + _, err = tx.Stmt(stmtDeleteAllMeta).Exec(honkid) + } else { + _, err = tx.Stmt(stmtDeleteSomeMeta).Exec(honkid) + } + if err != nil { + return err + } + return nil +} + +func jsonify(what interface{}) (string, error) { + var buf bytes.Buffer + e := json.NewEncoder(&buf) + e.SetEscapeHTML(false) + e.SetIndent("", "") + err := e.Encode(what) + return buf.String(), err +} + +func unjsonify(s string, dest interface{}) error { + d := json.NewDecoder(strings.NewReader(s)) + err := d.Decode(dest) + return err +} + +func getxonker(what, flav string) string { + var res string + row := stmtGetXonker.QueryRow(what, flav) + row.Scan(&res) + return res +} + +func savexonker(what, value, flav, when string) { + stmtSaveXonker.Exec(what, value, flav, when) +} + +func savehonker(user *WhatAbout, url, name, flavor, combos, mj string) error { + var owner string + if url[0] == '#' { + flavor = "peep" + if name == "" { + name = url[1:] + } + owner = url + } else { + info, err := investigate(url) + if err != nil { + ilog.Printf("failed to investigate honker: %s", err) + return err + } + url = info.XID + if name == "" { + name = info.Name + } + owner = info.Owner + } + + var x string + db := opendatabase() + row := db.QueryRow("select xid from honkers where xid = ? and userid = ? and flavor in ('sub', 'unsub', 'peep')", url, user.ID) + err := row.Scan(&x) + if err != sql.ErrNoRows { + if err != nil { + elog.Printf("honker scan err: %s", err) + } else { + err = fmt.Errorf("it seems you are already subscribed to them") + } + return err + } + + res, err := stmtSaveHonker.Exec(user.ID, name, url, flavor, combos, owner, mj) + if err != nil { + elog.Print(err) + return err + } + honkerid, _ := res.LastInsertId() + if flavor == "presub" { + followyou(user, honkerid) + } + return nil +} + +func cleanupdb(arg string) { + db := opendatabase() + days, err := strconv.Atoi(arg) + var sqlargs []interface{} + var where string + if err != nil { + honker := arg + expdate := time.Now().Add(-3 * 24 * time.Hour).UTC().Format(dbtimeformat) + where = "dt < ? and honker = ?" + sqlargs = append(sqlargs, expdate) + sqlargs = append(sqlargs, honker) + } else { + expdate := time.Now().Add(-time.Duration(days) * 24 * time.Hour).UTC().Format(dbtimeformat) + where = "dt < ? and convoy not in (select convoy from honks where flags & 4 or whofore = 2 or whofore = 3)" + sqlargs = append(sqlargs, expdate) + } + doordie(db, "delete from honks where flags & 4 = 0 and whofore = 0 and "+where, sqlargs...) + doordie(db, "delete from donks where honkid > 0 and honkid not in (select honkid from honks)") + doordie(db, "delete from onts where honkid not in (select honkid from honks)") + doordie(db, "delete from honkmeta where honkid not in (select honkid from honks)") + + doordie(db, "delete from filemeta where fileid not in (select fileid from donks)") + for _, u := range allusers() { + 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) + } + + filexids := make(map[string]bool) + blobdb := openblobdb() + rows, err := blobdb.Query("select xid from filedata") + if err != nil { + elog.Fatal(err) + } + for rows.Next() { + var xid string + err = rows.Scan(&xid) + if err != nil { + elog.Fatal(err) + } + filexids[xid] = true + } + rows.Close() + rows, err = db.Query("select xid from filemeta") + for rows.Next() { + var xid string + err = rows.Scan(&xid) + if err != nil { + elog.Fatal(err) + } + delete(filexids, xid) + } + rows.Close() + tx, err := blobdb.Begin() + if err != nil { + elog.Fatal(err) + } + for xid, _ := range filexids { + _, err = tx.Exec("delete from filedata where xid = ?", xid) + if err != nil { + elog.Fatal(err) + } + } + err = tx.Commit() + if err != nil { + elog.Fatal(err) + } +} + +var stmtHonkers, stmtDubbers, stmtNamedDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateHonker *sql.Stmt +var stmtDeleteHonker *sql.Stmt +var stmtAnyXonk, stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt +var stmtHonksByOntology, stmtHonksForUser, stmtHonksForMe, stmtSaveDub, stmtHonksByXonker *sql.Stmt +var stmtHonksFromLongAgo *sql.Stmt +var stmtHonksByHonker, stmtSaveHonk, stmtUserByName, stmtUserByNumber *sql.Stmt +var stmtEventHonks, stmtOneBonk, stmtFindZonk, stmtFindXonk, stmtSaveDonk *sql.Stmt +var stmtFindFile, stmtGetFileData, stmtSaveFileData, stmtSaveFile *sql.Stmt +var stmtCheckFileData *sql.Stmt +var stmtAddDoover, stmtGetDoovers, stmtLoadDoover, stmtZapDoover, stmtOneHonker *sql.Stmt +var stmtUntagged, stmtDeleteHonk, stmtDeleteDonks, stmtDeleteOnts, stmtSaveZonker *sql.Stmt +var stmtGetZonkers, stmtRecentHonkers, stmtGetXonker, stmtSaveXonker, stmtDeleteXonker, stmtDeleteOldXonkers *sql.Stmt +var stmtAllOnts, stmtSaveOnt, stmtUpdateFlags, stmtClearFlags *sql.Stmt +var stmtHonksForUserFirstClass *sql.Stmt +var stmtSaveMeta, stmtDeleteAllMeta, stmtDeleteOneMeta, stmtDeleteSomeMeta, stmtUpdateHonk *sql.Stmt +var stmtHonksISaved, stmtGetFilters, stmtSaveFilter, stmtDeleteFilter *sql.Stmt +var stmtGetTracks *sql.Stmt +var stmtSaveChonk, stmtLoadChonks, stmtGetChatters *sql.Stmt + +func preparetodie(db *sql.DB, s string) *sql.Stmt { + stmt, err := db.Prepare(s) + if err != nil { + elog.Fatalf("error %s: %s", err, s) + } + return stmt +} + +func prepareStatements(db *sql.DB) { + stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos, meta from honkers where userid = ? and (flavor = 'presub' or flavor = 'sub' or flavor = 'peep' or flavor = 'unsub') order by name") + stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos, owner, meta, folxid) values (?, ?, ?, ?, ?, ?, ?, '')") + stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ?, folxid = ? where userid = ? and name = ? and xid = ? and flavor = ?") + stmtUpdateHonker = preparetodie(db, "update honkers set name = ?, combos = ?, meta = ? where honkerid = ? and userid = ?") + stmtDeleteHonker = preparetodie(db, "delete from honkers where honkerid = ?") + stmtOneHonker = preparetodie(db, "select xid from honkers where name = ? and userid = ?") + stmtDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and flavor = 'dub'") + stmtNamedDubbers = preparetodie(db, "select honkerid, userid, name, xid, flavor from honkers where userid = ? and name = ? and flavor = 'dub'") + + selecthonks := "select honks.honkid, honks.userid, username, what, honker, oonker, honks.xid, rid, dt, url, audience, noise, precis, format, convoy, whofore, flags from honks join users on honks.userid = users.userid " + limit := " order by honks.honkid desc limit 250" + smalllimit := " order by honks.honkid desc limit ?" + butnotthose := " and convoy not in (select name from zonkers where userid = ? and wherefore = 'zonvoy' order by zonkerid desc limit 100)" + stmtOneXonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ?") + stmtAnyXonk = preparetodie(db, selecthonks+"where xid = ? order by honks.honkid asc") + stmtOneBonk = preparetodie(db, selecthonks+"where honks.userid = ? and xid = ? and what = 'bonk' and whofore = 2") + stmtPublicHonks = preparetodie(db, selecthonks+"where whofore = 2 and dt > ?"+smalllimit) + stmtEventHonks = preparetodie(db, selecthonks+"where (whofore = 2 or honks.userid = ?) and what = 'event'"+smalllimit) + stmtUserHonks = preparetodie(db, selecthonks+"where honks.honkid > ? and (whofore = 2 or whofore = ?) and username = ? and dt > ?"+smalllimit) + myhonkers := " and honker in (select xid from honkers where userid = ? and (flavor = 'sub' or flavor = 'peep' or flavor = 'presub') and combos not like '% - %')" + stmtHonksForUser = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ?"+myhonkers+butnotthose+limit) + stmtHonksForUserFirstClass = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ? and (what <> 'tonk')"+myhonkers+butnotthose+limit) + stmtHonksForMe = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit) + stmtHonksFromLongAgo = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ? and dt < ? and whofore = 2"+butnotthose+limit) + stmtHonksISaved = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and flags & 4 order by honks.honkid desc") + stmtHonksByHonker = preparetodie(db, selecthonks+"join honkers on (honkers.xid = honks.honker or honkers.xid = honks.oonker) where honks.honkid > ? and honks.userid = ? and honkers.name = ?"+butnotthose+limit) + stmtHonksByXonker = preparetodie(db, selecthonks+" where honks.honkid > ? and honks.userid = ? and (honker = ? or oonker = ?)"+butnotthose+limit) + stmtHonksByCombo = preparetodie(db, selecthonks+" where honks.honkid > ? and honks.userid = ? and honks.honker in (select xid from honkers where honkers.userid = ? and honkers.combos like ?) "+butnotthose+" union "+selecthonks+"join onts on honks.honkid = onts.honkid where honks.honkid > ? and honks.userid = ? and onts.ontology in (select xid from honkers where combos like ?)"+butnotthose+limit) + stmtHonksByConvoy = preparetodie(db, selecthonks+"where honks.honkid > ? and (honks.userid = ? or (? = -1 and whofore = 2)) and convoy = ?"+limit) + stmtHonksByOntology = preparetodie(db, selecthonks+"join onts on honks.honkid = onts.honkid where honks.honkid > ? and onts.ontology = ? and (honks.userid = ? or (? = -1 and honks.whofore = 2))"+limit) + + stmtSaveMeta = preparetodie(db, "insert into honkmeta (honkid, genus, json) values (?, ?, ?)") + stmtDeleteAllMeta = preparetodie(db, "delete from honkmeta where honkid = ?") + stmtDeleteSomeMeta = preparetodie(db, "delete from honkmeta where honkid = ? and genus not in ('oldrev')") + stmtDeleteOneMeta = preparetodie(db, "delete from honkmeta where honkid = ? and genus = ?") + stmtSaveHonk = preparetodie(db, "insert into honks (userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + stmtDeleteHonk = preparetodie(db, "delete from honks where honkid = ?") + stmtUpdateHonk = preparetodie(db, "update honks set precis = ?, noise = ?, format = ?, whofore = ?, dt = ? where honkid = ?") + stmtSaveOnt = preparetodie(db, "insert into onts (ontology, honkid) values (?, ?)") + stmtDeleteOnts = preparetodie(db, "delete from onts where honkid = ?") + stmtSaveDonk = preparetodie(db, "insert into donks (honkid, chonkid, fileid) values (?, ?, ?)") + stmtDeleteDonks = preparetodie(db, "delete from donks where honkid = ?") + stmtSaveFile = preparetodie(db, "insert into filemeta (xid, name, description, url, media, local) values (?, ?, ?, ?, ?, ?)") + blobdb := openblobdb() + stmtSaveFileData = preparetodie(blobdb, "insert into filedata (xid, media, hash, content) values (?, ?, ?, ?)") + stmtCheckFileData = preparetodie(blobdb, "select xid from filedata where hash = ?") + stmtGetFileData = preparetodie(blobdb, "select media, content from filedata where xid = ?") + stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?") + stmtFindFile = preparetodie(db, "select fileid, xid from filemeta where url = ? and local = 1") + stmtUserByName = preparetodie(db, "select userid, username, displayname, about, pubkey, seckey, options from users where username = ? and userid > 0") + stmtUserByNumber = preparetodie(db, "select userid, username, displayname, about, pubkey, seckey, options from users where userid = ?") + stmtSaveDub = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos, owner, meta, folxid) values (?, ?, ?, ?, '', '', '', ?)") + stmtAddDoover = preparetodie(db, "insert into doovers (dt, tries, userid, rcpt, msg) values (?, ?, ?, ?, ?)") + stmtGetDoovers = preparetodie(db, "select dooverid, dt from doovers") + stmtLoadDoover = preparetodie(db, "select tries, userid, rcpt, msg from doovers where dooverid = ?") + stmtZapDoover = preparetodie(db, "delete from doovers where dooverid = ?") + stmtUntagged = preparetodie(db, "select xid, rid, flags from (select honkid, xid, rid, flags from honks where userid = ? order by honkid desc limit 10000) order by honkid asc") + stmtFindZonk = preparetodie(db, "select zonkerid from zonkers where userid = ? and name = ? and wherefore = 'zonk'") + stmtGetZonkers = preparetodie(db, "select zonkerid, name, wherefore from zonkers where userid = ? and wherefore <> 'zonk'") + stmtSaveZonker = preparetodie(db, "insert into zonkers (userid, name, wherefore) values (?, ?, ?)") + stmtGetXonker = preparetodie(db, "select info from xonkers where name = ? and flavor = ?") + stmtSaveXonker = preparetodie(db, "insert into xonkers (name, info, flavor, dt) values (?, ?, ?, ?)") + stmtDeleteXonker = preparetodie(db, "delete from xonkers where name = ? and flavor = ? and dt < ?") + stmtDeleteOldXonkers = preparetodie(db, "delete from xonkers where flavor = ? and dt < ?") + 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") + stmtUpdateFlags = preparetodie(db, "update honks set flags = flags | ? where honkid = ?") + stmtClearFlags = preparetodie(db, "update honks set flags = flags & ~ ? where honkid = ?") + stmtAllOnts = preparetodie(db, "select ontology, count(ontology) from onts join honks on onts.honkid = honks.honkid where (honks.userid = ? or honks.whofore = 2) group by ontology") + stmtGetFilters = preparetodie(db, "select hfcsid, json from hfcs where userid = ?") + stmtSaveFilter = preparetodie(db, "insert into hfcs (userid, json) values (?, ?)") + stmtDeleteFilter = preparetodie(db, "delete from hfcs where userid = ? and hfcsid = ?") + stmtGetTracks = preparetodie(db, "select fetches from tracks where xid = ?") + stmtSaveChonk = preparetodie(db, "insert into chonks (userid, xid, who, target, dt, noise, format) values (?, ?, ?, ?, ?, ?, ?)") + stmtLoadChonks = preparetodie(db, "select chonkid, userid, xid, who, target, dt, noise, format from chonks where userid = ? and dt > ? order by chonkid asc") + stmtGetChatters = preparetodie(db, "select distinct(target) from chonks where userid = ?") +}
A deliverator.go

@@ -0,0 +1,178 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "fmt" + notrand "math/rand" + "time" + + "humungus.tedunangst.com/r/webs/gate" +) + +type Doover struct { + ID int64 + When time.Time +} + +func sayitagain(goarounds int64, userid int64, rcpt string, msg []byte) { + var drift time.Duration + switch goarounds { + case 1: + drift = 5 * time.Minute + case 2: + drift = 1 * time.Hour + case 3: + drift = 4 * time.Hour + case 4: + drift = 12 * time.Hour + case 5: + drift = 24 * time.Hour + default: + ilog.Printf("he's dead jim: %s", rcpt) + clearoutbound(rcpt) + return + } + drift += time.Duration(notrand.Int63n(int64(drift / 10))) + when := time.Now().Add(drift) + _, err := stmtAddDoover.Exec(when.UTC().Format(dbtimeformat), goarounds, userid, rcpt, msg) + if err != nil { + elog.Printf("error saving doover: %s", err) + } + select { + case pokechan <- 0: + default: + } +} + +func clearoutbound(rcpt string) { + hostname := originate(rcpt) + if hostname == "" { + return + } + xid := fmt.Sprintf("%%https://%s/%%", hostname) + ilog.Printf("clearing outbound for %s", xid) + db := opendatabase() + db.Exec("delete from doovers where rcpt like ?", xid) +} + +var garage = gate.NewLimiter(40) + +func deliverate(goarounds int64, userid int64, rcpt string, msg []byte, prio bool) { + garage.Start() + defer garage.Finish() + + var ki *KeyInfo + ok := ziggies.Get(userid, &ki) + if !ok { + elog.Printf("lost key for delivery") + return + } + var inbox string + // already did the box indirection + if rcpt[0] == '%' { + inbox = rcpt[1:] + } else { + var box *Box + ok := boxofboxes.Get(rcpt, &box) + if !ok { + ilog.Printf("failed getting inbox for %s", rcpt) + sayitagain(goarounds+1, userid, rcpt, msg) + return + } + inbox = box.In + } + err := PostMsg(ki.keyname, ki.seckey, inbox, msg) + if err != nil { + ilog.Printf("failed to post json to %s: %s", inbox, err) + if prio { + sayitagain(goarounds+1, userid, rcpt, msg) + } + return + } +} + +var pokechan = make(chan int, 1) + +func getdoovers() []Doover { + rows, err := stmtGetDoovers.Query() + if err != nil { + elog.Printf("wat?") + time.Sleep(1 * time.Minute) + return nil + } + defer rows.Close() + var doovers []Doover + for rows.Next() { + var d Doover + var dt string + err := rows.Scan(&d.ID, &dt) + if err != nil { + elog.Printf("error scanning dooverid: %s", err) + continue + } + d.When, _ = time.Parse(dbtimeformat, dt) + doovers = append(doovers, d) + } + return doovers +} + +func redeliverator() { + sleeper := time.NewTimer(5 * time.Second) + for { + select { + case <-pokechan: + if !sleeper.Stop() { + <-sleeper.C + } + time.Sleep(5 * time.Second) + case <-sleeper.C: + } + + doovers := getdoovers() + + now := time.Now() + nexttime := now.Add(24 * time.Hour) + for _, d := range doovers { + if d.When.Before(now) { + var goarounds, userid int64 + var rcpt string + var msg []byte + row := stmtLoadDoover.QueryRow(d.ID) + err := row.Scan(&goarounds, &userid, &rcpt, &msg) + if err != nil { + elog.Printf("error scanning doover: %s", err) + continue + } + _, err = stmtZapDoover.Exec(d.ID) + if err != nil { + elog.Printf("error deleting doover: %s", err) + continue + } + ilog.Printf("redeliverating %s try %d", rcpt, goarounds) + deliverate(goarounds, userid, rcpt, msg, true) + } else if d.When.Before(nexttime) { + nexttime = d.When + } + } + now = time.Now() + dur := 5 * time.Second + if now.Before(nexttime) { + dur += nexttime.Sub(now).Round(time.Second) + } + sleeper.Reset(dur) + } +}
A docs/activitypub.7

@@ -0,0 +1,175 @@

+.\" +.\" Copyright (c) 2019 Ted Unangst +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt ACTIVITYPUB 7 +.Os +.Sh NAME +.Nm activitypub +.Nd notes about the honk implementation +.Sh DESCRIPTION +The +.Xr honk 1 +utility processes status updates and other microblog activities using the +.Nm ActivityPub +protocol to exchange messages with other servers. +The specification is subject to interpretation, and not all implementations +behave in the same way. +This document attempts to clarify honk's behavior. +It is not intended to be a complete description of +.Nm ActivityPub , +but may be useful as a guide to other implementors looking to interoperate. +.Ss OBJECTS +The following object or document types are supported. +.Bl -tag -width tenletters +.It Vt Note +Fully supported. +The default object type for honk. +.It Vt Article +Fully supported. +.It Vt Page +Supported. +.It Vt Question +Read only support. +Appears similar to a Note. +.It Vt Event +Supported. +Appears similar to a Note. +Can be both created and received, but +.Vt Invite +activities are ignored. +.It Vt Video +Limited support. +.It Vt Audio +Limited Support. +.It Vt GuessWord +Guess the word game. +(Unofficial extension.) +The solution is stored in +.Fa content +with the possible words, one per line, in a file located at +.Fa wordlist . +.El +.Pp +Honk primarily supports HTML content, not markdown or other formats, +with a wide range of permitted HTML tags in object +.Fa content +fields. +The following tags are supported. +.Bd -literal -offset indent +a, img, span, +div, h1, h2, h3, h4, h5, h6, hr, +table, thead, tbody, tfoot, th, tr, td, colgroup, col, +p, br, pre, code, blockquote, q, +caption, kbd, time, wbr, aside, +ruby, rtc, rb, rt, details, summary, +samp, mark, ins, dfn, cite, abbr, address, +strong, em, b, i, s, u, sub, sup, del, tt, small, +ol, ul, li, dl, dt, dd +.Ed +.Pp +The following tag attributes are permitted. +.Bd -literal -offset indent +href, src, alt, colspan, rowspan +.Ed +.Pp +The following class names are used for syntax highlighting code blocks. +.Bd -literal -offset indent +kw, bi, st, nm, tp, op, cm, al, dl +.Ed +.Ss ACTIVITIES +The following activities are supported. +.Bl -tag -width tenletters +.It Vt Create +Fully supported. +.It Vt Announce +Supported with share semantics. +.It Vt Read +Supported. +Primarily used to acknowledge replies and complete threads. +Can be interpreted to mean reply is approved, if not endorsed. +.It Vt Add +Works with collections. +.It Vt Follow +Supported. +Can follow both actors and collections. +.It Vt Update +Supported. +Honk sends and receives +.Vt Update +activities. +.It Vt Delete +Does what it can. +.It Vt Like +Don't be ridiculous. +.It Vt EmojiReact +Be ridiculous. +.El +.Ss METADATA +The following additional object types are supported, typically as +.Fa tag +or +.Fa attachment . +.Bl -tag -width tenletters +.It Mention +Pretty @ machine. +.It Emoji +Inline text :emoji: with image replacement. +.It Place +Included as a +.Fa location . +Supports +.Fa name , +.Fa url , +.Fa latitude , +and +.Fa longitude . +.It Document +Plain text and images in jpeg, gif, png, and webp formats are supported. +Other formats are linked to origin. +.El +.Pp +The +.Fa replies +array will be populated with a list of acknowledged replies. +.Ss EXTENSIONS +Honk also supports a +.Vt Ping +activity and will respond with a +.Vt Pong +activity. +This is useful for debugging networking connectivity issues without +visible side effects. +See ping.txt for details. +.Ss SECURITY +Honk uses http signatures. +.Ss WEBFINGER +Honk implements the +.Vt webfinger +end point and will use it for @mention resolution. +It is not required for federation. +.Ss LD-JSON +Not really. +.Sh SEE ALSO +.Xr intro 1 , +.Xr honk 1 +.Sh STANDARDS +.Pp +.Lk https://www.w3.org/TR/activitypub/ "ActivityPub" +.Pp +.Lk https://www.w3.org/TR/activitystreams-vocabulary/ "Activity Vocabulary" +.Sh CAVEATS +The ActivityPub standard is subject to interpretation, and not all +implementations are as enlightened as honk.
A docs/changelog.txt

@@ -0,0 +1,378 @@

+changelog + +=== next + ++ Add a default icon.png. + ++ Try to fix hoot again because Twitter did a Twitter. + +=== 0.9.8 Tentative Tentacle + ++ Switch database to WAL mode. + +- go version 1.16 required. + ++ Specify banner: image in profile. + ++ Update activity compatibility with mastodon. + +- Signed fetch. + ++ Better unicode hashtags. + ++ Some more configuration options. + ++ Some UI improvements to web interface. + ++ Add atme class to mentions + ++ Improvements to the mastodon importer. + ++ More hydration capable pages. + ++ Support for local.js. + ++ Better error messages for timeouts. + ++ Some improved html and markdown. + +=== 0.9.7 Witless Weekender + ++++ Word guessing game. Wonk wonk! + ++ Flexible logging, to file, syslog, null, etc. + ++ Low key unread counters. + ++ Images in the hooter. + ++ More flexible hashtag characters. + ++ Fix the memetizer to work in more environments. + ++ Printing is prettier than ever before. + +=== 0.9.6 Virile Vigorous and Potent + ++ A bug, a fix, a bug fix, a fix bug. + ++ Fix Update processing. + ++ Better cookie rotation with weekly refresh. + ++ A new follow button in a surprise location. + ++ Fix mastodon import. + ++ Filters work better with hashtags. + ++ Fix hoot to work with Twitter's latest crap. + +=== 0.9.5 Emergency Ejection + ++ Fix honk init user creation. + +=== 0.9.4 Collegiate Colloquialism + ++ Add validation to some more user inputs to prevent mistakes. + ++ Easier to use ping command. + +=== 0.9.3 Notacanthous Nutshell + +++ backup command. + ++ Relax requirement for multipart/form-data posts in API. + ++ Dedupe blob file data. + ++ Better support for rich text bios. + ++ Follow and unfollow should work a little better. + ++ Option to mention all in replies. + ++ Reduce interference between various text substitution rules. + ++ Fix crash in search with extra space. + ++ Fix pubkey issue with domain only keys. + +- Custom lingo for those who don't like honking. + +=== 0.9.2 Malleable Maltote + ++ Fix compilation on mac. + +=== 0.9.1 Late Stage Lusciousness + +++ Boing boom tschak chonky chatter. Chat messages with Pleroma. + ++ Custom rgb flag: emoji. + ++ Slightly better ActivityPub compat + ++ ## headings for markdown + ++ Workaround js only twitter for hoot: feature. + ++ Quote unquote reliability improvements. + ++ Much better omit images handling. + ++ Fix update activity. + ++ A few API refinements and additions. + +=== 0.9.0 Monitor vs Merrimack + +--- Add Reactions. + ++++ Rename react to badonk. + ++ Quick fix to hide all images. + ++ Allow resending follow requests. + ++ Improved search query parsing. + ++ Tables + ++ Reduce retries talking to dumb servers. + ++ Maybe possible to use @user@example.com wihtout subdomain. + +=== 0.8.6 Sartorial Headpiece + +++ Import command now supports the elephant in the room. + ++ Minimal support for Move activity. + ++ deluser command. + ++ Configurable avatar colors. + ++ Optional pleroma color scheme for the home sick... + ++ Rebalance colors slightly. Looks a little fresher now? + ++ Add unplug command for servers that have dropped off the net. + ++ Add notes field to honkers to document their downfall. + ++ Add notes field to filters for record keeping. + ++ Negated search -terms. + ++ A raw sendactivity API action for the bold. + ++ More flexible meme names. + +=== 0.8.5 Turnkey Blaster + ++ Codenames in changelog. + ++ Fix some bugs that may have interfered with federation. + ++ Add some re: re: re: to replies. + ++ Set an avatar. If you must. + ++ Try a little harder to recover from httpsig failures. + ++ Add cite tag for block quote attributions. + ++ @media print styles. + ++ Disable overscroll (pull down) refresh. + ++ Can never seem to version the changelog correctly. + +=== 0.8.4 + ++ Fix bug preventing import of keys + ++ Option to switch map links to Apple. + +=== 0.8.3 + +- mistag. + +=== 0.8.2 Game Warden + +++ Import command to preserve those embarssassing old posts from Twitter. + +++ Add a limited /api for the robotrons. + ++ Resource usage stats on about page. + ++ Unveil and pledge restrictions on OpenBSD. + ++ Lists supported in markdown. + ++ Rewrite admin console to avoid large dependencies. + ++ "Bug" fixes. + +=== 0.8.1 + +++ Make it easier to upgrade by decoupling data dir from ".". + ++ Timestamps displayed in server time with TZ. + ++ version command to print current version. + ++ Amend changelog for 0.8.0 to include omitted elements: + Syntax highlighting for code blocks. + Something resembling an actual manual. + +=== 0.8.0 Ordinary Octology + ++++ Add Honk Filtering and Censorship System (HFCS). + ++++ Editing honks (Update activity). + +++ Subscribe to hashtags. + +++ Search. I hate it already. + +++ Hashtags that work? + +++ Dynamic refresh and page switching without reloads. + +++ Reply control. Ack replies to show them on the site. + ++ Allow PDF attachments. For serious business only. + ++ "untag me" button to mute part of a thread. + ++ Inline images in posts. Send and receive. + ++ Somewhat functional admin console (TTY). + ++ More JS free fallbacks for some basic functions. + ++ Add chpass command. + ++ Improved honker management. + ++ Better markdown output. + ++ Times for events. + ++ Split media database into separate blob.db. + ++ Location checkin. Welcome to the... danger zone! + ++ Quick mention @alias. + ++ Image descriptions. + ++ Unbonking. + ++ More robust retries for fetching objects. + ++ Don't decode excessively large images and run out of memory. + ++ Syntax highlighting for code blocks. + ++ Something resembling an actual manual. + +- Sometimes the cached state of the @me feed becomes unsynced. + Acked status may display incorrectly. + +=== 0.7.7 More 7 Than Ever + ++ Add another retry to workaround pixelfed's general unreliability. + ++ Attached images are not lost when previewing. + +- Remove sensitivity to spicy peppers. + ++ Keep reply to setting during preview. + ++ Increase max thread retrieval depth to 10. + +=== 0.7.6 + ++ Fix a bug where upgrades would not complete in one step. + +=== 0.7.5 + ++ Fix a bug (introdcued 0.7.4) preventing new user creation from working. + ++ Semi flexible URL patterns to allow transition from other software. + ++ Improved ActivityPub parsing conformance for better compat with others. + ++ Add server name to user agent. + ++ What may be considered UI improvements. + +=== 0.7.4 + ++ Ever more bug fixes. + ++ Collapse posts based on custom regex match. + ++ Tonks are now honk backs. + ++ Show both avatars for bonks. Other minor refinements to UI. + ++ Minimal support for Video activity and PeerTube compat. + ++ Support for some user selectable styling. Currently, skinny column mode. + ++ webp image transcoding. + +=== 0.7.3 + ++ Better fedicompat so bonks are visible to pleroma followers. + +=== 0.7.2 + ++ Add the funzone. Minor other UI tweaks. + +=== 0.7.1 + ++ Fix bug preventing unfollow from working. + +=== 0.7.0 Father Mother Maiden Crone Honker Bonker Zonker + ++++ Auto fetching and inlining of hoots. + +++ A new xzone to view and import data not otherwise visible. + +++ Preview before honking. + +++ Some extra commands for better database retention management. + +++ A changelog. + ++ Default robots.txt. + ++ Misc UI touchups. + ++ Read only support for qonks. + ++ About page. + ++ More reliable (retries) meta messages such as follow requests. + ++ Better thread support for missing context. + ++ Upgrade image library for cleaner screenshots. + ++ Not all summaries need labels. + ++ Add max-width for video tag. + +=== 0.6.0 Sixy Delights + +Most records from this time of primitive development have been lost. + +=== 0.5.0 Halfway to Heaven + +=== 0.4.0 Fore Score + +=== 0.3.0 Valorous Varaha
A docs/hfcs.1

@@ -0,0 +1,100 @@

+.\" +.\" Copyright (c) 2019 Ted Unangst +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt HFCS 1 +.Os +.Sh NAME +.Nm hfcs +.Nd honk filtering and censorship system +.Sh DESCRIPTION +The honk filtering and censorship system, +.Nm hfcs , +controls what messages are seen and how they are presented to the user. +Filter rules are based on a series of matches and actions. +It is accessed via the +.Pa filters +menu item. +.Pp +Each filter has an optional +.Ar name +and +.Ar notes +for user defined purposes. +.Pp +The following match types are possible. +All nonempty criteria must match. +.Bl -tag -width include-audience +.It Ar who +Match an actor or domain name. +Matches against +.Fa Ar actor +property. +.It Ar include audience +Previous match is applied against +.Fa to +and +.Fa cc +fields as well. +.It Ar text +Regular expression match against the post +.Fa content . +.It Ar is announce +Is announced (shared). +.It Ar announce of +Limit prevous match to only specified actor or domain name. +.El +.Pp +The following actions may be applied. +Multiple actions may be applied, but some are subsumed by others. +.Bl -tag -width tenletters +.It Ar reject +Reject this message entirely. +.It Ar skip media +Don't include images or attachments. +.It Ar hide +Remove this message from most feeds. +.It Ar collapse +Show only a short summary with click to view content. +.It Ar rewrite +Rewrite message content, using +.Ar replace +replacement text. +.El +.Pp +The +.Ar text +and +.Ar rewrite +fields are case insensitive word anchored regular expressions. +Specifically, an argument +.Ql re +will be automatically rewritten as +.Ql \\\b(?i:re)\\\b . +The +.Ar replace +text may refer to submatches using $1, etc. +.Pp +A post marked sensitive that does not otherwise contain a summary will +have an invisible summary of +.Dq unspecified horror +that can be matched against and will appear if the post is collapsed. +.Pp +An optional expiration may be specified as a duration. +XdYhZm for X days, Y hours, and Z minutes. +.Sh SEE ALSO +.Xr honk 1 +.Sh CAVEATS +Not seeing is not erasing.
A docs/honk.1

@@ -0,0 +1,230 @@

+.\" +.\" Copyright (c) 2019 Ted Unangst +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt HONK 1 +.Os +.Sh NAME +.Nm honk +.Nd federated status conveyance +.Sh DESCRIPTION +The +.Nm +utility processes federated status updates and other microblog activities. +This is the user manual. +For administration, see +.Xr honk 8 . +For other documentation, refer to the +.Xr intro 1 . +.Pp +This manual is still incomplete. +It'll get there eventually. +.Ss Honkers +Initially, there won't be much to see after logging in. +In order to receive regular updates from other users, they must first +be added to one's honker collection. +Begin at the +.Pa honkers +tab. +The +.Ar url +field is required. +Either of two forms are accepted, the user's handle (or webfinger) or their +ActivityPub actor URL. +.Pp +.Dl @user@example.social +.Dl https://example.social/users/user +.Pp +The +.Ar name +field is optional and will be automatically inferred. +The +.Ar notes +field is reserved for user remarks. +Fellow honkers may be added to one or more +.Ar combos +to suit one's organizational preferences. +These are accessed via the +.Pa combos +tab and allow easy access to particular groupings. +The special combo name of one hyphen +.Sq - +will exclude a honker's posts from the primary feed. +.Pp +It is also possible to skip subscribing. +In this case, regular posts are not received, but replies and posts fetched +via other means will appear in the relevant combos. +.Pp +In addition to honkers, it is possible to subscribe to a hashtag collection. +(Where supported.) +Enter the collection URL for +.Ar url . +.Pp +Separately, hashtags may be added to a combo by creating a honker with a +.Ar url +of the desired hashtag (including #). +Several hashtags may thus be collected in a single combo. +.Ss Viewing +The primary feed is accessed via the +.Pa home +tab. +It will contain posts from all honkers except those specifically excluded. +Posts mentioning the user, both followed and not, are collected under the +.Pa @me +tab. +Other feeds include +.Pa first +which excludes replies, the user defined options under the +.Pa combos +subheading, and the +.Pa events +page which lists only events. +.Pp +Individual honks contain a visual representation of the honker's ID, +their name, the activity (with a link back to origin), a link to the +parent post if applicable, and the convoy (thread) identifier. +A red border indicates the honk is not public. +Screenshot below. +.Pp +.Lk screenshot-honk.png screenshot of one honk +.Pp +Available actions are: +.Bl -tag -width tenletters +.It Ic bonk +Share with followers. +Not available for nonpublic honks. +.It Ic honk back +Reply. +.It Ic mute +Mute this entire thread. +Existing posts are hidden, and future posts will not appear in any feed. +.It Ic zonk +Delete this post. +When deleting one's own post, other servers will be requested to remove it, +but this is unreliable. +.It Ic ack +Acknowledge reading this post. +Typically if it's a reply to one's own post. +.It Ic save +Save this honk to the +.Pa saved +tab to find later. +.It Ic untag me +Sometimes a thread goes on entirely too long. +Untag will hide further replies to the selected post, but without muting the +entire thread. +Replies higher in the tree are still received. +.It Ic badonk +Please no. +.It Ic edit +Change it up. +Alas, Update activities do not federate reliably. +.Ss Refresh +Clicking the refresh button will load new honks, if any. +New honks will be subtly highlighted. +.El +.Ss Honking +Refer to the +.Xr honk 5 +section of the manual for details of honk composition. +.Ss Search +Find old honks. +It's basic substring match with a few extensions. +The following keywords are supported: +.Bl -tag -width honker +.It site +Substring match on the post domain name. +.It honker +Exact match, either AP actor or honker nickname. +.It - +Negate term. +.El +.Pp +Example: +.Dl honker:goose big moose -footloose +This query will find honks by the goose about the big moose, but excluding +those about footloose. +.Ss Filtering +Sometimes other users of the federation can get unruly. +The honk filtering and censorship system, +.Xr hfcs 1 , +can be of great use to restore order to one's timeline. +Accessed via the +.Pa filters +menu item. +.Ss Xzone +The +.Pa xzone +page lists recently seen honkers that are not otherwise tracked. +It also allows the import of external objects via URL, either individual +posts or actor URLs, in which case their recent outbox is imported. +.Ss Account +It's all about you. +An avatar may be selected from the +.Pa funzone +meme collection by adding +.Dq avatar: filename.png +to one's profile info. +If truly necessary. +A banner may be set by specifying +.Dq banner: image.jpg . +See +.Xr honk 8 +for more about the funzone. +.Pp +Some options to customize the site appearance: +.Bl -tag -width skinny +.It skinny +Use a narrower column for the main display. +.It omit images +Omit img tags, to lighten page loads on slow connections. +.It apple +Prefer Apple links for maps. +The default is OpenStreetMap. +.It reaction +Pick an emoji for reacting to posts. +.El +.Sh ENVIRONMENT +.Nm +is designed to work with most browsers, but for optimal results it is +recommended to use a +2015 or later Thinkpad X1 Carbon with 2560x1440 screen running +.Ox +and chromium at 150% scaling with the dwm window manager. +This will enable the main menu to line up just right. +.Sh SEE ALSO +.Xr intro 1 , +.Xr honk 8 +.Sh STANDARDS +.Pp +.Lk https://www.w3.org/TR/activitypub/ "ActivityPub" +.Pp +.Lk https://www.w3.org/TR/activitystreams-vocabulary/ "Activity Vocabulary" +.Sh HISTORY +Started March 2019. +.Sh AUTHORS +.An Ted Unangst Lk https://honk.tedunangst.com/u/tedu @tedu@honk.tedunangst.com +.Sh CAVEATS +Completing some operations, such as subscribing to new honkers, requires an +aptitude for clipboard use and tab switching along with a steady hand. +For the most part, these are infrequent operations, but they are also the +first operations new users encounter. +This is not ideal. +.Pp +The ActivityPub standard is subject to interpretation, and not all +implementations are as enlightened as +.Nm . +.Sh BUGS +It's a feature.
A docs/honk.3

@@ -0,0 +1,180 @@

+.\" +.\" Copyright (c) 2019 Ted Unangst +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt HONK 3 +.Os +.Sh NAME +.Nm honk +.Nd API access +.Sh DESCRIPTION +In addition to the standard web interface, some functionality is +available via the +.Nm +HTTP API. +.Pp +With the exception of login, all requests should contain +the following form values. +.Bl -tag -width action +.It Fa action +The desired action. +See below. +.It Fa token +An authorization token. +Alternatively, may be passed in the +.Dq Authorization +HTTP header. +.El +.Pp +The API URL for all actions other than login and logout is +.Pa /api . +.Ss login +Send a POST request to +.Pa /dologin +with the following form values. +.Bl -tag -width username +.It Fa username +User name. +.It Fa password +Pass phrase. +.It Fa gettoken +Must be +.Dq 1 . +.El +.Pp +This will return a token to be used for future requests. +The token is valid for one year. +.Ss logout +Send a request to +.Pa /logout +with the +.Fa token +to be expired. +.Ss honk +The +.Fa action +value should be +.Dq honk . +Content type should be multipart/form-data if an attachment is included. +The following values are recognized: +.Bl -tag -width placename +.It Fa noise +The contents of the honk. +.It Fa format +The format of noise. +Defaults to markdown. +May also be html. +.It Fa donk +A file to attach. +.It Fa donkdesc +A description for the attached file. +.It Fa donkxid +The XID of a previously uploaded attachment. +.It Fa placename +The name of an associated location. +.It Fa placeurl +The url of an associated location. +.It Fa placelat +The latitude of an associated location. +.It Fa placelong +The longitude of an associated location. +.It Fa timestart +The start time of an event. +.It Fa rid +The ActivityPub ID that this honk is in reply to. +.El +.Pp +Upon success, the honk action will return the URL for the created honk. +.Ss donk +Upload just an attachment using +.Fa donk +and +.Fa donkdesc . +Content type must be multipart/form-data. +Will return the XID. +.Ss gethonks +The +.Dq gethonks +.Fa action +can be used to query for honks. +The following parameters are used. +.Bl -tag -width placename +.It Fa page +Should be one of +.Dq home +or +.Dq atme . +.It Fa after +Only return honks after the specified ID. +.It Fa wait +If there are no results, wait this many seconds for something to appear. +.El +.Pp +The result will be returned as json. +.Ss zonkit +The +.Dq zonkit +action began life as a delete function, but has since evolved some other +powers as specified by the +.Fa wherefore +parameter. +The target of the action is specified by the +.Fa what +parameter and is generally the XID of a honk. +.Pp +Wherefore must be one of the following. +.Bl -tag -width zonvoy +.It bonk +Share honk with others. +.It unbonk +Undo share. +.It save +Mark honk as saved. +.It unsave +Unmark honk as saved. +.It react +Post an emoji reaction. +A custom reaction may be specified with +.Fa reaction . +.It ack +Mark honk as read. +.It deack +Unmark honk as read. +.It zonk +Delete this honk. +.It zonvoy +Mute this thread. +What should identify a convoy. +.El +.Ss sendactivity +Send anything. +No limits, no error checking. +.Bl -tag -width public +.It Fa rcpt +An actor to deliver the message to to. +May be specified more than once. +An inbox may be specified directly by prefixing with %. +.It Fa msg +The message. +It should be a valid json activity, but yolo. +.It Fa public +Set to 1 to use shared inboxes for delivery. +.El +.Sh EXAMPLES +Refer to the sample code in the +.Pa toys +directory. +.Sh SEE ALSO +.Xr vim 3
A docs/honk.5

@@ -0,0 +1,159 @@

+.\" +.\" Copyright (c) 2019 Ted Unangst +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt HONK 5 +.Os +.Sh NAME +.Nm honk +.Nd status composition +.Sh DESCRIPTION +Status updates composed in +.Nm +have many features beyond just plain text. +.Pp +The process begins by pressing the button marked +.Dq it's honking time +to activate the honk form. +.Pp +Honks are posted publicly. +.Ss Basics +A subset of markdown is supported. +.Bl -tag -width tenletters +.It bold +**bold text** +.It italics +*italicized text* +.It quotes +> This text is quoted. +.It code +Inline `code fragments` with single ticks. +.Bd -literal +```c +/* triple tick code blocks support syntax highlighting */ +int main() { return 0; } +``` +.Ed +.It headings +Heading lines starting with #. +.It lists +Lists of items starting with either +.Sq + +or +.Sq - . +.It tables +Table cells separated by |. +.It images +Inline images with img tags. +.Bd -literal +<img alt="Lifecycle of a honk" src="https://example.com/diagram.png"> +.Ed +.It links +URLs beginning with +.Dq http +or +.Dq https +will be autolinked. +.It rules +Exactly three dashes on a line, +.Dq --- , +will become a horizontal rule. +.El +.Pp +If the first line of a honk begins with +.Dq DZ: +(danger zone) it will be used a summary and the post marked sensitive. +.Pp +Mentioning a specfic user such as +.Pq @user@example.social +will send a copy of the message to them. +Several forms are supported. +.Ql @name +will work using the short name from the +.Pa honkers +table and be expanded automatically. +.Ql @handle@domain +will work for anyone. +.Ql @https://example.com +works as well. +When honking back, the author of the parent post is automatically mentioned. +.Ss Extras +Threads from the tiny bird site may be included as quotes in a post via the +.Ar hoot +operator followed by the URL. +.Dl hoot: https://twitter.com/tedunangst/status/850379741492367360 +.Pp +Custom emoji may be included by colon wrapping the image name. +.Pq :hellsyeah: +A meme (sticker, reaction gif) may be included with the +.Ar meme +operator followed by the file name. +.Dl meme: honk.mp4 +A full list of emoji and memes may be found in the +.Pa funzone . +See +.Xr honk 8 +for more about the funzone. +.Pp +Custom flag emoji may be generated on the fly by specifying comma separated +hexadecimal RGB values, one for each stripe. +.Dl flag:306,002,dcf +Vertical stripes may be selected by specfying "vert" for the first value. +.Pp +There are no length restrictions, but remember, somebody is going to have +to read this noise. +.Pp +One may attach a file to a post. +Images are automatically rescaled and reduced in size for federation. +A description, or caption, is encouraged. +Text files and PDFs are also supported as attachments. +Other formats are not supported. +.Pp +One may also check in to a location. +The available fields, all optional, are +.Ar name , +.Ar url , +.Ar latitude , +and +.Ar longitude . +By default, location data is rounded to approximately 1/100 decimal degree +accuracy. +Pressing the check in button a second time will refine this to more a +precise location. +.Pp +Adding a time to a post turns it into an event. +Supported formats for start time are HH:MM or YYYY-MM-DD HH:MM. +A 24 hour clock is assumed, unless am or pm are specified. +The duration is optional and may be specified as XdYhZm for X days, Y hours, +and Z minutes (1d12h would be a 36 hour event). +.Pp +When everything is at last ready to go, press the +.Dq it's gonna be honked +button. +.Sh EXAMPLES +(Slightly dated screenshots.) +.Pp +Composing a new honk with an attached image and location. +.Pp +.Lk screenshot-compose.png screenshot of honk composition +.Pp +After posting. +.Pp +.Lk screenshot-afterpost.jpg screenshot of honk after posting +.Sh SEE ALSO +.Xr honk 1 +.Sh CAVEATS +Markdown support is implemented with regexes. +Preview is recommended.
A docs/honk.8

@@ -0,0 +1,281 @@

+.\" +.\" Copyright (c) 2019 Ted Unangst +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt HONK 8 +.Os +.Sh NAME +.Nm honk +.Nd honk administration +.Sh DESCRIPTION +The +.Nm +daemon processes messages from other federated servers. +This is the admin manual. +For user operation, see +.Xr honk 1 . +.Ss Setup +.Pp +Set up a TLS reverse proxy. +.Nm +can listen on TCP or unix sockets, but will not terminate TLS. +https is a required component for federation. +Also, http signature verification requires accurate time keeping. +.Pp +Make sure to pass the Host header, if necessary (as for nginx). +.Bd -literal -offset indent +proxy_set_header Host $http_host; +.Ed +.Ss Build +Building +.Nm +requires a go compiler 1.13 and libsqlite. +On +.Ox +this is the go and sqlite3 packages. +Other platforms may require additional development libraries or headers +to be installed. +Run make. +Please be patient. +Even on fast machines, building from source can take several seconds. +.Ss Options +The following options control where +.Nm +looks for data. +.Bl -tag -width datadirxdirx +.It Fl datadir Ar dir +The root data directory, where the database and other user data are stored. +This directory contains all user data that persists across upgrades. +Requires write access. +Defaults to ".". +.It Fl viewdir Ar dir +The root view directory, where html and other templates are stored. +The contents of this directory are generally replaced with each release. +Read only. +Defaults to ".". +.El +.Pp +The following options control log output. +Acceptable values include "stderr" (the default), "stdout", "null", "syslog", +or a file name. +syslog messages will be sent to the UUCP facility. +.Bl -tag -width errorlogxlogx +.It Fl errorlog Ar log +The error log. +Something bad has happened. +.It Fl infolog Ar log +The informative messages log. +Something has happened, but probably not too bad. +.It Fl debuglog Ar log +The debug log. +There's probably no reason to care. +.It Fl log Ar log +Set all three logs. +.El +.Ss Init +Run the +.Ic init +command. +This will create the database and ask four questions, as well as creating +the initial user. +See below about importing existing data. +.Ss Operation +Run honk. +.Ss Customization +The funzone contains fun flair that users may add to posts and profiles. +Add custom memes (stickers) to the +.Pa memes +data directory. +Image and video files are supported. +Add custom emus (emoji) to the +.Pa emus +data directory. +PNG and GIF files are supported. +.Pp +Site CSS may be overridden by creating a +.Pa views/local.css +file in the data directory. +Site JS may similarly be included by creating +.Pa views/local.js . +A restart is required after changes. +A site icon.png and favicon.ico will be served from the views directory +in the data directory, if present. +.Pp +Custom HTML messages may be added to select pages by using the +.Ic admin +command. +This interface is a little rough. +A restart is required after changes. +.Bl -tag -width tenletters +.It server +Displayed on the home page. +.It about +Displayed on the about page. +.It login +Displayed on the login form. +.It avatar colors +Four 32-bit hex colors (RGBA). +.El +.Pp +.Ss User Admin +New users can be added with the +.Ic adduser +command. +This is discouraged. +.Pp +Passwords may be reset with the +.Ic chpass Ar username +command. +.Pp +Users may be deleted with the +.Ic deluser Ar username +command. +.Ss Maintenance +The database may grow large over time. +The +.Ic cleanup Op Ar days +command exists to purge old external data, by default 30 days. +This removes unreferenced, unsaved posts and attachments. +It does not remove any original content. +.Pp +Backups may be performed by running +.Ic backup dirname . +Backups only include the minimal necessary information, such as user posts +and follower information, but not external posts. +.Pp +Sometimes servers simply disappear, resulting in many errors trying to deliver +undeliverable messages. +Running +.Ic unplug Ar hostname +will delete all subscriptions and pending deliveries. +.Ss Upgrade +Stop the old honk process. +Backup the database. +Perform the upgrade with the +.Ic upgrade +command. +Restart. +.Pp +The current version of the honk binary may be printed with the +.Ic version +command. +.Ss Security +.Nm +is not currently hardened against SSRF, server side request forgery. +Be mindful of what other services may be exposed via localhost or the +local network. +.Ss Development +Development mode may be enabled or disabled by running +.Ic devel Ar on|off . +In devel mode, secure cookies are disabled, TLS certs are not verified, +and templates are reloaded every request. +.Ss Import +Data may be imported and converted from other services using the +.Ic import +command. +Currently supports Mastodon and Twitter exported data. +Posts are imported and backdated to appear as old honks. +The Mastodon following list is imported, but must be refollowed. +.Pp +To prepare a Mastodon data archive, extract the archive-longhash.tar.gz file. +.Dl ./honk import username mastodon source-directory +.Pp +To prepare a Twitter data archive, extract the twitter-longhash.zip file. +After unzipping the data archive, navigate to the tweet_media directory +and unzip any zip files contained within. +.Dl ./honk import username twitter source-directory +.Ss Advanced Options +Advanced configuration values may be set by running the +.Ic setconfig Ar key value +command. +For example, to increase the fast timeout value from 5 seconds to 10: +.Dl ./honk setconfig fasttimeout 10 +.Pp +To support separate mentions without a subdomain, +e.g. @user@example.com and https://honk.example.com/u/user, +set config key 'masqname' to 'example.com'. +Route +.Pa /.well-known/webfinger +from the top domain to honk. +.Pp +Custom URL seperators (not "u" and "h") may be specified by adding +"usersep" and "honksep" options to the config table. +e.g. example.com/users/username/honk/somehonk instead of +example.com/u/username/h/somehonk. +.Sh FILES +.Nm +files are split between the data directory and the view directory. +Both default to "." but may be specified by command line options. +.Pp +The data directory contains: +.Bl -tag -width views/local.css +.It Pa honk.db +The main database. +.It Pa blob.db +Media and attachment storage. +.It Pa emus +Custom emoji. +.It Pa memes +Stickers and such. +.It Pa views/local.js +Locally customized JS. +.It Pa views/local.css +Locally customized CSS. +.El +.Pp +The view directory contains: +.Bl -tag -width views +.It Pa views +HTML templates and CSS files. +.El +.Sh EXAMPLES +This series of commands creates a new database, sets a friendly +welcome message, and runs honk. +.Bd -literal -offset indent +honk-v98> make +honk-v98> ./honk -datadir ../honkdata init +username: puffy +password: OxychromaticBlowfishSwatDynamite +listen address: /var/www/honk.sock +server name: honk.example.com +honk-v98> ./honk -datadir ../honkdata admin +honk-v98> date; ./honk -log honk.log -datadir ../honkdata +.Ed +.Pp +The views directory includes a sample pleroma.css to change color scheme. +.Bd -literal -offset indent +honk-v98> mkdir ../honkdata/views +honk-v98> cp views/pleroma.css ../honkdata/views/local.css +.Ed +.Pp +Upgrade to the next version. +Clean things up a bit. +.Bd -literal -offset indent +datadir> cp honk.db backup.db +datadir> cd ../honk-v99 +honk-v99> make +honk-v99> ./honk -datadir ../honkdata upgrade +honk-v99> ./honk -datadir ../honkdata cleanup +honk-v99> date; ./honk -log honk.log -datadir ../honkdata +.Ed +.Sh ENVIRONMENT +Image processing and scaling requires considerable memory. +It is recommended to adjust the datasize ulimit to at least 1GB. +.Sh SEE ALSO +.Xr intro 1 , +.Xr honk 1 +.Sh CAVEATS +There's no online upgrade capability. +Upgrades may result in minutes of downtime.
A docs/intro.1

@@ -0,0 +1,45 @@

+.\" +.\" Copyright (c) 2019 Ted Unangst +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt INTRO 1 +.Os +.Sh NAME +.Nm intro +.Nd introduction to honk documentation +.Sh DESCRIPTION +Honk processes federated status updates and other microblog activities. +This is the index for the honk manual. +.Pp +.Bl -tag -width activitypubxxr +.It Xr honk 1 +User manual. +.It Xr honk 8 +Administration manual. +.It Xr honk 5 +Honk composition. +.It Xr hfcs 1 +Honk Filtering and Censorship System. +.It Xr activitypub 7 +ActivityPub implementation notes. +.It Xr honk 3 +API access for robotrons. +.It Xr vim 3 +Modifying honk. +.El +.Sh HISTORY +Started March 2019. +.Sh AUTHORS +.An Ted Unangst Lk https://honk.tedunangst.com/u/tedu @tedu@honk.tedunangst.com
A docs/mandoc.css

@@ -0,0 +1,291 @@

+/* $OpenBSD: mandoc.css,v 1.33 2019/06/02 16:50:46 schwarze Exp $ */ +/* + * Standard style sheet for mandoc(1) -Thtml and man.cgi(8). + * + * Written by Ingo Schwarze <schwarze@openbsd.org>. + * I place this file into the public domain. + * Permission to use, copy, modify, and distribute it for any purpose + * with or without fee is hereby granted, without any conditions. + */ + +/* Global defaults. */ + +html { max-width: 65em; + background: #305; + --bg: #002; + --fg: #dde; } +body { background: var(--bg); + color: var(--fg); + margin: 2em; + padding: 1em; + font-size: 18px; + font-family: Helvetica,Arial,sans-serif; } +a { color: var(--fg); } +h1 { font-size: 110%; } +table { margin-top: 0em; + margin-bottom: 0em; + border-collapse: collapse; } +/* Some browsers set border-color in a browser style for tbody, + * but not for table, resulting in inconsistent border styling. */ +tbody { border-color: inherit; } +tr { border-color: inherit; } +td { vertical-align: top; + padding-left: 0.2em; + padding-right: 0.2em; + border-color: inherit; } +ul, ol, dl { margin-top: 0em; + margin-bottom: 0em; } +li, dt { margin-top: 1em; } + +.permalink { border-bottom: thin dotted; + color: inherit; + font: inherit; + text-decoration: inherit; } +* { clear: both } + +/* Search form and search results. */ + +fieldset { border: thin solid silver; + border-radius: 1em; + text-align: center; } +input[name=expr] { + width: 25%; } + +table.results { margin-top: 1em; + margin-left: 2em; + font-size: smaller; } + +img { max-width: 100%; } +code, pre { + word-break: break-word; +} +pre { + white-space: pre-wrap; +} + + +/* Header and footer lines. */ + +table.head { width: 100%; + border-bottom: 1px dotted #808080; + margin-bottom: 1em; + font-size: smaller; } +td.head-vol { text-align: center; } +td.head-rtitle { + text-align: right; } + +table.foot { width: 100%; + border-top: 1px dotted #808080; + margin-top: 1em; + font-size: smaller; } +td.foot-os { text-align: right; } + +/* Sections and paragraphs. */ + +.manual-text { + margin-left: 3.8em; } +.Nd { } +section.Sh { } +h1.Sh { margin-top: 1.2em; + margin-bottom: 0.6em; + margin-left: -3.2em; } +section.Ss { } +h2.Ss { margin-top: 1.2em; + margin-bottom: 0.6em; + margin-left: -1.2em; + font-size: 105%; } +.Pp { margin: 0.6em 0em; } +.Sx { } +.Xr { } + +/* Displays and lists. */ + +.Bd { } +.Bd-indent { margin-left: 3.8em; } + +.Bl-bullet { list-style-type: disc; + padding-left: 1em; } +.Bl-bullet > li { } +.Bl-dash { list-style-type: none; + padding-left: 0em; } +.Bl-dash > li:before { + content: "\2014 "; } +.Bl-item { list-style-type: none; + padding-left: 0em; } +.Bl-item > li { } +.Bl-compact > li { + margin-top: 0em; } + +.Bl-enum { padding-left: 2em; } +.Bl-enum > li { } +.Bl-compact > li { + margin-top: 0em; } + +.Bl-diag { } +.Bl-diag > dt { + font-style: normal; + font-weight: bold; } +.Bl-diag > dd { + margin-left: 0em; } +.Bl-hang { } +.Bl-hang > dt { } +.Bl-hang > dd { + margin-left: 5.5em; } +.Bl-inset { } +.Bl-inset > dt { } +.Bl-inset > dd { + margin-left: 0em; } +.Bl-ohang { } +.Bl-ohang > dt { } +.Bl-ohang > dd { + margin-left: 0em; } +.Bl-tag { margin-top: 0.6em; + margin-left: 5.5em; } +.Bl-tag > dt { + float: left; + margin-top: 0em; + margin-left: -5.5em; + padding-right: 0.5em; + vertical-align: top; } +.Bl-tag > dd { + clear: right; + width: 100%; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0.6em; + vertical-align: top; + overflow: auto; } +.Bl-compact { margin-top: 0em; } +.Bl-compact > dd { + margin-bottom: 0em; } +.Bl-compact > dt { + margin-top: 0em; } + +.Bl-column { } +.Bl-column > tbody > tr { } +.Bl-column > tbody > tr > td { + margin-top: 1em; } +.Bl-compact > tbody > tr > td { + margin-top: 0em; } + +.Rs { font-style: normal; + font-weight: normal; } +.RsA { } +.RsB { font-style: italic; + font-weight: normal; } +.RsC { } +.RsD { } +.RsI { font-style: italic; + font-weight: normal; } +.RsJ { font-style: italic; + font-weight: normal; } +.RsN { } +.RsO { } +.RsP { } +.RsQ { } +.RsR { } +.RsT { text-decoration: underline; } +.RsU { } +.RsV { } + +.eqn { } +.tbl td { vertical-align: middle; } + +.HP { margin-left: 3.8em; + text-indent: -3.8em; } + +/* Semantic markup for command line utilities. */ + +table.Nm { } +code.Nm { font-style: normal; + font-weight: bold; + font-family: inherit; } +.Fl { font-style: normal; + font-weight: bold; + font-family: inherit; } +.Cm { font-style: normal; + font-weight: bold; + font-family: inherit; } +.Ar { font-style: italic; + font-weight: normal; } +.Op { display: inline; } +.Ic { font-style: normal; + font-weight: bold; + font-family: inherit; } +.Ev { font-style: normal; + font-weight: normal; + font-family: monospace; } +.Pa { font-style: italic; + font-weight: normal; } + +/* Semantic markup for function libraries. */ + +.Lb { } +code.In { font-style: normal; + font-weight: bold; + font-family: inherit; } +a.In { } +.Fd { font-style: normal; + font-weight: bold; + font-family: inherit; } +.Ft { font-style: italic; + font-weight: normal; } +.Fn { font-style: normal; + font-weight: bold; + font-family: inherit; } +.Fa { font-style: italic; + font-weight: normal; } +.Vt { font-style: italic; + font-weight: normal; } +.Va { font-style: italic; + font-weight: normal; } +.Dv { font-style: normal; + font-weight: normal; + font-family: monospace; } +.Er { font-style: normal; + font-weight: normal; + font-family: monospace; } + +/* Various semantic markup. */ + +.An { } +.Lk { } +.Mt { } +.Cd { font-style: normal; + font-weight: bold; + font-family: inherit; } +.Ad { font-style: italic; + font-weight: normal; } +.Ms { font-style: normal; + font-weight: bold; } +.St { } +.Ux { } + +/* Physical markup. */ + +.Bf { display: inline; } +.No { font-style: normal; + font-weight: normal; } +.Em { font-style: italic; + font-weight: normal; } +.Sy { font-style: normal; + font-weight: bold; } +.Li { font-style: normal; + font-weight: normal; + font-family: monospace; } + +/* Overrides to avoid excessive margins on small devices. */ + +@media (max-width: 37.5em) { +.manual-text { + margin-left: 0.5em; } +h1.Sh, h2.Ss { margin-left: 0em; } +.Bd-indent { margin-left: 2em; } +.Bl-hang > dd { + margin-left: 2em; } +.Bl-tag { margin-left: 2em; } +.Bl-tag > dt { + margin-left: -2em; } +.HP { margin-left: 2em; + text-indent: -2em; } +}
A docs/ping.txt

@@ -0,0 +1,86 @@

+ +A Ping extension for ActivityPub + +This is merely a draft. + +-- rationale + +Diagnosing communication failures between federated servers often requires +sending test messages. There is no dedicated activity type for this purpose, +however, and thus many operators use normal notes. This creates unnecessary +noise. It would be better to have a side effect free message that can be +triggered and sent on demand. + +The proposed Ping and corresponding Pong activities are similar to the ICMP +echo request and echo reply messages. (c.f. the familiar ping tool.) + +Other online social contexts often use the term ping to refer to a variety +of activities. The activity here is unrelated to any user visible activity or +action. + +-- message format + +The ping message has a type of Ping. Here, user pinger on server +h1.example.com is sending a Ping to testrcpt on h2.example.com. + +{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Ping", + "id": "https://h1.example.com/u/pinger/ping/r4nd0m1d", + "actor": "https://h1.example.com/u/pinger", + "to": "https://h2.example.com/u/testrcpt" +} + +The Pong message is similar, but includes an object field quoting the Ping id +field. + +{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Pong", + "id": "https://h2.example.com/u/testrcpt/pong/0pp0s1t3", + "actor": "https://h2.example.com/u/testrcpt", + "to": "https://h1.example.com/u/pinger", + "object": "https://h1.example.com/u/pinger/ping/r4nd0m1d" +} + +Ping and Pong id fields look like URLs, but need not be fetchable. They are +only intended as transient messages. + +-- semantics + +The Ping message should be sent from one actor to another, delivered to their +inbox. Upon receipt of a Ping message, a server should reply with a Pong +message. The Pong reply should quote the id of the Ping (just the id, not the +whole message) in the object field. + +Random ids may used. They should be probabilistically unique. + +The usual access and verification checks performed for other messages should +be performed for Ping and Pong as well. (If HTTP signatures are in use, +messages should be signed by senders and verified by receivers.) + +Ping and Pong messages should be queued using the normal facilities. (Don't +fast track.) Messages should not be retried. After one failure, drop the +message. + +As these messages are intended as administrator aids, they should not be +displayed to end users. They should not cause any lasting change in the state +of either the sending or receiving server. + +Rate limiting and abuse controls apply as usual. Servers may choose to impose +length restrictions on maximum id length. A minimum of 256 bytes should be +supported. + +Servers which do not understand the Ping activity will hopefully ignore it. + +-- usage + +It is unspecified how one initiates a ping, but it is expected to be a manual +operation performed by a system administrator. This will generate traffic, +which may then be logged. The admin reads the logs and solves the problem. +Specific problem solving instructions are not provided here. + +-- future + +It may be helpful to have a variant of Ping that does perform retries to test +recovery after disconnect.
A docs/vim.3

@@ -0,0 +1,81 @@

+.\" +.\" Copyright (c) 2019 Ted Unangst +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt VIM 3 +.Os +.Sh NAME +.Nm vim +.Nd vital improvements module +.Sh DESCRIPTION +The vital improvements module, +.Nm , +is used to customize and extend honk in such rare cases as the +existing functionality proves insufficient. +.Ss Files +.Bl -tag -width deliverator.go +.It activity.go +Conversion to and from ActivityPub format and interop with other +implementations. +.It admin.go +The console admin interface. +.It avatar.go +Code to generate blocky avatar images. +.It backend.go +Interface to the image resizing backend helper process. +.It bloat.go +Bad stuff. +.It database.go +Loading and saving things to database. +.It deliverator.go +Sending messages and handling retries. +.It fun.go +All sorts of fun stuff. +.It hfcs.go +Filtering framework. +.It honk.go +Just a few data types. +.It hoot.go +Twitter scraper. +.It import.go +Importers from other service data dumps. +.It markitzero.go +Markdown converter. +.It schema.go +Generated from schema.sql. +.It sensors.go +Monitor memory and CPU. +.It skulduggery.go +Reduce some stupidity. +.It unveil.go +OpenBSD pledge and unveil. +.It upgradedb.go +Upgrade between schema versions. +.It util.go +Boring code. +.It web.go +The web interface. +.El +.Ss Schema +The current schema is stored in +.Pa schema.sql . +.Pp +After changing the schema, edit +.Pa upgradedb.go +to update +.Va myVersion +and add relevant update statements to the bottom of the large switch. +.Sh SEE ALSO +.Xr honk 3
A fun.go

@@ -0,0 +1,695 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "crypto/rand" + "crypto/sha512" + "fmt" + "html/template" + "io" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "time" + + "golang.org/x/net/html" + "humungus.tedunangst.com/r/webs/cache" + "humungus.tedunangst.com/r/webs/htfilter" + "humungus.tedunangst.com/r/webs/httpsig" + "humungus.tedunangst.com/r/webs/mz" + "humungus.tedunangst.com/r/webs/templates" +) + +var allowedclasses = make(map[string]bool) + +func init() { + allowedclasses["kw"] = true + allowedclasses["bi"] = true + allowedclasses["st"] = true + allowedclasses["nm"] = true + allowedclasses["tp"] = true + allowedclasses["op"] = true + allowedclasses["cm"] = true + allowedclasses["al"] = true + allowedclasses["dl"] = true +} + +var relingo = make(map[string]string) + +func loadLingo() { + for _, l := range []string{"honked", "bonked", "honked back", "qonked", "evented"} { + v := l + k := "lingo-" + strings.ReplaceAll(l, " ", "") + getconfig(k, &v) + relingo[l] = v + } +} + +func reverbolate(userid int64, honks []*Honk) { + var user *WhatAbout + somenumberedusers.Get(userid, &user) + for _, h := range honks { + h.What += "ed" + if h.What == "tonked" { + h.What = "honked back" + h.Style += " subtle" + } + if !h.Public { + h.Style += " limited" + } + if h.Whofore == 1 { + h.Style += " atme" + } + translate(h) + local := false + if h.Whofore == 2 || h.Whofore == 3 { + local = true + } + if local && h.What != "bonked" { + h.Noise = re_memes.ReplaceAllString(h.Noise, "") + } + h.Username, h.Handle = handles(h.Honker) + if !local { + short := shortname(userid, h.Honker) + if short != "" { + h.Username = short + } else { + h.Username = h.Handle + if len(h.Username) > 20 { + h.Username = h.Username[:20] + ".." + } + } + } + if user != nil { + if user.Options.MentionAll { + hset := []string{"@" + h.Handle} + for _, a := range h.Audience { + if a == h.Honker || a == user.URL { + continue + } + _, hand := handles(a) + if hand != "" { + hand = "@" + hand + hset = append(hset, hand) + } + } + h.Handles = strings.Join(hset, " ") + } else if h.Honker != user.URL { + h.Handles = "@" + h.Handle + } + } + if h.URL == "" { + h.URL = h.XID + } + if h.Oonker != "" { + _, h.Oondle = handles(h.Oonker) + } + h.Precis = demoji(h.Precis) + h.Noise = demoji(h.Noise) + h.Open = "open" + for _, m := range h.Mentions { + if m.Where != h.Honker && !m.IsPresent(h.Noise) { + h.Noise = "(" + m.Who + ")" + h.Noise + } + } + + zap := make(map[string]bool) + { + var htf htfilter.Filter + htf.Imager = replaceimgsand(zap, false) + htf.SpanClasses = allowedclasses + htf.BaseURL, _ = url.Parse(h.XID) + emuxifier := func(e string) string { + for _, d := range h.Donks { + if d.Name == e { + zap[d.XID] = true + if d.Local { + return fmt.Sprintf(`<img class="emu" title="%s" src="/d/%s">`, d.Name, d.XID) + } + } + } + if local && h.What != "bonked" { + var emu Emu + emucache.Get(e, &emu) + if emu.ID != "" { + return fmt.Sprintf(`<img class="emu" title="%s" src="%s">`, emu.Name, emu.ID) + } + } + return e + } + htf.FilterText = func(w io.Writer, data string) { + data = htfilter.EscapeText(data) + data = re_emus.ReplaceAllStringFunc(data, emuxifier) + io.WriteString(w, data) + } + p, _ := htf.String(h.Precis) + n, _ := htf.String(h.Noise) + h.Precis = string(p) + h.Noise = string(n) + } + j := 0 + for i := 0; i < len(h.Donks); i++ { + if !zap[h.Donks[i].XID] { + h.Donks[j] = h.Donks[i] + j++ + } + } + h.Donks = h.Donks[:j] + } + + unsee(honks, userid) + + for _, h := range honks { + renderflags(h) + + h.HTPrecis = template.HTML(h.Precis) + h.HTML = template.HTML(h.Noise) + if h.What == "wonked" { + h.HTML = "? wonk ?" + } + if redo := relingo[h.What]; redo != "" { + h.What = redo + } + } +} + +func replaceimgsand(zap map[string]bool, absolute bool) func(node *html.Node) string { + return func(node *html.Node) string { + src := htfilter.GetAttr(node, "src") + alt := htfilter.GetAttr(node, "alt") + //title := GetAttr(node, "title") + if htfilter.HasClass(node, "Emoji") && alt != "" { + return alt + } + d := finddonk(src) + if d != nil { + zap[d.XID] = true + base := "" + if absolute { + base = "https://" + serverName + } + return string(templates.Sprintf(`<img alt="%s" title="%s" src="%s/d/%s">`, alt, alt, base, d.XID)) + } + return string(templates.Sprintf(`&lt;img alt="%s" src="<a href="%s">%s</a>"&gt;`, alt, src, src)) + } +} + +func translatechonk(ch *Chonk) { + noise := ch.Noise + if ch.Format == "markdown" { + noise = markitzero(noise) + } + var htf htfilter.Filter + htf.SpanClasses = allowedclasses + htf.BaseURL, _ = url.Parse(ch.XID) + ch.HTML, _ = htf.String(noise) +} + +func filterchonk(ch *Chonk) { + translatechonk(ch) + + noise := string(ch.HTML) + + local := originate(ch.XID) == serverName + + zap := make(map[string]bool) + emuxifier := func(e string) string { + for _, d := range ch.Donks { + if d.Name == e { + zap[d.XID] = true + if d.Local { + return fmt.Sprintf(`<img class="emu" title="%s" src="/d/%s">`, d.Name, d.XID) + } + } + } + if local { + var emu Emu + emucache.Get(e, &emu) + if emu.ID != "" { + return fmt.Sprintf(`<img class="emu" title="%s" src="%s">`, emu.Name, emu.ID) + } + } + return e + } + noise = re_emus.ReplaceAllStringFunc(noise, emuxifier) + j := 0 + for i := 0; i < len(ch.Donks); i++ { + if !zap[ch.Donks[i].XID] { + ch.Donks[j] = ch.Donks[i] + j++ + } + } + ch.Donks = ch.Donks[:j] + + if strings.HasPrefix(noise, "<p>") { + noise = noise[3:] + } + ch.HTML = template.HTML(noise) + if short := shortname(ch.UserID, ch.Who); short != "" { + ch.Handle = short + } else { + ch.Handle, _ = handles(ch.Who) + } + +} + +func inlineimgsfor(honk *Honk) func(node *html.Node) string { + return func(node *html.Node) string { + src := htfilter.GetAttr(node, "src") + alt := htfilter.GetAttr(node, "alt") + d := savedonk(src, "image", alt, "image", true) + if d != nil { + honk.Donks = append(honk.Donks, d) + } + dlog.Printf("inline img with src: %s", src) + return "" + } +} + +func imaginate(honk *Honk) { + var htf htfilter.Filter + htf.Imager = inlineimgsfor(honk) + htf.BaseURL, _ = url.Parse(honk.XID) + htf.String(honk.Noise) +} + +func translate(honk *Honk) { + if honk.Format == "html" { + return + } + noise := honk.Noise + if strings.HasPrefix(noise, "DZ:") { + idx := strings.Index(noise, "\n") + if idx == -1 { + honk.Precis = noise + noise = "" + } else { + honk.Precis = noise[:idx] + noise = noise[idx+1:] + } + } + honk.Precis = markitzero(strings.TrimSpace(honk.Precis)) + + var marker mz.Marker + marker.HashLinker = ontoreplacer + marker.AtLinker = attoreplacer + noise = strings.TrimSpace(noise) + noise = marker.Mark(noise) + honk.Noise = noise + honk.Onts = oneofakind(marker.HashTags) + honk.Mentions = bunchofgrapes(marker.Mentions) +} + +func redoimages(honk *Honk) { + zap := make(map[string]bool) + { + var htf htfilter.Filter + htf.Imager = replaceimgsand(zap, true) + htf.SpanClasses = allowedclasses + p, _ := htf.String(honk.Precis) + n, _ := htf.String(honk.Noise) + honk.Precis = string(p) + honk.Noise = string(n) + } + j := 0 + for i := 0; i < len(honk.Donks); i++ { + if !zap[honk.Donks[i].XID] { + honk.Donks[j] = honk.Donks[i] + j++ + } + } + honk.Donks = honk.Donks[:j] + + honk.Noise = re_memes.ReplaceAllString(honk.Noise, "") + honk.Noise = strings.Replace(honk.Noise, "<a href=", "<a class=\"mention u-url\" href=", -1) +} + +func xcelerate(b []byte) string { + letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234" + for i, c := range b { + b[i] = letters[c&63] + } + s := string(b) + return s +} + +func shortxid(xid string) string { + h := sha512.New512_256() + io.WriteString(h, xid) + return xcelerate(h.Sum(nil)[:20]) +} + +func xfiltrate() string { + var b [18]byte + rand.Read(b[:]) + return xcelerate(b[:]) +} + +func grapevine(mentions []Mention) []string { + var s []string + for _, m := range mentions { + s = append(s, m.Where) + } + return s +} + +func bunchofgrapes(m []string) []Mention { + var mentions []Mention + for i := range m { + where := gofish(m[i]) + if where != "" { + mentions = append(mentions, Mention{Who: m[i], Where: where}) + } + } + return mentions +} + +type Emu struct { + ID string + Name string + Type string +} + +var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`) + +var emucache = cache.New(cache.Options{Filler: func(ename string) (Emu, bool) { + fname := ename[1 : len(ename)-1] + exts := []string{".png", ".gif"} + for _, ext := range exts { + _, err := os.Stat(dataDir + "/emus/" + fname + ext) + if err != nil { + continue + } + url := fmt.Sprintf("https://%s/emu/%s%s", serverName, fname, ext) + return Emu{ID: url, Name: ename, Type: "image/" + ext[1:]}, true + } + return Emu{Name: ename, ID: "", Type: "image/png"}, true +}, Duration: 10 * time.Second}) + +func herdofemus(noise string) []Emu { + m := re_emus.FindAllString(noise, -1) + m = oneofakind(m) + var emus []Emu + for _, e := range m { + var emu Emu + emucache.Get(e, &emu) + if emu.ID == "" { + continue + } + emus = append(emus, emu) + } + return emus +} + +var re_memes = regexp.MustCompile("meme: ?([^\n]+)") +var re_avatar = regexp.MustCompile("avatar: ?([^\n]+)") +var re_banner = regexp.MustCompile("banner: ?([^\n]+)") + +func memetize(honk *Honk) { + repl := func(x string) string { + name := x[5:] + if name[0] == ' ' { + name = name[1:] + } + fd, err := os.Open(dataDir + "/memes/" + name) + if err != nil { + ilog.Printf("no meme for %s", name) + return x + } + var peek [512]byte + n, _ := fd.Read(peek[:]) + ct := http.DetectContentType(peek[:n]) + fd.Close() + + url := fmt.Sprintf("https://%s/meme/%s", serverName, name) + fileid, err := savefile(name, name, url, ct, false, nil) + if err != nil { + elog.Printf("error saving meme: %s", err) + return x + } + d := &Donk{ + FileID: fileid, + Name: name, + Media: ct, + URL: url, + Local: false, + } + honk.Donks = append(honk.Donks, d) + return "" + } + honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl) +} + +var re_quickmention = regexp.MustCompile("(^|[ \n])@[[:alnum:]]+([ \n.]|$)") + +func quickrename(s string, userid int64) string { + nonstop := true + for nonstop { + nonstop = false + s = re_quickmention.ReplaceAllStringFunc(s, func(m string) string { + prefix := "" + if m[0] == ' ' || m[0] == '\n' { + prefix = m[:1] + m = m[1:] + } + prefix += "@" + m = m[1:] + tail := "" + if last := m[len(m)-1]; last == ' ' || last == '\n' || last == '.' { + tail = m[len(m)-1:] + m = m[:len(m)-1] + } + + xid := fullname(m, userid) + + if xid != "" { + _, name := handles(xid) + if name != "" { + nonstop = true + m = name + } + } + return prefix + m + tail + }) + } + return s +} + +var shortnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) { + honkers := gethonkers(userid) + m := make(map[string]string) + for _, h := range honkers { + m[h.XID] = h.Name + } + return m, true +}, Invalidator: &honkerinvalidator}) + +func shortname(userid int64, xid string) string { + var m map[string]string + ok := shortnames.Get(userid, &m) + if ok { + return m[xid] + } + return "" +} + +var fullnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) { + honkers := gethonkers(userid) + m := make(map[string]string) + for _, h := range honkers { + m[h.Name] = h.XID + } + return m, true +}, Invalidator: &honkerinvalidator}) + +func fullname(name string, userid int64) string { + var m map[string]string + ok := fullnames.Get(userid, &m) + if ok { + return m[name] + } + return "" +} + +func attoreplacer(m string) string { + fill := `<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>` + where := gofish(m) + if where == "" { + return m + } + who := m[0 : 1+strings.IndexByte(m[1:], '@')] + return fmt.Sprintf(fill, html.EscapeString(where), html.EscapeString(who)) +} + +func ontoreplacer(h string) string { + return fmt.Sprintf(`<a href="https://%s/o/%s">%s</a>`, serverName, + strings.ToLower(h[1:]), h) +} + +var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)") +var re_urlhost = regexp.MustCompile("https://([^/ #)]+)") + +func originate(u string) string { + m := re_urlhost.FindStringSubmatch(u) + if len(m) > 1 { + return m[1] + } + return "" +} + +var allhandles = cache.New(cache.Options{Filler: func(xid string) (string, bool) { + handle := getxonker(xid, "handle") + if handle == "" { + dlog.Printf("need to get a handle: %s", xid) + info, err := investigate(xid) + if err != nil { + m := re_unurl.FindStringSubmatch(xid) + if len(m) > 2 { + handle = m[2] + } else { + handle = xid + } + } else { + handle = info.Name + } + } + return handle, true +}}) + +// handle, handle@host +func handles(xid string) (string, string) { + if xid == "" || xid == thewholeworld || strings.HasSuffix(xid, "/followers") { + return "", "" + } + var handle string + allhandles.Get(xid, &handle) + if handle == xid { + return xid, xid + } + return handle, handle + "@" + originate(xid) +} + +func butnottooloud(aud []string) { + for i, a := range aud { + if strings.HasSuffix(a, "/followers") { + aud[i] = "" + } + } +} + +func loudandproud(aud []string) bool { + for _, a := range aud { + if a == thewholeworld { + return true + } + } + return false +} + +func firstclass(honk *Honk) bool { + return honk.Audience[0] == thewholeworld +} + +func oneofakind(a []string) []string { + seen := make(map[string]bool) + seen[""] = true + j := 0 + for _, s := range a { + if !seen[s] { + seen[s] = true + a[j] = s + j++ + } + } + return a[:j] +} + +var ziggies = cache.New(cache.Options{Filler: func(userid int64) (*KeyInfo, bool) { + var user *WhatAbout + ok := somenumberedusers.Get(userid, &user) + if !ok { + return nil, false + } + ki := new(KeyInfo) + ki.keyname = user.URL + "#key" + ki.seckey = user.SecKey + return ki, true +}}) + +func ziggy(userid int64) *KeyInfo { + var ki *KeyInfo + ziggies.Get(userid, &ki) + return ki +} + +var zaggies = cache.New(cache.Options{Filler: func(keyname string) (httpsig.PublicKey, bool) { + data := getxonker(keyname, "pubkey") + if data == "" { + dlog.Printf("hitting the webs for missing pubkey: %s", keyname) + j, err := GetJunk(serverUID, keyname) + if err != nil { + ilog.Printf("error getting %s pubkey: %s", keyname, err) + when := time.Now().UTC().Format(dbtimeformat) + stmtSaveXonker.Exec(keyname, "failed", "pubkey", when) + return httpsig.PublicKey{}, true + } + allinjest(originate(keyname), j) + data = getxonker(keyname, "pubkey") + if data == "" { + ilog.Printf("key not found after ingesting") + when := time.Now().UTC().Format(dbtimeformat) + stmtSaveXonker.Exec(keyname, "failed", "pubkey", when) + return httpsig.PublicKey{}, true + } + } + if data == "failed" { + ilog.Printf("lookup previously failed key %s", keyname) + return httpsig.PublicKey{}, true + } + _, key, err := httpsig.DecodeKey(data) + if err != nil { + ilog.Printf("error decoding %s pubkey: %s", keyname, err) + return key, true + } + return key, true +}, Limit: 512}) + +func zaggy(keyname string) (httpsig.PublicKey, error) { + var key httpsig.PublicKey + zaggies.Get(keyname, &key) + return key, nil +} + +func savingthrow(keyname string) { + when := time.Now().Add(-30 * time.Minute).UTC().Format(dbtimeformat) + stmtDeleteXonker.Exec(keyname, "pubkey", when) + zaggies.Clear(keyname) +} + +func keymatch(keyname string, actor string) string { + hash := strings.IndexByte(keyname, '#') + if hash == -1 { + hash = len(keyname) + } + owner := keyname[0:hash] + if owner == actor { + return originate(actor) + } + return "" +}
A genschemago.sh

@@ -0,0 +1,6 @@

+echo "package main" > schema.go +echo "var sqlSchema = \`" >> schema.go +cat schema.sql >> schema.go +echo "\`" >> schema.go +go fmt schema.go +
A go.mod

@@ -0,0 +1,13 @@

+module humungus.tedunangst.com/r/honk + +go 1.16 + +require ( + github.com/andybalholm/cascadia v1.3.1 + github.com/gorilla/mux v1.8.0 + github.com/mattn/go-runewidth v0.0.13 + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 + humungus.tedunangst.com/r/go-sqlite3 v1.1.3 + humungus.tedunangst.com/r/webs v0.6.56 +)
A go.sum

@@ -0,0 +1,29 @@

+github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE= +golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +humungus.tedunangst.com/r/go-sqlite3 v1.1.3 h1:G2N4wzDS0NbuvrZtQJhh4F+3X+s7BF8b9ga8k38geUI= +humungus.tedunangst.com/r/go-sqlite3 v1.1.3/go.mod h1:FtEEmQM7U2Ey1TuEEOyY1BmphTZnmiEjPsNLEAkpf/M= +humungus.tedunangst.com/r/webs v0.6.56 h1:knrLMQ8vwcHjkPo//zXUm5IuRFMfYcnJgMuWuqy8BOA= +humungus.tedunangst.com/r/webs v0.6.56/go.mod h1:03R0N9BcT49HB4TDd1YmarpbiPvPzVDm74Mk4h1hYPc=
A hfcs.go

@@ -0,0 +1,465 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "net/http" + "regexp" + "sort" + "time" + + "humungus.tedunangst.com/r/webs/cache" +) + +type Filter struct { + ID int64 `json:"-"` + Actions []filtType `json:"-"` + Name string + Date time.Time + Actor string `json:",omitempty"` + IncludeAudience bool `json:",omitempty"` + Text string `json:",omitempty"` + re_text *regexp.Regexp + IsAnnounce bool `json:",omitempty"` + AnnounceOf string `json:",omitempty"` + Reject bool `json:",omitempty"` + SkipMedia bool `json:",omitempty"` + Hide bool `json:",omitempty"` + Collapse bool `json:",omitempty"` + Rewrite string `json:",omitempty"` + re_rewrite *regexp.Regexp + Replace string `json:",omitempty"` + Expiration time.Time + Notes string +} + +type filtType uint + +const ( + filtNone filtType = iota + filtAny + filtReject + filtSkipMedia + filtHide + filtCollapse + filtRewrite +) + +var filtNames = []string{"None", "Any", "Reject", "SkipMedia", "Hide", "Collapse", "Rewrite"} + +func (ft filtType) String() string { + return filtNames[ft] +} + +type afiltermap map[filtType][]*Filter + +var filtInvalidator cache.Invalidator +var filtcache *cache.Cache + +func init() { + // resolve init loop + filtcache = cache.New(cache.Options{Filler: filtcachefiller, Invalidator: &filtInvalidator}) +} + +func filtcachefiller(userid int64) (afiltermap, bool) { + rows, err := stmtGetFilters.Query(userid) + if err != nil { + elog.Printf("error querying filters: %s", err) + return nil, false + } + defer rows.Close() + + now := time.Now() + + var expflush time.Time + + filtmap := make(afiltermap) + for rows.Next() { + filt := new(Filter) + var j string + var filterid int64 + err = rows.Scan(&filterid, &j) + if err == nil { + err = unjsonify(j, filt) + } + if err != nil { + elog.Printf("error scanning filter: %s", err) + continue + } + if !filt.Expiration.IsZero() { + if filt.Expiration.Before(now) { + continue + } + if expflush.IsZero() || filt.Expiration.Before(expflush) { + expflush = filt.Expiration + } + } + if t := filt.Text; t != "" { + wordfront := t[0] != '#' + wordtail := true + t = "(?i:" + t + ")" + if wordfront { + t = "\\b" + t + } + if wordtail { + t = t + "\\b" + } + filt.re_text, err = regexp.Compile(t) + if err != nil { + elog.Printf("error compiling filter text: %s", err) + continue + } + } + if t := filt.Rewrite; t != "" { + wordfront := t[0] != '#' + wordtail := true + t = "(?i:" + t + ")" + if wordfront { + t = "\\b" + t + } + if wordtail { + t = t + "\\b" + } + filt.re_rewrite, err = regexp.Compile(t) + if err != nil { + elog.Printf("error compiling filter rewrite: %s", err) + continue + } + } + filt.ID = filterid + if filt.Reject { + filt.Actions = append(filt.Actions, filtReject) + filtmap[filtReject] = append(filtmap[filtReject], filt) + } + if filt.SkipMedia { + filt.Actions = append(filt.Actions, filtSkipMedia) + filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt) + } + if filt.Hide { + filt.Actions = append(filt.Actions, filtHide) + filtmap[filtHide] = append(filtmap[filtHide], filt) + } + if filt.Collapse { + filt.Actions = append(filt.Actions, filtCollapse) + filtmap[filtCollapse] = append(filtmap[filtCollapse], filt) + } + if filt.Rewrite != "" { + filt.Actions = append(filt.Actions, filtRewrite) + filtmap[filtRewrite] = append(filtmap[filtRewrite], filt) + } + filtmap[filtAny] = append(filtmap[filtAny], filt) + } + sorting := filtmap[filtAny] + sort.Slice(filtmap[filtAny], func(i, j int) bool { + return sorting[i].Name < sorting[j].Name + }) + if !expflush.IsZero() { + dur := expflush.Sub(now) + go filtcacheclear(userid, dur) + } + return filtmap, true +} + +func filtcacheclear(userid int64, dur time.Duration) { + time.Sleep(dur + time.Second) + filtInvalidator.Clear(userid) +} + +func getfilters(userid int64, scope filtType) []*Filter { + var filtmap afiltermap + ok := filtcache.Get(userid, &filtmap) + if ok { + return filtmap[scope] + } + return nil +} + +type arejectmap map[string][]*Filter + +var rejectAnyKey = "..." + +var rejectcache = cache.New(cache.Options{Filler: func(userid int64) (arejectmap, bool) { + m := make(arejectmap) + filts := getfilters(userid, filtReject) + for _, f := range filts { + if f.Text != "" { + key := rejectAnyKey + m[key] = append(m[key], f) + continue + } + if f.IsAnnounce && f.AnnounceOf != "" { + key := f.AnnounceOf + m[key] = append(m[key], f) + } + if f.Actor != "" { + key := f.Actor + m[key] = append(m[key], f) + } + } + return m, true +}, Invalidator: &filtInvalidator}) + +func rejectfilters(userid int64, name string) []*Filter { + var m arejectmap + rejectcache.Get(userid, &m) + return m[name] +} + +func rejectorigin(userid int64, origin string, isannounce bool) bool { + if o := originate(origin); o != "" { + origin = o + } + filts := rejectfilters(userid, origin) + for _, f := range filts { + if isannounce && f.IsAnnounce { + if f.AnnounceOf == origin { + return true + } + } + if f.Actor == origin { + return true + } + } + return false +} + +func rejectactor(userid int64, actor string) bool { + filts := rejectfilters(userid, actor) + for _, f := range filts { + if f.IsAnnounce { + continue + } + if f.Actor == actor { + ilog.Printf("rejecting actor: %s", actor) + return true + } + } + origin := originate(actor) + if origin == "" { + return false + } + filts = rejectfilters(userid, origin) + for _, f := range filts { + if f.IsAnnounce { + continue + } + if f.Actor == origin { + ilog.Printf("rejecting actor: %s", actor) + return true + } + } + return false +} + +func stealthmode(userid int64, r *http.Request) bool { + agent := r.UserAgent() + agent = originate(agent) + if agent != "" { + fake := rejectorigin(userid, agent, false) + if fake { + ilog.Printf("faking 404 for %s", agent) + return true + } + } + return false +} + +func matchfilter(h *Honk, f *Filter) bool { + return matchfilterX(h, f) != "" +} + +func matchfilterX(h *Honk, f *Filter) string { + rv := "" + match := true + if match && f.Actor != "" { + match = false + if f.Actor == h.Honker || f.Actor == h.Oonker { + match = true + rv = f.Actor + } + if !match && (f.Actor == originate(h.Honker) || + f.Actor == originate(h.Oonker) || + f.Actor == originate(h.XID)) { + match = true + rv = f.Actor + } + if !match && f.IncludeAudience { + for _, a := range h.Audience { + if f.Actor == a || f.Actor == originate(a) { + match = true + rv = f.Actor + break + } + } + } + } + if match && f.IsAnnounce { + match = false + if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker || + f.AnnounceOf == originate(h.Oonker) { + match = true + rv += " announce" + } + } + if match && f.Text != "" { + match = false + re := f.re_text + m := re.FindString(h.Precis) + if m == "" { + m = re.FindString(h.Noise) + } + if m == "" { + for _, d := range h.Donks { + m = re.FindString(d.Desc) + if m != "" { + break + } + } + } + if m != "" { + match = true + rv = m + } + } + if match { + return rv + } + return "" +} + +func rejectxonk(xonk *Honk) bool { + var m arejectmap + rejectcache.Get(xonk.UserID, &m) + filts := m[rejectAnyKey] + filts = append(filts, m[xonk.Honker]...) + filts = append(filts, m[originate(xonk.Honker)]...) + filts = append(filts, m[xonk.Oonker]...) + filts = append(filts, m[originate(xonk.Oonker)]...) + for _, a := range xonk.Audience { + filts = append(filts, m[a]...) + filts = append(filts, m[originate(a)]...) + } + for _, f := range filts { + if cause := matchfilterX(xonk, f); cause != "" { + ilog.Printf("rejecting %s because %s", xonk.XID, cause) + return true + } + } + return false +} + +func skipMedia(xonk *Honk) bool { + filts := getfilters(xonk.UserID, filtSkipMedia) + for _, f := range filts { + if matchfilter(xonk, f) { + return true + } + } + return false +} + +func unsee(honks []*Honk, userid int64) { + if userid != -1 { + colfilts := getfilters(userid, filtCollapse) + rwfilts := getfilters(userid, filtRewrite) + for _, h := range honks { + for _, f := range colfilts { + if bad := matchfilterX(h, f); bad != "" { + if h.Precis == "" { + h.Precis = bad + } + h.Open = "" + break + } + } + if h.Open == "open" && h.Precis == "unspecified horror" { + h.Precis = "" + } + for _, f := range rwfilts { + if matchfilter(h, f) { + h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace) + } + } + if len(h.Noise) > 6000 && h.Open == "open" { + if h.Precis == "" { + h.Precis = "really freaking long" + } + h.Open = "" + } + } + } +} + +var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) { + rows, err := stmtUntagged.Query(userid) + if err != nil { + elog.Printf("error query untagged: %s", err) + return nil, false + } + defer rows.Close() + bad := make(map[string]bool) + for rows.Next() { + var xid, rid string + var flags int64 + err = rows.Scan(&xid, &rid, &flags) + if err != nil { + elog.Printf("error scanning untag: %s", err) + continue + } + if flags&flagIsUntagged != 0 { + bad[xid] = true + } + if bad[rid] { + bad[xid] = true + } + } + return bad, true +}}) + +func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk { + var badparents map[string]bool + untagged.GetAndLock(userid, &badparents) + j := 0 + reversehonks(honks) + for _, h := range honks { + if badparents[h.RID] { + badparents[h.XID] = true + continue + } + honks[j] = h + j++ + } + untagged.Unlock() + honks = honks[0:j] + reversehonks(honks) + if !withfilt { + return honks + } + filts := getfilters(userid, filtHide) + j = 0 +outer: + for _, h := range honks { + for _, f := range filts { + if matchfilter(h, f) { + continue outer + } + } + honks[j] = h + j++ + } + honks = honks[0:j] + return honks +}
A honk.go

@@ -0,0 +1,419 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "flag" + "fmt" + "html/template" + golog "log" + "log/syslog" + notrand "math/rand" + "os" + "strconv" + "strings" + "time" + + "humungus.tedunangst.com/r/webs/httpsig" + "humungus.tedunangst.com/r/webs/log" +) + +var softwareVersion = "develop" + +func init() { + notrand.Seed(time.Now().Unix()) +} + +type WhatAbout struct { + ID int64 + Name string + Display string + About string + HTAbout template.HTML + Onts []string + Key string + URL string + Options UserOptions + SecKey httpsig.PrivateKey +} + +type UserOptions struct { + SkinnyCSS bool `json:",omitempty"` + OmitImages bool `json:",omitempty"` + Avahex bool `json:",omitempty"` + MentionAll bool `json:",omitempty"` + Avatar string `json:",omitempty"` + Banner string `json:",omitempty"` + MapLink string `json:",omitempty"` + Reaction string `json:",omitempty"` + MeCount int64 + ChatCount int64 +} + +type KeyInfo struct { + keyname string + seckey httpsig.PrivateKey +} + +const serverUID int64 = -2 + +type Honk struct { + ID int64 + UserID int64 + Username string + What string + Honker string + Handle string + Handles string + Oonker string + Oondle string + XID string + RID string + Date time.Time + URL string + Noise string + Precis string + Format string + Convoy string + Audience []string + Public bool + Whofore int64 + Replies []*Honk + Flags int64 + HTPrecis template.HTML + HTML template.HTML + Style string + Open string + Donks []*Donk + Onts []string + Place *Place + Time *Time + Mentions []Mention + Badonks []Badonk + Wonkles string + Guesses template.HTML +} + +type Badonk struct { + Who string + What string +} + +type Chonk struct { + ID int64 + UserID int64 + XID string + Who string + Target string + Date time.Time + Noise string + Format string + Donks []*Donk + Handle string + HTML template.HTML +} + +type Chatter struct { + Target string + Chonks []*Chonk +} + +type Mention struct { + Who string + Where string +} + +func (mention *Mention) IsPresent(noise string) bool { + nick := strings.TrimLeft(mention.Who, "@") + idx := strings.IndexByte(nick, '@') + if idx != -1 { + nick = nick[:idx] + } + return strings.Contains(noise, ">@"+nick) || strings.Contains(noise, "@<span>"+nick) +} + +type OldRevision struct { + Precis string + Noise string +} + +const ( + flagIsAcked = 1 + flagIsBonked = 2 + flagIsSaved = 4 + flagIsUntagged = 8 + flagIsReacted = 16 + flagIsWonked = 32 +) + +func (honk *Honk) IsAcked() bool { + return honk.Flags&flagIsAcked != 0 +} + +func (honk *Honk) IsBonked() bool { + return honk.Flags&flagIsBonked != 0 +} + +func (honk *Honk) IsSaved() bool { + return honk.Flags&flagIsSaved != 0 +} + +func (honk *Honk) IsUntagged() bool { + return honk.Flags&flagIsUntagged != 0 +} + +func (honk *Honk) IsReacted() bool { + return honk.Flags&flagIsReacted != 0 +} + +func (honk *Honk) IsWonked() bool { + return honk.Flags&flagIsWonked != 0 +} + +type Donk struct { + FileID int64 + XID string + Name string + Desc string + URL string + Media string + Local bool + External bool +} + +type Place struct { + Name string + Latitude float64 + Longitude float64 + Url string +} + +type Duration int64 + +func (d Duration) String() string { + s := time.Duration(d).String() + if strings.HasSuffix(s, "m0s") { + s = s[:len(s)-2] + } + if strings.HasSuffix(s, "h0m") { + s = s[:len(s)-2] + } + return s +} + +func parseDuration(s string) time.Duration { + didx := strings.IndexByte(s, 'd') + if didx != -1 { + days, _ := strconv.ParseInt(s[:didx], 10, 0) + dur, _ := time.ParseDuration(s[didx:]) + return dur + 24*time.Hour*time.Duration(days) + } + dur, _ := time.ParseDuration(s) + return dur +} + +type Time struct { + StartTime time.Time + EndTime time.Time + Duration Duration +} + +type Honker struct { + ID int64 + UserID int64 + Name string + XID string + Handle string + Flavor string + Combos []string + Meta HonkerMeta +} + +type HonkerMeta struct { + Notes string +} + +type SomeThing struct { + What int + XID string + Owner string + Name string +} + +const ( + SomeNothing int = iota + SomeActor + SomeCollection +) + +var serverName string +var serverPrefix string +var masqName string +var dataDir = "." +var viewDir = "." +var iconName = "icon.png" +var serverMsg template.HTML +var aboutMsg template.HTML +var loginMsg template.HTML + +func ElaborateUnitTests() { +} + +func unplugserver(hostname string) { + db := opendatabase() + xid := fmt.Sprintf("%%https://%s/%%", hostname) + db.Exec("delete from honkers where xid like ? and flavor = 'dub'", xid) + db.Exec("delete from doovers where rcpt like ?", xid) +} + +func reexecArgs(cmd string) []string { + args := []string{"-datadir", dataDir} + args = append(args, log.Args()...) + args = append(args, cmd) + return args +} + +var elog, ilog, dlog *golog.Logger + +func main() { + flag.StringVar(&dataDir, "datadir", dataDir, "data directory") + flag.StringVar(&viewDir, "viewdir", viewDir, "view directory") + flag.Parse() + + log.Init(log.Options{Progname: "honk", Facility: syslog.LOG_UUCP}) + elog = log.E + ilog = log.I + dlog = log.D + + args := flag.Args() + cmd := "run" + if len(args) > 0 { + cmd = args[0] + } + switch cmd { + case "init": + initdb() + case "upgrade": + upgradedb() + case "version": + fmt.Println(softwareVersion) + os.Exit(0) + } + db := opendatabase() + dbversion := 0 + getconfig("dbversion", &dbversion) + if dbversion != myVersion { + elog.Fatal("incorrect database version. run upgrade.") + } + getconfig("servermsg", &serverMsg) + getconfig("aboutmsg", &aboutMsg) + getconfig("loginmsg", &loginMsg) + getconfig("servername", &serverName) + getconfig("masqname", &masqName) + if masqName == "" { + masqName = serverName + } + serverPrefix = fmt.Sprintf("https://%s/", serverName) + getconfig("usersep", &userSep) + getconfig("honksep", &honkSep) + getconfig("devel", &develMode) + getconfig("fasttimeout", &fastTimeout) + getconfig("slowtimeout", &slowTimeout) + getconfig("signgets", &signGets) + prepareStatements(db) + switch cmd { + case "admin": + adminscreen() + case "import": + if len(args) != 4 { + elog.Fatal("import username mastodon|twitter srcdir") + } + importMain(args[1], args[2], args[3]) + case "devel": + if len(args) != 2 { + elog.Fatal("need an argument: devel (on|off)") + } + switch args[1] { + case "on": + setconfig("devel", 1) + case "off": + setconfig("devel", 0) + default: + elog.Fatal("argument must be on or off") + } + case "setconfig": + if len(args) != 3 { + elog.Fatal("need an argument: setconfig key val") + } + var val interface{} + var err error + if val, err = strconv.Atoi(args[2]); err != nil { + val = args[2] + } + setconfig(args[1], val) + case "adduser": + adduser() + case "deluser": + if len(args) < 2 { + fmt.Printf("usage: honk deluser username\n") + return + } + deluser(args[1]) + case "chpass": + chpass() + case "cleanup": + arg := "30" + if len(args) > 1 { + arg = args[1] + } + cleanupdb(arg) + case "unplug": + if len(args) < 2 { + fmt.Printf("usage: honk unplug servername\n") + return + } + name := args[1] + unplugserver(name) + case "backup": + if len(args) < 2 { + fmt.Printf("usage: honk backup dirname\n") + return + } + name := args[1] + svalbard(name) + case "ping": + if len(args) < 3 { + fmt.Printf("usage: honk ping (from username) (to username or url)\n") + return + } + name := args[1] + targ := args[2] + user, err := butwhatabout(name) + if err != nil { + elog.Printf("unknown user") + return + } + ping(user, targ) + case "run": + serve() + case "backend": + backendServer() + case "test": + ElaborateUnitTests() + default: + elog.Fatal("unknown command") + } +}
A hoot.go

@@ -0,0 +1,182 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "regexp" + "strings" + + "github.com/andybalholm/cascadia" + "golang.org/x/net/html" + "humungus.tedunangst.com/r/webs/htfilter" +) + +var tweetsel = cascadia.MustCompile("div[data-testid=tweetText]") +var linksel = cascadia.MustCompile("a time") +var replyingto = cascadia.MustCompile(".ReplyingToContextBelowAuthor") +var imgsel = cascadia.MustCompile("div[data-testid=tweetPhoto] img") +var authorregex = regexp.MustCompile("twitter.com/([^/]+)") + +var re_hoots = regexp.MustCompile(`hoot: ?https://\S+`) +var re_removepics = regexp.MustCompile(`pic\.twitter\.com/[[:alnum:]]+`) + +func hootextractor(r io.Reader, url string, seen map[string]bool) string { + root, err := html.Parse(r) + if err != nil { + elog.Printf("error parsing hoot: %s", err) + return url + } + + url = strings.Replace(url, "mobile.twitter.com", "twitter.com", -1) + wantmatch := authorregex.FindStringSubmatch(url) + var wanted string + if len(wantmatch) == 2 { + wanted = wantmatch[1] + } + + var htf htfilter.Filter + htf.Imager = func(node *html.Node) string { + alt := htfilter.GetAttr(node, "alt") + if htfilter.HasClass(node, "Emoji") && alt != "" { + return alt + } + return fmt.Sprintf(" <img src='%s'>", htfilter.GetAttr(node, "src")) + } + + var buf strings.Builder + fmt.Fprintf(&buf, "%s\n", url) + + divs := tweetsel.MatchAll(root) + for i, div := range divs { + { + twp := div.Parent.Parent.Parent.Parent.Parent + link := url + alink := linksel.MatchFirst(twp) + if alink == nil { + if i != 0 { + dlog.Printf("missing link") + continue + } + } else { + alink = alink.Parent + link = "https://twitter.com" + htfilter.GetAttr(alink, "href") + } + authormatch := authorregex.FindStringSubmatch(link) + if len(authormatch) < 2 { + dlog.Printf("no author?: %s", link) + continue + } + author := authormatch[1] + if wanted == "" { + wanted = author + } + if author != wanted { + continue + } + for _, img := range imgsel.MatchAll(twp) { + img.Parent.RemoveChild(img) + div.AppendChild(img) + } + text := htf.NodeText(div) + text = strings.Replace(text, "\n", " ", -1) + fmt.Fprintf(&buf, "> @%s: %s\n", author, text) + continue + } + + twp := div.Parent.Parent.Parent.Parent.Parent + link := url + alink := linksel.MatchFirst(twp) + if alink == nil { + if i != 0 { + dlog.Printf("missing link") + continue + } + } else { + link = "https://twitter.com" + htfilter.GetAttr(alink, "href") + } + replto := replyingto.MatchFirst(twp) + if replto != nil { + continue + } + authormatch := authorregex.FindStringSubmatch(link) + if len(authormatch) < 2 { + dlog.Printf("no author?: %s", link) + continue + } + author := authormatch[1] + if wanted == "" { + wanted = author + } + if author != wanted { + continue + } + for _, img := range imgsel.MatchAll(twp) { + img.Parent.RemoveChild(img) + div.AppendChild(img) + } + text := htf.NodeText(div) + text = strings.Replace(text, "\n", " ", -1) + text = re_removepics.ReplaceAllString(text, "") + + if seen[text] { + continue + } + + fmt.Fprintf(&buf, "> @%s: %s\n", author, text) + seen[text] = true + } + return buf.String() +} + +func hooterize(noise string) string { + seen := make(map[string]bool) + + hootfetcher := func(hoot string) string { + url := hoot[5:] + if url[0] == ' ' { + url = url[1:] + } + url = strings.Replace(url, "mobile.twitter.com", "twitter.com", -1) + dlog.Printf("hooterizing %s", url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + ilog.Printf("error: %s", err) + return hoot + } + req.Header.Set("User-Agent", "Bot") + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + resp, err := http.DefaultClient.Do(req) + if err != nil { + ilog.Printf("error: %s", err) + return hoot + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + ilog.Printf("error getting %s: %d", url, resp.StatusCode) + return hoot + } + ld, _ := os.Create("lasthoot.html") + r := io.TeeReader(resp.Body, ld) + return hootextractor(r, url, seen) + } + + return re_hoots.ReplaceAllStringFunc(noise, hootfetcher) +}
A hoot_test.go

@@ -0,0 +1,19 @@

+package main + +import ( + "fmt" + "log" + "os" + "testing" +) + +func TestHooterize(t *testing.T) { + dlog = log.Default() + fd, err := os.Open("lasthoot.html") + if err != nil { + return + } + seen := make(map[string]bool) + hoots := hootextractor(fd, "lasthoot.html", seen) + fmt.Printf("hoots: %s\n", hoots) +}
A import.go

@@ -0,0 +1,346 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "html" + "io/ioutil" + "os" + "regexp" + "sort" + "strings" + "time" +) + +func importMain(username, flavor, source string) { + switch flavor { + case "mastodon": + importMastodon(username, source) + case "twitter": + importTwitter(username, source) + default: + elog.Fatal("unknown source flavor") + } +} + +type TootObject struct { + Summary string + Content string + InReplyTo string + Conversation string + Published time.Time + Tag []struct { + Type string + Name string + } + Attachment []struct { + Type string + MediaType string + Url string + Name string + } +} + +type PlainTootObject TootObject + +func (obj *TootObject) UnmarshalJSON(b []byte) error { + p := (*PlainTootObject)(obj) + json.Unmarshal(b, p) + return nil +} + +func importMastodon(username, source string) { + user, err := butwhatabout(username) + if err != nil { + elog.Fatal(err) + } + + if _, err := os.Stat(source + "/outbox.json"); err == nil { + importMastotoots(user, source) + } else { + ilog.Printf("skipping outbox.json!") + } + if _, err := os.Stat(source + "/following_accounts.csv"); err == nil { + importMastotooters(user, source) + } else { + ilog.Printf("skipping following_accounts.csv!") + } +} + +func importMastotoots(user *WhatAbout, source string) { + type Toot struct { + Id string + Type string + To []string + Cc []string + Object TootObject + } + var outbox struct { + OrderedItems []Toot + } + ilog.Println("Importing honks...") + fd, err := os.Open(source + "/outbox.json") + if err != nil { + elog.Fatal(err) + } + dec := json.NewDecoder(fd) + err = dec.Decode(&outbox) + if err != nil { + elog.Fatalf("error parsing json: %s", err) + } + fd.Close() + + havetoot := func(xid string) bool { + var id int64 + row := stmtFindXonk.QueryRow(user.ID, xid) + err := row.Scan(&id) + if err == nil { + return true + } + return false + } + + re_tootid := regexp.MustCompile("[^/]+$") + for _, item := range outbox.OrderedItems { + toot := item + if toot.Type != "Create" { + continue + } + if strings.HasSuffix(toot.Id, "/activity") { + toot.Id = strings.TrimSuffix(toot.Id, "/activity") + } + tootid := re_tootid.FindString(toot.Id) + xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, tootid) + if havetoot(xid) { + continue + } + honk := Honk{ + UserID: user.ID, + What: "honk", + Honker: user.URL, + XID: xid, + RID: toot.Object.InReplyTo, + Date: toot.Object.Published, + URL: xid, + Audience: append(toot.To, toot.Cc...), + Noise: toot.Object.Content, + Convoy: toot.Object.Conversation, + Whofore: 2, + Format: "html", + Precis: toot.Object.Summary, + } + if honk.RID != "" { + honk.What = "tonk" + } + if !loudandproud(honk.Audience) { + honk.Whofore = 3 + } + for _, att := range toot.Object.Attachment { + switch att.Type { + case "Document": + fname := fmt.Sprintf("%s/%s", source, att.Url) + data, err := ioutil.ReadFile(fname) + if err != nil { + elog.Printf("error reading media: %s", fname) + continue + } + u := xfiltrate() + name := att.Name + desc := name + newurl := fmt.Sprintf("https://%s/d/%s", serverName, u) + fileid, err := savefile(name, desc, newurl, att.MediaType, true, data) + if err != nil { + elog.Printf("error saving media: %s", fname) + continue + } + donk := &Donk{ + FileID: fileid, + } + honk.Donks = append(honk.Donks, donk) + } + } + for _, t := range toot.Object.Tag { + switch t.Type { + case "Hashtag": + honk.Onts = append(honk.Onts, t.Name) + } + } + savehonk(&honk) + } +} + +func importMastotooters(user *WhatAbout, source string) { + ilog.Println("Importing honkers...") + fd, err := os.Open(source + "/following_accounts.csv") + if err != nil { + elog.Fatal(err) + } + r := csv.NewReader(fd) + data, err := r.ReadAll() + if err != nil { + elog.Fatal(err) + } + fd.Close() + + var meta HonkerMeta + mj, _ := jsonify(&meta) + + for i, d := range data { + if i == 0 { + continue + } + url := "@" + d[0] + name := "" + flavor := "peep" + combos := "" + err := savehonker(user, url, name, flavor, combos, mj) + if err != nil { + elog.Printf("trouble with a honker: %s", err) + } + } +} + +func importTwitter(username, source string) { + user, err := butwhatabout(username) + if err != nil { + elog.Fatal(err) + } + + type Tweet struct { + ID_str string + Created_at string + Full_text string + In_reply_to_screen_name string + In_reply_to_status_id string + Entities struct { + Hashtags []struct { + Text string + } + Media []struct { + Url string + Media_url string + } + Urls []struct { + Url string + Expanded_url string + } + } + date time.Time + convoy string + } + + var tweets []*Tweet + fd, err := os.Open(source + "/tweet.js") + if err != nil { + elog.Fatal(err) + } + // skip past window.YTD.tweet.part0 = + fd.Seek(25, 0) + dec := json.NewDecoder(fd) + err = dec.Decode(&tweets) + if err != nil { + elog.Fatalf("error parsing json: %s", err) + } + fd.Close() + tweetmap := make(map[string]*Tweet) + for _, t := range tweets { + t.date, _ = time.Parse("Mon Jan 02 15:04:05 -0700 2006", t.Created_at) + tweetmap[t.ID_str] = t + } + sort.Slice(tweets, func(i, j int) bool { + return tweets[i].date.Before(tweets[j].date) + }) + havetwid := func(xid string) bool { + var id int64 + row := stmtFindXonk.QueryRow(user.ID, xid) + err := row.Scan(&id) + if err == nil { + return true + } + return false + } + + for _, t := range tweets { + xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, t.ID_str) + if havetwid(xid) { + continue + } + what := "honk" + noise := "" + if parent := tweetmap[t.In_reply_to_status_id]; parent != nil { + t.convoy = parent.convoy + what = "tonk" + } else { + t.convoy = "data:,acoustichonkytonk-" + t.ID_str + if t.In_reply_to_screen_name != "" { + noise = fmt.Sprintf("re: https://twitter.com/%s/status/%s\n\n", + t.In_reply_to_screen_name, t.In_reply_to_status_id) + what = "tonk" + } + } + audience := []string{thewholeworld} + honk := Honk{ + UserID: user.ID, + Username: user.Name, + What: what, + Honker: user.URL, + XID: xid, + Date: t.date, + Format: "markdown", + Audience: audience, + Convoy: t.convoy, + Public: true, + Whofore: 2, + } + noise += t.Full_text + // unbelievable + noise = html.UnescapeString(noise) + for _, r := range t.Entities.Urls { + noise = strings.Replace(noise, r.Url, r.Expanded_url, -1) + } + for _, m := range t.Entities.Media { + u := m.Media_url + idx := strings.LastIndexByte(u, '/') + u = u[idx+1:] + fname := fmt.Sprintf("%s/tweet_media/%s-%s", source, t.ID_str, u) + data, err := ioutil.ReadFile(fname) + if err != nil { + elog.Printf("error reading media: %s", fname) + continue + } + newurl := fmt.Sprintf("https://%s/d/%s", serverName, u) + + fileid, err := savefile(u, u, newurl, "image/jpg", true, data) + if err != nil { + elog.Printf("error saving media: %s", fname) + continue + } + donk := &Donk{ + FileID: fileid, + } + honk.Donks = append(honk.Donks, donk) + noise = strings.Replace(noise, m.Url, "", -1) + } + for _, ht := range t.Entities.Hashtags { + honk.Onts = append(honk.Onts, "#"+ht.Text) + } + honk.Noise = noise + savehonk(&honk) + } +}
A markitzero.go

@@ -0,0 +1,25 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "humungus.tedunangst.com/r/webs/mz" +) + +func markitzero(s string) string { + var marker mz.Marker + return marker.Mark(s) +}
A preflight.sh

@@ -0,0 +1,19 @@

+set -e + +go version > /dev/null 2>&1 || (echo go 1.16+ is required && false) + +v=`go version | egrep -o "go1\.[^.]+"` || echo failed to identify go version +if [ "$v" \< "go1.16" ] ; then + echo go version is too old: $v + echo go 1.16+ is required + false +fi + +if [ \! \( -e /usr/include/sqlite3.h -o -e /usr/local/include/sqlite3.h \) ] ; then + echo unable to find sqlite3.h header + echo please install libsqlite3 dev package + false +fi + +touch .preflightcheck +
A restart.sh

@@ -0,0 +1,2 @@

+pkill honk +nohup ./honk &
A schema.sql

@@ -0,0 +1,39 @@

+ +create table honks (honkid integer primary key, userid integer, what text, honker text, xid text, rid text, dt text, url text, audience text, noise text, convoy text, whofore integer, format text, precis text, oonker text, flags integer); +create table chonks (chonkid integer primary key, userid integer, xid text, who txt, target text, dt text, noise text, format text); +create table donks (honkid integer, chonkid integer, fileid integer); +create table filemeta (fileid integer primary key, xid text, name text, description text, url text, media text, local integer); +create table honkers (honkerid integer primary key, userid integer, name text, xid text, flavor text, combos text, owner text, meta text, folxid text); +create table xonkers (xonkerid integer primary key, name text, info text, flavor text, dt text); +create table zonkers (zonkerid integer primary key, userid integer, name text, wherefore text); +create table doovers(dooverid integer primary key, dt text, tries integer, userid integer, rcpt text, msg blob); +create table onts (ontology text, honkid integer); +create table honkmeta (honkid integer, genus text, json text); +create table hfcs (hfcsid integer primary key, userid integer, json text); +create table tracks (xid text, fetches text); + +create index idx_honksxid on honks(xid); +create index idx_honksconvoy on honks(convoy); +create index idx_honkshonker on honks(honker); +create index idx_honksoonker on honks(oonker); +create index idx_donkshonk on donks(honkid); +create index idx_donkschonk on donks(chonkid); +create index idx_honkerxid on honkers(xid); +create index idx_xonkername on xonkers(name); +create index idx_zonkersname on zonkers(name); +create index idx_filesxid on filemeta(xid); +create index idx_filesurl on filemeta(url); +create index idx_ontology on onts(ontology); +create index idx_onthonkid on onts(honkid); +create index idx_honkmetaid on honkmeta(honkid); +create index idx_hfcsuser on hfcs(userid); +create index idx_trackhonkid on tracks(xid); + +create table config (key text, value text); + +create table users (userid integer primary key, username text, hash text, displayname text, about text, pubkey text, seckey text, options text); +create table auth (authid integer primary key, userid integer, hash text, expiry text); +CREATE index idxusers_username on users(username); +CREATE index idxauth_userid on auth(userid); +CREATE index idxauth_hash on auth(hash); +
A sensors.go

@@ -0,0 +1,50 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "syscall" + "time" +) + +type Sensors struct { + Memory float64 + Uptime float64 + CPU float64 +} + +var boottime = time.Now() + +func getSensors() Sensors { + var usage syscall.Rusage + syscall.Getrusage(syscall.RUSAGE_SELF, &usage) + + now := time.Now() + + var sensors Sensors + sensors.Memory = float64(usage.Maxrss) / 1024.0 + sensors.Uptime = now.Sub(boottime).Seconds() + sensors.CPU = time.Duration(usage.Utime.Nano()).Seconds() + + return sensors +} + +func setLimits() error { + var limit syscall.Rlimit + limit.Cur = 2 * 1024 * 1024 * 1024 + limit.Max = 2 * 1024 * 1024 * 1024 + return syscall.Setrlimit(syscall.RLIMIT_DATA, &limit) +}
A skulduggery.go

@@ -0,0 +1,55 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "regexp" + + "github.com/mattn/go-runewidth" +) + +var skinTones = "\U0001F3FB\U0001F3FC\U0001F3FD\U0001F3FE\U0001F3FF" +var re_moredumb = regexp.MustCompile("[\U0001f44f\U0001f6a8\U000026a0][" + skinTones + "\ufe0f]*") + +func demoji(s string) string { + s = re_moredumb.ReplaceAllString(s, ".") + + zw := false + for _, c := range s { + if runewidth.RuneWidth(c) == 0 { + zw = true + break + } + } + if zw { + x := make([]byte, 0, len(s)) + zw = false + for _, c := range s { + if runewidth.RuneWidth(c) == 0 { + if zw { + continue + } + zw = true + } else { + zw = false + } + q := string(c) + x = append(x, []byte(q)...) + } + return string(x) + } + return s +}
A toys/Makefile

@@ -0,0 +1,25 @@

+ +PROGS=autobonker gettoken saytheday sprayandpray wonkawonk youvegothonks + +all: $(PROGS) + +clean: + rm -f $(PROGS) + +autobonker: autobonker.go + go build autobonker.go + +gettoken: gettoken.go + go build gettoken.go fetch.go + +saytheday: saytheday.go + go build saytheday.go + +sprayandpray: sprayandpray.go + go build sprayandpray.go + +wonkawonk: wonkawonk.go fetch.go + go build wonkawonk.go fetch.go + +youvegothonks: youvegothonks.go + go build youvegothonks.go
A toys/README

@@ -0,0 +1,13 @@

+These are all standalone programs, meant to be compiled individually. + +A little of this, a little of that. + +autobonker.go - repeats mentioned posts + +gettoken.go - obtains an authorization token + +saytheday.go - posts a new honk that's a date based look and say sequence + +sprayandpray.go - send an activity with no error checking and hope it works + +youvegothonks.go - polls for new messages
A toys/autobonker.go

@@ -0,0 +1,113 @@

+package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +type Honk struct { + ID int + XID string + Honker string +} + +type HonkSet struct { + Honks []Honk +} + +func gethonks(server, token string, wanted int) HonkSet { + form := make(url.Values) + form.Add("action", "gethonks") + form.Add("page", "atme") + form.Add("after", fmt.Sprintf("%d", wanted)) + form.Add("wait", "30") + apiurl := fmt.Sprintf("https://%s/api?%s", server, form.Encode()) + req, err := http.NewRequest("GET", apiurl, nil) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Authorization", "Bearer "+token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + if resp.StatusCode == 502 { + log.Printf("server error 502...") + time.Sleep(5 * time.Minute) + return HonkSet{} + } + answer, _ := ioutil.ReadAll(resp.Body) + log.Fatalf("status: %d: %s", resp.StatusCode, answer) + } + var honks HonkSet + d := json.NewDecoder(resp.Body) + err = d.Decode(&honks) + if err != nil { + log.Fatal(err) + } + return honks +} + +func bonk(server, token string, honk Honk) { + log.Printf("bonking %s from %s", honk.XID, honk.Honker) + form := make(url.Values) + form.Add("action", "zonkit") + form.Add("wherefore", "bonk") + form.Add("what", honk.XID) + apiurl := fmt.Sprintf("https://%s/api", server) + req, err := http.NewRequest("POST", apiurl, strings.NewReader(form.Encode())) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Authorization", "Bearer "+token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + answer, _ := ioutil.ReadAll(resp.Body) + log.Fatalf("status: %d: %s", resp.StatusCode, answer) + } +} + +func main() { + server := "" + token := "" + flag.StringVar(&server, "server", server, "server to connnect") + flag.StringVar(&token, "token", token, "auth token to use") + flag.Parse() + + if server == "" || token == "" { + flag.Usage() + os.Exit(1) + } + + wanted := 0 + + for { + honks := gethonks(server, token, wanted) + for i, h := range honks.Honks { + bonk(server, token, h) + if i > 0 { + time.Sleep(3 * time.Second) + } + if wanted < h.ID { + wanted = h.ID + } + + } + time.Sleep(3 * time.Second) + } +}
A toys/fetch.go

@@ -0,0 +1,53 @@

+package main + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io" + "log" + "net/http" + "time" +) + +var debugClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, +} + +func fetchsome(url string) ([]byte, error) { + client := http.DefaultClient + if debugMode { + client = debugClient + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Printf("error fetching %s: %s", url, err) + return nil, err + } + req.Header.Set("User-Agent", "honksnonk/4.0") + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + log.Printf("error fetching %s: %s", url, err) + return nil, err + } + defer resp.Body.Close() + switch resp.StatusCode { + case 200: + case 201: + case 202: + default: + return nil, fmt.Errorf("http get not 200: %d %s", resp.StatusCode, url) + } + var buf bytes.Buffer + limiter := io.LimitReader(resp.Body, 10*1024*1024) + io.Copy(&buf, limiter) + return buf.Bytes(), nil +}
A toys/gettoken.go

@@ -0,0 +1,61 @@

+package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strings" +) + +var debugMode = false + +func main() { + server := "" + username := "" + password := "" + + flag.StringVar(&server, "server", server, "server to connnect") + flag.StringVar(&username, "username", username, "username to use") + flag.StringVar(&password, "password", password, "password to use") + flag.BoolVar(&debugMode, "debug", debugMode, "debug mode") + flag.Parse() + + if server == "" || username == "" || password == "" { + flag.Usage() + os.Exit(1) + } + + form := make(url.Values) + form.Add("username", username) + form.Add("password", password) + form.Add("gettoken", "1") + loginurl := fmt.Sprintf("https://%s/dologin", server) + req, err := http.NewRequest("POST", loginurl, strings.NewReader(form.Encode())) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + client := http.DefaultClient + if debugMode { + client = debugClient + } + + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + answer, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + if resp.StatusCode != 200 { + log.Fatalf("status: %d: %s", resp.StatusCode, answer) + } + fmt.Println(string(answer)) +}
A toys/saytheday.go

@@ -0,0 +1,83 @@

+package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +func lookandsay(n int) string { + s := "1" + + numbers := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} + var buf strings.Builder + for i := 2; i <= n; i++ { + count := 1 + prev := s[0] + for j := 1; j < len(s); j++ { + d := s[j] + if d == prev { + count++ + } else { + buf.WriteString(numbers[count]) + buf.WriteByte(prev) + count = 1 + prev = d + } + } + buf.WriteString(numbers[count]) + buf.WriteByte(prev) + s = buf.String() + buf.Reset() + } + return s +} + +func honkahonk(server, token, noise string) { + form := make(url.Values) + form.Add("token", token) + form.Add("action", "honk") + form.Add("noise", noise) + apiurl := fmt.Sprintf("https://%s/api", server) + req, err := http.NewRequest("POST", apiurl, strings.NewReader(form.Encode())) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + answer, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + if resp.StatusCode != 200 { + log.Fatalf("status: %d: %s", resp.StatusCode, answer) + } +} + +func main() { + server := "" + token := "" + flag.StringVar(&server, "server", server, "server to connnect") + flag.StringVar(&token, "token", token, "auth token to use") + flag.Parse() + + if server == "" || token == "" { + flag.Usage() + os.Exit(1) + } + + day := time.Now().Day() + say := lookandsay(day) + + honkahonk(server, token, say) +}
A toys/sprayandpray.go

@@ -0,0 +1,58 @@

+package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strings" +) + +func sendmsg(server, token, msg, rcpt string) { + form := make(url.Values) + form.Add("token", token) + form.Add("action", "sendactivity") + form.Add("msg", msg) + form.Add("rcpt", rcpt) + apiurl := fmt.Sprintf("https://%s/api", server) + req, err := http.NewRequest("POST", apiurl, strings.NewReader(form.Encode())) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + answer, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + if resp.StatusCode != 200 { + log.Fatalf("status: %d: %s", resp.StatusCode, answer) + } +} + +func main() { + var server, token, msgfile, rcpt string + flag.StringVar(&server, "server", server, "server to connnect") + flag.StringVar(&token, "token", token, "auth token to use") + flag.StringVar(&msgfile, "msgfile", token, "file with message to send") + flag.StringVar(&rcpt, "rcpt", rcpt, "rcpt to send it to") + flag.Parse() + + if server == "" || token == "" || msgfile == "" || rcpt == "" { + flag.Usage() + os.Exit(1) + } + msg, err := ioutil.ReadFile(msgfile) + if err != nil { + log.Fatal(err) + } + + sendmsg(server, token, string(msg), rcpt) +}
A toys/wonkawonk.go

@@ -0,0 +1,79 @@

+package main + +import ( + "crypto/rand" + "flag" + "fmt" + "io/ioutil" + "log" + "math/big" + "net/http" + "net/url" + "os" + "strings" +) + +var debugMode = false + +func honkahonk(server, token, wonk, wonkles string) { + form := make(url.Values) + form.Add("token", token) + form.Add("action", "honk") + form.Add("noise", wonk) + form.Add("wonkles", wonkles) + apiurl := fmt.Sprintf("https://%s/api", server) + req, err := http.NewRequest("POST", apiurl, strings.NewReader(form.Encode())) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + client := http.DefaultClient + if debugMode { + client = debugClient + } + + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + answer, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + if resp.StatusCode != 200 { + log.Fatalf("status: %d: %s", resp.StatusCode, answer) + } +} + +func main() { + server := "" + token := "" + wonkles := "" + flag.StringVar(&server, "server", server, "server to connnect") + flag.StringVar(&token, "token", token, "auth token to use") + flag.StringVar(&wonkles, "wonkles", wonkles, "wordlist to use") + flag.BoolVar(&debugMode, "debug", debugMode, "debug mode") + flag.Parse() + + if server == "" || token == "" || wonkles == "" { + flag.Usage() + os.Exit(1) + } + + wordlist, err := fetchsome(wonkles) + if err != nil { + log.Printf("error fetching wonkles: %s", err) + } + var words []string + for _, w := range strings.Split(string(wordlist), "\n") { + words = append(words, w) + } + max := big.NewInt(int64(len(words))) + i, _ := rand.Int(rand.Reader, max) + wonk := words[i.Int64()] + + log.Printf("picking: %s", wonk) + + honkahonk(server, token, wonk, wonkles) +}
A toys/youvegothonks.go

@@ -0,0 +1,79 @@

+package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "time" +) + +type Honk struct { + ID int + Honker string + Noise string +} + +type HonkSet struct { + Honks []Honk +} + +func gethonks(server, token string, wanted int) HonkSet { + form := make(url.Values) + form.Add("action", "gethonks") + form.Add("page", "atme") + form.Add("after", fmt.Sprintf("%d", wanted)) + form.Add("wait", "30") + apiurl := fmt.Sprintf("https://%s/api?%s", server, form.Encode()) + req, err := http.NewRequest("GET", apiurl, nil) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Authorization", "Bearer "+token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + answer, _ := ioutil.ReadAll(resp.Body) + log.Fatalf("status: %d: %s", resp.StatusCode, answer) + } + var honks HonkSet + d := json.NewDecoder(resp.Body) + err = d.Decode(&honks) + if err != nil { + log.Fatal(err) + } + return honks +} + +func main() { + server := "" + token := "" + flag.StringVar(&server, "server", server, "server to connnect") + flag.StringVar(&token, "token", token, "auth token to use") + flag.Parse() + + if server == "" || token == "" { + flag.Usage() + os.Exit(1) + } + + wanted := 0 + + for { + honks := gethonks(server, token, wanted) + for _, h := range honks.Honks { + fmt.Printf("you've got a honk from %s\n%s\n", h.Honker, h.Noise) + if wanted < h.ID { + wanted = h.ID + } + } + time.Sleep(1 * time.Second) + } +}
A unveil.go

@@ -0,0 +1,70 @@

+//go:build openbsd +// +build openbsd + +// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +/* +#include <stdlib.h> +#include <unistd.h> +*/ +import "C" + +import ( + "fmt" + "unsafe" +) + +func Unveil(path string, perms string) error { + cpath := C.CString(path) + defer C.free(unsafe.Pointer(cpath)) + cperms := C.CString(perms) + defer C.free(unsafe.Pointer(cperms)) + + rv, err := C.unveil(cpath, cperms) + if rv != 0 { + return fmt.Errorf("unveil(%s, %s) failure (%d)", path, perms, err) + } + return nil +} + +func Pledge(promises string) error { + cpromises := C.CString(promises) + defer C.free(unsafe.Pointer(cpromises)) + + rv, err := C.pledge(cpromises, nil) + if rv != 0 { + return fmt.Errorf("pledge(%s) failure (%d)", promises, err) + } + return nil +} + +func init() { + preservehooks = append(preservehooks, func() { + Unveil("/etc/ssl", "r") + if viewDir != dataDir { + Unveil(viewDir, "r") + } + Unveil(dataDir, "rwc") + C.unveil(nil, nil) + Pledge("stdio rpath wpath cpath flock dns inet unix") + }) + backendhooks = append(backendhooks, func() { + C.unveil(nil, nil) + Pledge("stdio unix") + }) +}
A upgradedb.go

@@ -0,0 +1,214 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "database/sql" + "os" + "strings" + "time" +) + +var myVersion = 41 + +type dbexecer interface { + Exec(query string, args ...interface{}) (sql.Result, error) +} + +func doordie(db dbexecer, s string, args ...interface{}) { + _, err := db.Exec(s, args...) + if err != nil { + elog.Fatalf("can't run %s: %s", s, err) + } +} + +func upgradedb() { + db := opendatabase() + dbversion := 0 + getconfig("dbversion", &dbversion) + getconfig("servername", &serverName) + + if dbversion < 13 { + elog.Fatal("database is too old to upgrade") + } + switch dbversion { + case 25: + doordie(db, "delete from auth") + doordie(db, "alter table auth add column expiry text") + doordie(db, "update config set value = 26 where key = 'dbversion'") + fallthrough + case 26: + s := "" + getconfig("servermsg", &s) + if s == "" { + setconfig("servermsg", "<h2>Things happen.</h2>") + } + s = "" + getconfig("aboutmsg", &s) + if s == "" { + setconfig("aboutmsg", "<h3>What is honk?</h3><p>Honk is amazing!") + } + s = "" + getconfig("loginmsg", &s) + if s == "" { + setconfig("loginmsg", "<h2>login</h2>") + } + d := -1 + getconfig("devel", &d) + if d == -1 { + setconfig("devel", 0) + } + doordie(db, "update config set value = 27 where key = 'dbversion'") + fallthrough + case 27: + createserveruser(db) + doordie(db, "update config set value = 28 where key = 'dbversion'") + fallthrough + case 28: + doordie(db, "drop table doovers") + doordie(db, "create table doovers(dooverid integer primary key, dt text, tries integer, userid integer, rcpt text, msg blob)") + doordie(db, "update config set value = 29 where key = 'dbversion'") + fallthrough + case 29: + doordie(db, "alter table honkers add column owner text") + doordie(db, "update honkers set owner = xid") + doordie(db, "update config set value = 30 where key = 'dbversion'") + fallthrough + case 30: + tx, err := db.Begin() + if err != nil { + elog.Fatal(err) + } + rows, err := tx.Query("select userid, options from users") + if err != nil { + elog.Fatal(err) + } + m := make(map[int64]string) + for rows.Next() { + var userid int64 + var options string + err = rows.Scan(&userid, &options) + if err != nil { + elog.Fatal(err) + } + var uo UserOptions + uo.SkinnyCSS = strings.Contains(options, " skinny ") + m[userid], err = jsonify(uo) + if err != nil { + elog.Fatal(err) + } + } + rows.Close() + for u, o := range m { + _, err = tx.Exec("update users set options = ? where userid = ?", o, u) + if err != nil { + elog.Fatal(err) + } + } + err = tx.Commit() + if err != nil { + elog.Fatal(err) + } + doordie(db, "update config set value = 31 where key = 'dbversion'") + fallthrough + case 31: + doordie(db, "create table tracks (xid text, fetches text)") + doordie(db, "create index idx_trackhonkid on tracks(xid)") + doordie(db, "update config set value = 32 where key = 'dbversion'") + fallthrough + case 32: + doordie(db, "alter table xonkers add column dt text") + doordie(db, "update xonkers set dt = ?", time.Now().UTC().Format(dbtimeformat)) + doordie(db, "update config set value = 33 where key = 'dbversion'") + fallthrough + case 33: + doordie(db, "alter table honkers add column meta text") + doordie(db, "update honkers set meta = '{}'") + doordie(db, "update config set value = 34 where key = 'dbversion'") + fallthrough + case 34: + doordie(db, "create table chonks (chonkid integer primary key, userid integer, xid text, who txt, target text, dt text, noise text, format text)") + doordie(db, "update config set value = 35 where key = 'dbversion'") + fallthrough + case 35: + doordie(db, "alter table donks add column chonkid integer") + doordie(db, "update donks set chonkid = -1") + doordie(db, "create index idx_donkshonk on donks(honkid)") + doordie(db, "create index idx_donkschonk on donks(chonkid)") + doordie(db, "update config set value = 36 where key = 'dbversion'") + fallthrough + case 36: + doordie(db, "alter table honkers add column folxid text") + doordie(db, "update honkers set folxid = 'lostdata'") + doordie(db, "update config set value = 37 where key = 'dbversion'") + fallthrough + case 37: + doordie(db, "update honkers set combos = '' where combos is null") + doordie(db, "update honkers set owner = '' where owner is null") + doordie(db, "update honkers set meta = '' where meta is null") + doordie(db, "update honkers set folxid = '' where folxid is null") + doordie(db, "update config set value = 38 where key = 'dbversion'") + fallthrough + case 38: + doordie(db, "update honkers set folxid = abs(random())") + doordie(db, "update config set value = 39 where key = 'dbversion'") + fallthrough + case 39: + blobdb := openblobdb() + doordie(blobdb, "alter table filedata add column hash text") + doordie(blobdb, "create index idx_filehash on filedata(hash)") + rows, err := blobdb.Query("select xid, content from filedata") + if err != nil { + elog.Fatal(err) + } + m := make(map[string]string) + for rows.Next() { + var xid string + var data sql.RawBytes + err := rows.Scan(&xid, &data) + if err != nil { + elog.Fatal(err) + } + hash := hashfiledata(data) + m[xid] = hash + } + rows.Close() + tx, err := blobdb.Begin() + if err != nil { + elog.Fatal(err) + } + for xid, hash := range m { + doordie(tx, "update filedata set hash = ? where xid = ?", hash, xid) + } + err = tx.Commit() + if err != nil { + elog.Fatal(err) + } + doordie(db, "update config set value = 40 where key = 'dbversion'") + fallthrough + case 40: + doordie(db, "PRAGMA journal_mode=WAL") + blobdb := openblobdb() + doordie(blobdb, "PRAGMA journal_mode=WAL") + doordie(db, "update config set value = 41 where key = 'dbversion'") + fallthrough + case 41: + + default: + elog.Fatalf("can't upgrade unknown version %d", dbversion) + } + os.Exit(0) +}
A util.go

@@ -0,0 +1,473 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +/* +#include <termios.h> + +void +termecho(int on) +{ + struct termios t; + tcgetattr(1, &t); + if (on) + t.c_lflag |= ECHO; + else + t.c_lflag &= ~ECHO; + tcsetattr(1, TCSADRAIN, &t); +} +*/ +import "C" + +import ( + "bufio" + "crypto/rand" + "crypto/rsa" + "crypto/sha512" + "database/sql" + "fmt" + "io/ioutil" + "net" + "os" + "os/signal" + "regexp" + "strings" + + "golang.org/x/crypto/bcrypt" + _ "humungus.tedunangst.com/r/go-sqlite3" + "humungus.tedunangst.com/r/webs/httpsig" + "humungus.tedunangst.com/r/webs/login" +) + +var savedassetparams = make(map[string]string) + +var re_plainname = regexp.MustCompile("^[[:alnum:]_-]+$") + +func getassetparam(file string) string { + if p, ok := savedassetparams[file]; ok { + return p + } + data, err := ioutil.ReadFile(file) + if err != nil { + return "" + } + hasher := sha512.New() + hasher.Write(data) + + return fmt.Sprintf("?v=%.8x", hasher.Sum(nil)) +} + +var dbtimeformat = "2006-01-02 15:04:05" + +var alreadyopendb *sql.DB +var stmtConfig *sql.Stmt + +func initdb() { + dbname := dataDir + "/honk.db" + _, err := os.Stat(dbname) + if err == nil { + elog.Fatalf("%s already exists", dbname) + } + db, err := sql.Open("sqlite3", dbname) + if err != nil { + elog.Fatal(err) + } + alreadyopendb = db + defer func() { + os.Remove(dbname) + os.Exit(1) + }() + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt) + go func() { + <-c + C.termecho(1) + fmt.Printf("\n") + os.Remove(dbname) + os.Exit(1) + }() + + _, err = db.Exec("PRAGMA journal_mode=WAL") + if err != nil { + elog.Print(err) + return + } + for _, line := range strings.Split(sqlSchema, ";") { + _, err = db.Exec(line) + if err != nil { + elog.Print(err) + return + } + } + r := bufio.NewReader(os.Stdin) + + initblobdb() + + prepareStatements(db) + + err = createuser(db, r) + if err != nil { + elog.Print(err) + return + } + // must came later or user above will have negative id + err = createserveruser(db) + if err != nil { + elog.Print(err) + return + } + + fmt.Printf("listen address: ") + addr, err := r.ReadString('\n') + if err != nil { + elog.Print(err) + return + } + addr = addr[:len(addr)-1] + if len(addr) < 1 { + elog.Print("that's way too short") + return + } + setconfig("listenaddr", addr) + fmt.Printf("server name: ") + addr, err = r.ReadString('\n') + if err != nil { + elog.Print(err) + return + } + addr = addr[:len(addr)-1] + if len(addr) < 1 { + elog.Print("that's way too short") + return + } + setconfig("servername", addr) + var randbytes [16]byte + rand.Read(randbytes[:]) + key := fmt.Sprintf("%x", randbytes) + setconfig("csrfkey", key) + setconfig("dbversion", myVersion) + + setconfig("servermsg", "<h2>Things happen.</h2>") + setconfig("aboutmsg", "<h3>What is honk?</h3><p>Honk is amazing!") + setconfig("loginmsg", "<h2>login</h2>") + setconfig("devel", 0) + + db.Close() + fmt.Printf("done.\n") + os.Exit(0) +} + +func initblobdb() { + blobdbname := dataDir + "/blob.db" + _, err := os.Stat(blobdbname) + if err == nil { + elog.Fatalf("%s already exists", blobdbname) + } + blobdb, err := sql.Open("sqlite3", blobdbname) + if err != nil { + elog.Print(err) + return + } + _, err = blobdb.Exec("PRAGMA journal_mode=WAL") + if err != nil { + elog.Print(err) + return + } + _, err = blobdb.Exec("create table filedata (xid text, media text, hash text, content blob)") + if err != nil { + elog.Print(err) + return + } + _, err = blobdb.Exec("create index idx_filexid on filedata(xid)") + if err != nil { + elog.Print(err) + return + } + _, err = blobdb.Exec("create index idx_filehash on filedata(hash)") + if err != nil { + elog.Print(err) + return + } + blobdb.Close() +} + +func adduser() { + db := opendatabase() + defer func() { + os.Exit(1) + }() + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt) + go func() { + <-c + C.termecho(1) + fmt.Printf("\n") + os.Exit(1) + }() + + r := bufio.NewReader(os.Stdin) + + err := createuser(db, r) + if err != nil { + elog.Print(err) + return + } + + os.Exit(0) +} + +func deluser(username string) { + user, _ := butwhatabout(username) + if user == nil { + elog.Printf("no userfound") + return + } + userid := user.ID + db := opendatabase() + + where := " where honkid in (select honkid from honks where userid = ?)" + doordie(db, "delete from donks"+where, userid) + doordie(db, "delete from onts"+where, userid) + doordie(db, "delete from honkmeta"+where, userid) + where = " where chonkid in (select chonkid from chonks where userid = ?)" + doordie(db, "delete from donks"+where, userid) + + doordie(db, "delete from honks where userid = ?", userid) + doordie(db, "delete from chonks where userid = ?", userid) + doordie(db, "delete from honkers where userid = ?", userid) + doordie(db, "delete from zonkers where userid = ?", userid) + doordie(db, "delete from doovers where userid = ?", userid) + doordie(db, "delete from hfcs where userid = ?", userid) + doordie(db, "delete from auth where userid = ?", userid) + doordie(db, "delete from users where userid = ?", userid) +} + +func chpass() { + if len(os.Args) < 3 { + fmt.Printf("need a username\n") + os.Exit(1) + } + user, err := butwhatabout(os.Args[2]) + if err != nil { + elog.Fatal(err) + } + defer func() { + os.Exit(1) + }() + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt) + go func() { + <-c + C.termecho(1) + fmt.Printf("\n") + os.Exit(1) + }() + + db := opendatabase() + login.Init(login.InitArgs{Db: db, Logger: ilog}) + + r := bufio.NewReader(os.Stdin) + + pass, err := askpassword(r) + if err != nil { + elog.Print(err) + return + } + err = login.SetPassword(user.ID, pass) + if err != nil { + elog.Print(err) + return + } + fmt.Printf("done\n") + os.Exit(0) +} + +func askpassword(r *bufio.Reader) (string, error) { + C.termecho(0) + fmt.Printf("password: ") + pass, err := r.ReadString('\n') + C.termecho(1) + fmt.Printf("\n") + if err != nil { + return "", err + } + pass = pass[:len(pass)-1] + if len(pass) < 6 { + return "", fmt.Errorf("that's way too short") + } + return pass, nil +} + +func createuser(db *sql.DB, r *bufio.Reader) error { + fmt.Printf("username: ") + name, err := r.ReadString('\n') + if err != nil { + return err + } + name = name[:len(name)-1] + if len(name) < 1 { + return fmt.Errorf("that's way too short") + } + if !re_plainname.MatchString(name) { + return fmt.Errorf("alphanumeric only please") + } + if _, err := butwhatabout(name); err == nil { + return fmt.Errorf("user already exists") + } + pass, err := askpassword(r) + if err != nil { + return err + } + hash, err := bcrypt.GenerateFromPassword([]byte(pass), 12) + if err != nil { + return err + } + k, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + pubkey, err := httpsig.EncodeKey(&k.PublicKey) + if err != nil { + return err + } + seckey, err := httpsig.EncodeKey(k) + if err != nil { + return err + } + about := "what about me?" + _, err = db.Exec("insert into users (username, displayname, about, hash, pubkey, seckey, options) values (?, ?, ?, ?, ?, ?, ?)", name, name, about, hash, pubkey, seckey, "{}") + if err != nil { + return err + } + return nil +} + +func createserveruser(db *sql.DB) error { + k, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + pubkey, err := httpsig.EncodeKey(&k.PublicKey) + if err != nil { + return err + } + seckey, err := httpsig.EncodeKey(k) + if err != nil { + return err + } + name := "server" + about := "server" + hash := "*" + _, err = db.Exec("insert into users (userid, username, displayname, about, hash, pubkey, seckey, options) values (?, ?, ?, ?, ?, ?, ?, ?)", serverUID, name, name, about, hash, pubkey, seckey, "") + if err != nil { + return err + } + return nil +} + +func opendatabase() *sql.DB { + if alreadyopendb != nil { + return alreadyopendb + } + dbname := dataDir + "/honk.db" + _, err := os.Stat(dbname) + if err != nil { + elog.Fatalf("unable to open database: %s", err) + } + db, err := sql.Open("sqlite3", dbname) + if err != nil { + elog.Fatalf("unable to open database: %s", err) + } + stmtConfig, err = db.Prepare("select value from config where key = ?") + if err != nil { + elog.Fatal(err) + } + alreadyopendb = db + return db +} + +func openblobdb() *sql.DB { + blobdbname := dataDir + "/blob.db" + _, err := os.Stat(blobdbname) + if err != nil { + elog.Fatalf("unable to open database: %s", err) + } + db, err := sql.Open("sqlite3", blobdbname) + if err != nil { + elog.Fatalf("unable to open database: %s", err) + } + return db +} + +func getconfig(key string, value interface{}) error { + m, ok := value.(*map[string]bool) + if ok { + rows, err := stmtConfig.Query(key) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var s string + err = rows.Scan(&s) + if err != nil { + return err + } + (*m)[s] = true + } + return nil + } + row := stmtConfig.QueryRow(key) + err := row.Scan(value) + if err == sql.ErrNoRows { + err = nil + } + return err +} + +func setconfig(key string, val interface{}) error { + db := opendatabase() + db.Exec("delete from config where key = ?", key) + _, err := db.Exec("insert into config (key, value) values (?, ?)", key, val) + return err +} + +func openListener() (net.Listener, error) { + var listenAddr string + err := getconfig("listenaddr", &listenAddr) + if err != nil { + return nil, err + } + if listenAddr == "" { + return nil, fmt.Errorf("must have listenaddr") + } + proto := "tcp" + if listenAddr[0] == '/' { + proto = "unix" + err := os.Remove(listenAddr) + if err != nil && !os.IsNotExist(err) { + elog.Printf("unable to unlink socket: %s", err) + } + } + listener, err := net.Listen(proto, listenAddr) + if err != nil { + return nil, err + } + if proto == "unix" { + os.Chmod(listenAddr, 0777) + } + return listener, nil +}
A views/about.html

@@ -0,0 +1,15 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +{{ .AboutMsg }} +<p> +<table style="font-size:0.8em"> +<tbody> +<tr><td>version:<td style="text-align:right">{{ .HonkVersion }} +<tr><td>memory:<td style="text-align:right">{{ printf "%.02f" .Sensors.Memory }}MB +<tr><td>uptime:<td style="text-align:right">{{ printf "%.02f" .Sensors.Uptime }}s +<tr><td>cputime:<td style="text-align:right">{{ printf "%.02f" .Sensors.CPU }}s +</table> +<p> +</div> +</main>
A views/account.html

@@ -0,0 +1,53 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +<p>account - <a href="/logout?CSRF={{ .LogoutCSRF }}">logout</a> +<p>username: {{ .User.Name }} +<div> +<form id="aboutform" action="/saveuser" method="POST"> +<input type="hidden" name="CSRF" value="{{ .UserCSRF }}"> +<p>about me: +<p><textarea name="whatabout">{{ .WhatAbout }}</textarea> +<p><label class="button" for="skinny">skinny layout:</label> +<input tabindex=1 type="checkbox" id="skinny" name="skinny" value="skinny" {{ if .User.Options.SkinnyCSS }}checked{{ end }}><span></span> +<p><label class="button" for="avahex">hex avatar:</label> +<input tabindex=1 type="checkbox" id="avahex" name="avahex" value="avahex" {{ if .User.Options.Avahex }}checked{{ end }}><span></span> +<p><label class="button" for="omitimages">omit images:</label> +<input tabindex=1 type="checkbox" id="omitimages" name="omitimages" value="omitimages" {{ if .User.Options.OmitImages }}checked{{ end }}><span></span> +<p><label class="button" for="mentionall">mention all:</label> +<input tabindex=1 type="checkbox" id="mentionall" name="mentionall" value="mentionall" {{ if .User.Options.MentionAll }}checked{{ end }}><span></span> + +<p><label class="button" for="maps">apple map links:</label> +<input tabindex=1 type="checkbox" id="maps" name="maps" value="apple" {{ if eq "apple" .User.Options.MapLink }}checked{{ end }}><span></span> +<p><label class="button" for="reaction">reaction:</label> +<select tabindex=1 name="reaction"> +<option {{ and (eq .User.Options.Reaction "none") "selected" }}>none</option> +<option {{ and (eq .User.Options.Reaction "\U0001F61E") "selected" }}>{{ "\U0001F61E" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F937") "selected" }}>{{ "\U0001F937" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F648") "selected" }}>{{ "\U0001F648" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F9BE") "selected" }}>{{ "\U0001F9BE" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F346") "selected" }}>{{ "\U0001F346" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F351") "selected" }}>{{ "\U0001F351" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F32E") "selected" }}>{{ "\U0001F32E" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F951") "selected" }}>{{ "\U0001F951" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F5FF") "selected" }}>{{ "\U0001F5FF" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F99A") "selected" }}>{{ "\U0001F99A" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F3BB") "selected" }}>{{ "\U0001F3BB" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001FA93") "selected" }}>{{ "\U0001FA93" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F1EB") "selected" }}>{{ "\U0001F1EB" }}</option> +<option {{ and (eq .User.Options.Reaction "\U0001F1FD") "selected" }}>{{ "\U0001F1FD" }}</option> +</select> +<p><button>update settings</button> +</form> +</div> +<hr> +<div> +<form action="/chpass" method="POST"> +<input type="hidden" name="CSRF" value="{{ .LogoutCSRF }}"> +<p>change password +<p><input tabindex=1 type="password" name="oldpass"> - oldpass +<p><input tabindex=1 type="password" name="newpass"> - newpass +<p><button>change</button> +</form> +</div> +</main>
A views/chatter.html

@@ -0,0 +1,66 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +<p> +<form action="/sendchonk" method="POST" enctype="multipart/form-data"> +<h3>new chatter</h3> +<input type="hidden" name="CSRF" value="{{ .ChonkCSRF }}"> +<p><label for=target>target:</label><br> +<input type="text" name="target" value="" autocomplete=off> +<p><label for=noise>noise:</label><br> +<textarea name="noise" id="noise"></textarea> +<p><button name="chonk" value="chonk">chonk</button> +<label class=button id="donker">attach: <input onchange="updatedonker(this);" type="file" name="donk"><span></span></label> +</form> +<script> +function updatedonker(el) { + el = el.parentElement + el.children[1].textContent = el.children[0].value.slice(-20) +} +</script> +</div> +{{ $chonkcsrf := .ChonkCSRF }} +{{ range .Chatter }} +<section class="honk"> +<p class="chattarget"> +chatter: {{ .Target }} +{{ $target := .Target }} +{{ range .Chonks }} +<div class="chat"> +<p> +<span class="chatstamp">{{ .Date.Local.Format "15:04" }} {{ .Handle }}:</span> +{{ .HTML }} +{{ range .Donks }} +{{ if .Local }} +{{ if eq .Media "text/plain" }} +<p><a href="/d/{{ .XID }}">Attachment: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} +{{ else if eq .Media "application/pdf" }} +<p><a href="/d/{{ .XID }}">Attachment: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} +{{ else }} +<p><img src="/d/{{ .XID }}" title="{{ .Desc }}" alt="{{ .Desc }}"> +{{ end }} +{{ else }} +{{ if .XID }} +<p><a href="{{ .URL }}" rel=noreferrer>External Attachment: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} +{{ else }} +{{ if eq .Media "video/mp4" }} +<p><video controls src="{{ .URL }}">{{ .Name }}</video> +{{ else }} +<p><img src="{{ .URL }}" title="{{ .Desc }}" alt="{{ .Desc }}"> +{{ end }} +{{ end }} +{{ end }} +{{ end }} +</div> +{{ end }} +<form action="/sendchonk" method="POST" enctype="multipart/form-data"> +<input type="hidden" name="CSRF" value="{{ $chonkcsrf }}"> +<input type="hidden" name="target" value="{{ $target }}" autocomplete=off> +<p><label for=noise>noise:</label><br> +<textarea name="noise" id="noise"></textarea> +<p><button name="chonk" value="chonk">chonk</button> +<label class=button id="donker">attach: <input onchange="updatedonker(this);" type="file" name="donk"><span></span></label> +</form> +</section> +{{ end }} +</main>
A views/combos.html

@@ -0,0 +1,13 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +<p>combos +</div> +{{ range .Combos }} +<section class="honk"> +<header> +<p style="font-size: 1.8em"><a href="/c/{{ . }}">{{ . }}</a> +</header> +</section> +{{ end }} +</main>
A views/funzone.html

@@ -0,0 +1,17 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +<p>Welcome the fun zone! +<ul> +{{ $m := .Emuext }} +{{ range .Emus }} +<li><img class="emu" src="/emu/{{ . }}{{ index $m . }}"> :{{ . }}: +{{ end }} +</ul> +<ul> +{{ range .Memes }} +<li>meme: <a href="/meme/{{ . }}">{{ . }}</a> +{{ end }} +</ul> +</div> +</main>
A views/header.html

@@ -0,0 +1,77 @@

+<!doctype html> +<html> +<head> +<title>honk</title> +<link href="/style.css{{ .StyleParam }}" rel="stylesheet"> +{{ if .LocalStyleParam }} +<link href="/local.css{{ .LocalStyleParam }}" rel="stylesheet"> +{{ end }} +<style> +{{ .UserStyle }} +</style> +<link href="/icon.png" rel="icon"> +<meta name="theme-color" content="#305"> +<meta name="viewport" content="width=device-width"> +</head> +<body> +<header> +{{ if .UserInfo }} +<details id="topmenu"> +<summary>menu<span> {{ .UserInfo.Name }}</span></summary> +<ul> +<li><a id="homelink" href="/">home</a> +<li><a id="atmelink" href="/atme">@me<span id=mecount>{{ if .UserInfo.Options.MeCount }}({{ .UserInfo.Options.MeCount }}){{ end }}</span></a> +<li><a id="firstlink" href="/first">first</a> +<li style="list-style-type:none; margin-left:-1em"> +<details> +<summary>combos</summary> +<ul> +{{ range .Combos }} +<li><a class="combolink" href="/c/{{ . }}">{{ . }}</a> +{{ end }} +</ul> +</details> +<li><a href="/chatter">chatter<span id=chatcount>{{ if .UserInfo.Options.ChatCount }}({{ .UserInfo.Options.ChatCount }}){{ end }}</span></a> +<li><a href="/o">tags</a> +<li><a href="/events">events</a> +<li><a id="longagolink" href="/longago">long ago</a> +<li><a id="savedlink" href="/saved">saved</a> +<li><a href="/honkers">honkers</a> +<li><a href="/hfcs">filters</a> +<li><a href="/account">account</a> +<li style="list-style-type:none; margin-left:-1em"> +<details> +<summary>more stuff</summary> +<ul> +<li><a href="/{{ .UserSep }}/{{ .UserInfo.Name }}">my honks</a> +<li><a href="/about">about</a> +<li><a href="/front">front</a> +<li><a href="/funzone">funzone</a> +<li><a href="/xzone">xzone</a> +</ul> +</details> +<li><a href="/help/honk.1.html">help</a> +<li> +<form action="/q" method="GET"> +<input type="text" name="q" autocomplete=off size=10 placeholder="search"> +</form> +</ul> +</details> +<div id="topspacer"> +<p> +<p class="nophone" onclick="window.scrollTo(0,0)">top +</div> +{{ else }} +<div id="topmenu"> +<span><a id="homelink" href="/">home</a></span> +<span><a href="/o">tags</a></span> +<span><a href="/events">events</a></span> +<span><a href="/about">about</a></span> +{{ if .ShowRSS }} +<span><a href="/rss">rss</a></span> +{{ end }} +<span><a href="/login">login</a></span> +</div> +{{ end }} +</header> +
A views/hfcs.html

@@ -0,0 +1,72 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +<p> +Honk Filtering and Censorship System +<form action="/savehfcs" method="POST"> +<input type="hidden" name="CSRF" value="{{ .FilterCSRF }}"> +<hr> +<h3>new filter</h3> +<p><label for="name">filter name:</label><br> +<input tabindex=1 type="text" name="name" value="" autocomplete=off> +<p><label for="filtnotes">notes:</label><br> +<textarea tabindex=1 name="filtnotes" height=4> +</textarea> +<hr> +<h3>match</h3> +<p><label for="actor">who or where:</label><br> +<input tabindex=1 type="text" name="actor" value="" autocomplete=off> +<p><span><label class=button for="incaud">include audience: +<input tabindex=1 type="checkbox" id="incaud" name="incaud" value="yes"><span></span></label></span> +<p><label for="filttext">text matches:</label><br> +<input tabindex=1 type="text" name="filttext" value="" autocomplete=off> +<p><span><label class=button for="isannounce">is announce: +<input tabindex=1 type="checkbox" id="isannounce" name="isannounce" value="yes"><span></span></label></span> +<p><label for="announceof">announce of:</label><br> +<input tabindex=1 type="text" name="announceof" value="" autocomplete=off> +<hr> +<h3>action</h3> +<p class="buttonarray"> +<span><label class=button for="doreject">reject: +<input tabindex=1 type="checkbox" id="doreject" name="doreject" value="yes"><span></span></label></span> +<span><label class=button for="doskipmedia">skip media: +<input tabindex=1 type="checkbox" id="doskipmedia" name="doskipmedia" value="yes"><span></span></label></span> +<span><label class=button for="dohide">hide: +<input tabindex=1 type="checkbox" id="dohide" name="dohide" value="yes"><span></span></label></span> +<span><label class=button for="docollapse">collapse: +<input tabindex=1 type="checkbox" id="docollapse" name="docollapse" value="yes"><span></span></label></span> +<p><label for="rewrite">rewrite:</label><br> +<input tabindex=1 type="text" name="filtrewrite" value="" autocomplete=off> +<p><label for="replace">replace:</label><br> +<input tabindex=1 type="text" name="filtreplace" value="" autocomplete=off> +<hr> +<h3>expiration</h3> +<p><label for="filtduration">duration:</label><br> +<input tabindex=1 type="text" name="filtduration" value="" autocomplete=off> +<hr> +<p><button>impose your will</button> +</form> +</div> +{{ $csrf := .FilterCSRF }} +{{ range .Filters }} +<section class="honk"> +<p>Name: {{ .Name }} +{{ with .Notes }}<p>Notes: {{ . }}{{ end }} +<p>Date: {{ .Date.Format "2006-01-02" }} +{{ with .Actor }}<p>Who: {{ . }}{{ end }} {{ with .IncludeAudience }} (inclusive) {{ end }} +{{ if .IsAnnounce }}<p>Announce: {{ .AnnounceOf }}{{ end }} +{{ with .Text }}<p>Text: {{ . }}{{ end }} +<p>Actions: {{ range .Actions }} {{ . }} {{ end }} +{{ with .Rewrite }}<p>Rewrite: {{ . }}{{ end }} +{{ with .Replace }}<p>Replace: {{ . }}{{ end }} +{{ if not .Expiration.IsZero }}<p>Expiration: {{ .Expiration.Format "2006-01-02 03:04" }}{{ end }} +<form action="/savehfcs" method="POST"> +<input type="hidden" name="CSRF" value="{{ $csrf }}"> +<input type="hidden" name="hfcsid" value="{{ .ID }}"> +<input type="hidden" name="itsok" value="iforgiveyou"> +<button name="pardon" value="pardon">pardon</button> +</form> +<p> +</section> +{{ end }} +</main>
A views/honk.html

@@ -0,0 +1,150 @@

+<article class="honk {{ .Honk.Style }}" data-convoy="{{ .Honk.Convoy }}"> +{{ $bonkcsrf := .BonkCSRF }} +{{ $IsPreview := .IsPreview }} +{{ $maplink := .MapLink }} +{{ $omitimages := .OmitImages }} +{{ with .Honk }} +<header> +{{ if $bonkcsrf }} +<a class="honkerlink" href="/h?xid={{ .Honker }}" data-xid="{{ .Honker }}"> +{{ else }} +<a href="{{ .Honker }}" rel=noreferrer> +{{ end }} +<img alt="" src="/a?a={{ .Honker}}"> +{{ if $bonkcsrf }} </a> {{ end }} +{{ if .Oonker }} +{{ if $bonkcsrf }} +<a class="honkerlink" href="/h?xid={{ .Oonker }}" data-xid="{{ .Oonker }}"> +{{ else }} +<a href="{{ .Oonker }}" rel=noreferrer> +{{ end }} +<img alt="" src="/a?a={{ .Oonker}}"> +{{ if $bonkcsrf }} </a> {{ end }} +{{ end }} +<p> +{{ if $bonkcsrf }} +<a class="honkerlink" href="/h?xid={{ .Honker }}" data-xid="{{ .Honker }}">{{ .Username }}</a> +{{ else }} +<a href="{{ .Honker }}" rel=noreferrer>{{ .Username }}</a> +{{ end }} +<span class="clip"><a href="{{ .URL }}" rel=noreferrer>{{ .What }}</a> <span style="float: right;">{{ .Date.Local.Format "02 Jan, 2006 15:04" }}</span> </span> +{{ if .Oonker }} +<br> +<span style="margin-left: 1em;" class="clip"> +{{ if $bonkcsrf }} +original: <a class="honkerlink" href="/h?xid={{ .Oonker }}" data-xid="{{ .Oonker }}">{{ .Oondle }}</a> +{{ else }} +original: <a href="{{ .Oonker }}" rel=noreferrer>{{ .Oondle }}</a> +{{ end }} +</span> +{{ else }} +{{ if .RID }} +<br> +<span style="margin-left: 1em;" class="clip"> +in reply to: <a href="{{ .RID }}" rel=noreferrer>{{ .RID }}</a> +</span> +{{ end }} +{{ end }} +<br> +{{ if $bonkcsrf }} +<span style="margin-left: 1em;" class="clip">convoy: <a class="convoylink" href="/t?c={{ .Convoy }}">{{ .Convoy }}</a></span> +{{ end }} +</header> +<p> +<details class="noise" {{ .Open }} > +<summary>{{ .HTPrecis }}<p></summary> +<p>{{ .HTPrecis }} +<p class="content">{{ .HTML }} +{{ with .Time }} +<p>Time: {{ .StartTime.Local.Format "03:04PM EDT Mon Jan 02"}} +{{ if .Duration }}<br>Duration: {{ .Duration }}{{ end }} +{{ end }} +{{ with .Place }} +<p>Location: {{ with .Url }}<a href="{{ . }}" rel=noreferrer>{{ end }}{{ .Name }}{{ if .Url }}</a>{{ end }}{{ if or .Latitude .Longitude }} <a href="{{ if eq $maplink "apple" }}https://maps.apple.com/?q={{ or .Name "here" }}&z=16&ll={{ .Latitude }},{{ .Longitude }}{{ else }}https://www.openstreetmap.org/?mlat={{ .Latitude }}&mlon={{ .Longitude}}#map=16/{{ .Latitude }}/{{ .Longitude }}{{ end }}" rel=noreferrer>{{ .Latitude }} {{ .Longitude }}</a>{{ end }} +{{ end }} +{{ range .Donks }} +{{ if .Local }} +{{ if eq .Media "text/plain" }} +<p><a href="/d/{{ .XID }}">Attachment: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} +{{ else if eq .Media "application/pdf" }} +<p><a href="/d/{{ .XID }}">Attachment: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} +{{ else }} +{{ if $omitimages }} +<p><a href="/d/{{ .XID }}">Image: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} +{{ else }} +<p><img src="/d/{{ .XID }}" title="{{ .Desc }}" alt="{{ .Desc }}"> +{{ end }} +{{ end }} +{{ else }} +{{ if .External }} +<p><a href="{{ .URL }}" rel=noreferrer>External Attachment: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} +{{ else }} +{{ if eq .Media "video/mp4" }} +<p><video controls src="{{ .URL }}">{{ .Name }}</video> +{{ else }} +<p><img src="{{ .URL }}" title="{{ .Desc }}" alt="{{ .Desc }}"> +{{ end }} +{{ end }} +{{ end }} +{{ end }} +</details> +{{ end }} +{{ if eq .Honk.What "wonked" }} +<p> +{{ if and $bonkcsrf .Honk.IsWonked }} +{{ .Honk.Guesses }} +<p>{{ .Honk.Noise }} +{{ else }} +<button onclick="return playit(this, '{{ .Honk.Noise }}', '{{ .Honk.Wonkles }}', '{{ .Honk.XID }}')">it's play time!</button> +{{ end }} +{{ end }} +{{ if and $bonkcsrf (not $IsPreview) }} +<p> +<details class="actions"> +<summary>Actions</summary> +<div> +<p> +{{ if .Honk.Public }} +{{ if .Honk.IsBonked }} +<button onclick="return unbonk(this, '{{ .Honk.XID }}');">unbonk</button> +{{ else }} +<button onclick="return bonk(this, '{{ .Honk.XID }}');">bonk</button> +{{ end }} +{{ else }} +<button disabled>nope</button> +{{ end }} +<button onclick="return showhonkform(this, '{{ .Honk.XID }}', '{{ .Honk.Handles }}');"><a href="/newhonk?rid={{ .Honk.XID }}">honk back</a></button> +<button onclick="return muteit(this, '{{ .Honk.Convoy }}');">mute</button> +<button onclick="return showelement('evenmore{{ .Honk.ID }}')">even more</button> +</div> +<div id="evenmore{{ .Honk.ID }}" style="display:none"> +<p> +<button onclick="return zonkit(this, '{{ .Honk.XID }}');">zonk</button> +{{ if .Honk.IsAcked }} +<button onclick="return flogit(this, 'deack', '{{ .Honk.XID }}');">deack</button> +{{ else }} +<button onclick="return flogit(this, 'ack', '{{ .Honk.XID }}');">ack</button> +{{ end }} +{{ if .Honk.IsSaved }} +<button onclick="return flogit(this, 'unsave', '{{ .Honk.XID }}');">unsave</button> +{{ else }} +<button onclick="return flogit(this, 'save', '{{ .Honk.XID }}');">save</button> +{{ end }} +{{ if .Honk.IsUntagged }} +<button disabled>untagged</button> +{{ else }} +<button onclick="return flogit(this, 'untag', '{{ .Honk.XID }}');">untag me</button> +{{ end }} +<button><a href="/edit?xid={{ .Honk.XID }}">edit</a></button> +{{ if not (eq .Badonk "none") }} +{{ if .Honk.IsReacted }} +<button disabled>badonked</button> +{{ else }} +<button onclick="return flogit(this, 'react', '{{ .Honk.XID }}');">{{ .Badonk }}</button> +{{ end }} +{{ end }} +</div> +</details> +<p> +{{ end }} +</article>
A views/honkers.html

@@ -0,0 +1,61 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +<p> +<form action="/submithonker" method="POST"> +<h3>add new honker</h3> +<input type="hidden" name="CSRF" value="{{ .HonkerCSRF }}"> +<p><label for=url>url:</label><br> +<input tabindex=1 type="text" name="url" value="" autocomplete=off> +<p><label for=name>name:</label><br> +<input tabindex=1 type="text" name="name" value="" placeholder="optional" autocomplete=off> +<p><label for=combos>combos:</label><br> +<input tabindex=1 type="text" name="combos" value="" placeholder="optional"> +<p><span><label class=button for="peep">skip subscribe: +<input tabindex=1 type="checkbox" id="peep" name="peep" value="peep"><span></span></label></span> +<p><label for="notes">notes:</label><br> +<textarea tabindex=1 name="notes"> +</textarea> +<p><button tabindex=1 name="add honker" value="add honker">add honker</button> +</form> +</div> +{{ $honkercsrf := .HonkerCSRF }} +<div class="info"> +<script> +function expandstuff() { + var els = document.querySelectorAll(".honk details") + for (var i = 0; i < els.length; i++) { + els[i].open = true + } +} +</script> +<p><button onclick="expandstuff()">expand</button> +</div> +{{ range .Honkers }} +<section class="honk"> +<header> +<img alt="avatar" src="/a?a={{ .XID }}"> +<p style="font-size: 1.8em"><a href="/h/{{ .Name }}">{{ .Name }}<a> +</header> +<p> +<details> +<p>url: <a href="{{ .XID }}" rel=noreferrer>{{ .XID }}</a> +<p>flavor: {{ .Flavor }} +<form action="/submithonker" method="POST"> +<input type="hidden" name="CSRF" value="{{ $honkercsrf }}"> +<input type="hidden" name="honkerid" value="{{ .ID }}"> +<p>name: <input type="text" name="name" value="{{ .Name }}"> +<p><label for="notes">notes:</label><br> +<textarea name="notes">{{ .Meta.Notes }}</textarea> +<p>combos: <input type="text" name="combos" value="{{ range .Combos }}{{ . }} {{end}}"> +<p> +<button name="save" value="save">save</button> +<button name="sub" value="sub">(re)sub</button> +<button name="unsub" value="unsub">unsub</button> +<button name="delete" value="delete">delete</button> +</form> +</details> +<p> +</section> +{{ end }} +</main>
A views/honkform.html

@@ -0,0 +1,48 @@

+<p id="honkformhost"> +<button id="honkingtime" onclick="return showhonkform();" {{ if .IsPreview }}style="display:none"{{ end }}><a href="/newhonk">new post</a></button> +<form id="honkform" action="/honk" method="POST" enctype="multipart/form-data" {{ if not .IsPreview }}style="display: none"{{ end }}> +<input type="hidden" name="CSRF" value="{{ .HonkCSRF }}"> +<input type="hidden" name="updatexid" id="updatexidinput" value = "{{ .UpdateXID }}"> +<input type="hidden" name="rid" id="ridinput" value="{{ .InReplyTo }}"> +<h4>new post</h4> +<p> +<details> +<summary>more options</summary> +<p> + +<label class=button id="donker">attach: <input onchange="updatedonker();" id="donkinput" type="file" name="donk"><span>{{ .SavedFile }}</span></label> +<input type="hidden" id="saveddonkxid" name="donkxid" value="{{ .SavedFile }}"> +<p id="donkdescriptor"><label for=donkdesc>description:</label><br> +<input type="text" name="donkdesc" value="{{ .DonkDesc }}" autocomplete=off> +{{ with .SavedPlace }} +<p><button id=checkinbutton type=button onclick="fillcheckin()">checkin</button> +<div id=placedescriptor> + <p><label>name:</label><br><input type="text" name="placename" id=placenameinput value="{{ .Name }}"> + <p><label>url:</label><br><input type="text" name="placeurl" id=placeurlinput value="{{ .Url }}"> + <p><label>lat: </label><input type="text" size=9 name="placelat" id=placelatinput value="{{ .Latitude}}"> + <label>lon: </label><input type="text" size=9 name="placelong" id=placelonginput value="{{ .Longitude }}"> +</div> +{{ else }} +<p><button id=checkinbutton type=button onclick="fillcheckin()">checkin</button> +<div id=placedescriptor style="display: none"> +<p><label>name:</label><br><input type="text" name="placename" id=placenameinput value=""> +<p><label>url:</label><br><input type="text" name="placeurl" id=placeurlinput value=""> +<p><label>lat: </label><input type="text" size=9 name="placelat" id=placelatinput value=""> +<label>lon: </label><input type="text" size=9 name="placelong" id=placelonginput value=""> +</div> +{{ end }} +<p><button id=addtimebutton type=button onclick="showelement('timedescriptor')">add time</button> +<div id=timedescriptor style="{{ or .ShowTime "display: none" }}"> +<p><label for=timestart>start:</label><br> +<input type="text" name="timestart" value="{{ .StartTime }}"> +<p><label for=timeend>duration:</label><br> +<input type="text" name="timeend" value="{{ .Duration }}"> +</div> +</details> +<p> +<textarea name="noise" id="honknoise">{{ .Noise }}</textarea> +<p class="buttonarray"> +<button>post!</button> +<button name="preview" value="preview">preview</button> +<button type=button name="cancel" value="cancel" onclick="cancelhonking()">cancel</button> +</form>
A views/honkfrags.html

@@ -0,0 +1,7 @@

+{{ $BonkCSRF := .HonkCSRF }} +{{ $MapLink := .MapLink }} +{{ $Badonk := .User.Options.Reaction }} +{{ $OmitImages := .User.Options.OmitImages }} +{{ range .Honks }} +{{ template "honk.html" map "Honk" . "MapLink" $MapLink "BonkCSRF" $BonkCSRF "Badonk" $Badonk "OmitImages" $OmitImages }} +{{ end }}
A views/honkpage.html

@@ -0,0 +1,55 @@

+{{ template "header.html" . }} +<main> +<div class="info" id="infobox"> +<div id="srvmsg"> +{{ if .Name }} +<p>{{ .Name }} <span style="margin-left:1em;"><a href="/u/{{ .Name }}/rss">rss</a></span> +<p>{{ .WhatAbout }} +{{ end }} +<p>{{ .ServerMessage }} +</div> +{{ if .HonkCSRF }} +{{ template "honkform.html" . }} +<script> +var csrftoken = {{ .HonkCSRF }} +var honksforpage = { } +var curpagestate = { name: "{{ .PageName }}", arg : "{{ .PageArg }}" } +var tophid = { } +tophid[curpagestate.name + ":" + curpagestate.arg] = "{{ .TopHID }}" +var servermsgs = { } +servermsgs[curpagestate.name + ":" + curpagestate.arg] = "{{ .ServerMessage }}" +</script> +<script src="/honkpage.js{{ .JSParam }}"></script> +{{ end }} +<script> +function playit(elem, word, wordlist, xid) { + import('/wonk.js').then(module => { + makeaguess = module.makeaguess + module.addguesscontrols(elem, word, wordlist, xid) + }) +} +</script> +{{ if .LocalJSParam }} +<script src="/local.js{{ .LocalJSParam }}"></script> +{{ end }} +</div> +{{ if and .HonkCSRF (not .IsPreview) }} +<div class="info" id="refreshbox"> +<p><button onclick="refreshhonks(this)">refresh</button><span></span> +<button onclick="oldestnewest(this)">scroll down</button> +</div> +{{ if eq .ServerMessage "one honk maybe more" }} <script> hideelement("refreshbox")</script> {{ end }} +{{ end }} +<div id="honksonpage"> +<div> +{{ $BonkCSRF := .HonkCSRF }} +{{ $IsPreview := .IsPreview }} +{{ $MapLink := .MapLink }} +{{ $Badonk := .User.Options.Reaction }} +{{ $OmitImages := .User.Options.OmitImages }} +{{ range .Honks }} +{{ template "honk.html" map "Honk" . "MapLink" $MapLink "BonkCSRF" $BonkCSRF "IsPreview" $IsPreview "Badonk" $Badonk "OmitImages" $OmitImages }} +{{ end }} +</div> +</div> +</main>
A views/honkpage.js

@@ -0,0 +1,371 @@

+function encode(hash) { + var s = [] + for (var key in hash) { + var val = hash[key] + s.push(escape(key) + "=" + escape(val)) + } + return s.join("&") +} +function post(url, data) { + var x = new XMLHttpRequest() + x.open("POST", url) + x.timeout = 30 * 1000 + x.setRequestHeader("Content-Type", "application/x-www-form-urlencoded") + x.send(data) +} +function get(url, whendone, whentimedout) { + var x = new XMLHttpRequest() + x.open("GET", url) + x.timeout = 15 * 1000 + x.responseType = "json" + x.onload = function() { whendone(x) } + if (whentimedout) { + x.ontimeout = function(e) { whentimedout(x, e) } + } + x.send() +} +function bonk(el, xid) { + el.innerHTML = "bonked" + el.disabled = true + post("/bonk", encode({"js": "2", "CSRF": csrftoken, "xid": xid})) + return false +} +function unbonk(el, xid) { + el.innerHTML = "unbonked" + el.disabled = true + post("/zonkit", encode({"CSRF": csrftoken, "wherefore": "unbonk", "what": xid})) +} +function muteit(el, convoy) { + el.innerHTML = "muted" + el.disabled = true + post("/zonkit", encode({"CSRF": csrftoken, "wherefore": "zonvoy", "what": convoy})) + var els = document.querySelectorAll('article.honk') + for (var i = 0; i < els.length; i++) { + var e = els[i] + if (e.getAttribute("data-convoy") == convoy) { + e.remove() + } + } +} +function zonkit(el, xid) { + el.innerHTML = "zonked" + el.disabled = true + post("/zonkit", encode({"CSRF": csrftoken, "wherefore": "zonk", "what": xid})) + var p = el + while (p && p.tagName != "ARTICLE") { + p = p.parentElement + } + if (p) { + p.remove() + } +} +function flogit(el, how, xid) { + var s = how + if (s[s.length-1] != "e") { s += "e" } + s += "d" + if (s == "untaged") s = "untagged" + if (s == "reacted") s = "badonked" + el.innerHTML = s + el.disabled = true + post("/zonkit", encode({"CSRF": csrftoken, "wherefore": how, "what": xid})) +} + +var lehonkform = document.getElementById("honkform") +var lehonkbutton = document.getElementById("honkingtime") + +function oldestnewest(btn) { + var els = document.getElementsByClassName("glow") + if (els.length) { + els[els.length-1].scrollIntoView() + } +} +function removeglow() { + var els = document.getElementsByClassName("glow") + while (els.length) { + els[0].classList.remove("glow") + } +} + +function fillinhonks(xhr, glowit) { + var resp = xhr.response + var stash = curpagestate.name + ":" + curpagestate.arg + tophid[stash] = resp.Tophid + var doc = document.createElement( 'div' ); + doc.innerHTML = resp.Srvmsg + var srvmsg = doc + doc = document.createElement( 'div' ); + doc.innerHTML = resp.Honks + var honks = doc.children + + var mecount = document.getElementById("mecount") + if (resp.MeCount) { + mecount.innerHTML = "(" + resp.MeCount + ")" + } else { + mecount.innerHTML = "" + } + var chatcount = document.getElementById("chatcount") + if (resp.ChatCount) { + chatcount.innerHTML = "(" + resp.ChatCount + ")" + } else { + chatcount.innerHTML = "" + } + + var srvel = document.getElementById("srvmsg") + while (srvel.children[0]) { + srvel.children[0].remove() + } + srvel.prepend(srvmsg) + + var frontload = true + if (curpagestate.name == "convoy") { + frontload = false + } + + var honksonpage = document.getElementById("honksonpage") + var holder = honksonpage.children[0] + var lenhonks = honks.length + for (var i = honks.length; i > 0; i--) { + var h = honks[i-1] + if (glowit) + h.classList.add("glow") + if (frontload) { + holder.prepend(h) + } else { + holder.append(h) + } + + } + relinklinks() + return lenhonks +} +function hydrargs() { + var name = curpagestate.name + var arg = curpagestate.arg + var args = { "page" : name } + if (name == "convoy") { + args["c"] = arg + } else if (name == "combo") { + args["c"] = arg + } else if (name == "honker") { + args["xid"] = arg + } else if (name == "user") { + args["uname"] = arg + } + return args +} +function refreshupdate(msg) { + var el = document.querySelector("#refreshbox p span") + if (el) { + el.innerHTML = msg + } +} +function refreshhonks(btn) { + removeglow() + btn.innerHTML = "refreshing" + btn.disabled = true + var args = hydrargs() + var stash = curpagestate.name + ":" + curpagestate.arg + args["tophid"] = tophid[stash] + get("/hydra?" + encode(args), function(xhr) { + btn.innerHTML = "refresh" + btn.disabled = false + if (xhr.status == 200) { + var lenhonks = fillinhonks(xhr, true) + refreshupdate(" " + lenhonks + " new") + } else { + refreshupdate(" status: " + xhr.status) + } + }, function(xhr, e) { + btn.innerHTML = "refresh" + btn.disabled = false + refreshupdate(" timed out") + }) +} +function statechanger(evt) { + var data = evt.state + if (!data) { + return + } + switchtopage(data.name, data.arg) +} +function switchtopage(name, arg) { + var stash = curpagestate.name + ":" + curpagestate.arg + var honksonpage = document.getElementById("honksonpage") + var holder = honksonpage.children[0] + holder.remove() + var srvel = document.getElementById("srvmsg") + var msg = srvel.children[0] + if (msg) { + msg.remove() + servermsgs[stash] = msg + } + showelement("refreshbox") + + honksforpage[stash] = holder + + curpagestate.name = name + curpagestate.arg = arg + // get the holder for the target page + var stash = name + ":" + arg + holder = honksforpage[stash] + if (holder) { + honksonpage.prepend(holder) + msg = servermsgs[stash] + if (msg) { + srvel.prepend(msg) + } + } else { + // or create one and fill it + honksonpage.prepend(document.createElement("div")) + var args = hydrargs() + get("/hydra?" + encode(args), function(xhr) { + if (xhr.status == 200) { + var lenhonks = fillinhonks(xhr, false) + } else { + refreshupdate(" status: " + xhr.status) + } + }, function(xhr, e) { + refreshupdate(" timed out") + }) + } + refreshupdate("") +} +function newpagestate(name, arg) { + return { "name": name, "arg": arg } +} +function pageswitcher(name, arg) { + return function(evt) { + var topmenu = document.getElementById("topmenu") + var isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + if (isMobile) { + topmenu.open = false + } else { + topmenu.open = true + } + + if (name == curpagestate.name && arg == curpagestate.arg) { + return false + } + switchtopage(name, arg) + var url = evt.srcElement.href + if (!url) { + url = evt.srcElement.parentElement.href + } + history.pushState(newpagestate(name, arg), "some title", url) + window.scrollTo(0, 0) + return false + } +} +function relinklinks() { + var els = document.getElementsByClassName("convoylink") + while (els.length) { + els[0].onclick = pageswitcher("convoy", els[0].text) + els[0].classList.remove("convoylink") + } + els = document.getElementsByClassName("combolink") + while (els.length) { + els[0].onclick = pageswitcher("combo", els[0].text) + els[0].classList.remove("combolink") + } + els = document.getElementsByClassName("honkerlink") + while (els.length) { + var el = els[0] + var xid = el.getAttribute("data-xid") + el.onclick = pageswitcher("honker", xid) + el.classList.remove("honkerlink") + } +} +(function() { + var el = document.getElementById("homelink") + el.onclick = pageswitcher("home", "") + el = document.getElementById("atmelink") + el.onclick = pageswitcher("atme", "") + el = document.getElementById("firstlink") + el.onclick = pageswitcher("first", "") + el = document.getElementById("savedlink") + el.onclick = pageswitcher("saved", "") + el = document.getElementById("longagolink") + el.onclick = pageswitcher("longago", "") + relinklinks() + window.onpopstate = statechanger + history.replaceState(curpagestate, "some title", "") +})(); +(function() { + hideelement("donkdescriptor") +})(); +function showhonkform(elem, rid, hname) { + var form = lehonkform + form.style = "display: block" + if (elem) { + form.remove() + elem.parentElement.parentElement.parentElement.insertAdjacentElement('beforebegin', form) + } else { + hideelement(lehonkbutton) + elem = document.getElementById("honkformhost") + elem.insertAdjacentElement('afterend', form) + } + var ridinput = document.getElementById("ridinput") + if (rid) { + ridinput.value = rid + if (hname) { + honknoise.value = hname + " " + } else { + honknoise.value = "" + } + } else { + ridinput.value = "" + honknoise.value = "" + } + var updateinput = document.getElementById("updatexidinput") + updateinput.value = "" + document.getElementById("honknoise").focus() + return false +} +function cancelhonking() { + hideelement(lehonkform) + showelement(lehonkbutton) +} +function showelement(el) { + if (typeof(el) == "string") + el = document.getElementById(el) + if (!el) return + el.style.display = "block" +} +function hideelement(el) { + if (typeof(el) == "string") + el = document.getElementById(el) + if (!el) return + el.style.display = "none" +} +function updatedonker() { + var el = document.getElementById("donker") + el.children[1].textContent = el.children[0].value.slice(-20) + var el = document.getElementById("donkdescriptor") + el.style.display = "" + var el = document.getElementById("saveddonkxid") + el.value = "" +} +var checkinprec = 100.0 +var gpsoptions = { + enableHighAccuracy: false, + timeout: 1000, + maximumAge: 0 +}; +function fillcheckin() { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function(pos) { + showelement("placedescriptor") + var el = document.getElementById("placelatinput") + el.value = Math.round(pos.coords.latitude * checkinprec) / checkinprec + el = document.getElementById("placelonginput") + el.value = Math.round(pos.coords.longitude * checkinprec) / checkinprec + checkinprec = 10000.0 + gpsoptions.enableHighAccuracy = true + gpsoptions.timeout = 2000 + }, function(err) { + showelement("placedescriptor") + el = document.getElementById("placenameinput") + el.value = err.message + }, gpsoptions) + } +}
A views/local.css

@@ -0,0 +1,381 @@

+:root { + --bg-page: #f4f4f4; + --bg-dark: #eee; + --bg-limited: #ddd; + --fg: #000; + --fg-subtle: #666; + --fg-limited: #509c93; + --hl: #c2c2c2; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-page: #111; + --bg-dark: #222; + --fg: #ccc; + --hl: #333; + --fg-subtle: #ccc; + --fg-limited: #509c93; + --bg-limited: #333; + } +} + +* { + font-size: 14px !important; +} + +body { + background: var(--bg-page); + color: var(--fg); + font-size: 14px !important; + word-wrap: break-word; + font-family: -apple-system, sans-serif, "Noto Color Emoji"; + line-height: 1.2; + overscroll-behavior-y: contain; +} +pre, code { + white-space: pre-wrap; +} +blockquote { + margin-left: 0em; + margin-bottom: 0em; + padding-left: 0.5em; + border-left: 1px solid var(--fg-subtle); +} +cite { + margin-left: 2em; +} +table { + display: block; + max-width: 100%; + overflow-x: auto; +} +a { + color: var(--fg); +} +form, input, textarea { + font-family: -apple-system, sans-serif, "Noto Color Emoji"; +} +p { + margin-top: 1em; + margin-bottom: 1em; +} +input { + background: var(--bg-page); + color: var(--fg); + font-size: 1.0em; + line-height: 1.2em; + padding: 0.4em; +} +#honkform input { + font-size: 0.8em; +} +body > header { + margin: 1em auto; + font-size: 1.5em; +} +body > header span { + margin-left: 2em; +} +body > header p { + padding: 1em; +} +header > details { + background: var(--bg-page); + padding: 1em 1em 1em 1em; + position: fixed; + top: 0; + left: 0; + display: inline; + max-height: calc(100% - 1em); + overflow: auto; + opacity: 0.7; + overscroll-behavior: contain; + z-index: 2; +} +header > details[open] { + padding: 1em 1em 0em 1em; + background: var(--bg-dark); + border: 1px solid var(--hl); + margin-bottom: 1em; + opacity: 1.0; +} +header > details summary span { + display: none; +} +header > details[open] summary span { + display: inline; +} +header > details li { + margin: 1em 0em 1em 0em; +} +details summary { + cursor: pointer; +} +main { + max-width: 1200px; + margin: auto; + font-size: 1.5em; +} +hr { + border-color: var(--hl); +} +.info { + background: var(--bg-dark); + border: 1px solid var(--hl); + margin-bottom: 1em; + padding: 0em 1em 0em 1em; +} +.info div { + margin-top: 1em; + margin-bottom: 1em; +} +label { + font-size: 0.8em; +} +label.button, button, select { + font-size: 16px; + font-family: -apple-system, sans-serif; + color: var(--fg); + background: var(--bg-page); + border: 1px solid var(--hl); + padding: 0.5em; + white-space: nowrap; +} +.buttonarray { + margin-top: -2.0em; +} +.buttonarray button, .buttonarray > span { + margin-top: 2.0em; + display: inline-block; +} +button a { + text-decoration: none; +} +button { + cursor: pointer; +} +form { + margin-top: 1em; +} +textarea { + padding: 0.5em; + font-size: 1em; + background: var(--bg-page); + color: var(--fg); + width: 600px; + height: 4em; + margin-bottom: 0.5em; + box-sizing: border-box; + max-width: 100%; +} +textarea#honknoise { + height: 10em; +} +input[type="checkbox"] { + position: fixed; + top: -9999px; +} +input[type="checkbox"] + span:after { + content: "no"; +} +input[type="checkbox"]:checked + span:after { + content: "yes"; +} +input[type="checkbox"]:focus + span:after { + outline: 1px solid var(--fg); +} +input[type=file] { + display: none; +} + +.glow { + box-shadow: 0px 0px 16px var(--hl); +} + +.honk { + margin: auto; + background: var(--bg-dark); + border: 0px solid var(--hl); + margin-bottom: 1em; + border-radius: 0em !important; + padding-left: 1em; + padding-right: 1em; + padding-top: 0; + overflow: hidden; +} + +.chat { + border-bottom: 0.5px solid var(--fg-subtle); + padding-left: 1em; +} +.chat p { + margin-top: 0.2em; + margin-bottom: 0.2em; +} +.chattarget { + border-bottom: 1px solid var(--fg-subtle); +} +.chatstamp { + margin-left: -1em; +} + +.honk #honkform { + padding: 1em; + border: 1px solid var(--fg); +} +.honk a { + color: var(--fg); +} +.honk header { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.8em; + line-height: 1.1; + margin-top: 1em; + height: 64px; +} + +.honk header .clip a { + color: var(--fg-subtle); +} + +.clip { + text-transform: lowercase; +} + +.honk header img { + float: left; + margin-right: 1em; + width: 64px; + height: 64px; +} +.honk header p { + margin-top: 0px; +} +.honk .actions button { + margin-left: 4em; + margin-top: 2em; +} +.honk .noise { + line-height: 1.4; +} + +.honk .noise code .kw { font-weight: bold; } +.honk .noise code .bi { font-weight: bold; } +.honk .noise code .st { color: var(--fg-subtle); } +.honk .noise code .nm { color: #ba88ff; } +.honk .noise code .op { color: #ba88ff; } +.honk .noise code .tp { font-weight: bold; } +.honk .noise code .cm { color: var(--fg-subtle); font-style: italic; } +.honk .noise code .al { color: #aaffbb; } +.honk .noise code .dl { color: #ffaabb; } + +.honk details.actions summary { + color: var(--fg-subtle); +} +.subtle .noise { + color: var(--fg-subtle); + font-size: 0.8em; +} +.subtle .noise a { + color: var(--fg-subtle); +} +.limited { + background: var(--bg-limited); + border: 0px solid var(--fg-limited); + color: var(--fg-subtle); +} +.limited .glow { + box-shadow: 0px 0px 16px var(--fg-limited); +} +.limited .noise { + color: var(--fg-subtle); +} +.limited .noise a { + color: var(--fg-limited); +} +.limited details.actions summary { + color: var(--fg-limited); +} +details.noise[open] summary { + display: none; +} +h1, h2 { + font-size: 1.2em; +} +h3, h4 { + font-size: 1.1em; +} + +nav { + float: right; +} + +nav ul { + padding: 0; + margin: 0; + list-style: none; + padding-bottom: 20px; +} + +nav ul li { + padding-right: 10px; + display: inline-block; +} + +img:not(.emu) { + background: var(--bg-page); +} +img, video { + max-width: 100%; + max-height: 600px; +} +.noise img:not(.emu) { + display: block; +} +img.emu { + width: 2em; + height: 2em; + vertical-align: middle; + margin: -2px; + object-fit: contain; +} +.nophone { + position: fixed; + opacity: 0.7; + cursor: pointer; +} +@media screen and (max-width: 1360px) { + .nophone { + display: none; + } +} +@media screen and (max-width: 740px) { + body { + font-size: 12px; + } + .honk header { + height: 52px; + } + .honk header img { + width: 48px; + height: 48px; + } + details summary { + outline: none; + } +} +@media print { + #topmenu, #topspacer, #infobox, #refreshbox, .actions { + display: none; + } + html { + --bg-page: white; + --bg-dark: white; + --fg: black; + --fg-subtle: black; + --fg-limited: #a79; + } +}
A views/local.js

@@ -0,0 +1,6 @@

+ const donk = document.getElementById("donkinput"); + + window.addEventListener('paste', e => { + donk.files = e.clipboardData.files; + }); + donk.onchange();
A views/login.html

@@ -0,0 +1,11 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +{{ .LoginMsg }} +<form action="/dologin" method="POST"> + <p><input tabindex=1 type="text" name="username" autocomplete=off> - username + <p><input tabindex=1 type="password" name="password"> - password + <p><button tabindex=1 name="login" value="login">login</button> +</form> +</div> +</main>
A views/msg.html

@@ -0,0 +1,7 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +<p> +{{ .ServerMessage }} +</div> +</main>
A views/onts.html

@@ -0,0 +1,17 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +<p>ontologies of interest +{{ $firstrune := .FirstRune }} +{{ $letter := 0 }} +<ul> +{{ range .Onts }} +{{ if not (eq $letter (call $firstrune .Name)) }} +{{ $letter = (call $firstrune .Name) }} +<li><p> +{{ end }} +<span style="white-space: nowrap;"><a href="/o/{{ .Name }}">#{{ .Name }}</a> ({{ .Count }})</span> +{{ end }} +</ul> +</div> +</main>
A views/pleroma.css

@@ -0,0 +1,7 @@

+html { + --bg-page: #1b2735; + --bg-dark: #121a24; + --fg: #b9b9ba; + --hl: #d8a070; + --fg-subtle: rgba(185, 185, 186, 0.5); +}
A views/style.css

@@ -0,0 +1,342 @@

+html { + --bg-page: #306; + --bg-dark: #002; + --fg: #dcf; + --hl: #dcf; + --fg-subtle: #a9c; + --fg-limited: #a79; +} + +body { + background: var(--bg-page); + color: var(--fg); + font-size: 1em; + word-wrap: break-word; + font-family: sans-serif, "Noto Color Emoji"; + line-height: 1.2; + overscroll-behavior-y: contain; +} +pre, code { + white-space: pre-wrap; +} +blockquote { + margin-left: 0em; + margin-bottom: 0em; + padding-left: 0.5em; + border-left: 1px solid var(--fg-subtle); +} +cite { + margin-left: 2em; +} +table { + display: block; + max-width: 100%; + overflow-x: auto; +} +a { + color: var(--fg); +} +form, input, textarea { + font-family: monospace, "Noto Color Emoji"; +} +p { + margin-top: 1em; + margin-bottom: 1em; +} +input { + background: var(--bg-page); + color: var(--fg); + font-size: 1.0em; + line-height: 1.2em; + padding: 0.4em; +} +#honkform input { + font-size: 0.8em; +} +body > header { + margin: 1em auto; + font-size: 1.5em; +} +body > header span { + margin-left: 2em; +} +body > header p { + padding: 1em; +} +header > details { + background: var(--bg-page); + padding: 1em 1em 1em 1em; + position: fixed; + top: 0; + left: 0; + display: inline; + max-height: calc(100% - 1em); + overflow: auto; + opacity: 0.7; + overscroll-behavior: contain; + z-index: 2; +} +header > details[open] { + padding: 1em 1em 0em 1em; + background: var(--bg-dark); + border: 1px solid var(--hl); + margin-bottom: 1em; + opacity: 1.0; +} +header > details summary span { + display: none; +} +header > details[open] summary span { + display: inline; +} +header > details li { + margin: 1em 0em 1em 0em; +} +details summary { + cursor: pointer; +} +main { + max-width: 1200px; + margin: auto; + font-size: 1.5em; +} +hr { + border-color: var(--hl); +} +.info { + background: var(--bg-dark); + border: 1px solid var(--hl); + margin-bottom: 1em; + padding: 0em 1em 0em 1em; +} +.info div { + margin-top: 1em; + margin-bottom: 1em; +} +label { + font-size: 0.8em; +} +label.button, button, select { + font-size: 16px; + font-family: monospace; + color: var(--fg); + background: var(--bg-page); + border: 1px solid var(--hl); + padding: 0.5em; + white-space: nowrap; +} +.buttonarray { + margin-top: -2.0em; +} +.buttonarray button, .buttonarray > span { + margin-top: 2.0em; + display: inline-block; +} +button a { + text-decoration: none; +} +button { + cursor: pointer; +} +form { + margin-top: 1em; +} +textarea { + padding: 0.5em; + font-size: 1em; + background: var(--bg-page); + color: var(--fg); + width: 600px; + height: 4em; + margin-bottom: 0.5em; + box-sizing: border-box; + max-width: 100%; +} +textarea#honknoise { + height: 10em; +} +input[type="checkbox"] { + position: fixed; + top: -9999px; +} +input[type="checkbox"] + span:after { + content: "no"; +} +input[type="checkbox"]:checked + span:after { + content: "yes"; +} +input[type="checkbox"]:focus + span:after { + outline: 1px solid var(--fg); +} +input[type=file] { + display: none; +} + +.glow { + box-shadow: 0px 0px 16px var(--hl); +} + +.honk { + margin: auto; + background: var(--bg-dark); + border: 1px solid var(--hl); + border-radius: 1em; + margin-bottom: 1em; + padding-left: 1em; + padding-right: 1em; + padding-top: 0; + overflow: hidden; +} + +.chat { + border-bottom: 0.5px solid var(--fg-subtle); + padding-left: 1em; +} +.chat p { + margin-top: 0.2em; + margin-bottom: 0.2em; +} +.chattarget { + border-bottom: 1px solid var(--fg-subtle); +} +.chatstamp { + margin-left: -1em; +} + +.honk #honkform { + padding: 1em; + border: 1px solid var(--fg); + } +.honk a { + color: var(--fg); + } +.honk header { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.8em; + line-height: 1.1; + margin-top: 1em; + height: 64px; + } + +.honk header .clip a { + color: var(--fg-subtle); + } +.honk header img { + float: left; + margin-right: 1em; + width: 64px; + height: 64px; + } +.honk header p { + margin-top: 0px; + } +.honk .actions button { + margin-left: 4em; + margin-top: 2em; + } +.honk .noise { + line-height: 1.4; + } + +.honk .noise code .kw { font-weight: bold; } +.honk .noise code .bi { font-weight: bold; } +.honk .noise code .st { color: var(--fg-subtle); } +.honk .noise code .nm { color: #ba88ff; } +.honk .noise code .op { color: #ba88ff; } +.honk .noise code .tp { font-weight: bold; } +.honk .noise code .cm { color: var(--fg-subtle); font-style: italic; } +.honk .noise code .al { color: #aaffbb; } +.honk .noise code .dl { color: #ffaabb; } + +.honk details.actions summary { + color: var(--fg-subtle); +} +.subtle .noise { + color: var(--fg-subtle); + font-size: 0.8em; +} +.subtle .noise a { + color: var(--fg-subtle); +} +.limited { + border: 1px solid var(--fg-limited); + color: var(--fg-limited); +} +.limited .glow { + box-shadow: 0px 0px 16px var(--fg-limited); +} +.limited .noise { + color: var(--fg-limited); + } +.limited .noise a { + color: var(--fg-limited); + } +.limited details.actions summary { + color: var(--fg-limited); + } +details.noise[open] summary { + display: none; +} +h1, h2 { + font-size: 1.2em; +} +h3, h4 { + font-size: 1.1em; +} + +img:not(.emu) { + background: var(--bg-page); +} +img, video { + max-width: 100%; + max-height: 600px; +} +.noise img:not(.emu) { + display: block; +} +img.emu { + width: 2em; + height: 2em; + vertical-align: middle; + margin: -2px; + object-fit: contain; +} +.nophone { + position: fixed; + opacity: 0.7; + cursor: pointer; +} +@media screen and (max-width: 1360px) { + .nophone { + display: none; + } +} +@media screen and (max-width: 740px) { + body { + font-size: 12px; + } + .honk header { + height: 52px; + } + .honk header img { + width: 48px; + height: 48px; + } + details summary { + outline: none; + } +} +@media print { + #topmenu, #topspacer, #infobox, #refreshbox, .actions { + display: none; + } + html { + --bg-page: white; + --bg-dark: white; + --fg: black; + --fg-subtle: black; + --fg-limited: #a79; + } +}
A views/wonk.js

@@ -0,0 +1,83 @@

+export function addguesscontrols(elem, word, wordlist, xid) { + var host = elem.parentElement + elem.innerHTML = "loading..." + + host.correctAnswer = word + host.guesses = [] + host.xid = xid + var xhr = new XMLHttpRequest() + xhr.open("GET", "/bloat/wonkles?w=" + escape(wordlist)) + xhr.responseType = "json" + xhr.onload = function() { + var wordlist = xhr.response.wordlist + var validguesses = {} + console.log("valid " + wordlist.length) + for (var i = 0; i < wordlist.length; i++) { + validguesses[wordlist[i]] = true + } + host.validGuesses = validguesses + var div = document.createElement( 'div' ); + div.innerHTML = "<p><input> <button onclick='return makeaguess(this)'>guess</button>" + host.append(div) + elem.remove() + } + xhr.send() +} +export function makeaguess(btn) { + var host = btn.parentElement.parentElement.parentElement + var correct = host.correctAnswer + var valid = host.validGuesses + var inp = btn.previousElementSibling + var g = inp.value.toLowerCase() + var res = "" + if (valid[g]) { + var letters = {} + var obfu = "" + for (var i = 0; i < correct.length; i++) { + var l = correct[i] + letters[l] = (letters[l] | 0) + 1 + } + for (var i = 0; i < g.length && i < correct.length; i++) { + if (g[i] == correct[i]) { + letters[g[i]] = letters[g[i]] - 1 + } + } + for (var i = 0; i < g.length; i++) { + if (i < correct.length && g[i] == correct[i]) { + res += g[i].toUpperCase() + obfu += "&#129001;" + } else if (letters[g[i]] > 0) { + res += g[i] + obfu += "&#129000;" + letters[g[i]] = letters[g[i]] - 1 + } else { + obfu += "&#11035;" + res += "." + } + } + + var div = document.createElement( 'div' ); + div.innerHTML = "<p style='font-family: monospace'>" + res + host.append(div) + host.guesses.push(obfu) + } else { + var div = document.createElement( 'div' ); + div.innerHTML = "<p> invalid guess" + host.append(div) + } + var div = document.createElement( 'div' ); + if (res == correct.toUpperCase()) { + var mess = "<p>you are very smart!" + mess += "<p>" + host.xid + for (var i = 0; i < host.guesses.length; i++) { + mess += "<p>" + host.guesses[i] + } + div.innerHTML = mess + if (typeof(csrftoken) != "undefined") + post("/zonkit", encode({"CSRF": csrftoken, "wherefore": "wonk", "guesses": host.guesses.join("<p>"), "what": host.xid})) + } else { + div.innerHTML = "<p><input> <button onclick='return makeaguess(this)'>guess</button>" + } + host.append(div) + btn.parentElement.remove() +}
A views/xzone.html

@@ -0,0 +1,16 @@

+{{ template "header.html" . }} +<main> +<div class="info"> +<form action="/ximport" method="POST"> +<input type="hidden" name="CSRF" value="{{ .XCSRF }}"> +<p><span class="title">import</span> +<p><input tabindex=1 type="text" name="xid" autocomplete=off> - xid +<p><button tabindex=1 name="fetch" value="fetch">fetch</button> +</form> +</div> +{{ range .Honkers }} +<section class="honk"> +<p><a href="/h?xid={{ .XID }}">honks</a> by {{ .Handle }} +</section> +{{ end }} +</main>
A web.go

@@ -0,0 +1,2559 @@

+// +// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "bytes" + "database/sql" + "fmt" + "html/template" + "io" + notrand "math/rand" + "net/http" + "net/url" + "os" + "os/signal" + "regexp" + "sort" + "strconv" + "strings" + "syscall" + "time" + "unicode/utf8" + + "github.com/gorilla/mux" + "humungus.tedunangst.com/r/webs/cache" + "humungus.tedunangst.com/r/webs/httpsig" + "humungus.tedunangst.com/r/webs/junk" + "humungus.tedunangst.com/r/webs/login" + "humungus.tedunangst.com/r/webs/rss" + "humungus.tedunangst.com/r/webs/templates" +) + +var readviews *templates.Template + +var userSep = "u" +var honkSep = "h" + +var develMode = false + +func getuserstyle(u *login.UserInfo) template.CSS { + if u == nil { + return "" + } + user, _ := butwhatabout(u.Username) + css := template.CSS("") + if user.Options.SkinnyCSS { + css += "main { max-width: 700px; }\n" + } + return css +} + +func getmaplink(u *login.UserInfo) string { + if u == nil { + return "osm" + } + user, _ := butwhatabout(u.Username) + ml := user.Options.MapLink + if ml == "" { + ml = "osm" + } + return ml +} + +func getInfo(r *http.Request) map[string]interface{} { + templinfo := make(map[string]interface{}) + templinfo["StyleParam"] = getassetparam(viewDir + "/views/style.css") + templinfo["LocalStyleParam"] = getassetparam(dataDir + "/views/local.css") + templinfo["JSParam"] = getassetparam(viewDir + "/views/honkpage.js") + templinfo["LocalJSParam"] = getassetparam(dataDir + "/views/local.js") + templinfo["ServerName"] = serverName + templinfo["IconName"] = iconName + templinfo["UserSep"] = userSep + if u := login.GetUserInfo(r); u != nil { + templinfo["UserInfo"], _ = butwhatabout(u.Username) + templinfo["UserStyle"] = getuserstyle(u) + var combos []string + combocache.Get(u.UserID, &combos) + templinfo["Combos"] = combos + } + return templinfo +} + +func homepage(w http.ResponseWriter, r *http.Request) { + templinfo := getInfo(r) + u := login.GetUserInfo(r) + var honks []*Honk + var userid int64 = -1 + + templinfo["ServerMessage"] = serverMsg + if u == nil || r.URL.Path == "/front" { + switch r.URL.Path { + case "/events": + honks = geteventhonks(userid) + templinfo["ServerMessage"] = "some recent and upcoming events" + default: + templinfo["ShowRSS"] = true + honks = getpublichonks() + } + } else { + userid = u.UserID + switch r.URL.Path { + case "/atme": + templinfo["ServerMessage"] = "at me!" + templinfo["PageName"] = "atme" + honks = gethonksforme(userid, 0) + honks = osmosis(honks, userid, false) + menewnone(userid) + case "/longago": + templinfo["ServerMessage"] = "long ago and far away!" + templinfo["PageName"] = "longago" + honks = gethonksfromlongago(userid, 0) + honks = osmosis(honks, userid, false) + case "/events": + templinfo["ServerMessage"] = "some recent and upcoming events" + templinfo["PageName"] = "events" + honks = geteventhonks(userid) + honks = osmosis(honks, userid, true) + case "/first": + templinfo["PageName"] = "first" + honks = gethonksforuserfirstclass(userid, 0) + honks = osmosis(honks, userid, true) + case "/saved": + templinfo["ServerMessage"] = "saved honks" + templinfo["PageName"] = "saved" + honks = getsavedhonks(userid, 0) + default: + templinfo["PageName"] = "home" + honks = gethonksforuser(userid, 0) + honks = osmosis(honks, userid, true) + } + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + } + + honkpage(w, u, honks, templinfo) +} + +func showfunzone(w http.ResponseWriter, r *http.Request) { + var emunames, memenames []string + emuext := make(map[string]string) + dir, err := os.Open(dataDir + "/emus") + if err == nil { + emunames, _ = dir.Readdirnames(0) + dir.Close() + } + for i, e := range emunames { + if len(e) > 4 { + emunames[i] = e[:len(e)-4] + emuext[emunames[i]] = e[len(e)-4:] + } + } + dir, err = os.Open(dataDir + "/memes") + if err == nil { + memenames, _ = dir.Readdirnames(0) + dir.Close() + } + sort.Strings(emunames) + sort.Strings(memenames) + templinfo := getInfo(r) + templinfo["Emus"] = emunames + templinfo["Emuext"] = emuext + templinfo["Memes"] = memenames + err = readviews.Execute(w, "funzone.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +func showrss(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + + var honks []*Honk + if name != "" { + honks = gethonksbyuser(name, false, 0) + } else { + honks = getpublichonks() + } + reverbolate(-1, honks) + + home := fmt.Sprintf("https://%s/", serverName) + base := home + if name != "" { + home += "u/" + name + name += " " + } + feed := rss.Feed{ + Title: name + "honk", + Link: home, + Description: name + "honk rss", + Image: &rss.Image{ + URL: base + "icon.png", + Title: name + "honk rss", + Link: home, + }, + } + var modtime time.Time + for _, honk := range honks { + if !firstclass(honk) { + continue + } + desc := string(honk.HTML) + if t := honk.Time; t != nil { + desc += fmt.Sprintf(`<p>Time: %s`, t.StartTime.Local().Format("03:04PM EDT Mon Jan 02")) + if t.Duration != 0 { + desc += fmt.Sprintf(`<br>Duration: %s`, t.Duration) + } + } + if p := honk.Place; p != nil { + desc += string(templates.Sprintf(`<p>Location: <a href="%s">%s</a> %f %f`, + p.Url, p.Name, p.Latitude, p.Longitude)) + } + for _, d := range honk.Donks { + desc += string(templates.Sprintf(`<p><a href="%s">Attachment: %s</a>`, + d.URL, d.Desc)) + if strings.HasPrefix(d.Media, "image") { + desc += string(templates.Sprintf(`<img src="%s">`, d.URL)) + } + } + + feed.Items = append(feed.Items, &rss.Item{ + Title: fmt.Sprintf("%s %s %s", honk.Username, honk.What, honk.XID), + Description: rss.CData{Data: desc}, + Link: honk.URL, + PubDate: honk.Date.Format(time.RFC1123), + Guid: &rss.Guid{IsPermaLink: true, Value: honk.URL}, + }) + if honk.Date.After(modtime) { + modtime = honk.Date + } + } + if !develMode { + w.Header().Set("Cache-Control", "max-age=300") + w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat)) + } + + err := feed.Write(w) + if err != nil { + elog.Printf("error writing rss: %s", err) + } +} + +func crappola(j junk.Junk) bool { + t, _ := j.GetString("type") + a, _ := j.GetString("actor") + o, _ := j.GetString("object") + if t == "Delete" && a == o { + dlog.Printf("crappola from %s", a) + return true + } + return false +} + +func ping(user *WhatAbout, who string) { + if targ := fullname(who, user.ID); targ != "" { + who = targ + } + if !strings.HasPrefix(who, "https:") { + who = gofish(who) + } + if who == "" { + ilog.Printf("nobody to ping!") + return + } + var box *Box + ok := boxofboxes.Get(who, &box) + if !ok { + ilog.Printf("no inbox to ping %s", who) + return + } + ilog.Printf("sending ping to %s", box.In) + j := junk.New() + j["@context"] = itiswhatitis + j["type"] = "Ping" + j["id"] = user.URL + "/ping/" + xfiltrate() + j["actor"] = user.URL + j["to"] = who + ki := ziggy(user.ID) + if ki == nil { + return + } + err := PostJunk(ki.keyname, ki.seckey, box.In, j) + if err != nil { + elog.Printf("can't send ping: %s", err) + return + } + ilog.Printf("sent ping to %s: %s", who, j["id"]) +} + +func pong(user *WhatAbout, who string, obj string) { + var box *Box + ok := boxofboxes.Get(who, &box) + if !ok { + ilog.Printf("no inbox to pong %s", who) + return + } + j := junk.New() + j["@context"] = itiswhatitis + j["type"] = "Pong" + j["id"] = user.URL + "/pong/" + xfiltrate() + j["actor"] = user.URL + j["to"] = who + j["object"] = obj + ki := ziggy(user.ID) + if ki == nil { + return + } + err := PostJunk(ki.keyname, ki.seckey, box.In, j) + if err != nil { + elog.Printf("can't send pong: %s", err) + return + } +} + +func inbox(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + user, err := butwhatabout(name) + if err != nil { + http.NotFound(w, r) + return + } + if stealthmode(user.ID, r) { + http.NotFound(w, r) + return + } + var buf bytes.Buffer + limiter := io.LimitReader(r.Body, 1*1024*1024) + io.Copy(&buf, limiter) + payload := buf.Bytes() + j, err := junk.FromBytes(payload) + if err != nil { + ilog.Printf("bad payload: %s", err) + ilog.Writer().Write(payload) + ilog.Writer().Write([]byte{'\n'}) + return + } + + if crappola(j) { + return + } + what, _ := j.GetString("type") + obj, _ := j.GetString("object") + if what == "Like" || (what == "EmojiReact" && originate(obj) != serverName) { + return + } + who, _ := j.GetString("actor") + if rejectactor(user.ID, who) { + return + } + + keyname, err := httpsig.VerifyRequest(r, payload, zaggy) + if err != nil && keyname != "" { + savingthrow(keyname) + keyname, err = httpsig.VerifyRequest(r, payload, zaggy) + } + if err != nil { + ilog.Printf("inbox message failed signature for %s from %s: %s", keyname, r.Header.Get("X-Forwarded-For"), err) + if keyname != "" { + ilog.Printf("bad signature from %s", keyname) + ilog.Writer().Write(payload) + ilog.Writer().Write([]byte{'\n'}) + } + http.Error(w, "what did you call me?", http.StatusTeapot) + return + } + origin := keymatch(keyname, who) + if origin == "" { + ilog.Printf("keyname actor mismatch: %s <> %s", keyname, who) + return + } + + switch what { + case "Ping": + id, _ := j.GetString("id") + ilog.Printf("ping from %s: %s", who, id) + pong(user, who, obj) + case "Pong": + ilog.Printf("pong from %s: %s", who, obj) + case "Follow": + if obj != user.URL { + ilog.Printf("can't follow %s", obj) + return + } + followme(user, who, who, j) + case "Accept": + followyou2(user, j) + case "Reject": + nofollowyou2(user, j) + case "Update": + obj, ok := j.GetMap("object") + if ok { + what, _ := obj.GetString("type") + switch what { + case "Service": + fallthrough + case "Person": + return + case "Question": + return + case "Note": + go xonksaver(user, j, origin) + return + } + } + ilog.Printf("unknown Update activity") + dumpactivity(j) + case "Undo": + obj, ok := j.GetMap("object") + if !ok { + folxid, ok := j.GetString("object") + if ok && originate(folxid) == origin { + unfollowme(user, "", "", j) + } + return + } + what, _ := obj.GetString("type") + switch what { + case "Follow": + unfollowme(user, who, who, j) + case "Announce": + xid, _ := obj.GetString("object") + dlog.Printf("undo announce: %s", xid) + case "Like": + default: + ilog.Printf("unknown undo: %s", what) + } + case "EmojiReact": + obj, ok := j.GetString("object") + if ok { + content, _ := j.GetString("content") + addreaction(user, obj, who, content) + } + default: + go xonksaver(user, j, origin) + } +} + +func serverinbox(w http.ResponseWriter, r *http.Request) { + user := getserveruser() + if stealthmode(user.ID, r) { + http.NotFound(w, r) + return + } + var buf bytes.Buffer + io.Copy(&buf, r.Body) + payload := buf.Bytes() + j, err := junk.FromBytes(payload) + if err != nil { + ilog.Printf("bad payload: %s", err) + ilog.Writer().Write(payload) + ilog.Writer().Write([]byte{'\n'}) + return + } + if crappola(j) { + return + } + keyname, err := httpsig.VerifyRequest(r, payload, zaggy) + if err != nil && keyname != "" { + savingthrow(keyname) + keyname, err = httpsig.VerifyRequest(r, payload, zaggy) + } + if err != nil { + ilog.Printf("inbox message failed signature for %s from %s: %s", keyname, r.Header.Get("X-Forwarded-For"), err) + if keyname != "" { + ilog.Printf("bad signature from %s", keyname) + ilog.Writer().Write(payload) + ilog.Writer().Write([]byte{'\n'}) + } + http.Error(w, "what did you call me?", http.StatusTeapot) + return + } + who, _ := j.GetString("actor") + origin := keymatch(keyname, who) + if origin == "" { + ilog.Printf("keyname actor mismatch: %s <> %s", keyname, who) + return + } + if rejectactor(user.ID, who) { + return + } + re_ont := regexp.MustCompile("https://" + serverName + "/o/([\\pL[:digit:]]+)") + what, _ := j.GetString("type") + dlog.Printf("server got a %s", what) + switch what { + case "Follow": + obj, _ := j.GetString("object") + if obj == user.URL { + ilog.Printf("can't follow the server!") + return + } + m := re_ont.FindStringSubmatch(obj) + if len(m) != 2 { + ilog.Printf("not sure how to handle this") + return + } + ont := "#" + m[1] + + followme(user, who, ont, j) + case "Undo": + obj, ok := j.GetMap("object") + if !ok { + ilog.Printf("unknown undo no object") + return + } + what, _ := obj.GetString("type") + if what != "Follow" { + ilog.Printf("unknown undo: %s", what) + return + } + targ, _ := obj.GetString("object") + m := re_ont.FindStringSubmatch(targ) + if len(m) != 2 { + ilog.Printf("not sure how to handle this") + return + } + ont := "#" + m[1] + unfollowme(user, who, ont, j) + default: + ilog.Printf("unhandled server activity: %s", what) + dumpactivity(j) + } +} + +func serveractor(w http.ResponseWriter, r *http.Request) { + user := getserveruser() + if stealthmode(user.ID, r) { + http.NotFound(w, r) + return + } + j := junkuser(user) + j.Write(w) +} + +func ximport(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + xid := strings.TrimSpace(r.FormValue("xid")) + xonk := getxonk(u.UserID, xid) + if xonk == nil { + p, _ := investigate(xid) + if p != nil { + xid = p.XID + } + j, err := GetJunk(u.UserID, xid) + if err != nil { + http.Error(w, "error getting external object", http.StatusInternalServerError) + ilog.Printf("error getting external object: %s", err) + return + } + allinjest(originate(xid), j) + dlog.Printf("importing %s", xid) + user, _ := butwhatabout(u.Username) + + info, _ := somethingabout(j) + if info == nil { + xonk = xonksaver(user, j, originate(xid)) + } else if info.What == SomeActor { + outbox, _ := j.GetString("outbox") + gimmexonks(user, outbox) + http.Redirect(w, r, "/h?xid="+url.QueryEscape(xid), http.StatusSeeOther) + return + } else if info.What == SomeCollection { + gimmexonks(user, xid) + http.Redirect(w, r, "/xzone", http.StatusSeeOther) + return + } + } + convoy := "" + if xonk != nil { + convoy = xonk.Convoy + } + http.Redirect(w, r, "/t?c="+url.QueryEscape(convoy), http.StatusSeeOther) +} + +func xzone(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + rows, err := stmtRecentHonkers.Query(u.UserID, u.UserID) + if err != nil { + elog.Printf("query err: %s", err) + return + } + defer rows.Close() + var honkers []Honker + for rows.Next() { + var xid string + rows.Scan(&xid) + honkers = append(honkers, Honker{XID: xid}) + } + rows.Close() + for i, _ := range honkers { + _, honkers[i].Handle = handles(honkers[i].XID) + } + templinfo := getInfo(r) + templinfo["XCSRF"] = login.GetCSRF("ximport", r) + templinfo["Honkers"] = honkers + err = readviews.Execute(w, "xzone.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +var oldoutbox = cache.New(cache.Options{Filler: func(name string) ([]byte, bool) { + user, err := butwhatabout(name) + if err != nil { + return nil, false + } + honks := gethonksbyuser(name, false, 0) + if len(honks) > 20 { + honks = honks[0:20] + } + + var jonks []junk.Junk + for _, h := range honks { + j, _ := jonkjonk(user, h) + jonks = append(jonks, j) + } + + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = user.URL + "/outbox" + j["attributedTo"] = user.URL + j["type"] = "OrderedCollection" + j["totalItems"] = len(jonks) + j["orderedItems"] = jonks + + return j.ToBytes(), true +}, Duration: 1 * time.Minute}) + +func outbox(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + user, err := butwhatabout(name) + if err != nil { + http.NotFound(w, r) + return + } + if stealthmode(user.ID, r) { + http.NotFound(w, r) + return + } + var j []byte + ok := oldoutbox.Get(name, &j) + if ok { + w.Header().Set("Content-Type", theonetruename) + w.Write(j) + } else { + http.NotFound(w, r) + } +} + +var oldempties = cache.New(cache.Options{Filler: func(url string) ([]byte, bool) { + colname := "/followers" + if strings.HasSuffix(url, "/following") { + colname = "/following" + } + user := fmt.Sprintf("https://%s%s", serverName, url[:len(url)-10]) + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = user + colname + j["attributedTo"] = user + j["type"] = "OrderedCollection" + j["totalItems"] = 0 + j["orderedItems"] = []junk.Junk{} + + return j.ToBytes(), true +}}) + +func emptiness(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + user, err := butwhatabout(name) + if err != nil { + http.NotFound(w, r) + return + } + if stealthmode(user.ID, r) { + http.NotFound(w, r) + return + } + var j []byte + ok := oldempties.Get(r.URL.Path, &j) + if ok { + w.Header().Set("Content-Type", theonetruename) + w.Write(j) + } else { + http.NotFound(w, r) + } +} + +func showuser(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + user, err := butwhatabout(name) + if err != nil { + ilog.Printf("user not found %s: %s", name, err) + http.NotFound(w, r) + return + } + if stealthmode(user.ID, r) { + http.NotFound(w, r) + return + } + if friendorfoe(r.Header.Get("Accept")) { + j, ok := asjonker(name) + if ok { + w.Header().Set("Content-Type", theonetruename) + w.Write(j) + } else { + http.NotFound(w, r) + } + return + } + u := login.GetUserInfo(r) + honks := gethonksbyuser(name, u != nil && u.Username == name, 0) + templinfo := getInfo(r) + templinfo["PageName"] = "user" + templinfo["PageArg"] = name + templinfo["Name"] = user.Name + templinfo["WhatAbout"] = user.HTAbout + templinfo["ServerMessage"] = "" + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + honkpage(w, u, honks, templinfo) +} + +func showhonker(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + name := mux.Vars(r)["name"] + var honks []*Honk + if name == "" { + name = r.FormValue("xid") + honks = gethonksbyxonker(u.UserID, name, 0) + } else { + honks = gethonksbyhonker(u.UserID, name, 0) + } + miniform := templates.Sprintf(`<form action="/submithonker" method="POST"> +<input type="hidden" name="CSRF" value="%s"> +<input type="hidden" name="url" value="%s"> +<button tabindex=1 name="add honker" value="add honker">add honker</button> +</form>`, login.GetCSRF("submithonker", r), name) + msg := templates.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>%s`, name, name, miniform) + templinfo := getInfo(r) + templinfo["PageName"] = "honker" + templinfo["PageArg"] = name + templinfo["ServerMessage"] = msg + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + honkpage(w, u, honks, templinfo) +} + +func showcombo(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + u := login.GetUserInfo(r) + honks := gethonksbycombo(u.UserID, name, 0) + honks = osmosis(honks, u.UserID, true) + templinfo := getInfo(r) + templinfo["PageName"] = "combo" + templinfo["PageArg"] = name + templinfo["ServerMessage"] = "honks by combo: " + name + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + honkpage(w, u, honks, templinfo) +} +func showconvoy(w http.ResponseWriter, r *http.Request) { + c := r.FormValue("c") + u := login.GetUserInfo(r) + honks := gethonksbyconvoy(u.UserID, c, 0) + templinfo := getInfo(r) + if len(honks) > 0 { + templinfo["TopHID"] = honks[0].ID + } + honks = osmosis(honks, u.UserID, false) + reversehonks(honks) + templinfo["PageName"] = "convoy" + templinfo["PageArg"] = c + templinfo["ServerMessage"] = "honks in convoy: " + c + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + honkpage(w, u, honks, templinfo) +} +func showsearch(w http.ResponseWriter, r *http.Request) { + q := r.FormValue("q") + u := login.GetUserInfo(r) + honks := gethonksbysearch(u.UserID, q, 0) + templinfo := getInfo(r) + templinfo["PageName"] = "search" + templinfo["PageArg"] = q + templinfo["ServerMessage"] = "honks for search: " + q + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + honkpage(w, u, honks, templinfo) +} +func showontology(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + u := login.GetUserInfo(r) + var userid int64 = -1 + if u != nil { + userid = u.UserID + } + honks := gethonksbyontology(userid, "#"+name, 0) + if friendorfoe(r.Header.Get("Accept")) { + if len(honks) > 40 { + honks = honks[0:40] + } + + var xids []string + for _, h := range honks { + xids = append(xids, h.XID) + } + + user := getserveruser() + + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = fmt.Sprintf("https://%s/o/%s", serverName, name) + j["name"] = "#" + name + j["attributedTo"] = user.URL + j["type"] = "OrderedCollection" + j["totalItems"] = len(xids) + j["orderedItems"] = xids + + j.Write(w) + return + } + + templinfo := getInfo(r) + templinfo["ServerMessage"] = "honks by ontology: " + name + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + honkpage(w, u, honks, templinfo) +} + +type Ont struct { + Name string + Count int64 +} + +func thelistingoftheontologies(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + var userid int64 = -1 + if u != nil { + userid = u.UserID + } + rows, err := stmtAllOnts.Query(userid) + if err != nil { + elog.Printf("selection error: %s", err) + return + } + defer rows.Close() + var onts []Ont + for rows.Next() { + var o Ont + err := rows.Scan(&o.Name, &o.Count) + if err != nil { + elog.Printf("error scanning ont: %s", err) + continue + } + if utf8.RuneCountInString(o.Name) > 24 { + continue + } + o.Name = o.Name[1:] + onts = append(onts, o) + } + sort.Slice(onts, func(i, j int) bool { + return onts[i].Name < onts[j].Name + }) + if u == nil && !develMode { + w.Header().Set("Cache-Control", "max-age=300") + } + templinfo := getInfo(r) + templinfo["Onts"] = onts + templinfo["FirstRune"] = func(s string) rune { r, _ := utf8.DecodeRuneInString(s); return r } + err = readviews.Execute(w, "onts.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +type Track struct { + xid string + who string +} + +func getbacktracks(xid string) []string { + c := make(chan bool) + dumptracks <- c + <-c + row := stmtGetTracks.QueryRow(xid) + var rawtracks string + err := row.Scan(&rawtracks) + if err != nil { + if err != sql.ErrNoRows { + elog.Printf("error scanning tracks: %s", err) + } + return nil + } + var rcpts []string + for _, f := range strings.Split(rawtracks, " ") { + idx := strings.LastIndexByte(f, '#') + if idx != -1 { + f = f[:idx] + } + if !strings.HasPrefix(f, "https://") { + f = fmt.Sprintf("%%https://%s/inbox", f) + } + rcpts = append(rcpts, f) + } + return rcpts +} + +func savetracks(tracks map[string][]string) { + db := opendatabase() + tx, err := db.Begin() + if err != nil { + elog.Printf("savetracks begin error: %s", err) + return + } + defer func() { + err := tx.Commit() + if err != nil { + elog.Printf("savetracks commit error: %s", err) + } + + }() + stmtGetTracks, err := tx.Prepare("select fetches from tracks where xid = ?") + if err != nil { + elog.Printf("savetracks error: %s", err) + return + } + stmtNewTracks, err := tx.Prepare("insert into tracks (xid, fetches) values (?, ?)") + if err != nil { + elog.Printf("savetracks error: %s", err) + return + } + stmtUpdateTracks, err := tx.Prepare("update tracks set fetches = ? where xid = ?") + if err != nil { + elog.Printf("savetracks error: %s", err) + return + } + count := 0 + for xid, f := range tracks { + count += len(f) + var prev string + row := stmtGetTracks.QueryRow(xid) + err := row.Scan(&prev) + if err == sql.ErrNoRows { + f = oneofakind(f) + stmtNewTracks.Exec(xid, strings.Join(f, " ")) + } else if err == nil { + all := append(strings.Split(prev, " "), f...) + all = oneofakind(all) + stmtUpdateTracks.Exec(strings.Join(all, " ")) + } else { + elog.Printf("savetracks error: %s", err) + } + } + dlog.Printf("saved %d new fetches", count) +} + +var trackchan = make(chan Track) +var dumptracks = make(chan chan bool) + +func tracker() { + timeout := 4 * time.Minute + sleeper := time.NewTimer(timeout) + tracks := make(map[string][]string) + workinprogress++ + for { + select { + case track := <-trackchan: + tracks[track.xid] = append(tracks[track.xid], track.who) + case <-sleeper.C: + if len(tracks) > 0 { + go savetracks(tracks) + tracks = make(map[string][]string) + } + sleeper.Reset(timeout) + case c := <-dumptracks: + if len(tracks) > 0 { + savetracks(tracks) + } + c <- true + case <-endoftheworld: + if len(tracks) > 0 { + savetracks(tracks) + } + readyalready <- true + return + } + } +} + +var re_keyholder = regexp.MustCompile(`keyId="([^"]+)"`) + +func trackback(xid string, r *http.Request) { + agent := r.UserAgent() + who := originate(agent) + sig := r.Header.Get("Signature") + if sig != "" { + m := re_keyholder.FindStringSubmatch(sig) + if len(m) == 2 { + who = m[1] + } + } + if who != "" { + trackchan <- Track{xid: xid, who: who} + } +} + +func showonehonk(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + user, err := butwhatabout(name) + if err != nil { + http.NotFound(w, r) + return + } + if stealthmode(user.ID, r) { + http.NotFound(w, r) + return + } + xid := fmt.Sprintf("https://%s%s", serverName, r.URL.Path) + + if friendorfoe(r.Header.Get("Accept")) { + j, ok := gimmejonk(xid) + if ok { + trackback(xid, r) + w.Header().Set("Content-Type", theonetruename) + w.Write(j) + } else { + http.NotFound(w, r) + } + return + } + honk := getxonk(user.ID, xid) + if honk == nil { + http.NotFound(w, r) + return + } + u := login.GetUserInfo(r) + if u != nil && u.UserID != user.ID { + u = nil + } + if !honk.Public { + if u == nil { + http.NotFound(w, r) + return + + } + honks := []*Honk{honk} + donksforhonks(honks) + templinfo := getInfo(r) + templinfo["ServerMessage"] = "one honk maybe more" + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + honkpage(w, u, honks, templinfo) + return + } + rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0) + reversehonks(rawhonks) + var honks []*Honk + for _, h := range rawhonks { + if h.XID == xid && len(honks) != 0 { + h.Style += " glow" + } + if h.Public && (h.Whofore == 2 || h.IsAcked()) { + honks = append(honks, h) + } + } + + templinfo := getInfo(r) + templinfo["ServerMessage"] = "one honk maybe more" + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + honkpage(w, u, honks, templinfo) +} + +func honkpage(w http.ResponseWriter, u *login.UserInfo, honks []*Honk, templinfo map[string]interface{}) { + var userid int64 = -1 + if u != nil { + userid = u.UserID + templinfo["User"], _ = butwhatabout(u.Username) + } + reverbolate(userid, honks) + templinfo["Honks"] = honks + templinfo["MapLink"] = getmaplink(u) + if templinfo["TopHID"] == nil { + if len(honks) > 0 { + templinfo["TopHID"] = honks[0].ID + } else { + templinfo["TopHID"] = 0 + } + } + if u == nil && !develMode { + w.Header().Set("Cache-Control", "max-age=60") + } + err := readviews.Execute(w, "honkpage.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +func saveuser(w http.ResponseWriter, r *http.Request) { + whatabout := r.FormValue("whatabout") + whatabout = strings.Replace(whatabout, "\r", "", -1) + u := login.GetUserInfo(r) + user, _ := butwhatabout(u.Username) + db := opendatabase() + + options := user.Options + if r.FormValue("skinny") == "skinny" { + options.SkinnyCSS = true + } else { + options.SkinnyCSS = false + } + if r.FormValue("avahex") == "avahex" { + options.Avahex = true + } else { + options.Avahex = false + } + if r.FormValue("omitimages") == "omitimages" { + options.OmitImages = true + } else { + options.OmitImages = false + } + if r.FormValue("mentionall") == "mentionall" { + options.MentionAll = true + } else { + options.MentionAll = false + } + if r.FormValue("maps") == "apple" { + options.MapLink = "apple" + } else { + options.MapLink = "" + } + options.Reaction = r.FormValue("reaction") + + sendupdate := false + ava := re_avatar.FindString(whatabout) + if ava != "" { + whatabout = re_avatar.ReplaceAllString(whatabout, "") + ava = ava[7:] + if ava[0] == ' ' { + ava = ava[1:] + } + ava = fmt.Sprintf("https://%s/meme/%s", serverName, ava) + } + if ava != options.Avatar { + options.Avatar = ava + sendupdate = true + } + ban := re_banner.FindString(whatabout) + if ban != "" { + whatabout = re_banner.ReplaceAllString(whatabout, "") + ban = ban[7:] + if ban[0] == ' ' { + ban = ban[1:] + } + ban = fmt.Sprintf("https://%s/meme/%s", serverName, ban) + } + if ban != options.Banner { + options.Banner = ban + sendupdate = true + } + whatabout = strings.TrimSpace(whatabout) + if whatabout != user.About { + sendupdate = true + } + j, err := jsonify(options) + if err == nil { + _, err = db.Exec("update users set about = ?, options = ? where username = ?", whatabout, j, u.Username) + } + if err != nil { + elog.Printf("error bouting what: %s", err) + } + somenamedusers.Clear(u.Username) + somenumberedusers.Clear(u.UserID) + oldjonkers.Clear(u.Username) + + if sendupdate { + updateMe(u.Username) + } + + http.Redirect(w, r, "/account", http.StatusSeeOther) +} + +func bonkit(xid string, user *WhatAbout) { + dlog.Printf("bonking %s", xid) + + xonk := getxonk(user.ID, xid) + if xonk == nil { + return + } + if !xonk.Public { + return + } + if xonk.IsBonked() { + return + } + donksforhonks([]*Honk{xonk}) + + _, err := stmtUpdateFlags.Exec(flagIsBonked, xonk.ID) + if err != nil { + elog.Printf("error acking bonk: %s", err) + } + + oonker := xonk.Oonker + if oonker == "" { + oonker = xonk.Honker + } + dt := time.Now().UTC() + bonk := &Honk{ + UserID: user.ID, + Username: user.Name, + What: "bonk", + Honker: user.URL, + Oonker: oonker, + XID: xonk.XID, + RID: xonk.RID, + Noise: xonk.Noise, + Precis: xonk.Precis, + URL: xonk.URL, + Date: dt, + Donks: xonk.Donks, + Whofore: 2, + Convoy: xonk.Convoy, + Audience: []string{thewholeworld, oonker}, + Public: true, + Format: xonk.Format, + Place: xonk.Place, + Onts: xonk.Onts, + Time: xonk.Time, + } + + err = savehonk(bonk) + if err != nil { + elog.Printf("uh oh") + return + } + + go honkworldwide(user, bonk) +} + +func submitbonk(w http.ResponseWriter, r *http.Request) { + xid := r.FormValue("xid") + userinfo := login.GetUserInfo(r) + user, _ := butwhatabout(userinfo.Username) + + bonkit(xid, user) + + if r.FormValue("js") != "1" { + templinfo := getInfo(r) + templinfo["ServerMessage"] = "Bonked!" + err := readviews.Execute(w, "msg.html", templinfo) + if err != nil { + elog.Print(err) + } + } +} + +func sendzonkofsorts(xonk *Honk, user *WhatAbout, what string, aux string) { + zonk := &Honk{ + What: what, + XID: xonk.XID, + Date: time.Now().UTC(), + Audience: oneofakind(xonk.Audience), + Noise: aux, + } + zonk.Public = loudandproud(zonk.Audience) + + dlog.Printf("announcing %sed honk: %s", what, xonk.XID) + go honkworldwide(user, zonk) +} + +func zonkit(w http.ResponseWriter, r *http.Request) { + wherefore := r.FormValue("wherefore") + what := r.FormValue("what") + userinfo := login.GetUserInfo(r) + user, _ := butwhatabout(userinfo.Username) + + if wherefore == "save" { + xonk := getxonk(userinfo.UserID, what) + if xonk != nil { + _, err := stmtUpdateFlags.Exec(flagIsSaved, xonk.ID) + if err != nil { + elog.Printf("error saving: %s", err) + } + } + return + } + + if wherefore == "unsave" { + xonk := getxonk(userinfo.UserID, what) + if xonk != nil { + _, err := stmtClearFlags.Exec(flagIsSaved, xonk.ID) + if err != nil { + elog.Printf("error unsaving: %s", err) + } + } + return + } + + if wherefore == "react" { + reaction := user.Options.Reaction + if r2 := r.FormValue("reaction"); r2 != "" { + reaction = r2 + } + if reaction == "none" { + return + } + xonk := getxonk(userinfo.UserID, what) + if xonk != nil { + _, err := stmtUpdateFlags.Exec(flagIsReacted, xonk.ID) + if err != nil { + elog.Printf("error saving: %s", err) + } + sendzonkofsorts(xonk, user, "react", reaction) + } + return + } + + if wherefore == "wonk" { + xonk := getxonk(userinfo.UserID, what) + if xonk != nil { + _, err := stmtUpdateFlags.Exec(flagIsWonked, xonk.ID) + if err == nil { + guesses := r.FormValue("guesses") + _, err = stmtSaveMeta.Exec(xonk.ID, "guesses", guesses) + } + if err != nil { + elog.Printf("error saving: %s", err) + } + } + return + } + + // my hammer is too big, oh well + defer oldjonks.Flush() + + if wherefore == "ack" { + xonk := getxonk(userinfo.UserID, what) + if xonk != nil && !xonk.IsAcked() { + _, err := stmtUpdateFlags.Exec(flagIsAcked, xonk.ID) + if err != nil { + elog.Printf("error acking: %s", err) + } + sendzonkofsorts(xonk, user, "ack", "") + } + return + } + + if wherefore == "deack" { + xonk := getxonk(userinfo.UserID, what) + if xonk != nil && xonk.IsAcked() { + _, err := stmtClearFlags.Exec(flagIsAcked, xonk.ID) + if err != nil { + elog.Printf("error deacking: %s", err) + } + sendzonkofsorts(xonk, user, "deack", "") + } + return + } + + if wherefore == "bonk" { + user, _ := butwhatabout(userinfo.Username) + bonkit(what, user) + return + } + + if wherefore == "unbonk" { + xonk := getbonk(userinfo.UserID, what) + if xonk != nil { + deletehonk(xonk.ID) + xonk = getxonk(userinfo.UserID, what) + _, err := stmtClearFlags.Exec(flagIsBonked, xonk.ID) + if err != nil { + elog.Printf("error unbonking: %s", err) + } + sendzonkofsorts(xonk, user, "unbonk", "") + } + return + } + + if wherefore == "untag" { + xonk := getxonk(userinfo.UserID, what) + if xonk != nil { + _, err := stmtUpdateFlags.Exec(flagIsUntagged, xonk.ID) + if err != nil { + elog.Printf("error untagging: %s", err) + } + } + var badparents map[string]bool + untagged.GetAndLock(userinfo.UserID, &badparents) + badparents[what] = true + untagged.Unlock() + return + } + + ilog.Printf("zonking %s %s", wherefore, what) + if wherefore == "zonk" { + xonk := getxonk(userinfo.UserID, what) + if xonk != nil { + deletehonk(xonk.ID) + if xonk.Whofore == 2 || xonk.Whofore == 3 { + sendzonkofsorts(xonk, user, "zonk", "") + } + } + } + _, err := stmtSaveZonker.Exec(userinfo.UserID, what, wherefore) + if err != nil { + elog.Printf("error saving zonker: %s", err) + return + } +} + +func edithonkpage(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + user, _ := butwhatabout(u.Username) + xid := r.FormValue("xid") + honk := getxonk(u.UserID, xid) + if !canedithonk(user, honk) { + http.Error(w, "no editing that please", http.StatusInternalServerError) + return + } + + noise := honk.Noise + + honks := []*Honk{honk} + donksforhonks(honks) + reverbolate(u.UserID, honks) + templinfo := getInfo(r) + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + templinfo["Honks"] = honks + templinfo["MapLink"] = getmaplink(u) + templinfo["Noise"] = noise + templinfo["SavedPlace"] = honk.Place + if tm := honk.Time; tm != nil { + templinfo["ShowTime"] = ";" + templinfo["StartTime"] = tm.StartTime.Format("2006-01-02 15:04") + if tm.Duration != 0 { + templinfo["Duration"] = tm.Duration + } + } + templinfo["ServerMessage"] = "honk edit 2" + templinfo["IsPreview"] = true + templinfo["UpdateXID"] = honk.XID + if len(honk.Donks) > 0 { + templinfo["SavedFile"] = honk.Donks[0].XID + } + err := readviews.Execute(w, "honkpage.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +func newhonkpage(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + rid := r.FormValue("rid") + noise := "" + + xonk := getxonk(u.UserID, rid) + if xonk != nil { + _, replto := handles(xonk.Honker) + if replto != "" { + noise = "@" + replto + " " + } + } + + templinfo := getInfo(r) + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + templinfo["InReplyTo"] = rid + templinfo["Noise"] = noise + templinfo["ServerMessage"] = "compose honk" + templinfo["IsPreview"] = true + err := readviews.Execute(w, "honkpage.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +func canedithonk(user *WhatAbout, honk *Honk) bool { + if honk == nil || honk.Honker != user.URL || honk.What == "bonk" { + return false + } + return true +} + +func submitdonk(w http.ResponseWriter, r *http.Request) (*Donk, error) { + if !strings.HasPrefix(strings.ToLower(r.Header.Get("Content-Type")), "multipart/form-data") { + return nil, nil + } + file, filehdr, err := r.FormFile("donk") + if err != nil { + if err == http.ErrMissingFile { + return nil, nil + } + elog.Printf("error reading donk: %s", err) + http.Error(w, "error reading donk", http.StatusUnsupportedMediaType) + return nil, err + } + var buf bytes.Buffer + io.Copy(&buf, file) + file.Close() + data := buf.Bytes() + var media, name string + img, err := shrinkit(data) + if err == nil { + data = img.Data + format := img.Format + media = "image/" + format + if format == "jpeg" { + format = "jpg" + } + name = xfiltrate() + "." + format + } else { + ct := http.DetectContentType(data) + switch ct { + case "application/pdf": + maxsize := 10000000 + if len(data) > maxsize { + ilog.Printf("bad image: %s too much pdf: %d", err, len(data)) + http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType) + return nil, err + } + media = ct + name = filehdr.Filename + if name == "" { + name = xfiltrate() + ".pdf" + } + default: + maxsize := 100000 + if len(data) > maxsize { + ilog.Printf("bad image: %s too much text: %d", err, len(data)) + http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType) + return nil, err + } + for i := 0; i < len(data); i++ { + if data[i] < 32 && data[i] != '\t' && data[i] != '\r' && data[i] != '\n' { + ilog.Printf("bad image: %s not text: %d", err, data[i]) + http.Error(w, "didn't like your attachment", http.StatusUnsupportedMediaType) + return nil, err + } + } + media = "text/plain" + name = filehdr.Filename + if name == "" { + name = xfiltrate() + ".txt" + } + } + } + desc := strings.TrimSpace(r.FormValue("donkdesc")) + if desc == "" { + desc = name + } + fileid, xid, err := savefileandxid(name, desc, "", media, true, data) + if err != nil { + elog.Printf("unable to save image: %s", err) + http.Error(w, "failed to save attachment", http.StatusUnsupportedMediaType) + return nil, err + } + d := &Donk{ + FileID: fileid, + XID: xid, + Desc: desc, + Local: true, + } + return d, nil +} + +func submitwebhonk(w http.ResponseWriter, r *http.Request) { + h := submithonk(w, r) + if h == nil { + return + } + http.Redirect(w, r, h.XID[len(serverName)+8:], http.StatusSeeOther) +} + +// what a hot mess this function is +func submithonk(w http.ResponseWriter, r *http.Request) *Honk { + rid := r.FormValue("rid") + noise := r.FormValue("noise") + format := r.FormValue("format") + if format == "" { + format = "markdown" + } + if !(format == "markdown" || format == "html") { + http.Error(w, "unknown format", 500) + return nil + } + + userinfo := login.GetUserInfo(r) + user, _ := butwhatabout(userinfo.Username) + + dt := time.Now().UTC() + updatexid := r.FormValue("updatexid") + var honk *Honk + if updatexid != "" { + honk = getxonk(userinfo.UserID, updatexid) + if !canedithonk(user, honk) { + http.Error(w, "no editing that please", http.StatusInternalServerError) + return nil + } + honk.Date = dt + honk.What = "update" + honk.Format = format + } else { + xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, xfiltrate()) + what := "honk" + if rid != "" { + what = "tonk" + } + wonkles := r.FormValue("wonkles") + if wonkles != "" { + what = "wonk" + } + honk = &Honk{ + UserID: userinfo.UserID, + Username: userinfo.Username, + What: what, + Honker: user.URL, + XID: xid, + Date: dt, + Format: format, + Wonkles: wonkles, + } + } + + noise = strings.Replace(noise, "\r", "", -1) + noise = quickrename(noise, userinfo.UserID) + noise = hooterize(noise) + honk.Noise = noise + translate(honk) + + var convoy string + if rid != "" { + xonk := getxonk(userinfo.UserID, rid) + if xonk == nil { + http.Error(w, "replyto disappeared", http.StatusNotFound) + return nil + } + if xonk.Public { + honk.Audience = append(honk.Audience, xonk.Audience...) + } + convoy = xonk.Convoy + for i, a := range honk.Audience { + if a == thewholeworld { + honk.Audience[0], honk.Audience[i] = honk.Audience[i], honk.Audience[0] + break + } + } + honk.RID = rid + if xonk.Precis != "" && honk.Precis == "" { + honk.Precis = xonk.Precis + if !(strings.HasPrefix(honk.Precis, "DZ:") || strings.HasPrefix(honk.Precis, "re: re: re: ")) { + honk.Precis = "re: " + honk.Precis + } + } + } else { + honk.Audience = []string{thewholeworld} + } + if honk.Noise != "" && honk.Noise[0] == '@' { + honk.Audience = append(grapevine(honk.Mentions), honk.Audience...) + } else { + honk.Audience = append(honk.Audience, grapevine(honk.Mentions)...) + } + + if convoy == "" { + convoy = "data:,electrichonkytonk-" + xfiltrate() + } + butnottooloud(honk.Audience) + honk.Audience = oneofakind(honk.Audience) + if len(honk.Audience) == 0 { + ilog.Printf("honk to nowhere") + http.Error(w, "honk to nowhere...", http.StatusNotFound) + return nil + } + honk.Public = loudandproud(honk.Audience) + honk.Convoy = convoy + + donkxid := r.FormValue("donkxid") + if donkxid == "" { + d, err := submitdonk(w, r) + if err != nil && err != http.ErrMissingFile { + return nil + } + if d != nil { + honk.Donks = append(honk.Donks, d) + donkxid = d.XID + } + } else { + xid := donkxid + url := fmt.Sprintf("https://%s/d/%s", serverName, xid) + donk := finddonk(url) + if donk != nil { + honk.Donks = append(honk.Donks, donk) + } else { + ilog.Printf("can't find file: %s", xid) + } + } + memetize(honk) + imaginate(honk) + + placename := strings.TrimSpace(r.FormValue("placename")) + placelat := strings.TrimSpace(r.FormValue("placelat")) + placelong := strings.TrimSpace(r.FormValue("placelong")) + placeurl := strings.TrimSpace(r.FormValue("placeurl")) + if placename != "" || placelat != "" || placelong != "" || placeurl != "" { + p := new(Place) + p.Name = placename + p.Latitude, _ = strconv.ParseFloat(placelat, 64) + p.Longitude, _ = strconv.ParseFloat(placelong, 64) + p.Url = placeurl + honk.Place = p + } + timestart := strings.TrimSpace(r.FormValue("timestart")) + if timestart != "" { + t := new(Time) + now := time.Now().Local() + for _, layout := range []string{"2006-01-02 3:04pm", "2006-01-02 15:04", "3:04pm", "15:04"} { + start, err := time.ParseInLocation(layout, timestart, now.Location()) + if err == nil { + if start.Year() == 0 { + start = time.Date(now.Year(), now.Month(), now.Day(), start.Hour(), start.Minute(), 0, 0, now.Location()) + } + t.StartTime = start + break + } + } + timeend := r.FormValue("timeend") + dur := parseDuration(timeend) + if dur != 0 { + t.Duration = Duration(dur) + } + if !t.StartTime.IsZero() { + honk.What = "event" + honk.Time = t + } + } + + if honk.Public { + honk.Whofore = 2 + } else { + honk.Whofore = 3 + } + + // back to markdown + honk.Noise = noise + + if r.FormValue("preview") == "preview" { + honks := []*Honk{honk} + reverbolate(userinfo.UserID, honks) + templinfo := getInfo(r) + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + templinfo["Honks"] = honks + templinfo["MapLink"] = getmaplink(userinfo) + templinfo["InReplyTo"] = r.FormValue("rid") + templinfo["Noise"] = r.FormValue("noise") + templinfo["SavedFile"] = donkxid + if tm := honk.Time; tm != nil { + templinfo["ShowTime"] = ";" + templinfo["StartTime"] = tm.StartTime.Format("2006-01-02 15:04") + if tm.Duration != 0 { + templinfo["Duration"] = tm.Duration + } + } + templinfo["IsPreview"] = true + templinfo["UpdateXID"] = updatexid + templinfo["ServerMessage"] = "honk preview" + err := readviews.Execute(w, "honkpage.html", templinfo) + if err != nil { + elog.Print(err) + } + return nil + } + + if updatexid != "" { + updatehonk(honk) + oldjonks.Clear(honk.XID) + } else { + err := savehonk(honk) + if err != nil { + elog.Printf("uh oh") + return nil + } + } + + // reload for consistency + honk.Donks = nil + donksforhonks([]*Honk{honk}) + + go honkworldwide(user, honk) + + return honk +} + +func showhonkers(w http.ResponseWriter, r *http.Request) { + userinfo := login.GetUserInfo(r) + templinfo := getInfo(r) + templinfo["Honkers"] = gethonkers(userinfo.UserID) + templinfo["HonkerCSRF"] = login.GetCSRF("submithonker", r) + err := readviews.Execute(w, "honkers.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +func showchatter(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + chatnewnone(u.UserID) + chatter := loadchatter(u.UserID) + for _, chat := range chatter { + for _, ch := range chat.Chonks { + filterchonk(ch) + } + } + + templinfo := getInfo(r) + templinfo["Chatter"] = chatter + templinfo["ChonkCSRF"] = login.GetCSRF("sendchonk", r) + err := readviews.Execute(w, "chatter.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +func submitchonk(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + user, _ := butwhatabout(u.Username) + noise := r.FormValue("noise") + target := r.FormValue("target") + format := "markdown" + dt := time.Now().UTC() + xid := fmt.Sprintf("%s/%s/%s", user.URL, "chonk", xfiltrate()) + + if !strings.HasPrefix(target, "https://") { + target = fullname(target, u.UserID) + } + if target == "" { + http.Error(w, "who is that?", http.StatusInternalServerError) + return + } + ch := Chonk{ + UserID: u.UserID, + XID: xid, + Who: user.URL, + Target: target, + Date: dt, + Noise: noise, + Format: format, + } + d, err := submitdonk(w, r) + if err != nil && err != http.ErrMissingFile { + return + } + if d != nil { + ch.Donks = append(ch.Donks, d) + } + + translatechonk(&ch) + savechonk(&ch) + // reload for consistency + ch.Donks = nil + donksforchonks([]*Chonk{&ch}) + go sendchonk(user, &ch) + + http.Redirect(w, r, "/chatter", http.StatusSeeOther) +} + +var combocache = cache.New(cache.Options{Filler: func(userid int64) ([]string, bool) { + honkers := gethonkers(userid) + var combos []string + for _, h := range honkers { + combos = append(combos, h.Combos...) + } + for i, c := range combos { + if c == "-" { + combos[i] = "" + } + } + combos = oneofakind(combos) + sort.Strings(combos) + return combos, true +}, Invalidator: &honkerinvalidator}) + +func showcombos(w http.ResponseWriter, r *http.Request) { + userinfo := login.GetUserInfo(r) + var combos []string + combocache.Get(userinfo.UserID, &combos) + templinfo := getInfo(r) + err := readviews.Execute(w, "combos.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +func submithonker(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + user, _ := butwhatabout(u.Username) + name := strings.TrimSpace(r.FormValue("name")) + url := strings.TrimSpace(r.FormValue("url")) + peep := r.FormValue("peep") + combos := strings.TrimSpace(r.FormValue("combos")) + combos = " " + combos + " " + honkerid, _ := strconv.ParseInt(r.FormValue("honkerid"), 10, 0) + + re_namecheck := regexp.MustCompile("[\\pL[:digit:]_.-]+") + if name != "" && !re_namecheck.MatchString(name) { + http.Error(w, "please use a plainer name", http.StatusInternalServerError) + return + } + + var meta HonkerMeta + meta.Notes = strings.TrimSpace(r.FormValue("notes")) + mj, _ := jsonify(&meta) + + defer honkerinvalidator.Clear(u.UserID) + + if honkerid > 0 { + if r.FormValue("delete") == "delete" { + unfollowyou(user, honkerid) + stmtDeleteHonker.Exec(honkerid) + http.Redirect(w, r, "/honkers", http.StatusSeeOther) + return + } + if r.FormValue("unsub") == "unsub" { + unfollowyou(user, honkerid) + } + if r.FormValue("sub") == "sub" { + followyou(user, honkerid) + } + _, err := stmtUpdateHonker.Exec(name, combos, mj, honkerid, u.UserID) + if err != nil { + elog.Printf("update honker err: %s", err) + return + } + http.Redirect(w, r, "/honkers", http.StatusSeeOther) + return + } + + if url == "" { + http.Error(w, "subscribing to nothing?", http.StatusInternalServerError) + return + } + + flavor := "presub" + if peep == "peep" { + flavor = "peep" + } + + err := savehonker(user, url, name, flavor, combos, mj) + if err != nil { + http.Error(w, "had some trouble with that: "+err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/honkers", http.StatusSeeOther) +} + +func hfcspage(w http.ResponseWriter, r *http.Request) { + userinfo := login.GetUserInfo(r) + + filters := getfilters(userinfo.UserID, filtAny) + + templinfo := getInfo(r) + templinfo["Filters"] = filters + templinfo["FilterCSRF"] = login.GetCSRF("filter", r) + err := readviews.Execute(w, "hfcs.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +func savehfcs(w http.ResponseWriter, r *http.Request) { + userinfo := login.GetUserInfo(r) + itsok := r.FormValue("itsok") + if itsok == "iforgiveyou" { + hfcsid, _ := strconv.ParseInt(r.FormValue("hfcsid"), 10, 0) + _, err := stmtDeleteFilter.Exec(userinfo.UserID, hfcsid) + if err != nil { + elog.Printf("error deleting filter: %s", err) + } + filtInvalidator.Clear(userinfo.UserID) + http.Redirect(w, r, "/hfcs", http.StatusSeeOther) + return + } + + filt := new(Filter) + filt.Name = strings.TrimSpace(r.FormValue("name")) + filt.Date = time.Now().UTC() + filt.Actor = strings.TrimSpace(r.FormValue("actor")) + filt.IncludeAudience = r.FormValue("incaud") == "yes" + filt.Text = strings.TrimSpace(r.FormValue("filttext")) + filt.IsAnnounce = r.FormValue("isannounce") == "yes" + filt.AnnounceOf = strings.TrimSpace(r.FormValue("announceof")) + filt.Reject = r.FormValue("doreject") == "yes" + filt.SkipMedia = r.FormValue("doskipmedia") == "yes" + filt.Hide = r.FormValue("dohide") == "yes" + filt.Collapse = r.FormValue("docollapse") == "yes" + filt.Rewrite = strings.TrimSpace(r.FormValue("filtrewrite")) + filt.Replace = strings.TrimSpace(r.FormValue("filtreplace")) + if dur := parseDuration(r.FormValue("filtduration")); dur > 0 { + filt.Expiration = time.Now().UTC().Add(dur) + } + filt.Notes = strings.TrimSpace(r.FormValue("filtnotes")) + + if filt.Actor == "" && filt.Text == "" && !filt.IsAnnounce { + ilog.Printf("blank filter") + http.Error(w, "can't save a blank filter", http.StatusInternalServerError) + return + } + + j, err := jsonify(filt) + if err == nil { + _, err = stmtSaveFilter.Exec(userinfo.UserID, j) + } + if err != nil { + elog.Printf("error saving filter: %s", err) + } + + filtInvalidator.Clear(userinfo.UserID) + http.Redirect(w, r, "/hfcs", http.StatusSeeOther) +} + +func accountpage(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + user, _ := butwhatabout(u.Username) + templinfo := getInfo(r) + templinfo["UserCSRF"] = login.GetCSRF("saveuser", r) + templinfo["LogoutCSRF"] = login.GetCSRF("logout", r) + templinfo["User"] = user + about := user.About + if ava := user.Options.Avatar; ava != "" { + about += "\n\navatar: " + ava[strings.LastIndexByte(ava, '/')+1:] + } + if ban := user.Options.Banner; ban != "" { + about += "\n\nbanner: " + ban[strings.LastIndexByte(ban, '/')+1:] + } + templinfo["WhatAbout"] = about + err := readviews.Execute(w, "account.html", templinfo) + if err != nil { + elog.Print(err) + } +} + +func dochpass(w http.ResponseWriter, r *http.Request) { + err := login.ChangePassword(w, r) + if err != nil { + elog.Printf("error changing password: %s", err) + } + http.Redirect(w, r, "/account", http.StatusSeeOther) +} + +func fingerlicker(w http.ResponseWriter, r *http.Request) { + orig := r.FormValue("resource") + + dlog.Printf("finger lick: %s", orig) + + if strings.HasPrefix(orig, "acct:") { + orig = orig[5:] + } + + name := orig + idx := strings.LastIndexByte(name, '/') + if idx != -1 { + name = name[idx+1:] + if fmt.Sprintf("https://%s/%s/%s", serverName, userSep, name) != orig { + ilog.Printf("foreign request rejected") + name = "" + } + } else { + idx = strings.IndexByte(name, '@') + if idx != -1 { + name = name[:idx] + if !(name+"@"+serverName == orig || name+"@"+masqName == orig) { + ilog.Printf("foreign request rejected") + name = "" + } + } + } + user, err := butwhatabout(name) + if err != nil { + http.NotFound(w, r) + return + } + if stealthmode(user.ID, r) { + http.NotFound(w, r) + return + } + + j := junk.New() + j["subject"] = fmt.Sprintf("acct:%s@%s", user.Name, masqName) + j["aliases"] = []string{user.URL} + l := junk.New() + l["rel"] = "self" + l["type"] = `application/activity+json` + l["href"] = user.URL + j["links"] = []junk.Junk{l} + + w.Header().Set("Content-Type", "application/jrd+json") + j.Write(w) +} + +func somedays() string { + secs := 432000 + notrand.Int63n(432000) + return fmt.Sprintf("%d", secs) +} + +func avatate(w http.ResponseWriter, r *http.Request) { + if develMode { + loadAvatarColors() + } + n := r.FormValue("a") + hex := r.FormValue("hex") == "1" + a := genAvatar(n, hex) + if !develMode { + w.Header().Set("Cache-Control", "max-age="+somedays()) + } + w.Write(a) +} + +func serveviewasset(w http.ResponseWriter, r *http.Request) { + serveasset(w, r, viewDir) +} +func servedataasset(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/favicon.ico" { + r.URL.Path = "/icon.png" + } + serveasset(w, r, dataDir) +} + +func serveasset(w http.ResponseWriter, r *http.Request, basedir string) { + if !develMode { + w.Header().Set("Cache-Control", "max-age=7776000") + } + http.ServeFile(w, r, basedir+"/views"+r.URL.Path) +} +func servehelp(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + if !develMode { + w.Header().Set("Cache-Control", "max-age=3600") + } + http.ServeFile(w, r, viewDir+"/docs/"+name) +} +func servehtml(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + templinfo := getInfo(r) + templinfo["AboutMsg"] = aboutMsg + templinfo["LoginMsg"] = loginMsg + templinfo["HonkVersion"] = softwareVersion + if r.URL.Path == "/about" { + templinfo["Sensors"] = getSensors() + } + if u == nil && !develMode { + w.Header().Set("Cache-Control", "max-age=60") + } + err := readviews.Execute(w, r.URL.Path[1:]+".html", templinfo) + if err != nil { + elog.Print(err) + } +} +func serveemu(w http.ResponseWriter, r *http.Request) { + emu := mux.Vars(r)["emu"] + + w.Header().Set("Cache-Control", "max-age="+somedays()) + http.ServeFile(w, r, dataDir+"/emus/"+emu) +} +func servememe(w http.ResponseWriter, r *http.Request) { + meme := mux.Vars(r)["meme"] + + w.Header().Set("Cache-Control", "max-age="+somedays()) + http.ServeFile(w, r, dataDir+"/memes/"+meme) +} + +func servefile(w http.ResponseWriter, r *http.Request) { + xid := mux.Vars(r)["xid"] + var media string + var data []byte + row := stmtGetFileData.QueryRow(xid) + err := row.Scan(&media, &data) + if err != nil { + elog.Printf("error loading file: %s", err) + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", media) + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Cache-Control", "max-age="+somedays()) + w.Write(data) +} + +func nomoroboto(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "User-agent: *\n") + io.WriteString(w, "Disallow: /a\n") + io.WriteString(w, "Disallow: /d/\n") + io.WriteString(w, "Disallow: /meme/\n") + io.WriteString(w, "Disallow: /o\n") + io.WriteString(w, "Disallow: /o/\n") + io.WriteString(w, "Disallow: /help/\n") + for _, u := range allusers() { + fmt.Fprintf(w, "Disallow: /%s/%s/%s/\n", userSep, u.Username, honkSep) + } +} + +type Hydration struct { + Tophid int64 + Srvmsg template.HTML + Honks string + MeCount int64 + ChatCount int64 +} + +func webhydra(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + userid := u.UserID + templinfo := getInfo(r) + templinfo["HonkCSRF"] = login.GetCSRF("honkhonk", r) + page := r.FormValue("page") + + wanted, _ := strconv.ParseInt(r.FormValue("tophid"), 10, 0) + + var hydra Hydration + + var honks []*Honk + switch page { + case "atme": + honks = gethonksforme(userid, wanted) + honks = osmosis(honks, userid, false) + menewnone(userid) + hydra.Srvmsg = "at me!" + case "longago": + honks = gethonksfromlongago(userid, wanted) + honks = osmosis(honks, userid, false) + hydra.Srvmsg = "from long ago" + case "home": + honks = gethonksforuser(userid, wanted) + honks = osmosis(honks, userid, true) + hydra.Srvmsg = serverMsg + case "first": + honks = gethonksforuserfirstclass(userid, wanted) + honks = osmosis(honks, userid, true) + hydra.Srvmsg = "first class only" + case "saved": + honks = getsavedhonks(userid, wanted) + templinfo["PageName"] = "saved" + hydra.Srvmsg = "saved honks" + case "combo": + c := r.FormValue("c") + honks = gethonksbycombo(userid, c, wanted) + honks = osmosis(honks, userid, false) + hydra.Srvmsg = templates.Sprintf("honks by combo: %s", c) + case "convoy": + c := r.FormValue("c") + honks = gethonksbyconvoy(userid, c, wanted) + honks = osmosis(honks, userid, false) + hydra.Srvmsg = templates.Sprintf("honks in convoy: %s", c) + case "honker": + xid := r.FormValue("xid") + honks = gethonksbyxonker(userid, xid, wanted) + miniform := templates.Sprintf(`<form action="/submithonker" method="POST"> + <input type="hidden" name="CSRF" value="%s"> + <input type="hidden" name="url" value="%s"> + <button tabindex=1 name="add honker" value="add honker">add honker</button> + </form>`, login.GetCSRF("submithonker", r), xid) + msg := templates.Sprintf(`honks by honker: <a href="%s" ref="noreferrer">%s</a>%s`, xid, xid, miniform) + hydra.Srvmsg = msg + case "user": + uname := r.FormValue("uname") + honks = gethonksbyuser(uname, u != nil && u.Username == uname, wanted) + hydra.Srvmsg = templates.Sprintf("honks by user: %s", uname) + default: + http.NotFound(w, r) + } + + if len(honks) > 0 { + hydra.Tophid = honks[0].ID + } else { + hydra.Tophid = wanted + } + reverbolate(userid, honks) + + user, _ := butwhatabout(u.Username) + + var buf strings.Builder + templinfo["Honks"] = honks + templinfo["MapLink"] = getmaplink(u) + templinfo["User"], _ = butwhatabout(u.Username) + err := readviews.Execute(&buf, "honkfrags.html", templinfo) + if err != nil { + elog.Printf("frag error: %s", err) + return + } + hydra.Honks = buf.String() + hydra.MeCount = user.Options.MeCount + hydra.ChatCount = user.Options.ChatCount + w.Header().Set("Content-Type", "application/json") + j, _ := jsonify(&hydra) + io.WriteString(w, j) +} + +var honkline = make(chan bool) + +func honkhonkline() { + for { + select { + case honkline <- true: + default: + return + } + } +} + +func apihandler(w http.ResponseWriter, r *http.Request) { + u := login.GetUserInfo(r) + userid := u.UserID + action := r.FormValue("action") + wait, _ := strconv.ParseInt(r.FormValue("wait"), 10, 0) + dlog.Printf("api request '%s' on behalf of %s", action, u.Username) + switch action { + case "honk": + h := submithonk(w, r) + if h == nil { + return + } + w.Write([]byte(h.XID)) + case "donk": + d, err := submitdonk(w, r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if d == nil { + http.Error(w, "missing donk", http.StatusBadRequest) + return + } + w.Write([]byte(d.XID)) + case "zonkit": + zonkit(w, r) + case "gethonks": + var honks []*Honk + wanted, _ := strconv.ParseInt(r.FormValue("after"), 10, 0) + page := r.FormValue("page") + var waitchan <-chan time.Time + requery: + switch page { + case "atme": + honks = gethonksforme(userid, wanted) + honks = osmosis(honks, userid, false) + menewnone(userid) + case "longago": + honks = gethonksfromlongago(userid, wanted) + honks = osmosis(honks, userid, false) + case "home": + honks = gethonksforuser(userid, wanted) + honks = osmosis(honks, userid, true) + case "myhonks": + honks = gethonksbyuser(u.Username, true, wanted) + honks = osmosis(honks, userid, true) + default: + http.Error(w, "unknown page", http.StatusNotFound) + return + } + if len(honks) == 0 && wait > 0 { + if waitchan == nil { + waitchan = time.After(time.Duration(wait) * time.Second) + } + select { + case <-honkline: + goto requery + case <-waitchan: + } + } + reverbolate(userid, honks) + j := junk.New() + j["honks"] = honks + j.Write(w) + case "sendactivity": + user, _ := butwhatabout(u.Username) + public := r.FormValue("public") == "1" + rcpts := boxuprcpts(user, r.Form["rcpt"], public) + msg := []byte(r.FormValue("msg")) + for rcpt := range rcpts { + go deliverate(0, userid, rcpt, msg, true) + } + default: + http.Error(w, "unknown action", http.StatusNotFound) + return + } +} + +var endoftheworld = make(chan bool) +var readyalready = make(chan bool) +var workinprogress = 0 + +func enditall() { + sig := make(chan os.Signal) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) + <-sig + ilog.Printf("stopping...") + for i := 0; i < workinprogress; i++ { + endoftheworld <- true + } + ilog.Printf("waiting...") + for i := 0; i < workinprogress; i++ { + <-readyalready + } + ilog.Printf("apocalypse") + os.Exit(0) +} + +var preservehooks []func() + +func bgmonitor() { + for { + when := time.Now().Add(-3 * 24 * time.Hour).UTC().Format(dbtimeformat) + _, err := stmtDeleteOldXonkers.Exec("pubkey", when) + if err != nil { + elog.Printf("error deleting old xonkers: %s", err) + } + zaggies.Flush() + time.Sleep(50 * time.Minute) + } +} + +func serve() { + db := opendatabase() + login.Init(login.InitArgs{Db: db, Logger: ilog, Insecure: develMode}) + + listener, err := openListener() + if err != nil { + elog.Fatal(err) + } + runBackendServer() + go enditall() + go redeliverator() + go tracker() + go bgmonitor() + loadLingo() + + readviews = templates.Load(develMode, + viewDir+"/views/honkpage.html", + viewDir+"/views/honkfrags.html", + viewDir+"/views/honkers.html", + viewDir+"/views/chatter.html", + viewDir+"/views/hfcs.html", + viewDir+"/views/combos.html", + viewDir+"/views/honkform.html", + viewDir+"/views/honk.html", + viewDir+"/views/account.html", + viewDir+"/views/about.html", + viewDir+"/views/funzone.html", + viewDir+"/views/login.html", + viewDir+"/views/xzone.html", + viewDir+"/views/msg.html", + viewDir+"/views/header.html", + viewDir+"/views/onts.html", + viewDir+"/views/honkpage.js", + ) + if !develMode { + assets := []string{ + viewDir + "/views/style.css", + dataDir + "/views/local.css", + viewDir + "/views/honkpage.js", + dataDir + "/views/local.js", + } + for _, s := range assets { + savedassetparams[s] = getassetparam(s) + } + loadAvatarColors() + } + + for _, h := range preservehooks { + h() + } + + mux := mux.NewRouter() + mux.Use(login.Checker) + + mux.Handle("/api", login.TokenRequired(http.HandlerFunc(apihandler))) + + posters := mux.Methods("POST").Subrouter() + getters := mux.Methods("GET").Subrouter() + + getters.HandleFunc("/", homepage) + getters.HandleFunc("/home", homepage) + getters.HandleFunc("/front", homepage) + getters.HandleFunc("/events", homepage) + getters.HandleFunc("/robots.txt", nomoroboto) + getters.HandleFunc("/rss", showrss) + getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}", showuser) + getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/"+honkSep+"/{xid:[\\pL[:digit:]]+}", showonehonk) + getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/rss", showrss) + posters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/inbox", inbox) + getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/outbox", outbox) + getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/followers", emptiness) + getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/following", emptiness) + getters.HandleFunc("/a", avatate) + getters.HandleFunc("/o", thelistingoftheontologies) + getters.HandleFunc("/o/{name:.+}", showontology) + getters.HandleFunc("/d/{xid:[\\pL[:digit:].]+}", servefile) + getters.HandleFunc("/emu/{emu:[^.]*[^/]+}", serveemu) + getters.HandleFunc("/meme/{meme:[^.]*[^/]+}", servememe) + getters.HandleFunc("/.well-known/webfinger", fingerlicker) + + getters.HandleFunc("/flag/{code:.+}", showflag) + + getters.HandleFunc("/server", serveractor) + posters.HandleFunc("/server/inbox", serverinbox) + posters.HandleFunc("/inbox", serverinbox) + + getters.HandleFunc("/style.css", serveviewasset) + getters.HandleFunc("/honkpage.js", serveviewasset) + getters.HandleFunc("/wonk.js", serveviewasset) + getters.HandleFunc("/local.css", servedataasset) + getters.HandleFunc("/local.js", servedataasset) + getters.HandleFunc("/icon.png", servedataasset) + getters.HandleFunc("/favicon.ico", servedataasset) + + getters.HandleFunc("/about", servehtml) + getters.HandleFunc("/login", servehtml) + posters.HandleFunc("/dologin", login.LoginFunc) + getters.HandleFunc("/logout", login.LogoutFunc) + getters.HandleFunc("/help/{name:[\\pL[:digit:]_.-]+}", servehelp) + + getters.HandleFunc("/bloat/wonkles", servewonkles) + + loggedin := mux.NewRoute().Subrouter() + loggedin.Use(login.Required) + loggedin.HandleFunc("/first", homepage) + loggedin.HandleFunc("/chatter", showchatter) + loggedin.Handle("/sendchonk", login.CSRFWrap("sendchonk", http.HandlerFunc(submitchonk))) + loggedin.HandleFunc("/saved", homepage) + loggedin.HandleFunc("/account", accountpage) + loggedin.HandleFunc("/funzone", showfunzone) + loggedin.HandleFunc("/chpass", dochpass) + loggedin.HandleFunc("/atme", homepage) + loggedin.HandleFunc("/longago", homepage) + loggedin.HandleFunc("/hfcs", hfcspage) + loggedin.HandleFunc("/xzone", xzone) + loggedin.HandleFunc("/newhonk", newhonkpage) + loggedin.HandleFunc("/edit", edithonkpage) + loggedin.Handle("/honk", login.CSRFWrap("honkhonk", http.HandlerFunc(submitwebhonk))) + loggedin.Handle("/bonk", login.CSRFWrap("honkhonk", http.HandlerFunc(submitbonk))) + loggedin.Handle("/zonkit", login.CSRFWrap("honkhonk", http.HandlerFunc(zonkit))) + loggedin.Handle("/savehfcs", login.CSRFWrap("filter", http.HandlerFunc(savehfcs))) + loggedin.Handle("/saveuser", login.CSRFWrap("saveuser", http.HandlerFunc(saveuser))) + loggedin.Handle("/ximport", login.CSRFWrap("ximport", http.HandlerFunc(ximport))) + loggedin.HandleFunc("/honkers", showhonkers) + loggedin.HandleFunc("/h/{name:[\\pL[:digit:]_.-]+}", showhonker) + loggedin.HandleFunc("/h", showhonker) + loggedin.HandleFunc("/c/{name:[\\pL[:digit:]_.-]+}", showcombo) + loggedin.HandleFunc("/c", showcombos) + loggedin.HandleFunc("/t", showconvoy) + loggedin.HandleFunc("/q", showsearch) + loggedin.HandleFunc("/hydra", webhydra) + loggedin.Handle("/submithonker", login.CSRFWrap("submithonker", http.HandlerFunc(submithonker))) + + err = http.Serve(listener, mux) + if err != nil { + elog.Fatal(err) + } +}