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/src/drawer.ts ADDED
@@ -0,0 +1,505 @@
1
+ import { Context, Service, h } from 'koishi'
2
+ import { Config } from './index'
3
+ import { SteamBind } from './database'
4
+ import { PlayerSummary, SteamProfile } from './service'
5
+ import { resolve } from 'path'
6
+ import { readFileSync } from 'fs'
7
+
8
+ declare module 'koishi' {
9
+ interface Context {
10
+ puppeteer: any
11
+ }
12
+ }
13
+
14
+ export class DrawService extends Service {
15
+ constructor(ctx: Context, public config: Config) {
16
+ super(ctx, 'drawer', true)
17
+ }
18
+
19
+ private getFontCss(): string {
20
+ // We can try to load fonts as base64 or just reference them if we assume the browser environment can see them.
21
+ // For simplicity and robustness, passing them as base64 in CSS @font-face is good but heavy.
22
+ // If we assume standard fonts or just let the browser handle it, it's easier.
23
+ // However, the original plugin used MiSans.
24
+ // Let's try to load them if possible, or fallback to sans-serif.
25
+ // Since we are in Node, we can read the files.
26
+
27
+ // Note: Puppeteer page.setContent usually runs in a context where local file access might be restricted
28
+ // unless we use `file://` protocol or base64. Base64 is safest.
29
+
30
+ let css = `
31
+ body { font-family: 'MiSans', sans-serif; margin: 0; padding: 0; background-color: #1e2024; color: #fff; }
32
+ `
33
+
34
+ // Helper to load font
35
+ const loadFont = (name: string, path: string, weight: string) => {
36
+ try {
37
+ const fullPath = resolve(this.ctx.baseDir, path)
38
+ const buffer = readFileSync(fullPath)
39
+ const base64 = buffer.toString('base64')
40
+ css += `
41
+ @font-face {
42
+ font-family: '${name}';
43
+ src: url(data:font/ttf;base64,${base64}) format('truetype');
44
+ font-weight: ${weight};
45
+ font-style: normal;
46
+ }
47
+ `
48
+ } catch (e) {
49
+ // Font not found, ignore
50
+ }
51
+ }
52
+
53
+ loadFont('MiSans', this.config.fonts.regular, 'normal')
54
+ loadFont('MiSans', this.config.fonts.light, '300')
55
+ loadFont('MiSans', this.config.fonts.bold, 'bold')
56
+
57
+ return css
58
+ }
59
+
60
+ private imageToBase64(img: string | Buffer): string {
61
+ if (Buffer.isBuffer(img)) {
62
+ return `data:image/png;base64,${img.toString('base64')}`
63
+ }
64
+ return img // URL
65
+ }
66
+
67
+ async drawStartGaming(player: PlayerSummary, nickname?: string): Promise<Buffer | string> {
68
+ const avatarUrl = player.avatarfull
69
+ const name = nickname || player.personaname
70
+ const game = player.gameextrainfo || 'Unknown Game'
71
+
72
+ const html = `
73
+ <html>
74
+ <head>
75
+ <style>
76
+ ${this.getFontCss()}
77
+ .container {
78
+ width: 400px;
79
+ height: 100px;
80
+ display: flex;
81
+ align-items: center;
82
+ background-color: #1e2024;
83
+ padding: 15px;
84
+ box-sizing: border-box;
85
+ }
86
+ .avatar {
87
+ width: 66px;
88
+ height: 66px;
89
+ margin-right: 20px;
90
+ border-radius: 4px;
91
+ }
92
+ .info {
93
+ display: flex;
94
+ flex-direction: column;
95
+ justify-content: center;
96
+ }
97
+ .name {
98
+ font-size: 19px;
99
+ color: #e3ffc2;
100
+ margin-bottom: 4px;
101
+ }
102
+ .status {
103
+ font-size: 17px;
104
+ color: #969696;
105
+ margin-bottom: 4px;
106
+ }
107
+ .game {
108
+ font-size: 14px;
109
+ font-weight: bold;
110
+ color: #91c257;
111
+ }
112
+ </style>
113
+ </head>
114
+ <body>
115
+ <div class="container">
116
+ <img class="avatar" src="${avatarUrl}" />
117
+ <div class="info">
118
+ <div class="name">${name}</div>
119
+ <div class="status">正在玩</div>
120
+ <div class="game">${game}</div>
121
+ </div>
122
+ </div>
123
+ </body>
124
+ </html>
125
+ `
126
+
127
+ const page = await this.ctx.puppeteer.page()
128
+ await page.setContent(html)
129
+ const element = await page.$('.container')
130
+ const buffer = await element.screenshot({ type: 'png' })
131
+ await page.close()
132
+ return buffer
133
+ }
134
+
135
+ async drawFriendsStatus(parentAvatar: Buffer | string, parentName: string, players: PlayerSummary[], binds: SteamBind[]): Promise<Buffer | string> {
136
+ const sorted = [...players].sort((a, b) => {
137
+ const stateA = this.getPlayerStateOrder(a)
138
+ const stateB = this.getPlayerStateOrder(b)
139
+ return stateA - stateB
140
+ })
141
+
142
+ const groups = [
143
+ { title: '游戏中', items: sorted.filter(p => p.gameextrainfo) },
144
+ { title: '在线好友', items: sorted.filter(p => !p.gameextrainfo && p.personastate !== 0) },
145
+ { title: '离线', items: sorted.filter(p => p.personastate === 0) }
146
+ ].filter(g => g.items.length > 0)
147
+
148
+ const parentAvatarSrc = this.imageToBase64(parentAvatar)
149
+
150
+ let listHtml = ''
151
+ for (const group of groups) {
152
+ listHtml += `<div class="group-title">${group.title} (${group.items.length})</div>`
153
+ for (const player of group.items) {
154
+ const bind = binds.find(b => b.steamId === player.steamid)
155
+ const name = bind?.nickname || player.personaname
156
+ const avatar = player.avatarmedium || player.avatar
157
+
158
+ let statusText = '离线'
159
+ let nameColor = '#656565'
160
+ let statusColor = '#656565'
161
+
162
+ if (player.gameextrainfo) {
163
+ statusText = player.gameextrainfo
164
+ nameColor = '#91c257'
165
+ statusColor = '#91c257'
166
+ } else if (player.personastate !== 0) {
167
+ statusText = this.getPersonaStateText(player.personastate)
168
+ nameColor = '#6dcff6'
169
+ statusColor = '#6dcff6' // Usually blue for online
170
+ }
171
+
172
+ listHtml += `
173
+ <div class="friend-item">
174
+ <img class="friend-avatar" src="${avatar}" />
175
+ <div class="friend-info">
176
+ <div class="friend-name" style="color: ${nameColor}">${name}</div>
177
+ <div class="friend-status" style="color: ${statusColor}">${statusText}</div>
178
+ </div>
179
+ </div>
180
+ `
181
+ }
182
+ }
183
+
184
+ const html = `
185
+ <html>
186
+ <head>
187
+ <style>
188
+ ${this.getFontCss()}
189
+ body {
190
+ width: 400px;
191
+ background-color: #1e2024;
192
+ }
193
+ .header {
194
+ padding: 16px;
195
+ display: flex;
196
+ align-items: center;
197
+ height: 120px;
198
+ box-sizing: border-box;
199
+ background: linear-gradient(to bottom, #2b2e34 0%, #1e2024 100%);
200
+ }
201
+ .parent-avatar {
202
+ width: 72px;
203
+ height: 72px;
204
+ border-radius: 4px;
205
+ margin-right: 16px;
206
+ }
207
+ .parent-info {
208
+ display: flex;
209
+ flex-direction: column;
210
+ }
211
+ .parent-name {
212
+ font-size: 20px;
213
+ font-weight: bold;
214
+ color: #6dcff6;
215
+ margin-bottom: 4px;
216
+ }
217
+ .parent-status {
218
+ font-size: 18px;
219
+ color: #4c91ac;
220
+ }
221
+ .search-bar {
222
+ height: 50px;
223
+ background-color: #434953;
224
+ display: flex;
225
+ align-items: center;
226
+ padding-left: 24px;
227
+ color: #b7ccd5;
228
+ font-size: 20px;
229
+ }
230
+ .list-container {
231
+ padding: 16px 0;
232
+ }
233
+ .group-title {
234
+ color: #c5d6d4;
235
+ font-size: 22px;
236
+ margin: 10px 22px;
237
+ }
238
+ .friend-item {
239
+ display: flex;
240
+ align-items: center;
241
+ height: 64px;
242
+ padding: 0 22px;
243
+ }
244
+ .friend-item:hover {
245
+ background-color: #3d4450;
246
+ }
247
+ .friend-avatar {
248
+ width: 50px;
249
+ height: 50px;
250
+ border-radius: 4px;
251
+ margin-right: 16px;
252
+ }
253
+ .friend-info {
254
+ display: flex;
255
+ flex-direction: column;
256
+ }
257
+ .friend-name {
258
+ font-size: 18px;
259
+ font-weight: bold;
260
+ margin-bottom: 4px;
261
+ }
262
+ .friend-status {
263
+ font-size: 16px;
264
+ }
265
+ </style>
266
+ </head>
267
+ <body>
268
+ <div class="main">
269
+ <div class="header">
270
+ <img class="parent-avatar" src="${parentAvatarSrc}" />
271
+ <div class="parent-info">
272
+ <div class="parent-name">${parentName}</div>
273
+ <div class="parent-status">在线</div>
274
+ </div>
275
+ </div>
276
+ <div class="search-bar">好友</div>
277
+ <div class="list-container">
278
+ ${listHtml}
279
+ </div>
280
+ </div>
281
+ </body>
282
+ </html>
283
+ `
284
+
285
+ const page = await this.ctx.puppeteer.page()
286
+ await page.setContent(html)
287
+ // We need to capture the full height.
288
+ // We can select 'body' or '.main'.
289
+ const element = await page.$('body')
290
+ const buffer = await element.screenshot({ type: 'png' })
291
+ await page.close()
292
+ return buffer
293
+ }
294
+
295
+ async drawPlayerStatus(profile: SteamProfile, steamId: string): Promise<Buffer | string> {
296
+ const bgSrc = this.imageToBase64(profile.background)
297
+ const avatarSrc = this.imageToBase64(profile.avatar)
298
+
299
+ let gamesHtml = ''
300
+ for (const game of profile.game_data) {
301
+ const gameImg = this.imageToBase64(game.game_image)
302
+ gamesHtml += `
303
+ <div class="game-row">
304
+ <img class="game-img" src="${gameImg}" />
305
+ <div class="game-info">
306
+ <div class="game-name">${game.game_name}</div>
307
+ <div class="game-stats">
308
+ <span class="play-time">${game.play_time ? `总时数 ${game.play_time} 小时` : ''}</span>
309
+ <span class="last-played">${game.last_played}</span>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ `
314
+ }
315
+
316
+ const html = `
317
+ <html>
318
+ <head>
319
+ <style>
320
+ ${this.getFontCss()}
321
+ body {
322
+ width: 960px;
323
+ background-color: #1b2838;
324
+ position: relative;
325
+ }
326
+ .bg-container {
327
+ position: fixed;
328
+ top: 0;
329
+ left: 0;
330
+ width: 100%;
331
+ height: 100%;
332
+ z-index: -1;
333
+ background-image: url('${bgSrc}');
334
+ background-size: cover;
335
+ background-position: center;
336
+ }
337
+ .bg-overlay {
338
+ position: fixed;
339
+ top: 0;
340
+ left: 0;
341
+ width: 100%;
342
+ height: 100%;
343
+ z-index: -1;
344
+ background-color: rgba(0, 0, 0, 0.7);
345
+ }
346
+ .content {
347
+ padding: 40px;
348
+ z-index: 1;
349
+ }
350
+ .header {
351
+ display: flex;
352
+ align-items: flex-start;
353
+ margin-bottom: 40px;
354
+ }
355
+ .profile-avatar {
356
+ width: 200px;
357
+ height: 200px;
358
+ border: 3px solid #53a4c4;
359
+ margin-right: 40px;
360
+ }
361
+ .profile-info {
362
+ flex: 1;
363
+ }
364
+ .profile-name {
365
+ font-size: 40px;
366
+ color: white;
367
+ margin-bottom: 10px;
368
+ }
369
+ .profile-id {
370
+ font-size: 20px;
371
+ color: #bfbfbf;
372
+ margin-bottom: 20px;
373
+ }
374
+ .profile-desc {
375
+ font-size: 22px;
376
+ color: #bfbfbf;
377
+ white-space: pre-wrap;
378
+ max-width: 640px;
379
+ }
380
+ .games-section {
381
+ margin-top: 20px;
382
+ }
383
+ .recent-header {
384
+ background-color: rgba(0, 0, 0, 0.5);
385
+ padding: 10px 20px;
386
+ display: flex;
387
+ justify-content: space-between;
388
+ color: white;
389
+ font-size: 26px;
390
+ margin-bottom: 10px;
391
+ }
392
+ .game-row {
393
+ background-color: rgba(0, 0, 0, 0.3);
394
+ height: 100px;
395
+ display: flex;
396
+ align-items: center;
397
+ padding: 10px 20px;
398
+ margin-bottom: 10px;
399
+ }
400
+ .game-img {
401
+ width: 184px; /* Standard capsule width */
402
+ height: 69px;
403
+ margin-right: 20px;
404
+ }
405
+ .game-info {
406
+ flex: 1;
407
+ display: flex;
408
+ flex-direction: column;
409
+ justify-content: center;
410
+ }
411
+ .game-name {
412
+ font-size: 26px;
413
+ color: white;
414
+ margin-bottom: 8px;
415
+ }
416
+ .game-stats {
417
+ font-size: 20px;
418
+ color: #969696;
419
+ display: flex;
420
+ gap: 20px;
421
+ }
422
+ </style>
423
+ </head>
424
+ <body>
425
+ <div class="bg-container"></div>
426
+ <div class="bg-overlay"></div>
427
+ <div class="content">
428
+ <div class="header">
429
+ <img class="profile-avatar" src="${avatarSrc}" />
430
+ <div class="profile-info">
431
+ <div class="profile-name">${profile.player_name}</div>
432
+ <div class="profile-id">ID: ${steamId}</div>
433
+ <div class="profile-desc">${profile.description}</div>
434
+ </div>
435
+ </div>
436
+ <div class="games-section">
437
+ <div class="recent-header">
438
+ <span>最近游戏</span>
439
+ <span>${profile.recent_2_week_play_time || ''}</span>
440
+ </div>
441
+ ${gamesHtml}
442
+ </div>
443
+ </div>
444
+ </body>
445
+ </html>
446
+ `
447
+
448
+ const page = await this.ctx.puppeteer.page()
449
+ await page.setContent(html)
450
+ const element = await page.$('body')
451
+ const buffer = await element.screenshot({ type: 'png' })
452
+ await page.close()
453
+ return buffer
454
+ }
455
+
456
+ async concatImages(images: (Buffer | string)[]): Promise<Buffer | string | null> {
457
+ if (images.length === 0) return null
458
+ if (images.length === 1) return images[0]
459
+
460
+ // We can use a simple HTML page to stack images vertically
461
+ let imgsHtml = ''
462
+ for (const img of images) {
463
+ const src = this.imageToBase64(img)
464
+ imgsHtml += `<img src="${src}" style="display: block;" />`
465
+ }
466
+
467
+ const html = `
468
+ <html>
469
+ <body style="margin:0; padding:0; display: flex; flex-direction: column; width: fit-content;">
470
+ ${imgsHtml}
471
+ </body>
472
+ </html>
473
+ `
474
+ const page = await this.ctx.puppeteer.page()
475
+ await page.setContent(html)
476
+ const element = await page.$('body')
477
+ const buffer = await element.screenshot({ type: 'png', omitBackground: true })
478
+ await page.close()
479
+ return buffer
480
+ }
481
+
482
+ async getDefaultAvatar(): Promise<Buffer | string> {
483
+ // Just a placeholder color
484
+ const html = `
485
+ <html><body style="margin:0;padding:0;"><div style="width:100px;height:100px;background-color:#ccc;"></div></body></html>
486
+ `
487
+ const page = await this.ctx.puppeteer.page()
488
+ await page.setContent(html)
489
+ const element = await page.$('div')
490
+ const buffer = await element.screenshot({ type: 'png' })
491
+ await page.close()
492
+ return buffer
493
+ }
494
+
495
+ private getPlayerStateOrder(p: PlayerSummary) {
496
+ if (p.gameextrainfo) return 0
497
+ if (p.personastate !== 0) return 1
498
+ return 2
499
+ }
500
+
501
+ private getPersonaStateText(state: number) {
502
+ const map = ['离线', '在线', '忙碌', '离开', '打盹', '寻求交易', '寻求游戏']
503
+ return map[state] || '未知'
504
+ }
505
+ }