Unfollow tool for Bluesky
1import { createSignal, type Component } from "solid-js";
2
3import styles from "./App.module.css";
4import { BskyAgent } from "@atproto/api";
5
6const [unfollowNotice, setUnfollowNotice] = createSignal("");
7let unfollowURIsIndexes: number[] = [];
8let followRecords: any[];
9
10const fetchFollows = async (agent: any) => {
11 const PAGE_LIMIT = 100;
12 const fetchPage = async (cursor?: any) => {
13 return await agent.com.atproto.repo.listRecords({
14 repo: agent.session.did,
15 collection: "app.bsky.graph.follow",
16 limit: PAGE_LIMIT,
17 cursor: cursor,
18 });
19 };
20
21 let res = await fetchPage();
22 let follows = res.data.records;
23
24 while (res.data.cursor && res.data.records.length >= PAGE_LIMIT) {
25 res = await fetchPage(res.data.cursor);
26 follows = follows.concat(res.data.records);
27 }
28
29 return follows;
30};
31
32const unfollowBsky = async (
33 userHandle: any,
34 userPassword: any,
35 serviceURL: any,
36 preview: boolean,
37) => {
38 setUnfollowNotice("");
39
40 const agent = new BskyAgent({
41 service: serviceURL,
42 });
43
44 await agent.login({
45 identifier: userHandle,
46 password: userPassword,
47 });
48
49 if (unfollowURIsIndexes.length == 0 || preview) {
50 if (preview) unfollowURIsIndexes = [];
51 followRecords = await fetchFollows(agent);
52
53 let followsDID: string[] = [];
54 for (let n = 0; n < followRecords.length; n++)
55 followsDID[n] = followRecords[n].value.subject;
56
57 for (let n = 0; n < followsDID.length; n = n + 25) {
58 const res = await agent.getProfiles({
59 actors: followsDID.slice(n, n + 25),
60 });
61
62 let tmpDID: string[] = [];
63 for (let i = 0; i < res.data.profiles.length; i++) {
64 tmpDID[i] = res.data.profiles[i].did;
65 if (res.data.profiles[i].viewer?.blockedBy) {
66 unfollowURIsIndexes.push(i + n);
67 setUnfollowNotice(
68 unfollowNotice() +
69 "Found blocked account: " +
70 followRecords[i + n].value.subject +
71 " (" +
72 res.data.profiles[i].handle +
73 ")<br>",
74 );
75 }
76 }
77 for (let i = 0; i < res.data.profiles.length; i++) {
78 if (!tmpDID.includes(followsDID[i + n])) {
79 unfollowURIsIndexes.push(i + n);
80 setUnfollowNotice(
81 unfollowNotice() +
82 "Found deleted account: " +
83 followRecords[i + n].value.subject +
84 "<br>",
85 );
86 }
87 }
88 }
89 }
90
91 if (!preview) {
92 for (const i of unfollowURIsIndexes) {
93 await agent.deleteFollow(followRecords[i].uri);
94 setUnfollowNotice(
95 unfollowNotice() +
96 "Unfollowed account: " +
97 followRecords[i].value.subject +
98 "<br>",
99 );
100 }
101 unfollowURIsIndexes = [];
102 followRecords = [];
103 }
104
105 setUnfollowNotice(unfollowNotice() + "Done");
106};
107
108const UnfollowForm: Component = () => {
109 const [userHandle, setUserHandle] = createSignal();
110 const [appPassword, setAppPassword] = createSignal();
111 const [serviceURL, setserviceURL] = createSignal("https://bsky.social");
112
113 return (
114 <div>
115 <form>
116 <div>
117 <input
118 type="text"
119 placeholder="https://bsky.social (optional)"
120 onInput={(e) => setserviceURL(e.currentTarget.value)}
121 />
122 </div>
123 <div>
124 <input
125 type="text"
126 placeholder="Handle"
127 onInput={(e) => setUserHandle(e.currentTarget.value)}
128 />
129 </div>
130 <div>
131 <input
132 type="password"
133 placeholder="App Password"
134 onInput={(e) => setAppPassword(e.currentTarget.value)}
135 />
136 </div>
137 <button
138 type="button"
139 onclick={() =>
140 unfollowBsky(userHandle(), appPassword(), serviceURL(), true)
141 }
142 >
143 Preview
144 </button>
145 <button
146 type="button"
147 onclick={() =>
148 unfollowBsky(userHandle(), appPassword(), serviceURL(), false)
149 }
150 >
151 Unfollow
152 </button>
153 </form>
154 <div innerHTML={unfollowNotice()}></div>
155 </div>
156 );
157};
158
159const App: Component = () => {
160 return (
161 <div class={styles.App}>
162 <h1>cleanfollow-bsky</h1>
163 <div class={styles.Warning}>
164 <p>
165 unfollows all deleted/deactivated accounts and accounts you follow
166 that have blocked you
167 </p>
168 <p>USE AT YOUR OWN RISK</p>
169 <a href="https://github.com/notjuliet/cleanfollow-bsky">Source Code</a>
170 </div>
171 <UnfollowForm />
172 </div>
173 );
174};
175
176export default App;