Unfollow tool for Bluesky

add app password fallback

notjuliet db870b2b d2c44249

Changed files
+69 -25
src
+69 -25
src/App.tsx
···
} from "solid-js";
import { createStore } from "solid-js/store";
-
import { XRPC } from "@atcute/client";
import {
AppBskyGraphFollow,
At,
···
const [loginState, setLoginState] = createSignal(false);
let rpc: XRPC;
let agent: OAuthUserAgent;
const resolveDid = async (did: string) => {
const res = await fetch(
···
const Login: Component = () => {
const [loginInput, setLoginInput] = createSignal("");
const [handle, setHandle] = createSignal("");
const [notice, setNotice] = createSignal("");
···
if (session) {
agent = new OAuthUserAgent(session);
rpc = new XRPC({ handler: agent });
setLoginState(true);
setHandle(await resolveDid(agent.sub));
···
setNotice("");
});
-
const loginBsky = async (handle: string) => {
-
try {
-
setNotice(`Resolving your identity...`);
-
const resolved = await resolveFromIdentity(handle);
-
setNotice(`Contacting your data server...`);
-
const authUrl = await createAuthorizationUrl({
-
scope: import.meta.env.VITE_OAUTH_SCOPE,
-
...resolved,
});
-
setNotice(`Redirecting...`);
-
await new Promise((resolve) => setTimeout(resolve, 250));
-
location.assign(authUrl);
-
} catch {
-
setNotice("Error during OAuth login");
}
};
···
class="dark:bg-dark-100 mb-2 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300"
onInput={(e) => setLoginInput(e.currentTarget.value)}
/>
<button
onclick={() => loginBsky(loginInput())}
class="rounded bg-blue-600 py-1.5 font-bold text-slate-100 hover:bg-blue-700"
···
Login
</button>
</form>
-
<div class="mt-3">
-
<p>Remember to use your main password, not an app password.</p>
-
<p>The session is only stored on your browser.</p>
-
<p>Make sure to check the URL you will be authenticated through.</p>
-
<p>
-
(<b>bsky.social</b> unless you are on a selfhosted server)
-
</p>
-
</div>
</Show>
<Show when={loginState() && handle()}>
<div class="mb-4">
···
const fetchPage = async (cursor?: string) => {
return await rpc.get("com.atproto.repo.listRecords", {
params: {
-
repo: agent.sub,
collection: "app.bsky.graph.follow",
limit: PAGE_LIMIT,
cursor: cursor,
···
viewer.blocking || viewer.blockingByList ?
RepoStatus.BLOCKEDBY | RepoStatus.BLOCKING
: RepoStatus.BLOCKEDBY;
-
} else if (res.data.did.includes(agent.sub)) {
status = RepoStatus.YOURSELF;
} else if (viewer.blocking || viewer.blockingByList) {
status = RepoStatus.BLOCKING;
···
for (let i = 0; i < writes.length; i += BATCHSIZE) {
await rpc.call("com.atproto.repo.applyWrites", {
data: {
-
repo: agent.sub,
writes: writes.slice(i, i + BATCHSIZE),
},
});
···
} from "solid-js";
import { createStore } from "solid-js/store";
+
import { CredentialManager, XRPC } from "@atcute/client";
import {
AppBskyGraphFollow,
At,
···
const [loginState, setLoginState] = createSignal(false);
let rpc: XRPC;
let agent: OAuthUserAgent;
+
let manager: CredentialManager;
+
let agentDID: string;
const resolveDid = async (did: string) => {
const res = await fetch(
···
const Login: Component = () => {
const [loginInput, setLoginInput] = createSignal("");
+
const [password, setPassword] = createSignal("");
const [handle, setHandle] = createSignal("");
const [notice, setNotice] = createSignal("");
···
if (session) {
agent = new OAuthUserAgent(session);
rpc = new XRPC({ handler: agent });
+
agentDID = agent.sub;
setLoginState(true);
setHandle(await resolveDid(agent.sub));
···
setNotice("");
});
+
const getPDS = async (did: string) => {
+
const res = await fetch(
+
did.startsWith("did:web") ?
+
`https://${did.split(":")[2]}/.well-known/did.json`
+
: "https://plc.directory/" + did,
+
);
+
+
return res.json().then((doc: any) => {
+
for (const service of doc.service) {
+
if (service.id === "#atproto_pds") return service.serviceEndpoint;
+
}
+
});
+
};
+
+
const resolveHandle = async (handle: string) => {
+
const rpc = new XRPC({
+
handler: new CredentialManager({
+
service: "https://public.api.bsky.app",
+
}),
+
});
+
const res = await rpc.get("com.atproto.identity.resolveHandle", {
+
params: { handle: handle },
+
});
+
return res.data.did;
+
};
+
+
const loginBsky = async (login: string) => {
+
if (password()) {
+
agentDID = login.startsWith("did:") ? login : await resolveHandle(login);
+
manager = new CredentialManager({ service: await getPDS(agentDID) });
+
rpc = new XRPC({ handler: manager });
+
await manager.login({
+
identifier: agentDID,
+
password: password(),
});
+
setLoginState(true);
+
} else {
+
try {
+
setNotice(`Resolving your identity...`);
+
const resolved = await resolveFromIdentity(login);
+
+
setNotice(`Contacting your data server...`);
+
const authUrl = await createAuthorizationUrl({
+
scope: import.meta.env.VITE_OAUTH_SCOPE,
+
...resolved,
+
});
+
setNotice(`Redirecting...`);
+
await new Promise((resolve) => setTimeout(resolve, 250));
+
location.assign(authUrl);
+
} catch {
+
setNotice("Error during OAuth login");
+
}
}
};
···
class="dark:bg-dark-100 mb-2 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300"
onInput={(e) => setLoginInput(e.currentTarget.value)}
/>
+
<label for="password" class="ml-0.5">
+
App Password
+
</label>
+
<input
+
type="password"
+
id="password"
+
placeholder="leave empty for oauth"
+
class="dark:bg-dark-100 mb-2 rounded-lg border border-gray-400 px-2 py-1 focus:outline-none focus:ring-1 focus:ring-gray-300"
+
onInput={(e) => setPassword(e.currentTarget.value)}
+
/>
<button
onclick={() => loginBsky(loginInput())}
class="rounded bg-blue-600 py-1.5 font-bold text-slate-100 hover:bg-blue-700"
···
Login
</button>
</form>
</Show>
<Show when={loginState() && handle()}>
<div class="mb-4">
···
const fetchPage = async (cursor?: string) => {
return await rpc.get("com.atproto.repo.listRecords", {
params: {
+
repo: agentDID,
collection: "app.bsky.graph.follow",
limit: PAGE_LIMIT,
cursor: cursor,
···
viewer.blocking || viewer.blockingByList ?
RepoStatus.BLOCKEDBY | RepoStatus.BLOCKING
: RepoStatus.BLOCKEDBY;
+
} else if (res.data.did.includes(agentDID)) {
status = RepoStatus.YOURSELF;
} else if (viewer.blocking || viewer.blockingByList) {
status = RepoStatus.BLOCKING;
···
for (let i = 0; i < writes.length; i += BATCHSIZE) {
await rpc.call("com.atproto.repo.applyWrites", {
data: {
+
repo: agentDID,
writes: writes.slice(i, i + BATCHSIZE),
},
});