adde927da58c — Peter Sanchez 9 months ago
Merge tedu upstream
8 files changed, 266 insertions(+), 19 deletions(-)

M .hgignore
M activity.go
M database.go
M honk.go
M views/honk.html
M views/honkers.html
M views/honkform.html
M web.go
M .hgignore +1 -0
@@ 6,6 6,7 @@ syntax: glob
 memes
 emus
 honk
+avatarcache
 violations.json
 docs/activitypub.7.html
 docs/hfcs.1.html

          
M activity.go +15 -0
@@ 165,6 165,9 @@ func GetJunkTimeout(userid int64, url st
 			Client:  &client,
 			Fixup:   sign,
 		})
+		if err != nil {
+			dlog.Printf("Outbound (GetJunkTimeout) Request: %v Failed! %v", url, err)
+		}
 		return j, err
 	}
 

          
@@ 1745,6 1748,18 @@ func somethingabout(obj junk.Junk) (*Som
 	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
 }
 

          
M database.go +31 -1
@@ 1145,6 1145,30 @@ func cleanupdb(arg string) {
 	}
 }
 
+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

          
@@ 1163,6 1187,9 @@ var stmtSaveMeta, stmtDeleteAllMeta, stm
 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
 var stmtDeliquentCheck, stmtDeliquentUpdate *sql.Stmt
 
 func preparetodie(db *sql.DB, s string) *sql.Stmt {

          
@@ 1174,7 1201,7 @@ func preparetodie(db *sql.DB, s string) 
 }
 
 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 = ?")

          
@@ 1250,6 1277,9 @@ func prepareStatements(db *sql.DB) {
 	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")
 	stmtDeliquentCheck = preparetodie(db, "select dooverid, msg from doovers where userid = ? and rcpt = ?")
 	stmtDeliquentUpdate = preparetodie(db, "update doovers set msg = ? where dooverid = ?")
 }

          
M honk.go +5 -4
@@ 253,10 253,11 @@ type HonkerMeta struct {
 }
 
 type SomeThing struct {
-	What  int
-	XID   string
-	Owner string
-	Name  string
+	What      int
+	XID       string
+	Owner     string
+	Name      string
+	AvatarURL string
 }
 
 const (

          
M views/honk.html +4 -4
@@ 10,7 10,7 @@ 
 {{ else }}
 <a href="{{ .Honker }}" rel=noreferrer>
 {{ end }}
-<img alt="" src="/a?a={{ .Honker}}">
+<img alt="" src="/a?a={{ .Honker}}" loading="lazy">
 </a>
 {{ if .Oonker }}
 {{ if $bonkcsrf }}

          
@@ 18,7 18,7 @@ 
 {{ else }}
 <a href="{{ .Oonker }}" rel=noreferrer>
 {{ end }}
-<img alt="" src="/a?a={{ .Oonker}}">
+<img alt="" src="/a?a={{ .Oonker}}" loading="lazy">
 </a>
 {{ end }}
 <p>

          
@@ 72,7 72,7 @@ in reply to: <a href="{{ .RID }}" rel=no
 {{ if $omitimages }}
 <p><a href="/d/{{ .XID }}">Image: {{ .Name }}</a>{{ if not (eq .Desc .Name) }} {{ .Desc }}{{ end }}
 {{ else }}
-<p><img src="/d/{{ .XID }}" loading=lazy title="{{ .Desc }}" alt="{{ .Desc }}">
+<p><img src="/d/{{ .XID }}" title="{{ .Desc }}" alt="{{ .Desc }}" loading="lazy">
 {{ end }}
 {{ end }}
 {{ else }}

          
@@ 82,7 82,7 @@ in reply to: <a href="{{ .RID }}" rel=no
 {{ if eq .Media "video/mp4" }}
 <p><video controls src="{{ .URL }}">{{ .Name }}</video>
 {{ else }}
-<p><img src="{{ .URL }}" title="{{ .Desc }}" alt="{{ .Desc }}">
+<p><img src="{{ .URL }}" title="{{ .Desc }}" alt="{{ .Desc }}" loading="lazy">
 {{ end }}
 {{ end }}
 {{ end }}

          
M views/honkers.html +1 -1
@@ 27,7 27,7 @@ 
 {{ range .Honkers }}
 <section class="honk">
 <header>
-<img alt="avatar" src="/a?a={{ .XID }}">
+<img alt="avatar" src="/a?a={{ .XID }}" loading="lazy">
 <p class="font18em"><a href="/h/{{ .Name }}">{{ .Name }}</a>
 </header>
 <p>

          
M views/honkform.html +20 -0
@@ 52,3 52,23 @@ 
 <button name="preview" value="preview">preview</button>
 <button type=button name="cancel" value="cancel">cancel</button>
 </form>
+<script>
+function addemu(data){
+	const box = document.getElementById("honknoise");
+	box.value += data;
+}
+function loademus() {
+	div = document.getElementById("emupicker")
+	request = new XMLHttpRequest();
+	request.open('GET', '/emus')
+	request.onload = function(){
+		div.innerHTML = request.responseText
+	}
+	if (div.style.display === "none") {
+		div.style.display = "block";
+	} else {
+		div.style.display = "none";
+	}
+	request.send()
+}
+</script>

          
M web.go +189 -9
@@ 17,8 17,10 @@ package main
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"crypto/sha512"
 	"database/sql"
+	"encoding/hex"
 	"fmt"
 	"html/template"
 	"io"

          
@@ 2337,11 2339,119 @@ func fingerlicker(w http.ResponseWriter,
 	}
 }
 
+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, '/')

          
@@ 2360,16 2470,75 @@ func avatate(w http.ResponseWriter, r *h
 	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) {

          
@@ 2425,23 2594,32 @@ func servememe(w http.ResponseWriter, r 
 	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")

          
@@ 2838,6 3016,8 @@ func serve() {
 	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)