@urugus/slack-cli 0.2.8 → 0.2.10
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/settings.local.json +6 -2
- package/.github/workflows/ci.yml +2 -2
- package/README.md +8 -0
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +16 -2
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/unread.d.ts.map +1 -1
- package/dist/commands/unread.js +32 -16
- package/dist/commands/unread.js.map +1 -1
- package/dist/types/commands.d.ts +2 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +5 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +5 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +1 -5
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/schedule-utils.d.ts +3 -0
- package/dist/utils/schedule-utils.d.ts.map +1 -0
- package/dist/utils/schedule-utils.js +34 -0
- package/dist/utils/schedule-utils.js.map +1 -0
- package/dist/utils/slack-api-client.d.ts +2 -1
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +3 -0
- package/dist/utils/slack-api-client.js.map +1 -1
- package/dist/utils/slack-operations/message-operations.d.ts +2 -1
- package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/message-operations.js +11 -0
- package/dist/utils/slack-operations/message-operations.js.map +1 -1
- package/dist/utils/validators.d.ts +4 -0
- package/dist/utils/validators.d.ts.map +1 -1
- package/dist/utils/validators.js +31 -0
- package/dist/utils/validators.js.map +1 -1
- package/eslint.config.js +38 -0
- package/package.json +13 -15
- package/src/commands/send.ts +21 -3
- package/src/commands/unread.ts +52 -22
- package/src/types/commands.ts +2 -0
- package/src/utils/constants.ts +7 -0
- package/src/utils/errors.ts +1 -5
- package/src/utils/schedule-utils.ts +41 -0
- package/src/utils/slack-api-client.ts +10 -1
- package/src/utils/slack-operations/message-operations.ts +25 -1
- package/src/utils/validators.ts +38 -0
- package/tests/commands/send.test.ts +235 -44
- package/tests/index.test.ts +2 -2
- package/tests/utils/schedule-utils.test.ts +63 -0
- package/tests/utils/slack-api-client.test.ts +18 -1
- package/tests/utils/slack-operations/message-operations.test.ts +19 -1
- package/.eslintrc.json +0 -25
- package/src/utils/formatters/output-formatter.ts +0 -7
- package/tests/utils/slack-operations/channel-operations-refactored.test.ts +0 -179
package/eslint.config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import tseslint from 'typescript-eslint';
|
|
3
|
+
import prettierConfig from 'eslint-config-prettier';
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{
|
|
7
|
+
ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'vitest.config.ts']
|
|
8
|
+
},
|
|
9
|
+
eslint.configs.recommended,
|
|
10
|
+
...tseslint.configs.recommended,
|
|
11
|
+
prettierConfig,
|
|
12
|
+
{
|
|
13
|
+
languageOptions: {
|
|
14
|
+
ecmaVersion: 2020,
|
|
15
|
+
sourceType: 'module',
|
|
16
|
+
parserOptions: {
|
|
17
|
+
project: './tsconfig.json'
|
|
18
|
+
},
|
|
19
|
+
globals: {
|
|
20
|
+
console: 'readonly',
|
|
21
|
+
process: 'readonly',
|
|
22
|
+
Buffer: 'readonly',
|
|
23
|
+
__dirname: 'readonly',
|
|
24
|
+
__filename: 'readonly',
|
|
25
|
+
exports: 'readonly',
|
|
26
|
+
module: 'readonly',
|
|
27
|
+
require: 'readonly',
|
|
28
|
+
global: 'readonly'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
rules: {
|
|
32
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
33
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
34
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
35
|
+
'no-console': 'off'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
];
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urugus/slack-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "A command-line tool for sending messages to Slack",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"slack-cli": "
|
|
7
|
+
"slack-cli": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
@@ -31,27 +31,25 @@
|
|
|
31
31
|
},
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@slack/web-api": "^
|
|
35
|
-
"chalk": "^4.1
|
|
36
|
-
"commander": "^
|
|
37
|
-
"dotenv": "^16.3.1",
|
|
38
|
-
"inquirer": "^8.2.6",
|
|
34
|
+
"@slack/web-api": "^7.9.3",
|
|
35
|
+
"chalk": "^5.4.1",
|
|
36
|
+
"commander": "^14.0.0",
|
|
39
37
|
"p-limit": "^3.1.0"
|
|
40
38
|
},
|
|
41
39
|
"devDependencies": {
|
|
42
|
-
"@types/inquirer": "^8.2.10",
|
|
43
40
|
"@types/node": "^20.10.0",
|
|
44
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
45
|
-
"@typescript-eslint/parser": "^
|
|
46
|
-
"@vitest/coverage-v8": "^
|
|
47
|
-
"eslint": "^
|
|
48
|
-
"eslint-config-prettier": "^
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
|
42
|
+
"@typescript-eslint/parser": "^8.34.1",
|
|
43
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
44
|
+
"eslint": "^9.30.0",
|
|
45
|
+
"eslint-config-prettier": "^10.1.5",
|
|
49
46
|
"prettier": "^3.1.1",
|
|
50
47
|
"ts-node": "^10.9.1",
|
|
51
48
|
"typescript": "^5.3.2",
|
|
52
|
-
"
|
|
49
|
+
"typescript-eslint": "^8.35.0",
|
|
50
|
+
"vitest": "^3.2.4"
|
|
53
51
|
},
|
|
54
52
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
53
|
+
"node": ">=20.0.0"
|
|
56
54
|
}
|
|
57
55
|
}
|
package/src/commands/send.ts
CHANGED
|
@@ -8,19 +8,26 @@ import { SendOptions } from '../types/commands';
|
|
|
8
8
|
import { extractErrorMessage } from '../utils/error-utils';
|
|
9
9
|
import { parseProfile } from '../utils/option-parsers';
|
|
10
10
|
import { createValidationHook, optionValidators } from '../utils/validators';
|
|
11
|
+
import { resolvePostAt } from '../utils/schedule-utils';
|
|
11
12
|
import * as fs from 'fs/promises';
|
|
12
13
|
|
|
13
14
|
export function setupSendCommand(): Command {
|
|
14
15
|
const sendCommand = new Command('send')
|
|
15
|
-
.description('Send a message to a Slack channel')
|
|
16
|
+
.description('Send or schedule a message to a Slack channel')
|
|
16
17
|
.requiredOption('-c, --channel <channel>', 'Target channel name or ID')
|
|
17
18
|
.option('-m, --message <message>', 'Message to send')
|
|
18
19
|
.option('-f, --file <file>', 'File containing message content')
|
|
19
20
|
.option('-t, --thread <thread>', 'Thread timestamp to reply to')
|
|
21
|
+
.option('--at <time>', 'Schedule time (Unix timestamp in seconds or ISO 8601)')
|
|
22
|
+
.option('--after <minutes>', 'Schedule message after N minutes')
|
|
20
23
|
.option('--profile <profile>', 'Use specific workspace profile')
|
|
21
24
|
.hook(
|
|
22
25
|
'preAction',
|
|
23
|
-
createValidationHook([
|
|
26
|
+
createValidationHook([
|
|
27
|
+
optionValidators.messageOrFile,
|
|
28
|
+
optionValidators.threadTimestamp,
|
|
29
|
+
optionValidators.scheduleTiming,
|
|
30
|
+
])
|
|
24
31
|
)
|
|
25
32
|
.action(
|
|
26
33
|
wrapCommand(async (options: SendOptions) => {
|
|
@@ -38,11 +45,22 @@ export function setupSendCommand(): Command {
|
|
|
38
45
|
messageContent = options.message!; // This is safe because of preAction validation
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
const postAt = resolvePostAt(options.at, options.after);
|
|
49
|
+
|
|
41
50
|
// Send message
|
|
42
51
|
const profile = parseProfile(options.profile);
|
|
43
52
|
const client = await createSlackClient(profile);
|
|
44
|
-
await client.sendMessage(options.channel, messageContent, options.thread);
|
|
45
53
|
|
|
54
|
+
if (postAt !== null) {
|
|
55
|
+
await client.scheduleMessage(options.channel, messageContent, postAt, options.thread);
|
|
56
|
+
const postAtIso = new Date(postAt * 1000).toISOString();
|
|
57
|
+
console.log(
|
|
58
|
+
chalk.green(`✓ ${SUCCESS_MESSAGES.MESSAGE_SCHEDULED(options.channel, postAtIso)}`)
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await client.sendMessage(options.channel, messageContent, options.thread);
|
|
46
64
|
console.log(chalk.green(`✓ ${SUCCESS_MESSAGES.MESSAGE_SENT(options.channel)}`));
|
|
47
65
|
})
|
|
48
66
|
);
|
package/src/commands/unread.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { wrapCommand } from '../utils/command-wrapper';
|
|
3
3
|
import { createSlackClient } from '../utils/client-factory';
|
|
4
|
-
import { SlackApiClient } from '../utils/slack-api-client';
|
|
4
|
+
import { SlackApiClient, ChannelUnreadResult, Channel } from '../utils/slack-api-client';
|
|
5
5
|
import { UnreadOptions } from '../types/commands';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { createChannelFormatter } from '../utils/formatters/channel-formatters';
|
|
@@ -9,15 +9,15 @@ import { createMessageFormatter } from '../utils/formatters/message-formatters';
|
|
|
9
9
|
import { DEFAULTS } from '../utils/constants';
|
|
10
10
|
import { parseLimit, parseFormat, parseBoolean } from '../utils/option-parsers';
|
|
11
11
|
|
|
12
|
-
async function
|
|
13
|
-
client
|
|
14
|
-
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
const result = await client.getChannelUnread(options.channel!);
|
|
17
|
-
|
|
18
|
-
const format = parseFormat(options.format);
|
|
19
|
-
const countOnly = parseBoolean(options.countOnly);
|
|
12
|
+
async function fetchChannelUnreadData(client: SlackApiClient, channelName: string) {
|
|
13
|
+
return await client.getChannelUnread(channelName);
|
|
14
|
+
}
|
|
20
15
|
|
|
16
|
+
function formatChannelUnreadOutput(
|
|
17
|
+
result: ChannelUnreadResult,
|
|
18
|
+
format: string,
|
|
19
|
+
countOnly: boolean
|
|
20
|
+
): void {
|
|
21
21
|
const formatter = createMessageFormatter(format);
|
|
22
22
|
formatter.format({
|
|
23
23
|
channel: result.channel,
|
|
@@ -26,40 +26,70 @@ async function handleSpecificChannelUnread(
|
|
|
26
26
|
countOnly: countOnly,
|
|
27
27
|
format: format,
|
|
28
28
|
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function markChannelAsRead(client: SlackApiClient, channel: Channel): Promise<void> {
|
|
32
|
+
await client.markAsRead(channel.id);
|
|
33
|
+
console.log(chalk.green(`✓ Marked messages in #${channel.name} as read`));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function handleSpecificChannelUnread(
|
|
37
|
+
client: SlackApiClient,
|
|
38
|
+
options: UnreadOptions
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const result = await fetchChannelUnreadData(client, options.channel!);
|
|
41
|
+
|
|
42
|
+
const format = parseFormat(options.format);
|
|
43
|
+
const countOnly = parseBoolean(options.countOnly);
|
|
44
|
+
|
|
45
|
+
formatChannelUnreadOutput(result, format, countOnly);
|
|
29
46
|
|
|
30
47
|
if (parseBoolean(options.markRead)) {
|
|
31
|
-
await client
|
|
32
|
-
console.log(chalk.green(`✓ Marked messages in #${result.channel.name} as read`));
|
|
48
|
+
await markChannelAsRead(client, result.channel);
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
51
|
|
|
52
|
+
async function fetchAllUnreadChannels(client: SlackApiClient) {
|
|
53
|
+
return await client.listUnreadChannels();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatAllChannelsOutput(
|
|
57
|
+
channels: Channel[],
|
|
58
|
+
format: string,
|
|
59
|
+
countOnly: boolean,
|
|
60
|
+
limit: number
|
|
61
|
+
): void {
|
|
62
|
+
const displayChannels = channels.slice(0, limit);
|
|
63
|
+
const formatter = createChannelFormatter(format, countOnly);
|
|
64
|
+
formatter.format({ channels: displayChannels, countOnly: countOnly });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function markAllChannelsAsRead(client: SlackApiClient, channels: Channel[]): Promise<void> {
|
|
68
|
+
for (const channel of channels) {
|
|
69
|
+
await client.markAsRead(channel.id);
|
|
70
|
+
}
|
|
71
|
+
console.log(chalk.green('✓ Marked all messages as read'));
|
|
72
|
+
}
|
|
73
|
+
|
|
36
74
|
async function handleAllChannelsUnread(
|
|
37
75
|
client: SlackApiClient,
|
|
38
76
|
options: UnreadOptions
|
|
39
77
|
): Promise<void> {
|
|
40
|
-
const channels = await client
|
|
78
|
+
const channels = await fetchAllUnreadChannels(client);
|
|
41
79
|
|
|
42
80
|
if (channels.length === 0) {
|
|
43
81
|
console.log(chalk.green('✓ No unread messages'));
|
|
44
82
|
return;
|
|
45
83
|
}
|
|
46
84
|
|
|
47
|
-
// Apply limit
|
|
48
85
|
const limit = parseLimit(options.limit, DEFAULTS.UNREAD_DISPLAY_LIMIT);
|
|
49
|
-
const displayChannels = channels.slice(0, limit);
|
|
50
|
-
|
|
51
86
|
const format = parseFormat(options.format);
|
|
52
87
|
const countOnly = parseBoolean(options.countOnly);
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
formatter.format({ channels: displayChannels, countOnly: countOnly });
|
|
89
|
+
formatAllChannelsOutput(channels, format, countOnly, limit);
|
|
56
90
|
|
|
57
91
|
if (parseBoolean(options.markRead)) {
|
|
58
|
-
|
|
59
|
-
for (const channel of channels) {
|
|
60
|
-
await client.markAsRead(channel.id);
|
|
61
|
-
}
|
|
62
|
-
console.log(chalk.green('✓ Marked all messages as read'));
|
|
92
|
+
await markAllChannelsAsRead(client, channels);
|
|
63
93
|
}
|
|
64
94
|
}
|
|
65
95
|
|
package/src/types/commands.ts
CHANGED
package/src/utils/constants.ts
CHANGED
|
@@ -14,6 +14,11 @@ export const ERROR_MESSAGES = {
|
|
|
14
14
|
NO_MESSAGE_OR_FILE: 'You must specify either --message or --file',
|
|
15
15
|
BOTH_MESSAGE_AND_FILE: 'Cannot use both --message and --file',
|
|
16
16
|
INVALID_THREAD_TIMESTAMP: 'Invalid thread timestamp format',
|
|
17
|
+
INVALID_SCHEDULE_AT:
|
|
18
|
+
'Invalid schedule time format. Use Unix timestamp (seconds) or ISO 8601 date-time',
|
|
19
|
+
INVALID_SCHEDULE_AFTER: '--after must be a positive integer (minutes)',
|
|
20
|
+
BOTH_SCHEDULE_OPTIONS: 'Cannot use both --at and --after',
|
|
21
|
+
SCHEDULE_TIME_IN_PAST: 'Schedule time must be in the future',
|
|
17
22
|
|
|
18
23
|
// API errors
|
|
19
24
|
API_ERROR: (error: string) => `API Error: ${error}`,
|
|
@@ -33,6 +38,8 @@ export const SUCCESS_MESSAGES = {
|
|
|
33
38
|
PROFILE_SWITCHED: (profileName: string) => `Switched to profile "${profileName}"`,
|
|
34
39
|
PROFILE_CLEARED: (profileName: string) => `Profile "${profileName}" cleared successfully`,
|
|
35
40
|
MESSAGE_SENT: (channel: string) => `Message sent successfully to #${channel}`,
|
|
41
|
+
MESSAGE_SCHEDULED: (channel: string, postAtIso: string) =>
|
|
42
|
+
`Message scheduled to #${channel} at ${postAtIso}`,
|
|
36
43
|
} as const;
|
|
37
44
|
|
|
38
45
|
// File and system constants
|
package/src/utils/errors.ts
CHANGED
|
@@ -4,34 +4,30 @@ export class SlackCliError extends Error {
|
|
|
4
4
|
public code?: string
|
|
5
5
|
) {
|
|
6
6
|
super(message);
|
|
7
|
-
this.name =
|
|
7
|
+
this.name = this.constructor.name;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export class ConfigurationError extends SlackCliError {
|
|
12
12
|
constructor(message: string) {
|
|
13
13
|
super(message, 'CONFIGURATION_ERROR');
|
|
14
|
-
this.name = 'ConfigurationError';
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export class ValidationError extends SlackCliError {
|
|
19
18
|
constructor(message: string) {
|
|
20
19
|
super(message, 'VALIDATION_ERROR');
|
|
21
|
-
this.name = 'ValidationError';
|
|
22
20
|
}
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
export class ApiError extends SlackCliError {
|
|
26
24
|
constructor(message: string) {
|
|
27
25
|
super(message, 'API_ERROR');
|
|
28
|
-
this.name = 'ApiError';
|
|
29
26
|
}
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
export class FileError extends SlackCliError {
|
|
33
30
|
constructor(message: string) {
|
|
34
31
|
super(message, 'FILE_ERROR');
|
|
35
|
-
this.name = 'FileError';
|
|
36
32
|
}
|
|
37
33
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function parseScheduledTimestamp(value: string): number | null {
|
|
2
|
+
const trimmed = value.trim();
|
|
3
|
+
|
|
4
|
+
if (/^\d+$/.test(trimmed)) {
|
|
5
|
+
const timestamp = Number.parseInt(trimmed, 10);
|
|
6
|
+
return Number.isSafeInteger(timestamp) ? timestamp : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const parsedMs = Date.parse(trimmed);
|
|
10
|
+
if (Number.isNaN(parsedMs)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return Math.floor(parsedMs / 1000);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolvePostAt(
|
|
18
|
+
at: string | undefined,
|
|
19
|
+
afterMinutes: string | undefined,
|
|
20
|
+
nowMs = Date.now()
|
|
21
|
+
): number | null {
|
|
22
|
+
if (at) {
|
|
23
|
+
return parseScheduledTimestamp(at);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!afterMinutes) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const trimmedAfter = afterMinutes.trim();
|
|
31
|
+
if (!/^\d+$/.test(trimmedAfter)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const minutes = Number.parseInt(trimmedAfter, 10);
|
|
36
|
+
if (!Number.isSafeInteger(minutes) || minutes <= 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Math.floor(nowMs / 1000) + minutes * 60;
|
|
41
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChatPostMessageResponse } from '@slack/web-api';
|
|
1
|
+
import { ChatPostMessageResponse, ChatScheduleMessageResponse } from '@slack/web-api';
|
|
2
2
|
import { ChannelOperations } from './slack-operations/channel-operations';
|
|
3
3
|
import { MessageOperations } from './slack-operations/message-operations';
|
|
4
4
|
|
|
@@ -85,6 +85,15 @@ export class SlackApiClient {
|
|
|
85
85
|
return this.messageOps.sendMessage(channel, text, thread_ts);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
async scheduleMessage(
|
|
89
|
+
channel: string,
|
|
90
|
+
text: string,
|
|
91
|
+
post_at: number,
|
|
92
|
+
thread_ts?: string
|
|
93
|
+
): Promise<ChatScheduleMessageResponse> {
|
|
94
|
+
return this.messageOps.scheduleMessage(channel, text, post_at, thread_ts);
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
async listChannels(options: ListChannelsOptions): Promise<Channel[]> {
|
|
89
98
|
return this.channelOps.listChannels(options);
|
|
90
99
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ChatPostMessageResponse,
|
|
3
|
+
ChatPostMessageArguments,
|
|
4
|
+
ChatScheduleMessageArguments,
|
|
5
|
+
ChatScheduleMessageResponse,
|
|
6
|
+
} from '@slack/web-api';
|
|
2
7
|
import { BaseSlackClient } from './base-client';
|
|
3
8
|
import { channelResolver } from '../channel-resolver';
|
|
4
9
|
import { DEFAULTS } from '../constants';
|
|
@@ -31,6 +36,25 @@ export class MessageOperations extends BaseSlackClient {
|
|
|
31
36
|
return await this.client.chat.postMessage(params);
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
async scheduleMessage(
|
|
40
|
+
channel: string,
|
|
41
|
+
text: string,
|
|
42
|
+
post_at: number,
|
|
43
|
+
thread_ts?: string
|
|
44
|
+
): Promise<ChatScheduleMessageResponse> {
|
|
45
|
+
const params: ChatScheduleMessageArguments = {
|
|
46
|
+
channel,
|
|
47
|
+
text,
|
|
48
|
+
post_at,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (thread_ts) {
|
|
52
|
+
params.thread_ts = thread_ts;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return await this.client.chat.scheduleMessage(params);
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
async getHistory(channel: string, options: HistoryOptions): Promise<HistoryResult> {
|
|
35
59
|
// Resolve channel name to ID if needed
|
|
36
60
|
const channelId = await channelResolver.resolveChannelId(channel, () =>
|
package/src/utils/validators.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { ERROR_MESSAGES } from './constants';
|
|
3
|
+
import { parseScheduledTimestamp } from './schedule-utils';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Common validation functions for CLI commands
|
|
@@ -164,6 +165,43 @@ export const optionValidators = {
|
|
|
164
165
|
return null;
|
|
165
166
|
},
|
|
166
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Validates schedule options for send command
|
|
170
|
+
*/
|
|
171
|
+
scheduleTiming: (options: Record<string, unknown>): string | null => {
|
|
172
|
+
const at = options.at as string | undefined;
|
|
173
|
+
const after = options.after as string | undefined;
|
|
174
|
+
|
|
175
|
+
if (at && after) {
|
|
176
|
+
return ERROR_MESSAGES.BOTH_SCHEDULE_OPTIONS;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (at) {
|
|
180
|
+
const postAt = parseScheduledTimestamp(at);
|
|
181
|
+
if (postAt === null) {
|
|
182
|
+
return ERROR_MESSAGES.INVALID_SCHEDULE_AT;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (postAt <= Math.floor(Date.now() / 1000)) {
|
|
186
|
+
return ERROR_MESSAGES.SCHEDULE_TIME_IN_PAST;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (after) {
|
|
191
|
+
const trimmedAfter = after.trim();
|
|
192
|
+
if (!/^\d+$/.test(trimmedAfter)) {
|
|
193
|
+
return ERROR_MESSAGES.INVALID_SCHEDULE_AFTER;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const minutes = Number.parseInt(trimmedAfter, 10);
|
|
197
|
+
if (!Number.isSafeInteger(minutes) || minutes <= 0) {
|
|
198
|
+
return ERROR_MESSAGES.INVALID_SCHEDULE_AFTER;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return null;
|
|
203
|
+
},
|
|
204
|
+
|
|
167
205
|
/**
|
|
168
206
|
* Validates message count for history command
|
|
169
207
|
*/
|