diff --git a/.hgignore b/.hgignore index a00a9cf..5596488 100644 --- a/.hgignore +++ b/.hgignore @@ -7,3 +7,11 @@ memes emus honk violations.json +docs/activitypub.7.html +docs/hfcs.1.html +docs/honk.1.html +docs/honk.3.html +docs/honk.5.html +docs/honk.8.html +docs/intro.1.html +docs/vim.3.html diff --git a/.hgtags b/.hgtags index 963d5f9..3f4055d 100644 --- a/.hgtags +++ b/.hgtags @@ -39,3 +39,5 @@ bc1bcfb9c0cc86b3c63325b07e13a36b9d4500f0 v0.9.7 4b8cf31560b7d1e1696af109b158766c4ce823ab v0.9.9 d7c3a01e7aaef67c40920bbc4e8507350fc33e31 v0.9.91 b1e7ac92a58a7183310b1a5cca8222d65f242d81 v1.0.0 +36c2a2746133f4b5b31103c0b4232554d2b15a5d v1.1.0 +135cdbfa6d7d1a9b1b436cb57d9837a943d83227 v1.1.1 diff --git a/Makefile b/Makefile index 8b87efd..f382c8a 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,11 @@ honk: .preflightcheck schema.sql *.go go.mod .preflightcheck: preflight.sh @sh ./preflight.sh +help: + for m in docs/*.[13578] ; do \ + mandoc -T html -O style=mandoc.css,man=%N.%S.html $$m | sed -E 's//
/g' > $$m.html ; \ + done + clean: rm -f honk diff --git a/README b/README index 12da7d2..3287cc7 100644 --- a/README +++ b/README @@ -20,7 +20,7 @@ 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. +You'll need a go compiler version 1.18 or later. And libsqlite3. Even on a fast machine, building from source can take several seconds. diff --git a/activity.go b/activity.go index c91b32c..2e8b997 100644 --- a/activity.go +++ b/activity.go @@ -59,12 +59,14 @@ func friendorfoe(ct string) bool { return false } -var develClient = &http.Client{ - Transport: &http.Transport{ +var honkClient = http.Client{} + +func gogglesDoNothing() { + honkClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, - }, + } } func PostJunk(keyname string, key httpsig.PrivateKey, url string, j junk.Junk) error { @@ -72,10 +74,6 @@ func PostJunk(keyname string, key httpsig.PrivateKey, url string, j junk.Junk) e } 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 @@ -86,7 +84,7 @@ func PostMsg(keyname string, key httpsig.PrivateKey, url string, msg []byte) err ctx, cancel := context.WithTimeout(context.Background(), 2*slowTimeout*time.Second) defer cancel() req = req.WithContext(ctx) - resp, err := client.Do(req) + resp, err := honkClient.Do(req) if err != nil { return err } @@ -130,13 +128,10 @@ func GetJunkHardMode(userid int64, url string) (junk.Junk, error) { var flightdeck = gate.NewSerializer() -var signGets = true - func GetJunkTimeout(userid int64, url string, timeout time.Duration) (junk.Junk, error) { if rejectorigin(userid, url, false) { return nil, fmt.Errorf("rejected origin: %s", url) } - client := http.DefaultClient sign := func(req *http.Request) error { var ki *KeyInfo ok := ziggies.Get(userid, &ki) @@ -146,9 +141,18 @@ func GetJunkTimeout(userid int64, url string, timeout time.Duration) (junk.Junk, return nil } if develMode { - client = develClient sign = nil } + client := honkClient + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if len(via) >= 5 { + return fmt.Errorf("stopped after 5 redirects") + } + if sign != nil { + sign(req) + } + return nil + } fn := func() (interface{}, error) { at := theonetruename if strings.Contains(url, ".well-known/webfinger?resource") { @@ -158,7 +162,7 @@ func GetJunkTimeout(userid int64, url string, timeout time.Duration) (junk.Junk, Accept: at, Agent: "honksnonk/5.0; " + serverName, Timeout: timeout, - Client: client, + Client: &client, Fixup: sign, }) return j, err @@ -173,10 +177,6 @@ func GetJunkTimeout(userid int64, url string, timeout time.Duration) (junk.Junk, } 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) @@ -186,7 +186,7 @@ func fetchsome(url string) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() req = req.WithContext(ctx) - resp, err := client.Do(req) + resp, err := honkClient.Do(req) if err != nil { ilog.Printf("error fetching %s: %s", url, err) return nil, err diff --git a/backend.go b/backend.go index 19b260f..2d20874 100644 --- a/backend.go +++ b/backend.go @@ -83,6 +83,31 @@ func imageFromSVG(data []byte) (*image.Image, error) { return svg, nil } +func bigshrink(data []byte) (*image.Image, error) { + if isSVG(data) { + return imageFromSVG(data) + } + 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: 14200 * 4200, + MaxWidth: 2600, + MaxHeight: 2048, + MaxSize: 768 * 1024, + }, + }, &res) + if err != nil { + return nil, err + } + return res.Image, nil +} + func shrinkit(data []byte) (*image.Image, error) { if isSVG(data) { return imageFromSVG(data) diff --git a/backupdb.go b/backupdb.go index 10d1853..d95539e 100644 --- a/backupdb.go +++ b/backupdb.go @@ -35,6 +35,7 @@ func svalbard(dirname string) { if err != nil { elog.Fatalf("can't open backup database") } + _, err = backup.Exec("PRAGMA journal_mode=WAL") for _, line := range strings.Split(sqlSchema, ";") { _, err = backup.Exec(line) if err != nil { @@ -76,16 +77,16 @@ func svalbard(dirname string) { 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) + rows = qordie(orig, "select honkid, userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags, plain from honks where convoy = ?", c) for rows.Next() { var honkid, userid int64 - var what, honker, xid, rid, dt, url, audience, noise, convoy string + var what, honker, xid, rid, dt, url, audience, noise, convoy, plain 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) + scanordie(rows, &honkid, &userid, &what, &honker, &xid, &rid, &dt, &url, &audience, &noise, &convoy, &whofore, &format, &precis, &oonker, &flags, &plain) 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) + doordie(tx, "insert into honks (honkid, userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags, plain) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", honkid, userid, what, honker, xid, rid, dt, url, audience, noise, convoy, whofore, format, precis, oonker, flags, plain) } rows.Close() } @@ -169,6 +170,7 @@ func svalbard(dirname string) { if err != nil { elog.Fatalf("can't open backup blob database") } + _, err = blob.Exec("PRAGMA journal_mode=WAL") 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)") diff --git a/database.go b/database.go index 8c9696b..e9cb3cb 100644 --- a/database.go +++ b/database.go @@ -36,6 +36,8 @@ import ( "humungus.tedunangst.com/r/webs/mz" ) +var honkwindow time.Duration = 7 + //go:embed schema.sql var sqlSchema string @@ -187,7 +189,7 @@ func getbonk(userid int64, xid string) *Honk { } func getpublichonks() []*Honk { - dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + dt := time.Now().Add(-honkwindow).UTC().Format(dbtimeformat) rows, err := stmtPublicHonks.Query(dt, 100) return getsomehonks(rows, err) } @@ -223,7 +225,7 @@ func geteventhonks(userid int64) []*Honk { return honks } func gethonksbyuser(name string, includeprivate bool, wanted int64) []*Honk { - dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + dt := time.Now().Add(-honkwindow).UTC().Format(dbtimeformat) limit := 50 whofore := 2 if includeprivate { @@ -233,19 +235,19 @@ func gethonksbyuser(name string, includeprivate bool, wanted int64) []*Honk { return getsomehonks(rows, err) } func gethonksforuser(userid int64, wanted int64) []*Honk { - dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + dt := time.Now().Add(-honkwindow).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) + dt := time.Now().Add(-honkwindow).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) + dt := time.Now().Add(-honkwindow).UTC().Format(dbtimeformat) + rows, err := stmtHonksForMe.Query(wanted, userid, dt, userid, 250) return getsomehonks(rows, err) } func gethonksfromlongago(userid int64, wanted int64) []*Honk { @@ -305,11 +307,11 @@ func gethonksbysearch(userid int64, q string, wanted int64) []*Honk { continue } if t == "@me" { - queries = append(queries, "whofore = 1") + queries = append(queries, negate+"whofore = 1") continue } if t == "@self" { - queries = append(queries, "(whofore = 2 or whofore = 3)") + queries = append(queries, negate+"(whofore = 2 or whofore = 3)") continue } if strings.HasPrefix(t, "before:") { @@ -790,9 +792,19 @@ func loadchatter(userid int64) []*Chatter { } func (honk *Honk) Plain() string { + return honktoplain(honk, false) +} + +func (honk *Honk) VeryPlain() string { + return honktoplain(honk, true) +} + +func honktoplain(honk *Honk, very bool) string { var plain []string var filt htfilter.Filter - filt.WithLinks = true + if !very { + filt.WithLinks = true + } if honk.Precis != "" { t, _ := filt.TextOnly(honk.Precis) plain = append(plain, t) @@ -1184,7 +1196,7 @@ func prepareStatements(db *sql.DB) { 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 (rid = '' or what = 'bonk')"+myhonkers+butnotthose+limit) - stmtHonksForMe = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ? and whofore = 1"+butnotthose+limit) + stmtHonksForMe = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ? and whofore = 1"+butnotthose+smalllimit) 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) diff --git a/deliverator.go b/deliverator.go index 6f55964..e1c8377 100644 --- a/deliverator.go +++ b/deliverator.go @@ -118,8 +118,9 @@ func deliverate(userid int64, rcpt string, msg []byte) { var garage = gate.NewLimiter(40) func deliveration(doover Doover) { - garage.Start() - defer garage.Finish() + rcpt := doover.Rcpt + garage.StartKey(rcpt) + defer garage.FinishKey(rcpt) var ki *KeyInfo ok := ziggies.Get(doover.Userid, &ki) @@ -128,7 +129,6 @@ func deliveration(doover Doover) { return } var inbox string - rcpt := doover.Rcpt // already did the box indirection if rcpt[0] == '%' { inbox = rcpt[1:] diff --git a/docs/changelog.txt b/docs/changelog.txt index 5e7adbd..987b4b2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -1,5 +1,39 @@ changelog +### next + ++ Finally fix slow public queries. + +### 1.1.1 Required Refinement + ++ Fix help file stylesheet link. + +### 1.1.0 Eventual Enshittification + ++ Fix backup command. + ++ Fixes for markdown. + ++ Allow bigger image uploads. + ++ Some hotkeys for the web UI. + ++ Upload multiple files (but beware). + ++ Better page titles. + ++ Refine thread sort. + ++ Send updates to correct audience. + ++ Run analyze to improve database performance. + ++ Delivery performance improvements. + ++ Export command to ActivityPub data. (And import.) + ++ Note that we require go 1.18 now. + ### 1.0.0 Happy Honker + A great big honk composition text box. diff --git a/docs/honk.1 b/docs/honk.1 index 4b4582a..bd8a583 100644 --- a/docs/honk.1 +++ b/docs/honk.1 @@ -130,11 +130,28 @@ Replies higher in the tree are still received. 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 Hotkeys +The following keyboard shortcuts may also be used to navigate. +.Bl -tag -width short +.It j +Scroll to next honk. +.It k +Scroll to previous honk. +.It r +Refresh. +.It s +Scroll down to oldest newest. +.It m +Open menu. +.It esc +Close menu. +.It / +Search. +.El .Ss Honking Refer to the .Xr honk 5 diff --git a/docs/honk.8 b/docs/honk.8 index 966a939..af6942a 100644 --- a/docs/honk.8 +++ b/docs/honk.8 @@ -41,7 +41,7 @@ proxy_set_header Host $http_host; .Ss Build Building .Nm -requires a go compiler 1.16 and libsqlite. +requires a go compiler 1.18 and libsqlite. On .Ox this is the go and sqlite3 packages. @@ -194,10 +194,13 @@ and templates are reloaded every request. Data may be imported and converted from other services using the .Ic import command. -Currently supports Mastodon, Twitter, and Instagram exported data. +Currently supports Honk, Mastodon, Twitter, and Instagram 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 Honk data archive, extract the export.zip file. +.Dl ./honk import username honk source-directory +.Pp To prepare a Mastodon data archive, extract the archive-longhash.tar.gz file. .Dl ./honk import username mastodon source-directory .Pp @@ -208,6 +211,13 @@ and unzip any zip files contained within. .Pp To prepare an Instagram data archive, extract the igusername.zip file. .Dl ./honk import username instagram source-directory +.Ss Export +User data may be exported to a zip archive using the +.Ic export +command. +This will export the user's outbox and inbox in ActvityPub json format, +along with associated media. +.Dl ./honk export username zipname .Ss Advanced Options Advanced configuration values may be set by running the .Ic setconfig Ar key value diff --git a/fun.go b/fun.go index d013ac9..202c25e 100644 --- a/fun.go +++ b/fun.go @@ -330,6 +330,7 @@ func precipitate(honk *Honk) { noise = noise[idx+1:] } var marker mz.Marker + marker.Short = true honk.Precis = marker.Mark(strings.TrimSpace(honk.Precis)) honk.Noise = noise } @@ -344,6 +345,7 @@ func translate(honk *Honk) { var marker mz.Marker marker.HashLinker = ontoreplacer marker.AtLinker = attoreplacer + marker.AllowImages = true noise = strings.TrimSpace(noise) noise = marker.Mark(noise) honk.Noise = noise @@ -432,6 +434,9 @@ var emucache = cache.New(cache.Options{Filler: func(ename string) (Emu, bool) { continue } url := fmt.Sprintf("https://%s/emu/%s%s", serverName, fname, ext) + if develMode { + url = fmt.Sprintf("/emu/%s%s", fname, ext) + } return Emu{ID: url, Name: ename, Type: "image/" + ext[1:]}, true } return Emu{Name: ename, ID: "", Type: "image/png"}, true diff --git a/go.mod b/go.mod index a19ad41..96c2c56 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,18 @@ module humungus.tedunangst.com/r/honk -go 1.16 +go 1.18 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 + golang.org/x/crypto v0.12.0 + golang.org/x/net v0.14.0 humungus.tedunangst.com/r/go-sqlite3 v1.1.3 - humungus.tedunangst.com/r/webs v0.6.68 + humungus.tedunangst.com/r/webs v0.7.9 +) + +require ( + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/image v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index 5fef797..9625746 100644 --- a/go.sum +++ b/go.sum @@ -6,24 +6,47 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4 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= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.68 h1:veKjASf1krPf4o3O7hMRsNvE4+Z6LzXVso/qMccZntk= -humungus.tedunangst.com/r/webs v0.6.68/go.mod h1:03R0N9BcT49HB4TDd1YmarpbiPvPzVDm74Mk4h1hYPc= +humungus.tedunangst.com/r/webs v0.7.9 h1:LC9o2F9joAcf4SxWaRFs5ZqXHSbzdfre9/9BY0gcM0w= +humungus.tedunangst.com/r/webs v0.7.9/go.mod h1:ylhqHSPI0Oi7b4nsnx5mSO7AjLXN7wFpEHayLfN/ugk= diff --git a/import.go b/import.go index 7dfbe6e..dd0d3a3 100644 --- a/import.go +++ b/import.go @@ -16,6 +16,7 @@ package main import ( + "archive/zip" "encoding/csv" "encoding/json" "fmt" @@ -27,12 +28,16 @@ import ( "sort" "strings" "time" + + "humungus.tedunangst.com/r/webs/junk" ) func importMain(username, flavor, source string) { switch flavor { case "mastodon": importMastodon(username, source) + case "honk": + importHonk(username, source) case "twitter": importTwitter(username, source) case "instagram": @@ -42,11 +47,17 @@ func importMain(username, flavor, source string) { } } -type TootObject struct { +type ActivityObject struct { + AttributedTo string Summary string Content string + Source struct { + MediaType string + Content string + } InReplyTo string Conversation string + Context string Published time.Time Tag []struct { Type string @@ -60,10 +71,10 @@ type TootObject struct { } } -type PlainTootObject TootObject +type PlainActivityObject ActivityObject -func (obj *TootObject) UnmarshalJSON(b []byte) error { - p := (*PlainTootObject)(obj) +func (obj *ActivityObject) UnmarshalJSON(b []byte) error { + p := (*PlainActivityObject)(obj) json.Unmarshal(b, p) return nil } @@ -74,8 +85,9 @@ func importMastodon(username, source string) { elog.Fatal(err) } - if _, err := os.Stat(source + "/outbox.json"); err == nil { - importMastotoots(user, source) + outbox := source + "/outbox.json" + if _, err := os.Stat(outbox); err == nil { + importActivities(user, outbox, source) } else { ilog.Printf("skipping outbox.json!") } @@ -86,19 +98,33 @@ func importMastodon(username, source string) { } } -func importMastotoots(user *WhatAbout, source string) { - type Toot struct { +func importHonk(username, source string) { + user, err := butwhatabout(username) + if err != nil { + elog.Fatal(err) + } + + outbox := source + "/outbox.json" + if _, err := os.Stat(outbox); err == nil { + importActivities(user, outbox, source) + } else { + ilog.Printf("skipping outbox.json!") + } +} + +func importActivities(user *WhatAbout, filename, source string) { + type Activity struct { Id string Type string - To []string + To interface{} Cc []string - Object TootObject + Object ActivityObject } var outbox struct { - OrderedItems []Toot + OrderedItems []Activity } ilog.Println("Importing honks...") - fd, err := os.Open(source + "/outbox.json") + fd, err := os.Open(filename) if err != nil { elog.Fatal(err) } @@ -120,7 +146,11 @@ func importMastotoots(user *WhatAbout, source string) { } re_tootid := regexp.MustCompile("[^/]+$") - for _, item := range outbox.OrderedItems { + items := outbox.OrderedItems + 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 { toot := item if toot.Type != "Create" { continue @@ -133,6 +163,27 @@ func importMastotoots(user *WhatAbout, source string) { if havetoot(xid) { continue } + + convoy := toot.Object.Context + if convoy == "" { + convoy = toot.Object.Conversation + } + var audience []string + to, ok := toot.To.(string) + if ok { + audience = append(audience, to) + } else { + for _, t := range toot.To.([]interface{}) { + audience = append(audience, t.(string)) + } + } + content := toot.Object.Content + format := "html" + if toot.Object.Source.MediaType == "text/markdown" { + content = toot.Object.Source.Content + format = "markdown" + } + audience = append(audience, toot.Cc...) honk := Honk{ UserID: user.ID, What: "honk", @@ -141,11 +192,11 @@ func importMastotoots(user *WhatAbout, source string) { RID: toot.Object.InReplyTo, Date: toot.Object.Published, URL: xid, - Audience: append(toot.To, toot.Cc...), - Noise: toot.Object.Content, - Convoy: toot.Object.Conversation, + Audience: audience, + Noise: content, + Convoy: convoy, Whofore: 2, - Format: "html", + Format: format, Precis: toot.Object.Summary, } if !loudandproud(honk.Audience) { @@ -157,7 +208,7 @@ func importMastotoots(user *WhatAbout, source string) { fname := fmt.Sprintf("%s/%s", source, att.Url) data, err := ioutil.ReadFile(fname) if err != nil { - elog.Printf("error reading media: %s", fname) + elog.Printf("error reading media for %s: %s", honk.XID, fname) continue } u := xfiltrate() @@ -513,3 +564,95 @@ func importInstagram(username, source string) { log.Printf("honk saved %v -> %v", xid, err) } } + +func export(username, file string) { + user, err := butwhatabout(username) + if err != nil { + elog.Fatal(err) + } + fd, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) + if err != nil { + elog.Fatal(err) + } + zd := zip.NewWriter(fd) + donks := make(map[string]bool) + { + w, err := zd.Create("outbox.json") + if err != nil { + elog.Fatal("error creating outbox.json", err) + } + var jonks []junk.Junk + rows, err := stmtUserHonks.Query(0, 3, user.Name, "0", 1234567) + honks := getsomehonks(rows, err) + for _, honk := range honks { + for _, donk := range honk.Donks { + donk.URL = "media/" + donk.XID + donks[donk.XID] = true + } + noise := honk.Noise + j, jo := jonkjonk(user, honk) + if honk.Format == "markdown" { + source := junk.New() + source["mediaType"] = "text/markdown" + source["content"] = noise + jo["source"] = source + } + 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 + j.Write(w) + } + { + w, err := zd.Create("inbox.json") + if err != nil { + elog.Fatal("error creating inbox.json", err) + } + var jonks []junk.Junk + rows, err := stmtHonksForMe.Query(0, user.ID, "0", user.ID, 1234567) + honks := getsomehonks(rows, err) + for _, honk := range honks { + for _, donk := range honk.Donks { + donk.URL = "media/" + donk.XID + donks[donk.XID] = true + } + j, _ := jonkjonk(user, honk) + jonks = append(jonks, j) + } + j := junk.New() + j["@context"] = itiswhatitis + j["id"] = user.URL + "/inbox" + j["attributedTo"] = user.URL + j["type"] = "OrderedCollection" + j["totalItems"] = len(jonks) + j["orderedItems"] = jonks + j.Write(w) + } + zd.Create("media/") + for donk := range donks { + if donk == "" { + continue + } + var media string + var data []byte + w, err := zd.Create("media/" + donk) + if err != nil { + elog.Printf("error creating %s: %s", donk, err) + continue + } + row := stmtGetFileData.QueryRow(donk) + err = row.Scan(&media, &data) + if err != nil { + elog.Printf("error scanning file %s: %s", donk, err) + continue + } + w.Write(data) + } + zd.Close() + fd.Close() +} diff --git a/main.go b/main.go index 5e944d1..b35e6bf 100644 --- a/main.go +++ b/main.go @@ -64,6 +64,11 @@ func reexecArgs(cmd string) []string { var elog, ilog, dlog *golog.Logger +func errx(msg string, args ...interface{}) { + fmt.Fprintf(os.Stderr, msg+"\n", args...) + os.Exit(1) +} + func main() { flag.StringVar(&dataDir, "datadir", dataDir, "data directory") flag.StringVar(&viewDir, "viewdir", viewDir, "view directory") @@ -110,21 +115,32 @@ func main() { getconfig("usersep", &userSep) getconfig("honksep", &honkSep) getconfig("devel", &develMode) + if develMode { + gogglesDoNothing() + } getconfig("fasttimeout", &fastTimeout) getconfig("slowtimeout", &slowTimeout) - getconfig("signgets", &signGets) + getconfig("honkwindow", &honkwindow) + honkwindow *= 24 * time.Hour + prepareStatements(db) + switch cmd { case "admin": adminscreen() case "import": if len(args) != 4 { - elog.Fatal("import username mastodon|twitter srcdir") + errx("import username honk|mastodon|twitter srcdir") } importMain(args[1], args[2], args[3]) + case "export": + if len(args) != 3 { + errx("export username destdir") + } + export(args[1], args[2]) case "devel": if len(args) != 2 { - elog.Fatal("need an argument: devel (on|off)") + errx("need an argument: devel (on|off)") } switch args[1] { case "on": @@ -132,11 +148,11 @@ func main() { case "off": setconfig("devel", 0) default: - elog.Fatal("argument must be on or off") + errx("argument must be on or off") } case "setconfig": if len(args) != 3 { - elog.Fatal("need an argument: setconfig key val") + errx("need an argument: setconfig key val") } var val interface{} var err error @@ -148,66 +164,55 @@ func main() { adduser() case "deluser": if len(args) < 2 { - fmt.Printf("usage: honk deluser username\n") - return + errx("usage: honk deluser username") } deluser(args[1]) case "chpass": if len(args) < 2 { - fmt.Printf("usage: honk chpass username\n") - return + errx("usage: honk chpass username") } chpass(args[1]) case "follow": if len(args) < 3 { - fmt.Printf("usage: honk follow username url\n") - return + errx("usage: honk follow username url") } user, err := butwhatabout(args[1]) if err != nil { - fmt.Printf("user not found\n") - return + errx("user %s not found", args[1]) } var meta HonkerMeta mj, _ := jsonify(&meta) honkerid, err := savehonker(user, args[2], "", "presub", "", mj) if err != nil { - fmt.Printf("had some trouble with that: %s\n", err) - return + errx("had some trouble with that: %s", err) } followyou(user, honkerid, true) case "unfollow": if len(args) < 3 { - fmt.Printf("usage: honk unfollow username url\n") - return + errx("usage: honk unfollow username url") } user, err := butwhatabout(args[1]) if err != nil { - fmt.Printf("user not found\n") - return + errx("user not found") } row := db.QueryRow("select honkerid from honkers where xid = ? and userid = ? and flavor in ('sub')", args[2], user.ID) var honkerid int64 err = row.Scan(&honkerid) if err != nil { - fmt.Printf("sorry couldn't find them\n") - return + errx("sorry couldn't find them") } unfollowyou(user, honkerid, true) case "sendmsg": if len(args) < 4 { - fmt.Printf("usage: honk send username filename rcpt\n") - return + errx("usage: honk send username filename rcpt") } user, err := butwhatabout(args[1]) if err != nil { - fmt.Printf("user not found\n") - return + errx("user %s not found", args[1]) } data, err := os.ReadFile(args[2]) if err != nil { - fmt.Printf("can't read file\n") - return + errx("can't read file: %s", err) } deliverate(user.ID, args[3], data) case "cleanup": @@ -218,29 +223,25 @@ func main() { cleanupdb(arg) case "unplug": if len(args) < 2 { - fmt.Printf("usage: honk unplug servername\n") - return + errx("usage: honk unplug servername") } name := args[1] unplugserver(name) case "backup": if len(args) < 2 { - fmt.Printf("usage: honk backup dirname\n") - return + errx("usage: honk backup dirname") } name := args[1] svalbard(name) case "ping": if len(args) < 3 { - fmt.Printf("usage: honk ping (from username) (to username or url)\n") - return + errx("usage: honk ping (from username) (to username or url)") } name := args[1] targ := args[2] user, err := butwhatabout(name) if err != nil { - elog.Printf("unknown user") - return + errx("unknown user %s", name) } ping(user, targ) case "run": @@ -250,6 +251,6 @@ func main() { case "test": ElaborateUnitTests() default: - elog.Fatal("unknown command") + errx("unknown command") } } diff --git a/preflight.sh b/preflight.sh index 5ebc0ab..7aeb972 100644 --- a/preflight.sh +++ b/preflight.sh @@ -1,11 +1,11 @@ set -e -go version > /dev/null 2>&1 || (echo go 1.16+ is required && false) +go version > /dev/null 2>&1 || (echo go 1.18+ is required && false) v=`go version | egrep -o "go1\.[^.]+"` || echo failed to identify go version -if [ "$v" \< "go1.16" ] ; then +if [ "$v" \< "go1.18" ] ; then echo go version is too old: $v - echo go 1.16+ is required + echo go 1.18+ is required false fi diff --git a/schema.sql b/schema.sql index 1ce0071..6816987 100644 --- a/schema.sql +++ b/schema.sql @@ -15,6 +15,7 @@ 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_honkswhotwo on honks(whofore) where whofore = 2; create index idx_donkshonk on donks(honkid); create index idx_donkschonk on donks(chonkid); create index idx_honkerxid on honkers(xid); diff --git a/skulduggery.go b/skulduggery.go index c8f669c..1f90cef 100644 --- a/skulduggery.go +++ b/skulduggery.go @@ -29,6 +29,9 @@ func demoji(s string) string { zw := false for _, c := range s { + if c == '\n' { + continue + } if runewidth.RuneWidth(c) == 0 { zw = true break diff --git a/unveil.go b/unveil.go index cbce333..6ede087 100644 --- a/unveil.go +++ b/unveil.go @@ -1,5 +1,4 @@ //go:build openbsd -// +build openbsd // // Copyright (c) 2019 Ted Unangst diff --git a/upgradedb.go b/upgradedb.go index de6cb61..62f9d08 100644 --- a/upgradedb.go +++ b/upgradedb.go @@ -23,7 +23,7 @@ import ( "humungus.tedunangst.com/r/webs/htfilter" ) -var myVersion = 45 +var myVersion = 46 // idx whotwo type dbexecer interface { Exec(query string, args ...interface{}) (sql.Result, error) @@ -172,6 +172,11 @@ func upgradedb() { tx = nil fallthrough case 45: + try("create index idx_honkswhotwo on honks(whofore) where whofore = 2") + setV(46) + fallthrough + case 46: + try("analyze") default: elog.Fatalf("can't upgrade unknown version %d", dbversion) diff --git a/util.go b/util.go index f709513..13508fd 100644 --- a/util.go +++ b/util.go @@ -36,10 +36,8 @@ import ( "bufio" "crypto/rand" "crypto/rsa" - "crypto/sha512" "database/sql" "fmt" - "io/ioutil" "net" "os" "os/signal" @@ -52,24 +50,8 @@ import ( "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 diff --git a/views/about.html b/views/about.html index 65bae2e..9c48090 100644 --- a/views/about.html +++ b/views/about.html @@ -5,10 +5,10 @@

-
version:{{ .HonkVersion }} -
memory:{{ printf "%.02f" .Sensors.Memory }}MB -
uptime:{{ printf "%.02f" .Sensors.Uptime }}s -
cputime:{{ printf "%.02f" .Sensors.CPU }}s +
version:{{ .HonkVersion }} +
memory:{{ printf "%.02f" .Sensors.Memory }}MB +
uptime:{{ printf "%.02f" .Sensors.Uptime }}s +
cputime:{{ printf "%.02f" .Sensors.CPU }}s

diff --git a/views/header.html b/views/header.html index bd10995..ef927f2 100644 --- a/views/header.html +++ b/views/header.html @@ -1,7 +1,7 @@ -honk +{{ or .Title .ServerName }} {{ if .LocalStyleParam }} @@ -52,7 +52,7 @@

  • help
  • - +
    diff --git a/views/honkform.html b/views/honkform.html index b5eb4a3..b519529 100644 --- a/views/honkform.html +++ b/views/honkform.html @@ -9,8 +9,7 @@
    more options

    - - +


    {{ with .SavedPlace }} diff --git a/views/honkpage.html b/views/honkpage.html index cbb1565..15d4b96 100644 --- a/views/honkpage.html +++ b/views/honkpage.html @@ -18,8 +18,8 @@ {{ if and .HonkCSRF (not .IsPreview) }}

    -

    - +

    +

    {{ end }}
    diff --git a/views/honkpage.js b/views/honkpage.js index ec9d8db..8270149 100644 --- a/views/honkpage.js +++ b/views/honkpage.js @@ -82,7 +82,7 @@ var lehonkbutton = document.getElementById("honkingtime") function oldestnewest(btn) { var els = document.getElementsByClassName("glow") if (els.length) { - els[els.length-1].scrollIntoView() + els[els.length-1].scrollIntoView({ behavior: "smooth" }) } } function removeglow() { @@ -384,13 +384,13 @@ function hideelement(el) { if (!el) return el.style.display = "none" } -function updatedonker() { - var el = document.getElementById("donker") +function updatedonker(ev) { + var el = ev.target.parentElement el.children[1].textContent = el.children[0].value.slice(-20) - el = document.getElementById("donkdescriptor") - el.style.display = "" - el = document.getElementById("saveddonkxid") + el = el.nextSibling el.value = "" + el = el.parentElement.nextSibling + el.style.display = "" } var checkinprec = 100.0 var gpsoptions = { @@ -416,6 +416,76 @@ function fillcheckin() { }, gpsoptions) } } + +function scrollnexthonk() { + var honks = document.getElementsByClassName("honk"); + for (var i = 0; i < honks.length; i++) { + var h = honks[i]; + var b = h.getBoundingClientRect(); + if (b.top > 1.0) { + h.scrollIntoView() + var a = h.querySelector(".actions summary") + if (a) a.focus({ preventScroll: true }) + break + } + } +} + +function scrollprevioushonk() { + var honks = document.getElementsByClassName("honk"); + for (var i = 1; i < honks.length; i++) { + var b = honks[i].getBoundingClientRect(); + if (b.top > -1.0) { + honks[i-1].scrollIntoView() + var a = honks[i-1].querySelector(".actions summary") + if (a) a.focus({ preventScroll: true }) + break + } + } +} + +function hotkey(e) { + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) + return + if (e.ctrlKey || e.altKey) + return + + switch (e.code) { + case "KeyR": + refreshhonks(document.getElementById("honkrefresher")); + break; + case "KeyS": + oldestnewest(document.getElementById("newerscroller")); + break; + case "KeyJ": + scrollnexthonk(); + break; + case "KeyK": + scrollprevioushonk(); + break; + case "KeyM": + var menu = document.getElementById("topmenu") + if (!menu.open) { + menu.open = true + menu.querySelector("a").focus() + } else { + menu.open = false + } + break + case "Escape": + var menu = document.getElementById("topmenu") + menu.open = false + break + case "Slash": + document.getElementById("topmenu").open = true + document.getElementById("searchbox").focus() + e.preventDefault() + break + } +} + +document.addEventListener("keydown", hotkey) + function addemu(elem) { const data = elem.alt const box = document.getElementById("honknoise"); diff --git a/views/style.css b/views/style.css index cb2dae2..7c52719 100644 --- a/views/style.css +++ b/views/style.css @@ -389,9 +389,15 @@ li.details { display: none; } -.textright { +.text-left { + text-align: left; +} +.text-right { text-align: right; } +.text-center { + text-align: center; +} .font08em { font-size: 0.8em; diff --git a/web.go b/web.go index 905327c..2f40d26 100644 --- a/web.go +++ b/web.go @@ -17,11 +17,13 @@ package main import ( "bytes" + "crypto/sha512" "database/sql" "fmt" "html/template" "io" notrand "math/rand" + "mime/multipart" "net/http" "net/url" "os" @@ -36,6 +38,7 @@ import ( "github.com/gorilla/mux" "humungus.tedunangst.com/r/webs/cache" + "humungus.tedunangst.com/r/webs/gencache" "humungus.tedunangst.com/r/webs/httpsig" "humungus.tedunangst.com/r/webs/junk" "humungus.tedunangst.com/r/webs/login" @@ -81,11 +84,14 @@ func getInfo(r *http.Request) 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["MiscJSParam"] = getassetparam(viewDir + "/views/misc.js") templinfo["LocalJSParam"] = getassetparam(dataDir + "/views/local.js") - templinfo["MiscJSParam"] = getassetparam(dataDir + "/views/misc.js") templinfo["ServerName"] = serverName templinfo["IconName"] = iconName templinfo["UserSep"] = userSep + if r == nil { + return templinfo + } if u := login.GetUserInfo(r); u != nil { templinfo["UserInfo"], _ = butwhatabout(u.Username) templinfo["UserStyle"] = getuserstyle(u) @@ -96,9 +102,50 @@ func getInfo(r *http.Request) map[string]interface{} { return templinfo } +var oldnews = gencache.New(gencache.Options[string, []byte]{ + Fill: func(url string) ([]byte, bool) { + templinfo := getInfo(nil) + var honks []*Honk + var userid int64 = -1 + + templinfo["ServerMessage"] = serverMsg + switch url { + case "/events": + honks = geteventhonks(userid) + templinfo["ServerMessage"] = "some recent and upcoming events" + default: + templinfo["ShowRSS"] = true + honks = getpublichonks() + } + reverbolate(userid, honks) + templinfo["Honks"] = honks + templinfo["MapLink"] = getmaplink(nil) + var buf bytes.Buffer + err := readviews.Execute(&buf, "honkpage.html", templinfo) + if err != nil { + elog.Print(err) + } + return buf.Bytes(), true + + }, + Duration: 1 * time.Minute, +}) + +func lonelypage(w http.ResponseWriter, r *http.Request) { + page, _ := oldnews.Get(r.URL.Path) + if !develMode { + w.Header().Set("Cache-Control", "max-age=60") + } + w.Write(page) +} + func homepage(w http.ResponseWriter, r *http.Request) { - templinfo := getInfo(r) u := login.GetUserInfo(r) + if u == nil { + lonelypage(w, r) + return + } + templinfo := getInfo(r) var honks []*Honk var userid int64 = -1 @@ -449,7 +496,20 @@ func inbox(w http.ResponseWriter, r *http.Request) { addreaction(user, obj, who, content) } default: - go xonksaver(user, j, origin) + go saveandcheck(user, j, origin) + } +} + +func saveandcheck(user *WhatAbout, j junk.Junk, origin string) { + xonk := xonksaver(user, j, origin) + if xonk == nil { + return + } + if sname := shortname(user.ID, xonk.Honker); sname == "" { + dlog.Printf("received unexpected activity from %s", xonk.Honker) + if xonk.Whofore == 0 { + dlog.Printf("it's not even for me!") + } } } @@ -1065,9 +1125,16 @@ func threadsort(honks []*Honk) []*Honk { } p.Style += fmt.Sprintf(" level%d", level) childs := kids[p.XID] - sort.SliceStable(childs, func(i, j int) bool { - return sameperson(childs[i], p) && !sameperson(childs[j], p) - }) + if false { + sort.SliceStable(childs, func(i, j int) bool { + return sameperson(childs[i], p) && !sameperson(childs[j], p) + }) + } + if true { + sort.SliceStable(childs, func(i, j int) bool { + return !sameperson(childs[i], p) && sameperson(childs[j], p) + }) + } for _, h := range childs { if !done[h] { done[h] = true @@ -1177,10 +1244,10 @@ func showonehonk(w http.ResponseWriter, r *http.Request) { //reversehonks(rawhonks) rawhonks = threadsort(rawhonks) var honks []*Honk - for _, h := range rawhonks { + for i, h := range rawhonks { if h.XID == xid { templinfo["Honkology"] = honkology(h) - if len(honks) != 0 { + if i > 0 { h.Style += " glow" } } @@ -1548,11 +1615,15 @@ func edithonkpage(w http.ResponseWriter, r *http.Request) { templinfo["Duration"] = tm.Duration } } - templinfo["ServerMessage"] = "honk edit 2" + templinfo["ServerMessage"] = "honk edit" templinfo["IsPreview"] = true templinfo["UpdateXID"] = honk.XID if len(honk.Donks) > 0 { - templinfo["SavedFile"] = honk.Donks[0].XID + var savedfiles []string + for _, d := range honk.Donks { + savedfiles = append(savedfiles, fmt.Sprintf("%s:%d", d.XID, d.FileID)) + } + templinfo["SavedFile"] = strings.Join(savedfiles, ",") } err := readviews.Execute(w, "honkpage.html", templinfo) if err != nil { @@ -1592,11 +1663,26 @@ func canedithonk(user *WhatAbout, honk *Honk) bool { return true } -func submitdonk(w http.ResponseWriter, r *http.Request) (*Donk, error) { +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") + var donks []*Donk + for i, hdr := range r.MultipartForm.File["donk"] { + if i > 16 { + break + } + donk, err := formtodonk(w, r, hdr) + if err != nil { + return nil, err + } + donks = append(donks, donk) + } + return donks, nil +} + +func formtodonk(w http.ResponseWriter, r *http.Request, filehdr *multipart.FileHeader) (*Donk, error) { + file, err := filehdr.Open() if err != nil { if err == http.ErrMissingFile { return nil, nil @@ -1610,7 +1696,7 @@ func submitdonk(w http.ResponseWriter, r *http.Request) (*Donk, error) { file.Close() data := buf.Bytes() var media, name string - img, err := shrinkit(data) + img, err := bigshrink(data) if err == nil { data = img.Data format := img.Format @@ -1770,7 +1856,7 @@ func submithonk(w http.ResponseWriter, r *http.Request) *Honk { honk.Precis = "re: " + honk.Precis } } - } else { + } else if updatexid == "" { honk.Audience = []string{thewholeworld} } if honk.Noise != "" && honk.Noise[0] == '@' { @@ -1792,18 +1878,26 @@ func submithonk(w http.ResponseWriter, r *http.Request) *Honk { honk.Public = loudandproud(honk.Audience) honk.Convoy = convoy - donkxid := r.FormValue("donkxid") + donkxid := strings.Join(r.Form["donkxid"], ",") if donkxid == "" { - d, err := submitdonk(w, r) + donks, err := submitdonk(w, r) if err != nil && err != http.ErrMissingFile { return nil } - if d != nil { - honk.Donks = append(honk.Donks, d) - donkxid = fmt.Sprintf("%s:%d", d.XID, d.FileID) + if len(donks) > 0 { + honk.Donks = append(honk.Donks, donks...) + var xids []string + for _, d := range honk.Donks { + xids = append(xids, fmt.Sprintf("%s:%d", d.XID, d.FileID)) + } + donkxid = strings.Join(xids, ",") } } else { - for _, xid := range r.Form["donkxid"] { + xids := strings.Split(donkxid, ",") + for i, xid := range xids { + if i > 16 { + break + } p := strings.Split(xid, ":") xid = p[0] url := fmt.Sprintf("https://%s/d/%s", serverName, xid) @@ -1972,12 +2066,12 @@ func submitchonk(w http.ResponseWriter, r *http.Request) { Noise: noise, Format: format, } - d, err := submitdonk(w, r) + donks, err := submitdonk(w, r) if err != nil && err != http.ErrMissingFile { return } - if d != nil { - ch.Donks = append(ch.Donks, d) + if len(donks) > 0 { + ch.Donks = append(ch.Donks, donks...) } translatechonk(&ch) @@ -2485,15 +2579,16 @@ func apihandler(w http.ResponseWriter, r *http.Request) { } fmt.Fprintf(w, "%s", h.XID) case "donk": - d, err := submitdonk(w, r) + donks, err := submitdonk(w, r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - if d == nil { + if len(donks) == 0 { http.Error(w, "missing donk", http.StatusBadRequest) return } + d := donks[0] donkxid := fmt.Sprintf("%s:%d", d.XID, d.FileID) w.Write([]byte(donkxid)) case "zonkit": @@ -2643,6 +2738,22 @@ func emuinit() { }) } +var savedassetparams = make(map[string]string) + +func getassetparam(file string) string { + if p, ok := savedassetparams[file]; ok { + return p + } + data, err := os.ReadFile(file) + if err != nil { + return "" + } + hasher := sha512.New() + hasher.Write(data) + + return fmt.Sprintf("?v=%.8x", hasher.Sum(nil)) +} + func serve() { db := opendatabase() login.Init(login.InitArgs{Db: db, Logger: ilog, Insecure: develMode, SameSiteStrict: !develMode}) @@ -2685,6 +2796,7 @@ func serve() { viewDir + "/views/style.css", dataDir + "/views/local.css", viewDir + "/views/honkpage.js", + viewDir + "/views/misc.js", dataDir + "/views/local.js", } for _, s := range assets {