mirror of
https://github.com/snachodog/tok-to-insta-follower-bridge.git
synced 2025-04-23 20:12:22 -06:00
refacotr: create service
This commit is contained in:
parent
e07fbf91c3
commit
fb0b439137
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { BSKY_USER_MATCH_TYPE, MATCH_TYPE_LABEL_AND_COLOR } from "../constants";
|
|
||||||
import type { MatchType } from "../../types";
|
import type { MatchType } from "../../types";
|
||||||
|
import { BSKY_USER_MATCH_TYPE, MATCH_TYPE_LABEL_AND_COLOR } from "../constants";
|
||||||
|
|
||||||
export type MatchTypeFilterValue = {
|
export type MatchTypeFilterValue = {
|
||||||
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: boolean;
|
[BSKY_USER_MATCH_TYPE.DESCRIPTION]: boolean;
|
||||||
|
@ -3,14 +3,9 @@ import { Storage } from "@plasmohq/storage";
|
|||||||
import { useStorage } from "@plasmohq/storage/hook";
|
import { useStorage } from "@plasmohq/storage/hook";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { BskyServiceWorkerClient } from "~lib/bskyServiceWorkerClient";
|
import { BskyServiceWorkerClient } from "~lib/bskyServiceWorkerClient";
|
||||||
import {
|
import { type MESSAGE_NAMES, STORAGE_KEYS } from "~lib/constants";
|
||||||
type MESSAGE_NAMES,
|
|
||||||
MESSAGE_NAME_TO_QUERY_PARAM_MAP,
|
|
||||||
STORAGE_KEYS,
|
|
||||||
} from "~lib/constants";
|
|
||||||
import { extractUserData, getUserCells } from "~lib/domHelpers";
|
|
||||||
import { searchBskyUser } from "~lib/searchBskyUsers";
|
import { searchBskyUser } from "~lib/searchBskyUsers";
|
||||||
import { wait } from "~lib/utils";
|
import { XService } from "~lib/services/x";
|
||||||
import type { CrawledUserInfo, MatchType } from "~types";
|
import type { CrawledUserInfo, MatchType } from "~types";
|
||||||
|
|
||||||
export type BskyUser = {
|
export type BskyUser = {
|
||||||
@ -26,21 +21,8 @@ export type BskyUser = {
|
|||||||
blockingUri: string | null;
|
blockingUri: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const detectXUsers = (userCellQueryParam: string) => {
|
|
||||||
const userCells = getUserCells({
|
|
||||||
queryParam: userCellQueryParam,
|
|
||||||
filterInsertedElement: true,
|
|
||||||
});
|
|
||||||
return userCells.map((userCell) => {
|
|
||||||
return extractUserData(userCell);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRetrieveBskyUsers = () => {
|
export const useRetrieveBskyUsers = () => {
|
||||||
const bskyClient = React.useRef<BskyServiceWorkerClient | null>(null);
|
const bskyClient = React.useRef<BskyServiceWorkerClient | null>(null);
|
||||||
const [detectedXUsers, setDetectedXUsers] = React.useState<
|
|
||||||
ReturnType<typeof detectXUsers>
|
|
||||||
>([]);
|
|
||||||
const [users, setUsers] = useStorage<BskyUser[]>(
|
const [users, setUsers] = useStorage<BskyUser[]>(
|
||||||
{
|
{
|
||||||
key: STORAGE_KEYS.DETECTED_BSKY_USERS,
|
key: STORAGE_KEYS.DETECTED_BSKY_USERS,
|
||||||
@ -106,22 +88,7 @@ export const useRetrieveBskyUsers = () => {
|
|||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
const queryParam = MESSAGE_NAME_TO_QUERY_PARAM_MAP[messageName];
|
const xService = new XService(messageName);
|
||||||
|
|
||||||
let scrollElement: HTMLElement | Window;
|
|
||||||
let modalScrollInterval: number;
|
|
||||||
|
|
||||||
if (messageName === "search_bsky_user_on_list_members_page") {
|
|
||||||
// select the modal wrapper using viewport selector to avoid conflation with feed in the background
|
|
||||||
scrollElement = document.querySelector(
|
|
||||||
'div[data-viewportview="true"]',
|
|
||||||
) as HTMLElement;
|
|
||||||
// base interval off of intitial scroll height
|
|
||||||
modalScrollInterval = scrollElement.scrollHeight;
|
|
||||||
} else {
|
|
||||||
// for other cases, use the window, no need to cache a scroll interval due to different window scroll logic
|
|
||||||
scrollElement = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
// loop until we get to the bottom
|
// loop until we get to the bottom
|
||||||
while (!isBottomReached) {
|
while (!isBottomReached) {
|
||||||
@ -129,42 +96,15 @@ export const useRetrieveBskyUsers = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = detectXUsers(queryParam).filter((u) => {
|
const data = xService.getCrawledUsers();
|
||||||
return !detectedXUsers.some((t) => t.accountName === u.accountName);
|
|
||||||
});
|
|
||||||
setDetectedXUsers((prev) => [...prev, ...data]);
|
|
||||||
await retrieveBskyUsers(data);
|
await retrieveBskyUsers(data);
|
||||||
|
|
||||||
// handle scrolling pattern for both modal and window
|
const isEnd = await xService.performScrollAndCheckEnd();
|
||||||
if (scrollElement instanceof HTMLElement) {
|
|
||||||
scrollElement.scrollTop += modalScrollInterval;
|
|
||||||
} else {
|
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for fetching data by x
|
if (isEnd) {
|
||||||
await wait(3000);
|
setIsBottomReached(true);
|
||||||
|
setLoading(false);
|
||||||
// break if bottom is reached
|
break;
|
||||||
if (scrollElement instanceof HTMLElement) {
|
|
||||||
if (
|
|
||||||
scrollElement.scrollTop + scrollElement.clientHeight >=
|
|
||||||
scrollElement.scrollHeight
|
|
||||||
) {
|
|
||||||
setIsBottomReached(true);
|
|
||||||
setLoading(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const documentElement = document.documentElement;
|
|
||||||
if (
|
|
||||||
documentElement.scrollTop + documentElement.clientHeight >=
|
|
||||||
documentElement.scrollHeight
|
|
||||||
) {
|
|
||||||
setIsBottomReached(true);
|
|
||||||
setLoading(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
@ -174,13 +114,9 @@ export const useRetrieveBskyUsers = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[retrieveBskyUsers, detectedXUsers, isBottomReached],
|
[retrieveBskyUsers, isBottomReached],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
chrome.storage.local.set({ users: JSON.stringify(users) });
|
|
||||||
}, [users]);
|
|
||||||
|
|
||||||
const stopRetrieveLoop = React.useCallback(() => {
|
const stopRetrieveLoop = React.useCallback(() => {
|
||||||
if (abortControllerRef.current) {
|
if (abortControllerRef.current) {
|
||||||
abortControllerRef.current.abort();
|
abortControllerRef.current.abort();
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
import { MESSAGE_NAMES } from "~lib/constants";
|
||||||
|
import { BSKY_DOMAIN, MESSAGE_NAME_TO_QUERY_PARAM_MAP } from "~lib/constants";
|
||||||
|
import { wait } from "~lib/utils";
|
||||||
|
import type { CrawledUserInfo, MessageName } from "~types";
|
||||||
|
|
||||||
|
export class XService {
|
||||||
|
// 対象のdomを取得する処理
|
||||||
|
messageName: MessageName;
|
||||||
|
crawledUsers: Set<string>;
|
||||||
|
|
||||||
|
constructor(messageName: string) {
|
||||||
|
// TODO: add type check
|
||||||
|
this.messageName = messageName as MessageName;
|
||||||
|
this.crawledUsers = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractUserData(userCell: Element): CrawledUserInfo {
|
||||||
|
const anchors = Array.from(userCell.querySelectorAll("a"));
|
||||||
|
const [avatarEl, displayNameEl] = anchors;
|
||||||
|
const accountName = avatarEl?.getAttribute("href")?.replace("/", "");
|
||||||
|
const accountNameRemoveUnderscore = accountName.replaceAll("_", ""); // bsky does not allow underscores in handle, so remove them.
|
||||||
|
const accountNameReplaceUnderscore = accountName.replaceAll("_", "-");
|
||||||
|
const displayName = displayNameEl?.textContent;
|
||||||
|
const bskyHandle =
|
||||||
|
userCell.textContent?.match(
|
||||||
|
new RegExp(`([^/\\s]+\\.${BSKY_DOMAIN})`),
|
||||||
|
)?.[1] ??
|
||||||
|
userCell.textContent
|
||||||
|
?.match(/bsky\.app\/profile\/([^/\s]+)…?/)?.[1]
|
||||||
|
?.replace("…", "") ??
|
||||||
|
"";
|
||||||
|
|
||||||
|
return {
|
||||||
|
accountName,
|
||||||
|
displayName,
|
||||||
|
accountNameRemoveUnderscore,
|
||||||
|
accountNameReplaceUnderscore,
|
||||||
|
bskyHandle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getCrawledUsers(): CrawledUserInfo[] {
|
||||||
|
const userCells = Array.from(
|
||||||
|
document.querySelectorAll(
|
||||||
|
MESSAGE_NAME_TO_QUERY_PARAM_MAP[this.messageName],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const users = userCells
|
||||||
|
.map((userCell) => this.extractUserData(userCell))
|
||||||
|
.filter((user) => !this.crawledUsers.has(user.accountName));
|
||||||
|
|
||||||
|
this.crawledUsers = new Set([
|
||||||
|
...this.crawledUsers,
|
||||||
|
...users.map((user) => user.accountName),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
async performScrollAndCheckEnd(): Promise<boolean> {
|
||||||
|
const isListMembersPage =
|
||||||
|
this.messageName === MESSAGE_NAMES.SEARCH_BSKY_USER_ON_LIST_MEMBERS_PAGE;
|
||||||
|
|
||||||
|
const scrollTarget = isListMembersPage
|
||||||
|
? (document.querySelector('div[data-viewportview="true"]') as HTMLElement)
|
||||||
|
: document.documentElement;
|
||||||
|
|
||||||
|
const initialScrollHeight = scrollTarget.scrollHeight;
|
||||||
|
scrollTarget.scrollTop += initialScrollHeight;
|
||||||
|
|
||||||
|
await wait(3000);
|
||||||
|
|
||||||
|
const hasReachedEnd =
|
||||||
|
scrollTarget.scrollTop + scrollTarget.clientHeight >=
|
||||||
|
scrollTarget.scrollHeight;
|
||||||
|
|
||||||
|
return hasReachedEnd;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import type { BSKY_USER_MATCH_TYPE } from "~lib/constants";
|
import type { BSKY_USER_MATCH_TYPE, MESSAGE_NAMES } from "~lib/constants";
|
||||||
|
|
||||||
export type MatchType =
|
export type MatchType =
|
||||||
(typeof BSKY_USER_MATCH_TYPE)[keyof typeof BSKY_USER_MATCH_TYPE];
|
(typeof BSKY_USER_MATCH_TYPE)[keyof typeof BSKY_USER_MATCH_TYPE];
|
||||||
|
|
||||||
|
export type MESSAGE_NAME = (typeof MESSAGE_NAMES)[keyof typeof MESSAGE_NAMES];
|
||||||
|
|
||||||
export type BskyUser = {
|
export type BskyUser = {
|
||||||
did: string;
|
did: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user