diff --git a/src/api/skland.ts b/src/api/skland.ts index a65ded2..a048aba 100644 --- a/src/api/skland.ts +++ b/src/api/skland.ts @@ -1,25 +1,35 @@ -import { command_header, generateSignature, ofetch } from '../utils' -import { BINDING_URL, CRED_CODE_URL, SKLAND_ATTENDANCE_URL, SKLAND_CHECKIN_URL } from '../constant' -import type { AttendanceResponse, BindingResponse, CredResponse, SklandBoard } from '../types' +import { createFetch } from 'ofetch' +import type { AttendanceResponse, BindingResponse, CredResponse, GetAttendanceResponse, SklandBoard } from '../types' +import { command_header, onSignatureRequest } from '../utils' + +const fetch = createFetch({ + defaults: { + baseURL: 'https://zonai.skland.com', + onRequest: onSignatureRequest, + // @ts-expect-error ignore + agent: new ProxyAgent(), + }, +}) /** * grant_code 获得森空岛用户的 token 等信息 * @param grant_code 从 OAuth 接口获取的 grant_code */ export async function signIn(grant_code: string) { - const data = await ofetch(CRED_CODE_URL, { - method: 'POST', - headers: Object.assign({ - 'Content-Type': 'application/json; charset=utf-8', - }, command_header), - body: JSON.stringify({ - code: grant_code, - kind: 1, - }) - }) - - if (data.code !== 0) - throw new Error(`登录获取 cred 错误:${data.message}`) + const data = await fetch( + '/api/v1/user/auth/generate_cred_by_code', + { + method: 'POST', + headers: command_header, + body: { + code: grant_code, + kind: 1, + }, + onRequestError(ctx) { + throw new Error(`登录获取 cred 错误:${ctx.error.message}`) + }, + }, + ) return data.data } @@ -29,12 +39,15 @@ export async function signIn(grant_code: string) { * @param token 森空岛用户的 token */ export async function getBinding(cred: string, token: string) { - const [sign, headers] = generateSignature(token, BINDING_URL) - const data = await ofetch(BINDING_URL, { - headers: Object.assign(headers, { sign, cred }), - }) - if (data.code !== 0) - throw new Error(`获取绑定角色错误:${data.message}`) + const data = await fetch( + '/api/v1/game/player/binding', + { + headers: { token, cred }, + onRequestError(ctx) { + throw new Error(`获取绑定角色错误:${ctx.error.message}`) + }, + }, + ) return data.data } @@ -45,27 +58,49 @@ export async function getBinding(cred: string, token: string) { * @param token 森空岛用户的 token */ export async function checkIn(cred: string, token: string, id: SklandBoard) { - const body = { gameId: id.toString() } - const [sign, cryptoHeaders] = generateSignature(token, SKLAND_CHECKIN_URL, body) - const headers = Object.assign(cryptoHeaders, { sign, cred, 'Content-Type': 'application/json;charset=utf-8' }, command_header) - const data = await ofetch<{ code: number, message: string, timestamp: string }>( - SKLAND_CHECKIN_URL, - { method: 'POST', headers, body: JSON.stringify(body) }, + const data = await fetch<{ code: number, message: string, timestamp: string }>( + '/api/v1/score/checkin', + { + method: 'POST', + headers: Object.assign({ token, cred }, command_header), + body: { gameId: id.toString() }, + }, ) return data } + /** * 明日方舟每日签到 * @param cred 鹰角网络通行证账号的登录凭证 * @param token 森空岛用户的 token */ export async function attendance(cred: string, token: string, body: { uid: string, gameId: string }) { - const [sign, cryptoHeaders] = generateSignature(token, SKLAND_ATTENDANCE_URL, body) - const headers = Object.assign(cryptoHeaders, { sign, cred, 'Content-Type': 'application/json;charset=utf-8' }, command_header) - const data = await ofetch( - SKLAND_ATTENDANCE_URL, - { method: 'POST', headers, body: JSON.stringify(body) }, + const record = await fetch( + '/api/v1/game/attendance', + { + headers: Object.assign({ token, cred }, command_header), + query: body + }, ) - return data + + const todayAttended = record.data.records.find((i) => { + const today = new Date().setHours(0, 0, 0, 0); + return new Date(Number(i.ts) * 1000).setHours(0, 0, 0, 0) === today; + }) + if (todayAttended) { + // 今天已经签到过了 + return false + } + else { + const data = await fetch( + '/api/v1/game/attendance', + { + method: 'POST', + headers: Object.assign({ token, cred }, command_header), + body + }, + ) + return data + } } diff --git a/src/index.ts b/src/index.ts index 9bf6634..3fe0a18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ -import { setTimeout } from 'node:timers/promises' import process from 'node:process' +import { setTimeout } from 'node:timers/promises' import { attendance, auth, checkIn, getBinding, signIn } from './api' -import { SKLAND_BOARD_IDS, SKLAND_BOARD_NAME_MAPPING } from './constant' import { bark, serverChan } from './notifications' import { getPrivacyName } from './utils' +import { SKLAND_BOARD_IDS, SKLAND_BOARD_NAME_MAPPING } from './constant' interface Options { /** server 酱推送功能的启用,false 或者 server 酱的token */ @@ -54,6 +54,35 @@ export async function doAttendanceForAccount(token: string, options: Options) { const [combineMessage, excutePushMessage, addMessage] = createCombinePushMessage() + addMessage('## 明日方舟签到') + let successAttendance = 0 + const characterList = list.map(i => i.bindingList).flat() + await Promise.all(characterList.map(async (character) => { + console.log(`将签到第${successAttendance + 1}个角色`) + const data = await attendance(cred, signToken, { + uid: character.uid, + gameId: character.channelMasterId, + }) + if (data) { + if (data.code === 0 && data.message === 'OK') { + const msg = `${(Number(character.channelMasterId) - 1) ? 'B 服' : '官服'}角色 ${getPrivacyName(character.nickName)} 签到成功${`, 获得了${data.data.awards.map(a => `「${a.resource.name}」${a.count}个`).join(',')}`}` + combineMessage(msg) + successAttendance++ + } + else { + const msg = `${(Number(character.channelMasterId) - 1) ? 'B 服' : '官服'}角色 ${getPrivacyName(character.nickName)} 签到失败${`, 错误消息: ${data.message}\n\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``}` + combineMessage(msg, true) + } + } + else { + combineMessage(`${(Number(character.channelMasterId) - 1) ? 'B 服' : '官服'}角色 ${getPrivacyName(character.nickName)} 今天已经签到过了`) + } + + // 多个角色之间的延时 + await setTimeout(3000) + })) + combineMessage(`成功签到${successAttendance}个角色`) + addMessage(`# 森空岛每日签到 \n\n> ${new Intl.DateTimeFormat('zh-CN', { dateStyle: 'full', timeStyle: 'short', timeZone: 'Asia/Shanghai' }).format(new Date())}`) addMessage('## 森空岛各版面每日检票') await Promise.all(SKLAND_BOARD_IDS.map(async (id) => { @@ -70,27 +99,5 @@ export async function doAttendanceForAccount(token: string, options: Options) { await setTimeout(3000) })) - addMessage('## 明日方舟签到') - let successAttendance = 0 - const characterList = list.map(i => i.bindingList).flat() - await Promise.all(characterList.map(async (character) => { - const data = await attendance(cred, signToken, { - uid: character.uid, - gameId: character.channelMasterId, - }) - console.log(`将签到第${successAttendance + 1}个角色`) - if (data.code === 0 && data.message === 'OK') { - const msg = `${(Number(character.channelMasterId) - 1) ? 'B 服' : '官服'}角色 ${getPrivacyName(character.nickName)} 签到成功${`, 获得了${data.data.awards.map(a => `「${a.resource.name}」${a.count}个`).join(',')}`}` - combineMessage(msg) - successAttendance++ - } - else { - const msg = `${(Number(character.channelMasterId) - 1) ? 'B 服' : '官服'}角色 ${getPrivacyName(character.nickName)} 签到失败${`, 错误消息: ${data.message}\n\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``}` - combineMessage(msg, true) - } - // 多个角色之间的延时 - await setTimeout(3000) - })) - combineMessage(`成功签到${successAttendance}个角色`) await excutePushMessage() } diff --git a/src/types.ts b/src/types.ts index 7ab9777..db19747 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,6 +30,30 @@ export type BindingResponse = SklandResponse<{ }[] }> +export type GetAttendanceResponse = SklandResponse<{ + currentTs: string + calendar: { + resourceId: string + type: string + count: number + available: boolean + done: boolean + }[] + records: { + resourceId: string + type: string + count: number + ts: string + }[] + resourceInfoMap: { + [key: string]: { + id: string + name: string + type: string + } + } +}> + export type AttendanceResponse = SklandResponse<{ ts: number awards: { diff --git a/src/utils.ts b/src/utils.ts index 0a7cfc2..3fd41a3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,12 +1,11 @@ import { createHash, createHmac } from 'node:crypto' -import { createFetch } from 'ofetch' -import { ProxyAgent } from 'proxy-agent' +import type { FetchContext } from 'ofetch' export const command_header = { 'User-Agent': 'Skland/1.21.0 (com.hypergryph.skland; build:102100065; Android 34; ) Okhttp/4.11.0', 'Accept-Encoding': 'gzip', 'Connection': 'close', - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', } export const sign_header = { @@ -18,12 +17,41 @@ export const sign_header = { const MILLISECOND_PER_SECOND = 1000 -export function generateSignature>(token: string, uri: string, data?: T) { +export function getPrivacyName(name: string) { + return name.split('') + .map((s, i) => (i > 0 && i < name.length - 1) ? '*' : s) + .join('') +} + +export function getRequestURL(request: RequestInfo, baseURL?: string) { + const url = typeof request === 'string' ? request : request.url + if (URL.canParse(url)) + return new URL(url) + return new URL(url, baseURL) +} + +const WHITE_LIST = ['/api/v1/user/auth/generate_cred_by_code'] + +export function onSignatureRequest(ctx: FetchContext) { + const { pathname } = getRequestURL(ctx.request, ctx.options.baseURL) + + if (WHITE_LIST.includes(pathname)) + return + const headers = new Headers(ctx.options.headers) + + const token = headers.get('token') + if (!token) + throw new Error('token 不存在') + + const searchParams = new URLSearchParams(ctx.options.query) const timestamp = (Date.now() - 2 * MILLISECOND_PER_SECOND).toString().slice(0, -3) - const header = { ...sign_header } - header.timestamp = timestamp - const { pathname, searchParams } = new URL(uri) - const str = `${pathname}${searchParams.toString()}${data ? JSON.stringify(data) : ''}${timestamp}${JSON.stringify(header)}` + const signatureHeaders = { + platform: '1', + timestamp, + dId: '', + vName: '1.21.0', + } + const str = `${pathname}${searchParams.toString()}${ctx.options.body ? JSON.stringify(ctx.options.body) : ''}${timestamp}${JSON.stringify(signatureHeaders)}` const hmacSha256ed = createHmac('sha256', token) .update(str, 'utf-8') @@ -33,18 +61,11 @@ export function generateSignature>(token: strin .update(hmacSha256ed) .digest('hex') - return [sign.toString(), header as typeof sign_header] as const -} + Object.entries(signatureHeaders).forEach(([key, value]) => { + headers.append(key, value) + }) + headers.append('sign', sign) + headers.delete('token') -export function getPrivacyName(name: string) { - return name.split('') - .map((s, i) => (i > 0 && i < name.length - 1) ? '*' : s) - .join('') + ctx.options.headers = headers } - -export const ofetch = createFetch({ - defaults: { - //@ts-expect-error ignore - agent: new ProxyAgent(), - } -}) diff --git a/tsconfig.json b/tsconfig.json index 2393f82..78c0aca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,13 @@ { "compilerOptions": { "target": "ESNext", - "lib": ["ESNext"], + "lib": [ + "ESNext", + "DOM" + ], "module": "ESNext", "moduleResolution": "Bundler", + "strict": true, "allowSyntheticDefaultImports": true }, "include": [