koishi-plugin-steam-info-check 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logger = exports.Config = exports.inject = exports.name = void 0;
4
+ exports.apply = apply;
5
+ const koishi_1 = require("koishi");
6
+ const service_1 = require("./service");
7
+ const drawer_1 = require("./drawer");
8
+ exports.name = 'steam-info';
9
+ exports.inject = ['model', 'http', 'puppeteer', 'database'];
10
+ exports.Config = koishi_1.Schema.object({
11
+ steamApiKey: koishi_1.Schema.array(String).required().description('Steam API Key (supports multiple)'),
12
+ proxy: koishi_1.Schema.string().description('Proxy URL (e.g., http://127.0.0.1:7890)'),
13
+ steamRequestInterval: koishi_1.Schema.number().default(300).description('Polling interval in seconds'),
14
+ steamBroadcastType: koishi_1.Schema.union(['all', 'part', 'none']).default('part').description('Broadcast type: all (list), part (gaming only), none (text only)'),
15
+ steamDisableBroadcastOnStartup: koishi_1.Schema.boolean().default(false).description('Disable broadcast on startup'),
16
+ fonts: koishi_1.Schema.object({
17
+ regular: koishi_1.Schema.string().default('fonts/MiSans-Regular.ttf'),
18
+ light: koishi_1.Schema.string().default('fonts/MiSans-Light.ttf'),
19
+ bold: koishi_1.Schema.string().default('fonts/MiSans-Bold.ttf'),
20
+ }).description('Font paths relative to the plugin resource directory or absolute paths'),
21
+ commandAuthority: koishi_1.Schema.object({
22
+ bind: koishi_1.Schema.number().default(1).description('Authority for bind command'),
23
+ unbind: koishi_1.Schema.number().default(1).description('Authority for unbind command'),
24
+ info: koishi_1.Schema.number().default(1).description('Authority for info command'),
25
+ check: koishi_1.Schema.number().default(1).description('Authority for check command'),
26
+ enable: koishi_1.Schema.number().default(2).description('Authority for enable broadcast'),
27
+ disable: koishi_1.Schema.number().default(2).description('Authority for disable broadcast'),
28
+ update: koishi_1.Schema.number().default(2).description('Authority for update group info'),
29
+ nickname: koishi_1.Schema.number().default(1).description('Authority for nickname command'),
30
+ }).description('Command Authorities'),
31
+ });
32
+ exports.logger = new koishi_1.Logger('steam-info');
33
+ function apply(ctx, config) {
34
+ // Localization
35
+ ctx.i18n.define('zh-CN', require('./locales/zh-CN'));
36
+ // Services
37
+ ctx.plugin(service_1.SteamService, config);
38
+ ctx.plugin(drawer_1.DrawService, config);
39
+ // Database
40
+ ctx.model.extend('steam_bind', {
41
+ id: 'unsigned',
42
+ userId: 'string',
43
+ channelId: 'string',
44
+ steamId: 'string',
45
+ nickname: 'string',
46
+ }, {
47
+ primary: 'id',
48
+ autoInc: true,
49
+ });
50
+ ctx.model.extend('steam_channel', {
51
+ id: 'string', // channelId
52
+ enable: 'boolean',
53
+ name: 'string',
54
+ avatar: 'string',
55
+ }, {
56
+ primary: 'id',
57
+ });
58
+ // Commands
59
+ ctx.command('steam', 'Steam Information');
60
+ ctx.command('steam.bind <steamId:string>', 'Bind Steam ID', { authority: config.commandAuthority.bind })
61
+ .alias('steambind', '绑定steam')
62
+ .action(async ({ session }, steamId) => {
63
+ if (!session)
64
+ return;
65
+ if (!steamId || !/^\d+$/.test(steamId))
66
+ return session.text('.invalid_id');
67
+ const targetId = await ctx.steam.getSteamId(steamId);
68
+ if (!targetId)
69
+ return session.text('.id_not_found');
70
+ await ctx.database.upsert('steam_bind', [
71
+ {
72
+ userId: session.userId,
73
+ channelId: session.channelId,
74
+ steamId: targetId,
75
+ }
76
+ ], ['userId', 'channelId']);
77
+ return session.text('.bind_success', [targetId]);
78
+ });
79
+ ctx.command('steam.unbind', 'Unbind Steam ID', { authority: config.commandAuthority.unbind })
80
+ .alias('steamunbind', '解绑steam')
81
+ .action(async ({ session }) => {
82
+ if (!session)
83
+ return;
84
+ const result = await ctx.database.remove('steam_bind', {
85
+ userId: session.userId,
86
+ channelId: session.channelId,
87
+ });
88
+ return result ? session.text('.unbind_success') : session.text('.not_bound');
89
+ });
90
+ ctx.command('steam.info [target:text]', 'View Steam Profile', { authority: config.commandAuthority.info })
91
+ .alias('steaminfo', 'steam信息')
92
+ .action(async ({ session }, target) => {
93
+ if (!session)
94
+ return;
95
+ let steamId = null;
96
+ if (target) {
97
+ // Check if target is mention
98
+ const [platform, userId] = session.resolve(target);
99
+ if (userId) {
100
+ const bind = await ctx.database.get('steam_bind', { userId, channelId: session.channelId });
101
+ if (bind.length)
102
+ steamId = bind[0].steamId;
103
+ }
104
+ else if (/^\d+$/.test(target)) {
105
+ steamId = await ctx.steam.getSteamId(target);
106
+ }
107
+ }
108
+ else {
109
+ const bind = await ctx.database.get('steam_bind', { userId: session.userId, channelId: session.channelId });
110
+ if (bind.length)
111
+ steamId = bind[0].steamId;
112
+ }
113
+ if (!steamId)
114
+ return session.text('.user_not_found');
115
+ const profile = await ctx.steam.getUserData(steamId);
116
+ const image = await ctx.drawer.drawPlayerStatus(profile, steamId);
117
+ if (typeof image === 'string')
118
+ return session.send(image);
119
+ return session.send(koishi_1.h.image(image, 'image/png'));
120
+ });
121
+ ctx.command('steam.check', 'Check Friends Status', { authority: config.commandAuthority.check })
122
+ .alias('steamcheck', '查看steam', '查steam')
123
+ .action(async ({ session }) => {
124
+ if (!session)
125
+ return;
126
+ const binds = await ctx.database.get('steam_bind', { channelId: session.channelId });
127
+ if (binds.length === 0)
128
+ return session.text('.no_binds');
129
+ const steamIds = binds.map(b => b.steamId);
130
+ const summaries = await ctx.steam.getPlayerSummaries(steamIds);
131
+ if (summaries.length === 0)
132
+ return session.text('.api_error');
133
+ const channelInfo = await ctx.database.get('steam_channel', { id: session.channelId });
134
+ const parentAvatar = channelInfo[0]?.avatar
135
+ ? Buffer.from(channelInfo[0].avatar, 'base64')
136
+ : await ctx.drawer.getDefaultAvatar();
137
+ const parentName = channelInfo[0]?.name || session.channelId || 'Unknown';
138
+ const image = await ctx.drawer.drawFriendsStatus(parentAvatar, parentName, summaries, binds);
139
+ if (typeof image === 'string')
140
+ return session.send(image);
141
+ return session.send(koishi_1.h.image(image, 'image/png'));
142
+ });
143
+ ctx.command('steam.enable', 'Enable Broadcast', { authority: config.commandAuthority.enable })
144
+ .alias('steamenable', '启用steam')
145
+ .action(async ({ session }) => {
146
+ if (!session)
147
+ return;
148
+ await ctx.database.upsert('steam_channel', [{ id: session.channelId, enable: true }]);
149
+ return session.text('.enable_success');
150
+ });
151
+ ctx.command('steam.disable', 'Disable Broadcast', { authority: config.commandAuthority.disable })
152
+ .alias('steamdisable', '禁用steam')
153
+ .action(async ({ session }) => {
154
+ if (!session)
155
+ return;
156
+ await ctx.database.upsert('steam_channel', [{ id: session.channelId, enable: false }]);
157
+ return session.text('.disable_success');
158
+ });
159
+ ctx.command('steam.update [name:string] [avatar:image]', 'Update Group Info', { authority: config.commandAuthority.update })
160
+ .alias('steamupdate', '更新群信息')
161
+ .action(async ({ session }, name, avatar) => {
162
+ if (!session)
163
+ return;
164
+ const img = session.elements && session.elements.find(e => e.type === 'img');
165
+ const imgUrl = img?.attrs?.src;
166
+ let avatarBase64 = null;
167
+ if (imgUrl) {
168
+ const buffer = await ctx.http.get(imgUrl, { responseType: 'arraybuffer' });
169
+ avatarBase64 = Buffer.from(buffer).toString('base64');
170
+ }
171
+ if (!name && !avatarBase64)
172
+ return session.text('.args_missing');
173
+ const update = { id: session.channelId };
174
+ if (name)
175
+ update.name = name;
176
+ if (avatarBase64)
177
+ update.avatar = avatarBase64;
178
+ await ctx.database.upsert('steam_channel', [update]);
179
+ return session.text('.update_success');
180
+ });
181
+ ctx.command('steam.nickname <nickname:string>', 'Set Steam Nickname', { authority: config.commandAuthority.nickname })
182
+ .alias('steamnickname', 'steam昵称')
183
+ .action(async ({ session }, nickname) => {
184
+ if (!session)
185
+ return;
186
+ const bind = await ctx.database.get('steam_bind', { userId: session.userId, channelId: session.channelId });
187
+ if (!bind.length)
188
+ return session.text('.not_bound');
189
+ await ctx.database.upsert('steam_bind', [{ ...bind[0], nickname }]);
190
+ return session.text('.nickname_set', [nickname]);
191
+ });
192
+ // Scheduler
193
+ ctx.setInterval(async () => {
194
+ await broadcast(ctx);
195
+ }, config.steamRequestInterval * 1000);
196
+ }
197
+ // Broadcast Logic
198
+ const statusCache = new Map();
199
+ async function broadcast(ctx) {
200
+ const channels = await ctx.database.get('steam_channel', { enable: true });
201
+ if (channels.length === 0)
202
+ return;
203
+ const channelIds = channels.map(c => c.id);
204
+ const binds = await ctx.database.get('steam_bind', { channelId: channelIds });
205
+ if (binds.length === 0)
206
+ return;
207
+ const steamIds = [...new Set(binds.map(b => b.steamId))];
208
+ const currentSummaries = await ctx.steam.getPlayerSummaries(steamIds);
209
+ const currentMap = new Map(currentSummaries.map(p => [p.steamid, p]));
210
+ for (const channel of channels) {
211
+ const channelBinds = binds.filter(b => b.channelId === channel.id);
212
+ const msgs = [];
213
+ const startGamingPlayers = [];
214
+ for (const bind of channelBinds) {
215
+ const current = currentMap.get(bind.steamId);
216
+ const old = statusCache.get(bind.steamId);
217
+ if (!current)
218
+ continue;
219
+ if (!old)
220
+ continue;
221
+ const oldGame = old.gameextrainfo;
222
+ const newGame = current.gameextrainfo;
223
+ const name = bind.nickname || current.personaname;
224
+ if (newGame && !oldGame) {
225
+ msgs.push(`${name} 开始玩 ${newGame} 了`);
226
+ startGamingPlayers.push({ ...current, nickname: bind.nickname });
227
+ }
228
+ else if (!newGame && oldGame) {
229
+ msgs.push(`${name} 玩了 ${oldGame} 后不玩了`);
230
+ }
231
+ else if (newGame && oldGame && newGame !== oldGame) {
232
+ msgs.push(`${name} 停止玩 ${oldGame},开始玩 ${newGame} 了`);
233
+ }
234
+ }
235
+ if (msgs.length > 0) {
236
+ const bot = ctx.bots[0];
237
+ if (!bot)
238
+ continue;
239
+ if (ctx.config.steamBroadcastType === 'none') {
240
+ await bot.sendMessage(channel.id, msgs.join('\n'));
241
+ }
242
+ else if (ctx.config.steamBroadcastType === 'part') {
243
+ if (startGamingPlayers.length > 0) {
244
+ const images = await Promise.all(startGamingPlayers.map(p => ctx.drawer.drawStartGaming(p, p.nickname)));
245
+ const combined = await ctx.drawer.concatImages(images);
246
+ const img = combined ? (typeof combined === 'string' ? combined : koishi_1.h.image(combined, 'image/png')) : '';
247
+ await bot.sendMessage(channel.id, msgs.join('\n') + img);
248
+ }
249
+ else {
250
+ await bot.sendMessage(channel.id, msgs.join('\n'));
251
+ }
252
+ }
253
+ else if (ctx.config.steamBroadcastType === 'all') {
254
+ const channelPlayers = channelBinds.map(b => currentMap.get(b.steamId)).filter(Boolean);
255
+ const parentAvatar = channel.avatar ? Buffer.from(channel.avatar, 'base64') : await ctx.drawer.getDefaultAvatar();
256
+ const image = await ctx.drawer.drawFriendsStatus(parentAvatar, channel.name || channel.id, channelPlayers, channelBinds);
257
+ const img = image ? (typeof image === 'string' ? image : koishi_1.h.image(image, 'image/png')) : '';
258
+ await bot.sendMessage(channel.id, msgs.join('\n') + img);
259
+ }
260
+ }
261
+ }
262
+ for (const p of currentSummaries) {
263
+ statusCache.set(p.steamid, p);
264
+ }
265
+ }
@@ -0,0 +1 @@
1
+ {"invalid_id":"请输入有效的 Steam ID。","id_not_found":"无法找到该 Steam ID。","bind_success":"绑定成功!Steam ID: {0}","unbind_success":"解绑成功。","not_bound":"你还没有绑定 Steam ID。","user_not_found":"未找到用户信息。","error":"发生错误。","no_binds":"本群尚无绑定用户。","api_error":"连接 Steam API 失败。","enable_success":"已开启本群播报。","disable_success":"已关闭本群播报。","args_missing":"参数缺失。","update_success":"更新群信息成功。","nickname_set":"昵称已设置为 {0}。"}
@@ -0,0 +1,42 @@
1
+ import { Context, Service } from 'koishi';
2
+ import { Config } from './index';
3
+ export interface PlayerSummary {
4
+ steamid: string;
5
+ personaname: string;
6
+ profileurl: string;
7
+ avatar: string;
8
+ avatarmedium: string;
9
+ avatarfull: string;
10
+ personastate: number;
11
+ gameextrainfo?: string;
12
+ lastlogoff?: number;
13
+ }
14
+ export interface SteamProfile {
15
+ steamid: string;
16
+ player_name: string;
17
+ avatar: string | Buffer;
18
+ background: string | Buffer;
19
+ description: string;
20
+ recent_2_week_play_time: string;
21
+ game_data: GameData[];
22
+ }
23
+ export interface GameData {
24
+ game_name: string;
25
+ game_image: string | Buffer;
26
+ play_time: string;
27
+ last_played: string;
28
+ achievements: Achievement[];
29
+ completed_achievement_number?: number;
30
+ total_achievement_number?: number;
31
+ }
32
+ export interface Achievement {
33
+ name: string;
34
+ image: string | Buffer;
35
+ }
36
+ export declare class SteamService extends Service {
37
+ config: Config;
38
+ constructor(ctx: Context, config: Config);
39
+ getSteamId(input: string): Promise<string | null>;
40
+ getPlayerSummaries(steamIds: string[]): Promise<PlayerSummary[]>;
41
+ getUserData(steamId: string): Promise<SteamProfile>;
42
+ }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SteamService = void 0;
7
+ const koishi_1 = require("koishi");
8
+ const cheerio_1 = __importDefault(require("cheerio"));
9
+ // Constants
10
+ const STEAM_ID_OFFSET = BigInt('76561197960265728');
11
+ class SteamService extends koishi_1.Service {
12
+ constructor(ctx, config) {
13
+ super(ctx, 'steam', true);
14
+ this.config = config;
15
+ }
16
+ async getSteamId(input) {
17
+ if (!/^\d+$/.test(input))
18
+ return null;
19
+ const id = BigInt(input);
20
+ if (id < STEAM_ID_OFFSET) {
21
+ return (id + STEAM_ID_OFFSET).toString();
22
+ }
23
+ return input;
24
+ }
25
+ async getPlayerSummaries(steamIds) {
26
+ if (steamIds.length === 0)
27
+ return [];
28
+ // Split into chunks of 100
29
+ const chunks = [];
30
+ for (let i = 0; i < steamIds.length; i += 100) {
31
+ chunks.push(steamIds.slice(i, i + 100));
32
+ }
33
+ const players = [];
34
+ for (const chunk of chunks) {
35
+ // Try keys
36
+ for (const key of this.config.steamApiKey) {
37
+ try {
38
+ const url = `http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${key}&steamids=${chunk.join(',')}`;
39
+ const data = await this.ctx.http.get(url);
40
+ if (data?.response?.players) {
41
+ players.push(...data.response.players);
42
+ break; // Success for this chunk
43
+ }
44
+ }
45
+ catch (e) {
46
+ this.ctx.logger('steam').warn(`API key ${key} failed: ${e}`);
47
+ }
48
+ }
49
+ }
50
+ return players;
51
+ }
52
+ async getUserData(steamId) {
53
+ const url = `https://steamcommunity.com/profiles/${steamId}`;
54
+ // Cookie for timezone
55
+ const headers = {
56
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
57
+ 'Cookie': 'timezoneOffset=28800,0'
58
+ };
59
+ const html = await this.ctx.http.get(url, { headers });
60
+ const $ = cheerio_1.default.load(html);
61
+ const player_name = $('.actual_persona_name').text().trim() || $('title').text().replace('Steam 社区 :: ', '');
62
+ const description = $('.profile_summary').text().trim().replace(/\t/g, '');
63
+ let background = '';
64
+ const bgStyle = $('.no_header.profile_page').attr('style') || '';
65
+ const bgMatch = bgStyle.match(/background-image:\s*url\(\s*['"]?([^'" ]+)['"]?\s*\)/);
66
+ if (bgMatch)
67
+ background = bgMatch[1];
68
+ const avatar = $('.playerAvatarAutoSizeInner > img').attr('src') || '';
69
+ const recent_2_week_play_time = $('.recentgame_quicklinks.recentgame_recentplaytime > div').text().trim();
70
+ const game_data = [];
71
+ $('.recent_game').each((i, el) => {
72
+ const game_name = $(el).find('.game_name').text().trim();
73
+ const game_image = $(el).find('.game_capsule').attr('src') || '';
74
+ const details = $(el).find('.game_info_details').text();
75
+ const play_time_match = details.match(/总时数\s*([\d\.]+)\s*小时/);
76
+ const play_time = play_time_match ? play_time_match[1] : '';
77
+ const last_played_match = details.match(/最后运行日期:(.*) 日/);
78
+ const last_played = last_played_match ? `最后运行日期:${last_played_match[1]} 日` : '当前正在游戏';
79
+ const achievements = [];
80
+ $(el).find('.game_info_achievement').each((j, achEl) => {
81
+ if ($(achEl).hasClass('plus_more'))
82
+ return;
83
+ achievements.push({
84
+ name: $(achEl).attr('data-tooltip-text') || '',
85
+ image: $(achEl).find('img').attr('src') || ''
86
+ });
87
+ });
88
+ const summary = $(el).find('.game_info_achievement_summary').find('.ellipsis').text();
89
+ let completed_achievement_number = 0;
90
+ let total_achievement_number = 0;
91
+ if (summary) {
92
+ const parts = summary.split('/');
93
+ if (parts.length === 2) {
94
+ completed_achievement_number = parseInt(parts[0]);
95
+ total_achievement_number = parseInt(parts[1]);
96
+ }
97
+ }
98
+ game_data.push({
99
+ game_name,
100
+ game_image,
101
+ play_time,
102
+ last_played,
103
+ achievements,
104
+ completed_achievement_number,
105
+ total_achievement_number
106
+ });
107
+ });
108
+ return {
109
+ steamid: steamId,
110
+ player_name,
111
+ avatar,
112
+ background,
113
+ description,
114
+ recent_2_week_play_time,
115
+ game_data
116
+ };
117
+ }
118
+ }
119
+ exports.SteamService = SteamService;
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "koishi-plugin-steam-info-check",
3
+ "version": "1.0.0",
4
+ "description": "Steam friends status broadcast plugin for Koishi",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "koishi start"
10
+ },
11
+ "keywords": [
12
+ "koishi",
13
+ "plugin",
14
+ "steam",
15
+ "game",
16
+ "bot",
17
+ "chatbot"
18
+ ],
19
+ "author": "MuxYang <tlnkmc.b@gmail.com>",
20
+ "license": "MIT",
21
+ "homepage": "https://github.com/MuxYang/koishi-plugin-steam-info",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/MuxYang/koishi-plugin-steam-info.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/MuxYang/koishi-plugin-steam-info/issues"
28
+ },
29
+ "koishi": {
30
+ "description": {
31
+ "en": "Steam friends status broadcast plugin for Koishi",
32
+ "zh-CN": "Koishi 的 Steam 好友状态播报插件"
33
+ },
34
+ "service": {
35
+ "required": [
36
+ "model",
37
+ "http",
38
+ "puppeteer",
39
+ "database"
40
+ ]
41
+ }
42
+ },
43
+ "peerDependencies": {
44
+ "koishi": "^4.16.0",
45
+ "koishi-plugin-puppeteer": "*"
46
+ },
47
+ "dependencies": {
48
+ "cheerio": "^1.0.0-rc.12",
49
+ "luxon": "^3.4.4"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^20.0.0",
53
+ "typescript": "^5.0.0",
54
+ "koishi": "^4.16.0"
55
+ }
56
+ }
package/readme.md ADDED
@@ -0,0 +1,5 @@
1
+ # koishi-plugin-steam-info-check
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-steam-info-check?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-steam-info-check)
4
+
5
+ Steam 好友状态播报 koishi 插件 (基于nonebot版本修改,https://github.com/zhaomaoniu/nonebot-plugin-steam-info)
@@ -0,0 +1,21 @@
1
+ export interface SteamBind {
2
+ id: number
3
+ userId: string
4
+ channelId: string
5
+ steamId: string
6
+ nickname?: string
7
+ }
8
+
9
+ export interface SteamChannel {
10
+ id: string
11
+ enable: boolean
12
+ name?: string
13
+ avatar?: string
14
+ }
15
+
16
+ declare module 'koishi' {
17
+ interface Tables {
18
+ steam_bind: SteamBind
19
+ steam_channel: SteamChannel
20
+ }
21
+ }