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 const PROFILES_LIMIT = 25;
58
59 for (let n = 0; n < followsDID.length; n = n + PROFILES_LIMIT) {
60 const res = await agent.getProfiles({
61 actors: followsDID.slice(n, n + PROFILES_LIMIT),
62 });
63
64 let tmpDID: string[] = [];
65 for (let i = 0; i < res.data.profiles.length; i++) {
66 tmpDID[i] = res.data.profiles[i].did;
67 if (res.data.profiles[i].viewer?.blockedBy) {
68 unfollowURIsIndexes.push(i + n);
69 setUnfollowNotice(
70 unfollowNotice() +
71 "Found account you are blocked by: " +
72 followRecords[i + n].value.subject +
73 " (" +
74 res.data.profiles[i].handle +
75 ")<br>",
76 );
77 }
78 }
79 for (let i = 0; i < res.data.profiles.length; i++) {
80 if (!tmpDID.includes(followsDID[i + n])) {
81 try {
82 await agent.getProfile({ actor: followsDID[i + n] });
83 } catch (e: any) {
84 if (e.message.includes("not found")) {
85 setUnfollowNotice(unfollowNotice() + "Found deleted account: ");
86 } else if (e.message.includes(" deactivated")) {
87 setUnfollowNotice(
88 unfollowNotice() + "Found deactivated account: ",
89 );
90 }
91 }
92 setUnfollowNotice(
93 unfollowNotice() + followRecords[i + n].value.subject + "<br>",
94 );
95 unfollowURIsIndexes.push(i + n);
96 }
97 }
98 }
99 }
100
101 if (!preview) {
102 for (const i of unfollowURIsIndexes) {
103 await agent.deleteFollow(followRecords[i].uri);
104 setUnfollowNotice(
105 unfollowNotice() +
106 "Unfollowed account: " +
107 followRecords[i].value.subject +
108 "<br>",
109 );
110 }
111 unfollowURIsIndexes = [];
112 followRecords = [];
113 }
114
115 setUnfollowNotice(unfollowNotice() + "Done");
116};
117
118const UnfollowForm: Component = () => {
119 const [userHandle, setUserHandle] = createSignal();
120 const [appPassword, setAppPassword] = createSignal();
121 const [serviceURL, setserviceURL] = createSignal("https://bsky.social");
122
123 return (
124 <div>
125 <form>
126 <div>
127 <input
128 type="text"
129 placeholder="https://bsky.social (optional)"
130 onInput={(e) => setserviceURL(e.currentTarget.value)}
131 />
132 </div>
133 <div>
134 <input
135 type="text"
136 placeholder="Handle"
137 onInput={(e) => setUserHandle(e.currentTarget.value)}
138 />
139 </div>
140 <div>
141 <input
142 type="password"
143 placeholder="App Password"
144 onInput={(e) => setAppPassword(e.currentTarget.value)}
145 />
146 </div>
147 <button
148 type="button"
149 onclick={() =>
150 unfollowBsky(userHandle(), appPassword(), serviceURL(), true)
151 }
152 >
153 Preview
154 </button>
155 <button
156 type="button"
157 onclick={() =>
158 unfollowBsky(userHandle(), appPassword(), serviceURL(), false)
159 }
160 >
161 Unfollow
162 </button>
163 </form>
164 <div innerHTML={unfollowNotice()}></div>
165 </div>
166 );
167};
168
169const App: Component = () => {
170 return (
171 <div class={styles.App}>
172 <h1>cleanfollow-bsky</h1>
173 <div class={styles.Warning}>
174 <p>Unfollows all deleted, deactivated, and blocked by accounts</p>
175 <p>USE AT YOUR OWN RISK</p>
176 <a href="https://github.com/notjuliet/cleanfollow-bsky">Source Code</a>
177 </div>
178 <UnfollowForm />
179 </div>
180 );
181};
182
183export default App;