Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inspect metadata of people I follow #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ toolchain go1.22.2
require (
github.com/PuerkitoBio/goquery v1.9.2
github.com/bluesky-social/indigo v0.0.0-20240503174839-ef8e99bfcc76
github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.16.0
github.com/gorilla/websocket v1.5.1
github.com/ipfs/go-cid v0.4.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
Expand Down
15 changes: 14 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,24 @@ func main() {
HelpName: "follows",
Action: doFollows,
},
{
Name: "inactive-follows",
Description: "Show inactive follows",
Usage: "Show inactive follows",
UsageText: "bsky inactive-follows",
Flags: []cli.Flag{
&cli.StringFlag{Name: "handle", Aliases: []string{"H"}, Value: "", Usage: "user handle"},
&cli.BoolFlag{Name: "json", Usage: "output JSON"},
&cli.BoolFlag{Name: "strict", Usage: "output only inactive"},
},
HelpName: "inactive-follows",
Action: doInactiveFollows,
},
{
Name: "followers",
Description: "Show followers",
Usage: "Show followers",
UsageText: "bsky followres",
UsageText: "bsky followers",
Flags: []cli.Flag{
&cli.StringFlag{Name: "handle", Aliases: []string{"H"}, Value: "", Usage: "user handle"},
&cli.BoolFlag{Name: "json", Usage: "output JSON"},
Expand Down
129 changes: 129 additions & 0 deletions profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/bluesky-social/indigo/xrpc"
"github.com/dustin/go-humanize"
"net/http"
"os"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -298,6 +301,132 @@ func doFollows(cCtx *cli.Context) error {
return nil
}

func doInactiveFollows(cCtx *cli.Context) error {
if cCtx.Args().Present() {
return cli.ShowSubcommandHelp(cCtx)
}

xrpcc, err := makeXRPCC(cCtx)
if err != nil {
return fmt.Errorf("cannot create client: %w", err)
}

arg := cCtx.String("handle")
if arg == "" {
arg = xrpcc.Auth.Handle
}

var cursor string
for {
follows, err := bsky.GraphGetFollows(context.TODO(), xrpcc, arg, cursor, 100)
if err != nil {
return fmt.Errorf("getting record: %w", err)
}

strict := cCtx.Bool("strict")

if cCtx.Bool("json") {
for _, f := range follows.Follows {
json.NewEncoder(os.Stdout).Encode(f)
}
} else {
for _, f := range follows.Follows {
latestPost, err := getLatestPost(xrpcc, f.Handle)
if err != nil {
return fmt.Errorf("getting latest post: %w", err)
break
}
followsMe := f.Viewer.FollowedBy != nil
if strict && followsMe {
continue
}
if latestPost == nil || (latestPost != nil && latestPost.Post == nil) {
outputAccount(f, color.FgHiRed, "never", followsMe)
} else {
var prettyTime string
cor := color.FgRed

dtime, err := time.Parse(time.RFC3339, latestPost.Post.IndexedAt)
if err != nil {
// fallback to index time
prettyTime = latestPost.Post.IndexedAt
} else {
cor = colorFor(dtime)
prettyTime = humanize.Time(dtime)
}

inactiveColors := []color.Attribute{color.FgRed, color.FgYellow}
if strict && !slices.Contains(inactiveColors, cor) {
continue
}

outputAccount(f, cor, prettyTime, followsMe)
}
}
}
if follows.Cursor == nil {
break
}
cursor = *follows.Cursor
}
return nil
}

func outputAccount(f *bsky.ActorDefs_ProfileView, statusColor color.Attribute, prettyTime string, followsMe bool) {
color.Set(color.FgHiRed)
fmt.Print(f.Handle)
color.Set(color.Reset)
fmt.Printf(" [%s] ", stringp(f.DisplayName))
color.Set(color.FgBlue)
fmt.Print(f.Did)
color.Set(color.Reset)
if followsMe {
color.Set(color.FgGreen)
fmt.Printf(" [💚] ")
} else {
color.Set(color.FgRed)
fmt.Printf(" [❌] ")
}
color.Set(color.Reset)
color.Set(statusColor)
fmt.Printf(" [%s]\n", prettyTime)
color.Set(color.Reset)
}

func colorFor(date time.Time) color.Attribute {
duration := time.Now().Sub(date)

// Calculate weeks, months, and years ago
weeksAgo := 7 * 24 * time.Hour
monthAgo := 30 * 24 * time.Hour
yearAgo := 365 * 24 * time.Hour

var timeColor color.Attribute
switch {
case duration <= weeksAgo:
timeColor = color.FgGreen // Within the last week
case duration <= monthAgo:
timeColor = color.FgCyan // Within the last month
case duration <= yearAgo:
timeColor = color.FgYellow // Within the last year
default:
timeColor = color.FgRed // More than a year ago
}

return timeColor
}

func getLatestPost(xrpcc *xrpc.Client, handle string) (*bsky.FeedDefs_FeedViewPost, error) {
resp, err := bsky.FeedGetAuthorFeed(context.TODO(), xrpcc, handle, "", "", 1)
if err != nil {
return nil, fmt.Errorf("cannot get author feed: %w", err)
}
if len(resp.Feed) == 0 {
return nil, nil
}
return resp.Feed[0], nil
}

func doFollowers(cCtx *cli.Context) error {
if cCtx.Args().Present() {
return cli.ShowSubcommandHelp(cCtx)
Expand Down