M activity.go +10 -3
@@ 1439,9 1439,6 @@ func jonkjonk(user *WhatAbout, h *Honk)
jo = junk.New()
jo["id"] = h.XID
jo["type"] = "Note"
- if h.What == "event" {
- jo["type"] = "Event"
- }
if h.What == "update" {
j["type"] = "Update"
jo["updated"] = dt
@@ 1555,6 1552,12 @@ func jonkjonk(user *WhatAbout, h *Honk)
jl["href"] = h.Link
atts = append(atts, jl)
}
+ if tooooFancy(h.Noise) {
+ jo["type"] = "Article"
+ }
+ if h.What == "event" {
+ jo["type"] = "Event"
+ }
if len(atts) > 0 {
jo["attachment"] = atts
}
@@ 1615,6 1618,10 @@ func jonkjonk(user *WhatAbout, h *Honk)
return j, jo
}
+func tooooFancy(noise string) bool {
+ return strings.Contains(noise, "<img") || strings.Contains(noise, "<table")
+}
+
var oldjonks = gencache.New(gencache.Options[string, []byte]{Fill: func(xid string) ([]byte, bool) {
row := stmtAnyXonk.QueryRow(xid)
honk := scanhonk(row)
M admin.go +7 -8
@@ 59,15 59,11 @@ func adminscreen() {
},
}
- termvc.Start()
- defer termvc.Restore()
- go termvc.Catch(nil)
-
app := termvc.NewApp()
scr := termvc.NewScreen()
scr.DefaultColor(35)
var tabs []termvc.Element
- insns := termvc.NewTextLabel("honk admin")
+ insns := termvc.NewStringWrapper("honk admin")
tabs = append(tabs, insns)
for _, m := range messages {
@@ 107,9 103,12 @@ func adminscreen() {
group.SetFocus(1)
group.SetHeight(len(tabs)-1, 1)
group.SetHeight(len(tabs)-2, 6)
- pane := termvc.NewMainPanel(group)
+
+ app.Element = group
+ app.Screen = scr
- app.Element = pane
- app.Screen = scr
+ termvc.Start()
+ defer termvc.Restore()
+ go termvc.Catch(nil)
app.Loop()
}
M cli.go +48 -40
@@ 10,7 10,9 @@ import (
type cmd struct {
help string
+ help2 string
callback func(args []string)
+ nargs int
}
var commands = map[string]cmd{
@@ 40,29 42,41 @@ var commands = map[string]cmd{
},
},
"import": {
- help: "import data into honk",
+ help: "import data into honk",
+ help2: "import username honk|mastodon|twitter srcdir",
callback: func(args []string) {
- if len(args) != 4 {
- errx("import username honk|mastodon|twitter srcdir")
- }
importMain(args[1], args[2], args[3])
},
+ nargs: 4,
},
"export": {
- help: "export data from honk",
+ help: "export data from honk",
+ help2: "export username destdir",
callback: func(args []string) {
- if len(args) != 3 {
- errx("export username destdir")
- }
export(args[1], args[2])
},
+ nargs: 3,
+ },
+ "dumpthread": {
+ help: "export a thread for debugging",
+ help2: "dumpthread user convoy",
+ callback: func(args []string) {
+ dumpthread(args[1], args[2])
+ },
+ nargs: 3,
+ },
+ "rawimport": {
+ help: "import activity objects for debugging",
+ help2: "rawimport username filename",
+ callback: func(args []string) {
+ rawimport(args[1], args[2])
+ },
+ nargs: 3,
},
"devel": {
- help: "turn devel on/off",
+ help: "turn devel on/off",
+ help2: "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)
@@ 72,13 86,12 @@ var commands = map[string]cmd{
errx("argument must be on or off")
}
},
+ nargs: 2,
},
"setconfig": {
- help: "set honk config",
+ help: "set honk config",
+ help2: "setconfig key val",
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 {
@@ 86,6 99,7 @@ var commands = map[string]cmd{
}
setconfig(args[1], val)
},
+ nargs: 3,
},
"adduser": {
help: "add a user to honk",
@@ 94,29 108,25 @@ var commands = map[string]cmd{
},
},
"deluser": {
- help: "delete a user from honk",
+ help: "delete a user from honk",
+ help2: "deluser username",
callback: func(args []string) {
- if len(args) < 2 {
- errx("usage: honk deluser username")
- }
deluser(args[1])
},
+ nargs: 2,
},
"chpass": {
- help: "change password of an account",
+ help: "change password of an account",
+ help2: "chpass username",
callback: func(args []string) {
- if len(args) < 2 {
- errx("usage: honk chpass username")
- }
chpass(args[1])
},
+ nargs: 2,
},
"follow": {
- help: "follow an account",
+ help: "follow an account",
+ help2: "follow username url",
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])
@@ 131,13 141,12 @@ var commands = map[string]cmd{
followyou(user, honkerid, true)
}
},
+ nargs: 3,
},
"unfollow": {
- help: "unfollow an account",
+ help: "unfollow an account",
+ help2: "unfollow username url",
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")
@@ 149,13 158,12 @@ var commands = map[string]cmd{
}
unfollowyou(user, honkerid, true)
},
+ nargs: 3,
},
"sendmsg": {
- help: "send a raw activity",
+ help: "send a raw activity",
+ help2: "sendmsg username filename rcpt",
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])
@@ 166,6 174,7 @@ var commands = map[string]cmd{
}
deliverate(user.ID, args[3], data)
},
+ nargs: 4,
},
"cleanup": {
help: "clean up stale data from database",
@@ 196,14 205,13 @@ var commands = map[string]cmd{
},
},
"unplug": {
- help: "disconnect from a dead server",
+ help: "disconnect from a dead server",
+ help2: "unplug servername",
callback: func(args []string) {
- if len(args) < 2 {
- errx("usage: honk unplug servername")
- }
name := args[1]
unplugserver(name)
},
+ nargs: 2,
},
"backup": {
help: "backup honk",
M database.go +2 -2
@@ 301,7 301,7 @@ func gethonksbycombo(userid UserID, comb
return getsomehonks(rows, err)
}
func gethonksbyconvoy(userid UserID, convoy string, wanted int64) []*Honk {
- rows, err := stmtHonksByConvoy.Query(convoy, wanted, userid)
+ rows, err := stmtHonksByConvoy.Query(convoy, wanted, userid, 1000)
return getsomehonks(rows, err)
}
func gethonksbysearch(userid UserID, q string, wanted int64) []*Honk {
@@ 1181,7 1181,7 @@ func prepareStatements(db *sql.DB) {
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)
+ ) `+selecthonks+"where honks.honkid > ? and honks.userid = ? and xid in (select x from getthread)"+smalllimit)
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 (?, ?, ?)")
M docs/changelog.txt +2 -0
@@ 2,6 2,8 @@ changelog
### next
++ Another tune up for thread sort.
+
+ Rework setup and admin screens.
+ Add some compat for forgefed activities.
M fun.go +3 -0
@@ 639,12 639,15 @@ var allhandles = gencache.New(gencache.O
dlog.Printf("need to get a handle: %s", xid)
info, _, err := investigate(xid)
if err != nil {
+ dlog.Printf("failed to get handle: %s", err)
m := re_unurl.FindStringSubmatch(xid)
if len(m) > 2 {
handle = m[2]
} else {
handle = xid
}
+ when := time.Now().UTC().Format(dbtimeformat)
+ savexonker(xid, handle, "handle", when)
} else {
handle = info.Name
}
M go.mod +2 -2
@@ 10,8 10,8 @@ require (
golang.org/x/net v0.21.0
humungus.tedunangst.com/r/go-sqlite3 v1.2.1
humungus.tedunangst.com/r/gonix v0.1.4
- humungus.tedunangst.com/r/termvc v0.1.0
- humungus.tedunangst.com/r/webs v0.7.12
+ humungus.tedunangst.com/r/termvc v0.1.2
+ humungus.tedunangst.com/r/webs v0.7.15
)
require (
M go.sum +4 -4
@@ 50,7 50,7 @@ humungus.tedunangst.com/r/go-sqlite3 v1.
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/termvc v0.1.0 h1:Xe5ImK7W4jZqAOtZhTiec1Lc7CbNpmC2UMuDBUMcVwk=
-humungus.tedunangst.com/r/termvc v0.1.0/go.mod h1:TnlG9PbH77OpEf46iDyb/H9drjegQNwhpXalmGGrbhU=
-humungus.tedunangst.com/r/webs v0.7.12 h1:SbAOmzwn4LPB5AmAzc1KLIA1zymEkXFyFMV2Cp3MDdo=
-humungus.tedunangst.com/r/webs v0.7.12/go.mod h1:ylhqHSPI0Oi7b4nsnx5mSO7AjLXN7wFpEHayLfN/ugk=
+humungus.tedunangst.com/r/termvc v0.1.2 h1:TPH5ThFRjR+f1ko9Uh4dhm3ieNKvmoPZAvu3tU5lNIE=
+humungus.tedunangst.com/r/termvc v0.1.2/go.mod h1:TnlG9PbH77OpEf46iDyb/H9drjegQNwhpXalmGGrbhU=
+humungus.tedunangst.com/r/webs v0.7.15 h1:97l++EcyhAgCUgBDSUvvNHG1wFW1jAdqlqEbWvbbWik=
+humungus.tedunangst.com/r/webs v0.7.15/go.mod h1:ylhqHSPI0Oi7b4nsnx5mSO7AjLXN7wFpEHayLfN/ugk=
M honk.go +4 -0
@@ 200,6 200,10 @@ func (honk *Honk) IsReacted() bool {
return honk.Flags&flagIsReacted != 0
}
+func (honk *Honk) ShortXID() string {
+ return shortxid(honk.XID)
+}
+
type Donk struct {
FileID int64
XID string
M import.go +117 -0
@@ 48,6 48,7 @@ func importMain(username, flavor, source
}
type ActivityObject struct {
+ Id string
AttributedTo string
Summary string
Content string
@@ 658,3 659,119 @@ func export(username, file string) {
zd.Close()
fd.Close()
}
+
+func dumpthread(username, convoy string) {
+ user, _ := butwhatabout(username)
+ honks := gethonksbyconvoy(user.ID, convoy, 0)
+ var jonks []junk.Junk
+ for _, honk := range honks {
+ 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["orderedItems"] = jonks
+ j.Write(os.Stdout)
+}
+func rawimport(username, filename string) {
+ user, _ := butwhatabout(username)
+ type Activity struct {
+ Id string
+ Type string
+ To interface{}
+ Cc []string
+ Object ActivityObject
+ }
+ var outbox struct {
+ OrderedItems []Activity
+ }
+ ilog.Println("Importing honks...")
+ fd, err := os.Open(filename)
+ if err != nil {
+ elog.Fatal(err)
+ }
+ dec := json.NewDecoder(fd)
+ err = dec.Decode(&outbox)
+ if err != nil {
+ elog.Fatalf("error parsing json: %s", err)
+ }
+ fd.Close()
+
+ havetoot := func(xid string) bool {
+ var id int64
+ row := stmtFindXonk.QueryRow(user.ID, xid)
+ err := row.Scan(&id)
+ if err == nil {
+ return true
+ }
+ return false
+ }
+
+ 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
+ }
+ xid := toot.Object.Id
+ 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",
+ Honker: toot.Object.AttributedTo,
+ XID: xid,
+ RID: toot.Object.InReplyTo,
+ Date: toot.Object.Published,
+ URL: xid,
+ Audience: audience,
+ Noise: content,
+ Convoy: convoy,
+ Whofore: 2,
+ Format: format,
+ Precis: toot.Object.Summary,
+ }
+ if !loudandproud(honk.Audience) {
+ honk.Whofore = 3
+ }
+ for _, t := range toot.Object.Tag {
+ switch t.Type {
+ case "Hashtag":
+ honk.Onts = append(honk.Onts, t.Name)
+ }
+ }
+ savehonk(&honk)
+ }
+}
M main.go +3 -0
@@ 186,6 186,9 @@ func main() {
if !ok {
errx("don't know about %q", cmd)
}
+ if c.nargs > 0 && len(args) != c.nargs {
+ errx("incorrect arg count: %s", c.help2)
+ }
c.callback(args)
}
M upgradedb.go +1 -0
@@ 226,6 226,7 @@ func upgradedb() {
setV(53)
fallthrough
case 53:
+ setcsrfkey()
try("analyze")
closedatabases()
M util.go +43 -6
@@ 109,7 109,7 @@ func initdb() {
t2 := termvc.NewTextArea()
group := termvc.NewVStack(t1, form, t2)
group.SetFocus(1)
- app.Element = termvc.NewMainPanel(group)
+ app.Element = group
app.Screen = termvc.NewScreen()
btn.Submit = func() {
t2.Value = ""
@@ 147,10 147,7 @@ func initdb() {
return
}
- var randbytes [16]byte
- rand.Read(randbytes[:])
- key := fmt.Sprintf("%x", randbytes)
- setconfig("csrfkey", key)
+ setcsrfkey()
setconfig("dbversion", myVersion)
setconfig("servermsg", "<h2>Things happen.</h2>")
@@ 164,6 161,13 @@ func initdb() {
os.Exit(0)
}
+func setcsrfkey() {
+ var randbytes [16]byte
+ rand.Read(randbytes[:])
+ key := fmt.Sprintf("%x", randbytes)
+ setconfig("csrfkey", key)
+}
+
func initblobdb(blobdbname string) {
_, err := os.Stat(blobdbname)
if err == nil {
@@ 193,7 197,40 @@ func initblobdb(blobdbname string) {
}
func adduser() {
- panic("todo")
+ termvc.Start()
+ defer termvc.Restore()
+ go termvc.Catch(nil)
+
+ db := opendatabase()
+ app := termvc.NewApp()
+ t1 := termvc.NewTextArea()
+ t1.Value = "\n\n\tHello.\n\t\tLet's invite a friend!"
+ var inputs []termvc.Element
+ var offset int
+ namefield := termvc.NewTextInput("username", &offset)
+ inputs = append(inputs, namefield)
+ passfield := termvc.NewPasswordInput("password", &offset)
+ inputs = append(inputs, passfield)
+ btn := termvc.NewButton("let's go!")
+ left := 25
+ inputs = append(inputs, termvc.NewHPad(&left, btn, nil))
+ form := termvc.NewForm(inputs...)
+ t2 := termvc.NewTextArea()
+ group := termvc.NewVStack(t1, form, t2)
+ group.SetFocus(1)
+ app.Element = group
+ app.Screen = termvc.NewScreen()
+ btn.Submit = func() {
+ t2.Value = ""
+ err := createuser(db, namefield.Value, passfield.Value)
+ if err != nil {
+ t2.Value += fmt.Sprintf("error: %s\n", err)
+ return
+ }
+ app.Quit()
+ }
+
+ app.Loop()
}
func deluser(username string) {
M views/honk.html +2 -2
@@ 1,4 1,4 @@
-<article class="honk {{ .Honk.Style }}" data-convoy="{{ .Honk.Convoy }}" data-hname="{{ .Honk.Handles }}" data-xid="{{ .Honk.XID }}" data-id="{{ .Honk.ID }}">
+<article id="{{ .Honk.ShortXID }}" class="honk {{ .Honk.Style }}" data-convoy="{{ .Honk.Convoy }}" data-hname="{{ .Honk.Handles }}" data-xid="{{ .Honk.XID }}" data-id="{{ .Honk.ID }}">
{{ $bonkcsrf := .BonkCSRF }}
{{ $IsPreview := .IsPreview }}
{{ $maplink := .MapLink }}
@@ 47,7 47,7 @@ in reply to: <a href="{{ .RID }}" rel=no
{{ end }}
<br>
{{ if $bonkcsrf }}
-<span class="left1em clip">convoy: <a class="convoylink" href="/t?c={{ .Convoy }}">{{ .Convoy }}</a></span>
+<span class="left1em clip">convoy: <a class="convoylink" href="/t?c={{ .Convoy }}#{{ .ShortXID }}">{{ .Convoy }}</a></span>
{{ end }}
</header>
<p>
M views/honkpage.html +1 -0
@@ 38,3 38,4 @@
</div>
</div>
</main>
+<div class="footpad"></div>
M views/honkpage.js +39 -5
@@ 194,7 194,7 @@ function statechanger(evt) {
}
switchtopage(data.name, data.arg)
}
-function switchtopage(name, arg) {
+function switchtopage(name, arg, anchor) {
var stash = curpagestate.name + ":" + curpagestate.arg
var honksonpage = document.getElementById("honksonpage")
var holder = honksonpage.children[0]
@@ 220,6 220,10 @@ function switchtopage(name, arg) {
if (msg) {
srvel.prepend(msg)
}
+ if (anchor) {
+ let el = document.getElementById(anchor)
+ el.scrollIntoView()
+ }
} else {
// or create one and fill it
honksonpage.prepend(document.createElement("div"))
@@ 227,6 231,10 @@ function switchtopage(name, arg) {
get("/hydra?" + encode(args), function(xhr) {
if (xhr.status == 200) {
fillinhonks(xhr, false)
+ if (anchor) {
+ let el = document.getElementById(anchor)
+ el.scrollIntoView()
+ }
} else {
refreshupdate(" status: " + xhr.status)
}
@@ 246,11 254,14 @@ function pageswitcher(name, arg) {
if (name == curpagestate.name && arg == curpagestate.arg) {
return false
}
- switchtopage(name, arg)
- var url = evt.srcElement.href
- if (!url) {
+ let url = evt.srcElement.href
+ if (!url)
url = evt.srcElement.parentElement.href
- }
+ let anchor
+ let arr = url.split("#")
+ if (arr.length == 2)
+ anchor = arr[1]
+ switchtopage(name, arg, anchor)
history.pushState(newpagestate(name, arg), "some title", url)
window.scrollTo(0, 0)
return false
@@ 377,6 388,7 @@ function showhonkform(elem, rid, hname)
ridinput.value = ""
honknoise.value = ""
}
+ honknoise.ondrop = donkdrop
var updateinput = document.getElementById("updatexidinput")
updateinput.value = ""
var savedfile = document.getElementById("saveddonkxid")
@@ 384,6 396,28 @@ function showhonkform(elem, rid, hname)
honknoise.focus()
return false
}
+function donkdrop(evt) {
+ evt.preventDefault()
+ let donks = document.querySelector("#donker input")
+ Array.from(evt.dataTransfer.items).forEach((item) => {
+ if (item.kind == "file") {
+ let olddonks = donks.files
+ let donkarama = new DataTransfer();
+ for (donk of olddonks)
+ donkarama.items.add(donk)
+ let file = item.getAsFile()
+ donkarama.items.add(file)
+ donks.files = donkarama.files;
+ let t = evt.target
+ let start = t.selectionStart
+ let s = t.value.substr(0, start)
+ let e = t.value.substr(start)
+ t.value = s + `<img src=${donks.files.length}>` + e
+ t.selectionStart = start
+ t.selectionEnd = start
+ }
+ })
+}
function cancelhonking() {
hideelement(lehonkform)
showelement(lehonkbutton)
M views/style.css +4 -0
@@ 448,3 448,7 @@ li.details {
.fontmonospace {
font-family: monospace;
}
+div.footpad {
+ min-height: 14em;
+ border-top: 1px solid var(--fg-subtle);
+}
M web.go +32 -13
@@ 1108,11 1108,18 @@ func thelistingoftheontologies(w http.Re
if utf8.RuneCountInString(o.Name) > 24 {
continue
}
+ if o.Count < 3 {
+ continue
+ }
o.Name = o.Name[1:]
onts = append(onts, o)
- if o.Count > 1 {
- pops = append(pops, o)
- }
+ pops = append(pops, o)
+ }
+ if len(onts) > 1024 {
+ sort.Slice(onts, func(i, j int) bool {
+ return onts[i].Count > onts[j].Count
+ })
+ onts = onts[:1024]
}
sort.Slice(onts, func(i, j int) bool {
return onts[i].Name < onts[j].Name
@@ 1315,6 1322,14 @@ func threadsort(honks []*Honk) []*Honk {
thread := make([]*Honk, 0, len(honks))
var nextlevel func(p *Honk)
level := 0
+ hasreply := func(p *Honk, who string) bool {
+ for _, h := range kids[p.XID] {
+ if h.Honker == who {
+ return true
+ }
+ }
+ return false
+ }
nextlevel = func(p *Honk) {
levelup := level < 4
if pp := honkx[p.RID]; p.RID == "" || (pp != nil && sameperson(p, pp)) {
@@ 1330,16 1345,20 @@ func threadsort(honks []*Honk) []*Honk {
}
p.Style += fmt.Sprintf(" level%d", level)
childs := kids[p.XID]
- 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)
- })
- }
+ sort.SliceStable(childs, func(i, j int) bool {
+ var ipts, jpts int
+ if sameperson(childs[i], p) {
+ ipts += 1
+ } else if hasreply(childs[i], p.Honker) {
+ ipts += 2
+ }
+ if sameperson(childs[j], p) {
+ jpts += 1
+ } else if hasreply(childs[j], p.Honker) {
+ jpts += 2
+ }
+ return ipts > jpts
+ })
for _, h := range childs {
if !done[h] {
done[h] = true