diff --git a/.gitignore b/.gitignore index bafa3b38..83f7f041 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ npm-debug.log* out/ build/ dist/ +*.local.yml # plasmo - https://www.plasmo.com .plasmo diff --git a/package.json b/package.json index 3876489f..ce7f3688 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "package": "plasmo package", "clean": "run-script-os", "clean:nix": "rm -rf build && rm -rf .plasmo", - "clean:windows": "rmdir /s /q build && rmdir /s /q .plasmo", + "clean:windows": "if exist build rmdir /s /q build && if exist .plasmo rmdir /s /q .plasmo", "test": "playwright test", "test:prepare": "run-script-os", "test:prepare:nix": "mv build/chrome-mv3-prod build/extension", @@ -66,6 +66,7 @@ "manifest": { "host_permissions": [ "*://api.vtbs.moe/*", + "*://api.bilibili.com/*", "*://api.live.bilibili.com/*", "*://live.bilibili.com/*", "*://*.bilivideo.com/*", diff --git a/src/api/bilibili.ts b/src/api/bilibili.ts index 5e0515c5..dfc5fb3c 100644 --- a/src/api/bilibili.ts +++ b/src/api/bilibili.ts @@ -111,6 +111,7 @@ export async function requestUserInfo(mid: string): Promise } }) + if (res.code === -404) return undefined if (res.code !== 0) throw new Error(`B站API请求错误: ${res.message}`) return res.data } diff --git a/src/logger.ts b/src/logger.ts index 9e75a91f..30fbb1aa 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,6 +1,16 @@ + +function ciOnly(func: any) { + return function (...data: any[]) { + if (process.env.CI || process.env.NODE_ENV === 'development') { + func(...data) + } + } +} + + console.info = console.info.bind(console, '[bilibili-vup-stream-enhancer]') console.warn = console.warn.bind(console, '[bilibili-vup-stream-enhancer]') console.error = console.error.bind(console, '[bilibili-vup-stream-enhancer]') console.log = console.log.bind(console, '[bilibili-vup-stream-enhancer]') -console.debug = process.env.NODE_ENV === 'production' ? () => {} : console.debug.bind(console, '[bilibili-vup-stream-enhancer]') -console.trace = process.env.NODE_ENV === 'production' ? () => {} : console.trace.bind(console, '[bilibili-vup-stream-enhancer]') \ No newline at end of file +console.debug = ciOnly(console.debug.bind(console, '[bilibili-vup-stream-enhancer]')) +console.trace = ciOnly(console.trace.bind(console, '[bilibili-vup-stream-enhancer]')) \ No newline at end of file diff --git a/src/settings/components/DataTable.tsx b/src/settings/components/DataTable.tsx index c40ea852..7f8eb90f 100644 --- a/src/settings/components/DataTable.tsx +++ b/src/settings/components/DataTable.tsx @@ -23,7 +23,7 @@ export type DataTableProps = { headers: TableHeader[] values: T[] actions: TableAction[] - onAdd?: (value: string) => void + onAdd?: (value: string, e: KeyboardEvent) => void headerSlot?: React.ReactNode } @@ -45,7 +45,7 @@ function DataTable(props: DataTableProps): JSX.Element { const onAdd = (e: Event | any) => { e?.preventDefault() if (!input) return - props.onAdd?.(input) + props.onAdd?.(input, e) setInput('') } diff --git a/src/settings/features/jimaku/components/ListingFragment.tsx b/src/settings/features/jimaku/components/ListingFragment.tsx index add03cd3..d1b063f8 100644 --- a/src/settings/features/jimaku/components/ListingFragment.tsx +++ b/src/settings/features/jimaku/components/ListingFragment.tsx @@ -5,6 +5,7 @@ import type { ExposeHandler, StateProxy } from "~hooks/binding" import type { TableHeader } from "~settings/components/DataTable" import DataTable from "~settings/components/DataTable" import DeleteIcon from "~settings/components/DeleteIcon" +import type { WbiAccInfoResponse } from "~types/bilibili" import type { ArrElement, PickKeys } from "~types/common" import { catcher } from "~utils/fetch" import { removeArr } from "~utils/misc" @@ -45,7 +46,7 @@ const user_headers: TableHeader[] = [ function ListingFragment({ state, useHandler }: StateProxy): JSX.Element { - const addUserRecord = >(key: K) => async (value: string) => { + const addUserRecord = >(key: K) => async (value: string, e: KeyboardEvent) => { const handler = (state as ExposeHandler) @@ -53,13 +54,40 @@ function ListingFragment({ state, useHandler }: StateProxy): JSX. toast.error(`用户 ${value} 已经在列表中`) return } - const user = await catcher(requestUserInfo(value)) - if (!user) { - toast.error(`用户 ${value} 不存在`) - return + + let user: Partial = undefined + + if (!e.shiftKey) { + try { + user = await requestUserInfo(value) + if (!user) { + toast.error(`用户 ${value} 不存在`) + return + } + } catch (err) { + console.error(err) + toast.error( +
+
+ 添加用户 {value} 失败: {err?.message ?? String(err)} +
+ (使用 Shift Enter 可略过用户验证) +
+ ) + } + } else { + const mid = parseInt(value) + if (isNaN(mid)) { + toast.error(`添加用户 ${value} 失败: 不是有效的数字`) + return + } + user = { mid, name: '用戶' + value } } + + state[key].push({ id: user.mid.toString(), name: user.name, addedDate: new Date().toLocaleDateString() }) handler.set(key, state[key] as any) + toast.success(`添加用户 ${value} 成功`) } diff --git a/tests/features/jimaku.spec.ts b/tests/features/jimaku.spec.ts index 43e9792f..99b8d6fa 100644 --- a/tests/features/jimaku.spec.ts +++ b/tests/features/jimaku.spec.ts @@ -204,6 +204,79 @@ test('測試房間名單列表(黑名單/白名單)', async ({ room, content, co }) +test('测试添加同传用户名单/黑名单', async ({ content, context, tabUrl, room }) => { + + const subtitleList = content.locator('#subtitle-list') + await expect(subtitleList).toBeVisible() + + const user1 = 1 + const user2 = 2 + + const txt = '由 playwright 工具發送' + const subtitles = content.locator('#subtitle-list > p').filter({ hasText: txt }) + + const withBracket = `【${txt}】` + + logger.info('正在測試同傳字幕正則覆蓋....') + + await room.sendDanmaku(withBracket, user1) + await room.sendDanmaku(withBracket, user1) + await room.sendDanmaku(txt, user1) // this should not be covered + + await expect(subtitles).toHaveCount(2) + + logger.info('正在測試添加同傳用戶名單...') + + const settingsPage = await context.newPage() + await settingsPage.goto(tabUrl('settings.html'), { waitUntil: 'domcontentloaded' }) + + await settingsPage.getByText('功能设定').click() + await settingsPage.getByText('同传名单设定').click() + + await settingsPage.getByTestId('tongchuan-mans-input').fill(user1.toString()) + await settingsPage.getByTestId('tongchuan-mans-input').press('Shift+Enter') + await settingsPage.getByText(`添加用户 ${user1} 成功`).waitFor({ state: 'visible' }) + + await settingsPage.getByText('保存设定').click() + + await room.page.bringToFront() + await subtitleList.waitFor({ state: 'visible' }) + await room.sendDanmaku(withBracket, user1) + await room.sendDanmaku(withBracket, user1) + await room.sendDanmaku(txt, user1) // this should be covered + + await expect(subtitles).toHaveCount(3) + + logger.info('正在測試添加同傳用戶黑名單...') + + await settingsPage.bringToFront() + await settingsPage.getByTestId('tongchuan-blacklist-input').fill(user2.toString()) + await settingsPage.getByTestId('tongchuan-blacklist-input').press('Shift+Enter') + await settingsPage.getByText(`添加用户 ${user2} 成功`).waitFor({ state: 'visible' }) + + await settingsPage.getByText('保存设定').click() + + await room.page.bringToFront() + await subtitleList.waitFor({ state: 'visible' }) + await room.sendDanmaku(withBracket, user2) // should be blacklisted + await room.sendDanmaku(withBracket, user2) // should be blacklisted + + await expect(subtitles).toHaveCount(0) + + // github host runner does not have access to bilibili user api + if (!process.env.CI) { + + logger.info('正在嘗試添加不存在用戶...') + + await settingsPage.bringToFront() + await settingsPage.getByTestId('tongchuan-mans-input').fill('1234569') + await settingsPage.getByTestId('tongchuan-mans-input').press('Enter') + + await expect(settingsPage.getByText('用户 1234569 不存在')).toBeVisible() + + } +}) + test('測試全屏時字幕區塊是否存在 + 顯示切換', async ({ content: p, room }) => { test.skip(await room.isThemePage(), '此測試不適用於大海報房間') diff --git a/tests/helpers/bilibili-page.ts b/tests/helpers/bilibili-page.ts index be5b135f..b08c0da1 100644 --- a/tests/helpers/bilibili-page.ts +++ b/tests/helpers/bilibili-page.ts @@ -90,9 +90,10 @@ export class BilibiliPage implements AsyncDisposable { /** * 模拟发送弹幕。 - * @param danmaku 弹幕内容。 + * @param danmaku 弹幕内容 + * @param uid 弹幕发送者用户ID。 */ - async sendDanmaku(danmaku: string): Promise { + async sendDanmaku(danmaku: string, uid: number = randomNumber()): Promise { const f = await this.getContentLocator() await sendFakeBLiveMessage(f, 'DANMU_MSG', { cmd: 'DANMU_MSG', @@ -118,7 +119,7 @@ export class BilibiliPage implements AsyncDisposable { ], danmaku, [ - randomNumber(), + uid, "username", undefined, undefined, @@ -154,8 +155,9 @@ export class BilibiliPage implements AsyncDisposable { * @param user 用户名。 * @param price 价格。 * @param message 消息内容。 + * @param uid 用户ID。 */ - async sendSuperChat(user: string, price: number, message: string): Promise { + async sendSuperChat(user: string, price: number, message: string, uid: number = randomNumber()): Promise { const f = await this.getContentLocator() await sendFakeBLiveMessage(f, 'SUPER_CHAT_MESSAGE', { cmd: 'SUPER_CHAT_MESSAGE', @@ -169,7 +171,7 @@ export class BilibiliPage implements AsyncDisposable { name_color: '#444444', uname: user }, - uid: randomNumber(), + uid: uid, price: price, message: message, start_time: Date.now()