From 3026c3c12dd757a85101e69d5106fa57555df305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=8F=B6?= <1936472877@qq.com> Date: Thu, 2 Jan 2025 17:34:55 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1429?= =?UTF-8?q?=E6=97=B6=E5=8F=AF=E5=88=87=E6=8D=A2=E5=85=B6=E4=BB=96api?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- apps/push.js | 8 +-- apps/setting.js | 13 ++-- components/App.js | 10 ++- components/Config.js | 1 + config/default_config/push.yaml | 22 +++++++ models/api/ISteamUserOAuth.js | 40 ++++++++++++ models/api/index.js | 1 + models/db/token.js | 8 +++ models/setting/index.js | 23 +++++++ models/task/index.js | 6 +- models/utils/steam.js | 108 ++++++++++++++++++++++++++++++-- 12 files changed, 222 insertions(+), 20 deletions(-) create mode 100644 models/api/ISteamUserOAuth.js diff --git a/README.md b/README.md index c220867..2ecca8b 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ ## **注意** -1. 一定要填**Steam Web API Key**,否则无法使用绝大部分功能,通常会返回 401 或 403 错误,请前往[Steam API](https://steamcommunity.com/dev/apikey)申请API Key +1. 一定要填**Steam Web API Key**,否则无法使用绝大部分功能,通常会返回 401 或 403 错误,请前往[Steam API](https://steamcommunity.com/dev/apikey)申请API Key, 域名随意填写 相关链接: diff --git a/apps/push.js b/apps/push.js index 9e0c64d..b23e65c 100644 --- a/apps/push.js +++ b/apps/push.js @@ -101,9 +101,6 @@ const rule = { tips: true }, fnc: async e => { - if (!e.group_id) { - return false - } const isAll = e.msg.includes('全部') let list = [] if (isAll) { @@ -112,6 +109,9 @@ const rule = { await e.reply([Config.tips.noSteamIdTips]) return true } + } else if (!e.group_id) { + await e.reply('请在群内使用') + return true } else { const memberList = await utils.bot.getGroupMemberList(e.self_id, e.group_id) list = memberList.length @@ -154,7 +154,7 @@ const rule = { name: nickname, appid: i.personaname, desc: utils.steam.getPersonaState(i.personastate), - image: await utils.bot.getUserAvatar(userInfo.botId, userInfo.userId, userInfo.groupId) || i.avatarfull, + image: await utils.bot.getUserAvatar(userInfo.botId, userInfo.userId, userInfo.groupId) || (Config.other.steamAvatar ? i.avatarfull : `https://q.qlogo.cn/g?b=qq&s=100&nk=${userInfo.userId}`), isAvatar: true, descBgColor: getColor(i.personastate) }) diff --git a/apps/setting.js b/apps/setting.js index 9aee7dc..68f20ba 100644 --- a/apps/setting.js +++ b/apps/setting.js @@ -35,12 +35,17 @@ const rule = { return false } + const toggleKeys = [ + '随机Bot', + '推送api' + ] const key = (() => { - if (/随机Bot/i.test(regRet[1])) { - return '随机Bot' - } else { - return regRet[1] + for (const i of toggleKeys) { + if (new RegExp(i, 'i').test(regRet[1])) { + return i + } } + return regRet[1] })() if (key == '全部') { diff --git a/components/App.js b/components/App.js index 93684cd..5423032 100644 --- a/components/App.js +++ b/components/App.js @@ -34,9 +34,9 @@ export default class App { if (options.recallMsg) { setTimeout(() => { if (e.group?.recallMsg) { - e.group.recallMsg(res.message_id).catch(() => {}) + e.group.recallMsg(res.message_id)?.catch?.(() => {}) } else if (e.friend?.recallMsg) { - e.friend.recallMsg(res.message_id).catch(() => {}) + e.friend.recallMsg(res.message_id)?.catch?.(() => {}) } }, options.recallMsg * 1000) } @@ -94,7 +94,11 @@ export default class App { App.reply(e, Config.tips.loadingTips, { recallMsg: 5, at: true }) } const res = await fnc(e).catch(error => { - logger.error(error.message) + if (error.isAxiosError) { + logger.error(error.message) + } else { + logger.error(error) + } let message = error.message const keyMap = [ { key: 'apiProxy', title: 'api反代' }, diff --git a/components/Config.js b/components/Config.js index 9a3b901..87e0fcd 100644 --- a/components/Config.js +++ b/components/Config.js @@ -97,6 +97,7 @@ class Config { * @returns {{ * enable: boolean, * stateChange: boolean, + * pushApi: number, * pushMode: number, * time: number, * defaultPush: boolean, diff --git a/config/default_config/push.yaml b/config/default_config/push.yaml index 1c6a0f7..76adf67 100644 --- a/config/default_config/push.yaml +++ b/config/default_config/push.yaml @@ -4,6 +4,28 @@ enable: true # 是否开启状态改变推送功能 比如上线 下线等 stateChange: true +# 设置每次检查时请求的api + +# 1: ISteamUserOAuth/GetUserSummaries/v2 +# 此接口需要access_token 429情况未知 +# 和2接口参数返回值一样, 但是使用access_token鉴权 +# 需要有人扫码登录获取access_token后才可以调用 + +# 2: ISteamUser/GetPlayerSummaries/v2 +# 此接口会有429限制, 经测试 40+steamid 3min 会出现 steamid越多越容易出现 +# 429 是根据apiKey进行限制, 可配置多个apiKey + +# 3: IPlayerService/GetPlayerLinkDetails/v1 +# 429情况暂时未知, 但是这个接口只会返回正在玩的appid不会返回name, 所以需要再请求一个接口获得游戏名 + +# 4: 随机 + +# tips: 依次进行获取 如果选择1出现429则尝试使用2 2出现429则尝试使用3 3出现429则停止尝试 +# 1如果没有access_token则跳过 + +# more api please wait or issue/pr... +pushApi: 2 + # 推送模式 # 1: 文字推送 一条消息就是一个群友 xxx正在玩xxx # 2: 图片推送 一张图片展示所有群友 diff --git a/models/api/ISteamUserOAuth.js b/models/api/ISteamUserOAuth.js new file mode 100644 index 0000000..e867a47 --- /dev/null +++ b/models/api/ISteamUserOAuth.js @@ -0,0 +1,40 @@ +import _ from 'lodash' +import { utils } from '#models' + +/** + * 获取用户相关信息 + * @param {string} accessToken + * @param {string|string[]} steamIds + * @returns {Promise<{ + * steamid: string, + * communityvisibilitystate: number, + * profilestate: number, + * personaname: string, + * profileurl: string, + * avatar: string, + * avatarmedium: string, + * avatarfull: string, + * lastlogoff?: number, + * personastate: number, + * timecreated: string, + * gameid?: string, + * gameextrainfo?: string, + * }[]>} + */ +export async function GetUserSummaries (accessToken, steamIds) { + !Array.isArray(steamIds) && (steamIds = [steamIds]) + const result = [] + // 一次只能获取100个用户信息 + for (const items of _.chunk(steamIds, 100)) { + const res = await utils.request.get('ISteamUserOAuth/GetUserSummaries/v2', { + params: { + access_token: accessToken, + steamIds: items.join(',') + } + }) + if (res.players?.length) { + result.push(...res.players) + } + } + return result +} diff --git a/models/api/index.js b/models/api/index.js index 82d3b9b..6612a38 100644 --- a/models/api/index.js +++ b/models/api/index.js @@ -2,6 +2,7 @@ export * as store from './store.js' export * as ISteamUser from './ISteamUser.js' export * as IStoreService from './IStoreService.js' export * as IPlayerService from './IPlayerService.js' +export * as ISteamUserOAuth from './ISteamUserOAuth.js' export * as ISteamUserStats from './ISteamUserStats.js' export * as IWishlistService from './IWishlistService.js' export * as ICheckoutService from './ICheckoutService.js' diff --git a/models/db/token.js b/models/db/token.js index 705cdf3..02b88fb 100644 --- a/models/db/token.js +++ b/models/db/token.js @@ -142,3 +142,11 @@ export async function TokenTableDeleteByUserIdAndSteamId (userId, steamId) { } }) } + +/** + * 查询所有accessToken + * @returns {Promise} + */ +export async function TokenTableGetAll () { + return await TokenTable.findAll().then(res => res.map(item => item.dataValues)) +} diff --git a/models/setting/index.js b/models/setting/index.js index bc9243f..8ff7ad3 100644 --- a/models/setting/index.js +++ b/models/setting/index.js @@ -82,6 +82,29 @@ export const cfgSchema = { def: true, desc: '是否推送游戏状态改变 比如上线 下线等' }, + pushApi: { + title: '推送请求api', + key: '推送api', + type: 'number', + def: 2, + min: 1, + max: 4, + input: (n) => { + if (n >= 1 && n <= 4) { + return n * 1 + } else { + return 2 + } + }, + component: 'RadioGroup', + options: [ + { label: '需要access_token', value: 1 }, + { label: '默认 可能会出现429', value: 2 }, + { label: '需要请求两个接口', value: 3 }, + { label: '随机', value: 4 } + ], + desc: '推送消息的api 请查看default_config/push.yaml的注释' + }, pushMode: { title: '推送模式', key: '推送模式', diff --git a/models/task/index.js b/models/task/index.js index 4a7ca91..a84a37b 100644 --- a/models/task/index.js +++ b/models/task/index.js @@ -1,6 +1,6 @@ import { Config, Version, Render } from '#components' import { Bot, logger, redis, segment } from '#lib' -import { api, db, utils } from '#models' +import { db, utils } from '#models' import _ from 'lodash' let timer = null @@ -26,7 +26,7 @@ export function startTimer () { // 所有的steamId const steamIds = _.uniq(PushData.map(i => i.steamId)) // 获取所有steamId现在的状态 - const result = await api.ISteamUser.GetPlayerSummaries(steamIds) + const result = await utils.steam.getUserSummaries(steamIds) const userList = {} for (const player of result) { // 获取上一次的状态 @@ -104,7 +104,7 @@ export function startTimer () { desc: `已${utils.steam.getPersonaState(player.personastate)}`, image: await utils.bot.getUserAvatar(i.botId, i.userId, i.groupId) || (Config.other.steamAvatar ? i.avatarfull : ''), isAvatar: true, - descBgColor: getColor(i.personastate) + descBgColor: getColor(player.personastate) }) if (player.personastate === 0) { db.StatsTableUpdate(i.userId, i.groupId, i.botId, i.steamId, player.gameid, player.gameextrainfo, 'onlineTime', time).catch(e => logger.error('更新统计数据失败', e.message)) diff --git a/models/utils/steam.js b/models/utils/steam.js index 4dc82a3..d403873 100644 --- a/models/utils/steam.js +++ b/models/utils/steam.js @@ -1,5 +1,7 @@ -import { api, db } from '#models' +import _ from 'lodash' import moment from 'moment' +import { api, db } from '#models' +import { Config } from '#components' const steamIdOffset = 76561197960265728n /** @@ -110,25 +112,121 @@ export function decodeAccessTokenJwt (jwt) { /** * 获取对应用户的access_token - * @param {*} userId - * @param {*} steamId + * @param {string} userId + * @param {string} steamId * @returns {Promise} */ export async function getAccessToken (userId, steamId) { if (!userId || !steamId) return '' const token = await db.TokenTableGetByUserIdAndSteamId(userId, steamId) + return await refreshAccessToken(token) +} + +/** + * 刷新access_token + * @param {import('models/db').TokenColumns} token + * @returns + */ +export async function refreshAccessToken (token) { if (!token) return '' - const now = moment.unix() + const now = moment().unix() // 提前30分钟刷新access_token const exp = token.accessTokenExpires - 60 * 30 if (exp > now) return token.accessToken // 判断refresh_token是否过期 const rtExp = token.refreshTokenExpires - 60 * 30 if (rtExp < now) { + await db.TokenTableDeleteByUserIdAndSteamId(token.userId, token.steamId) throw new Error('refresh_token已过期, 请重新登录') } const accessToken = (await api.IAuthenticationService.GenerateAccessTokenForApp(token.refreshToken, token.steamId)).access_token if (!accessToken) throw new Error('刷新access_token失败') - await db.TokenTableAddData(token.userId, token.accessToken) + await db.TokenTableAddData(token.userId, accessToken) return accessToken } + +/** + * 获取用户相关信息 + * @param {string|string[]} steamIds + * @returns {Promise<{ +* steamid: string, +* communityvisibilitystate: number, +* profilestate: number, +* personaname: string, +* avatar: string, +* avatarmedium: string, +* avatarfull: string, +* lastlogoff?: number, +* personastate: number, +* timecreated: string, +* gameid?: string, +* gameextrainfo?: string, +* }[]>} +*/ +export async function getUserSummaries (steamIds) { + if (_.isEmpty(steamIds)) return [] + let type = Math.floor(Number(Config.push.pushApi)) || 2 + if (type > 4 || type < 1) type = 2 + if (type === 4) { + type = _.random(1, 3) + } + if (type === 1) { + let accessToken = null + const tokenList = await db.TokenTableGetAll() + while (!accessToken) { + const token = _.sample(tokenList) + if (!token) { + break + } + _.pull(tokenList, token) + accessToken = await refreshAccessToken(token) + } + if (accessToken) { + const data = await api.ISteamUserOAuth.GetUserSummaries(accessToken, steamIds).catch(err => { + if ([429, 401, 403].includes(err.status)) { + logger.info(`请求 ISteamUserOAuth.GetUserSummaries/v2 失败: ${err.status} 尝试使用 ISteamUser.GetPlayerSummaries/v2`) + return false + } + throw err + }) + if (data !== false) { + return data + } + } + type = 2 + } + if (type === 2) { + const data = await api.ISteamUser.GetPlayerSummaries(steamIds).catch(err => { + if (err.status === 429) { + logger.info('请求 ISteamUser/GetPlayerSummaries/v2 失败: 429 尝试使用 IPlayerService.GetPlayerLinkDetails 接口不同返回的参数会有不同') + return false + } + throw err + }) + if (data !== false) { + return data + } + } + return await api.IPlayerService.GetPlayerLinkDetails(steamIds).then(async res => { + const appids = res.map(i => i.private_data.game_id).filter(Boolean) + const appInfo = await api.IStoreBrowseService.GetItems(appids) + return res.map(i => { + const avatarhash = Buffer.from(i.public_data.sha_digest_avatar, 'base64').toString('hex') + const gameid = i.private_data.game_id + return { + steamid: i.public_data.steamid, + communityvisibilitystate: i.public_data.visibility_state, + profilestate: i.public_data.profile_state, + personaname: i.public_data.persona_name, + avatar: `https://avatars.steamstatic.com/${avatarhash}.jpg`, + avatarmedium: `https://avatars.steamstatic.com/${avatarhash}_medium.jpg`, + avatarfull: `https://avatars.steamstatic.com/${avatarhash}_full.jpg`, + avatarhash, + personastate: i.private_data.persona_state ?? 0, + timecreated: i.private_data.time_created, + gameid, + gameextrainfo: appInfo[gameid]?.name + } + }) + }) +}