agent-messenger 2.37.2 → 2.38.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/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -0
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/cli.d.ts.map +1 -1
- package/dist/src/platforms/discord/cli.js +3 -1
- package/dist/src/platforms/discord/cli.js.map +1 -1
- package/dist/src/platforms/discord/client.d.ts +17 -1
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +46 -0
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/discord/commands/emoji.d.ts +13 -0
- package/dist/src/platforms/discord/commands/emoji.d.ts.map +1 -0
- package/dist/src/platforms/discord/commands/emoji.js +93 -0
- package/dist/src/platforms/discord/commands/emoji.js.map +1 -0
- package/dist/src/platforms/discord/commands/index.d.ts +2 -0
- package/dist/src/platforms/discord/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/index.js +2 -0
- package/dist/src/platforms/discord/commands/index.js.map +1 -1
- package/dist/src/platforms/discord/commands/server.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/server.js +16 -1
- package/dist/src/platforms/discord/commands/server.js.map +1 -1
- package/dist/src/platforms/discord/commands/sticker.d.ts +15 -0
- package/dist/src/platforms/discord/commands/sticker.d.ts.map +1 -0
- package/dist/src/platforms/discord/commands/sticker.js +97 -0
- package/dist/src/platforms/discord/commands/sticker.js.map +1 -0
- package/dist/src/platforms/discord/expression-format.d.ts +41 -0
- package/dist/src/platforms/discord/expression-format.d.ts.map +1 -0
- package/dist/src/platforms/discord/expression-format.js +107 -0
- package/dist/src/platforms/discord/expression-format.js.map +1 -0
- package/dist/src/platforms/discord/expression-names.d.ts +9 -0
- package/dist/src/platforms/discord/expression-names.d.ts.map +1 -0
- package/dist/src/platforms/discord/expression-names.js +33 -0
- package/dist/src/platforms/discord/expression-names.js.map +1 -0
- package/dist/src/platforms/discord/index.d.ts +1 -1
- package/dist/src/platforms/discord/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/index.js.map +1 -1
- package/dist/src/platforms/discord/types.d.ts +29 -0
- package/dist/src/platforms/discord/types.d.ts.map +1 -1
- package/dist/src/platforms/discord/types.js.map +1 -1
- package/docs/content/docs/cli/discord.mdx +73 -0
- package/docs/content/docs/sdk/discord.mdx +50 -0
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +69 -1
- package/skills/agent-discord/references/common-patterns.md +77 -0
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/cli.ts +4 -0
- package/src/platforms/discord/client.test.ts +133 -0
- package/src/platforms/discord/client.ts +72 -0
- package/src/platforms/discord/commands/emoji.test.ts +122 -0
- package/src/platforms/discord/commands/emoji.ts +124 -0
- package/src/platforms/discord/commands/index.ts +2 -0
- package/src/platforms/discord/commands/server.test.ts +45 -1
- package/src/platforms/discord/commands/server.ts +21 -1
- package/src/platforms/discord/commands/sticker.test.ts +118 -0
- package/src/platforms/discord/commands/sticker.ts +135 -0
- package/src/platforms/discord/expression-format.test.ts +107 -0
- package/src/platforms/discord/expression-format.ts +117 -0
- package/src/platforms/discord/expression-names.test.ts +41 -0
- package/src/platforms/discord/expression-names.ts +36 -0
- package/src/platforms/discord/index.ts +2 -0
- package/src/platforms/discord/types.ts +31 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { DiscordClient } from '../client'
|
|
7
|
+
import { DiscordCredentialManager } from '../credential-manager'
|
|
8
|
+
import { createAction, deleteAction, listAction } from './emoji'
|
|
9
|
+
|
|
10
|
+
let clientListEmojisSpy: ReturnType<typeof spyOn>
|
|
11
|
+
let clientCreateEmojiSpy: ReturnType<typeof spyOn>
|
|
12
|
+
let clientDeleteEmojiSpy: ReturnType<typeof spyOn>
|
|
13
|
+
let credManagerLoadSpy: ReturnType<typeof spyOn>
|
|
14
|
+
let tempDir: string
|
|
15
|
+
const originalLog = console.log
|
|
16
|
+
const originalExit = process.exit
|
|
17
|
+
|
|
18
|
+
function pngBytes(width: number, height: number, padding = 0): Uint8Array {
|
|
19
|
+
const bytes = new Uint8Array(24 + padding)
|
|
20
|
+
bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0)
|
|
21
|
+
bytes.set([0x00, 0x00, 0x00, 0x0d], 8)
|
|
22
|
+
bytes.set([0x49, 0x48, 0x44, 0x52], 12)
|
|
23
|
+
const view = new DataView(bytes.buffer)
|
|
24
|
+
view.setUint32(16, width)
|
|
25
|
+
view.setUint32(20, height)
|
|
26
|
+
return bytes
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function writeTempImage(filename: string, bytes: Uint8Array): Promise<string> {
|
|
30
|
+
const path = join(tempDir, filename)
|
|
31
|
+
await writeFile(path, bytes)
|
|
32
|
+
return path
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
tempDir = await mkdtemp(join(tmpdir(), 'discord-emoji-'))
|
|
37
|
+
|
|
38
|
+
clientListEmojisSpy = spyOn(DiscordClient.prototype, 'listEmojis').mockResolvedValue([
|
|
39
|
+
{ id: 'e1', name: 'potato_01', animated: false },
|
|
40
|
+
{ id: 'e2', name: 'potato_02', animated: false },
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
clientCreateEmojiSpy = spyOn(DiscordClient.prototype, 'createEmoji').mockResolvedValue({
|
|
44
|
+
id: 'e9',
|
|
45
|
+
name: 'potato_13',
|
|
46
|
+
animated: false,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
clientDeleteEmojiSpy = spyOn(DiscordClient.prototype, 'deleteEmoji').mockResolvedValue(undefined)
|
|
50
|
+
|
|
51
|
+
credManagerLoadSpy = spyOn(DiscordCredentialManager.prototype, 'load').mockResolvedValue({
|
|
52
|
+
token: 'test-token',
|
|
53
|
+
current_server: 'server-1',
|
|
54
|
+
servers: {},
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterEach(async () => {
|
|
59
|
+
console.log = originalLog
|
|
60
|
+
process.exit = originalExit
|
|
61
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
62
|
+
clientListEmojisSpy?.mockRestore()
|
|
63
|
+
clientCreateEmojiSpy?.mockRestore()
|
|
64
|
+
clientDeleteEmojiSpy?.mockRestore()
|
|
65
|
+
credManagerLoadSpy?.mockRestore()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('list: reports the guild emoji with static and animated counts', async () => {
|
|
69
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
70
|
+
console.log = consoleSpy
|
|
71
|
+
|
|
72
|
+
await listAction('g1', { pretty: false })
|
|
73
|
+
|
|
74
|
+
const output = consoleSpy.mock.calls[0][0]
|
|
75
|
+
expect(output).toContain('potato_01')
|
|
76
|
+
expect(output).toContain('"static_count":2')
|
|
77
|
+
expect(output).toContain('"animated_count":0')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('create: uploads the image with its content type and derives the name from the filename', async () => {
|
|
81
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
82
|
+
console.log = consoleSpy
|
|
83
|
+
const path = await writeTempImage('potato_13.png', pngBytes(128, 128))
|
|
84
|
+
|
|
85
|
+
await createAction('g1', path, { pretty: false })
|
|
86
|
+
|
|
87
|
+
expect(clientCreateEmojiSpy).toHaveBeenCalledWith('g1', 'potato_13', expect.any(Uint8Array), 'potato_13.png')
|
|
88
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('potato_13')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('create: prefers an explicit --name over the filename', async () => {
|
|
92
|
+
console.log = mock((_msg: string) => {})
|
|
93
|
+
const path = await writeTempImage('potato_13.png', pngBytes(128, 128))
|
|
94
|
+
|
|
95
|
+
await createAction('g1', path, { name: 'spud', pretty: false })
|
|
96
|
+
|
|
97
|
+
expect(clientCreateEmojiSpy.mock.calls[0][1]).toBe('spud')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('delete: removes the emoji by id', async () => {
|
|
101
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
102
|
+
console.log = consoleSpy
|
|
103
|
+
|
|
104
|
+
await deleteAction('g1', 'e1', { pretty: false })
|
|
105
|
+
|
|
106
|
+
expect(clientDeleteEmojiSpy).toHaveBeenCalledWith('g1', 'e1')
|
|
107
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('"success":true')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('create: rejects a hyphenated name without calling the API', async () => {
|
|
111
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
112
|
+
console.log = consoleSpy
|
|
113
|
+
spyOn(process, 'exit').mockImplementation(() => {
|
|
114
|
+
throw new Error('exit')
|
|
115
|
+
})
|
|
116
|
+
const path = await writeTempImage('potato-13.png', pngBytes(128, 128))
|
|
117
|
+
|
|
118
|
+
await expect(createAction('g1', path, { pretty: false })).rejects.toThrow('exit')
|
|
119
|
+
|
|
120
|
+
expect(clientCreateEmojiSpy).not.toHaveBeenCalled()
|
|
121
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('letters, digits, underscores')
|
|
122
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, extname, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander'
|
|
5
|
+
|
|
6
|
+
import { handleError } from '@/shared/utils/error-handler'
|
|
7
|
+
import { formatOutput } from '@/shared/utils/output'
|
|
8
|
+
|
|
9
|
+
import { DiscordClient } from '../client'
|
|
10
|
+
import { DiscordCredentialManager } from '../credential-manager'
|
|
11
|
+
import { validateEmojiName } from '../expression-names'
|
|
12
|
+
|
|
13
|
+
export async function listAction(serverId: string, options: { pretty?: boolean }): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
const credManager = new DiscordCredentialManager()
|
|
16
|
+
const config = await credManager.load()
|
|
17
|
+
|
|
18
|
+
if (!config.token) {
|
|
19
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const client = await new DiscordClient().login({ token: config.token })
|
|
24
|
+
const emojis = await client.listEmojis(serverId)
|
|
25
|
+
const staticCount = emojis.filter((emoji) => !emoji.animated).length
|
|
26
|
+
|
|
27
|
+
console.log(
|
|
28
|
+
formatOutput(
|
|
29
|
+
{
|
|
30
|
+
server_id: serverId,
|
|
31
|
+
count: emojis.length,
|
|
32
|
+
static_count: staticCount,
|
|
33
|
+
animated_count: emojis.length - staticCount,
|
|
34
|
+
emojis: emojis.map((emoji) => ({ id: emoji.id, name: emoji.name, animated: emoji.animated ?? false })),
|
|
35
|
+
},
|
|
36
|
+
options.pretty,
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
} catch (error) {
|
|
40
|
+
handleError(error as Error)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function createAction(
|
|
45
|
+
serverId: string,
|
|
46
|
+
path: string,
|
|
47
|
+
options: { name?: string; pretty?: boolean },
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
const credManager = new DiscordCredentialManager()
|
|
51
|
+
const config = await credManager.load()
|
|
52
|
+
|
|
53
|
+
if (!config.token) {
|
|
54
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const client = await new DiscordClient().login({ token: config.token })
|
|
59
|
+
|
|
60
|
+
const filePath = resolve(path)
|
|
61
|
+
const filename = basename(filePath)
|
|
62
|
+
const name = options.name ?? basename(filename, extname(filename))
|
|
63
|
+
|
|
64
|
+
const nameError = validateEmojiName(name)
|
|
65
|
+
if (nameError) {
|
|
66
|
+
console.log(formatOutput({ error: nameError }, options.pretty))
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const image = new Uint8Array(await readFile(filePath))
|
|
71
|
+
|
|
72
|
+
const emoji = await client.createEmoji(serverId, name, image, filename)
|
|
73
|
+
|
|
74
|
+
console.log(formatOutput({ success: true, server_id: serverId, id: emoji.id, name: emoji.name }, options.pretty))
|
|
75
|
+
} catch (error) {
|
|
76
|
+
handleError(error as Error)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function deleteAction(serverId: string, emojiId: string, options: { pretty?: boolean }): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
const credManager = new DiscordCredentialManager()
|
|
83
|
+
const config = await credManager.load()
|
|
84
|
+
|
|
85
|
+
if (!config.token) {
|
|
86
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const client = await new DiscordClient().login({ token: config.token })
|
|
91
|
+
await client.deleteEmoji(serverId, emojiId)
|
|
92
|
+
|
|
93
|
+
console.log(formatOutput({ success: true, server_id: serverId, id: emojiId }, options.pretty))
|
|
94
|
+
} catch (error) {
|
|
95
|
+
handleError(error as Error)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const emojiCommand = new Command('emoji')
|
|
100
|
+
.description('Custom emoji commands')
|
|
101
|
+
.addCommand(
|
|
102
|
+
new Command('list')
|
|
103
|
+
.description('List custom emoji in a server')
|
|
104
|
+
.argument('<server-id>', 'Server ID')
|
|
105
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
106
|
+
.action(listAction),
|
|
107
|
+
)
|
|
108
|
+
.addCommand(
|
|
109
|
+
new Command('create')
|
|
110
|
+
.description('Upload a custom emoji to a server')
|
|
111
|
+
.argument('<server-id>', 'Server ID')
|
|
112
|
+
.argument('<file>', 'Image file (PNG, JPEG, GIF or WebP, max 256KB)')
|
|
113
|
+
.option('--name <name>', 'Emoji name (defaults to the filename without extension)')
|
|
114
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
115
|
+
.action(createAction),
|
|
116
|
+
)
|
|
117
|
+
.addCommand(
|
|
118
|
+
new Command('delete')
|
|
119
|
+
.description('Delete a custom emoji from a server')
|
|
120
|
+
.argument('<server-id>', 'Server ID')
|
|
121
|
+
.argument('<emoji-id>', 'Emoji ID')
|
|
122
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
123
|
+
.action(deleteAction),
|
|
124
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { authCommand } from './auth'
|
|
2
2
|
export { channelCommand } from './channel'
|
|
3
3
|
export { dmCommand } from './dm'
|
|
4
|
+
export { emojiCommand } from './emoji'
|
|
4
5
|
export { fileCommand } from './file'
|
|
5
6
|
export { friendCommand } from './friend'
|
|
6
7
|
export { memberCommand } from './member'
|
|
@@ -11,6 +12,7 @@ export { profileCommand } from './profile'
|
|
|
11
12
|
export { reactionCommand } from './reaction'
|
|
12
13
|
export { serverCommand } from './server'
|
|
13
14
|
export { snapshotCommand } from './snapshot'
|
|
15
|
+
export { stickerCommand } from './sticker'
|
|
14
16
|
export { threadCommand } from './thread'
|
|
15
17
|
export { userCommand } from './user'
|
|
16
18
|
export { whoamiCommand } from './whoami'
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
|
|
2
2
|
|
|
3
3
|
import { DiscordClient } from '../client'
|
|
4
4
|
import { DiscordCredentialManager } from '../credential-manager'
|
|
5
|
+
import { infoAction } from './server'
|
|
5
6
|
|
|
6
7
|
let clientListServersSpy: ReturnType<typeof spyOn>
|
|
7
8
|
let clientGetServerSpy: ReturnType<typeof spyOn>
|
|
8
9
|
let credManagerLoadSpy: ReturnType<typeof spyOn>
|
|
10
|
+
const originalLog = console.log
|
|
9
11
|
let credManagerSetCurrentServerSpy: ReturnType<typeof spyOn>
|
|
10
12
|
let credManagerGetCurrentServerSpy: ReturnType<typeof spyOn>
|
|
11
13
|
|
|
@@ -46,6 +48,7 @@ beforeEach(() => {
|
|
|
46
48
|
})
|
|
47
49
|
|
|
48
50
|
afterEach(() => {
|
|
51
|
+
console.log = originalLog
|
|
49
52
|
clientListServersSpy?.mockRestore()
|
|
50
53
|
clientGetServerSpy?.mockRestore()
|
|
51
54
|
credManagerLoadSpy?.mockRestore()
|
|
@@ -132,3 +135,44 @@ it('current: returns current server info', async () => {
|
|
|
132
135
|
expect(current).toBe('server-1')
|
|
133
136
|
expect(config.current_server).toBe('server-1')
|
|
134
137
|
})
|
|
138
|
+
|
|
139
|
+
it('info: reports the boost tier, expression counts and remaining sticker slots', async () => {
|
|
140
|
+
clientGetServerSpy.mockResolvedValue({
|
|
141
|
+
id: 'server-1',
|
|
142
|
+
name: 'Server One',
|
|
143
|
+
icon: 'icon1',
|
|
144
|
+
owner_id: 'user-9',
|
|
145
|
+
premium_tier: 2,
|
|
146
|
+
premium_subscription_count: 8,
|
|
147
|
+
emojis: [{ id: 'e1', name: 'potato_01', animated: false }],
|
|
148
|
+
stickers: [{ id: 's1', name: 'potato_01', tags: 'potato', type: 2, format_type: 1 }],
|
|
149
|
+
})
|
|
150
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
151
|
+
console.log = consoleSpy
|
|
152
|
+
|
|
153
|
+
await infoAction('server-1', { pretty: false })
|
|
154
|
+
|
|
155
|
+
const output = consoleSpy.mock.calls[0][0]
|
|
156
|
+
expect(output).toContain('"owner_id":"user-9"')
|
|
157
|
+
expect(output).toContain('"premium_tier":2')
|
|
158
|
+
expect(output).toContain('"emoji_count":1')
|
|
159
|
+
expect(output).toContain('"sticker_count":1')
|
|
160
|
+
expect(output).toContain('"sticker_slots":30')
|
|
161
|
+
expect(output).toContain('"static_emoji_slots_remaining":149')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('info: falls back to the base sticker allowance when the server has no boosts', async () => {
|
|
165
|
+
clientGetServerSpy.mockResolvedValue({
|
|
166
|
+
id: 'server-1',
|
|
167
|
+
name: 'Server One',
|
|
168
|
+
premium_tier: 0,
|
|
169
|
+
emojis: [],
|
|
170
|
+
stickers: [],
|
|
171
|
+
})
|
|
172
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
173
|
+
console.log = consoleSpy
|
|
174
|
+
|
|
175
|
+
await infoAction('server-1', { pretty: false })
|
|
176
|
+
|
|
177
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('"sticker_slots":5')
|
|
178
|
+
})
|
|
@@ -6,6 +6,10 @@ import { formatOutput } from '@/shared/utils/output'
|
|
|
6
6
|
import { DiscordClient } from '../client'
|
|
7
7
|
import { DiscordCredentialManager } from '../credential-manager'
|
|
8
8
|
|
|
9
|
+
/** Discord raises both allowances with the server's boost level. */
|
|
10
|
+
const STATIC_EMOJI_SLOTS_BY_TIER = [50, 100, 150, 250]
|
|
11
|
+
const STICKER_SLOTS_BY_TIER = [5, 15, 30, 60]
|
|
12
|
+
|
|
9
13
|
export async function listAction(options: { pretty?: boolean }): Promise<void> {
|
|
10
14
|
try {
|
|
11
15
|
const credManager = new DiscordCredentialManager()
|
|
@@ -37,11 +41,27 @@ export async function infoAction(serverId: string, options: { pretty?: boolean }
|
|
|
37
41
|
const client = await new DiscordClient().login({ token: config.token })
|
|
38
42
|
const server = await client.getServer(serverId)
|
|
39
43
|
|
|
44
|
+
const tier = server.premium_tier ?? 0
|
|
45
|
+
const emojis = server.emojis ?? []
|
|
46
|
+
const stickers = server.stickers ?? []
|
|
47
|
+
const staticEmojiCount = emojis.filter((emoji) => !emoji.animated).length
|
|
48
|
+
|
|
40
49
|
const output = {
|
|
41
50
|
id: server.id,
|
|
42
51
|
name: server.name,
|
|
43
52
|
icon: server.icon,
|
|
44
|
-
|
|
53
|
+
owner_id: server.owner_id,
|
|
54
|
+
premium_tier: tier,
|
|
55
|
+
premium_subscription_count: server.premium_subscription_count ?? 0,
|
|
56
|
+
emoji_count: emojis.length,
|
|
57
|
+
sticker_count: stickers.length,
|
|
58
|
+
static_emoji_slots: STATIC_EMOJI_SLOTS_BY_TIER[tier] ?? STATIC_EMOJI_SLOTS_BY_TIER[0],
|
|
59
|
+
static_emoji_slots_remaining: Math.max(
|
|
60
|
+
0,
|
|
61
|
+
(STATIC_EMOJI_SLOTS_BY_TIER[tier] ?? STATIC_EMOJI_SLOTS_BY_TIER[0]) - staticEmojiCount,
|
|
62
|
+
),
|
|
63
|
+
sticker_slots: STICKER_SLOTS_BY_TIER[tier] ?? STICKER_SLOTS_BY_TIER[0],
|
|
64
|
+
sticker_slots_remaining: Math.max(0, (STICKER_SLOTS_BY_TIER[tier] ?? STICKER_SLOTS_BY_TIER[0]) - stickers.length),
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
console.log(formatOutput(output, options.pretty))
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { DiscordClient } from '../client'
|
|
7
|
+
import { DiscordCredentialManager } from '../credential-manager'
|
|
8
|
+
import { createAction, deleteAction, listAction } from './sticker'
|
|
9
|
+
|
|
10
|
+
let clientListStickersSpy: ReturnType<typeof spyOn>
|
|
11
|
+
let clientCreateStickerSpy: ReturnType<typeof spyOn>
|
|
12
|
+
let clientDeleteStickerSpy: ReturnType<typeof spyOn>
|
|
13
|
+
let credManagerLoadSpy: ReturnType<typeof spyOn>
|
|
14
|
+
let tempDir: string
|
|
15
|
+
const originalLog = console.log
|
|
16
|
+
const originalExit = process.exit
|
|
17
|
+
|
|
18
|
+
function pngBytes(width: number, height: number, padding = 0): Uint8Array {
|
|
19
|
+
const bytes = new Uint8Array(24 + padding)
|
|
20
|
+
bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0)
|
|
21
|
+
bytes.set([0x00, 0x00, 0x00, 0x0d], 8)
|
|
22
|
+
bytes.set([0x49, 0x48, 0x44, 0x52], 12)
|
|
23
|
+
const view = new DataView(bytes.buffer)
|
|
24
|
+
view.setUint32(16, width)
|
|
25
|
+
view.setUint32(20, height)
|
|
26
|
+
return bytes
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function writeTempImage(filename: string, bytes: Uint8Array): Promise<string> {
|
|
30
|
+
const path = join(tempDir, filename)
|
|
31
|
+
await writeFile(path, bytes)
|
|
32
|
+
return path
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
tempDir = await mkdtemp(join(tmpdir(), 'discord-sticker-'))
|
|
37
|
+
|
|
38
|
+
clientListStickersSpy = spyOn(DiscordClient.prototype, 'listStickers').mockResolvedValue([
|
|
39
|
+
{ id: 's1', name: 'potato_01', tags: 'potato', type: 2, format_type: 1 },
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
clientCreateStickerSpy = spyOn(DiscordClient.prototype, 'createSticker').mockResolvedValue({
|
|
43
|
+
id: 's9',
|
|
44
|
+
name: 'potato_13',
|
|
45
|
+
tags: 'potato',
|
|
46
|
+
type: 2,
|
|
47
|
+
format_type: 1,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
clientDeleteStickerSpy = spyOn(DiscordClient.prototype, 'deleteSticker').mockResolvedValue(undefined)
|
|
51
|
+
|
|
52
|
+
credManagerLoadSpy = spyOn(DiscordCredentialManager.prototype, 'load').mockResolvedValue({
|
|
53
|
+
token: 'test-token',
|
|
54
|
+
current_server: 'server-1',
|
|
55
|
+
servers: {},
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
afterEach(async () => {
|
|
60
|
+
console.log = originalLog
|
|
61
|
+
process.exit = originalExit
|
|
62
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
63
|
+
clientListStickersSpy?.mockRestore()
|
|
64
|
+
clientCreateStickerSpy?.mockRestore()
|
|
65
|
+
clientDeleteStickerSpy?.mockRestore()
|
|
66
|
+
credManagerLoadSpy?.mockRestore()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('list: reports the guild stickers', async () => {
|
|
70
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
71
|
+
console.log = consoleSpy
|
|
72
|
+
|
|
73
|
+
await listAction('g1', { pretty: false })
|
|
74
|
+
|
|
75
|
+
const output = consoleSpy.mock.calls[0][0]
|
|
76
|
+
expect(output).toContain('potato_01')
|
|
77
|
+
expect(output).toContain('"count":1')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('create: uploads a 320x320 PNG with its multipart fields', async () => {
|
|
81
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
82
|
+
console.log = consoleSpy
|
|
83
|
+
const path = await writeTempImage('potato_13.png', pngBytes(320, 320))
|
|
84
|
+
|
|
85
|
+
await createAction('g1', path, { tags: 'potato', description: 'a potato', pretty: false })
|
|
86
|
+
|
|
87
|
+
expect(clientCreateStickerSpy).toHaveBeenCalledWith(
|
|
88
|
+
'g1',
|
|
89
|
+
{ name: 'potato_13', description: 'a potato', tags: 'potato' },
|
|
90
|
+
expect.any(Uint8Array),
|
|
91
|
+
'potato_13.png',
|
|
92
|
+
)
|
|
93
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('potato_13')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('delete: removes the sticker by id', async () => {
|
|
97
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
98
|
+
console.log = consoleSpy
|
|
99
|
+
|
|
100
|
+
await deleteAction('g1', 's1', { pretty: false })
|
|
101
|
+
|
|
102
|
+
expect(clientDeleteStickerSpy).toHaveBeenCalledWith('g1', 's1')
|
|
103
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('"success":true')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('create: rejects a one-character name without calling the API', async () => {
|
|
107
|
+
const consoleSpy = mock((_msg: string) => {})
|
|
108
|
+
console.log = consoleSpy
|
|
109
|
+
spyOn(process, 'exit').mockImplementation(() => {
|
|
110
|
+
throw new Error('exit')
|
|
111
|
+
})
|
|
112
|
+
const path = await writeTempImage('potato_13.png', pngBytes(320, 320))
|
|
113
|
+
|
|
114
|
+
await expect(createAction('g1', path, { name: '흥', tags: '😤', pretty: false })).rejects.toThrow('exit')
|
|
115
|
+
|
|
116
|
+
expect(clientCreateStickerSpy).not.toHaveBeenCalled()
|
|
117
|
+
expect(consoleSpy.mock.calls[0][0]).toContain('at least 2')
|
|
118
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, extname, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander'
|
|
5
|
+
|
|
6
|
+
import { handleError } from '@/shared/utils/error-handler'
|
|
7
|
+
import { formatOutput } from '@/shared/utils/output'
|
|
8
|
+
|
|
9
|
+
import { DiscordClient } from '../client'
|
|
10
|
+
import { DiscordCredentialManager } from '../credential-manager'
|
|
11
|
+
import { validateStickerName } from '../expression-names'
|
|
12
|
+
|
|
13
|
+
export async function listAction(serverId: string, options: { pretty?: boolean }): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
const credManager = new DiscordCredentialManager()
|
|
16
|
+
const config = await credManager.load()
|
|
17
|
+
|
|
18
|
+
if (!config.token) {
|
|
19
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const client = await new DiscordClient().login({ token: config.token })
|
|
24
|
+
const stickers = await client.listStickers(serverId)
|
|
25
|
+
|
|
26
|
+
console.log(
|
|
27
|
+
formatOutput(
|
|
28
|
+
{
|
|
29
|
+
server_id: serverId,
|
|
30
|
+
count: stickers.length,
|
|
31
|
+
stickers: stickers.map((sticker) => ({
|
|
32
|
+
id: sticker.id,
|
|
33
|
+
name: sticker.name,
|
|
34
|
+
tags: sticker.tags,
|
|
35
|
+
description: sticker.description ?? null,
|
|
36
|
+
})),
|
|
37
|
+
},
|
|
38
|
+
options.pretty,
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
handleError(error as Error)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function createAction(
|
|
47
|
+
serverId: string,
|
|
48
|
+
path: string,
|
|
49
|
+
options: { name?: string; description?: string; tags: string; pretty?: boolean },
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
const credManager = new DiscordCredentialManager()
|
|
53
|
+
const config = await credManager.load()
|
|
54
|
+
|
|
55
|
+
if (!config.token) {
|
|
56
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const client = await new DiscordClient().login({ token: config.token })
|
|
61
|
+
|
|
62
|
+
const filePath = resolve(path)
|
|
63
|
+
const filename = basename(filePath)
|
|
64
|
+
const name = options.name ?? basename(filename, extname(filename))
|
|
65
|
+
|
|
66
|
+
const nameError = validateStickerName(name)
|
|
67
|
+
if (nameError) {
|
|
68
|
+
console.log(formatOutput({ error: nameError }, options.pretty))
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const image = new Uint8Array(await readFile(filePath))
|
|
73
|
+
|
|
74
|
+
const sticker = await client.createSticker(
|
|
75
|
+
serverId,
|
|
76
|
+
{ name, description: options.description ?? '', tags: options.tags },
|
|
77
|
+
image,
|
|
78
|
+
filename,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
console.log(
|
|
82
|
+
formatOutput({ success: true, server_id: serverId, id: sticker.id, name: sticker.name }, options.pretty),
|
|
83
|
+
)
|
|
84
|
+
} catch (error) {
|
|
85
|
+
handleError(error as Error)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function deleteAction(serverId: string, stickerId: string, options: { pretty?: boolean }): Promise<void> {
|
|
90
|
+
try {
|
|
91
|
+
const credManager = new DiscordCredentialManager()
|
|
92
|
+
const config = await credManager.load()
|
|
93
|
+
|
|
94
|
+
if (!config.token) {
|
|
95
|
+
console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const client = await new DiscordClient().login({ token: config.token })
|
|
100
|
+
await client.deleteSticker(serverId, stickerId)
|
|
101
|
+
|
|
102
|
+
console.log(formatOutput({ success: true, server_id: serverId, id: stickerId }, options.pretty))
|
|
103
|
+
} catch (error) {
|
|
104
|
+
handleError(error as Error)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const stickerCommand = new Command('sticker')
|
|
109
|
+
.description('Custom sticker commands')
|
|
110
|
+
.addCommand(
|
|
111
|
+
new Command('list')
|
|
112
|
+
.description('List custom stickers in a server')
|
|
113
|
+
.argument('<server-id>', 'Server ID')
|
|
114
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
115
|
+
.action(listAction),
|
|
116
|
+
)
|
|
117
|
+
.addCommand(
|
|
118
|
+
new Command('create')
|
|
119
|
+
.description('Upload a custom sticker to a server')
|
|
120
|
+
.argument('<server-id>', 'Server ID')
|
|
121
|
+
.argument('<file>', 'Image file (PNG, APNG, GIF or Lottie JSON, max 512KB; Discord documents 320x320)')
|
|
122
|
+
.requiredOption('--tags <emoji>', 'Unicode emoji this sticker relates to')
|
|
123
|
+
.option('--name <name>', 'Sticker name (defaults to the filename without extension)')
|
|
124
|
+
.option('--description <text>', 'Sticker description')
|
|
125
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
126
|
+
.action(createAction),
|
|
127
|
+
)
|
|
128
|
+
.addCommand(
|
|
129
|
+
new Command('delete')
|
|
130
|
+
.description('Delete a custom sticker from a server')
|
|
131
|
+
.argument('<server-id>', 'Server ID')
|
|
132
|
+
.argument('<sticker-id>', 'Sticker ID')
|
|
133
|
+
.option('--pretty', 'Pretty print JSON output')
|
|
134
|
+
.action(deleteAction),
|
|
135
|
+
)
|