Skip to content

Commit

Permalink
feat: 检查今日签到情况
Browse files Browse the repository at this point in the history
  • Loading branch information
enpitsuLin committed Aug 12, 2024
1 parent 6eba4cd commit e73bc30
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 80 deletions.
103 changes: 69 additions & 34 deletions src/api/skland.ts
Original file line number Diff line number Diff line change
@@ -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<CredResponse>(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<CredResponse>(
'/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
}
Expand All @@ -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<BindingResponse>(BINDING_URL, {
headers: Object.assign(headers, { sign, cred }),
})
if (data.code !== 0)
throw new Error(`获取绑定角色错误:${data.message}`)
const data = await fetch<BindingResponse>(
'/api/v1/game/player/binding',
{
headers: { token, cred },
onRequestError(ctx) {
throw new Error(`获取绑定角色错误:${ctx.error.message}`)
},
},
)

return data.data
}
Expand All @@ -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<AttendanceResponse>(
SKLAND_ATTENDANCE_URL,
{ method: 'POST', headers, body: JSON.stringify(body) },
const record = await fetch<GetAttendanceResponse>(
'/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<AttendanceResponse>(
'/api/v1/game/attendance',
{
method: 'POST',
headers: Object.assign({ token, cred }, command_header),
body
},
)
return data
}
}
55 changes: 31 additions & 24 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -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) => {
Expand All @@ -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()
}
24 changes: 24 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
63 changes: 42 additions & 21 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -18,12 +17,41 @@ export const sign_header = {

const MILLISECOND_PER_SECOND = 1000

export function generateSignature<T extends Record<string, string>>(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')
Expand All @@ -33,18 +61,11 @@ export function generateSignature<T extends Record<string, string>>(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(),
}
})
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext"],
"lib": [
"ESNext",
"DOM"
],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"allowSyntheticDefaultImports": true
},
"include": [
Expand Down

0 comments on commit e73bc30

Please sign in to comment.