From 6ab1edcea52587fc7361a22eb7f0fcbb879473ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=8F=B6?= <1936472877@qq.com> Date: Mon, 6 Jan 2025 15:47:21 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=8E=A8=E9=80=81=E6=A8=A1=E5=BC=8F3?= =?UTF-8?q?=20=E7=BB=86=E5=8C=96=E6=8E=A8=E9=80=81=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/index.js | 12 +- components/Config.js | 4 + components/Render.js | 312 ++------------------------------ config/default_config/push.yaml | 19 +- lib/logger.js | 4 +- models/canvas/canvas.js | 71 ++++++++ models/canvas/game.js | 79 ++++++++ models/canvas/index.js | 2 + models/canvas/inventory.js | 251 +++++++++++++++++++++++++ models/db/history.js | 105 +---------- models/index.js | 5 +- models/setting/index.js | 41 ++++- models/task/index.js | 63 ++++--- models/utils/bot.js | 8 +- models/utils/steam.js | 6 +- resources/game/friend_add.png | Bin 0 -> 5174 bytes resources/game/friend_bg.png | Bin 0 -> 7467 bytes resources/game/game.css | 66 +++++++ resources/game/game.html | 33 ++++ resources/game/game.png | Bin 0 -> 8807 bytes 20 files changed, 627 insertions(+), 454 deletions(-) create mode 100644 models/canvas/canvas.js create mode 100644 models/canvas/game.js create mode 100644 models/canvas/index.js create mode 100644 models/canvas/inventory.js create mode 100644 resources/game/friend_add.png create mode 100644 resources/game/friend_bg.png create mode 100644 resources/game/game.css create mode 100644 resources/game/game.html create mode 100644 resources/game/game.png diff --git a/apps/index.js b/apps/index.js index 8e03201..f075f51 100644 --- a/apps/index.js +++ b/apps/index.js @@ -13,19 +13,13 @@ const apps = {} for (const i of files) { if (i === 'index.js') continue try { + const startTime = Date.now() const exp = await import(`file://${join(path, i)}`) const id = i.replace('.js', '') - // const app = new App(exp.app || { - // id: i.replace('.js', ''), - // name: i.replace('.js', '') - // }) - // for (const key in exp.rule) { - // const rule = exp.rule[key] - // app.rule(key, rule.reg, rule.fnc, rule.cfg) - // } apps[id] = exp.app + logger.debug(`加载js: apps/${i}成功 耗时: ${Date.now() - startTime}ms`) } catch (error) { - logger.error('error', `[${Version.pluginName}]加载js: apps/${i}错误\n`, error) + logger.error('error', `加载js: apps/${i}错误\n`, error) } } diff --git a/components/Config.js b/components/Config.js index 19eb03f..f5f66bf 100644 --- a/components/Config.js +++ b/components/Config.js @@ -115,7 +115,11 @@ class Config { * 获取推送配置 * @returns {{ * enable: boolean, + * playStart: boolean, + * playEnd: boolean, * stateChange: boolean, + * stateOnline: boolean, + * stateOffline: boolean, * pushApi: number, * pushMode: number, * time: number, diff --git a/components/Render.js b/components/Render.js index 43f7542..2394043 100644 --- a/components/Render.js +++ b/components/Render.js @@ -1,23 +1,11 @@ import fs from 'fs' import _ from 'lodash' import { join } from 'path' +import { canvas } from '#models' import template from 'art-template' import { execSync } from 'child_process' -import { logger, puppeteer, segment } from '#lib' import { Version, Config } from '#components' -import { utils } from '#models' - -const canvasPKG = await (async () => { - try { - const pkg = await import('@napi-rs/canvas') - const { GlobalFonts } = pkg - const fontPath = join(Version.pluginPath, 'resources', 'common', 'font', 'MiSans-Normal.ttf') - GlobalFonts.registerFromPath(fontPath, 'MiSans') - return pkg - } catch (error) { - return null - } -})() +import { logger, puppeteer, segment } from '#lib' function scale (pct = 1) { const scale = Math.min(2, Math.max(0.5, Config.other.renderScale / 100)) @@ -64,10 +52,14 @@ const Render = { data.style = `` // 暂时只支持inventory/index if (Config.other.renderType == 2) { - if (!canvasPKG) { - throw new Error('请先pnpm i 安装依赖') - } - return renderCanvas(params.data, minLength) + return canvas.inventory.render(params.data, minLength) + } + } else if (path === 'game/game') { + params.data = params.data.map(i => _.sortBy(i.games, 'name')).flat() + if (Config.other.renderType == 2) { + return canvas.game.render(params.data) + } else { + return this.simpleRender(path, params) } } const img = await puppeteer.screenshot(`${Version.pluginName}/${path}`, data) @@ -85,11 +77,16 @@ const Render = { saveId: path.split('/').pop(), imgType: 'jpeg', pageGotoParams: { - waitUntil: 'networkidle0' // +0.5s + waitUntil: 'load' }, ...params } - return await puppeteer.screenshot(`${Version.pluginName}/${path}`, data) + const img = await puppeteer.screenshot(`${Version.pluginName}/${path}`, data) + if (img) { + return img + } else { + return '制作图片出错辣!再试一次吧' + } }, /** * 渲染gif 需要传入tempName参数 用于存放临时文件 建议使用唯一id 比如steamId @@ -195,279 +192,4 @@ function tplFile (path, params, tempPath) { return tplPath.replace(/\\/g, '/') } -function drawBackgroundColor (ctx, color, x, y, width, height, radius) { - ctx.beginPath() - const backgroundX = x - 5 - const backgroundY = y - 20 - const backgroundWidth = width + 2 - const backgroundHeight = height + 5 - ctx.fillStyle = color - ctx.moveTo(backgroundX + radius, backgroundY) - ctx.arcTo(backgroundX + backgroundWidth, backgroundY, backgroundX + backgroundWidth, backgroundY + backgroundHeight, radius) - ctx.arcTo(backgroundX + backgroundWidth, backgroundY + backgroundHeight, backgroundX, backgroundY + backgroundHeight, radius) - ctx.arcTo(backgroundX, backgroundY + backgroundHeight, backgroundX, backgroundY, radius) - ctx.arcTo(backgroundX, backgroundY, backgroundX + backgroundWidth, backgroundY, radius) - ctx.closePath() - ctx.fill() -} - -async function renderCanvas (data, minLength) { - const { createCanvas, loadImage } = canvasPKG - const start = Date.now() - - // 每一项的宽高间距 - const gameWidth = 468 - const gameHeight = 93 - const spacing = 10 - - // 每行显示的游戏数量 - const lineItemCount = minLength - - // 总行数 - const lineTotal = _.sum(data.map(i => Math.ceil(i.games.length / lineItemCount))) - // 额外高度 - const extraHeight = _.sumBy(data, i => i.desc.length * 30 + 50) + 30 - - // 创建画布 - // 宽度为 (每项的宽+间距)*每行个数 + 左间距 - const canvasWidth = (gameWidth + spacing) * lineItemCount + spacing - // 高度为 (每项的高+间距)*行数 + 额外高度 - const canvasHeight = (gameHeight + spacing) * lineTotal + extraHeight - - // 计算居中坐标 - const centerX = canvasWidth / 2 - - const canvas = createCanvas(canvasWidth, canvasHeight) - const ctx = canvas.getContext('2d') - - // 设置背景颜色 - ctx.fillStyle = '#ffffff' - ctx.fillRect(0, 0, canvasWidth, canvasHeight) - - // 设置字体和颜色 - ctx.font = '20px MiSans' - ctx.fillStyle = '#000000' - - // 异步加载图片 - const imgs = await Promise.all(data.map(i => i.games).flat().map(async i => { - if (i.noImg) { - return {} - } - const Image = await loadImage(i.image || utils.steam.getHeaderImgUrlByAppid(i.appid)).catch(() => null) - return { - ...i, - Image - } - })).then(imgs => imgs.reduce((acc, cur) => { - if (cur?.Image) { - acc[`${cur.name}${cur.appid}${cur.desc}`] = cur.Image - } - return acc - }, {})) - - let startX = 0 - let startY = 0 - - for (const g of data) { - startY += 40 - // title - ctx.save() - ctx.font = 'bold 24px MiSans' - ctx.textAlign = 'center' - ctx.fillText(g.title, centerX, startY) - ctx.restore() - - // desc - if (g.desc.length) { - for (const desc of g.desc) { - startY += 30 - ctx.save() - ctx.font = 'bold 20px MiSans' - ctx.textAlign = 'center' - ctx.fillText(desc, centerX, startY) - ctx.restore() - } - } - - let x = 10 - gameWidth + startX - let y = startY + 10 - let index = 1 - - for (const items of _.chunk(g.games, lineItemCount)) { - const remainingItems = items.length - - // 如果是最后一行且元素数量不足,则居中 - if (remainingItems < lineItemCount) { - const totalWidth = gameWidth * remainingItems + spacing * (remainingItems - 1) - // 计算居中偏移量 - x = (canvasWidth - totalWidth) / 2 - } else { - x = 10 - } - - // 绘制当前行的元素 - for (const i of items) { - ctx.save() - - const nameY = y + 24 - const appidY = y + 52 - const descY = y + 79 - - let currentX = x - - // 边框 - ctx.strokeStyle = '#ccc' - ctx.lineWidth = 1 - ctx.beginPath() - ctx.roundRect(currentX, y, gameWidth, gameHeight, 10) - ctx.stroke() - - // 内边距10 - currentX += 10 - - // 最大内容宽度 20内间距 - let maxContentWidth = gameWidth - 20 - - // 图片 - if (!i.noImg) { - const img = imgs[`${i.name}${i.appid}${i.desc}`] - const imgY = y + 10 - const imgWidth = i.isAvatar ? 72 : 156 - const imgHeight = 72 - const radius = 10 - if (img) { - ctx.save() - - // 圆角矩形路径 - ctx.beginPath() - ctx.moveTo(currentX + radius, imgY) - ctx.arcTo(currentX + imgWidth, imgY, currentX + imgWidth, imgY + imgHeight, radius) - ctx.arcTo(currentX + imgWidth, imgY + imgHeight, currentX, imgY + imgHeight, radius) - ctx.arcTo(currentX, imgY + imgHeight, currentX, imgY, radius) - ctx.arcTo(currentX, imgY, currentX + imgWidth, imgY, radius) - ctx.closePath() - - // 设置裁剪区域 - ctx.clip() - - // 绘制图片 - ctx.drawImage(img, currentX, imgY, imgWidth, imgHeight) - - // 恢复绘图状态 - ctx.restore() - } - // 图片右边距5 - currentX += imgWidth + 5 - maxContentWidth -= (imgWidth + 5) - } - - // 价格 - if (i.price) { - const priceXOffset = 385 + x - const maxPriceWidth = 70 - maxContentWidth -= maxPriceWidth - ctx.font = '20px MiSans' - if (i.price.discount) { - // 打折的话原价格加删除线 - const originalWidth = ctx.measureText(i.price.original).width - ctx.fillStyle = '#999' - ctx.fillText(i.price.original, priceXOffset, nameY) - // 删除线 - ctx.strokeStyle = '#999' - // 删除线宽度 - ctx.lineWidth = 1 - ctx.beginPath() - const lineOffset = -7 - ctx.moveTo(priceXOffset, nameY + lineOffset) - ctx.lineTo(priceXOffset + originalWidth, nameY + lineOffset) - ctx.stroke() - - // 折扣率的背景色 - drawBackgroundColor(ctx, '#beee11', priceXOffset, appidY, maxPriceWidth, 20, 10) - - // 折扣率 - ctx.fillStyle = '#333' - ctx.fillText(`-${i.price.discount}%`, priceXOffset, appidY) - ctx.font = 'blob 20px MiSans' - ctx.fillText(i.price.current, priceXOffset, descY) - } else { - ctx.fillText(i.price.original, priceXOffset, nameY) - } - } - - // name - ctx.font = 'bold 20px MiSans' - while (ctx.measureText(i.name).width > maxContentWidth) { - i.name = i.name.slice(0, -1) - } - ctx.fillText(i.name, currentX, nameY) - - // appid - ctx.font = '20px MiSans' - i.appid = i.appid ? String(i.appid) : '' - while (ctx.measureText(i.appid).width > maxContentWidth) { - i.appid = i.appid.slice(0, -1) - } - if (i.appidPercent) { - ctx.fillStyle = '#999999' - ctx.fillRect(currentX, appidY - 18, maxContentWidth * (i.appidPercent / 100), 20) - } - - ctx.fillStyle = '#666' - ctx.fillText(String(i.appid), currentX, appidY) - - // desc - i.desc = i.desc || '' - while (ctx.measureText(i.desc).width > maxContentWidth) { - i.desc = i.desc.slice(0, -1) - } - - if (i.descBgColor) { - drawBackgroundColor(ctx, i.descBgColor, currentX, descY, ctx.measureText(i.desc).width + 10, 20, 10) - ctx.fillStyle = '#ffffff' - } else { - ctx.fillStyle = '#999' - } - - ctx.fillText(i.desc, currentX, descY) - - // 序号 - ctx.font = '12px MiSans' - const indexText = `No. ${index}` - const indexWidth = ctx.measureText(indexText).width - drawBackgroundColor(ctx, '#ffffff', x + 30, y + 10, indexWidth + 8, 11, 0) - ctx.fillStyle = 'black' - ctx.fillText(indexText, x + 30, y + 5) - index++ - - ctx.restore() - - // 更新 x 坐标 - x += gameWidth + spacing - } - - // 更新 y 坐标 - y += gameHeight + spacing - } - // 减去最后一行的间距 - y -= spacing - startX = x - startY = y - } - - // 底部文字 - ctx.font = 'bold 20px MiSans' - ctx.textAlign = 'center' - ctx.fillText(`Created By ${Version.BotName} v${Version.BotVersion} & ${Version.pluginName} v${Version.pluginVersion}`, centerX, startY + 30) - - const buffer = canvas.toBuffer('image/jpeg') - const end = Date.now() - logger.info(`[图片生成][inventory/index] ${(buffer.length / 1024).toFixed(2)}KB ${end - start}ms`) - if (Version.BotName === 'Karin') { - return segment.image(`base64://${buffer.toString('base64')}`) - } else { - return segment.image(buffer) - } -} - export default Render diff --git a/config/default_config/push.yaml b/config/default_config/push.yaml index 76adf67..fea55fd 100644 --- a/config/default_config/push.yaml +++ b/config/default_config/push.yaml @@ -1,9 +1,21 @@ -# 是否开启游玩推送功能 +# 游玩推送总开关 enable: true -# 是否开启状态改变推送功能 比如上线 下线等 +# 开始游戏后是否推送 +playStart: true + +# 游玩结束后是否推送 +playEnd: true + +# 状态推送总开关 stateChange: true +# 上线是否推送 +stateOnline: true + +# 下线是否推送 +stateOffline: true + # 设置每次检查时请求的api # 1: ISteamUserOAuth/GetUserSummaries/v2 @@ -28,7 +40,8 @@ pushApi: 2 # 推送模式 # 1: 文字推送 一条消息就是一个群友 xxx正在玩xxx -# 2: 图片推送 一张图片展示所有群友 +# 2: 图片推送 一张图片展示所有群友 会展示游戏的header图片 +# 3: 仿steam风格的播报图片 只会展示头像和游戏名 不会展示游戏的header图片和时间 pushMode: 1 # Steam Web API 使用条款 diff --git a/lib/logger.js b/lib/logger.js index c795db6..9de7e15 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -19,5 +19,7 @@ export default { ...logger, log: (level, ...logs) => logger[level](chalk.hex(getRandomHexColor())(`[${Version.pluginName}]`, ...logs)), info: (...logs) => logger[Config.other.log ? 'info' : 'debug'](chalk.hex(getRandomHexColor())(`[${Version.pluginName}]`, ...logs)), - error: (...logs) => Config.other.log && logger.error(`[${Version.pluginName}]`, ...logs) + error: (...logs) => Config.other.log && logger.error(`[${Version.pluginName}]`, ...logs), + debug: (...logs) => logger.debug(`[${Version.pluginName}]`, ...logs), + warn: (...logs) => logger.warn(`[${Version.pluginName}]`, ...logs) } diff --git a/models/canvas/canvas.js b/models/canvas/canvas.js new file mode 100644 index 0000000..57fea61 --- /dev/null +++ b/models/canvas/canvas.js @@ -0,0 +1,71 @@ +import { join } from 'path' +import { Version } from '#components' + +export const canvasPKG = await (async () => { + try { + const pkg = await import('@napi-rs/canvas') + const { GlobalFonts } = pkg + const basePath = join(Version.pluginPath, 'resources', 'common', 'font') + const normalFontPath = join(basePath, 'MiSans-Normal.ttf') + const boldFontPath = join(basePath, 'MiSans-Bold.ttf') + GlobalFonts.registerFromPath(normalFontPath, 'MiSans') + GlobalFonts.registerFromPath(boldFontPath, 'Bold') + return pkg + } catch (error) { + return null + } +})() + +export const hasCanvas = !!canvasPKG + +const startTimeMap = new Map() + +export function createCanvas (width, height) { + if (!hasCanvas) throw new Error('请先pnpm i 安装依赖') + const canvas = canvasPKG.createCanvas(width, height) + const ctx = canvas.getContext('2d') + const start = Date.now() + canvas.canvasId = Symbol('canvasId') + startTimeMap.set(canvas.canvasId, start) + setTimeout(() => { + if (startTimeMap.has(canvas.canvasId)) { + logger.error('[图片生成][canvas] 超时 30000ms') + startTimeMap.delete(canvas.canvasId) + } + }, 1000 * 30) + return { ctx, canvas } +} + +/** + * 转换成可发送的图片 + * @param {import('@napi-rs/canvas').Canvas} canvas + * @returns + */ +export function toImage (canvas) { + const buffer = canvas.toBuffer('image/jpeg') + const end = Date.now() + const start = startTimeMap.get(canvas.canvasId) || end - 1000 * 30 + startTimeMap.delete(canvas.canvasId) + logger.info(`[图片生成][canvas] ${(buffer.length / 1024).toFixed(2)}KB ${end - start}ms`) + if (Version.BotName === 'Karin') { + return segment.image(`base64://${buffer.toString('base64')}`) + } else { + return segment.image(buffer) + } +} + +export function drawBackgroundColor (ctx, color, x, y, width, height, radius) { + ctx.beginPath() + const backgroundX = x - 5 + const backgroundY = y - 20 + const backgroundWidth = width + 2 + const backgroundHeight = height + 5 + ctx.fillStyle = color + ctx.moveTo(backgroundX + radius, backgroundY) + ctx.arcTo(backgroundX + backgroundWidth, backgroundY, backgroundX + backgroundWidth, backgroundY + backgroundHeight, radius) + ctx.arcTo(backgroundX + backgroundWidth, backgroundY + backgroundHeight, backgroundX, backgroundY + backgroundHeight, radius) + ctx.arcTo(backgroundX, backgroundY + backgroundHeight, backgroundX, backgroundY, radius) + ctx.arcTo(backgroundX, backgroundY, backgroundX + backgroundWidth, backgroundY, radius) + ctx.closePath() + ctx.fill() +} diff --git a/models/canvas/game.js b/models/canvas/game.js new file mode 100644 index 0000000..a306f31 --- /dev/null +++ b/models/canvas/game.js @@ -0,0 +1,79 @@ +import { join } from 'path' +import { canvasPKG, createCanvas, toImage } from './canvas.js' +import { Version } from '#components' + +export async function render (data) { + const { loadImage } = canvasPKG + + const bg = await loadImage(join(Version.pluginPath, 'resources', 'game', 'game.png')) + + const { ctx, canvas } = createCanvas(bg.width, bg.height * data.length) + + let x = 0 + let y = 0 + + const images = await Promise.all(data.map(async (i) => ({ ...i, _avatar: await loadImage(i.avatar || i.image) }))).then(i => i.reduce((acc, cur) => { + if (cur?._avatar) { + acc[`${cur.name}${cur.appid}${cur.desc}`] = cur._avatar + } + return acc + }, {})) + + for (const i of data) { + ctx.drawImage(bg, x, y, bg.width, bg.height) + + x += 15 + y += 20 + const avatar = images[`${i.name}${i.appid}${i.desc}`] + if (avatar) { + ctx.drawImage(avatar, x, y, 66, 66) + } + + ctx.font = '19px MiSans' + ctx.fillStyle = '#e3ffc2' + + let nickname = i.isAvatar ? i.name : i.appid + + if (ctx.measureText(nickname).width > 300) { + while (ctx.measureText(nickname).width > 300) { + nickname = nickname.slice(0, -1) + } + nickname += ' ...' + } + + x += 85 + y += 15 + ctx.fillText(nickname, x, y) + + if (!i.isAvatar) { + y += 25 + ctx.font = '17px MiSans' + ctx.fillStyle = '#969696' + ctx.fillText(i.type === 'end' ? '结束玩' : '正在玩', x, y) + + let name = i.name + + if (ctx.measureText(name).width > 357) { + while (ctx.measureText(name).width > 357) { + name = name.slice(0, -1) + } + name += ' ...' + } + + y += 25 + ctx.font = '14px Bold' + ctx.fillStyle = '#91c257' + ctx.fillText(name, x, y) + } else { + y += 50 + ctx.font = '14px Bold' + ctx.fillStyle = i.desc.includes('在线') ? '#beee11' : '#999999' + ctx.fillText(i.desc, x, y) + } + + x = 0 + y += 20 + } + + return toImage(canvas) +} diff --git a/models/canvas/index.js b/models/canvas/index.js new file mode 100644 index 0000000..d621497 --- /dev/null +++ b/models/canvas/index.js @@ -0,0 +1,2 @@ +export * as game from './game.js' +export * as inventory from './inventory.js' diff --git a/models/canvas/inventory.js b/models/canvas/inventory.js new file mode 100644 index 0000000..fd2921a --- /dev/null +++ b/models/canvas/inventory.js @@ -0,0 +1,251 @@ +import _ from 'lodash' +import { utils } from '#models' +import { Version } from '#components' +import { canvasPKG, drawBackgroundColor, createCanvas, toImage } from './canvas.js' + +export async function render (data, lineItemCount) { + const { loadImage } = canvasPKG + + // 每一项的宽高间距 + const gameWidth = 468 + const gameHeight = 93 + const spacing = 10 + + // 总行数 + const lineTotal = _.sum(data.map(i => Math.ceil(i.games.length / lineItemCount))) + // 额外高度 + const extraHeight = _.sumBy(data, i => i.desc.length * 30 + 50) + 30 + + // 创建画布 + // 宽度为 (每项的宽+间距)*每行个数 + 左间距 + const canvasWidth = (gameWidth + spacing) * lineItemCount + spacing + // 高度为 (每项的高+间距)*行数 + 额外高度 + const canvasHeight = (gameHeight + spacing) * lineTotal + extraHeight + + // 计算居中坐标 + const centerX = canvasWidth / 2 + + const { ctx, canvas } = createCanvas(canvasWidth, canvasHeight) + + // 设置背景颜色 + ctx.fillStyle = '#ffffff' + ctx.fillRect(0, 0, canvasWidth, canvasHeight) + + // 设置字体和颜色 + ctx.font = '20px MiSans' + ctx.fillStyle = '#000000' + + // 异步加载图片 + const imgs = await Promise.all(data.map(i => i.games).flat().map(async i => { + if (i.noImg) { + return {} + } + const Image = await loadImage(i.image || utils.steam.getHeaderImgUrlByAppid(i.appid)).catch(() => null) + return { + ...i, + Image + } + })).then(imgs => imgs.reduce((acc, cur) => { + if (cur?.Image) { + acc[`${cur.name}${cur.appid}${cur.desc}`] = cur.Image + } + return acc + }, {})) + + let startX = 0 + let startY = 0 + + for (const g of data) { + startY += 40 + // title + ctx.save() + ctx.font = 'bold 24px MiSans' + ctx.textAlign = 'center' + ctx.fillText(g.title, centerX, startY) + ctx.restore() + + // desc + if (g.desc.length) { + for (const desc of g.desc) { + startY += 30 + ctx.save() + ctx.font = 'bold 20px MiSans' + ctx.textAlign = 'center' + ctx.fillText(desc, centerX, startY) + ctx.restore() + } + } + + let x = 10 - gameWidth + startX + let y = startY + 10 + let index = 1 + + for (const items of _.chunk(g.games, lineItemCount)) { + const remainingItems = items.length + + // 如果是最后一行且元素数量不足,则居中 + if (remainingItems < lineItemCount) { + const totalWidth = gameWidth * remainingItems + spacing * (remainingItems - 1) + // 计算居中偏移量 + x = (canvasWidth - totalWidth) / 2 + } else { + x = 10 + } + + // 绘制当前行的元素 + for (const i of items) { + ctx.save() + + const nameY = y + 24 + const appidY = y + 52 + const descY = y + 79 + + let currentX = x + + // 边框 + ctx.strokeStyle = '#ccc' + ctx.lineWidth = 1 + ctx.beginPath() + ctx.roundRect(currentX, y, gameWidth, gameHeight, 10) + ctx.stroke() + + // 内边距10 + currentX += 10 + + // 最大内容宽度 20内间距 + let maxContentWidth = gameWidth - 20 + + // 图片 + if (!i.noImg) { + const img = imgs[`${i.name}${i.appid}${i.desc}`] + const imgY = y + 10 + const imgWidth = i.isAvatar ? 72 : 156 + const imgHeight = 72 + const radius = 10 + if (img) { + ctx.save() + + // 圆角矩形路径 + ctx.beginPath() + ctx.moveTo(currentX + radius, imgY) + ctx.arcTo(currentX + imgWidth, imgY, currentX + imgWidth, imgY + imgHeight, radius) + ctx.arcTo(currentX + imgWidth, imgY + imgHeight, currentX, imgY + imgHeight, radius) + ctx.arcTo(currentX, imgY + imgHeight, currentX, imgY, radius) + ctx.arcTo(currentX, imgY, currentX + imgWidth, imgY, radius) + ctx.closePath() + + // 设置裁剪区域 + ctx.clip() + + // 绘制图片 + ctx.drawImage(img, currentX, imgY, imgWidth, imgHeight) + + // 恢复绘图状态 + ctx.restore() + } + // 图片右边距5 + currentX += imgWidth + 5 + maxContentWidth -= (imgWidth + 5) + } + + // 价格 + if (i.price) { + const priceXOffset = 385 + x + const maxPriceWidth = 70 + maxContentWidth -= maxPriceWidth + ctx.font = '20px MiSans' + if (i.price.discount) { + // 打折的话原价格加删除线 + const originalWidth = ctx.measureText(i.price.original).width + ctx.fillStyle = '#999' + ctx.fillText(i.price.original, priceXOffset, nameY) + // 删除线 + ctx.strokeStyle = '#999' + // 删除线宽度 + ctx.lineWidth = 1 + ctx.beginPath() + const lineOffset = -7 + ctx.moveTo(priceXOffset, nameY + lineOffset) + ctx.lineTo(priceXOffset + originalWidth, nameY + lineOffset) + ctx.stroke() + + // 折扣率的背景色 + drawBackgroundColor(ctx, '#beee11', priceXOffset, appidY, maxPriceWidth, 20, 10) + + // 折扣率 + ctx.fillStyle = '#333' + ctx.fillText(`-${i.price.discount}%`, priceXOffset, appidY) + ctx.font = 'blob 20px MiSans' + ctx.fillText(i.price.current, priceXOffset, descY) + } else { + ctx.fillText(i.price.original, priceXOffset, nameY) + } + } + + // name + ctx.font = 'bold 20px MiSans' + while (ctx.measureText(i.name).width > maxContentWidth) { + i.name = i.name.slice(0, -1) + } + ctx.fillText(i.name, currentX, nameY) + + // appid + ctx.font = '20px MiSans' + i.appid = i.appid ? String(i.appid) : '' + while (ctx.measureText(i.appid).width > maxContentWidth) { + i.appid = i.appid.slice(0, -1) + } + if (i.appidPercent) { + ctx.fillStyle = '#999999' + ctx.fillRect(currentX, appidY - 18, maxContentWidth * (i.appidPercent / 100), 20) + } + + ctx.fillStyle = '#666' + ctx.fillText(String(i.appid), currentX, appidY) + + // desc + i.desc = i.desc || '' + while (ctx.measureText(i.desc).width > maxContentWidth) { + i.desc = i.desc.slice(0, -1) + } + + if (i.descBgColor) { + drawBackgroundColor(ctx, i.descBgColor, currentX, descY, ctx.measureText(i.desc).width + 10, 20, 10) + ctx.fillStyle = '#ffffff' + } else { + ctx.fillStyle = '#999' + } + + ctx.fillText(i.desc, currentX, descY) + + // 序号 + ctx.font = '12px MiSans' + const indexText = `No. ${index}` + const indexWidth = ctx.measureText(indexText).width + drawBackgroundColor(ctx, '#ffffff', x + 30, y + 10, indexWidth + 8, 11, 0) + ctx.fillStyle = 'black' + ctx.fillText(indexText, x + 30, y + 5) + index++ + + ctx.restore() + + // 更新 x 坐标 + x += gameWidth + spacing + } + + // 更新 y 坐标 + y += gameHeight + spacing + } + // 减去最后一行的间距 + y -= spacing + startX = x + startY = y + } + + // 底部文字 + ctx.font = 'bold 20px MiSans' + ctx.textAlign = 'center' + ctx.fillText(`Created By ${Version.BotName} v${Version.BotVersion} & ${Version.pluginName} v${Version.pluginVersion}`, centerX, startY + 30) + + return toImage(canvas) +} diff --git a/models/db/history.js b/models/db/history.js index a8a10d1..a32d605 100644 --- a/models/db/history.js +++ b/models/db/history.js @@ -1,103 +1,4 @@ -import { sequelize, DataTypes } from './base.js' +import { sequelize } from './base.js' -/** - * @typedef {Object} HistoryColumns - * @property {number} id - * @property {string} userId 用户id - * @property {string} groupId 群id - * @property {string} botId 机器人id - * @property {string} steamId steamId - * @property {string} appid 游戏id 如果有appid就是玩游戏记录 没有就是上线记录 - * @property {string} name 游戏名称 - * @property {number} start 开始的时间戳 - * @property {number} end 结束的时间戳 - */ -const HistoryTable = sequelize.define('history', { - id: { - type: DataTypes.BIGINT, - primaryKey: true, - autoIncrement: true - }, - userId: { - type: DataTypes.STRING, - allowNull: false - }, - groupId: { - type: DataTypes.STRING, - allowNull: false - }, - botId: { - type: DataTypes.STRING, - allowNull: false - }, - steamId: { - type: DataTypes.STRING, - allowNull: false - }, - appid: { - type: DataTypes.STRING - }, - name: { - type: DataTypes.STRING - }, - start: { - type: DataTypes.BIGINT - }, - end: { - type: DataTypes.BIGINT - } -}, { - freezeTableName: true -}) - -await HistoryTable.sync() - -/** - * 添加一条记录 - * @param {string} userId - * @param {string} groupId - * @param {string} botId - * @param {string} steamId - * @param {number} start - * @param {number?} end - * @param {string?} appid - * @param {string?} name - */ -export async function HistoryAdd (userId, groupId, botId, steamId, start, end, appid, name) { - userId = String(userId) - groupId = String(groupId) - botId = String(botId) - steamId = String(steamId) - start = Number(start) - if (end) { - end = Number(end) - } - - const baseData = { userId, groupId, botId, steamId, start } - - const existingRecord = await HistoryTable.findOne({ - where: baseData - }) - - if (existingRecord) { - // 如果有记录,更新 end 字段 - if (end) { - await existingRecord.update({ end }) - } - } else { - // 如果没有记录,创建新记录 - const newRecord = { - ...baseData, - end - } - - if (appid) { - Object.assign(newRecord, { - appid: String(appid), - name - }) - } - - await HistoryTable.create(newRecord) - } -} +// 没啥用 2025年1月6日13:39:04 +sequelize.query('DROP TABLE IF EXISTS history;') diff --git a/models/index.js b/models/index.js index 26822b0..921fb4e 100644 --- a/models/index.js +++ b/models/index.js @@ -1,7 +1,8 @@ export * as db from './db/index.js' -export * as bind from './bind/index.js' -export * as utils from './utils/index.js' export * as api from './api/index.js' +export * as bind from './bind/index.js' export * as task from './task/index.js' export * as help from './help/index.js' +export * as utils from './utils/index.js' +export * as canvas from './canvas/index.js' export * as setting from './setting/index.js' diff --git a/models/setting/index.js b/models/setting/index.js index a2cc5b2..5494156 100644 --- a/models/setting/index.js +++ b/models/setting/index.js @@ -62,12 +62,26 @@ export const cfgSchema = { title: '推送设置', cfg: { enable: { - title: '推送总开关', + title: '游玩推送总开关', key: '推送', type: 'boolean', def: true, desc: '是否开启推送功能' }, + playStart: { + title: '游戏开始推送', + key: '开始', + type: 'boolean', + def: true, + desc: '是否推送开始游戏' + }, + playEnd: { + title: '游戏结束推送', + key: '结束', + type: 'boolean', + def: true, + desc: '是否推送结束游戏' + }, defaultPush: { title: '默认开启推送', key: '默认推送', @@ -76,11 +90,25 @@ export const cfgSchema = { desc: '是否默认开启推送, 绑定steamId后自动开启推送' }, stateChange: { - title: '状态改变推送', + title: '状态改变推送总开关', key: '状态推送', type: 'boolean', def: true, - desc: '是否推送游戏状态改变 比如上线 下线等' + desc: '是否推送游戏状态改变' + }, + stateOnline: { + title: '上线推送', + key: '上线', + type: 'boolean', + def: true, + desc: '是否推送上线' + }, + stateOffline: { + title: '下线推送', + key: '下线', + type: 'boolean', + def: true, + desc: '是否推送下线' }, pushApi: { title: '推送请求api', @@ -113,16 +141,17 @@ export const cfgSchema = { component: 'RadioGroup', options: [ { label: '文字推送', value: 1 }, - { label: '图片推送', value: 2 } + { label: '图片推送', value: 2 }, + { label: 'steam风格图片', value: 3 } ], input: (n) => { - if (n >= 1 && n <= 2) { + if (n >= 1 && n <= 3) { return n * 1 } else { return 1 } }, - desc: '推送模式 1: 文字推送 2: 图片推送' + desc: '推送模式 1: 文字推送 2: 图片推送 3: steam风格图片' }, randomBot: { title: '随机推送Bot', diff --git a/models/task/index.js b/models/task/index.js index a84a37b..dc9efc1 100644 --- a/models/task/index.js +++ b/models/task/index.js @@ -56,6 +56,7 @@ export function startTimer () { } else if (!Bot[i.botId] && !Config.push.randomBot) { continue } + const avatar = await utils.bot.getUserAvatar(i.botId, i.userId, i.groupId) // 0 就是没有人绑定 const nickname = i.userId == '0' ? player.personaname : await utils.bot.getUserName(i.botId, i.userId, i.groupId) // 先收集所有要推送的用户 @@ -69,53 +70,51 @@ export function startTimer () { state: [] } } - if (Config.push.enable && player.gameid && player.gameid != lastPlay.appid) { + if (Config.push.enable && Config.push.playStart && player.gameid && player.gameid != lastPlay.appid) { const time = now - lastPlay.playTime state.playTime = now userList[i.groupId][i.botId].start.push({ name: player.gameextrainfo, appid: `${nickname}(${player.personaname})`, desc: lastPlay.playTime ? `距离上次 ${utils.formatDuration(time)}` : '', - image: iconUrl + image: iconUrl, + avatar, + type: 'start' }) db.StatsTableUpdate(i.userId, i.groupId, i.botId, i.steamId, player.gameid, player.gameextrainfo, 'playTotal', 1).catch(e => logger.error('更新统计数据失败', e.message)) - db.HistoryAdd(i.userId, i.groupId, i.botId, i.steamId, now, null, player.gameid, player.gameextrainfo).catch(e => logger.error('添加历史记录失败', e.message)) } - if (Config.push.enable && lastPlay.name && lastPlay.name != player.gameextrainfo) { + if (Config.push.enable && Config.push.playEnd && lastPlay.name && lastPlay.name != player.gameextrainfo) { const time = now - lastPlay.playTime state.playTime = now userList[i.groupId][i.botId].end.push({ name: lastPlay.name, appid: `${nickname}(${player.personaname})`, desc: `时长: ${utils.formatDuration(time)}`, - image: utils.steam.getHeaderImgUrlByAppid(lastPlay.appid) + image: utils.steam.getHeaderImgUrlByAppid(lastPlay.appid), + avatar, + type: 'end' }) db.StatsTableUpdate(i.userId, i.groupId, i.botId, i.steamId, lastPlay.appid, lastPlay.name, 'playTime', time).catch(e => logger.error('更新统计数据失败', e.message)) - db.HistoryAdd(i.userId, i.groupId, i.botId, i.steamId, lastPlay.playTime, now, lastPlay.appid, lastPlay.name).catch(e => logger.error('添加历史记录失败', e.message)) } // 在线状态改变 if (Config.push.stateChange && player.personastate != lastPlay.state) { const time = now - lastPlay.onlineTime - if ([0, 1].includes(player.personastate)) { - state.onlineTime = now - userList[i.groupId][i.botId].state.push({ - name: `${nickname}(${player.personaname})`, - appid: lastPlay.onlineTime ? `距离上次 ${utils.formatDuration(time)}` : '', - 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(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)) - db.HistoryAdd(i.userId, i.groupId, i.botId, i.steamId, lastPlay.onlineTime, now).catch(e => logger.error('添加历史记录失败', e.message)) - } else { - db.StatsTableUpdate(i.userId, i.groupId, i.botId, i.steamId, player.gameid, player.gameextrainfo, 'onlineTotal', 1).catch(e => logger.error('更新统计数据失败', e)) - db.HistoryAdd(i.userId, i.groupId, i.botId, i.steamId, now).catch(e => logger.error('添加历史记录失败', e.message)) - } + if (Config.push.stateOffline && 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)) + } else if (Config.push.stateOnline && player.personastate === 1) { + db.StatsTableUpdate(i.userId, i.groupId, i.botId, i.steamId, player.gameid, player.gameextrainfo, 'onlineTotal', 1).catch(e => logger.error('更新统计数据失败', e.message)) } else { - state.state = player.personastate === 0 ? 0 : 1 + continue } + state.onlineTime = now + userList[i.groupId][i.botId].state.push({ + name: `${nickname}(${player.personaname})`, + appid: lastPlay.onlineTime ? `距离上次 ${utils.formatDuration(time)}` : '', + desc: `已${utils.steam.getPersonaState(player.personastate)}`, + image: avatar || (Config.other.steamAvatar ? i.avatarfull : ''), + isAvatar: true, + descBgColor: getColor(player.personastate) + }) } } } @@ -125,8 +124,9 @@ export function startTimer () { for (const botId in userList[gid]) { const i = userList[gid][botId] const data = [] + const isImg = [2, 3].includes(Number(Config.push.pushMode)) if (i.start.length) { - if (Config.push.pushMode == 2) { + if (isImg) { data.push({ title: '开始玩游戏的群友', games: i.start @@ -136,7 +136,7 @@ export function startTimer () { } } if (i.end.length) { - if (Config.push.pushMode == 2) { + if (isImg) { data.push({ title: '结束玩游戏的群友', games: i.end @@ -146,7 +146,7 @@ export function startTimer () { } } if (i.state.length) { - if (Config.push.pushMode == 2) { + if (isImg) { data.push({ title: '在线状态改变的群友', games: i.state @@ -161,9 +161,12 @@ export function startTimer () { if (!data.length) { continue } - if (Config.push.pushMode == 2) { - const img = await Render.render('inventory/index', { data }) - await utils.bot.sendGroupMsg(botId, gid, img) + if (isImg) { + const path = Config.push.pushMode === 2 ? 'inventory/index' : 'game/game' + const img = await Render.render(path, { data }) + if (typeof img !== 'string') { + await utils.bot.sendGroupMsg(botId, gid, img) + } } else { for (const msg of data) { await utils.bot.sendGroupMsg(botId, gid, msg) diff --git a/models/utils/bot.js b/models/utils/bot.js index 4781813..f45cf8c 100644 --- a/models/utils/bot.js +++ b/models/utils/bot.js @@ -48,19 +48,19 @@ export async function getUserAvatar (botId, uid, gid) { try { if (Version.BotName === 'Karin') { const bot = Bot.getBot(botId) - const avatarUrl = await bot.getAvatarUrl(uid) - return avatarUrl || await bot.getAvatarUrl(botId) || '' + const avatarUrl = await bot.getAvatarUrl(uid, 100) + return avatarUrl || '' } else { uid = Number(uid) || uid const bot = (Config.push.randomBot && Version.BotName === 'Trss-Yunzai') ? Bot : Bot[botId] if (gid) { gid = Number(gid) || gid const group = bot.pickGroup(gid) - const avatarUrl = (await group.pickMember(uid)).getAvatarUrl() + const avatarUrl = (await group.pickMember(uid)).getAvatarUrl(100) return avatarUrl || bot.avatar } else { const user = bot.pickUser(uid) - const avatarUrl = await user.getAvatarUrl() + const avatarUrl = await user.getAvatarUrl(100) return avatarUrl || bot.avatar || '' } } diff --git a/models/utils/steam.js b/models/utils/steam.js index 82ad4fa..822461c 100644 --- a/models/utils/steam.js +++ b/models/utils/steam.js @@ -161,6 +161,7 @@ export async function refreshAccessToken (token) { * timecreated: string, * gameid?: string, * gameextrainfo?: string, +* community_icon?: string * }[]>} */ export async function getUserSummaries (steamIds) { @@ -209,7 +210,7 @@ export async function getUserSummaries (steamIds) { } return await api.IPlayerService.GetPlayerLinkDetails(steamIds).then(async res => { const appids = res.map(i => i.private_data.game_id).filter(Boolean) - const appInfo = appids.length ? await api.IStoreBrowseService.GetItems(appids) : {} + const appInfo = appids.length ? await api.IStoreBrowseService.GetItems(appids, { include_assets: true }) : {} return res.map(i => { const avatarhash = Buffer.from(i.public_data.sha_digest_avatar, 'base64').toString('hex') const gameid = i.private_data.game_id @@ -226,7 +227,8 @@ export async function getUserSummaries (steamIds) { timecreated: i.private_data.time_created, gameid, gameextrainfo: appInfo[gameid]?.name, - lastlogoff: i.private_data.last_logoff_time + lastlogoff: i.private_data.last_logoff_time, + community_icon: appInfo[gameid]?.assets?.community_icon } }) }) diff --git a/resources/game/friend_add.png b/resources/game/friend_add.png new file mode 100644 index 0000000000000000000000000000000000000000..382e250a967d12af6c2a8683db12193699d2a087 GIT binary patch literal 5174 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 literal 0 HcmV?d00001 diff --git a/resources/game/friend_bg.png b/resources/game/friend_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..395aeb3b6f34c3d9e9bb4e7af22f3ed16895d860 GIT binary patch literal 7467 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 literal 0 HcmV?d00001 diff --git a/resources/game/game.css b/resources/game/game.css new file mode 100644 index 0000000..389ca46 --- /dev/null +++ b/resources/game/game.css @@ -0,0 +1,66 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +#container { + width: 402px; +} + +.box { + position: relative; + height: 105px; + background-image: url("./game.png"); +} + +.avatar { + position: absolute; + top: 20px; + left: 15px; +} + +.info { + position: absolute; + top: 16px; + left: 100px; + height: 70px; + display: flex; + flex-direction: column; + justify-content: space-between; + width: 290px; +} + +.info>div { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.name { + font-size: 19px; + color: #e3ffc2; +} + +.desc { + font-size: 15px; + color: #969696; +} + +.game { + font-size: 14px; + color: #91c257; + font-weight: bold; +} + +.online { + font-size: 14px; + color: #beee11; + font-weight: bold; +} + +.offline { + font-size: 14px; + color: #999999; + font-weight: bold; +} \ No newline at end of file diff --git a/resources/game/game.html b/resources/game/game.html new file mode 100644 index 0000000..15154f7 --- /dev/null +++ b/resources/game/game.html @@ -0,0 +1,33 @@ + + + + + + + steam-plugin + + + + +
+ {{each data i}} +
+
+ 头像 +
+
+ {{if i.isAvatar}} +
{{i.name}}
+
{{i.desc}}
+ {{else}} +
{{i.appid}}
+
{{i.type === 'start' ? '正在玩' : '结束玩'}}
+
{{i.name}}
+ {{/if}} +
+
+ {{/each}} +
+ + + \ No newline at end of file diff --git a/resources/game/game.png b/resources/game/game.png new file mode 100644 index 0000000000000000000000000000000000000000..188017dfc6836c00cf060cb3ed2f62d40ec79dc0 GIT binary patch literal 8807 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 literal 0 HcmV?d00001