@urugus/slack-cli 0.2.5 → 0.2.7

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.
Files changed (55) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/README.md +7 -0
  3. package/dist/commands/history.d.ts.map +1 -1
  4. package/dist/commands/history.js +7 -7
  5. package/dist/commands/history.js.map +1 -1
  6. package/dist/commands/send.d.ts.map +1 -1
  7. package/dist/commands/send.js +7 -11
  8. package/dist/commands/send.js.map +1 -1
  9. package/dist/types/commands.d.ts +1 -0
  10. package/dist/types/commands.d.ts.map +1 -1
  11. package/dist/utils/constants.d.ts +1 -0
  12. package/dist/utils/constants.d.ts.map +1 -1
  13. package/dist/utils/constants.js +1 -0
  14. package/dist/utils/constants.js.map +1 -1
  15. package/dist/utils/profile-config.d.ts.map +1 -1
  16. package/dist/utils/profile-config.js +2 -6
  17. package/dist/utils/profile-config.js.map +1 -1
  18. package/dist/utils/slack-api-client.d.ts +1 -1
  19. package/dist/utils/slack-api-client.d.ts.map +1 -1
  20. package/dist/utils/slack-api-client.js +2 -2
  21. package/dist/utils/slack-api-client.js.map +1 -1
  22. package/dist/utils/slack-operations/channel-operations.d.ts +9 -0
  23. package/dist/utils/slack-operations/channel-operations.d.ts.map +1 -1
  24. package/dist/utils/slack-operations/channel-operations.js +77 -50
  25. package/dist/utils/slack-operations/channel-operations.js.map +1 -1
  26. package/dist/utils/slack-operations/message-operations.d.ts +1 -1
  27. package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
  28. package/dist/utils/slack-operations/message-operations.js +7 -3
  29. package/dist/utils/slack-operations/message-operations.js.map +1 -1
  30. package/dist/utils/token-utils.d.ts +7 -0
  31. package/dist/utils/token-utils.d.ts.map +1 -0
  32. package/dist/utils/token-utils.js +18 -0
  33. package/dist/utils/token-utils.js.map +1 -0
  34. package/dist/utils/validators.d.ts +79 -0
  35. package/dist/utils/validators.d.ts.map +1 -0
  36. package/dist/utils/validators.js +175 -0
  37. package/dist/utils/validators.js.map +1 -0
  38. package/package.json +1 -1
  39. package/src/commands/history.ts +17 -12
  40. package/src/commands/send.ts +10 -11
  41. package/src/types/commands.ts +1 -0
  42. package/src/utils/constants.ts +1 -0
  43. package/src/utils/profile-config.ts +3 -15
  44. package/src/utils/slack-api-client.ts +6 -2
  45. package/src/utils/slack-operations/channel-operations.ts +91 -54
  46. package/src/utils/slack-operations/message-operations.ts +14 -4
  47. package/src/utils/token-utils.ts +17 -0
  48. package/src/utils/validators.ts +212 -0
  49. package/tests/commands/send.test.ts +69 -2
  50. package/tests/utils/option-parsers.test.ts +173 -0
  51. package/tests/utils/profile-config.test.ts +282 -0
  52. package/tests/utils/slack-operations/channel-operations-refactored.test.ts +179 -0
  53. package/tests/utils/token-utils.test.ts +33 -0
  54. package/tests/utils/validators.test.ts +307 -0
  55. package/src/utils/profile-config-refactored.ts +0 -161
@@ -0,0 +1,79 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Common validation functions for CLI commands
4
+ */
5
+ export interface ValidationRule<T = unknown> {
6
+ validate: (value: T) => boolean | string;
7
+ errorMessage?: string;
8
+ }
9
+ export interface ValidationOptions {
10
+ required?: boolean;
11
+ rules?: ValidationRule[];
12
+ }
13
+ /**
14
+ * Validates that a value exists (not undefined, null, or empty string)
15
+ */
16
+ export declare function validateRequired(value: unknown, fieldName: string): string | null;
17
+ /**
18
+ * Validates mutually exclusive options
19
+ */
20
+ export declare function validateMutuallyExclusive(options: Record<string, unknown>, fields: string[], errorMessage?: string): string | null;
21
+ /**
22
+ * Validates string format using regex
23
+ */
24
+ export declare function validateFormat(value: string, pattern: RegExp, errorMessage: string): string | null;
25
+ /**
26
+ * Validates numeric range
27
+ */
28
+ export declare function validateRange(value: number, min?: number, max?: number, fieldName?: string): string | null;
29
+ /**
30
+ * Validates date format
31
+ */
32
+ export declare function validateDateFormat(dateString: string): string | null;
33
+ /**
34
+ * Common format validators
35
+ */
36
+ export declare const formatValidators: {
37
+ /**
38
+ * Validates Slack thread timestamp format (1234567890.123456)
39
+ */
40
+ threadTimestamp: (value: string) => string | null;
41
+ /**
42
+ * Validates Slack channel ID format (C1234567890, D1234567890, G1234567890)
43
+ */
44
+ channelId: (value: string) => string | null;
45
+ /**
46
+ * Validates output format options
47
+ */
48
+ outputFormat: (value: string) => string | null;
49
+ };
50
+ /**
51
+ * Creates a preAction hook for command validation
52
+ */
53
+ export declare function createValidationHook(validations: Array<(options: Record<string, unknown>, command: Command) => string | null>): (thisCommand: Command) => void;
54
+ /**
55
+ * Common command option validators
56
+ */
57
+ export declare const optionValidators: {
58
+ /**
59
+ * Validates message/file options for send command
60
+ */
61
+ messageOrFile: (options: Record<string, unknown>) => string | null;
62
+ /**
63
+ * Validates thread timestamp if provided
64
+ */
65
+ threadTimestamp: (options: Record<string, unknown>) => string | null;
66
+ /**
67
+ * Validates message count for history command
68
+ */
69
+ messageCount: (options: Record<string, unknown>) => string | null;
70
+ /**
71
+ * Validates date format for history command
72
+ */
73
+ sinceDate: (options: Record<string, unknown>) => string | null;
74
+ };
75
+ /**
76
+ * Creates a validated option parser
77
+ */
78
+ export declare function createOptionParser<T>(parser: (value: string | undefined, defaultValue: T) => T, validator?: (value: T) => string | null): (value: string | undefined, defaultValue: T) => T;
79
+ //# sourceMappingURL=validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../src/utils/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC;;GAEG;AAEH,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO;IACzC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,GAAG,MAAM,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKjF;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,MAAM,EAAE,MAAM,EAAE,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,GAAG,IAAI,CASf;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB,MAAM,GAAG,IAAI,CAKf;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,GAAG,CAAC,EAAE,MAAM,EACZ,GAAG,CAAC,EAAE,MAAM,EACZ,SAAS,SAAU,GAClB,MAAM,GAAG,IAAI,CAQf;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMpE;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB;IAC3B;;OAEG;6BACsB,MAAM,KAAG,MAAM,GAAG,IAAI;IAK/C;;OAEG;uBACgB,MAAM,KAAG,MAAM,GAAG,IAAI;IAKzC;;OAEG;0BACmB,MAAM,KAAG,MAAM,GAAG,IAAI;CAO7C,CAAC;AAEF;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC,GACxF,CAAC,WAAW,EAAE,OAAO,KAAK,IAAI,CAYhC;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB;IAC3B;;OAEG;6BACsB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,GAAG,IAAI;IAUhE;;OAEG;+BACwB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,GAAG,IAAI;IAOlE;;OAEG;4BACqB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,GAAG,IAAI;IAW/D;;OAEG;yBACkB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,GAAG,IAAI;CAS7D,CAAC;AAEF;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,EACzD,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,GACtC,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,CAWnD"}
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.optionValidators = exports.formatValidators = void 0;
4
+ exports.validateRequired = validateRequired;
5
+ exports.validateMutuallyExclusive = validateMutuallyExclusive;
6
+ exports.validateFormat = validateFormat;
7
+ exports.validateRange = validateRange;
8
+ exports.validateDateFormat = validateDateFormat;
9
+ exports.createValidationHook = createValidationHook;
10
+ exports.createOptionParser = createOptionParser;
11
+ const constants_1 = require("./constants");
12
+ /**
13
+ * Validates that a value exists (not undefined, null, or empty string)
14
+ */
15
+ function validateRequired(value, fieldName) {
16
+ if (value === undefined || value === null || value === '') {
17
+ return `${fieldName} is required`;
18
+ }
19
+ return null;
20
+ }
21
+ /**
22
+ * Validates mutually exclusive options
23
+ */
24
+ function validateMutuallyExclusive(options, fields, errorMessage) {
25
+ const presentFields = fields.filter((field) => options[field] !== undefined);
26
+ if (presentFields.length > 1) {
27
+ return errorMessage || `Cannot use both ${presentFields.join(' and ')}`;
28
+ }
29
+ if (presentFields.length === 0) {
30
+ return errorMessage || `Must specify one of: ${fields.join(', ')}`;
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * Validates string format using regex
36
+ */
37
+ function validateFormat(value, pattern, errorMessage) {
38
+ if (!pattern.test(value)) {
39
+ return errorMessage;
40
+ }
41
+ return null;
42
+ }
43
+ /**
44
+ * Validates numeric range
45
+ */
46
+ function validateRange(value, min, max, fieldName = 'Value') {
47
+ if (min !== undefined && value < min) {
48
+ return `${fieldName} must be at least ${min}`;
49
+ }
50
+ if (max !== undefined && value > max) {
51
+ return `${fieldName} must be at most ${max}`;
52
+ }
53
+ return null;
54
+ }
55
+ /**
56
+ * Validates date format
57
+ */
58
+ function validateDateFormat(dateString) {
59
+ const date = new Date(dateString);
60
+ if (isNaN(date.getTime())) {
61
+ return 'Invalid date format';
62
+ }
63
+ return null;
64
+ }
65
+ /**
66
+ * Common format validators
67
+ */
68
+ exports.formatValidators = {
69
+ /**
70
+ * Validates Slack thread timestamp format (1234567890.123456)
71
+ */
72
+ threadTimestamp: (value) => {
73
+ const pattern = /^\d{10}\.\d{6}$/;
74
+ return validateFormat(value, pattern, constants_1.ERROR_MESSAGES.INVALID_THREAD_TIMESTAMP);
75
+ },
76
+ /**
77
+ * Validates Slack channel ID format (C1234567890, D1234567890, G1234567890)
78
+ */
79
+ channelId: (value) => {
80
+ const pattern = /^[CDG][A-Z0-9]{10,}$/;
81
+ return validateFormat(value, pattern, 'Invalid channel ID format');
82
+ },
83
+ /**
84
+ * Validates output format options
85
+ */
86
+ outputFormat: (value) => {
87
+ const validFormats = ['table', 'simple', 'json', 'compact'];
88
+ if (!validFormats.includes(value)) {
89
+ return `Invalid format. Must be one of: ${validFormats.join(', ')}`;
90
+ }
91
+ return null;
92
+ },
93
+ };
94
+ /**
95
+ * Creates a preAction hook for command validation
96
+ */
97
+ function createValidationHook(validations) {
98
+ return (thisCommand) => {
99
+ const options = thisCommand.opts();
100
+ for (const validation of validations) {
101
+ const error = validation(options, thisCommand);
102
+ if (error) {
103
+ thisCommand.error(`Error: ${error}`);
104
+ break; // Stop processing after first error
105
+ }
106
+ }
107
+ };
108
+ }
109
+ /**
110
+ * Common command option validators
111
+ */
112
+ exports.optionValidators = {
113
+ /**
114
+ * Validates message/file options for send command
115
+ */
116
+ messageOrFile: (options) => {
117
+ if (!options.message && !options.file) {
118
+ return constants_1.ERROR_MESSAGES.NO_MESSAGE_OR_FILE;
119
+ }
120
+ if (options.message && options.file) {
121
+ return constants_1.ERROR_MESSAGES.BOTH_MESSAGE_AND_FILE;
122
+ }
123
+ return null;
124
+ },
125
+ /**
126
+ * Validates thread timestamp if provided
127
+ */
128
+ threadTimestamp: (options) => {
129
+ if (options.thread) {
130
+ return exports.formatValidators.threadTimestamp(options.thread);
131
+ }
132
+ return null;
133
+ },
134
+ /**
135
+ * Validates message count for history command
136
+ */
137
+ messageCount: (options) => {
138
+ if (options.number) {
139
+ const count = parseInt(options.number, 10);
140
+ if (isNaN(count)) {
141
+ return 'Message count must be a number';
142
+ }
143
+ return validateRange(count, 1, 1000, 'Message count');
144
+ }
145
+ return null;
146
+ },
147
+ /**
148
+ * Validates date format for history command
149
+ */
150
+ sinceDate: (options) => {
151
+ if (options.since) {
152
+ const error = validateDateFormat(options.since);
153
+ if (error) {
154
+ return 'Invalid date format. Use YYYY-MM-DD HH:MM:SS';
155
+ }
156
+ }
157
+ return null;
158
+ },
159
+ };
160
+ /**
161
+ * Creates a validated option parser
162
+ */
163
+ function createOptionParser(parser, validator) {
164
+ return (value, defaultValue) => {
165
+ const parsed = parser(value, defaultValue);
166
+ if (validator) {
167
+ const error = validator(parsed);
168
+ if (error) {
169
+ throw new Error(error);
170
+ }
171
+ }
172
+ return parsed;
173
+ };
174
+ }
175
+ //# sourceMappingURL=validators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.js","sourceRoot":"","sources":["../../src/utils/validators.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urugus/slack-cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "A command-line tool for sending messages to Slack",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -4,11 +4,9 @@ import { wrapCommand } from '../utils/command-wrapper';
4
4
  import { createSlackClient } from '../utils/client-factory';
5
5
  import { HistoryOptions } from '../types/commands';
6
6
  import { API_LIMITS } from '../utils/constants';
7
- import {
8
- validateMessageCount,
9
- validateDateFormat,
10
- prepareSinceTimestamp,
11
- } from './history-validators';
7
+ import { parseCount, parseProfile } from '../utils/option-parsers';
8
+ import { createValidationHook, optionValidators } from '../utils/validators';
9
+ import { prepareSinceTimestamp } from './history-validators';
12
10
  import { displayHistoryResults } from './history-display';
13
11
 
14
12
  export function setupHistoryCommand(): Command {
@@ -22,17 +20,24 @@ export function setupHistoryCommand(): Command {
22
20
  )
23
21
  .option('--since <date>', 'Get messages since specific date (YYYY-MM-DD HH:MM:SS)')
24
22
  .option('--profile <profile>', 'Use specific workspace profile')
25
- .hook('preAction', (thisCommand) => {
26
- const options = thisCommand.opts();
27
- validateMessageCount(options.number, thisCommand);
28
- validateDateFormat(options.since, thisCommand);
29
- })
23
+ .hook(
24
+ 'preAction',
25
+ createValidationHook([optionValidators.messageCount, optionValidators.sinceDate])
26
+ )
30
27
  .action(
31
28
  wrapCommand(async (options: HistoryOptions) => {
32
- const client = await createSlackClient(options.profile);
29
+ const profile = parseProfile(options.profile);
30
+ const client = await createSlackClient(profile);
31
+
32
+ const limit = parseCount(
33
+ options.number,
34
+ API_LIMITS.DEFAULT_MESSAGE_COUNT,
35
+ API_LIMITS.MIN_MESSAGE_COUNT,
36
+ API_LIMITS.MAX_MESSAGE_COUNT
37
+ );
33
38
 
34
39
  const historyOptions: ApiHistoryOptions = {
35
- limit: parseInt(options.number || API_LIMITS.DEFAULT_MESSAGE_COUNT.toString(), 10),
40
+ limit,
36
41
  };
37
42
 
38
43
  const oldest = prepareSinceTimestamp(options.since);
@@ -6,6 +6,8 @@ import { createSlackClient } from '../utils/client-factory';
6
6
  import { FileError } from '../utils/errors';
7
7
  import { SendOptions } from '../types/commands';
8
8
  import { extractErrorMessage } from '../utils/error-utils';
9
+ import { parseProfile } from '../utils/option-parsers';
10
+ import { createValidationHook, optionValidators } from '../utils/validators';
9
11
  import * as fs from 'fs/promises';
10
12
 
11
13
  export function setupSendCommand(): Command {
@@ -14,16 +16,12 @@ export function setupSendCommand(): Command {
14
16
  .requiredOption('-c, --channel <channel>', 'Target channel name or ID')
15
17
  .option('-m, --message <message>', 'Message to send')
16
18
  .option('-f, --file <file>', 'File containing message content')
19
+ .option('-t, --thread <thread>', 'Thread timestamp to reply to')
17
20
  .option('--profile <profile>', 'Use specific workspace profile')
18
- .hook('preAction', (thisCommand) => {
19
- const options = thisCommand.opts();
20
- if (!options.message && !options.file) {
21
- thisCommand.error(`Error: ${ERROR_MESSAGES.NO_MESSAGE_OR_FILE}`);
22
- }
23
- if (options.message && options.file) {
24
- thisCommand.error(`Error: ${ERROR_MESSAGES.BOTH_MESSAGE_AND_FILE}`);
25
- }
26
- })
21
+ .hook(
22
+ 'preAction',
23
+ createValidationHook([optionValidators.messageOrFile, optionValidators.threadTimestamp])
24
+ )
27
25
  .action(
28
26
  wrapCommand(async (options: SendOptions) => {
29
27
  // Get message content
@@ -41,8 +39,9 @@ export function setupSendCommand(): Command {
41
39
  }
42
40
 
43
41
  // Send message
44
- const client = await createSlackClient(options.profile);
45
- await client.sendMessage(options.channel, messageContent);
42
+ const profile = parseProfile(options.profile);
43
+ const client = await createSlackClient(profile);
44
+ await client.sendMessage(options.channel, messageContent, options.thread);
46
45
 
47
46
  console.log(chalk.green(`✓ ${SUCCESS_MESSAGES.MESSAGE_SENT(options.channel)}`));
48
47
  })
@@ -19,6 +19,7 @@ export interface SendOptions {
19
19
  channel: string;
20
20
  message?: string;
21
21
  file?: string;
22
+ thread?: string;
22
23
  profile?: string;
23
24
  }
24
25
 
@@ -13,6 +13,7 @@ export const ERROR_MESSAGES = {
13
13
  // Validation errors
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
+ INVALID_THREAD_TIMESTAMP: 'Invalid thread timestamp format',
16
17
 
17
18
  // API errors
18
19
  API_ERROR: (error: string) => `API Error: ${error}`,
@@ -2,13 +2,8 @@ import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
3
  import * as os from 'os';
4
4
  import type { Config, ConfigOptions, ConfigStore, Profile } from '../types/config';
5
- import {
6
- TOKEN_MASK_LENGTH,
7
- TOKEN_MIN_LENGTH,
8
- DEFAULT_PROFILE_NAME,
9
- ERROR_MESSAGES,
10
- FILE_PERMISSIONS,
11
- } from './constants';
5
+ import { DEFAULT_PROFILE_NAME, ERROR_MESSAGES, FILE_PERMISSIONS } from './constants';
6
+ import { maskToken } from './token-utils';
12
7
 
13
8
  export class ProfileConfigManager {
14
9
  private configPath: string;
@@ -98,14 +93,7 @@ export class ProfileConfigManager {
98
93
  }
99
94
 
100
95
  maskToken(token: string): string {
101
- if (token.length <= TOKEN_MIN_LENGTH) {
102
- return '****';
103
- }
104
-
105
- const prefix = token.substring(0, TOKEN_MASK_LENGTH);
106
- const suffix = token.substring(token.length - TOKEN_MASK_LENGTH);
107
-
108
- return `${prefix}-****-****-${suffix}`;
96
+ return maskToken(token);
109
97
  }
110
98
 
111
99
  private async getConfigStore(): Promise<ConfigStore> {
@@ -77,8 +77,12 @@ export class SlackApiClient {
77
77
  this.messageOps = new MessageOperations(token);
78
78
  }
79
79
 
80
- async sendMessage(channel: string, text: string): Promise<ChatPostMessageResponse> {
81
- return this.messageOps.sendMessage(channel, text);
80
+ async sendMessage(
81
+ channel: string,
82
+ text: string,
83
+ thread_ts?: string
84
+ ): Promise<ChatPostMessageResponse> {
85
+ return this.messageOps.sendMessage(channel, text, thread_ts);
82
86
  }
83
87
 
84
88
  async listChannels(options: ListChannelsOptions): Promise<Channel[]> {
@@ -2,6 +2,7 @@ import { BaseSlackClient } from './base-client';
2
2
  import { channelResolver } from '../channel-resolver';
3
3
  import { DEFAULTS } from '../constants';
4
4
  import { Channel, ListChannelsOptions } from '../slack-api-client';
5
+ import { WebClient } from '@slack/web-api';
5
6
 
6
7
  interface ChannelWithUnreadInfo extends Channel {
7
8
  unread_count: number;
@@ -10,6 +11,15 @@ interface ChannelWithUnreadInfo extends Channel {
10
11
  }
11
12
 
12
13
  export class ChannelOperations extends BaseSlackClient {
14
+ constructor(tokenOrClient: string | WebClient) {
15
+ if (typeof tokenOrClient === 'string') {
16
+ super(tokenOrClient);
17
+ } else {
18
+ super('dummy-token'); // Call parent constructor
19
+ this.client = tokenOrClient; // Override the client for testing
20
+ }
21
+ }
22
+
13
23
  async listChannels(options: ListChannelsOptions): Promise<Channel[]> {
14
24
  const channels: Channel[] = [];
15
25
  let cursor: string | undefined;
@@ -34,65 +44,15 @@ export class ChannelOperations extends BaseSlackClient {
34
44
  }
35
45
 
36
46
  async listUnreadChannels(): Promise<Channel[]> {
37
- // Get all conversations the user is a member of
38
- const response = await this.client.conversations.list({
39
- types: 'public_channel,private_channel,im,mpim',
40
- exclude_archived: true,
41
- limit: 1000,
42
- });
43
-
44
- const channels = response.channels as Channel[];
47
+ const channels = await this.fetchAllChannels();
45
48
  const channelsWithUnread: Channel[] = [];
46
49
 
47
50
  // Process channels one by one with delay to avoid rate limits
48
51
  for (const channel of channels) {
49
52
  try {
50
- const info = await this.client.conversations.info({
51
- channel: channel.id,
52
- include_num_members: false,
53
- });
54
- const channelInfo = info.channel as ChannelWithUnreadInfo;
55
-
56
- // Get the latest message in the channel
57
- const history = await this.client.conversations.history({
58
- channel: channel.id,
59
- limit: 1,
60
- });
61
-
62
- // Always check for messages after last_read timestamp
63
- if (channelInfo.last_read) {
64
- // Fetch messages after last_read
65
- const unreadHistory = await this.client.conversations.history({
66
- channel: channel.id,
67
- oldest: channelInfo.last_read,
68
- limit: 100, // Get up to 100 unread messages
69
- });
70
-
71
- const unreadCount = unreadHistory.messages?.length || 0;
72
- if (unreadCount > 0) {
73
- channelsWithUnread.push({
74
- ...channel,
75
- unread_count: unreadCount,
76
- unread_count_display: unreadCount,
77
- last_read: channelInfo.last_read,
78
- });
79
- }
80
- } else if (history.messages && history.messages.length > 0) {
81
- // If no last_read, all messages are unread
82
- const allHistory = await this.client.conversations.history({
83
- channel: channel.id,
84
- limit: 100,
85
- });
86
- const unreadCount = allHistory.messages?.length || 0;
87
-
88
- if (unreadCount > 0) {
89
- channelsWithUnread.push({
90
- ...channel,
91
- unread_count: unreadCount,
92
- unread_count_display: unreadCount,
93
- last_read: channelInfo.last_read,
94
- });
95
- }
53
+ const unreadInfo = await this.getChannelUnreadInfo(channel);
54
+ if (unreadInfo) {
55
+ channelsWithUnread.push(unreadInfo);
96
56
  }
97
57
 
98
58
  // Add delay between API calls to avoid rate limiting
@@ -106,6 +66,83 @@ export class ChannelOperations extends BaseSlackClient {
106
66
  return channelsWithUnread;
107
67
  }
108
68
 
69
+ private async fetchAllChannels(): Promise<Channel[]> {
70
+ const response = await this.client.conversations.list({
71
+ types: 'public_channel,private_channel,im,mpim',
72
+ exclude_archived: true,
73
+ limit: 1000,
74
+ });
75
+
76
+ return response.channels as Channel[];
77
+ }
78
+
79
+ private async getChannelUnreadInfo(channel: Channel): Promise<Channel | null> {
80
+ const channelInfo = await this.fetchChannelInfo(channel.id);
81
+ const unreadCount = await this.calculateUnreadCount(channel.id, channelInfo);
82
+
83
+ if (unreadCount > 0) {
84
+ return {
85
+ ...channel,
86
+ unread_count: unreadCount,
87
+ unread_count_display: unreadCount,
88
+ last_read: channelInfo.last_read,
89
+ };
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ private async fetchChannelInfo(channelId: string): Promise<ChannelWithUnreadInfo> {
96
+ const info = await this.client.conversations.info({
97
+ channel: channelId,
98
+ include_num_members: false,
99
+ });
100
+ return info.channel as ChannelWithUnreadInfo;
101
+ }
102
+
103
+ private async calculateUnreadCount(
104
+ channelId: string,
105
+ channelInfo: ChannelWithUnreadInfo
106
+ ): Promise<number> {
107
+ // Get the latest message to check if channel has any messages
108
+ const latestMessage = await this.fetchLatestMessage(channelId);
109
+ if (!latestMessage) {
110
+ return 0;
111
+ }
112
+
113
+ if (channelInfo.last_read) {
114
+ return await this.fetchUnreadMessageCount(channelId, channelInfo.last_read);
115
+ } else {
116
+ // If no last_read, all messages are unread
117
+ return await this.fetchAllMessageCount(channelId);
118
+ }
119
+ }
120
+
121
+ private async fetchLatestMessage(channelId: string): Promise<any> {
122
+ const history = await this.client.conversations.history({
123
+ channel: channelId,
124
+ limit: 1,
125
+ });
126
+ return history.messages && history.messages.length > 0 ? history.messages[0] : null;
127
+ }
128
+
129
+ private async fetchUnreadMessageCount(channelId: string, lastRead: string): Promise<number> {
130
+ const unreadHistory = await this.client.conversations.history({
131
+ channel: channelId,
132
+ oldest: lastRead,
133
+ limit: 100, // Get up to 100 unread messages
134
+ });
135
+ return unreadHistory.messages?.length || 0;
136
+ }
137
+
138
+ private async fetchAllMessageCount(channelId: string): Promise<number> {
139
+ const allHistory = await this.client.conversations.history({
140
+ channel: channelId,
141
+ limit: 100,
142
+ });
143
+ return allHistory.messages?.length || 0;
144
+ }
145
+
109
146
  async getChannelInfo(channelNameOrId: string): Promise<ChannelWithUnreadInfo> {
110
147
  const channelId = await channelResolver.resolveChannelId(channelNameOrId, () =>
111
148
  this.listChannels({
@@ -1,4 +1,4 @@
1
- import { ChatPostMessageResponse } from '@slack/web-api';
1
+ import { ChatPostMessageResponse, ChatPostMessageArguments } from '@slack/web-api';
2
2
  import { BaseSlackClient } from './base-client';
3
3
  import { channelResolver } from '../channel-resolver';
4
4
  import { DEFAULTS } from '../constants';
@@ -14,11 +14,21 @@ export class MessageOperations extends BaseSlackClient {
14
14
  this.channelOps = new ChannelOperations(token);
15
15
  }
16
16
 
17
- async sendMessage(channel: string, text: string): Promise<ChatPostMessageResponse> {
18
- return await this.client.chat.postMessage({
17
+ async sendMessage(
18
+ channel: string,
19
+ text: string,
20
+ thread_ts?: string
21
+ ): Promise<ChatPostMessageResponse> {
22
+ const params: ChatPostMessageArguments = {
19
23
  channel,
20
24
  text,
21
- });
25
+ };
26
+
27
+ if (thread_ts) {
28
+ params.thread_ts = thread_ts;
29
+ }
30
+
31
+ return await this.client.chat.postMessage(params);
22
32
  }
23
33
 
24
34
  async getHistory(channel: string, options: HistoryOptions): Promise<HistoryResult> {
@@ -0,0 +1,17 @@
1
+ import { TOKEN_MASK_LENGTH, TOKEN_MIN_LENGTH } from './constants';
2
+
3
+ /**
4
+ * Masks a token for display purposes, showing only first and last few characters
5
+ * @param token The token to mask
6
+ * @returns Masked token in format "xoxb-****-****-abcd"
7
+ */
8
+ export function maskToken(token: string): string {
9
+ if (token.length <= TOKEN_MIN_LENGTH) {
10
+ return '****';
11
+ }
12
+
13
+ const prefix = token.substring(0, TOKEN_MASK_LENGTH);
14
+ const suffix = token.substring(token.length - TOKEN_MASK_LENGTH);
15
+
16
+ return `${prefix}-****-****-${suffix}`;
17
+ }