Merge remote-tracking branch 'localhonk/master' into masto
jump to
@@ -8,7 +8,7 @@
Files without explicit licenses and the conglomeration as a whole is subject to the license below. -// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// Copyright (c) 2019-2024 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
@@ -8,4 +8,6 @@ The gob encoding for backend rpc uses more memory than needed.
A custom encoding could reduce allocations. Maybe the backend could fetch the data itself. - +Several columns and indices have potentially long shared prefixes. + These could be stored rearranged, perhaps with last four bytes prepended. + https://here/there/9876 -> 9876https://here/there/9876
@@ -285,6 +285,7 @@ return donk
} ilog.Printf("saving donk: %s", url) data := []byte{} + var meta DonkMeta if localize { fn := func() (interface{}, error) { return fetchsome(url)@@ -309,6 +310,8 @@ data = []byte{}
goto saveit } data = img.Data + meta.Width = img.Width + meta.Height = img.Height media = "image/" + img.Format } else if media == "application/pdf" { if len(data) > 1000000 {@@ -321,9 +324,10 @@ ilog.Printf("not saving large attachment")
localize = false data = []byte{} } + meta.Length = len(data) } saveit: - fileid, err := savefile(name, desc, url, media, localize, data) + fileid, err := savefile(name, desc, url, media, localize, data, &meta) if err != nil { elog.Printf("error saving file %s: %s", url, err) return nil@@ -583,11 +587,17 @@ var re_roma1ink = regexp.MustCompile(`https://[[:alnum:].]+/notice/[[:alnum:]]+`)
var re_qtlinks = regexp.MustCompile(`>https://[^\s<]+<`) func xonksaver(user *WhatAbout, item junk.Junk, origin string) *Honk { + return xonksaver2(user, item, origin, false) +} +func xonksaver2(user *WhatAbout, item junk.Junk, origin string, myown bool) *Honk { depth := 0 maxdepth := 10 currenttid := "" goingup := 0 - var xonkxonkfn func(junk.Junk, string, bool, string) *Honk + var xonkxonkfn2 func(junk.Junk, string, bool, string, bool) *Honk + xonkxonkfn := func(item junk.Junk, origin string, isUpdate bool, bonker string) *Honk { + return xonkxonkfn2(item, origin, isUpdate, bonker, false) + } qutify := func(user *WhatAbout, qurl, content string) string { if depth >= maxdepth {@@ -658,7 +668,7 @@ }
xonkxonkfn(obj, originate(xid), false, "") } - xonkxonkfn = func(item junk.Junk, origin string, isUpdate bool, bonker string) *Honk { + xonkxonkfn2 = func(item junk.Junk, origin string, isUpdate bool, bonker string, myown bool) *Honk { id, _ := item.GetString("id") what := firstofmany(item, "type") dt, ok := item.GetString("published")@@ -744,7 +754,6 @@ ilog.Printf("out of bounds actor in bonk: %s not from %s", bonker, origin)
item.Write(ilog.Writer()) return nil } - bonker, _ = item.GetString("actor") origin = originate(xid) if ok && originate(id) == origin { dlog.Printf("using object in announce for %s", xid)@@ -778,7 +787,7 @@ if obj == nil {
ilog.Printf("no object for creation %s", id) return nil } - return xonkxonkfn(obj, origin, isUpdate, bonker) + return xonkxonkfn2(obj, origin, isUpdate, bonker, myown) case "Read": xid, ok = item.GetString("object") if ok {@@ -839,6 +848,10 @@ case "ChatMessage":
bonker = "" obj = item what = "chonk" + case "Like": + return nil + case "Dislike": + return nil default: ilog.Printf("unknown activity: %s", what) dumpactivity(item)@@ -873,6 +886,11 @@ xonk.Honker, _ = item.GetString("actor")
if xonk.Honker == "" { xonk.Honker = extractattrto(item) } + if myown && xonk.Honker != user.URL { + ilog.Printf("not allowing local impersonation: %s <> %s", xonk.Honker, user.URL) + item.Write(ilog.Writer()) + return nil + } if originate(xonk.Honker) != origin { ilog.Printf("out of bounds honker %s from %s", xonk.Honker, origin) item.Write(ilog.Writer())@@ -1190,9 +1208,17 @@ }
xonk.Format = "html" xonk.Convoy = convoy xonk.Mentions = mentions - for _, m := range mentions { - if m.Where == user.URL { - xonk.Whofore = 1 + if myown { + if xonk.Public { + xonk.Whofore = 2 + } else { + xonk.Whofore = 3 + } + } else { + for _, m := range mentions { + if m.Where == user.URL { + xonk.Whofore = 1 + } } } imaginate(&xonk)@@ -1243,7 +1269,7 @@ xonk.ID = prev.ID
updatehonk(&xonk) } } - if !isUpdate && needxonk(user, &xonk) { + if !isUpdate && (myown || needxonk(user, &xonk)) { if rid != "" && xonk.Public { if needxonkid(user, rid) { goingup++@@ -1278,7 +1304,7 @@ }
return &xonk } - return xonkxonkfn(item, origin, false, "") + return xonkxonkfn2(item, origin, false, "", myown) } func dumpactivity(item junk.Junk) {@@ -1365,9 +1391,9 @@ 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["actor"] = h.Honker j["published"] = dt - if h.Public { + if h.Public && h.Honker == user.URL { h.Audience = append(h.Audience, user.URL+"/followers") } j["to"] = h.Audience[0]@@ -1394,7 +1420,7 @@ jo["updated"] = dt
} jo["published"] = dt jo["url"] = h.XID - jo["attributedTo"] = user.URL + jo["attributedTo"] = h.Honker if h.RID != "" { jo["inReplyTo"] = h.RID }@@ -1409,7 +1435,6 @@ }
if !h.Public { jo["directMessage"] = true } - h.Noise = re_retag.ReplaceAllString(h.Noise, "") translate(h) redoimages(h) if h.Precis != "" {
@@ -1,3 +1,18 @@
+// +// Copyright (c) 2020 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 (
@@ -1,5 +1,5 @@
// -// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// Copyright (c) 2023 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
@@ -0,0 +1,231 @@
+package main + +import ( + "fmt" + "os" + "strconv" +) + +type cmd struct { + help string + callback func(args []string) +} + +var commands = map[string]cmd{ + "init": { + help: "initialize honk", + callback: func(args []string) { + initdb() + }, + }, + "upgrade": { + help: "upgrade honk", + callback: func(args []string) { + upgradedb() + }, + }, + "version": { + help: "print version", + callback: func(args []string) { + fmt.Println(softwareVersion) + os.Exit(0) + }, + }, + "admin": { + help: "admin interface", + callback: func(args []string) { + adminscreen() + }, + }, + "import": { + help: "import data into honk", + callback: func(args []string) { + if len(args) != 4 { + errx("import username honk|mastodon|twitter srcdir") + } + importMain(args[1], args[2], args[3]) + }, + }, + "export": { + help: "export data from honk", + callback: func(args []string) { + if len(args) != 3 { + errx("export username destdir") + } + export(args[1], args[2]) + }, + }, + "devel": { + help: "turn devel on/off", + callback: func(args []string) { + if len(args) != 2 { + errx("need an argument: devel (on|off)") + } + switch args[1] { + case "on": + setconfig("devel", 1) + case "off": + setconfig("devel", 0) + default: + errx("argument must be on or off") + } + }, + }, + "setconfig": { + help: "set honk config", + callback: func(args []string) { + if len(args) != 3 { + errx("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) + }, + }, + "adduser": { + help: "add a user to honk", + callback: func(args []string) { + adduser() + }, + }, + "deluser": { + help: "delete a user from honk", + callback: func(args []string) { + if len(args) < 2 { + errx("usage: honk deluser username") + } + deluser(args[1]) + }, + }, + "chpass": { + help: "change password of an account", + callback: func(args []string) { + if len(args) < 2 { + errx("usage: honk chpass username") + } + chpass(args[1]) + }, + }, + "follow": { + help: "follow an account", + callback: func(args []string) { + if len(args) < 3 { + errx("usage: honk follow username url") + } + user, err := butwhatabout(args[1]) + if err != nil { + errx("user %s not found", args[1]) + } + var meta HonkerMeta + mj, _ := jsonify(&meta) + honkerid, flavor, err := savehonker(user, args[2], "", "presub", "", mj) + if err != nil { + errx("had some trouble with that: %s", err) + } + if flavor == "presub" { + followyou(user, honkerid, true) + } + }, + }, + "unfollow": { + help: "unfollow an account", + callback: func(args []string) { + if len(args) < 3 { + errx("usage: honk unfollow username url") + } + user, err := butwhatabout(args[1]) + if err != nil { + errx("user not found") + } + + honkerid, err := gethonker(user.ID, args[2]) + if err != nil { + errx("sorry couldn't find them") + } + unfollowyou(user, honkerid, true) + }, + }, + "sendmsg": { + help: "send a raw activity", + callback: func(args []string) { + if len(args) < 4 { + errx("usage: honk sendmsg username filename rcpt") + } + user, err := butwhatabout(args[1]) + if err != nil { + errx("user %s not found", args[1]) + } + data, err := os.ReadFile(args[2]) + if err != nil { + errx("can't read file: %s", err) + } + deliverate(user.ID, args[3], data) + }, + }, + "cleanup": { + help: "clean up stale data from database", + callback: func(args []string) { + arg := "30" + if len(args) > 1 { + arg = args[1] + } + cleanupdb(arg) + }, + }, + "unplug": { + help: "disconnect from a dead server", + callback: func(args []string) { + if len(args) < 2 { + errx("usage: honk unplug servername") + } + name := args[1] + unplugserver(name) + }, + }, + "backup": { + help: "backup honk", + callback: func(args []string) { + if len(args) < 2 { + errx("usage: honk backup dirname") + } + name := args[1] + svalbard(name) + }, + }, + "ping": { + help: "ping from user to user/url", + callback: func(args []string) { + if len(args) < 3 { + errx("usage: honk ping (from username) (to username or url)") + } + name := args[1] + targ := args[2] + user, err := butwhatabout(name) + if err != nil { + errx("unknown user %s", name) + } + ping(user, targ) + }, + }, + "run": { + help: "run honk", + callback: func(args []string) { + serve() + }, + }, + "backend": { + help: "run backend", + callback: func(args []string) { + backendServer() + }, + }, + "test": { + help: "run test", + callback: func(args []string) { + ElaborateUnitTests() + }, + }, +}
@@ -103,6 +103,15 @@ }
return user } +func gethonker(userid UserID, xid string) (int64, error) { + row := opendatabase(). + QueryRow("select honkerid from honkers where xid = ? and userid = ? and flavor in ('sub')", xid, userid) + var honkerid int64 + + err := row.Scan(&honkerid) + return honkerid, err +} + func butwhatabout(name string) (*WhatAbout, error) { user, ok := somenamedusers.Get(name) if !ok {@@ -292,9 +301,8 @@ rows, err := stmtHonksByCombo.Query(wanted, userid, userid, combo, userid, wanted, userid, combo, userid)
return getsomehonks(rows, err) } func gethonksbyconvoy(userid UserID, convoy string, wanted int64) []*Honk { - rows, err := stmtHonksByConvoy.Query(wanted, userid, userid, convoy) - honks := getsomehonks(rows, err) - return honks + rows, err := stmtHonksByConvoy.Query(convoy, wanted, userid) + return getsomehonks(rows, err) } func gethonksbysearch(userid UserID, q string, wanted int64) []*Honk { var queries []string@@ -387,7 +395,7 @@ elog.Printf("error querying honks: %s", err)
return nil } defer rows.Close() - var honks []*Honk + honks := make([]*Honk, 0, 64) for rows.Next() { h := scanhonk(rows) if h != nil {@@ -422,15 +430,15 @@ }
func donksforhonks(honks []*Honk) { db := opendatabase() - var ids []string - hmap := make(map[int64]*Honk) + ids := make([]string, 0, len(honks)) + hmap := make(map[int64]*Honk, len(honks)) 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) + q := fmt.Sprintf("select honkid, donks.fileid, xid, name, description, url, media, local, meta 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)@@ -439,12 +447,14 @@ }
defer rows.Close() for rows.Next() { var hid int64 + var j string d := new(Donk) - err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.Desc, &d.URL, &d.Media, &d.Local) + err = rows.Scan(&hid, &d.FileID, &d.XID, &d.Name, &d.Desc, &d.URL, &d.Media, &d.Local, &j) if err != nil { elog.Printf("error scanning donk: %s", err) continue } + unjsonify(j, &d.Meta) d.External = !strings.HasPrefix(d.URL, serverPrefix) h := hmap[hid] h.Donks = append(h.Donks, d)@@ -536,8 +546,8 @@ }
func donksforchonks(chonks []*Chonk) { db := opendatabase() - var ids []string - chmap := make(map[int64]*Chonk) + ids := make([]string, 0, len(chonks)) + chmap := make(map[int64]*Chonk, len(chonks)) for _, ch := range chonks { ids = append(ids, fmt.Sprintf("%d", ch.ID)) chmap[ch.ID] = ch@@ -564,8 +574,8 @@ 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) +func savefile(name string, desc string, url string, media string, local bool, data []byte, meta *DonkMeta) (int64, error) { + fileid, _, err := savefileandxid(name, desc, url, media, local, data, meta) return fileid, err }@@ -575,7 +585,7 @@ 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) { +func savefileandxid(name string, desc string, url string, media string, local bool, data []byte, meta *DonkMeta) (int64, string, error) { var xid string if local { hash := hashfiledata(data)@@ -608,7 +618,11 @@ url = serverURL("/d/%s", xid)
} } - res, err := stmtSaveFile.Exec(xid, name, desc, url, media, local) + j := "{}" + if meta != nil { + j, _ = jsonify(meta) + } + res, err := stmtSaveFile.Exec(xid, name, desc, url, media, local, j) if err != nil { return 0, "", err }@@ -651,6 +665,7 @@ if err != nil {
elog.Printf("can't begin tx: %s", err) return err } + defer tx.Rollback() res, err := tx.Stmt(stmtSaveChonk).Exec(ch.UserID, ch.XID, ch.Who, ch.Target, dt, ch.Noise, ch.Format) if err == nil {@@ -664,8 +679,6 @@ }
} chatplusone(tx, ch.UserID) err = tx.Commit() - } else { - tx.Rollback() } return err }@@ -850,6 +863,7 @@ if err != nil {
elog.Printf("can't begin tx: %s", err) return err } + defer tx.Rollback() plain := h.Plain() res, err := tx.Stmt(stmtSaveHonk).Exec(h.UserID, h.What, h.Honker, h.XID, h.RID, dt, h.URL,@@ -865,8 +879,6 @@ dlog.Printf("another one for me: %s", h.XID)
meplusone(tx, h.UserID) } err = tx.Commit() - } else { - tx.Rollback() } if err != nil { elog.Printf("error saving honk: %s", err)@@ -886,6 +898,7 @@ if err != nil {
elog.Printf("can't begin tx: %s", err) return err } + defer tx.Rollback() plain := h.Plain() err = deleteextras(tx, h.ID, false)@@ -907,8 +920,6 @@ }
} if err == nil { err = tx.Commit() - } else { - tx.Rollback() } if err != nil { elog.Printf("error updating honk %d: %s", h.ID, err)@@ -923,6 +934,7 @@ if err != nil {
elog.Printf("can't begin tx: %s", err) return err } + defer tx.Rollback() err = deleteextras(tx, honkid, true) if err == nil {@@ -930,8 +942,6 @@ _, 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)@@ -1027,7 +1037,10 @@ }
h.Badonks = append(h.Badonks, Badonk{Who: who, What: react}) j, _ := jsonify(h.Badonks) db := opendatabase() - tx, _ := db.Begin() + tx, err := db.Begin() + if err != nil { + return + } _, _ = tx.Stmt(stmtDeleteOneMeta).Exec(h.ID, "badonks") _, _ = tx.Stmt(stmtSaveMeta).Exec(h.ID, "badonks", j) tx.Commit()@@ -1126,6 +1139,9 @@ elog.Print(err)
return 0, "", err } honkerid, _ := res.LastInsertId() + if strings.HasSuffix(url, ".rss") { + go syndicate(user, url) + } return honkerid, flavor, nil }@@ -1346,7 +1362,15 @@ 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) + stmtHonksByConvoy = preparetodie(db, `with recursive getthread(x, c) as ( + values('', ?) + union + select xid, convoy from honks, getthread where honks.convoy = getthread.c + union + select xid, convoy from honks, getthread where honks.rid <> '' and honks.rid = getthread.x + union + select rid, convoy from honks, getthread where honks.xid = getthread.x and rid <> '' + ) `+selecthonks+"where honks.honkid > ? and honks.userid = ? and xid in (select x from getthread)"+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 (?, ?, ?)")@@ -1360,7 +1384,7 @@ 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 (?, ?, ?, ?, ?, ?)") + stmtSaveFile = preparetodie(db, "insert into filemeta (xid, name, description, url, media, local, meta) values (?, ?, ?, ?, ?, ?, ?)") g_blobdb = openblobdb() stmtSaveFileData = preparetodie(g_blobdb, "insert into filedata (xid, media, hash, content) values (?, ?, ?, ?)") stmtCheckFileData = preparetodie(g_blobdb, "select xid from filedata where hash = ?")
@@ -1,5 +1,5 @@
// -// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// Copyright (c) 2019,2023 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
@@ -60,6 +60,86 @@ + Fix handling of svg with bom fucks.
+ FastCGI listening. ++ Experimental support for C2S activities. + ++ Try harder to retrieve threads from the database. + +### 1.3.1 Retooled Reticule + ++ Some logging fixes. + ++ Ignore more activities that we don't care about. + ++ Fix filter matching. + ++ More documentation examples. + ++ Retain advanced metadata in preview. + ++ Alphabetical jump links for tags and honkers. + ++ Poll rss when adding instead of waiting a full cycle. + +### 1.3.0 Big Bonsai + ++ Some performance tuning. + ++ Follow .rss feeds for hashtags. + ++ Easier to inline attached images. + ++ Optional .json urls for activities. + ++ cc, link, name, and tags fields for advanced metadata. + ++ <big> tag supported in html. + +### 1.2.3 Regarded Reflection + ++ Don't serve attachments to clients expecting activities. + +### 1.2.2 Nameless Neophyte + ++ Ensure fetched activities are compatible content types. + ++ Some federation interop improvements. + ++ More complete API support. + ++ More compact image arrangement. + +### 1.2.1 Solipsist Satisfaction + ++ Federation reliability and compat improvements. + ++ Fix 32 bit support. + ++ Close databases to give the wal file a chance to checkpoint. + ++ Dim images in darkmode. + +- Remove the hoot: feature. The bird is dead. + +### 1.2.0 Forgotten Followup + ++ Filter option to match unknown actors. + ++ Update some dependencies. + ++ Watch local.css for changes. + ++ MacOS support. lol. + ++ Wait for requests to drain on shutdown. + ++ Handle quoteUrl property. + ++ Reroute memes to donks in emergencies. + ++ Fix handling of svg with bom fucks. + ++ FastCGI listening. + + Finally fix slow public queries. ### 1.1.1 Required Refinement
@@ -96,8 +96,13 @@ .Pp
An optional expiration may be specified as a duration. XdYhZm for X days, Y hours, and Z minutes. .Sh EXAMPLES -A rudimentary spam filter may be constructed with where set to mastodon.social, -checking the only unknowns option, with an action of reject. +A rudimentary spam filter to reject randos shilling their discord. +It will expire after two days. +.Lk filter.png screenshot of filter +.Pp +Following a group my result in the timeline being flooded with replies. +These may also be rejected. +.Lk filtermemes.png screenshot of filter .Sh SEE ALSO .Xr honk 1 .Sh CAVEATS
@@ -77,6 +77,9 @@ 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. +.Lk followhonk.png screenshot of adding honker +.Lk tagrss.png screenshot of adding honker +.Lk tagcombo.png screenshot of adding honker .Ss Viewing The primary feed is accessed via the .Pa home
@@ -62,11 +62,15 @@ 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 ".". +Also set by +.Ev HONK_DATADIR . .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 ".". +Also set by +.Ev HONK_VIEWDIR . .El .Pp The following options control log output.
@@ -1,3 +1,18 @@
+// +// Copyright (c) 2024 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 (
@@ -109,7 +109,6 @@ if h.Whofore == 2 || h.Whofore == 3 {
local = true } if local && h.What != "bonked" { - h.Noise = re_retag.ReplaceAllString(h.Noise, "") h.Noise = re_memes.ReplaceAllString(h.Noise, "") } h.Username, h.Handle = handles(h.Honker)@@ -512,7 +511,6 @@ var re_memes = regexp.MustCompile("meme: ?([^\n]+)")
var re_avatar = regexp.MustCompile("avatar: ?([^\n]+)") var re_banner = regexp.MustCompile("banner: ?([^\n]+)") var re_convoy = regexp.MustCompile("convoy: ?([^\n]+)") -var re_retag = regexp.MustCompile("tags: ?([^\n]+)") var re_convalidate = regexp.MustCompile("^(https?|tag|data):") func memetize(honk *Honk) {@@ -532,7 +530,7 @@ ct := http.DetectContentType(peek[:n])
fd.Close() url := serverURL("/meme/%s", name) - fileid, err := savefile(name, name, url, ct, false, nil) + fileid, err := savefile(name, name, url, ct, false, nil, nil) if err != nil { elog.Printf("error saving meme: %s", err) return x@@ -548,24 +546,6 @@ honk.Donks = append(honk.Donks, d)
return "" } honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl) -} - -func recategorize(honk *Honk) { - repl := func(x string) string { - x = x[5:] - for _, t := range strings.Split(x, " ") { - if t == "" { - continue - } - if t[0] != '#' { - t = "#" + t - } - dlog.Printf("hashtag: %s", t) - honk.Onts = append(honk.Onts, t) - } - return "" - } - honk.Noise = re_retag.ReplaceAllStringFunc(honk.Noise, repl) } var re_quickmention = regexp.MustCompile("(^|[ \n])@[[:alnum:]_]+([ \n:;.,']|$)")@@ -730,6 +710,11 @@ seen[s] = true
a[j] = s j++ } + } + if j < len(a)/2 { + rv := make([]string, j) + copy(rv, a[:j]) + return rv } return a[:j] }
@@ -3,15 +3,12 @@
go 1.18 require ( - github.com/dustin/go-humanize v1.0.1 - github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 - github.com/jmoiron/sqlx v1.3.5 github.com/mattn/go-runewidth v0.0.15 github.com/mmcdole/gofeed v1.2.1 golang.org/x/crypto v0.19.0 golang.org/x/net v0.21.0 - humungus.tedunangst.com/r/go-sqlite3 v1.1.3 + humungus.tedunangst.com/r/go-sqlite3 v1.2.1 humungus.tedunangst.com/r/gonix v0.1.4 humungus.tedunangst.com/r/webs v0.7.10 )@@ -19,7 +16,6 @@
require ( github.com/PuerkitoBio/goquery v1.8.0 // indirect github.com/andybalholm/cascadia v1.3.1 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mmcdole/goxpp v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -5,27 +5,13 @@ github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s= github.com/mmcdole/gofeed v1.2.1/go.mod h1:2wVInNpgmC85q16QTTuwbuKxtKkHLCDDtf0dCmnrNr4= github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI=@@ -60,8 +46,8 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -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/go-sqlite3 v1.2.1 h1:Hgos3bSVcsw0RQtvrqIJ12iUheRoVT4GAyUjho04tmI= +humungus.tedunangst.com/r/go-sqlite3 v1.2.1/go.mod h1:YrRIH0O7uePPLbJriXrER44ym5aQ0QxK8CnaT/GWOkg= humungus.tedunangst.com/r/gonix v0.1.4 h1:FuvWYQlFIzmfHxfvIfq5SYpSiHhFcpJqq3pi+w45s78= humungus.tedunangst.com/r/gonix v0.1.4/go.mod h1:VFBc2bPDXr1ayHOmHUutxYu8fSM+pkwK8o36h4rkORg= humungus.tedunangst.com/r/webs v0.7.10 h1:DPEsA7DU1P1uOBWYrhJWjqDtll6SGJkWQtJ/2N6P8DI=
@@ -114,6 +114,7 @@ if filt.Expiration.Before(now) {
continue } if expflush.IsZero() || filt.Expiration.Before(expflush) { + dlog.Printf("filter expired: %s", filt.Name) expflush = filt.Expiration } }@@ -184,6 +185,7 @@ return filtmap, true
} func filtcacheclear(userid UserID, dur time.Duration) { + dlog.Printf("clearing filters in %s", dur.String()) time.Sleep(dur + time.Second) filtInvalidator.Clear(userid) }@@ -250,13 +252,14 @@
func rejectactor(userid UserID, actor string) bool { filts := rejectfilters(userid, actor) for _, f := range filts { - if f.IsAnnounce { + if f.IsAnnounce || f.IsReply { continue } - if f.Actor == actor { - ilog.Printf("rejecting actor: %s", actor) - return true + if f.Text != "" { + continue } + ilog.Printf("rejecting actor: %s", actor) + return true } origin := originate(actor) if origin == "" {
@@ -203,6 +203,7 @@ if !loudandproud(honk.Audience) {
honk.Whofore = 3 } for _, att := range toot.Object.Attachment { + var meta DonkMeta switch att.Type { case "Document": fname := fmt.Sprintf("%s/%s", source, att.Url)@@ -215,7 +216,7 @@ 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) + fileid, err := savefile(name, desc, newurl, att.MediaType, true, data, &meta) if err != nil { elog.Printf("error saving media: %s", fname) continue@@ -389,7 +390,7 @@ } `json:"tweet"`
} var tweets []*Tweet - fd, err := os.Open(source + "/tweets.js") + fd, err := os.Open(source + "/tweet.js") if err != nil { elog.Fatal(err) }@@ -458,10 +459,11 @@ for _, r := range t.Tweet.Entities.Urls {
noise = strings.Replace(noise, r.URL, r.ExpandedURL, -1) } for _, m := range t.Tweet.Entities.Media { + var meta DonkMeta u := m.MediaURL idx := strings.LastIndexByte(u, '/') u = u[idx+1:] - fname := fmt.Sprintf("%s/tweets_media/%s-%s", source, t.Tweet.IdStr, u) + fname := fmt.Sprintf("%s/tweet_media/%s-%s", source, t.Tweet.IdStr, u) data, err := ioutil.ReadFile(fname) if err != nil { elog.Printf("error reading media: %s", fname)@@ -469,7 +471,7 @@ continue
} newurl := fmt.Sprintf("https://%s/d/%s", serverName, u) - fileid, err := savefile(u, u, newurl, "image/jpg", true, data) + fileid, err := savefile(u, u, newurl, "image/jpg", true, data, &meta) if err != nil { elog.Printf("error saving media: %s", fname) continue@@ -540,6 +542,7 @@ Public: true,
Whofore: 2, } { + var meta DonkMeta u := xfiltrate() fname := fmt.Sprintf("%s/%s", source, g.URI) data, err := ioutil.ReadFile(fname)@@ -549,7 +552,7 @@ continue
} newurl := fmt.Sprintf("https://%s/d/%s", serverName, u) - fileid, err := savefile(u, u, newurl, "image/jpg", true, data) + fileid, err := savefile(u, u, newurl, "image/jpg", true, data, &meta) if err != nil { elog.Printf("error saving media: %s", fname) continue
@@ -24,7 +24,8 @@ "log/syslog"
notrand "math/rand" "os" "runtime/pprof" - "strconv" + "sort" + "strings" "time" "humungus.tedunangst.com/r/webs/log"@@ -83,9 +84,29 @@ var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
var memprofile = flag.String("memprofile", "", "write memory profile to this file") var memprofilefd *os.File +func usage() { + flag.PrintDefaults() + out := flag.CommandLine.Output() + fmt.Fprintf(out, "\n available honk commands:\n") + var msgs []string + for n, c := range commands { + msgs = append(msgs, fmt.Sprintf(" %s: %s\n", n, c.help)) + } + sort.Strings(msgs) + fmt.Fprintf(out, "%s", strings.Join(msgs, "")) +} + func main() { - flag.StringVar(&dataDir, "datadir", dataDir, "data directory") - flag.StringVar(&viewDir, "viewdir", viewDir, "view directory") + commands["help"] = cmd{ + help: "you're looking at it", + callback: func(args []string) { + usage() + }, + } + flag.StringVar(&dataDir, "datadir", getenv("HONK_DATADIR", dataDir), "data directory") + flag.StringVar(&viewDir, "viewdir", getenv("HONK_VIEWDIR", viewDir), "view directory") + flag.Usage = usage + flag.Parse() if *cpuprofile != "" { f, err := os.Create(*cpuprofile)@@ -118,12 +139,11 @@ cmd = args[0]
} switch cmd { case "init": - initdb() + commands["init"].callback(args) case "upgrade": - upgradedb() + commands["upgrade"].callback(args) case "version": - fmt.Println(softwareVersion) - os.Exit(0) + commands["version"].callback(args) } db := opendatabase() dbversion := 0@@ -153,140 +173,10 @@ honkwindow *= 24 * time.Hour
prepareStatements(db) - dbx := opendatabasex() - if dbx == nil { - elog.Fatal("dbx is nil") + c, ok := commands[cmd] + if !ok { + errx("don't know about %q", cmd) } - prepareStatementsx(dbx) - switch cmd { - case "admin": - adminscreen() - case "import": - if len(args) != 4 { - 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 { - errx("need an argument: devel (on|off)") - } - switch args[1] { - case "on": - setconfig("devel", 1) - case "off": - setconfig("devel", 0) - default: - errx("argument must be on or off") - } - case "setconfig": - if len(args) != 3 { - errx("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 { - errx("usage: honk deluser username") - } - deluser(args[1]) - case "chpass": - if len(args) < 2 { - errx("usage: honk chpass username") - } - chpass(args[1]) - case "follow": - if len(args) < 3 { - errx("usage: honk follow username url") - } - user, err := butwhatabout(args[1]) - if err != nil { - errx("user %s not found", args[1]) - } - var meta HonkerMeta - mj, _ := jsonify(&meta) - honkerid, flavor, err := savehonker(user, args[2], "", "presub", "", mj) - if err != nil { - errx("had some trouble with that: %s", err) - } - if flavor == "presub" { - followyou(user, honkerid, true) - } - case "unfollow": - if len(args) < 3 { - errx("usage: honk unfollow username url") - } - user, err := butwhatabout(args[1]) - if err != nil { - 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 { - errx("sorry couldn't find them") - } - unfollowyou(user, honkerid, true) - case "sendmsg": - if len(args) < 4 { - errx("usage: honk send username filename rcpt") - } - user, err := butwhatabout(args[1]) - if err != nil { - errx("user %s not found", args[1]) - } - data, err := os.ReadFile(args[2]) - if err != nil { - errx("can't read file: %s", err) - } - deliverate(user.ID, args[3], data) - case "cleanup": - arg := "30" - if len(args) > 1 { - arg = args[1] - } - cleanupdb(arg) - case "unplug": - if len(args) < 2 { - errx("usage: honk unplug servername") - } - name := args[1] - unplugserver(name) - case "backup": - if len(args) < 2 { - errx("usage: honk backup dirname") - } - name := args[1] - svalbard(name) - case "ping": - if len(args) < 3 { - errx("usage: honk ping (from username) (to username or url)") - } - name := args[1] - targ := args[2] - user, err := butwhatabout(name) - if err != nil { - errx("unknown user %s", name) - } - ping(user, targ) - case "run": - serve() - case "backend": - backendServer() - case "test": - ElaborateUnitTests() - default: - errx("unknown command") - } + c.callback(args) }
@@ -9,11 +9,25 @@ echo go 1.18+ is required
false fi -if [ \! \( -e /usr/include/sqlite3.h -o -e /usr/local/include/sqlite3.h -o `uname` = "Darwin" \) ] ; then - echo unable to find sqlite3.h header - echo please install libsqlite3 dev package - false +sqlhdr= +if [ `uname` = "Darwin" ] ; then + : # okay +else + if [ -e /usr/include/sqlite3.h ] ; then + sqlhdr=/usr/include/sqlite3.h + elif [ -e /usr/local/include/sqlite3.h ] ; then + sqlhdr=/usr/local/include/sqlite3.h + else + echo unable to find sqlite3.h header + echo please install libsqlite3 dev package + false + fi + sqlvers=`grep "#define SQLITE_VERSION_NUMBER" $sqlhdr | cut -f3 -d' '` + if [ $sqlvers -lt 3034000 ] ; then + echo sqlite3.h header is too old: $sqlvers + echo version 3.34.0+ is required + false + fi fi touch .preflightcheck -
@@ -1,7 +1,7 @@
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, plain text); 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 filemeta (fileid integer primary key, xid text, name text, description text, url text, media text, local integer, meta text); 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);@@ -15,6 +15,7 @@ create table mastokens (clientid text, accesstoken text)
create index idx_honksxid on honks(xid); create index idx_honksurl on honks(url); +create index idx_honksrid on honks(rid) where rid <> ''; create index idx_honksconvoy on honks(convoy); create index idx_honkshonker on honks(honker); create index idx_honksoonker on honks(oonker);
@@ -1,3 +1,18 @@
+// +// Copyright (c) 2024 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 (
@@ -23,7 +23,7 @@
"humungus.tedunangst.com/r/webs/htfilter" ) -var myVersion = 49 // index honks url +var myVersion = 51 // filemeta.meta type dbexecer interface { Exec(query string, args ...interface{}) (sql.Result, error)@@ -214,6 +214,15 @@ try("create index idx_honksurl on honks(url)")
setV(49) fallthrough case 49: + try("create index idx_honksrid on honks(rid) where rid <> ''") + setV(50) + fallthrough + case 50: + try("alter table filemeta add column meta text") + try("update filemeta set meta = '{}'") + setV(51) + fallthrough + case 51: try("analyze") closedatabases()
@@ -46,7 +46,7 @@ "strings"
"github.com/jmoiron/sqlx" "golang.org/x/crypto/bcrypt" - _ "humungus.tedunangst.com/r/go-sqlite3" + "humungus.tedunangst.com/r/go-sqlite3" "humungus.tedunangst.com/r/webs/httpsig" "humungus.tedunangst.com/r/webs/login" )@@ -58,6 +58,15 @@
var alreadyopendb *sql.DB var alreadyopendbx *sqlx.DB var stmtConfig *sql.Stmt + +func init() { + vers, num, _ := sqlite3.Version() + if num < 3034000 { + fmt.Fprintf(os.Stderr, "libsqlite is too old. required: %s found: %s\n", + "3.34.0", vers) + os.Exit(1) + } +} func initdb() { blobdbname := dataDir + "/blob.db"@@ -490,3 +499,10 @@ }
listenSocket = listener return listener, nil } + +func getenv(key, def string) string { + if val, ok := os.LookupEnv(key); ok { + return val + } + return def +}
@@ -10,6 +10,8 @@ {{ end }}
<style> {{ .UserStyle }} </style> + {{ .APAltLink }} + {{ .Honkology }} <link rel="manifest" href="/manifest.webmanifest"> <link href="/icon.png" rel="icon"> <meta name="theme-color" media="(prefers-color-scheme: light) "content="#f4f4f4">
@@ -66,14 +66,14 @@ {{ 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 }}</p> +<p><a href="/d/{{ .XID }}">Attachment: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} ({{ .Meta.Length }})</p> {{ else if eq .Media "application/pdf" }} -<p><a href="/d/{{ .XID }}">Attachment: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }}</p> +<p><a href="/d/{{ .XID }}">Attachment: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} ({{ .Meta.Length }})</p> {{ else }} {{ if $omitimages }} -<p><a href="/d/{{ .XID }}">Image: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }}</p> +<p><a href="/d/{{ .XID }}">Image: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} ({{.Meta.Width}}x{{.Meta.Height}} {{ .Meta.Length }})</p> {{ else }} -<img class="donk donklink" src="/d/{{ .XID }}" loading=lazy title="{{ .Desc }}" alt="{{ .Desc }}"> +<img class="donk donklink" src="/d/{{ .XID }}" loading=lazy title="{{ .Desc }}" alt="{{ .Desc }}" width="{{.Meta.Width}}" height="{{.Meta.Height}}"> {{ end }} {{ end }} {{ else }}
@@ -23,8 +23,15 @@ </div>
{{ $honkercsrf := .HonkerCSRF }} <div class="info"> <p><button class="expand">expand</button> +<p>{{ range .Letters }}<a href="#{{.}}">{{.}}</a> {{ end }} </div> +{{ $firstrune := .FirstRune }} +{{ $letter := 0 }} {{ range .Honkers }} +{{ if not (eq $letter (call $firstrune .Name)) }} +{{ $letter = (call $firstrune .Name) }} +<a name="{{ printf "%c" $letter}}"></a> +{{ end }} <section class="honk"> <header> <img alt="avatar" src="/a?a={{ .XID }}">
@@ -2,6 +2,7 @@ {{ template "header.html" . }}
<main> <div class="info"> <p>ontologies of interest +<p>{{ range .Letters }}<a href="#{{.}}">{{.}}</a> {{ end }} {{ $firstrune := .FirstRune }} <ul> <li><p>@@ -12,7 +13,7 @@ {{ $letter := 0 }}
{{ range .Onts }} {{ if not (eq $letter (call $firstrune .Name)) }} {{ $letter = (call $firstrune .Name) }} -<li><p> +<li><p><a name="{{ printf "%c" $letter}}"></a> {{ end }} <span class="wsnowrap"><a href="/o/{{ .Name }}">#{{ .Name }}</a> ({{ .Count }})</span> {{ end }}
@@ -43,6 +43,15 @@ .noise img:not(.emu):hover {
opacity: 1; } } +@media (prefers-color-scheme: dark) { +.noise img:not(.emu) { + opacity: .5; + transition: opacity .5s ease-in-out; +} +.noise img:not(.emu):hover { + opacity: 1; +} +} body { padding: 0px;@@ -239,6 +248,10 @@ padding-left: 0.3em;
padding-right: 0.3em; padding-top: 0.7em; overflow: hidden; +} +.honk { + max-height: 120vh; + overflow-y: scroll; } .level1 {@@ -461,8 +474,10 @@ img:not(.emu) {
background: var(--bg-page); } img, video { - max-width: 100%; - max-height: 600px; + max-width: 100%; + object-fit: scale-down; + width: auto; + height: auto; } .noise img:not(.emu) { display: block;@@ -472,10 +487,14 @@ max-height: 400px;
max-width: 48%; display: inline; } +.noise img.donk { + max-height: 400px; + max-width: 48%; + display: inline; +} img.emu { - height: 2em; - vertical-align: middle; - object-fit: contain; + height: 2em; + vertical-align: middle; } .nophone { position: fixed;
@@ -1,5 +1,5 @@
// -// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com> +// Copyright (c) 2019-2024 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@@ -320,12 +320,35 @@ }
} 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) + t := firstofmany(j, "type") + if t == "Delete" { + a, _ := j.GetString("actor") + o, _ := j.GetString("object") + if a == o { + dlog.Printf("crappola from %s", a) + return true + } + } + if t == "Announce" { + if obj, ok := j.GetMap("object"); ok { + j = obj + t = firstofmany(j, "type") + } + } + if t == "Undo" { + if obj, ok := j.GetMap("object"); ok { + j = obj + t = firstofmany(j, "type") + } + } + if t == "Like" || t == "Dislike" || t == "Listen" { return true + } + if t == "EmojiReact" { + o, _ := j.GetString("object") + if originate(o) != serverName { + return true + } } return false }@@ -389,7 +412,43 @@ return
} } -func inbox(w http.ResponseWriter, r *http.Request) { +func getinbox(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + user, err := butwhatabout(name) + if err != nil { + http.NotFound(w, r) + return + } + u := login.GetUserInfo(r) + if user.ID != UserID(u.UserID) { + http.Error(w, "that's not you!", http.StatusForbidden) + return + } + + honks := gethonksforuser(user.ID, 0) + if len(honks) > 60 { + honks = honks[0:60] + } + + jonks := make([]junk.Junk, 0, len(honks)) + for _, h := range honks { + j, _ := jonkjonk(user, h) + 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 + + w.Header().Set("Content-Type", theonetruename) + j.Write(w) +} + +func postinbox(w http.ResponseWriter, r *http.Request) { name := mux.Vars(r)["name"] user, err := butwhatabout(name) if err != nil {@@ -416,19 +475,6 @@ if crappola(j) {
return } what := firstofmany(j, "type") - obj, _ := j.GetString("object") - switch what { - case "Like": - return - case "Dislike": - return - case "Listen": - return - case "EmojiReact": - if originate(obj) != serverName { - return - } - } who, _ := j.GetString("actor") if rejectactor(user.ID, who) { return@@ -472,8 +518,10 @@ id, _ := j.GetString("id")
ilog.Printf("ping from %s: %s", who, id) pong(user, who, id) case "Pong": + obj, _ := j.GetString("object") ilog.Printf("pong from %s: %s", who, obj) case "Follow": + obj, _ := j.GetString("object") if obj != user.URL { ilog.Printf("can't follow %s", obj) return@@ -683,7 +731,7 @@ elog.Printf("query err: %s", err)
return } defer rows.Close() - var honkers []Honker + honkers := make([]Honker, 0, 256) for rows.Next() { var xid string rows.Scan(&xid)@@ -712,7 +760,7 @@ if len(honks) > 20 {
honks = honks[0:20] } - var jonks []junk.Junk + jonks := make([]junk.Junk, 0, len(honks)) for _, h := range honks { j, _ := jonkjonk(user, h) jonks = append(jonks, j)@@ -729,7 +777,7 @@
return j.ToBytes(), true }, Duration: 1 * time.Minute}) -func outbox(w http.ResponseWriter, r *http.Request) { +func getoutbox(w http.ResponseWriter, r *http.Request) { name := mux.Vars(r)["name"] user, err := butwhatabout(name) if err != nil {@@ -749,6 +797,45 @@ http.NotFound(w, r)
} } +func postoutbox(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + user, err := butwhatabout(name) + if err != nil { + http.NotFound(w, r) + return + } + u := login.GetUserInfo(r) + if user.ID != UserID(u.UserID) { + http.Error(w, "that's not you!", http.StatusForbidden) + return + } + + limiter := io.LimitReader(r.Body, 1*1024*1024) + j, err := junk.Read(limiter) + if err != nil { + http.Error(w, "that's not json!", http.StatusBadRequest) + return + } + + who, _ := j.GetString("actor") + if who != user.URL { + http.Error(w, "that's not you!", http.StatusForbidden) + return + } + what := firstofmany(j, "type") + switch what { + case "Create": + honk := xonksaver2(user, j, serverName, true) + if honk == nil { + dlog.Printf("returned nil") + return + } + go honkworldwide(user, honk) + default: + http.Error(w, "not sure about that", http.StatusBadRequest) + } +} + var oldempties = gencache.New(gencache.Options[string, []byte]{Fill: func(url string) ([]byte, bool) { colname := "/followers" if strings.HasSuffix(url, "/following") {@@ -912,7 +999,7 @@ if len(honks) > 40 {
honks = honks[0:40] } - var xids []string + xids := make([]string, 0, len(honks)) for _, h := range honks { xids = append(xids, h.XID) }@@ -955,7 +1042,8 @@ elog.Printf("selection error: %s", err)
return } defer rows.Close() - var onts, pops []Ont + onts := make([]Ont, 0, 1024) + pops := make([]Ont, 0, 128) for rows.Next() { var o Ont err := rows.Scan(&o.Name, &o.Count)@@ -981,13 +1069,22 @@ })
if len(pops) > 40 { pops = pops[:40] } + letters := make([]string, 0, 64) + var lastrune rune = -1 + for _, o := range onts { + if r := firstRune(o.Name); r != lastrune { + letters = append(letters, string(r)) + lastrune = r + } + } if u == nil && !develMode { w.Header().Set("Cache-Control", "max-age=300") } templinfo := getInfo(r) templinfo["Pops"] = pops templinfo["Onts"] = onts - templinfo["FirstRune"] = func(s string) rune { r, _ := utf8.DecodeRuneInString(s); return r } + templinfo["Letters"] = letters + templinfo["FirstRune"] = firstRune err = readviews.Execute(w, "onts.html", templinfo) if err != nil { elog.Print(err)@@ -1153,15 +1250,15 @@ func threadsort(honks []*Honk) []*Honk {
sort.Slice(honks, func(i, j int) bool { return honks[i].Date.Before(honks[j].Date) }) - honkx := make(map[string]*Honk) - kids := make(map[string][]*Honk) + honkx := make(map[string]*Honk, len(honks)) + kids := make(map[string][]*Honk, len(honks)) for _, h := range honks { honkx[h.XID] = h rid := h.RID kids[rid] = append(kids[rid], h) } - done := make(map[*Honk]bool) - var thread []*Honk + done := make(map[*Honk]bool, len(honks)) + thread := make([]*Honk, 0, len(honks)) var nextlevel func(p *Honk) level := 0 nextlevel = func(p *Honk) {@@ -1318,7 +1415,7 @@ templinfo := getInfo(r)
rawhonks := gethonksbyconvoy(honk.UserID, honk.Convoy, 0) //reversehonks(rawhonks) rawhonks = threadsort(rawhonks) - var honks []*Honk + honks := make([]*Honk, 0, len(rawhonks)) for i, h := range rawhonks { if h.XID == xid { templinfo["Honkology"] = honkology(h)@@ -1668,6 +1765,7 @@ var savedfiles []string
honks := []*Honk{honk} donksforhonks(honks) + var savedfiles []string for _, d := range honk.Donks { savedfiles = append(savedfiles, fmt.Sprintf("%s:%d", d.XID, d.FileID)) }@@ -1767,9 +1865,12 @@ io.Copy(&buf, file)
file.Close() data := buf.Bytes() var media, name string + var donkmeta DonkMeta img, err := bigshrink(data) if err == nil { data = img.Data + donkmeta.Width = img.Width + donkmeta.Height = img.Height format := img.Format media = "image/" + format if format == "jpeg" {@@ -1815,11 +1916,12 @@ name = xfiltrate() + ".txt"
} } } + donkmeta.Length = len(data) desc := strings.TrimSpace(r.FormValue("donkdesc")) if desc == "" { desc = name } - fileid, xid, err := savefileandxid(name, desc, "", media, true, data) + fileid, xid, err := savefileandxid(name, desc, "", media, true, data, &donkmeta) if err != nil { elog.Printf("unable to save image: %s", err) http.Error(w, "failed to save attachment", http.StatusUnsupportedMediaType)@@ -1905,7 +2007,6 @@ noise = quickrename(noise, user.ID)
honk.Noise = noise precipitate(honk) noise = honk.Noise - recategorize(honk) translate(honk) if rid != "" {@@ -1939,6 +2040,7 @@ honk.Audience = append(grapevine(honk.Mentions), honk.Audience...)
} else { honk.Audience = append(honk.Audience, grapevine(honk.Mentions)...) } + honk.Convoy = convoy if convoy == "" { convoy = fmt.Sprintf("data:,%s-", masqName) + xfiltrate()@@ -2050,6 +2152,10 @@ templinfo["Honks"] = honks
templinfo["MapLink"] = getmaplink(u) templinfo["InReplyTo"] = r.FormValue("rid") templinfo["Noise"] = r.FormValue("noise") + templinfo["Onties"] = honk.Onties + templinfo["SeeAlso"] = honk.SeeAlso + templinfo["Link"] = honk.Link + templinfo["LegalName"] = honk.LegalName templinfo["SavedFile"] = donkxid if tm := honk.Time; tm != nil { templinfo["ShowTime"] = " "@@ -2088,10 +2194,23 @@
return honk } +func firstRune(s string) rune { r, _ := utf8.DecodeRuneInString(s); return r } + func showhonkers(w http.ResponseWriter, r *http.Request) { userid := UserID(login.GetUserInfo(r).UserID) + honkers := gethonkers(userid) + letters := make([]string, 0, 64) + var lastrune rune = -1 + for _, h := range honkers { + if r := firstRune(h.Name); r != lastrune { + letters = append(letters, string(r)) + lastrune = r + } + } templinfo := getInfo(r) - templinfo["Honkers"] = gethonkers(userid) + templinfo["FirstRune"] = firstRune + templinfo["Letters"] = letters + templinfo["Honkers"] = honkers templinfo["HonkerCSRF"] = login.GetCSRF("submithonker", r) err := readviews.Execute(w, "honkers.html", templinfo) if err != nil {@@ -2163,7 +2282,7 @@ }
var combocache = gencache.New(gencache.Options[UserID, []string]{Fill: func(userid UserID) ([]string, bool) { honkers := gethonkers(userid) - var combos []string + combos := make([]string, 0, len(honkers)) for _, h := range honkers { combos = append(combos, h.Combos...) }@@ -2553,9 +2672,13 @@ }
xid := mux.Vars(r)["xid"] preview := r.FormValue("preview") == "1" var media string - var data []byte - row := stmtGetFileData.QueryRow(xid) - err := row.Scan(&media, &data) + var data sql.RawBytes + rows, err := stmtGetFileData.Query(xid) + if err == nil { + defer rows.Close() + rows.Next() + err = rows.Scan(&media, &data) + } if err != nil { elog.Printf("error loading file: %s", err) http.NotFound(w, r)@@ -2896,6 +3019,7 @@ if err == nil {
emunames, _ = dir.Readdirnames(0) dir.Close() } + allemus = make([]Emu, 0, len(emunames)) for _, e := range emunames { if len(e) <= 4 { continue@@ -3034,8 +3158,10 @@ getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}", redirectPretty)
getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/"+honkSep+"/{xid:[\\pL[:digit:]]+}", showonehonk) getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/"+honkSep+"/{xid:[\\pL[:digit:]]+}.json", 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) + posters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/inbox", postinbox) + getters.Handle("/"+userSep+"/{name:[\\pL[:digit:]]+}/inbox", login.TokenRequired(http.HandlerFunc(getinbox))) + getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/outbox", getoutbox) + posters.Handle("/"+userSep+"/{name:[\\pL[:digit:]]+}/outbox", login.TokenRequired(http.HandlerFunc(postoutbox))) getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/followers", emptiness) getters.HandleFunc("/"+userSep+"/{name:[\\pL[:digit:]]+}/following", emptiness) getters.HandleFunc("/a", avatate)@@ -3096,7 +3222,7 @@ 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/{name:[\\pL[:digit:]#_.-]+}", showcombo) loggedin.HandleFunc("/c", showcombos) loggedin.HandleFunc("/t", showconvoy) loggedin.HandleFunc("/q", showsearch)