Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@
"Bluesky",
"BSKY"
],
"editor.defaultFormatter": "biomejs.biome"
"editor.defaultFormatter": "biomejs.biome",
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 18 additions & 4 deletions src/background/messages/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@ import { AUTH_FACTOR_TOKEN_REQUIRED_ERROR_MESSAGE } from "~lib/constants";
import { BskyClient } from "../../lib/bskyClient";

const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
const { identifier, password, authFactorToken } = req.body;
const { domain, identifier, password, authFactorToken } = req.body;
console.log("[handler] Received domain:", domain); // DEBUG domain received

try {
const agent = await BskyClient.createAgent({
// Prepare params, only include domain if truthy
const params: {
identifier: string;
password: string;
domain?: string;
authFactorToken?: string;
} = {
identifier,
password,
...(authFactorToken && { authFactorToken: authFactorToken }),
});
};

if (domain) params.domain = domain;
if (authFactorToken) params.authFactorToken = authFactorToken;

console.log("[Caller] Using domain:", params.domain);

// Pass the prepared params object here instead of hardcoded values
const agent = await BskyClient.createAgent(params);

res.send({
session: agent.session,
Expand Down
31 changes: 30 additions & 1 deletion src/components/popup/AuthForm.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BSKY_DOMAIN } from "~lib/constants";
import { getMessageWithLink } from "~lib/utils";

interface AuthFormProps {
isLoading: boolean;
password: string;
setPassword: (value: string) => void;
domain: string;
setDomain: (value: string) => void;
identifier: string;
setIdentifier: (value: string) => void;
authFactorToken: string;
Expand All @@ -17,6 +18,8 @@ export const AuthForm = ({
isLoading,
password,
setPassword,
domain,
setDomain,
identifier,
setIdentifier,
authFactorToken,
Expand All @@ -26,6 +29,32 @@ export const AuthForm = ({
}: AuthFormProps) => {
return (
<form onSubmit={onSubmit} className="mt-5">
<label className="w-full block domain" htmlFor="domain">
<div className="text-sm flex gap-2 items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12,1A11,11,0,1,0,23,12,10.95,10.95,0,0,0,12,1ZM20.05,8H17.6a13.9,13.9,0,0,0-1.5-4A9.25,9.25,0,0,1,20.05,8ZM21,12a8.75,8.75,0,0,1-.25,2H17.9c0-.65,0-1.3,0-2s0-1.35,0-2h3.85A8.75,8.75,0,0,1,21,12ZM3,12a8.75,8.75,0,0,1,.25-2h3.85c0,.65,0,1.3,0,2s0,1.35,0,2H3.25A8.75,8.75,0,0,1,3,12Zm5,0c0-.7,0-1.35,0-2H11v4H8.1C8,13.35,8,12.7,8,12ZM13,3.35a5.85,5.85,0,0,1,1.5,1.85A10.85,10.85,0,0,1,15.55,8H13Zm-2,0V8H8.45A10.85,10.85,0,0,1,9.5,5.2,5.85,5.85,0,0,1,11,3.35ZM11,16v4.65a5.85,5.85,0,0,1-1.5-1.85A10.85,10.85,0,0,1,8.45,16Zm2,4.65V16h2.55A10.85,10.85,0,0,1,14.5,18.8,5.85,5.85,0,0,1,13,20.65ZM13,14v-4h2.9c0,.65,0,1.3,0,2s0,1.35,0,2ZM7.9,4a13.9,13.9,0,0,0-1.5,4H3.95A9.25,9.25,0,0,1,7.9,4ZM3.95,16h2.45a13.9,13.9,0,0,0,1.5,4A9.25,9.25,0,0,1,3.95,16Zm12.15,4a13.9,13.9,0,0,0,1.5-4h2.45A9.25,9.25,0,0,1,16.1,20Z" />
</svg>
Domain
</div>

<input
type="text"
name="domain"
placeholder="bsky.social"
value={domain}
onChange={(e) => setDomain(e.target.value)}
className="input input-bordered input-sm w-full max-w-xs join-item focus:outline-none mt-1"
/>
</label>
<label className="w-full block" htmlFor="identifier">
<div className="text-sm flex gap-2 items-center">
<svg
Expand Down
29 changes: 29 additions & 0 deletions src/components/popup/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ interface LoginFormProps {
isLoading: boolean;
password: string;
setPassword: (value: string) => void;
domain: string;
setDomain: (value: string) => void;
identifier: string;
setIdentifier: (value: string) => void;
authFactorToken: string;
Expand All @@ -26,6 +28,33 @@ export const LoginForm = ({
}: LoginFormProps) => {
return (
<form onSubmit={onSubmit} className="mt-5">
<label className="w-full block" htmlFor="domain">
<div className="text-sm flex gap-2 items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
{chrome.i18n.getMessage("domain")}
</div>
<input
type="text"
name="domain"
placeholder="bsky.social"
value={domain}
onChange={(e) => setDomain(e.target.value)}
className="input input-bordered input-sm w-full max-w-xs join-item focus:outline-none mt-1"
/>
</label>
<label className="w-full block" htmlFor="identifier">
<div className="text-sm flex gap-2 items-center">
<svg
Expand Down
11 changes: 9 additions & 2 deletions src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import { useErrorMessage } from "./useErrorMessage";

export const useAuth = () => {
const [isLoading, setIsLoading] = useState(false);
const [password, setPassword] = useState("");
const [domain, setDomain] = useState("");
const [identifier, setIdentifier] = useState("");
const [password, setPassword] = useState("");
const [authFactorToken, setAuthFactorToken] = useState("");
const [isShowAuthFactorTokenInput, setIsShowAuthFactorTokenInput] =
useState(false);
Expand All @@ -32,6 +33,7 @@ export const useAuth = () => {
useErrorMessage();

const saveCredentialsToStorage = async () => {
await setToChromeStorage(STORAGE_KEYS.BSKY_DOMAIN, domain);
await setToChromeStorage(STORAGE_KEYS.BSKY_USER_ID, identifier);
await setToChromeStorage(STORAGE_KEYS.BSKY_PASSWORD, password);
};
Expand All @@ -53,18 +55,20 @@ export const useAuth = () => {

const loadCredentialsFromStorage = useCallback(async () => {
const storage = await getChromeStorage<{
[STORAGE_KEYS.BSKY_DOMAIN]: string;
[STORAGE_KEYS.BSKY_USER_ID]: string;
[STORAGE_KEYS.BSKY_PASSWORD]: string;
[STORAGE_KEYS.BSKY_SHOW_AUTH_FACTOR_TOKEN_INPUT]: boolean;
[STORAGE_KEYS.BSKY_CLIENT_SESSION]: string;
}>(null);

setDomain(storage?.[STORAGE_KEYS.BSKY_DOMAIN] || "");
setIdentifier(storage?.[STORAGE_KEYS.BSKY_USER_ID] || "");
setPassword(storage?.[STORAGE_KEYS.BSKY_PASSWORD] || "");
setIsShowAuthFactorTokenInput(
storage?.[STORAGE_KEYS.BSKY_SHOW_AUTH_FACTOR_TOKEN_INPUT] || false,
);
return {
domain: storage?.[STORAGE_KEYS.BSKY_DOMAIN],
identifier: storage?.[STORAGE_KEYS.BSKY_USER_ID],
password: storage?.[STORAGE_KEYS.BSKY_PASSWORD],
session: storage?.[STORAGE_KEYS.BSKY_CLIENT_SESSION],
Expand Down Expand Up @@ -156,6 +160,7 @@ export const useAuth = () => {
const { session, error } = await sendToBackground({
name: "login",
body: {
domain,
identifier: formattedIdentifier,
password,
...(authFactorToken && { authFactorToken: authFactorToken }),
Expand Down Expand Up @@ -230,6 +235,8 @@ export const useAuth = () => {
isLoading,
password,
setPassword,
domain,
setDomain,
identifier,
setIdentifier,
authFactorToken,
Expand Down
56 changes: 45 additions & 11 deletions src/lib/bskyClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,106 @@ import destr from "destr";
import { BSKY_DOMAIN } from "./constants";
import { debugLog } from "./utils";

// try and cut down the amount of session resumes by caching the clients
// Extend session type to hold service URL for resume
interface CustomSessionData extends AtpSessionData {
service?: string;
}

// Cache to reuse clients by DID
const clientCache = new Map<string, BskyClient>();

export type BskyLoginParams = {
domain: string; // optional domain for custom servers
identifier: string;
password: string;
authFactorToken?: string;
};

export class BskyClient {
private service = `https://${BSKY_DOMAIN}`;
me: {
private service: string;
me!: {
did: string;
handle: string;
email: string;
};
agent: AtpAgent;
session = {};
session: Partial<CustomSessionData> = {};

private constructor() {
private constructor(service: string) {
if (!service) {
throw new Error("[BskyClient] service URL must be provided");
}
this.service = service;
this.agent = new AtpAgent({
service: this.service,
persistSession: (evt, session) => {
this.session = session;
// Save session with domain info for resuming later
this.session = {
...session,
service: this.service,
};
debugLog("[BskyClient] Persisted session with service:", this.service);
},
});
}

public static async createAgentFromSession(
session: AtpSessionData,
session: CustomSessionData,
): Promise<BskyClient> {
let client = clientCache.get(session.did);

// Use service from session or fallback to default
const service = session.service ?? `https://${BSKY_DOMAIN}`;
if (!client) {
client = new BskyClient();
client = new BskyClient(service);
await client.agent.resumeSession(destr(session));
clientCache.set(session.did, client);
debugLog(
`[BskyClient] Resumed session for DID ${session.did} using service ${service}`,
);
} else {
debugLog(`[BskyClient] Using cached client for DID ${session.did}`);
}

client.me = {
did: session.did,
handle: session.handle,
email: session.email,
};

return client;
}

public static async createAgent({
domain,
identifier,
password,
authFactorToken,
}: BskyLoginParams): Promise<BskyClient> {
const client = new BskyClient();
const selectedDomain = domain ?? BSKY_DOMAIN;
if (!selectedDomain)
throw new Error(
"[BskyClient] No domain specified and BSKY_DOMAIN is undefined",
);

const service = `https://${selectedDomain}`;
debugLog("[BskyClient] Creating new agent with service:", service);

const client = new BskyClient(service);

const { data } = await client.agent.login({
identifier,
password,
...(authFactorToken && { authFactorToken }),
});

client.me = {
did: data.did,
handle: data.handle,
email: data.email,
};

clientCache.set(data.did, client);

return client;
}

Expand Down Expand Up @@ -107,7 +142,6 @@ export class BskyClient {
};

public unblock = async (blockUri: string) => {
// TODO: unblock is not working. Need to fix it.
const { rkey } = new AtUri(blockUri);
return await this.agent.app.bsky.graph.block.delete({
repo: this.me.did,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/bskyServiceWorkerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { setToChromeStorage } from "~lib/chromeHelper";
import { STORAGE_KEYS } from "./constants";

export type BskyLoginParams = {
domain: string;
identifier: string;
password: string;
authFactorToken?: string;
Expand All @@ -18,6 +19,7 @@ export class BskyServiceWorkerClient {
}

public static async createAgentFromLoginParams({
domain,
identifier,
password,
authFactorToken,
Expand Down
5 changes: 3 additions & 2 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ export const MESSAGE_NAME_TO_ACTION_MODE_MAP = {

const STORAGE_PREFIX = "sky_follower_bridge_storage";
export const STORAGE_KEYS = {
BSKY_USER_ID: `${STORAGE_PREFIX}_bsky_password`,
BSKY_PASSWORD: `${STORAGE_PREFIX}_bsky_user`,
BSKY_DOMAIN: `${STORAGE_PREFIX}_bsky_domain`,
BSKY_USER_ID: `${STORAGE_PREFIX}_bsky_user`,
BSKY_PASSWORD: `${STORAGE_PREFIX}_bsky_password`,
BSKY_SHOW_AUTH_FACTOR_TOKEN_INPUT: `${STORAGE_PREFIX}_bsky_show_auth_factor_token_input`,
BSKY_CLIENT_SESSION: `${STORAGE_PREFIX}_bsky_client_session`,
BSKY_MESSAGE_NAME: `${STORAGE_PREFIX}_bsky_message_name`,
Expand Down
4 changes: 4 additions & 0 deletions src/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ function IndexPopup() {
password,
setPassword,
identifier,
domain,
setDomain,
setIdentifier,
authFactorToken,
setAuthFactorToken,
Expand Down Expand Up @@ -51,6 +53,8 @@ function IndexPopup() {
isLoading={isAuthLoading}
password={password}
setPassword={setPassword}
domain={domain}
setDomain={setDomain}
identifier={identifier}
setIdentifier={setIdentifier}
authFactorToken={authFactorToken}
Expand Down
3 changes: 3 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@
background-position: 0 0;
}
}
.w-full.block.domain {
padding-bottom: 18px;
}