diff --git a/package.json b/package.json index 3eca44c..3f76452 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ "dev": "npm run clear && cross-env NODE_ENV=development run-p dev:*", "dev:prepare": "esno scripts/prepare.ts", "dev:web": "vite", + "dev:js": "npm run build:js -- --mode development", "dev:bg": "tsup --watch ./src", - "build": "cross-env NODE_ENV=production run-s clear build:web build:prepare build:bg", + "build": "cross-env NODE_ENV=production run-s clear build:web build:prepare build:js build:bg", "build:prepare": "esno scripts/prepare.ts", "build:web": "vite build", + "build:js": "vite build --config vite.config.content.ts", "build:bg": "tsup", "pack": "cross-env NODE_ENV=production run-p pack:*", "pack:zip": "rimraf extension.zip && jszip-cli add extension/* -o ./extension.zip", diff --git a/shim.d.ts b/shim.d.ts index b49b98c..82be99e 100644 --- a/shim.d.ts +++ b/shim.d.ts @@ -1,4 +1,5 @@ import type { ProtocolWithReturn } from 'webext-bridge' +import type { ICaptchaResponse } from '~/types' declare module 'webext-bridge' { export interface ProtocolMap { @@ -11,5 +12,9 @@ declare module 'webext-bridge' { 'set_role_alert_status': { uid: string; status: boolean } 'delete_role_request': ProtocolWithReturn<{ uid: string }, boolean> 'request_cookie_read': ProtocolWithReturn<{ oversea: boolean }, number> + 'create_verification': ProtocolWithReturn<{ uid: string }, ICaptchaResponse | false> + 'get_selected_role': ProtocolWithReturn<{}, string> + 'finish_captcha': ProtocolWithReturn<{ geetest: ICaptchaRequest; uid: string; tabId: number }, boolean> + 'request_captcha': { verification: ICaptchaResponse; uid: string; tabId: number } } } diff --git a/src/background/index.ts b/src/background/index.ts index 872efdf..6bec875 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,11 +1,11 @@ -import { onMessage } from 'webext-bridge' -import { action, alarms, cookies, i18n, notifications, runtime, storage } from 'webextension-polyfill' +import { onMessage, sendMessage } from 'webext-bridge' +import { action, alarms, cookies, i18n, notifications, runtime, storage, tabs } from 'webextension-polyfill' import type { Cookies, Notifications } from 'webextension-polyfill' import type { IAlertSetting, IAlertStatus, IRoleDataItem, IUserData, IUserDataItem } from '~/types' -import { getRoleDataByCookie, getRoleInfoByCookie } from '~/utils' +import { createVerification, getRoleDataByCookie, getRoleInfoByCookie, verifyVerification } from '~/utils' // 一分钟 -const INTERVAL_TIME = 1 +const INTERVAL_TIME = 3 // 角色的默认提醒设定 const defaultAlertSetting: IAlertSetting = { @@ -171,6 +171,10 @@ const targetPages = [ 'https://api-takumi.mihoyo.com/binding/api/getUserGameRolesByCookie?game_biz=hk4e_cn&asource=paimon', 'https://bbs-api-os.hoyolab.com/game_record/app/genshin/api/dailyNote?asource=paimon*', 'https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/dailyNote?asource=paimon*', + 'https://api-takumi-record.mihoyo.com/game_record/app/card/wapi/createVerification?asource=paimon*', + 'https://api-takumi-record.mihoyo.com/game_record/app/card/wapi/verifyVerification?asource=paimon*', + 'https://bbs-api-os.hoyolab.com/game_record/app/card/wapi/createVerification?asource=paimon*', + 'https://bbs-api-os.hoyolab.com/game_record/app/card/wapi/verifyVerification?asource=paimon*', ] let currentCookie = '' @@ -408,7 +412,19 @@ const refreshData = async function (uiOnly = false) { for (const role of enabledRoleList) { const data = uiOnly ? role.data : await getRoleDataByCookie(role.serverType === 'os', role.cookie, role.uid, role.serverRegion, setCookie) - if (data) { + if (Number.isInteger(data)) { + // error code + switch (data) { + case 1034: + // risk control + // 获取失败,写入错误信息 + role.isError = true + role.errorMessage = '触发风控' + role.updateTimestamp = Date.now() + break + } + } + else if (data && typeof data === 'object') { // 更新 roleList const roleIndex = originRoleList.findIndex((item) => { return item.uid === role.uid @@ -581,3 +597,82 @@ onMessage<{ oversea: boolean }, 'request_cookie_read'>('request_cookie_read', as return -1 } }) + +onMessage<{ uid: string }, 'create_verification'>('create_verification', async ({ data: { uid } }) => { + const originRoleList = await readDataFromStorage('roleList', []) + const index = originRoleList.findIndex((item) => { + return item.uid === uid + }) + const cookie = originRoleList[index].cookie + const oversea = originRoleList[index].serverType === 'os' + + const setCookie = async (cookie: string) => { + currentCookie = cookie + await updateRules() + } + + return await createVerification(oversea, cookie, setCookie) +}) + +onMessage<{ uid: string }, 'request_captcha_bg'>('request_captcha_bg', async ({ data: { uid } }) => { + // open captcha tab + const curtab = await tabs.create({ + url: 'https://paimon-webext.daidr.me/captcha.html', + }) + + // wait for curtab loaded + await new Promise((resolve, reject) => { + const check = async () => { + if (curtab.id === undefined) { + reject(new Error('tab id is undefined')) + return + } + + const tab = await tabs.get(curtab.id) + if (tab.status === 'complete') + resolve(true) + else + setTimeout(check, 100) + } + check() + }) + + console.log('tab loaded') + + // send message to captcha tab + const originRoleList = await readDataFromStorage('roleList', []) + const index = originRoleList.findIndex((item) => { + return item.uid === uid + }) + const cookie = originRoleList[index].cookie + const oversea = originRoleList[index].serverType === 'os' + + const setCookie = async (cookie: string) => { + currentCookie = cookie + await updateRules() + } + + const verification = await createVerification(oversea, cookie, setCookie) + if (verification && curtab.id) + await sendMessage('request_captcha', { verification, uid, tabId: curtab.id }, { tabId: curtab.id, context: 'content-script' }) +}) + +onMessage('finish_captcha', async ({ data: { tabId, uid, geetest } }) => { + const originRoleList = await readDataFromStorage('roleList', []) + const index = originRoleList.findIndex((item) => { + return item.uid === uid + }) + + const cookie = originRoleList[index].cookie + const oversea = originRoleList[index].serverType === 'os' + const setCookie = async (cookie: string) => { + currentCookie = cookie + await updateRules() + } + const result = await verifyVerification(oversea, cookie, geetest, setCookie) + + tabs.remove(tabId) + getRoleInfoByCookie(oversea, cookie, setCookie) + // refreshData() + return result +}) diff --git a/src/contentScripts/index.ts b/src/contentScripts/index.ts new file mode 100644 index 0000000..c5a37b5 --- /dev/null +++ b/src/contentScripts/index.ts @@ -0,0 +1,50 @@ +/* eslint-disable */ +import { onMessage, sendMessage } from 'webext-bridge' +import { ICaptchaRequest } from '~/types' + +(() => { + const Config = { + uid: "", + tabId: -1, + } + + console.info('[paimon-webext] init') + + // eslint-disable-next-line + window.postMessage({ + direction: 'from-content-script', + type: 'init', + }, '*') + + onMessage('request_captcha', ({ data }) => { + console.info('[paimon-webext] request_captcha', data) + const { uid, tabId } = data + Config.uid = uid + Config.tabId = tabId + window.postMessage({ + direction: 'from-content-script', + type: 'request_captcha', + payload: JSON.stringify(data), + }, '*') + }) + + window.addEventListener('message', (event) => { + if (event.source === window + && event.data.direction + && event.data.direction === 'from-page-script') { + console.log('Received message from page script: ', event.data) + if (event.data.type === 'finish_captcha') { + const data = JSON.parse(event.data.payload) + sendCaptchaResult(data) + } + } + }) + + function sendCaptchaResult(data: ICaptchaRequest) { + sendMessage('finish_captcha', { + geetest: data, + uid: Config.uid, + tabId: Config.tabId, + }) + } +})() diff --git a/src/manifest.ts b/src/manifest.ts index 28aee68..aef37cc 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -21,6 +21,12 @@ export async function getManifest() { background: { service_worker: './dist/background/index.mjs', }, + content_scripts: [ + { + matches: ['https://paimon-webext.daidr.me/captcha.html'], + js: ['./dist/contentScripts/index.global.js'], + }, + ], options_ui: { page: './dist/options/index.html', open_in_tab: false, @@ -38,6 +44,7 @@ export async function getManifest() { if (isDev) { // add dev-only features here + delete manifest.content_scripts manifest.permissions?.push('webNavigation') } diff --git a/src/popup/Popup.vue b/src/popup/Popup.vue index 624fbc8..68ed3bc 100644 --- a/src/popup/Popup.vue +++ b/src/popup/Popup.vue @@ -16,8 +16,10 @@ const onSelectUidChange = (e: any) => { } const userData = computed(() => { - const data = userDataList.value.find(item => item.uid === selectedUid.value) - return data || {} as IUserDataItem + const data = userDataList.value.find( + (item) => item.uid === selectedUid.value, + ) + return data || ({} as IUserDataItem) }) const updateUserInfo = async () => { @@ -26,11 +28,11 @@ const updateUserInfo = async () => { isLoaded.value = true // 筛选出启用的角色列表 - userDataList.value = userDataList.value.filter(item => item.isEnabled) + userDataList.value = userDataList.value.filter((item) => item.isEnabled) if (userDataList.value.length > 0) { // 查看选中uid是否存在 - if (!userDataList.value.find(item => item.uid === selectedUid.value)) { + if (!userDataList.value.find((item) => item.uid === selectedUid.value)) { // 不存在 selectedUid.value = userDataList.value[0].uid } @@ -41,7 +43,9 @@ onMounted(() => { updateUserInfo() }) -setInterval(() => { updateUserInfo() }, 10 * 1000) +setInterval(() => { + updateUserInfo() +}, 10 * 1000) const openOptionsPage = () => { browser.runtime.openOptionsPage() @@ -62,7 +66,9 @@ const TimeComponent = (props: { time: { hour: number; minute: number } }) => { ] } -const DayComponent = (props: { time: { day: 'today' | 'tomorrow'; hour: number; minute: number } }) => { +const DayComponent = (props: { + time: { day: 'today' | 'tomorrow'; hour: number; minute: number } +}) => { return [ h('span', { class: 'unit' }, i18n.getMessage(`popup_${props.time.day}`)), ' ', @@ -73,25 +79,40 @@ const DayComponent = (props: { time: { day: 'today' | 'tomorrow'; hour: number; ] } -const calcRecoveryTime = (time: { Day: number; Hour: number; Minute: number; Second: number }) => { +const calcRecoveryTime = (time: { + Day: number + Hour: number + Minute: number + Second: number +}) => { if (time.Day > 0) return time.Day + i18n.getMessage('popup_recovery_day') else if (time.Hour > 0) return time.Hour + i18n.getMessage('popup_recovery_hour') else if (time.Minute > 0) return time.Minute + i18n.getMessage('popup_recovery_minute') - else - return time.Second + i18n.getMessage('popup_recovery_second') + else return time.Second + i18n.getMessage('popup_recovery_second') +} + +const showCaptchaContainer = ref(false) + +const openCaptcha = async () => { + await sendMessage('request_captcha_bg', { uid: selectedUid.value }) }