@urugus/slack-cli 0.2.2 → 0.2.4
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 +2 -1
- package/dist/commands/channels.d.ts.map +1 -1
- package/dist/commands/channels.js +7 -4
- package/dist/commands/channels.js.map +1 -1
- package/dist/commands/unread.d.ts.map +1 -1
- package/dist/commands/unread.js +17 -21
- package/dist/commands/unread.js.map +1 -1
- package/dist/utils/config/config-file-manager.d.ts +13 -0
- package/dist/utils/config/config-file-manager.d.ts.map +1 -0
- package/dist/utils/config/config-file-manager.js +85 -0
- package/dist/utils/config/config-file-manager.js.map +1 -0
- package/dist/utils/config/profile-manager.d.ts +16 -0
- package/dist/utils/config/profile-manager.d.ts.map +1 -0
- package/dist/utils/config/profile-manager.js +64 -0
- package/dist/utils/config/profile-manager.js.map +1 -0
- package/dist/utils/config/token-crypto-service.d.ts +11 -0
- package/dist/utils/config/token-crypto-service.d.ts.map +1 -0
- package/dist/utils/config/token-crypto-service.js +111 -0
- package/dist/utils/config/token-crypto-service.js.map +1 -0
- package/dist/utils/format-utils.d.ts.map +1 -1
- package/dist/utils/format-utils.js +2 -1
- package/dist/utils/format-utils.js.map +1 -1
- package/dist/utils/formatters/base-formatter.d.ts +23 -0
- package/dist/utils/formatters/base-formatter.d.ts.map +1 -0
- package/dist/utils/formatters/base-formatter.js +26 -0
- package/dist/utils/formatters/base-formatter.js.map +1 -0
- package/dist/utils/formatters/channel-formatters.d.ts +4 -13
- package/dist/utils/formatters/channel-formatters.d.ts.map +1 -1
- package/dist/utils/formatters/channel-formatters.js +18 -26
- package/dist/utils/formatters/channel-formatters.js.map +1 -1
- package/dist/utils/formatters/channels-list-formatters.d.ts +3 -10
- package/dist/utils/formatters/channels-list-formatters.d.ts.map +1 -1
- package/dist/utils/formatters/channels-list-formatters.js +15 -22
- package/dist/utils/formatters/channels-list-formatters.js.map +1 -1
- package/dist/utils/formatters/message-formatters.d.ts +9 -0
- package/dist/utils/formatters/message-formatters.d.ts.map +1 -0
- package/dist/utils/formatters/message-formatters.js +72 -0
- package/dist/utils/formatters/message-formatters.js.map +1 -0
- package/dist/utils/mention-utils.d.ts +17 -0
- package/dist/utils/mention-utils.d.ts.map +1 -0
- package/dist/utils/mention-utils.js +45 -0
- package/dist/utils/mention-utils.js.map +1 -0
- package/dist/utils/option-parsers.d.ts +47 -0
- package/dist/utils/option-parsers.d.ts.map +1 -0
- package/dist/utils/option-parsers.js +75 -0
- package/dist/utils/option-parsers.js.map +1 -0
- package/dist/utils/profile-config-refactored.d.ts +20 -0
- package/dist/utils/profile-config-refactored.d.ts.map +1 -0
- package/dist/utils/profile-config-refactored.js +174 -0
- package/dist/utils/profile-config-refactored.js.map +1 -0
- package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/message-operations.js +3 -2
- package/dist/utils/slack-operations/message-operations.js.map +1 -1
- package/dist/utils/slack-patterns.d.ts +6 -0
- package/dist/utils/slack-patterns.d.ts.map +1 -0
- package/dist/utils/slack-patterns.js +11 -0
- package/dist/utils/slack-patterns.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/channels.ts +7 -4
- package/src/commands/unread.ts +18 -21
- package/src/utils/config/config-file-manager.ts +56 -0
- package/src/utils/config/profile-manager.ts +79 -0
- package/src/utils/config/token-crypto-service.ts +80 -0
- package/src/utils/format-utils.ts +3 -1
- package/src/utils/formatters/base-formatter.ts +34 -0
- package/src/utils/formatters/channel-formatters.ts +25 -23
- package/src/utils/formatters/channels-list-formatters.ts +27 -31
- package/src/utils/formatters/message-formatters.ts +85 -0
- package/src/utils/mention-utils.ts +47 -0
- package/src/utils/option-parsers.ts +100 -0
- package/src/utils/profile-config-refactored.ts +161 -0
- package/src/utils/slack-operations/message-operations.ts +3 -2
- package/src/utils/slack-patterns.ts +9 -0
- package/tests/commands/unread.test.ts +112 -0
- package/tests/utils/config/config-file-manager.test.ts +118 -0
- package/tests/utils/config/profile-manager.test.ts +266 -0
- package/tests/utils/config/token-crypto-service.test.ts +98 -0
- package/tests/utils/mention-utils.test.ts +100 -0
- package/tests/utils/slack-operations/message-operations.test.ts +126 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
2
|
+
import { AbstractFormatter, JsonFormatter, createFormatterFactory } from './base-formatter';
|
|
3
3
|
import { Channel } from '../slack-api-client';
|
|
4
4
|
import { formatChannelName } from '../channel-formatter';
|
|
5
5
|
import { formatSlackTimestamp } from '../date-utils';
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
7
|
+
export interface ChannelFormatterOptions {
|
|
8
|
+
channels: Channel[];
|
|
9
|
+
countOnly?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class ChannelTableFormatter extends AbstractFormatter<ChannelFormatterOptions> {
|
|
13
|
+
format({ channels }: ChannelFormatterOptions): void {
|
|
9
14
|
console.log(chalk.bold('Channel Unread Last Message'));
|
|
10
15
|
console.log('─'.repeat(50));
|
|
11
16
|
|
|
@@ -19,8 +24,8 @@ export class ChannelTableFormatter extends BaseFormatter<Channel> {
|
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
format(channels:
|
|
27
|
+
class ChannelSimpleFormatter extends AbstractFormatter<ChannelFormatterOptions> {
|
|
28
|
+
format({ channels }: ChannelFormatterOptions): void {
|
|
24
29
|
channels.forEach((channel) => {
|
|
25
30
|
const channelName = formatChannelName(channel.name);
|
|
26
31
|
console.log(`${channelName} (${channel.unread_count || 0})`);
|
|
@@ -28,19 +33,18 @@ export class ChannelSimpleFormatter extends BaseFormatter<Channel> {
|
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
class ChannelJsonFormatter extends JsonFormatter<ChannelFormatterOptions> {
|
|
37
|
+
protected transform({ channels }: ChannelFormatterOptions) {
|
|
38
|
+
return channels.map((channel) => ({
|
|
34
39
|
channel: formatChannelName(channel.name),
|
|
35
40
|
channelId: channel.id,
|
|
36
41
|
unreadCount: channel.unread_count || 0,
|
|
37
42
|
}));
|
|
38
|
-
console.log(JSON.stringify(output, null, 2));
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
format(channels:
|
|
46
|
+
class ChannelCountFormatter extends AbstractFormatter<ChannelFormatterOptions> {
|
|
47
|
+
format({ channels }: ChannelFormatterOptions): void {
|
|
44
48
|
let totalUnread = 0;
|
|
45
49
|
channels.forEach((channel) => {
|
|
46
50
|
const count = channel.unread_count || 0;
|
|
@@ -52,18 +56,16 @@ export class ChannelCountFormatter extends BaseFormatter<Channel> {
|
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const channelFormatterFactory = createFormatterFactory<ChannelFormatterOptions>({
|
|
60
|
+
table: new ChannelTableFormatter(),
|
|
61
|
+
simple: new ChannelSimpleFormatter(),
|
|
62
|
+
json: new ChannelJsonFormatter(),
|
|
63
|
+
count: new ChannelCountFormatter(),
|
|
64
|
+
});
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
case 'simple':
|
|
64
|
-
return new ChannelSimpleFormatter();
|
|
65
|
-
case 'table':
|
|
66
|
-
default:
|
|
67
|
-
return new ChannelTableFormatter();
|
|
66
|
+
export function createChannelFormatter(format: string, countOnly: boolean) {
|
|
67
|
+
if (countOnly) {
|
|
68
|
+
return channelFormatterFactory.create('count');
|
|
68
69
|
}
|
|
70
|
+
return channelFormatterFactory.create(format);
|
|
69
71
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AbstractFormatter, JsonFormatter, createFormatterFactory } from './base-formatter';
|
|
2
2
|
import { ChannelInfo } from '../channel-formatter';
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
4
|
+
export interface ChannelsListFormatterOptions {
|
|
5
|
+
channels: ChannelInfo[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class ChannelsTableFormatter extends AbstractFormatter<ChannelsListFormatterOptions> {
|
|
9
|
+
format({ channels }: ChannelsListFormatterOptions): void {
|
|
6
10
|
// Print table header
|
|
7
11
|
console.log('Name Type Members Created Description');
|
|
8
12
|
console.log('─'.repeat(65));
|
|
@@ -21,39 +25,31 @@ export class ChannelsTableFormatter extends BaseFormatter<ChannelInfo> {
|
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
format(channels:
|
|
28
|
+
class ChannelsSimpleFormatter extends AbstractFormatter<ChannelsListFormatterOptions> {
|
|
29
|
+
format({ channels }: ChannelsListFormatterOptions): void {
|
|
26
30
|
channels.forEach((channel) => console.log(channel.name));
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
purpose: channel.purpose,
|
|
41
|
-
})),
|
|
42
|
-
null,
|
|
43
|
-
2
|
|
44
|
-
)
|
|
45
|
-
);
|
|
34
|
+
class ChannelsJsonFormatter extends JsonFormatter<ChannelsListFormatterOptions> {
|
|
35
|
+
protected transform({ channels }: ChannelsListFormatterOptions) {
|
|
36
|
+
return channels.map((channel) => ({
|
|
37
|
+
id: channel.id,
|
|
38
|
+
name: channel.name,
|
|
39
|
+
type: channel.type,
|
|
40
|
+
members: channel.members,
|
|
41
|
+
created: channel.created + 'T00:00:00Z',
|
|
42
|
+
purpose: channel.purpose,
|
|
43
|
+
}));
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return new ChannelsTableFormatter();
|
|
58
|
-
}
|
|
47
|
+
const channelsListFormatterFactory = createFormatterFactory<ChannelsListFormatterOptions>({
|
|
48
|
+
table: new ChannelsTableFormatter(),
|
|
49
|
+
simple: new ChannelsSimpleFormatter(),
|
|
50
|
+
json: new ChannelsJsonFormatter(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export function createChannelsListFormatter(format: string) {
|
|
54
|
+
return channelsListFormatterFactory.create(format);
|
|
59
55
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { AbstractFormatter, JsonFormatter, createFormatterFactory } from './base-formatter';
|
|
3
|
+
import { formatSlackTimestamp } from '../date-utils';
|
|
4
|
+
import { formatMessageWithMentions } from '../format-utils';
|
|
5
|
+
import { formatChannelName } from '../channel-formatter';
|
|
6
|
+
|
|
7
|
+
export interface MessageFormatterOptions {
|
|
8
|
+
channel: any;
|
|
9
|
+
messages: any[];
|
|
10
|
+
users: Map<string, string>;
|
|
11
|
+
countOnly: boolean;
|
|
12
|
+
format: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class TableMessageFormatter extends AbstractFormatter<MessageFormatterOptions> {
|
|
16
|
+
format(options: MessageFormatterOptions): void {
|
|
17
|
+
const { channel, messages, users, countOnly } = options;
|
|
18
|
+
const channelName = formatChannelName(channel.name);
|
|
19
|
+
|
|
20
|
+
console.log(chalk.bold(`${channelName}: ${channel.unread_count || 0} unread messages`));
|
|
21
|
+
|
|
22
|
+
if (!countOnly && messages.length > 0) {
|
|
23
|
+
console.log('');
|
|
24
|
+
messages.forEach((message) => {
|
|
25
|
+
const timestamp = formatSlackTimestamp(message.ts);
|
|
26
|
+
const author = message.user ? users.get(message.user) || message.user : 'unknown';
|
|
27
|
+
console.log(`${chalk.gray(timestamp)} ${chalk.cyan(author)}`);
|
|
28
|
+
const text = message.text ? formatMessageWithMentions(message.text, users) : '(no text)';
|
|
29
|
+
console.log(text);
|
|
30
|
+
console.log('');
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class SimpleMessageFormatter extends AbstractFormatter<MessageFormatterOptions> {
|
|
37
|
+
format(options: MessageFormatterOptions): void {
|
|
38
|
+
const { channel, messages, users, countOnly } = options;
|
|
39
|
+
const channelName = formatChannelName(channel.name);
|
|
40
|
+
|
|
41
|
+
console.log(`${channelName} (${channel.unread_count || 0})`);
|
|
42
|
+
|
|
43
|
+
if (!countOnly && messages.length > 0) {
|
|
44
|
+
messages.forEach((message) => {
|
|
45
|
+
const timestamp = formatSlackTimestamp(message.ts);
|
|
46
|
+
const author = message.user ? users.get(message.user) || message.user : 'unknown';
|
|
47
|
+
const text = message.text ? formatMessageWithMentions(message.text, users) : '(no text)';
|
|
48
|
+
console.log(`[${timestamp}] ${author}: ${text}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class JsonMessageFormatter extends JsonFormatter<MessageFormatterOptions> {
|
|
55
|
+
protected transform(options: MessageFormatterOptions) {
|
|
56
|
+
const { channel, messages, users, countOnly } = options;
|
|
57
|
+
const channelName = formatChannelName(channel.name);
|
|
58
|
+
|
|
59
|
+
const output: any = {
|
|
60
|
+
channel: channelName,
|
|
61
|
+
channelId: channel.id,
|
|
62
|
+
unreadCount: channel.unread_count || 0,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (!countOnly && messages.length > 0) {
|
|
66
|
+
output.messages = messages.map((message) => ({
|
|
67
|
+
timestamp: formatSlackTimestamp(message.ts),
|
|
68
|
+
author: message.user ? users.get(message.user) || message.user : 'unknown',
|
|
69
|
+
text: message.text || '(no text)',
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return output;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const messageFormatterFactory = createFormatterFactory<MessageFormatterOptions>({
|
|
78
|
+
table: new TableMessageFormatter(),
|
|
79
|
+
simple: new SimpleMessageFormatter(),
|
|
80
|
+
json: new JsonMessageFormatter(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export function createMessageFormatter(format: string) {
|
|
84
|
+
return messageFormatterFactory.create(format);
|
|
85
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { USER_MENTION_PATTERN } from './slack-patterns';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts all user IDs from mentions in a text
|
|
5
|
+
* @param text - The text containing Slack mentions
|
|
6
|
+
* @returns Array of unique user IDs found in mentions
|
|
7
|
+
*/
|
|
8
|
+
export function extractUserIdsFromMentions(text: string): string[] {
|
|
9
|
+
const userIds: string[] = [];
|
|
10
|
+
const matches = text.matchAll(USER_MENTION_PATTERN);
|
|
11
|
+
|
|
12
|
+
for (const match of matches) {
|
|
13
|
+
const userId = match[1];
|
|
14
|
+
if (userId) {
|
|
15
|
+
userIds.push(userId);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return userIds;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extracts all unique user IDs from an array of messages
|
|
24
|
+
* Includes both message authors and mentioned users
|
|
25
|
+
* @param messages - Array of messages to extract user IDs from
|
|
26
|
+
* @returns Array of unique user IDs
|
|
27
|
+
*/
|
|
28
|
+
export function extractAllUserIds(messages: Array<{ user?: string; text?: string }>): string[] {
|
|
29
|
+
const userIds = new Set<string>();
|
|
30
|
+
|
|
31
|
+
for (const message of messages) {
|
|
32
|
+
// Add message author
|
|
33
|
+
if (message.user) {
|
|
34
|
+
userIds.add(message.user);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Add mentioned users
|
|
38
|
+
if (message.text) {
|
|
39
|
+
const mentionedIds = extractUserIdsFromMentions(message.text);
|
|
40
|
+
for (const id of mentionedIds) {
|
|
41
|
+
userIds.add(id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Array.from(userIds);
|
|
47
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common option parsing utilities to reduce duplication
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse format option with default value
|
|
7
|
+
*/
|
|
8
|
+
export function parseFormat(format?: string, defaultFormat = 'table'): string {
|
|
9
|
+
return format || defaultFormat;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse limit option with default value
|
|
14
|
+
*/
|
|
15
|
+
export function parseLimit(limit: string | undefined, defaultLimit: number): number {
|
|
16
|
+
return parseInt(limit || defaultLimit.toString(), 10);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse boolean option with default value
|
|
21
|
+
*/
|
|
22
|
+
export function parseBoolean(value?: boolean, defaultValue = false): boolean {
|
|
23
|
+
return value !== undefined ? value : defaultValue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse count option with default value
|
|
28
|
+
*/
|
|
29
|
+
export function parseCount(
|
|
30
|
+
count: string | undefined,
|
|
31
|
+
defaultCount: number,
|
|
32
|
+
min?: number,
|
|
33
|
+
max?: number
|
|
34
|
+
): number {
|
|
35
|
+
const parsed = parseInt(count || defaultCount.toString(), 10);
|
|
36
|
+
|
|
37
|
+
if (isNaN(parsed)) {
|
|
38
|
+
return defaultCount;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (min !== undefined && parsed < min) {
|
|
42
|
+
return min;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (max !== undefined && parsed > max) {
|
|
46
|
+
return max;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse profile option
|
|
54
|
+
*/
|
|
55
|
+
export function parseProfile(profile?: string): string | undefined {
|
|
56
|
+
return profile;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Common option defaults
|
|
61
|
+
*/
|
|
62
|
+
export const OPTION_DEFAULTS = {
|
|
63
|
+
format: 'table',
|
|
64
|
+
limit: 100,
|
|
65
|
+
countOnly: false,
|
|
66
|
+
includeArchived: false,
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse common list options
|
|
71
|
+
*/
|
|
72
|
+
export interface ListOptions {
|
|
73
|
+
format?: string;
|
|
74
|
+
limit?: string;
|
|
75
|
+
countOnly?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ParsedListOptions {
|
|
79
|
+
format: string;
|
|
80
|
+
limit: number;
|
|
81
|
+
countOnly: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseListOptions(
|
|
85
|
+
options: ListOptions,
|
|
86
|
+
defaults?: Partial<ParsedListOptions>
|
|
87
|
+
): ParsedListOptions {
|
|
88
|
+
const mergedDefaults = {
|
|
89
|
+
format: OPTION_DEFAULTS.format,
|
|
90
|
+
limit: OPTION_DEFAULTS.limit,
|
|
91
|
+
countOnly: OPTION_DEFAULTS.countOnly,
|
|
92
|
+
...defaults,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
format: parseFormat(options.format, mergedDefaults.format),
|
|
97
|
+
limit: parseLimit(options.limit, mergedDefaults.limit),
|
|
98
|
+
countOnly: parseBoolean(options.countOnly, mergedDefaults.countOnly),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { Config, ConfigOptions, Profile } from '../types/config';
|
|
2
|
+
import { TOKEN_MASK_LENGTH, TOKEN_MIN_LENGTH, DEFAULT_PROFILE_NAME } from './constants';
|
|
3
|
+
import { ConfigFileManager } from './config/config-file-manager';
|
|
4
|
+
import { TokenCryptoService } from './config/token-crypto-service';
|
|
5
|
+
import { ProfileManager } from './config/profile-manager';
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
|
|
8
|
+
export class ProfileConfigManager {
|
|
9
|
+
private fileManager: ConfigFileManager;
|
|
10
|
+
private cryptoService: TokenCryptoService;
|
|
11
|
+
private profileManager: ProfileManager;
|
|
12
|
+
|
|
13
|
+
constructor(_options: ConfigOptions = {}) {
|
|
14
|
+
// Note: ConfigFileManager currently doesn't support custom configDir
|
|
15
|
+
// This would need to be added if required
|
|
16
|
+
this.fileManager = new ConfigFileManager();
|
|
17
|
+
this.cryptoService = new TokenCryptoService();
|
|
18
|
+
this.profileManager = new ProfileManager(this.fileManager, this.cryptoService);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async setToken(token: string, profile?: string): Promise<void> {
|
|
22
|
+
const profileName = profile || (await this.profileManager.getCurrentProfile());
|
|
23
|
+
const config: Config = {
|
|
24
|
+
token,
|
|
25
|
+
updatedAt: new Date().toISOString(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
await this.profileManager.setProfile(profileName, config);
|
|
29
|
+
|
|
30
|
+
// Set as default profile if it's the first one or explicitly setting default
|
|
31
|
+
const profiles = await this.profileManager.listProfiles();
|
|
32
|
+
if (profiles.length === 1 || profileName === DEFAULT_PROFILE_NAME) {
|
|
33
|
+
await this.profileManager.setCurrentProfile(profileName);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getConfig(profile?: string): Promise<Config | null> {
|
|
38
|
+
const profileName = profile || (await this.profileManager.getCurrentProfile());
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
return await this.profileManager.getProfile(profileName);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// Return null if profile not found
|
|
44
|
+
if (error instanceof Error && error.message.includes('not found')) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async listProfiles(): Promise<Profile[]> {
|
|
52
|
+
const profileNames = await this.profileManager.listProfiles();
|
|
53
|
+
const currentProfile = await this.profileManager.getCurrentProfile();
|
|
54
|
+
|
|
55
|
+
const profiles: Profile[] = [];
|
|
56
|
+
for (const name of profileNames) {
|
|
57
|
+
const config = await this.profileManager.getProfile(name);
|
|
58
|
+
profiles.push({
|
|
59
|
+
name,
|
|
60
|
+
config,
|
|
61
|
+
isDefault: name === currentProfile,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return profiles;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async useProfile(profile: string): Promise<void> {
|
|
69
|
+
const exists = await this.profileManager.profileExists(profile);
|
|
70
|
+
if (!exists) {
|
|
71
|
+
throw new Error(`Profile "${profile}" does not exist`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await this.profileManager.setCurrentProfile(profile);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getCurrentProfile(): Promise<string> {
|
|
78
|
+
return await this.profileManager.getCurrentProfile();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async clearConfig(profile?: string): Promise<void> {
|
|
82
|
+
const profileName = profile || (await this.profileManager.getCurrentProfile());
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await this.profileManager.deleteProfile(profileName);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// If profile doesn't exist, do nothing
|
|
88
|
+
if (error instanceof Error && error.message.includes('not found')) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If we deleted the current profile, set a new default
|
|
95
|
+
const currentProfile = await this.profileManager.getCurrentProfile();
|
|
96
|
+
if (currentProfile === profileName) {
|
|
97
|
+
const remainingProfiles = await this.profileManager.listProfiles();
|
|
98
|
+
if (remainingProfiles.length > 0) {
|
|
99
|
+
await this.profileManager.setCurrentProfile(remainingProfiles[0]);
|
|
100
|
+
} else {
|
|
101
|
+
// No profiles left, delete the config file
|
|
102
|
+
try {
|
|
103
|
+
await fs.unlink(this.fileManager.getConfigPath());
|
|
104
|
+
} catch (error: unknown) {
|
|
105
|
+
if (error && typeof error === 'object' && 'code' in error && error.code !== 'ENOENT') {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
maskToken(token: string): string {
|
|
114
|
+
if (token.length <= TOKEN_MIN_LENGTH) {
|
|
115
|
+
return '****';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const prefix = token.substring(0, TOKEN_MASK_LENGTH);
|
|
119
|
+
const suffix = token.substring(token.length - TOKEN_MASK_LENGTH);
|
|
120
|
+
|
|
121
|
+
return `${prefix}-****-****-${suffix}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Migration support - to be called separately if needed
|
|
125
|
+
async migrateIfNeeded(): Promise<void> {
|
|
126
|
+
const data = await this.fileManager.read();
|
|
127
|
+
|
|
128
|
+
// Check if migration is needed (old format detection)
|
|
129
|
+
const anyData = data as unknown as Record<string, unknown>;
|
|
130
|
+
if (anyData.token && !anyData.profiles) {
|
|
131
|
+
// Old format detected, migrate
|
|
132
|
+
const oldConfig: Config = {
|
|
133
|
+
token: anyData.token as string,
|
|
134
|
+
updatedAt: (anyData.updatedAt as string) || new Date().toISOString(),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Create new format
|
|
138
|
+
const newData = {
|
|
139
|
+
profiles: { [DEFAULT_PROFILE_NAME]: oldConfig },
|
|
140
|
+
currentProfile: DEFAULT_PROFILE_NAME,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
await this.fileManager.write(newData);
|
|
144
|
+
|
|
145
|
+
// Re-encrypt token using new service
|
|
146
|
+
await this.setToken(oldConfig.token, DEFAULT_PROFILE_NAME);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Export a simplified version for backward compatibility
|
|
152
|
+
export const profileConfig = {
|
|
153
|
+
getCurrentProfile: (): string => {
|
|
154
|
+
return DEFAULT_PROFILE_NAME;
|
|
155
|
+
},
|
|
156
|
+
getToken: (_profile?: string): string | undefined => {
|
|
157
|
+
// This is a simplified version for testing
|
|
158
|
+
// In real usage, it would need to be async
|
|
159
|
+
return undefined;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
@@ -4,6 +4,7 @@ import { channelResolver } from '../channel-resolver';
|
|
|
4
4
|
import { DEFAULTS } from '../constants';
|
|
5
5
|
import { Message, HistoryOptions, HistoryResult, ChannelUnreadResult } from '../slack-api-client';
|
|
6
6
|
import { ChannelOperations } from './channel-operations';
|
|
7
|
+
import { extractAllUserIds } from '../mention-utils';
|
|
7
8
|
|
|
8
9
|
export class MessageOperations extends BaseSlackClient {
|
|
9
10
|
private channelOps: ChannelOperations;
|
|
@@ -38,8 +39,8 @@ export class MessageOperations extends BaseSlackClient {
|
|
|
38
39
|
|
|
39
40
|
const messages = response.messages as Message[];
|
|
40
41
|
|
|
41
|
-
//
|
|
42
|
-
const userIds =
|
|
42
|
+
// Extract all unique user IDs (authors and mentioned users)
|
|
43
|
+
const userIds = extractAllUserIds(messages);
|
|
43
44
|
const users = await this.fetchUserInfo(userIds);
|
|
44
45
|
|
|
45
46
|
return { messages, users };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common regex patterns for Slack message parsing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Matches Slack user mentions in the format <@USERID>
|
|
6
|
+
export const USER_MENTION_PATTERN = /<@([A-Z0-9]+)>/g;
|
|
7
|
+
|
|
8
|
+
// Matches a single user mention (non-global)
|
|
9
|
+
export const SINGLE_USER_MENTION_PATTERN = /<@([A-Z0-9]+)>/;
|