the gods have spoken, again
@@ -59,12 +59,14 @@ }
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 @@ return PostMsg(keyname, key, url, j.ToBytes())
} func PostMsg(keyname string, key httpsig.PrivateKey, url string, msg []byte) error { - client := http.DefaultClient - if develMode { - client = develClient - } req, err := http.NewRequest("POST", url, bytes.NewReader(msg)) if err != nil { return err@@ -86,7 +84,7 @@ httpsig.SignRequest(keyname, key, req, msg)
ctx, cancel := context.WithTimeout(context.Background(), 2*slowTimeout*time.Second) defer cancel() req = req.WithContext(ctx) - resp, err := client.Do(req) + resp, err := honkClient.Do(req) if err != nil { return err }@@ -130,13 +128,10 @@ }
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 @@ }
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 @@ j, err := junk.Get(url, junk.GetArgs{
Accept: at, Agent: "honksnonk/5.0; " + serverName, Timeout: timeout, - Client: client, + Client: &client, Fixup: sign, }) return j, err@@ -173,10 +177,6 @@ return j, nil
} func fetchsome(url string) ([]byte, error) { - client := http.DefaultClient - if develMode { - client = develClient - } req, err := http.NewRequest("GET", url, nil) if err != nil { ilog.Printf("error fetching %s: %s", url, err)@@ -186,7 +186,7 @@ req.Header.Set("User-Agent", "honksnonk/5.0; "+serverName)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() req = req.WithContext(ctx) - resp, err := client.Do(req) + resp, err := honkClient.Do(req) if err != nil { ilog.Printf("error fetching %s: %s", url, err) return nil, err
@@ -80,6 +80,31 @@ }
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)
@@ -307,11 +307,11 @@ if t == "" {
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:") {@@ -792,9 +792,19 @@ return 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)@@ -1214,7 +1224,7 @@ stmtUserHonks = preparetodie(db, selecthonks+"where honks.honkid > ? and (whofore = 2 or whofore = ?) and username = ? and dt > ?"+smalllimit)
myhonkers := " and honker in (select xid from honkers where userid = ? and (flavor = 'sub' or flavor = 'peep' or flavor = 'presub') and combos not like '% - %')" stmtHonksForUser = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ?"+myhonkers+butnotthose+limit) stmtHonksForUserFirstClass = preparetodie(db, selecthonks+"where honks.honkid > ? and honks.userid = ? and dt > ? and (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)
@@ -118,8 +118,9 @@
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 @@ elog.Printf("lost key for delivery")
return } var inbox string - rcpt := doover.Rcpt // already did the box indirection if rcpt[0] == '%' { inbox = rcpt[1:]
@@ -1,5 +1,27 @@
changelog +### next + ++ 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.
@@ -130,10 +130,27 @@ .It Ic badonk
Please no. .It Ic edit Change it up. -Alas, Update activities do not federate reliably. .Ss Refresh Clicking the refresh button will load new honks, if any. New honks will be subtly highlighted. +.El +.Ss 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
@@ -191,9 +191,12 @@ .Ss Import
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@@ -205,6 +208,13 @@ .Dl ./honk import username twitter source-directory
.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
@@ -371,6 +371,7 @@
var marker mz.Marker marker.HashLinker = ontoreplacer marker.AtLinker = attoreplacer + marker.AllowImages = true noise = strings.TrimSpace(noise) noise = marker.Mark(noise) honk.Noise = noise@@ -459,6 +460,9 @@ if err != nil {
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
@@ -3,12 +3,13 @@
go 1.16 require ( - github.com/andybalholm/cascadia v1.3.1 - github.com/dustin/go-humanize v1.0.0 + github.com/andybalholm/cascadia v1.3.2 + github.com/dustin/go-humanize v1.0.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 + github.com/mattn/go-runewidth v0.0.15 + github.com/rivo/uniseg v0.4.4 // indirect + 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.7 )
@@ -1,31 +1,81 @@
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +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/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/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= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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.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/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.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE= golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +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/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.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/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +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.7 h1:1IITkwf5T3Zv2mG0rzmORPVcXuP/YGoTPeh6uBpUkCo= +humungus.tedunangst.com/r/webs v0.7.7/go.mod h1:ylhqHSPI0Oi7b4nsnx5mSO7AjLXN7wFpEHayLfN/ugk=
@@ -16,6 +16,7 @@
package main import ( + "archive/zip" "encoding/csv" "encoding/json" "fmt"@@ -27,12 +28,16 @@ "regexp"
"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 @@ elog.Fatal("unknown source flavor")
} } -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 @@ Name string
} } -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 @@ if err != nil {
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 @@ ilog.Printf("skipping following_accounts.csv!")
} } -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 @@ return false
} 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 @@ xid := fmt.Sprintf("%s/%s/%s", user.URL, honkSep, tootid)
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 @@ XID: xid,
RID: toot.Object.InReplyTo, Date: toot.Object.Published, URL: xid, - Audience: append(toot.To, toot.Cc...), - Noise: toot.Object.Content, - Convoy: toot.Object.Conversation, + Audience: audience, + Noise: content, + Convoy: convoy, Whofore: 2, - Format: "html", + Format: format, Precis: toot.Object.Summary, } if !loudandproud(honk.Audience) {@@ -157,7 +208,7 @@ case "Document":
fname := fmt.Sprintf("%s/%s", source, att.Url) data, err := ioutil.ReadFile(fname) if err != nil { - elog.Printf("error reading media: %s", fname) + elog.Printf("error reading media for %s: %s", honk.XID, fname) continue } u := xfiltrate()@@ -513,3 +564,95 @@ err := savehonk(&honk)
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() +}
@@ -64,6 +64,11 @@ }
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 @@ serverPrefix = fmt.Sprintf("https://%s/", serverName)
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 @@ setconfig("devel", 1)
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 @@ case "adduser":
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 @@ }
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 @@ backendServer()
case "test": ElaborateUnitTests() default: - elog.Fatal("unknown command") + errx("unknown command") } }
@@ -29,6 +29,9 @@ s = re_moredumb.ReplaceAllString(s, ".")
zw := false for _, c := range s { + if c == '\n' { + continue + } if runewidth.RuneWidth(c) == 0 { zw = true break
@@ -172,6 +172,7 @@ }
tx = nil fallthrough case 45: + try("analyze") default: elog.Fatalf("can't upgrade unknown version %d", dbversion)
@@ -9,7 +9,7 @@ <p>
<details> <summary>more options</summary> <p> -<label class=button id="donker">attach: <input type="file" name="donk" multiple><span>{{ .SavedFile }}</span></label> +<label class=button id="donker">attach: <input type="file" multiple name="donk"><span>{{ .SavedFile }}</span></label><input type="hidden" id="saveddonkxid" name="donkxid" value="{{ .SavedFile }}"> <input type="hidden" id="saveddonkxid" name="donkxid" value="{{ .SavedFile }}"> <p id="donkdescriptor"><label for=donkdesc>description:</label><br> <input type="text" name="donkdesc" value="{{ .DonkDesc }}" autocomplete=off>
@@ -18,8 +18,8 @@ {{ end }}
</div> {{ if and .HonkCSRF (not .IsPreview) }} <div class="info" id="refreshbox"> -<p><button class="refresh">refresh</button><span></span> -<button class="scrolldown">scroll down</button> +<p><button id="honkrefresher" class="refresh">refresh</button><span></span> +<button id="newerscroller" class="scrolldown">scroll down</button> </div> {{ end }} <div id="honksonpage">
@@ -386,13 +386,13 @@ el = document.getElementById(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 = el.nextSibling + el.value = "" + el = el.parentElement.nextSibling el.style.display = "" - el = document.getElementById("saveddonkxid") - el.value = "" } var checkinprec = 100.0 var gpsoptions = {@@ -418,6 +418,72 @@ el.value = err.message
}, 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") + menu.open = true + menu.querySelector("a").focus() + 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");
@@ -529,8 +529,14 @@ .hide {
display: none; } -.textright { +.text-left { + text-align: left; +} +.text-right { text-align: right; +} +.text-center { + text-align: center; } .font08em {
@@ -22,6 +22,7 @@ "fmt"
"html/template" "io" notrand "math/rand" + "mime/multipart" "net/http" "net/url" "os"@@ -82,8 +83,8 @@ templinfo := make(map[string]interface{})
templinfo["StyleParam"] = getassetparam(viewDir + "/views/style.css") templinfo["LocalStyleParam"] = getassetparam(dataDir + "/views/local.css") templinfo["JSParam"] = getassetparam(viewDir + "/views/honkpage.js") + templinfo["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@@ -450,7 +451,20 @@ content, _ := j.GetString("content")
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!") + } } }@@ -1066,9 +1080,16 @@ level++
} 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@@ -1573,11 +1594,15 @@ if tm.Duration != 0 {
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 {@@ -1617,11 +1642,26 @@ }
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@@ -1635,7 +1675,7 @@ io.Copy(&buf, file)
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@@ -1795,7 +1835,7 @@ if !re_dangerous.MatchString(honk.Precis) {
honk.Precis = "re: " + honk.Precis } } - } else { + } else if updatexid == "" { honk.Audience = []string{thewholeworld} } if honk.Noise != "" && honk.Noise[0] == '@' {@@ -1816,21 +1856,28 @@ return nil
} 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 _, d := range r.Form["donkxid"] { - p := strings.Split(d, ":") - xid := p[0] + 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) var donk *Donk if len(p) > 1 {@@ -1997,12 +2044,12 @@ Date: dt,
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)@@ -2585,15 +2632,16 @@ }
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":@@ -2785,6 +2833,7 @@ assets := []string{
viewDir + "/views/style.css", dataDir + "/views/local.css", viewDir + "/views/honkpage.js", + viewDir + "/views/misc.js", dataDir + "/views/local.js", viewDir + "/views/manifest.webmanifest", viewDir + "/views/sw.js",