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;