@urugus/slack-cli 0.2.2 → 0.2.3
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/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/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/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/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/option-parsers.ts +100 -0
- package/src/utils/profile-config-refactored.ts +161 -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/src/commands/channels.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { ERROR_MESSAGES } from '../utils/constants';
|
|
|
5
5
|
import { ChannelsOptions } from '../types/commands';
|
|
6
6
|
import { mapChannelToInfo, getChannelTypes } from '../utils/channel-formatter';
|
|
7
7
|
import { createChannelsListFormatter } from '../utils/formatters/channels-list-formatters';
|
|
8
|
+
import { parseFormat, parseLimit, parseBoolean } from '../utils/option-parsers';
|
|
8
9
|
|
|
9
10
|
export function setupChannelsCommand(): Command {
|
|
10
11
|
const channelsCommand = new Command('channels');
|
|
@@ -25,10 +26,11 @@ export function setupChannelsCommand(): Command {
|
|
|
25
26
|
const types = getChannelTypes(options.type);
|
|
26
27
|
|
|
27
28
|
// List channels
|
|
29
|
+
const limit = parseLimit(options.limit, 100);
|
|
28
30
|
const channels = await client.listChannels({
|
|
29
31
|
types,
|
|
30
|
-
exclude_archived: !options.includeArchived,
|
|
31
|
-
limit:
|
|
32
|
+
exclude_archived: !parseBoolean(options.includeArchived),
|
|
33
|
+
limit: limit,
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
if (channels.length === 0) {
|
|
@@ -38,8 +40,9 @@ export function setupChannelsCommand(): Command {
|
|
|
38
40
|
|
|
39
41
|
// Format and display channels
|
|
40
42
|
const channelInfos = channels.map(mapChannelToInfo);
|
|
41
|
-
const
|
|
42
|
-
formatter
|
|
43
|
+
const format = parseFormat(options.format);
|
|
44
|
+
const formatter = createChannelsListFormatter(format);
|
|
45
|
+
formatter.format({ channels: channelInfos });
|
|
43
46
|
})
|
|
44
47
|
);
|
|
45
48
|
|
package/src/commands/unread.ts
CHANGED
|
@@ -4,34 +4,28 @@ import { createSlackClient } from '../utils/client-factory';
|
|
|
4
4
|
import { SlackApiClient } from '../utils/slack-api-client';
|
|
5
5
|
import { UnreadOptions } from '../types/commands';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import { formatSlackTimestamp } from '../utils/date-utils';
|
|
8
|
-
import { formatChannelName } from '../utils/channel-formatter';
|
|
9
7
|
import { createChannelFormatter } from '../utils/formatters/channel-formatters';
|
|
8
|
+
import { createMessageFormatter } from '../utils/formatters/message-formatters';
|
|
10
9
|
import { DEFAULTS } from '../utils/constants';
|
|
11
|
-
import {
|
|
10
|
+
import { parseLimit, parseFormat, parseBoolean } from '../utils/option-parsers';
|
|
12
11
|
|
|
13
12
|
async function handleSpecificChannelUnread(
|
|
14
13
|
client: SlackApiClient,
|
|
15
14
|
options: UnreadOptions
|
|
16
15
|
): Promise<void> {
|
|
17
16
|
const result = await client.getChannelUnread(options.channel!);
|
|
18
|
-
const channelName = formatChannelName(result.channel.name);
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
const format = parseFormat(options.format);
|
|
19
|
+
const countOnly = parseBoolean(options.countOnly);
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
result.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
: '(no text)';
|
|
31
|
-
console.log(text);
|
|
32
|
-
console.log('');
|
|
33
|
-
});
|
|
34
|
-
}
|
|
21
|
+
const formatter = createMessageFormatter(format);
|
|
22
|
+
formatter.format({
|
|
23
|
+
channel: result.channel,
|
|
24
|
+
messages: result.messages,
|
|
25
|
+
users: result.users,
|
|
26
|
+
countOnly: countOnly,
|
|
27
|
+
format: format,
|
|
28
|
+
});
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
async function handleAllChannelsUnread(
|
|
@@ -46,11 +40,14 @@ async function handleAllChannelsUnread(
|
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
// Apply limit
|
|
49
|
-
const limit =
|
|
43
|
+
const limit = parseLimit(options.limit, DEFAULTS.UNREAD_DISPLAY_LIMIT);
|
|
50
44
|
const displayChannels = channels.slice(0, limit);
|
|
51
45
|
|
|
52
|
-
const
|
|
53
|
-
|
|
46
|
+
const format = parseFormat(options.format);
|
|
47
|
+
const countOnly = parseBoolean(options.countOnly);
|
|
48
|
+
|
|
49
|
+
const formatter = createChannelFormatter(format, countOnly);
|
|
50
|
+
formatter.format({ channels: displayChannels, countOnly: countOnly });
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
export function setupUnreadCommand(): Command {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
export interface ConfigData {
|
|
6
|
+
profiles: Record<string, any>;
|
|
7
|
+
currentProfile: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ConfigFileManager {
|
|
11
|
+
private readonly configPath: string;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.configPath = path.join(os.homedir(), '.slack-cli', 'config.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async read(): Promise<ConfigData> {
|
|
18
|
+
try {
|
|
19
|
+
await fs.access(this.configPath);
|
|
20
|
+
const data = await fs.readFile(this.configPath, 'utf-8');
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(data);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error('Invalid configuration file');
|
|
25
|
+
}
|
|
26
|
+
} catch (error: any) {
|
|
27
|
+
if (error.message === 'Invalid configuration file') {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
// File doesn't exist, return default config
|
|
31
|
+
return {
|
|
32
|
+
profiles: {},
|
|
33
|
+
currentProfile: 'default',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async write(data: ConfigData): Promise<void> {
|
|
39
|
+
const dir = path.dirname(this.configPath);
|
|
40
|
+
await fs.mkdir(dir, { recursive: true });
|
|
41
|
+
await fs.writeFile(this.configPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async exists(): Promise<boolean> {
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(this.configPath);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getConfigPath(): string {
|
|
54
|
+
return this.configPath;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Config } from '../../types/config';
|
|
2
|
+
import { ConfigFileManager, ConfigData } from './config-file-manager';
|
|
3
|
+
import { TokenCryptoService } from './token-crypto-service';
|
|
4
|
+
|
|
5
|
+
export class ProfileManager {
|
|
6
|
+
constructor(
|
|
7
|
+
private fileManager: ConfigFileManager,
|
|
8
|
+
private cryptoService: TokenCryptoService
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
async getProfile(profileName: string): Promise<Config> {
|
|
12
|
+
const data = await this.fileManager.read();
|
|
13
|
+
const profile = data.profiles[profileName];
|
|
14
|
+
|
|
15
|
+
if (!profile) {
|
|
16
|
+
throw new Error(`Profile "${profileName}" not found`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Decrypt token if encrypted
|
|
20
|
+
const token = this.cryptoService.isEncrypted(profile.token)
|
|
21
|
+
? this.cryptoService.decrypt(profile.token)
|
|
22
|
+
: profile.token;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
...profile,
|
|
26
|
+
token,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async setProfile(profileName: string, config: Config): Promise<void> {
|
|
31
|
+
const data = await this.fileManager.read();
|
|
32
|
+
|
|
33
|
+
// Encrypt the token before saving
|
|
34
|
+
const encryptedConfig = {
|
|
35
|
+
...config,
|
|
36
|
+
token: this.cryptoService.encrypt(config.token),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
data.profiles[profileName] = encryptedConfig;
|
|
40
|
+
await this.fileManager.write(data);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async deleteProfile(profileName: string): Promise<void> {
|
|
44
|
+
const data = await this.fileManager.read();
|
|
45
|
+
|
|
46
|
+
if (!data.profiles[profileName]) {
|
|
47
|
+
throw new Error(`Profile "${profileName}" not found`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
delete data.profiles[profileName];
|
|
51
|
+
await this.fileManager.write(data);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async listProfiles(): Promise<string[]> {
|
|
55
|
+
const data = await this.fileManager.read();
|
|
56
|
+
return Object.keys(data.profiles);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getCurrentProfile(): Promise<string> {
|
|
60
|
+
const data = await this.fileManager.read();
|
|
61
|
+
return data.currentProfile || 'default';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async setCurrentProfile(profileName: string): Promise<void> {
|
|
65
|
+
const data = await this.fileManager.read();
|
|
66
|
+
|
|
67
|
+
if (!data.profiles[profileName]) {
|
|
68
|
+
throw new Error(`Profile "${profileName}" not found`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
data.currentProfile = profileName;
|
|
72
|
+
await this.fileManager.write(data);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async profileExists(profileName: string): Promise<boolean> {
|
|
76
|
+
const data = await this.fileManager.read();
|
|
77
|
+
return profileName in data.profiles;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export class TokenCryptoService {
|
|
4
|
+
private readonly algorithm = 'aes-256-cbc';
|
|
5
|
+
private readonly keyLength = 32;
|
|
6
|
+
private readonly ivLength = 16;
|
|
7
|
+
private readonly separator = ':';
|
|
8
|
+
|
|
9
|
+
private deriveKey(): Buffer {
|
|
10
|
+
// Derive a consistent key from a fixed string
|
|
11
|
+
// In production, this should use a more secure method
|
|
12
|
+
const fixedSalt = 'slack-cli-salt-v1';
|
|
13
|
+
return crypto.pbkdf2Sync('slack-cli-key', fixedSalt, 100000, this.keyLength, 'sha256');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
encrypt(token: string): string {
|
|
17
|
+
try {
|
|
18
|
+
const key = this.deriveKey();
|
|
19
|
+
const iv = crypto.randomBytes(this.ivLength);
|
|
20
|
+
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
|
|
21
|
+
|
|
22
|
+
let encrypted = cipher.update(token, 'utf8', 'hex');
|
|
23
|
+
encrypted += cipher.final('hex');
|
|
24
|
+
|
|
25
|
+
// Combine IV and encrypted data
|
|
26
|
+
return iv.toString('hex') + this.separator + encrypted;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error('Failed to encrypt token');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
decrypt(encryptedData: string): string {
|
|
33
|
+
try {
|
|
34
|
+
if (!encryptedData || !encryptedData.includes(this.separator)) {
|
|
35
|
+
throw new Error('Invalid encrypted data format');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parts = encryptedData.split(this.separator);
|
|
39
|
+
if (parts.length !== 2) {
|
|
40
|
+
throw new Error('Invalid encrypted data format');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
44
|
+
const encrypted = parts[1];
|
|
45
|
+
|
|
46
|
+
if (iv.length !== this.ivLength) {
|
|
47
|
+
throw new Error('Invalid IV length');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const key = this.deriveKey();
|
|
51
|
+
const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
|
|
52
|
+
|
|
53
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
54
|
+
decrypted += decipher.final('utf8');
|
|
55
|
+
|
|
56
|
+
return decrypted;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error('Failed to decrypt token');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
isEncrypted(value: string): boolean {
|
|
63
|
+
if (!value) return false;
|
|
64
|
+
|
|
65
|
+
// Check if the value has the expected format
|
|
66
|
+
const parts = value.split(this.separator);
|
|
67
|
+
if (parts.length !== 2) return false;
|
|
68
|
+
|
|
69
|
+
// Check if the IV part is valid hex and has correct length
|
|
70
|
+
try {
|
|
71
|
+
const ivHex = parts[0];
|
|
72
|
+
if (!/^[0-9a-fA-F]+$/.test(ivHex)) return false;
|
|
73
|
+
if (ivHex.length !== this.ivLength * 2) return false;
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface BaseFormatter<T> {
|
|
2
|
+
format(data: T): void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export abstract class AbstractFormatter<T> implements BaseFormatter<T> {
|
|
6
|
+
abstract format(data: T): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export abstract class JsonFormatter<TInput, TOutput = any> extends AbstractFormatter<TInput> {
|
|
10
|
+
protected abstract transform(data: TInput): TOutput;
|
|
11
|
+
|
|
12
|
+
format(data: TInput): void {
|
|
13
|
+
console.log(JSON.stringify(this.transform(data), null, 2));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FormatterMap<T> {
|
|
18
|
+
table: BaseFormatter<T>;
|
|
19
|
+
simple: BaseFormatter<T>;
|
|
20
|
+
json: BaseFormatter<T>;
|
|
21
|
+
[key: string]: BaseFormatter<T>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class FormatterFactory<T> {
|
|
25
|
+
constructor(private formatters: FormatterMap<T>) {}
|
|
26
|
+
|
|
27
|
+
create(format: string = 'table'): BaseFormatter<T> {
|
|
28
|
+
return this.formatters[format] || this.formatters.table;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createFormatterFactory<T>(formatters: FormatterMap<T>): FormatterFactory<T> {
|
|
33
|
+
return new FormatterFactory(formatters);
|
|
34
|
+
}
|
|
@@ -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
|
+
}
|