mirror of
https://github.com/snachodog/tok-to-insta-follower-bridge.git
synced 2025-09-13 07:23:32 -06:00
🚀 add blocking feature
wip
This commit is contained in:
@@ -1,8 +1,19 @@
|
||||
import { BskyAgent } from "@atproto/api";
|
||||
import { AtUri, BskyAgent } from "@atproto/api";
|
||||
|
||||
export type BskyLoginParams = {
|
||||
identifier: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class BskyClient {
|
||||
private service = "https://bsky.social";
|
||||
me: {
|
||||
did: string;
|
||||
handle: string;
|
||||
email: string;
|
||||
};
|
||||
agent: BskyAgent;
|
||||
|
||||
private constructor() {
|
||||
this.agent = new BskyAgent({ service: this.service });
|
||||
}
|
||||
@@ -10,12 +21,14 @@ export class BskyClient {
|
||||
public static async createAgent({
|
||||
identifier,
|
||||
password,
|
||||
}: {
|
||||
identifier: string;
|
||||
password: string;
|
||||
}): Promise<BskyClient> {
|
||||
}: BskyLoginParams): Promise<BskyClient> {
|
||||
const client = new BskyClient();
|
||||
await client.agent.login({ identifier, password });
|
||||
const {data} = await client.agent.login({ identifier, password });
|
||||
client.me = {
|
||||
did: data.did,
|
||||
handle: data.handle,
|
||||
email: data.email,
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -40,4 +53,25 @@ export class BskyClient {
|
||||
public unfollow = async (followUri: string) => {
|
||||
return await this.agent.deleteFollow(followUri);
|
||||
}
|
||||
|
||||
public block = async (subjectDid: string) => {
|
||||
return await this.agent.app.bsky.graph.block.create({
|
||||
repo: this.me.did,
|
||||
collection: "app.bsky.graph.block",
|
||||
},
|
||||
{
|
||||
subject: subjectDid,
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
collection: "app.bsky.graph.block",
|
||||
rkey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,21 +1,25 @@
|
||||
export const MESSAGE_NAMES = {
|
||||
SEARCH_BSKY_USER: "search_bsky_user"
|
||||
SEARCH_BSKY_USER_ON_FOLLOW_PAGE: "search_bsky_user_on_follow_page",
|
||||
SEARCH_BSKY_USER_ON_BLOCK_PAGE: "search_bsky_user_on_block_page",
|
||||
}
|
||||
|
||||
const STORAGE_PREFIX = "sky_follower_bridge_storage"
|
||||
export const STORAGE_KEYS = {
|
||||
BSKY_USER_ID: `${STORAGE_PREFIX}_bsky_password`,
|
||||
BSKY_PASSWORD: `${STORAGE_PREFIX}_bsky_user`,
|
||||
}
|
||||
} as const
|
||||
|
||||
export const TARGET_URLS_REGEX = [
|
||||
/^https:\/\/twitter\.com\/[^/]+\/following$/,
|
||||
/^https:\/\/twitter\.com\/[^/]+\/followers$/,
|
||||
/^https:\/\/x\.com\/[^/]+\/following$/,
|
||||
/^https:\/\/x\.com\/[^/]+\/followers$/,
|
||||
]
|
||||
export const TARGET_URLS_REGEX = {
|
||||
FOLLOW: /^https:\/\/(twitter|x)\.com\/[^/]+\/follow/,
|
||||
BLOCK: /^https:\/\/(twitter|x)\.com\/settings\/blocked/,
|
||||
} as const
|
||||
|
||||
export const MESSAGE_TYPE = {
|
||||
ERROR: "error",
|
||||
SUCCESS: "success",
|
||||
} as const
|
||||
|
||||
export const VIEWER_STATE = {
|
||||
BLOCKING: "blocking",
|
||||
FOLLOWING: "following",
|
||||
} as const
|
||||
|
@@ -1,7 +1,13 @@
|
||||
import type { ProfileView } from "@atproto/api/dist/client/types/app/bsky/actor/defs"
|
||||
import type { ProfileView, ViewerState } from "@atproto/api/dist/client/types/app/bsky/actor/defs"
|
||||
|
||||
export const getUserCells = ({ filterInsertedElement }: { filterInsertedElement: boolean } = { filterInsertedElement: true }) => {
|
||||
const userCells = document.querySelectorAll('[data-testid="primaryColumn"] [data-testid="UserCell"]');
|
||||
export type UserCellBtnLabel = {
|
||||
add: string,
|
||||
remove: string,
|
||||
progressive: string,
|
||||
}
|
||||
|
||||
export const getUserCells = ({ queryParam, filterInsertedElement }: { queryParam: string, filterInsertedElement: boolean }) => {
|
||||
const userCells = document.querySelectorAll(queryParam);
|
||||
|
||||
// filter out already inserted elements
|
||||
if (filterInsertedElement) {
|
||||
@@ -46,9 +52,19 @@ export const getAccountNameAndDisplayName = (userCell: Element) => {
|
||||
const twDisplayName = displayNameEl?.textContent
|
||||
return { twAccountName, twDisplayName }
|
||||
}
|
||||
export const insertBskyProfileEl = ({ dom, profile, abortController, followAction, unfollowAction }: { dom: Element, profile: ProfileView, abortController: AbortController, followAction: () => void, unfollowAction: () => void }) => {
|
||||
|
||||
// TODO: vanjsを使ってdom操作を描き直したい
|
||||
export const insertBskyProfileEl = ({ dom, profile, statusKey, btnLabel, abortController, followAction, unfollowAction }: {
|
||||
dom: Element,
|
||||
profile: ProfileView,
|
||||
statusKey: keyof ViewerState,
|
||||
btnLabel: UserCellBtnLabel,
|
||||
abortController: AbortController,
|
||||
followAction: () => void,
|
||||
unfollowAction: () => void
|
||||
}) => {
|
||||
const avatarEl = profile.avatar ? `<img src="${profile.avatar}" width="48" />` : "<div class='no-avatar'></div>"
|
||||
const followButtonEl = profile.viewer?.following ? "<button class='follow-button follow-button__following'>Following on Bluesky</button>" : "<button class='follow-button'>Follow on Bluesky</button>"
|
||||
const actionBtnEl = profile.viewer[statusKey] ? `<button class='follow-button follow-button__following'>${btnLabel.progressive} on Bluesky</button>` : `<button class='follow-button'>${btnLabel.add} on Bluesky</button>`
|
||||
dom.insertAdjacentHTML('afterend', `
|
||||
<div class="bsky-user-content">
|
||||
<div class="icon-section">
|
||||
@@ -63,7 +79,7 @@ export const insertBskyProfileEl = ({ dom, profile, abortController, followActio
|
||||
<p class="handle">@${profile.handle}</p>
|
||||
</div>
|
||||
<div>
|
||||
${followButtonEl}
|
||||
${actionBtnEl}
|
||||
</div>
|
||||
</div>
|
||||
${profile.description ? `<p class="description">${profile.description}</p>` : ""}
|
||||
@@ -82,9 +98,10 @@ export const insertBskyProfileEl = ({ dom, profile, abortController, followActio
|
||||
target.textContent = "processing..."
|
||||
target.classList.add('follow-button__processing')
|
||||
await followAction()
|
||||
target.textContent = "Following on Bluesky"
|
||||
target.textContent = `${btnLabel.progressive} on Bluesky`
|
||||
target.classList.remove('follow-button__processing')
|
||||
target.classList.add('follow-button__following')
|
||||
target.classList.add('follow-button__just-followed')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -93,22 +110,20 @@ export const insertBskyProfileEl = ({ dom, profile, abortController, followActio
|
||||
target.textContent = "processing..."
|
||||
target.classList.add('follow-button__processing')
|
||||
await unfollowAction()
|
||||
target.textContent = "Follow on Bluesky"
|
||||
target.textContent = `${btnLabel.add} on Bluesky`
|
||||
target.classList.remove('follow-button__processing')
|
||||
target.classList.remove('follow-button__following')
|
||||
target.classList.add('follow-button__just-followed')
|
||||
return
|
||||
}
|
||||
}, {
|
||||
signal: abortController.signal
|
||||
})
|
||||
|
||||
// register a hover action
|
||||
bskyUserContentDom?.addEventListener('mouseover', async (e) => {
|
||||
const target = e.target as Element
|
||||
const classList = target.classList
|
||||
if (classList.contains('follow-button') && classList.contains('follow-button__following')) {
|
||||
target.textContent = "Unfollow on Bluesky"
|
||||
target.textContent = `${btnLabel.remove} on Bluesky`
|
||||
}
|
||||
}, {
|
||||
signal: abortController.signal
|
||||
@@ -116,11 +131,11 @@ export const insertBskyProfileEl = ({ dom, profile, abortController, followActio
|
||||
bskyUserContentDom?.addEventListener('mouseout', async (e) => {
|
||||
const target = e.target as Element
|
||||
const classList = target.classList
|
||||
if(classList.contains('follow-button__just-followed')) {
|
||||
if (classList.contains('follow-button__just-followed')) {
|
||||
target.classList.remove('follow-button__just-followed')
|
||||
}
|
||||
if (classList.contains('follow-button') && classList.contains('follow-button__following')) {
|
||||
target.textContent = "Following on Bluesky"
|
||||
target.textContent = `${btnLabel.progressive} on Bluesky`
|
||||
}
|
||||
}, {
|
||||
signal: abortController.signal
|
||||
|
129
src/lib/searchAndInsertBskyUsers.ts
Normal file
129
src/lib/searchAndInsertBskyUsers.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { UserCellBtnLabel, isOutOfTopViewport } from './domHelpers';
|
||||
import { getAccountNameAndDisplayName, getUserCells, insertBskyProfileEl, insertNotFoundEl, insertReloadEl, removeReloadElIfExists } from "~lib/domHelpers";
|
||||
import { isSimilarUser } from "~lib/bskyHelpers";
|
||||
import { debugLog } from "~lib/utils";
|
||||
import type { BskyClient } from './bskyClient';
|
||||
import type { ViewerState } from '@atproto/api/dist/client/types/app/bsky/actor/defs';
|
||||
|
||||
|
||||
let abortController = new AbortController();
|
||||
|
||||
const notFoundUserCache = new Set<string>()
|
||||
|
||||
const followerUrlMap = new Map<string, string>()
|
||||
|
||||
export const initialize = async () => {
|
||||
abortController.abort()
|
||||
abortController = new AbortController()
|
||||
}
|
||||
|
||||
export const searchBskyUsers = async (
|
||||
{
|
||||
agent,
|
||||
btnLabel,
|
||||
userCellQueryParam,
|
||||
statusKey,
|
||||
addQuery,
|
||||
removeQuery,
|
||||
}: {
|
||||
agent: BskyClient,
|
||||
userCellQueryParam: string,
|
||||
btnLabel: UserCellBtnLabel,
|
||||
statusKey: keyof ViewerState,
|
||||
addQuery: (arg: string) => Promise<any>,
|
||||
removeQuery: (arg: string) => Promise<any>,
|
||||
}) => {
|
||||
removeReloadElIfExists()
|
||||
|
||||
const userCells = getUserCells({
|
||||
queryParam: userCellQueryParam,
|
||||
filterInsertedElement: true,
|
||||
})
|
||||
debugLog(`userCells length: ${userCells.length}`)
|
||||
|
||||
let index = 0
|
||||
for (const userCell of userCells) {
|
||||
if (isOutOfTopViewport(userCell)) {
|
||||
continue
|
||||
}
|
||||
const { twAccountName, twDisplayName } = getAccountNameAndDisplayName(userCell)
|
||||
if (notFoundUserCache.has(twAccountName)) {
|
||||
insertNotFoundEl(userCell)
|
||||
continue
|
||||
}
|
||||
|
||||
const [searchResultByAccountName] = await agent.searchUser({
|
||||
term: twAccountName,
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
// TODO: Refactor, this is duplicated
|
||||
// first, search by account name
|
||||
if (isSimilarUser(twDisplayName, searchResultByAccountName) || isSimilarUser(twAccountName, searchResultByAccountName)) {
|
||||
insertBskyProfileEl({
|
||||
dom: userCell,
|
||||
profile: searchResultByAccountName,
|
||||
statusKey,
|
||||
btnLabel,
|
||||
abortController,
|
||||
followAction: async () => {
|
||||
const result = await addQuery(searchResultByAccountName.did);
|
||||
followerUrlMap.set(searchResultByAccountName.did, result.uri)
|
||||
},
|
||||
unfollowAction: async () => {
|
||||
if (searchResultByAccountName?.viewer?.following) {
|
||||
await removeQuery(searchResultByAccountName?.viewer?.following);
|
||||
} else {
|
||||
await removeQuery(followerUrlMap.get(searchResultByAccountName.did));
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// if not found, search by display name
|
||||
const [searchResultByDisplayName] = await agent.searchUser({
|
||||
term: twDisplayName,
|
||||
limit: 1,
|
||||
})
|
||||
if (isSimilarUser(twDisplayName, searchResultByDisplayName) || isSimilarUser(twAccountName, searchResultByDisplayName)) {
|
||||
insertBskyProfileEl({
|
||||
dom: userCell,
|
||||
profile: searchResultByDisplayName,
|
||||
abortController,
|
||||
statusKey,
|
||||
btnLabel,
|
||||
followAction: async () => {
|
||||
const result = await addQuery(searchResultByDisplayName.did);
|
||||
followerUrlMap.set(searchResultByDisplayName.did, result.uri)
|
||||
},
|
||||
unfollowAction: async () => {
|
||||
if (searchResultByDisplayName?.viewer?.following) {
|
||||
await removeQuery(searchResultByDisplayName?.viewer?.following);
|
||||
} else {
|
||||
await removeQuery(followerUrlMap.get(searchResultByDisplayName.did));
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
insertNotFoundEl(userCell)
|
||||
notFoundUserCache.add(twAccountName)
|
||||
}
|
||||
}
|
||||
|
||||
index++
|
||||
if (process.env.NODE_ENV === "development" && index > 5) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: if there are more users, insert reload button
|
||||
insertReloadEl(async () => {
|
||||
await searchBskyUsers({
|
||||
agent,
|
||||
btnLabel,
|
||||
userCellQueryParam,
|
||||
statusKey,
|
||||
addQuery,
|
||||
removeQuery,
|
||||
})
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user