M activity.go +52 -12
@@ 591,12 591,12 @@ func grabhonk(user *WhatAbout, xid strin
xonksaver(user, j, originate(final))
}
-var re_mast0link = regexp.MustCompile(`https://[[:alnum:].]+/users/[[:alnum:]]+/statuses/[[:digit:]]+`)
-var re_masto1ink = regexp.MustCompile(`https://([[:alnum:].]+)/@([[:alnum:]]+)(@[[:alnum:].]+)?/([[:digit:]]+)`)
-var re_misslink = regexp.MustCompile(`https://[[:alnum:].]+/notes/[[:alnum:]]+`)
-var re_honklink = regexp.MustCompile(`https://[[:alnum:].]+/u/[[:alnum:]]+/h/[[:alnum:]]+`)
-var re_r0malink = regexp.MustCompile(`https://[[:alnum:].]+/objects/[[:alnum:]-]+`)
-var re_roma1ink = regexp.MustCompile(`https://[[:alnum:].]+/notice/[[:alnum:]]+`)
+var re_mast0link = regexp.MustCompile(`https://[[:alnum:].-]+/users/[[:alnum:]_]+/statuses/[[:digit:]]+`)
+var re_masto1ink = regexp.MustCompile(`https://([[:alnum:].-]+)/@([[:alnum:]_]+)(@[[:alnum:].]+)?/([[:digit:]]+)`)
+var re_misslink = regexp.MustCompile(`https://[[:alnum:].-]+/notes/[[:alnum:]]+`)
+var re_honklink = regexp.MustCompile(`https://[[:alnum:].-]+/u/[[:alnum:]_]+/h/[[:alnum:]]+`)
+var re_r0malink = regexp.MustCompile(`https://[[:alnum:].-]+/objects/[[:alnum:]-]+`)
+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 {
@@ 1045,10 1045,24 @@ func xonksaver2(user *WhatAbout, item ju
if ua, ok := att.GetArray("url"); ok && len(ua) > 0 {
u, ok = ua[0].(string)
if !ok {
- if uu, ok := ua[0].(junk.Junk); ok {
- u, _ = uu.GetString("href")
- if mt == "" {
- mt, _ = uu.GetString("mediaType")
+ mtprio := -1
+ for _, item := range ua {
+ if uu, ok := item.(junk.Junk); ok {
+ p := 0
+ m, _ := uu.GetString("mediaType")
+ switch m {
+ case "image/jpeg":
+ p = 1
+ case "image/avif":
+ if acceptAVIF {
+ p = 2
+ }
+ }
+ if p > mtprio {
+ mtprio = p
+ u, _ = uu.GetString("href")
+ mt = m
+ }
}
}
}
@@ 1411,11 1425,22 @@ func activatedonks(donks []*Donk) []junk
continue
}
jd := junk.New()
- jd["mediaType"] = d.Media
jd["name"] = d.Name
jd["summary"] = html.EscapeString(d.Desc)
jd["type"] = "Document"
- jd["url"] = d.URL
+ if convertAVIF && d.Media == "image/jpeg" {
+ var u [2]junk.Junk
+ u[0] = junk.New()
+ u[0]["mediaType"] = "image/jpeg"
+ u[0]["href"] = d.URL
+ u[1] = junk.New()
+ u[1]["mediaType"] = "image/avif"
+ u[1]["href"] = d.URL + ".avif"
+ jd["url"] = u
+ } else {
+ jd["mediaType"] = d.Media
+ jd["url"] = d.URL
+ }
atts = append(atts, jd)
}
return atts
@@ 1758,7 1783,22 @@ func sendchonk(user *WhatAbout, ch *Chon
}
}
+func trigger(user *WhatAbout, honk *Honk) {
+ j := junk.New()
+ j["honk"] = honk
+ ki := ziggy(user.ID)
+ if ki == nil {
+ return
+ }
+ err := PostJunk(ki.keyname, ki.seckey, user.Options.Trigger, j)
+ if err != nil {
+ ilog.Printf("error triggering: %s", err)
+ }
+}
func honkworldwide(user *WhatAbout, honk *Honk) {
+ if user.Options.Trigger != "" {
+ trigger(user, honk)
+ }
jonk, _ := jonkjonk(user, honk)
jonk["@context"] = itiswhatitis
msg := jonk.ToBytes()
M bloat.go +0 -35
@@ 14,38 14,3 @@
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package main
-
-import (
- "io"
- "net"
- "time"
-)
-
-func qotd() {
- var qotdaddr string
- getconfig("qotdaddr", &qotdaddr)
- if qotdaddr == "" {
- return
- }
- s, err := net.Listen("tcp", ":8017")
- if err != nil {
- return
- }
- for {
- c, err := s.Accept()
- if err != nil {
- time.Sleep(time.Second)
- continue
- }
- honks := getpublichonks()
- for _, honk := range honks {
- if !firstclass(honk) {
- continue
- }
- io.WriteString(c, honk.Noise)
- io.WriteString(c, "\n")
- break
- }
- c.Close()
- }
-}
M database.go +8 -3
@@ 269,8 269,13 @@ func gethonksfromlongago(userid UserID,
params = append(params, wanted)
params = append(params, userid)
now := time.Now()
- for i := 1; i <= 6; i++ {
- dt := time.Date(now.Year()-i, now.Month(), now.Day(), now.Hour(), now.Minute(),
+
+ for i := 1; i <= 100; i++ {
+ yr := now.Year() - i
+ if yr < 2019 {
+ break
+ }
+ dt := time.Date(yr, now.Month(), now.Day(), now.Hour(), now.Minute(),
now.Second(), 0, now.Location())
dt1 := dt.Add(-36 * time.Hour).UTC().Format(dbtimeformat)
dt2 := dt.Add(12 * time.Hour).UTC().Format(dbtimeformat)
@@ 1204,7 1209,7 @@ func prepareStatements(db *sql.DB) {
stmtCheckFileHash = preparetodie(db, "select xid from filehashes where hash = ?")
stmtGetFileMedia = preparetodie(db, "select media from filehashes where xid = ?")
stmtFindXonk = preparetodie(db, "select honkid from honks where userid = ? and xid = ?")
- stmtGetFileInfo = preparetodie(db, "select url from filemeta where xid = ?")
+ stmtGetFileInfo = preparetodie(db, "select url, media, meta from filemeta where xid = ?")
stmtFindFile = preparetodie(db, "select fileid, xid from filemeta where url = ? and local = 1")
stmtFindFileId = preparetodie(db, "select xid, local, description from filemeta where fileid = ? and url = ? and local = 1")
stmtUserByName = preparetodie(db, "select userid, username, displayname, about, pubkey, seckey, options from users where username = ? and userid > 0")
M docs/changelog.txt +8 -0
@@ 1,5 1,13 @@
changelog
+### next
+
++ Triggers.
+
++ AVIF image support. Optional with config option convertavif.
+
+- Remove the qotd server.
+
### 1.4.2 Kindred Key
+ Fix shortcuts for alternate keymaps.
M docs/honk.1 +4 -3
@@ 214,12 214,13 @@ See
.Xr honk 8
for more about the funzone.
.Pp
+A web trigger may be set which will receive POSTs when new honks are posted.
+.Dq http://spybridge.honk.example/trigger
+.Pp
Some options to customize the site appearance:
-.Bl -tag -width skinny
+.Bl -tag -width reaction
.It skinny
Use a narrower column for the main display.
-.It omit images
-Omit img tags, to lighten page loads on slow connections.
.It apple
Prefer Apple links for maps.
The default is OpenStreetMap.
M docs/honk.8 +2 -0
@@ 260,6 260,8 @@ Custom URL seperators (not "u" and "h")
e.g. example.com/users/username/honk/somehonk instead of
example.com/u/username/h/somehonk.
.Bl -tag -width collectforwards
+.It convertavif
+Support AVIF images if libavif can be found.
.It fasttimeout
Short timeout for fetching activities.
(Default: 5)
M filestoragemanagerfactory.go +28 -10
@@ 35,6 35,19 @@ func hashfiledata(data []byte) string {
return fmt.Sprintf("%x", h.Sum(nil))
}
+func needfilehash(data []byte, xid *string) string {
+ hash := hashfiledata(data)
+ row := stmtCheckFileHash.QueryRow(hash)
+ err := row.Scan(xid)
+ if err == nil {
+ return ""
+ }
+ if err != sql.ErrNoRows {
+ elog.Printf("error checking file hash: %s", err)
+ }
+ return hash
+}
+
func filepath(xid string) string {
parts := strings.SplitN(xid, ".", 2)
subdir := "xx"
@@ 65,10 78,7 @@ func savefiledata(xid string, data []byt
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)
- row := stmtCheckFileHash.QueryRow(hash)
- err := row.Scan(&xid)
- if err == sql.ErrNoRows {
+ if hash := needfilehash(data, &xid); hash != "" {
xid = xfildate()
switch media {
case "image/png":
@@ 82,16 92,13 @@ func savefileandxid(name string, desc st
case "text/plain":
xid += ".txt"
}
- err = savefiledata(xid, data)
+ err := savefiledata(xid, data)
if err == nil {
_, err = stmtSaveFileHash.Exec(xid, hash, media)
}
if err != nil {
return 0, "", err
}
- } else if err != nil {
- elog.Printf("error checking file hash: %s", err)
- return 0, "", err
}
if url == "" {
url = serverURL("/d/%s", xid)
@@ 113,9 120,11 @@ func savefileandxid(name string, desc st
func getfileinfo(xid string) *Donk {
donk := new(Donk)
row := stmtGetFileInfo.QueryRow(xid)
- err := row.Scan(&donk.URL)
+ var j string
+ err := row.Scan(&donk.URL, &donk.Media, &j)
if err == nil {
donk.XID = xid
+ unjsonify(j, &donk.Meta)
return donk
}
if err != sql.ErrNoRows {
@@ 194,7 203,9 @@ func loaddata(xid string) ([]byte, func(
}
func servefiledata(w http.ResponseWriter, r *http.Request, xid string) {
- if strings.HasSuffix(xid, ".avif") {
+ wantAVIF := false
+ if convertAVIF && strings.HasSuffix(xid, ".avif") {
+ wantAVIF = true
xid = xid[:len(xid)-5]
}
var media string
@@ 219,6 230,13 @@ func servefiledata(w http.ResponseWriter
data = img.Data
}
}
+ if wantAVIF {
+ d2 := avifEncode(data)
+ if d2 != nil {
+ data = d2
+ media = "image/avif"
+ }
+ }
w.Header().Set("Content-Type", media)
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Cache-Control", "max-age="+somedays())
M go.mod +1 -1
@@ 11,7 11,7 @@ require (
humungus.tedunangst.com/r/go-sqlite3 v1.2.1
humungus.tedunangst.com/r/gonix v0.1.4
humungus.tedunangst.com/r/termvc v0.1.3
- humungus.tedunangst.com/r/webs v0.7.23
+ humungus.tedunangst.com/r/webs v0.7.26
)
require (
M go.sum +2 -2
@@ 80,5 80,5 @@ humungus.tedunangst.com/r/gonix v0.1.4 h
humungus.tedunangst.com/r/gonix v0.1.4/go.mod h1:VFBc2bPDXr1ayHOmHUutxYu8fSM+pkwK8o36h4rkORg=
humungus.tedunangst.com/r/termvc v0.1.3 h1:BYxcqdA2Ijhqolf2BdNlGw5355qE80EzAqiNgi7d5tk=
humungus.tedunangst.com/r/termvc v0.1.3/go.mod h1:TnlG9PbH77OpEf46iDyb/H9drjegQNwhpXalmGGrbhU=
-humungus.tedunangst.com/r/webs v0.7.23 h1:LEamoWvtgBOukGuzHj/T1qhRwNQkoz2RMCiwvvxwIug=
-humungus.tedunangst.com/r/webs v0.7.23/go.mod h1:ylhqHSPI0Oi7b4nsnx5mSO7AjLXN7wFpEHayLfN/ugk=
+humungus.tedunangst.com/r/webs v0.7.26 h1:Sfak8XRalfEYHiE1gh2ZW6wSxCq8tBxp8FxD5iD9c5Y=
+humungus.tedunangst.com/r/webs v0.7.26/go.mod h1:ylhqHSPI0Oi7b4nsnx5mSO7AjLXN7wFpEHayLfN/ugk=
M honk.go +1 -0
@@ 53,6 53,7 @@ type UserOptions struct {
ChatPubKey string
ChatSecKey string
TOTP string `json:",omitempty"`
+ Trigger string `json:",omitempty"`
}
type KeyInfo struct {
M main.go +12 -4
@@ 50,19 50,18 @@ var serverMsg template.HTML
var aboutMsg template.HTML
var loginMsg template.HTML
var collectForwards = true
+var convertAVIF = false
+var acceptAVIF = false
func serverURL(u string, args ...interface{}) string {
return fmt.Sprintf("https://"+serverName+u, args...)
}
func ElaborateUnitTests() {
- data, _ := os.ReadFile("input.jpg")
- d2 := avifEncode(data)
- os.WriteFile("output.avif", d2, 0600)
}
func avifEncode(data []byte) []byte {
- return lazif.Encode(data)
+ return lazif.EncodeJPEG(data)
}
func unplugserver(hostname string) {
@@ 185,6 184,15 @@ func main() {
getconfig("honkwindow", &honkwindow)
honkwindow *= 24 * time.Hour
getconfig("collectforwards", &collectForwards)
+ getconfig("convertavif", &convertAVIF)
+ if convertAVIF {
+ if !lazif.Load().HasAVIF() {
+ elog.Printf("libavif could not be loaded")
+ convertAVIF = false
+ } else {
+ getconfig("acceptavif", &acceptAVIF)
+ }
+ }
prepareStatements(db)
M views/account.html +12 -5
@@ 7,19 7,26 @@
<form id="aboutform" action="/saveuser" method="POST">
<input type="hidden" name="CSRF" value="{{ .UserCSRF }}">
<p>about me:
-<p><textarea name="whatabout">{{ .WhatAbout }}</textarea>
+<br><textarea tabindex=1 name="whatabout">{{ .WhatAbout }}</textarea>
+
+<p>trigger:
+<br><input tabindex=1 name="trigger" value="{{ .User.Options.Trigger }}">
+
<p><label class="button" for="skinny">skinny layout:</label>
<input tabindex=1 type="checkbox" id="skinny" name="skinny" value="skinny" {{ if .User.Options.SkinnyCSS }}checked{{ end }}><span></span>
-<p><label class="button" for="omitimages">omit images:</label>
-<input tabindex=1 type="checkbox" id="omitimages" name="omitimages" value="omitimages" {{ if .User.Options.OmitImages }}checked{{ end }}><span></span>
+
<p><label class="button" for="mentionall">mention all:</label>
<input tabindex=1 type="checkbox" id="mentionall" name="mentionall" value="mentionall" {{ if .User.Options.MentionAll }}checked{{ end }}><span></span>
+
<p><label class="button" for="inlineqts">inline quotes:</label>
<input tabindex=1 type="checkbox" id="inlineqts" name="inlineqts" value="inlineqts" {{ if .User.Options.InlineQuotes }}checked{{ end }}><span></span>
+
<p><label class="button" for="maps">apple map links:</label>
<input tabindex=1 type="checkbox" id="maps" name="maps" value="apple" {{ if eq "apple" .User.Options.MapLink }}checked{{ end }}><span></span>
+
<p><label class="button" for="enabletotp">make logins hard:</label>
<input tabindex=1 type="checkbox" id="enabletotp" name="enabletotp" value="enabletotp" {{ if .User.Options.TOTP }}checked{{ end }}><span></span>
+
<p><label class="button" for="reaction">reaction:</label>
<select tabindex=1 name="reaction">
<option {{ and (eq .User.Options.Reaction "none") "selected" }}>none</option>
@@ 32,7 39,7 @@
<option {{ and (eq .User.Options.Reaction "\U0001FA93") "selected" }}>{{ "\U0001FA93" }}</option>
<option {{ and (eq .User.Options.Reaction "\U0001F9EF") "selected" }}>{{ "\U0001F9EF" }}</option>
</select>
-<p><button>update settings</button>
+<p><button tabindex=1>update settings</button>
</form>
</div>
<hr>
@@ 42,7 49,7 @@
<p>change password
<p><input tabindex=1 type="password" name="oldpass"> - oldpass
<p><input tabindex=1 type="password" name="newpass"> - newpass
-<p><button>change</button>
+<p><button tabindex=1>change</button>
</form>
</div>
{{ if .User.Options.TOTP }}
M views/common.js +1 -1
@@ 1,7 1,7 @@
let re_link = /https?:[^ ]*/
function hotkey(e) {
- if (e.ctrlKey || e.altKey)
+ if (e.ctrlKey || e.altKey || e.metaKey)
return
if (e.code == "Escape") {
var menu = document.getElementById("topmenu")
M views/honk.html +0 -1
@@ 2,7 2,6 @@
{{ $bonkcsrf := .BonkCSRF }}
{{ $IsPreview := .IsPreview }}
{{ $maplink := .MapLink }}
-{{ $omitimages := .OmitImages }}
{{ with .Honk }}
<header>
{{ if $bonkcsrf }}
M web.go +18 -6
@@ 1563,7 1563,11 @@ func (d *Donk) HTML() template.HTML {
if d.Media == "text/plain" || d.Media == "application/pdf" {
return templates.Sprintf(`<p><a href="/d/%s">Attachment: %s</a>%s (%d)</p>`, d.XID, d.Name, desc, d.Meta.Length)
} else {
- return templates.Sprintf(`<picture><source type="image/avif" srcset="/d/%s.avif"><img class="donk donklink" src="/d/%s" loading=lazy title="%s" alt="%s" width="%d" height="%d"></picture>`, d.XID, d.XID, d.Desc, d.Desc, d.Meta.Width, d.Meta.Height)
+ var sources template.HTML
+ if convertAVIF && d.Media == "image/jpeg" {
+ sources += templates.Sprintf(`<source type="image/avif" srcset="/d/%s.avif">`, d.XID)
+ }
+ return templates.Sprintf(`<picture>%s<img class="donk donklink" src="/d/%s" loading=lazy title="%s" alt="%s" width="%d" height="%d"></picture>`, sources, d.XID, d.Desc, d.Desc, d.Meta.Width, d.Meta.Height)
}
} else {
if d.External {
@@ 1584,8 1588,8 @@ func saveuser(w http.ResponseWriter, r *
db := opendatabase()
options := user.Options
+ options.Trigger = r.FormValue("trigger")
options.SkinnyCSS = r.FormValue("skinny") == "skinny"
- options.OmitImages = r.FormValue("omitimages") == "omitimages"
options.MentionAll = r.FormValue("mentionall") == "mentionall"
options.InlineQuotes = r.FormValue("inlineqts") == "inlineqts"
options.MapLink = r.FormValue("maps")
@@ 1976,10 1980,19 @@ func formtodonk(w http.ResponseWriter, r
io.Copy(&buf, file)
file.Close()
data := buf.Bytes()
- var media, name string
+ var xid, media, name string
var donkmeta DonkMeta
- img, err := bigshrink(data)
- if err == nil {
+ if needfilehash(data, &xid) == "" {
+ d := getfileinfo(xid)
+ if d == nil {
+ elog.Printf("lost a file xid somehow: %s", xid)
+ } else {
+ name = d.Name
+ donkmeta.Width = d.Meta.Width
+ donkmeta.Height = d.Meta.Height
+ media = d.Media
+ }
+ } else if img, err := bigshrink(data); err == nil {
data = img.Data
donkmeta.Width = img.Width
donkmeta.Height = img.Height
@@ 3280,7 3293,6 @@ func serve() {
go tracker()
go syndicator()
go bgmonitor()
- go qotd()
loadLingo()
emuinit()