mirror of
https://github.com/snachodog/tok-to-insta-follower-bridge.git
synced 2025-04-10 14:11:22 -06:00
refactor: import list
This commit is contained in:
parent
93b4d2f8ae
commit
d354030056
21
src/background/messages/getMyProfile.ts
Normal file
21
src/background/messages/getMyProfile.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { PlasmoMessaging } from "@plasmohq/messaging";
|
||||
import { BskyClient } from "~lib/bskyClient";
|
||||
|
||||
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
|
||||
const { session } = req.body;
|
||||
const client = await BskyClient.createAgentFromSession(session);
|
||||
|
||||
try {
|
||||
res.send({
|
||||
result: await client.getMyProfile(),
|
||||
});
|
||||
} catch (e) {
|
||||
res.send({
|
||||
error: {
|
||||
message: e.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
@ -71,12 +71,8 @@ const App = () => {
|
||||
sendToBackground({ name: "openOptionPage" });
|
||||
};
|
||||
|
||||
const stopAndShowDetectedUsers = async () => {
|
||||
const stopAndShowDetectedUsers = () => {
|
||||
stopRetrieveLoop();
|
||||
await chrome.storage.local.set({
|
||||
users: JSON.stringify(users),
|
||||
listName: listName,
|
||||
});
|
||||
openOptionPage();
|
||||
};
|
||||
|
||||
|
@ -169,4 +169,12 @@ export class BskyClient {
|
||||
await this.addUserToList({ userDid, listUri });
|
||||
}
|
||||
};
|
||||
|
||||
public getMyProfile = async () => {
|
||||
return {
|
||||
pdsUrl: this.agent.pdsUrl,
|
||||
did: this.agent.session.did,
|
||||
handle: this.agent.session.handle,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -164,5 +164,19 @@ export class BskyServiceWorkerClient {
|
||||
for (const userDid of userDids) {
|
||||
await this.addUserToList({ userDid, listUri });
|
||||
}
|
||||
const listId = listUri.split("/").pop();
|
||||
return listId;
|
||||
};
|
||||
|
||||
public getMyProfile = async () => {
|
||||
const { result, error } = await sendToBackground({
|
||||
name: "getMyProfile",
|
||||
body: {
|
||||
session: this.session,
|
||||
},
|
||||
});
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
@ -3,9 +3,10 @@ import React from "react";
|
||||
type Props = {
|
||||
onClick: () => Promise<void>;
|
||||
label: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const AsyncButton = ({ onClick, label }: Props) => {
|
||||
const AsyncButton = ({ onClick, label, className }: Props) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
@ -17,7 +18,7 @@ const AsyncButton = ({ onClick, label }: Props) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-wide btn-sm mb-2"
|
||||
className={`btn btn-primary btn-wide btn-sm mb-2 ${className}`}
|
||||
onClick={handleClick}
|
||||
disabled={loading}
|
||||
>
|
||||
|
@ -31,8 +31,14 @@ export const Default: Story = {
|
||||
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: 10,
|
||||
[BSKY_USER_MATCH_TYPE.FOLLOWING]: 10,
|
||||
},
|
||||
actionAll: async () => {
|
||||
console.log("actionAll");
|
||||
importList: async () => {
|
||||
console.log("importList");
|
||||
},
|
||||
followAll: async () => {
|
||||
console.log("followAll");
|
||||
},
|
||||
blockAll: async () => {
|
||||
console.log("blockAll");
|
||||
},
|
||||
actionMode: ACTION_MODE.FOLLOW,
|
||||
},
|
||||
@ -56,8 +62,14 @@ export const NoDetections: Story = {
|
||||
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: 0,
|
||||
[BSKY_USER_MATCH_TYPE.FOLLOWING]: 0,
|
||||
},
|
||||
actionAll: async () => {
|
||||
console.log("actionAll");
|
||||
importList: async () => {
|
||||
console.log("importList");
|
||||
},
|
||||
followAll: async () => {
|
||||
console.log("followAll");
|
||||
},
|
||||
blockAll: async () => {
|
||||
console.log("blockAll");
|
||||
},
|
||||
actionMode: ACTION_MODE.FOLLOW,
|
||||
},
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { match } from "ts-pattern";
|
||||
import type { MatchType, MatchTypeFilterValue } from "../../types";
|
||||
import {
|
||||
ACTION_MODE,
|
||||
@ -12,36 +13,30 @@ type Props = {
|
||||
detectedCount: number;
|
||||
filterValue: MatchTypeFilterValue;
|
||||
onChangeFilter: (key: MatchType) => void;
|
||||
actionAll: () => Promise<void>;
|
||||
actionMode: (typeof ACTION_MODE)[keyof typeof ACTION_MODE];
|
||||
matchTypeStats: Record<Exclude<MatchType, "none">, number>;
|
||||
importList: () => Promise<void>;
|
||||
followAll: () => Promise<void>;
|
||||
blockAll: () => Promise<void>;
|
||||
};
|
||||
|
||||
const Sidebar = ({
|
||||
detectedCount,
|
||||
filterValue,
|
||||
onChangeFilter,
|
||||
actionAll,
|
||||
actionMode,
|
||||
matchTypeStats,
|
||||
importList,
|
||||
followAll,
|
||||
blockAll,
|
||||
}: Props) => {
|
||||
const getActionLabel = () => {
|
||||
switch (actionMode) {
|
||||
case ACTION_MODE.FOLLOW:
|
||||
return "Follow All";
|
||||
case ACTION_MODE.BLOCK:
|
||||
return "Block All";
|
||||
case ACTION_MODE.IMPORT_LIST:
|
||||
return "Import List";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="bg-base-300 w-80 min-h-screen p-4 border-r border-base-300 flex flex-col">
|
||||
<div className="flex-grow">
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href="https://sky-follower-bridge.de"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -63,7 +58,7 @@ const Sidebar = ({
|
||||
</g>
|
||||
</svg>
|
||||
<span className="text-2xl font-bold">Sky Follower Bridge</span>
|
||||
</div>
|
||||
</a>
|
||||
<div className="divider" />
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<svg
|
||||
@ -157,7 +152,17 @@ const Sidebar = ({
|
||||
</svg>
|
||||
<p className="text-xl font-bold">Action</p>
|
||||
</div>
|
||||
<AsyncButton onClick={actionAll} label={getActionLabel()} />
|
||||
{match(actionMode)
|
||||
.with(ACTION_MODE.FOLLOW, () => (
|
||||
<AsyncButton onClick={followAll} label="Follow All" />
|
||||
))
|
||||
.with(ACTION_MODE.BLOCK, () => (
|
||||
<AsyncButton onClick={blockAll} label="Block All" />
|
||||
))
|
||||
.with(ACTION_MODE.IMPORT_LIST, () => (
|
||||
<AsyncButton onClick={importList} label="Import List" />
|
||||
))
|
||||
.otherwise(() => null)}
|
||||
<p className="text-xs">
|
||||
⚠️ User detection is not perfect and may include false positives.
|
||||
</p>
|
||||
|
@ -38,6 +38,7 @@ export const STORAGE_KEYS = {
|
||||
BSKY_CLIENT_SESSION: `${STORAGE_PREFIX}_bsky_client_session`,
|
||||
BSKY_MESSAGE_NAME: `${STORAGE_PREFIX}_bsky_message_name`,
|
||||
DETECTED_BSKY_USERS: `${STORAGE_PREFIX}_detected_bsky_users`,
|
||||
LIST_NAME: `${STORAGE_PREFIX}_list_name`,
|
||||
} as const;
|
||||
|
||||
export const TARGET_URLS_REGEX = {
|
||||
|
@ -125,20 +125,23 @@ export const useBskyUserManager = () => {
|
||||
});
|
||||
}, [users, matchTypeFilter, actionMode]);
|
||||
|
||||
const actionAll = React.useCallback(async () => {
|
||||
// Import list
|
||||
const importList = React.useCallback(async () => {
|
||||
if (!bskyClient.current) return;
|
||||
const listUri = await bskyClient.current.createListAndAddUsers({
|
||||
name: listName,
|
||||
description: "List imported via Sky Follower Bridge",
|
||||
userDids: filteredUsers.map((user) => user.did),
|
||||
});
|
||||
const myProfile = await bskyClient.current.getMyProfile();
|
||||
return `https://bsky.app/profile/${myProfile.handle}/lists/${listUri}`;
|
||||
}, [filteredUsers, listName]);
|
||||
|
||||
// Follow All
|
||||
const followAll = React.useCallback(async () => {
|
||||
if (!bskyClient.current) return;
|
||||
let actionCount = 0;
|
||||
|
||||
if (actionMode === ACTION_MODE.IMPORT_LIST) {
|
||||
const userDids = filteredUsers.map((user) => user.did);
|
||||
await bskyClient.current.createListAndAddUsers({
|
||||
name: listName,
|
||||
description: "List imported via Sky Follower Bridge",
|
||||
userDids,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const user of filteredUsers) {
|
||||
let resultUri: string | null = null;
|
||||
// follow
|
||||
@ -163,8 +166,17 @@ export const useBskyUserManager = () => {
|
||||
await wait(300);
|
||||
actionCount++;
|
||||
}
|
||||
}
|
||||
return actionCount;
|
||||
}, [filteredUsers, actionMode, setUsers]);
|
||||
|
||||
// block
|
||||
// Block All
|
||||
const blockAll = React.useCallback(async () => {
|
||||
if (!bskyClient.current) return;
|
||||
// block
|
||||
let actionCount = 0;
|
||||
for (const user of filteredUsers) {
|
||||
let resultUri: string | null = null;
|
||||
if (actionMode === ACTION_MODE.BLOCK) {
|
||||
if (user.isBlocking) {
|
||||
continue;
|
||||
@ -188,7 +200,7 @@ export const useBskyUserManager = () => {
|
||||
}
|
||||
}
|
||||
return actionCount;
|
||||
}, [filteredUsers, actionMode, setUsers, listName]);
|
||||
}, [filteredUsers, actionMode, setUsers]);
|
||||
|
||||
React.useEffect(() => {
|
||||
chrome.storage.local.get(
|
||||
@ -227,7 +239,9 @@ export const useBskyUserManager = () => {
|
||||
matchTypeFilter,
|
||||
changeMatchTypeFilter,
|
||||
filteredUsers,
|
||||
actionAll,
|
||||
matchTypeStats,
|
||||
importList,
|
||||
followAll,
|
||||
blockAll,
|
||||
};
|
||||
};
|
||||
|
@ -46,6 +46,15 @@ export const useRetrieveBskyUsers = () => {
|
||||
},
|
||||
(v) => (v === undefined ? [] : v),
|
||||
);
|
||||
const [listName, setListName] = useStorage<string>(
|
||||
{
|
||||
key: STORAGE_KEYS.LIST_NAME,
|
||||
instance: new Storage({
|
||||
area: "local",
|
||||
}),
|
||||
},
|
||||
(v) => (v === undefined ? "" : v),
|
||||
);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [errorMessage, setErrorMessage] = React.useState("");
|
||||
const [isBottomReached, setIsBottomReached] = React.useState(false);
|
||||
@ -54,7 +63,6 @@ export const useRetrieveBskyUsers = () => {
|
||||
session: AtpSessionData;
|
||||
messageName: (typeof MESSAGE_NAMES)[keyof typeof MESSAGE_NAMES];
|
||||
}>(null);
|
||||
const [listName, setListName] = React.useState<string>("");
|
||||
|
||||
const retrieveBskyUsers = React.useCallback(
|
||||
async (usersData: CrawledUserInfo[]) => {
|
||||
@ -161,8 +169,7 @@ export const useRetrieveBskyUsers = () => {
|
||||
|
||||
bskyClient.current = new BskyServiceWorkerClient(session);
|
||||
|
||||
const listName = scrapeListNameFromPage();
|
||||
setListName(listName);
|
||||
setListName(scrapeListNameFromPage());
|
||||
|
||||
startRetrieveLoop(messageName).catch((e) => {
|
||||
console.error(e);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { MESSAGE_NAMES } from "~lib/constants";
|
||||
import { BSKY_DOMAIN, MESSAGE_NAME_TO_QUERY_PARAM_MAP } from "~lib/constants";
|
||||
import { BSKY_DOMAIN } from "~lib/constants";
|
||||
import { wait } from "~lib/utils";
|
||||
import type { CrawledUserInfo } from "~types";
|
||||
import { AbstractService } from "./abstractService";
|
||||
|
@ -10,16 +10,20 @@ const Option = () => {
|
||||
const {
|
||||
users,
|
||||
filteredUsers,
|
||||
listName,
|
||||
matchTypeFilter,
|
||||
changeMatchTypeFilter,
|
||||
handleClickAction,
|
||||
actionMode,
|
||||
actionAll,
|
||||
matchTypeStats,
|
||||
importList,
|
||||
followAll,
|
||||
blockAll,
|
||||
} = useBskyUserManager();
|
||||
|
||||
const { confirm, ConfirmationDialog } = useConfirm({
|
||||
const {
|
||||
confirm: followAllConfirm,
|
||||
ConfirmationDialog: FollowAllConfirmationDialog,
|
||||
} = useConfirm({
|
||||
title: "Proceed with Execution?",
|
||||
message:
|
||||
"User detection is not perfect and may include false positives. Do you still want to proceed?",
|
||||
@ -27,13 +31,71 @@ const Option = () => {
|
||||
okText: "OK",
|
||||
});
|
||||
|
||||
const handleActionAll = async () => {
|
||||
if (!(await confirm())) {
|
||||
const {
|
||||
confirm: importListConfirm,
|
||||
ConfirmationDialog: ImportListConfirmationDialog,
|
||||
} = useConfirm({
|
||||
title: "Proceed with Execution?",
|
||||
message:
|
||||
"Importing a list will create a new list and add all detected users to it. This feature is experimental and may not work as expected. Do you still want to proceed?",
|
||||
cancelText: "Cancel",
|
||||
okText: "OK",
|
||||
});
|
||||
|
||||
const handleFollowAll = async () => {
|
||||
if (!(await followAllConfirm())) {
|
||||
return;
|
||||
}
|
||||
toast.promise(followAll, {
|
||||
pending: "Processing...",
|
||||
success: {
|
||||
render({ data }) {
|
||||
return <span className="font-bold">Followed {data} users🎉</span>;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const result = await actionAll();
|
||||
toast.success(`Followed ${result} users`);
|
||||
const handleBlockAll = async () => {
|
||||
if (!(await followAllConfirm())) {
|
||||
return;
|
||||
}
|
||||
toast.promise(blockAll, {
|
||||
pending: "Processing...",
|
||||
success: {
|
||||
render({ data }) {
|
||||
return <span className="font-bold">Blocked {data} users🎉</span>;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleImportList = async () => {
|
||||
if (!(await importListConfirm())) {
|
||||
return;
|
||||
}
|
||||
toast.promise(importList, {
|
||||
pending: "Processing...",
|
||||
success: {
|
||||
render({ data }) {
|
||||
return (
|
||||
<>
|
||||
<span className="font-bold">List imported successfully🎉</span>
|
||||
<br />
|
||||
<a href={data} target="_blank" rel="noreferrer" className="link">
|
||||
View Imported List
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
error: {
|
||||
render({ data }) {
|
||||
console.log(data);
|
||||
return `Failed to import list: ${data}`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -44,9 +106,11 @@ const Option = () => {
|
||||
detectedCount={users.length}
|
||||
filterValue={matchTypeFilter}
|
||||
onChangeFilter={changeMatchTypeFilter}
|
||||
actionAll={handleActionAll}
|
||||
actionMode={actionMode}
|
||||
matchTypeStats={matchTypeStats}
|
||||
importList={handleImportList}
|
||||
followAll={handleFollowAll}
|
||||
blockAll={handleBlockAll}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 ml-80 p-6 pt-0 overflow-y-auto">
|
||||
@ -72,7 +136,8 @@ const Option = () => {
|
||||
autoClose={5000}
|
||||
className="text-sm"
|
||||
/>
|
||||
<ConfirmationDialog />
|
||||
<FollowAllConfirmationDialog />
|
||||
<ImportListConfirmationDialog />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user