Interactively go through your bluesky follow graph and decide to keep or remove follow records
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Get follow graph records, and display information for each profile

yemou 98045e1c d8506d9c

+226 -31
+3 -3
flake.lock
··· 2 2 "nodes": { 3 3 "nixpkgs": { 4 4 "locked": { 5 - "lastModified": 1770197578, 6 - "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", 5 + "lastModified": 1772773019, 6 + "narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=", 7 7 "owner": "NixOS", 8 8 "repo": "nixpkgs", 9 - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", 9 + "rev": "aca4d95fce4914b3892661bcb80b8087293536c6", 10 10 "type": "github" 11 11 }, 12 12 "original": {
+22 -3
go.mod
··· 1 - module tangled.org/yemou.pink/bsky-sieve 1 + module tangled.org/yemou.pink/atproto-sieve 2 2 3 3 go 1.25.5 4 4 5 - require github.com/bluesky-social/indigo v0.0.0-20260206210545-4bec71212487 5 + require ( 6 + github.com/bluesky-social/indigo v0.0.0-20260206210545-4bec71212487 7 + github.com/mattn/go-sixel v0.0.8 8 + golang.org/x/term v0.38.0 9 + ) 10 + 11 + require github.com/soniakeys/quant v1.0.0 // indirect 6 12 7 13 require ( 8 14 github.com/beorn7/perks v1.0.1 // indirect ··· 11 17 github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 12 18 github.com/google/go-querystring v1.1.0 // indirect 13 19 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 20 + github.com/ipfs/go-cid v0.4.1 // indirect 21 + github.com/klauspost/cpuid/v2 v2.2.7 // indirect 14 22 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 23 + github.com/minio/sha256-simd v1.0.1 // indirect 15 24 github.com/mr-tron/base58 v1.2.0 // indirect 25 + github.com/multiformats/go-base32 v0.1.0 // indirect 26 + github.com/multiformats/go-base36 v0.2.0 // indirect 27 + github.com/multiformats/go-multibase v0.2.0 // indirect 28 + github.com/multiformats/go-multihash v0.2.3 // indirect 29 + github.com/multiformats/go-varint v0.0.7 // indirect 16 30 github.com/prometheus/client_golang v1.17.0 // indirect 17 31 github.com/prometheus/client_model v0.5.0 // indirect 18 32 github.com/prometheus/common v0.45.0 // indirect 19 33 github.com/prometheus/procfs v0.12.0 // indirect 34 + github.com/spaolacci/murmur3 v1.1.0 // indirect 35 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 20 36 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 21 37 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 22 38 golang.org/x/crypto v0.21.0 // indirect 23 - golang.org/x/sys v0.22.0 // indirect 39 + golang.org/x/image v0.36.0 40 + golang.org/x/sys v0.39.0 // indirect 24 41 golang.org/x/time v0.3.0 // indirect 42 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 25 43 google.golang.org/protobuf v1.33.0 // indirect 44 + lukechampine.com/blake3 v1.2.1 // indirect 26 45 )
+11 -2
go.sum
··· 21 21 github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 22 22 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 23 23 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 24 + github.com/mattn/go-sixel v0.0.8 h1:H0bBGQVOJoSvzvtTgCInxvg1IZiNlTcIIIx8A6uvjpQ= 25 + github.com/mattn/go-sixel v0.0.8/go.mod h1:wbDSbrwpykVI1qEHyjZYsDgaJTwpVg9wSwmmh2slnBw= 24 26 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 25 27 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 26 28 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= ··· 47 49 github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 48 50 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 49 51 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 52 + github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y= 53 + github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds= 50 54 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 51 55 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 52 56 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= ··· 59 63 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 60 64 golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 61 65 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 62 - golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 63 - golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 66 + golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= 67 + golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= 68 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 + golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 70 + golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 71 + golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= 72 + golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 64 73 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 65 74 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 66 75 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+179 -16
main.go
··· 1 + // TODO: Look into storing and reusing the oauth token instead of making a whole bunch 1 2 package main 2 3 3 4 import ( 5 + "bytes" 4 6 "context" 5 7 _ "embed" 6 8 "errors" 7 9 "flag" 8 10 "fmt" 11 + "image" 12 + _ "image/jpeg" 13 + _ "image/png" 9 14 "log" 10 15 "net" 11 16 "net/http" 12 17 "net/url" 18 + "os" 13 19 20 + "github.com/bluesky-social/indigo/api/atproto" 21 + "github.com/bluesky-social/indigo/api/bsky" 22 + "github.com/bluesky-social/indigo/atproto/atclient" 14 23 "github.com/bluesky-social/indigo/atproto/auth/oauth" 24 + "github.com/bluesky-social/indigo/atproto/identity" 25 + "github.com/bluesky-social/indigo/atproto/syntax" 26 + "github.com/mattn/go-sixel" 27 + "golang.org/x/image/draw" 28 + "golang.org/x/term" 15 29 ) 16 30 17 31 //go:embed static/callback.html 18 32 var callbackHTML []byte 33 + 34 + const font_size = 16 19 35 20 36 func callbackListener(ctx context.Context, result chan url.Values) (int, error) { 21 37 listener, err := net.Listen("tcp", ":0") ··· 44 60 return listener.Addr().(*net.TCPAddr).Port, nil 45 61 } 46 62 47 - func main() { 48 - handle := flag.String("h", "", "The account handle to use") 49 - flag.Parse() 50 - 51 - if *handle == "" { 52 - log.Fatal("Missing bluesky handle") 53 - } 54 - 63 + func authenticate(ctx context.Context, handle *string) (*oauth.ClientApp, *oauth.ClientSessionData, error) { 55 64 config := oauth.ClientConfig{ 56 - ClientID: "https://bsky-sieve.yem.pink/oauth-client-metadata.json", 65 + // TODO: Want to rename this back to bsky-sieve. It's unlikely I ever make this generic enough to work on other 66 + // atproto services or even past bluesky follows and likes unless the demand is there 67 + ClientID: "https://atproto-sieve.yem.pink/oauth-client-metadata.json", 57 68 Scopes: []string{ 58 69 "atproto", 59 70 "atproto repo?collection=app.bsky.feed.like&collection=app.bsky.graph.follow&action=delete", 60 71 }, 61 - UserAgent: "bsky-sieve", 72 + UserAgent: "atproto-sieve", 62 73 } 63 74 75 + // TODO: Store the oauth stuff somewhere so we don't have to authenticate everytime 64 76 oauthClient := oauth.NewClientApp(&config, oauth.NewMemStore()) 65 - 66 - ctx := context.Background() 67 77 68 78 callbackResult := make(chan url.Values, 1) 69 79 listenPort, err := callbackListener(ctx, callbackResult) 70 80 if err != nil { 71 - log.Fatal(err) 81 + return nil, nil, err 72 82 } 73 83 74 84 config.CallbackURL = fmt.Sprintf("http://127.0.0.1:%d/callback", listenPort) 75 85 76 86 authURL, err := oauthClient.StartAuthFlow(ctx, *handle) 77 87 if err != nil { 78 - log.Fatal(err) 88 + return nil, nil, err 79 89 } 80 90 81 91 fmt.Println("Login URL:", authURL) 82 92 83 - session, err := oauthClient.ProcessCallback(ctx, <-callbackResult) 93 + sessionData, err := oauthClient.ProcessCallback(ctx, <-callbackResult) 94 + if err != nil { 95 + return nil, nil, err 96 + } 97 + 98 + return oauthClient, sessionData, nil 99 + } 100 + 101 + func main() { 102 + handle := flag.String("h", "", "Account handle to use") 103 + // collection := flag.String("c", "likes", "Collection to sift ('likes' or 'follows')") 104 + flag.Parse() 105 + 106 + if *handle == "" { 107 + log.Fatal("Missing an atproto (bluesky) handle") 108 + } 109 + 110 + ctx := context.Background() 111 + 112 + oauthClient, sessionData, err := authenticate(ctx, handle) 84 113 if err != nil { 85 114 log.Fatal(err) 86 115 } 87 116 88 - fmt.Printf("Logged in as: %s (%s)\n", *handle, session.AccountDID) 117 + fmt.Printf("Logged in as: %s (%s)\n", *handle, sessionData.AccountDID) 118 + 119 + session, err := oauthClient.ResumeSession( 120 + ctx, 121 + sessionData.AccountDID, 122 + sessionData.SessionID, 123 + ) 124 + 125 + if err != nil { 126 + log.Fatal(err) 127 + } 128 + 129 + apiClient := session.APIClient() 130 + dir := identity.DefaultDirectory() 131 + 132 + // TODO: This should be in a while loop until we get no records back 133 + records, err := atproto.RepoListRecords(ctx, apiClient, 134 + "app.bsky.graph.follow", 135 + "", 136 + 5, // TODO: When done testing, increase this number 137 + sessionData.AccountDID.String(), 138 + false, 139 + ) 140 + 141 + if err != nil { 142 + log.Fatal(err) 143 + } 144 + 145 + for _, record := range records.Records { 146 + followRecord := record.Value.Val.(*bsky.GraphFollow) 147 + profileRecord, err := atproto.RepoGetRecord(ctx, apiClient, 148 + "", 149 + "app.bsky.actor.profile", 150 + followRecord.Subject, 151 + "self", 152 + ) 153 + 154 + if err != nil { 155 + log.Fatal(err) 156 + } 157 + 158 + profile := profileRecord.Value.Val.(*bsky.ActorProfile) 159 + did, err := syntax.ParseDID(followRecord.Subject) 160 + if err != nil { 161 + log.Fatal(err) 162 + } 163 + 164 + doc, err := dir.LookupDID(ctx, did) 165 + if err != nil { 166 + log.Fatal(err) 167 + } 168 + 169 + blobClient := atclient.NewAPIClient(doc.DIDDocument().Service[0].ServiceEndpoint) 170 + avatar, err := atproto.SyncGetBlob(ctx, blobClient, 171 + profile.Avatar.Ref.String(), 172 + followRecord.Subject, 173 + ) 174 + 175 + if err != nil { 176 + log.Fatal(err) 177 + } 178 + 179 + avatarImg, _, _ := image.Decode(bytes.NewReader(avatar)) 180 + imageWidth := avatarImg.Bounds().Max.X 181 + imageHeight := avatarImg.Bounds().Max.Y 182 + imageRatio := imageWidth / imageHeight 183 + 184 + terminalWidth, terminalHeight, err := term.GetSize(0) 185 + if err != nil { 186 + log.Fatal(err) 187 + } 188 + 189 + if terminalWidth > terminalHeight { 190 + terminalWidth = terminalHeight * imageRatio 191 + } else { 192 + terminalHeight = terminalWidth / imageRatio 193 + } 194 + 195 + // TODO: Make the FONT_SIZE configureable via cli flag or figure 196 + // out a way to get the cell height from the terminal 197 + maxImageWidth := (terminalWidth / 2) * font_size 198 + maxImageHeight := (terminalHeight / 2) * font_size 199 + 200 + maxImageRatio := maxImageWidth / maxImageHeight 201 + 202 + var width, height int 203 + if maxImageRatio > imageRatio { 204 + width = maxImageWidth 205 + height = width / imageRatio 206 + } else { 207 + height = maxImageHeight 208 + width = height * imageRatio 209 + } 210 + 211 + resizedAvatar := image.NewRGBA(image.Rect(0, 0, width, height)) 212 + draw.NearestNeighbor.Scale( 213 + resizedAvatar, 214 + resizedAvatar.Rect, 215 + avatarImg, 216 + avatarImg.Bounds(), 217 + draw.Over, 218 + nil, 219 + ) 220 + 221 + if sixel.NewEncoder(os.Stdout).Encode(resizedAvatar) != nil { 222 + log.Fatal(err) 223 + } 224 + 225 + fmt.Printf("\nUser: %s (%s)\n", *profile.DisplayName, doc.Handle) 226 + fmt.Printf("DID: %s\n", followRecord.Subject) 227 + fmt.Println("Profile URL: " + "https://bsky.app/profile/" + followRecord.Subject) 228 + fmt.Printf("Description: %s\n\n", *profile.Description) 229 + fmt.Print("Keep this record ([Y]es / [n]o / [q]uit)? ") 230 + 231 + var answer string 232 + _, err = fmt.Scanln(&answer) 233 + if err != nil { 234 + log.Fatal(err) 235 + } 236 + 237 + switch answer { 238 + case "": 239 + fmt.Println("Default, Yes") 240 + case "Y", "y": 241 + fmt.Println("Yes") 242 + case "N", "n": 243 + fmt.Println("No") 244 + case "Q", "q": 245 + fmt.Println("Quit") 246 + return 247 + default: 248 + // TODO: Replace this, just ask the user again 249 + panic("Input should be either y, n, or q") 250 + } 251 + } 89 252 }
+7 -3
readme.md
··· 1 - # bsky-sieve 2 - Interactively goes through either likes and follows and allows you to decide to 3 - keep or remove the record from your account. 1 + # atproto-sieve 2 + Interactively go through a collection and decide to keep or remove records from 3 + your account. 4 + 5 + # TODO 6 + - The oauth permissions need to be changed to allow deleting from any collection 7 + if we want this to work generally for all collections
+2 -2
static/callback.html
··· 3 3 <head> 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width"> 6 - <title>Bsky Sieve</title> 6 + <title>ATproto Sieve</title> 7 7 </head> 8 8 <body> 9 - <h1>Bsky Sieve</h1> 9 + <h1>ATproto Sieve</h1> 10 10 <p><strong>Authenticated!</strong></p> 11 11 <p>You can safely close this tab.</p> 12 12 </body>
+2 -2
static/client-metadata.json
··· 1 1 { 2 - "client_id": "https://bsky-sieve.yem.pink/oauth-client-metadata.json", 2 + "client_id": "https://atproto-sieve.yem.pink/oauth-client-metadata.json", 3 3 "application_type": "native", 4 4 "grant_types": [ 5 5 "authorization_code" ··· 13 13 ], 14 14 "token_endpoint_auth_method": "none", 15 15 "dpop_bound_access_tokens": true, 16 - "client_name": "bsky-sieve" 16 + "client_name": "atproto-sieve" 17 17 }