···
import styles from "./App.module.css";
import { BskyAgent } from "@atproto/api";
15
+
type FollowRecord = {
20
+
toBeDeleted: boolean;
let [notices, setNotices] = createSignal<string[]>([], { equals: false });
let [progress, setProgress] = createSignal(0);
let [followCount, setFollowCount] = createSignal(0);
15
-
let followRecords: Record<
17
-
{ handle: string; uri: string; toBeDeleted: boolean }
26
+
let [followRecords, setFollowRecords] = createStore<FollowRecord[]>([]);
const fetchFollows = async (agent: any) => {
···
78
-
const unfollowBsky = async (form: Form, preview: boolean) => {
86
+
const fetchHiddenAccounts = async (handle: string, password: string) => {
81
-
const serviceURL = await fetchServiceEndpoint(form.handle);
89
+
const serviceURL = await fetchServiceEndpoint(handle);
const agent = new BskyAgent({
···
89
-
identifier: form.handle,
90
-
password: form.password,
updateNotices(e.message);
97
-
if (Object.keys(followRecords).length == 0 || preview) {
98
-
if (preview) followRecords = {};
106
+
if (followRecords.length == 0) {
await fetchFollows(agent).then((follows) =>
follows.forEach((record: any) => {
102
-
followRecords[record.value.subject] = {
109
+
setFollowRecords(followRecords.length, {
110
+
did: record.value.subject,
113
+
status: RepoStatus.ACTIVE,
111
-
setFollowCount(Object.keys(followRecords).length);
120
+
setFollowCount(followRecords.length);
113
-
Object.keys(followRecords).forEach(async (did) => {
122
+
followRecords.forEach(async (record, index) => {
115
-
const res = await agent.getProfile({ actor: did });
124
+
const res = await agent.getProfile({ actor: record.did });
if (res.data.viewer?.blockedBy) {
117
-
followRecords[did].handle = res.data.handle;
118
-
followRecords[did].toBeDeleted = true;
120
-
`Found account you are blocked by: ${did} (${res.data.handle})`,
126
+
setFollowRecords(index, "handle", res.data.handle);
127
+
setFollowRecords(index, "status", RepoStatus.BLOCKEDBY);
124
-
console.log(e.message);
126
-
did.startsWith("did:web")
127
-
? "https://" + did.split(":")[2] + "/.well-known/did.json"
128
-
: "https://plc.directory/" + did,
131
+
record.did.startsWith("did:web")
132
+
? "https://" + record.did.split(":")[2] + "/.well-known/did.json"
133
+
: "https://plc.directory/" + record.did,
131
-
followRecords[did].handle = await res.json().then((doc) => {
132
-
for (const alias of doc.alsoKnownAs) {
133
-
if (alias.includes("at://")) {
134
-
return alias.split("//")[1];
139
+
await res.json().then((doc) => {
140
+
for (const alias of doc.alsoKnownAs) {
141
+
if (alias.includes("at://")) {
142
+
return alias.split("//")[1];
if (e.message.includes("not found")) {
140
-
followRecords[did].toBeDeleted = true;
142
-
`Found deleted account: ${did} (${followRecords[did].handle})`,
149
+
setFollowRecords(index, "status", RepoStatus.DELETED);
} else if (e.message.includes("deactivated")) {
145
-
followRecords[did].toBeDeleted = true;
147
-
`Found deactivated account: ${did} (${followRecords[did].handle})`,
151
+
setFollowRecords(index, "status", RepoStatus.DEACTIVATED);
} else if (e.message.includes("suspended")) {
150
-
followRecords[did].toBeDeleted = true;
152
-
`Found suspended account: ${did} (${followRecords[did].handle})`,
153
+
setFollowRecords(index, "status", RepoStatus.SUSPENDED);
setProgress(progress() + 1);
163
-
const unfollowCount = Object.values(followRecords).filter(
164
-
(record) => record.toBeDeleted,
167
-
const writes = Object.values(followRecords)
168
-
.filter((record) => record.toBeDeleted)
171
-
$type: "com.atproto.repo.applyWrites#delete",
172
-
collection: "app.bsky.graph.follow",
173
-
rkey: record.uri.split("/").pop(),
177
-
const BATCHSIZE = 200;
178
-
if (agent.session) {
179
-
for (let i = 0; i < writes.length; i += BATCHSIZE) {
180
-
await agent.com.atproto.repo.applyWrites({
181
-
repo: agent.session.did,
182
-
writes: writes.slice(i, i + BATCHSIZE),
187
-
setNotices([`Unfollowed ${unfollowCount} accounts.`]);
188
-
followRecords = {};
161
+
// setFollowCount(0);
163
+
// const unfollowCount = Object.values(followRecords).filter(
164
+
// (record) => record.toBeDeleted,
167
+
// const writes = Object.values(followRecords)
168
+
// .filter((record) => record.toBeDeleted)
169
+
// .map((record) => {
171
+
// $type: "com.atproto.repo.applyWrites#delete",
172
+
// collection: "app.bsky.graph.follow",
173
+
// rkey: record.uri.split("/").pop(),
177
+
// const BATCHSIZE = 200;
178
+
// if (agent.session) {
179
+
// for (let i = 0; i < writes.length; i += BATCHSIZE) {
180
+
// await agent.com.atproto.repo.applyWrites({
181
+
// repo: agent.session.did,
182
+
// writes: writes.slice(i, i + BATCHSIZE),
187
+
// setNotices([`Unfollowed ${unfollowCount} accounts.`]);
188
+
// followRecords = {};
192
-
const UnfollowForm: Component = () => {
193
-
const [formStore, setFormStore] = createStore<Form>({
192
+
const Form: Component = () => {
193
+
const [handle, setHandle] = createSignal("");
194
+
const [password, setPassword] = createSignal("");
···
204
-
onInput={(e) => setFormStore("handle", e.currentTarget.value)}
202
+
onInput={(e) => setHandle(e.currentTarget.value)}
placeholder="App Password"
211
-
onInput={(e) => setFormStore("password", e.currentTarget.value)}
209
+
onInput={(e) => setPassword(e.currentTarget.value)}
214
-
<button type="button" onclick={() => unfollowBsky(formStore, true)}>
214
+
onclick={() => fetchHiddenAccounts(handle(), password())}
217
-
<button type="button" onclick={() => unfollowBsky(formStore, false)}>
···
<h1>cleanfollow-bsky</h1>
<div class={styles.Warning}>
229
-
<p>Unfollows all blocked by, deleted, and deactivated accounts</p>
228
+
Unfollows blocked by, deleted, suspended, and deactivated accounts
<a href="https://github.com/notjuliet/cleanfollow-bsky">Source Code</a>
<Show when={followCount()}>
Progress: {progress()}/{followCount()}