# HG changeset patch # User Peter Sanchez # Date 1680884999 21600 # Fri Apr 07 10:29:59 2023 -0600 # Node ID 2953b00b64c0c5ff13766213226c97460d72dffa # Parent d6113fb0ab06e3b097fb6505524c302bd8f2905c # Parent a985912e2e747f67fff80a2891c635e912f5317f Merge tedu upstream diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -2,3 +2,4 @@ memes emus honk +avatarcache diff --git a/activity.go b/activity.go --- a/activity.go +++ b/activity.go @@ -167,7 +167,9 @@ defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, fmt.Errorf("http get status: %d", resp.StatusCode) + errorSample := make([]byte, 100) + io.ReadFull(resp.Body, errorSample) + return nil, fmt.Errorf("http get status: %d [%s]", resp.StatusCode, errorSample) } return junk.Read(resp.Body) } @@ -188,6 +190,9 @@ Timeout: timeout, Client: client, }) + if err != nil { + dlog.Printf("Outbound (GetJunkTimeout) Request: %v Failed! %v", url, err) + } return j, err } @@ -1721,6 +1726,18 @@ if info.Owner == "" { info.Owner = info.XID } + + iconInfo, ok := obj.GetMap("icon") + if ok { + mType, _ := iconInfo.GetString("mediaType") + if strings.HasPrefix(mType, "image/") { + AvatarUrl, ok := iconInfo.GetString("url") + if ok { + info.AvatarURL = AvatarUrl + } + } + } + return info, nil } diff --git a/database.go b/database.go --- a/database.go +++ b/database.go @@ -35,6 +35,8 @@ "humungus.tedunangst.com/r/webs/mz" ) +const honkwindow time.Duration = 90 * 24 * time.Hour + //go:embed schema.sql var sqlSchema string @@ -186,7 +188,7 @@ } func getpublichonks() []*Honk { - dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + dt := time.Now().Add(-honkwindow).UTC().Format(dbtimeformat) rows, err := stmtPublicHonks.Query(dt, 100) return getsomehonks(rows, err) } @@ -222,7 +224,7 @@ return honks } func gethonksbyuser(name string, includeprivate bool, wanted int64) []*Honk { - dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + dt := time.Now().Add(-honkwindow).UTC().Format(dbtimeformat) limit := 50 whofore := 2 if includeprivate { @@ -232,18 +234,18 @@ return getsomehonks(rows, err) } func gethonksforuser(userid int64, wanted int64) []*Honk { - dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + dt := time.Now().Add(-honkwindow).UTC().Format(dbtimeformat) rows, err := stmtHonksForUser.Query(wanted, userid, dt, userid, userid) return getsomehonks(rows, err) } func gethonksforuserfirstclass(userid int64, wanted int64) []*Honk { - dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + dt := time.Now().Add(-honkwindow).UTC().Format(dbtimeformat) rows, err := stmtHonksForUserFirstClass.Query(wanted, userid, dt, userid, userid) return getsomehonks(rows, err) } func gethonksforme(userid int64, wanted int64) []*Honk { - dt := time.Now().Add(-7 * 24 * time.Hour).UTC().Format(dbtimeformat) + dt := time.Now().Add(-honkwindow).UTC().Format(dbtimeformat) rows, err := stmtHonksForMe.Query(wanted, userid, dt, userid) return getsomehonks(rows, err) } @@ -1088,6 +1090,30 @@ } } +func getusercount() int { + row := stmtGetUserCount.QueryRow() + var count int + row.Scan(&count) + return count +} + +func getactiveusercount(monthsago int) int { + origin := time.Now().AddDate(0, -monthsago, 0).UTC().Format(dbtimeformat) + row := stmtGetActiveUserCount.QueryRow(origin) + + var count int + row.Scan(&count) + return count +} + +func getlocalhonkcount() int { + row := stmtGetLocalHonkCount.QueryRow() + + var count int + row.Scan(&count) + return count +} + var stmtHonkers, stmtDubbers, stmtNamedDubbers, stmtSaveHonker, stmtUpdateFlavor, stmtUpdateHonker *sql.Stmt var stmtDeleteHonker *sql.Stmt var stmtAnyXonk, stmtOneXonk, stmtPublicHonks, stmtUserHonks, stmtHonksByCombo, stmtHonksByConvoy *sql.Stmt @@ -1106,6 +1132,9 @@ var stmtHonksISaved, stmtGetFilters, stmtSaveFilter, stmtDeleteFilter *sql.Stmt var stmtGetTracks *sql.Stmt var stmtSaveChonk, stmtLoadChonks, stmtGetChatters *sql.Stmt +var stmtGetUserCount *sql.Stmt +var stmtGetActiveUserCount *sql.Stmt +var stmtGetLocalHonkCount *sql.Stmt func preparetodie(db *sql.DB, s string) *sql.Stmt { stmt, err := db.Prepare(s) @@ -1116,7 +1145,7 @@ } func prepareStatements(db *sql.DB) { - stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos, meta from honkers where userid = ? and (flavor = 'presub' or flavor = 'sub' or flavor = 'peep' or flavor = 'unsub') order by name") + stmtHonkers = preparetodie(db, "select honkerid, userid, name, xid, flavor, combos, meta from honkers where userid = ? and (flavor = 'presub' or flavor = 'sub' or flavor = 'peep' or flavor = 'unsub') order by name collate nocase") stmtSaveHonker = preparetodie(db, "insert into honkers (userid, name, xid, flavor, combos, owner, meta, folxid) values (?, ?, ?, ?, ?, ?, ?, '')") stmtUpdateFlavor = preparetodie(db, "update honkers set flavor = ?, folxid = ? where userid = ? and name = ? and xid = ? and flavor = ?") stmtUpdateHonker = preparetodie(db, "update honkers set name = ?, combos = ?, meta = ? where honkerid = ? and userid = ?") @@ -1192,4 +1221,7 @@ stmtSaveChonk = preparetodie(db, "insert into chonks (userid, xid, who, target, dt, noise, format) values (?, ?, ?, ?, ?, ?, ?)") stmtLoadChonks = preparetodie(db, "select chonkid, userid, xid, who, target, dt, noise, format from chonks where userid = ? and dt > ? order by chonkid asc") stmtGetChatters = preparetodie(db, "select distinct(target) from chonks where userid = ?") + stmtGetUserCount = preparetodie(db, "select count(*) from users where userid > 0") + stmtGetActiveUserCount = preparetodie(db, "select count(distinct honker) from honks where whofore = 2 and dt > ?") + stmtGetLocalHonkCount = preparetodie(db, "select count(*) from honks where whofore = 2") } diff --git a/honk.go b/honk.go --- a/honk.go +++ b/honk.go @@ -242,10 +242,11 @@ } type SomeThing struct { - What int - XID string - Owner string - Name string + What int + XID string + Owner string + Name string + AvatarURL string } const ( @@ -363,8 +364,8 @@ adduser() case "deluser": if len(args) < 2 { - fmt.Printf("usage: honk deluser username\n") - return + fmt.Fprintf(os.Stderr, "usage: honk deluser username\n") + os.Exit(1) } deluser(args[1]) case "chpass": @@ -417,29 +418,29 @@ cleanupdb(arg) case "unplug": if len(args) < 2 { - fmt.Printf("usage: honk unplug servername\n") - return + fmt.Fprintf(os.Stderr, "usage: honk unplug servername\n") + os.Exit(1) } name := args[1] unplugserver(name) case "backup": if len(args) < 2 { - fmt.Printf("usage: honk backup dirname\n") - return + fmt.Fprintf(os.Stderr, "usage: honk backup dirname\n") + os.Exit(1) } name := args[1] svalbard(name) case "ping": if len(args) < 3 { - fmt.Printf("usage: honk ping (from username) (to username or url)\n") - return + fmt.Fprintf(os.Stderr, "usage: honk ping (from username) (to username or url)\n") + os.Exit(1) } name := args[1] targ := args[2] user, err := butwhatabout(name) if err != nil { elog.Printf("unknown user") - return + os.Exit(1) } ping(user, targ) case "run": diff --git a/views/honk.html b/views/honk.html --- a/views/honk.html +++ b/views/honk.html @@ -10,7 +10,7 @@ {{ else }} {{ end }} - + {{ if .Oonker }} {{ if $bonkcsrf }} @@ -18,7 +18,7 @@ {{ else }} {{ end }} - + {{ end }}

@@ -72,7 +72,7 @@ {{ if $omitimages }}

Image: {{ .Name }}{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }} {{ else }} -

{{ .Desc }} +

{{ .Desc }} {{ end }} {{ end }} {{ else }} @@ -82,7 +82,7 @@ {{ if eq .Media "video/mp4" }}

{{ else }} -

{{ .Desc }} +

{{ .Desc }} {{ end }} {{ end }} {{ end }} diff --git a/views/honkers.html b/views/honkers.html --- a/views/honkers.html +++ b/views/honkers.html @@ -27,7 +27,7 @@ {{ range .Honkers }}

-avatar +avatar

{{ .Name }}

diff --git a/views/honkform.html b/views/honkform.html --- a/views/honkform.html +++ b/views/honkform.html @@ -53,3 +53,23 @@ + diff --git a/web.go b/web.go --- a/web.go +++ b/web.go @@ -17,7 +17,9 @@ import ( "bytes" + "crypto/sha256" "database/sql" + "encoding/hex" "fmt" "html/template" "io" @@ -2109,6 +2111,7 @@ return } if stealthmode(user.ID, r) { + dlog.Printf("not serving webfinger due to user agent not having a link: %v", r.UserAgent()) http.NotFound(w, r) return } @@ -2126,11 +2129,119 @@ j.Write(w) } +func knowninformation(w http.ResponseWriter, r *http.Request) { + j := junk.New() + l := junk.New() + + l["rel"] = `http://nodeinfo.diaspora.software/ns/schema/2.0` + l["href"] = fmt.Sprintf("https://%s/nodeinfo/2.0", serverName) + j["links"] = []junk.Junk{l} + + w.Header().Set("Content-Type", "application/json") + j.Write(w) +} + +func actualinformation(w http.ResponseWriter, r *http.Request) { + j := junk.New() + + soft := junk.New() + soft["name"] = "honk" + soft["version"] = softwareVersion + + services := junk.New() + services["inbound"] = []string{} + services["outbound"] = []string{"rss2.0"} + + users := junk.New() + users["total"] = getusercount() + users["activeHalfyear"] = getactiveusercount(6) + users["activeMonth"] = getactiveusercount(1) + + usage := junk.New() + usage["users"] = users + usage["localPosts"] = getlocalhonkcount() + + j["version"] = "2.0" + j["protocols"] = []string{"activitypub"} + j["software"] = soft + j["services"] = services + j["openRegistrations"] = false + j["usage"] = usage + + w.Header().Set("Content-Type", "application/json") + j.Write(w) +} + func somedays() string { secs := 432000 + notrand.Int63n(432000) return fmt.Sprintf("%d", secs) } +type avaKey struct { + uid int64 + url string +} + +var avacache = cache.New(cache.Options{Filler: func(key avaKey) (string, bool) { + j, err := GetJunkFast(key.uid, key.url) + if err != nil { + dlog.Println("avatating: getting junk:", err) + return "", false + } + pfpurl, _ := j.GetString("icon", "url") + res, err := http.Get(pfpurl) + if err != nil { + dlog.Println("avatating: getting pfp url:", err) + return "", false + } + if res.StatusCode != 200 { + dlog.Printf("avatating: %s: not ok: %d", key.url, res.StatusCode) + return "", false + } + defer res.Body.Close() + + data, err := io.ReadAll(res.Body) + if err != nil { + dlog.Println("avatating: io.ReadAll(), whoops:", err) + return "", false + } + + img, err := shrinkit(data) + if err != nil { + return "", false + } + data = img.Data + format := img.Format + desc := fmt.Sprintf("avatar for %s", key.url) + media := "image/" + format + if format == "jpeg" { + format = "jpg" + } + name := xfiltrate() + "." + format + _, xid, err := savefileandxid(name, desc, "", media, true, data) + if err != nil { + elog.Printf("avatate: unable to save image: %s", err) + return "", false + } + return xid, true +}, Reducer: func(key avaKey) string { + return key.url +}, Duration: 24 * 7 * time.Hour}) + +func isURL(s string) bool { + u, err := url.Parse(s) + return err == nil && u.Scheme != "" && u.Host != "" +} + +func avatateautogen(w http.ResponseWriter, r *http.Request) { + n := r.FormValue("a") + a := genAvatar(n) + if !develMode { + w.Header().Set("Cache-Control", "max-age="+somedays()) + } + w.Write(a) +} + func lookatme(ava string) string { if strings.Contains(ava, serverName+"/"+userSep) { idx := strings.LastIndexByte(ava, '/') @@ -2149,16 +2260,75 @@ if develMode { loadAvatarColors() } + var uid int64 n := r.FormValue("a") + + if !isURL(n) { + avatateautogen(w, r) + return + } + + hasher := sha256.New() + hasher.Write([]byte(n)) + hashString := hex.EncodeToString(hasher.Sum(nil)) + + fileKey := fmt.Sprintf("%s/avatarcache/%s", dataDir, hashString) + s, err := os.Stat(fileKey) + if err == nil { + if time.Since(s.ModTime()) < (time.Hour * 24 * 7) { + b, _ := os.ReadFile(fileKey) + w.Header().Set("Content-Type", http.DetectContentType(b)) + if !develMode { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Cache-Control", "max-age="+somedays()) + } + w.Write(b) + return + } + } + if redir := lookatme(n); redir != "" { http.Redirect(w, r, redir, http.StatusSeeOther) return } - a := genAvatar(n) + if !develMode { w.Header().Set("Cache-Control", "max-age="+somedays()) } - w.Write(a) + + uinfo := login.GetUserInfo(r) + if uinfo != nil { + uid = uinfo.UserID + } else { + uid = 0 + } + + // Else, we fetch it now + xid := n + // j, err := GetJunk(u.UserID, xid) + j, err := GetJunk(uid, xid) + if err != nil { + avatateautogen(w, r) + return + } + + info, _ := somethingabout(j) + if info.AvatarURL == "" { + avatateautogen(w, r) + return + } + imageBytes, err := fetchsome(info.AvatarURL) + if err != nil { + avatateautogen(w, r) + return + } + w.Header().Set("Content-Type", http.DetectContentType(imageBytes)) + w.Write(imageBytes) + + go func() { + os.MkdirAll(fmt.Sprintf("%s/avatarcache/", dataDir), 0755) + os.WriteFile(fileKey, imageBytes, 0644) + }() } func serveviewasset(w http.ResponseWriter, r *http.Request) { @@ -2214,23 +2384,32 @@ http.ServeFile(w, r, dataDir+"/memes/"+meme) } -func servefile(w http.ResponseWriter, r *http.Request) { - xid := mux.Vars(r)["xid"] +func reallyservefile(xid string, w http.ResponseWriter) error { var media string var data []byte row := stmtGetFileData.QueryRow(xid) err := row.Scan(&media, &data) if err != nil { elog.Printf("error loading file: %s", err) - http.NotFound(w, r) - return + return err } - w.Header().Set("Content-Type", media) - w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("Cache-Control", "max-age="+somedays()) + + if !develMode { + w.Header().Set("Content-Type", media) + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Cache-Control", "max-age="+somedays()) + } w.Write(data) + return nil } +func servefile(w http.ResponseWriter, r *http.Request) { + xid := mux.Vars(r)["xid"] + err := reallyservefile(xid, w) + if err != nil { + http.NotFound(w, r) + } +} func nomoroboto(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "User-agent: *\n") io.WriteString(w, "Disallow: /a\n") @@ -2587,6 +2766,8 @@ getters.HandleFunc("/emu/{emu:[^.]*[^/]+}", serveemu) getters.HandleFunc("/meme/{meme:[^.]*[^/]+}", servememe) getters.HandleFunc("/.well-known/webfinger", fingerlicker) + getters.HandleFunc("/.well-known/nodeinfo", knowninformation) + getters.HandleFunc("/nodeinfo/2.0", actualinformation) getters.HandleFunc("/flag/{code:.+}", showflag)