package main import ( "encoding/json" "fmt" "html/template" "io" "net/http" "os" "strings" "time" ) // PDSAccount represents a Bluesky account on the PDS type PDSAccount struct { DID string `json:"did"` Handle string `json:"handle"` DisplayName string `json:"displayName"` Description string `json:"description"` Avatar string `json:"avatar"` } // handleAccountsDirectory serves the 1440.news accounts directory page func (c *Crawler) handleAccountsDirectory(w http.ResponseWriter, r *http.Request) { pdsHost := os.Getenv("PDS_HOST") if pdsHost == "" { pdsHost = "https://pds.1440.news" } // Fetch all repos from PDS listReposURL := pdsHost + "/xrpc/com.atproto.sync.listRepos?limit=1000" resp, err := http.Get(listReposURL) if err != nil { http.Error(w, "Failed to fetch accounts: "+err.Error(), http.StatusInternalServerError) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var reposResp struct { Repos []struct { DID string `json:"did"` Head string `json:"head"` Active bool `json:"active"` } `json:"repos"` } if err := json.Unmarshal(body, &reposResp); err != nil { http.Error(w, "Failed to parse repos: "+err.Error(), http.StatusInternalServerError) return } // Fetch profile for each account using unauthenticated endpoints var accounts []PDSAccount client := &http.Client{Timeout: 5 * time.Second} for _, repo := range reposResp.Repos { if !repo.Active { continue } // Get handle using describeRepo describeURL := pdsHost + "/xrpc/com.atproto.repo.describeRepo?repo=" + repo.DID describeResp, err := client.Get(describeURL) if err != nil { continue } describeBody, _ := io.ReadAll(describeResp.Body) describeResp.Body.Close() var repoInfo struct { Handle string `json:"handle"` DID string `json:"did"` } if err := json.Unmarshal(describeBody, &repoInfo); err != nil { continue } // Skip the main 1440.news account (directory account itself) if repoInfo.Handle == "1440.news" { continue } account := PDSAccount{ DID: repoInfo.DID, Handle: repoInfo.Handle, } // Get profile record for display name, description, avatar recordURL := pdsHost + "/xrpc/com.atproto.repo.getRecord?repo=" + repo.DID + "&collection=app.bsky.actor.profile&rkey=self" recordResp, err := client.Get(recordURL) if err == nil { recordBody, _ := io.ReadAll(recordResp.Body) recordResp.Body.Close() var record struct { Value struct { DisplayName string `json:"displayName"` Description string `json:"description"` Avatar struct { Ref struct { Link string `json:"$link"` } `json:"ref"` } `json:"avatar"` } `json:"value"` } if json.Unmarshal(recordBody, &record) == nil { account.DisplayName = record.Value.DisplayName account.Description = record.Value.Description if record.Value.Avatar.Ref.Link != "" { account.Avatar = pdsHost + "/xrpc/com.atproto.sync.getBlob?did=" + repo.DID + "&cid=" + record.Value.Avatar.Ref.Link } } } accounts = append(accounts, PDSAccount{ DID: account.DID, Handle: account.Handle, DisplayName: account.DisplayName, Description: account.Description, Avatar: account.Avatar, }) } // Render the page tmpl := template.Must(template.New("accounts").Parse(accountsDirectoryHTML)) w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, map[string]interface{}{ "Accounts": accounts, "Count": len(accounts), }) } func (c *Crawler) handleDashboard(w http.ResponseWriter, r *http.Request) { stats, err := c.GetDashboardStats() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } funcMap := template.FuncMap{ "pct": func(a, b int) float64 { if b == 0 { return 0 } return float64(a) * 100.0 / float64(b) }, "comma": func(n interface{}) string { var val int switch v := n.(type) { case int: val = v case int32: val = int(v) case int64: val = int(v) default: return "0" } if val < 0 { return "-" + commaFormat(-val) } return commaFormat(val) }, } tmpl, err := template.New("dashboard").Funcs(funcMap).Parse(dashboardHTML) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html") tmpl.Execute(w, stats) } func (c *Crawler) handleAPIStats(w http.ResponseWriter, r *http.Request) { stats, err := c.GetDashboardStats() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(stats) } // handleRedirect handles short URL redirects for url.1440.news func (c *Crawler) handleRedirect(w http.ResponseWriter, r *http.Request) { code := strings.TrimPrefix(r.URL.Path, "/") if code == "" { http.NotFound(w, r) return } // Look up the short URL shortURL, err := c.GetShortURL(code) if err != nil { http.NotFound(w, r) return } // Record the click asynchronously go func() { if err := c.RecordClick(code, r); err != nil { fmt.Printf("Failed to record click for %s: %v\n", code, err) } }() // Redirect to original URL http.Redirect(w, r, shortURL.OriginalURL, http.StatusFound) } const accountsDirectoryHTML = ` 1440.news - News Feed Directory

1440.news

Curated news feeds on Bluesky

{{.Count}} feeds available

{{range .Accounts}} {{else}}

No feeds available yet.

{{end}}

No feeds match your search.

` const dashboardHTML = ` 1440.news Feed Crawler

Domains

{{comma .TotalDomains}}
All
{{comma .PassDomains}}
Pass
{{comma .SkipDomains}}
Skip
{{comma .HoldDomains}}
Hold
{{comma .DeadDomains}}
Dead
{{comma .DomainCheckRate}}
alive/min
{{comma .FeedCrawlRate}}
crawl/min
{{comma .FeedCheckRate}}
check/min

Feeds

{{comma .TotalFeeds}}
All
{{comma .AliveFeeds}}
Alive
{{comma .PublishFeeds}}
Pass
{{comma .SkipFeeds}}
Skip
{{comma .HoldFeeds}}
Hold
{{comma .DeadFeeds}}
Dead
{{comma .EmptyFeeds}}
Empty
{{comma .RSSFeeds}}
RSS
{{comma .AtomFeeds}}
Atom
{{comma .JSONFeeds}}
JSON
{{comma .UnknownFeeds}}
Unknown
Last updated: {{.UpdatedAt.Format "2006-01-02 15:04:05"}}
`