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;