@nu-art/slack-backend 0.400.5

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.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Created by AlanBen on 29/08/2019.
3
+ */
4
+ import { Module } from '@nu-art/ts-common';
5
+ import { WebClientOptions } from '@slack/web-api';
6
+ import { PreSendSlackStructuredMessage } from '@nu-art/slack-shared';
7
+ import { Stream } from 'stream';
8
+ export type ConfigType_ModuleBE_Slack = {
9
+ token: string;
10
+ defaultChannel: string;
11
+ throttlingTime?: number;
12
+ slackConfig?: Partial<WebClientOptions>;
13
+ unfurl_links?: boolean;
14
+ unfurl_media?: boolean;
15
+ };
16
+ type _SlackMessage = {
17
+ text: string;
18
+ channel?: string;
19
+ messageId?: string;
20
+ };
21
+ export type SlackMessage = string | _SlackMessage;
22
+ export type ThreadPointer = {
23
+ ts?: string;
24
+ channel: string;
25
+ };
26
+ export declare class ModuleBE_Slack_Class extends Module<ConfigType_ModuleBE_Slack, any> {
27
+ private web;
28
+ private messageMap;
29
+ constructor();
30
+ protected init(): void;
31
+ postMessage(text: string, channel?: string, thread?: ThreadPointer): Promise<ThreadPointer | undefined>;
32
+ postFile(file: Buffer, name: string, thread?: ThreadPointer): Promise<import("@slack/web-api").FilesCompleteUploadExternalResponse>;
33
+ postStructuredMessage(message: PreSendSlackStructuredMessage, thread?: ThreadPointer): Promise<ThreadPointer | undefined>;
34
+ private postMessageImpl;
35
+ uploadFile: (file: Buffer | Stream, name: string, tp?: ThreadPointer) => Promise<import("@slack/web-api").FilesUploadResponse>;
36
+ getUserIdByEmail(email: string): Promise<string | undefined>;
37
+ openDM(userIds: string[]): Promise<string | undefined>;
38
+ getDefaultChannel: () => any;
39
+ }
40
+ export declare const ModuleBE_Slack: ModuleBE_Slack_Class;
41
+ export {};
@@ -0,0 +1,155 @@
1
+ /*
2
+ * Storm contains a list of utility functions.. this project
3
+ * might be broken down into more smaller projects in the future.
4
+ *
5
+ * Copyright (C) 2020 Adam van der Kruk aka TacB0sS
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ /**
20
+ * Created by AlanBen on 29/08/2019.
21
+ */
22
+ import { currentTimeMillis, generateHex, ImplementationMissingException, md5, Minute, Module } from '@nu-art/ts-common';
23
+ import { WebClient, } from '@slack/web-api';
24
+ import { addRoutes, AxiosHttpModule, createBodyServerApi } from '@nu-art/thunderstorm-backend';
25
+ import { ApiDef_Slack } from '@nu-art/slack-shared';
26
+ import { postSlackMessageErrorHandler } from './utils.js';
27
+ import { HttpCodes } from '@nu-art/ts-common/core/exceptions/http-codes';
28
+ import { SlackBuilderBE } from './SlackBuilderBE.js';
29
+ import { HttpMethod } from '@nu-art/thunderstorm-shared';
30
+ export class ModuleBE_Slack_Class extends Module {
31
+ web;
32
+ messageMap = {};
33
+ constructor() {
34
+ super('slack');
35
+ this.setDefaultConfig({ unfurl_links: false, unfurl_media: false });
36
+ }
37
+ init() {
38
+ if (!this.config.token)
39
+ throw new ImplementationMissingException('Missing config token for ModuleBE_Slack. Please add it');
40
+ this.web = new WebClient(this.config.token, {
41
+ rejectRateLimitedCalls: true,
42
+ ...this.config.slackConfig
43
+ });
44
+ addRoutes([
45
+ createBodyServerApi(ApiDef_Slack.vv1.postMessage, async (request) => {
46
+ await this.postMessage(request.message, request.channel);
47
+ }),
48
+ createBodyServerApi(ApiDef_Slack.vv1.postStructuredMessage, async (request) => {
49
+ return { threadPointer: await this.postStructuredMessage(request.message, request.thread) };
50
+ }),
51
+ createBodyServerApi(ApiDef_Slack.vv1.sendFEMessage, async (request) => {
52
+ const slackMessage = new SlackBuilderBE(request.channel, request.messageBlocks, request.messageReplies);
53
+ await slackMessage.send();
54
+ }),
55
+ createBodyServerApi(ApiDef_Slack.vv1.postFiles, async (request) => this.postFile(request.file, request.name, request.thread))
56
+ ]);
57
+ }
58
+ async postMessage(text, channel, thread) {
59
+ const message = {
60
+ text,
61
+ channel: channel ?? this.config.defaultChannel,
62
+ };
63
+ //Block same message on throttling time
64
+ const time = this.messageMap[md5(text)];
65
+ if (time && currentTimeMillis() - time < (this.config.throttlingTime || Minute))
66
+ return;
67
+ //Post and return thread
68
+ return await this.postMessageImpl(message, thread);
69
+ }
70
+ async postFile(file, name, thread) {
71
+ // Get a URL to upload
72
+ const uploadUrlResponse = await this.web.files.getUploadURLExternal({
73
+ filename: name,
74
+ length: file.length
75
+ });
76
+ if (!uploadUrlResponse.ok)
77
+ throw HttpCodes._5XX.INTERNAL_SERVER_ERROR(`Failed at getting a URL from slack: ${uploadUrlResponse.error}`);
78
+ const { upload_url, file_id } = uploadUrlResponse;
79
+ try {
80
+ await AxiosHttpModule.createRequest({
81
+ fullUrl: upload_url,
82
+ path: '',
83
+ method: HttpMethod.POST
84
+ })
85
+ .setBody(file)
86
+ .executeSync();
87
+ }
88
+ catch (e) {
89
+ throw HttpCodes._5XX.INTERNAL_SERVER_ERROR(`Failed at uploading file to url: ${e.message}`);
90
+ }
91
+ // Complete the upload - post the file to slack message
92
+ const completeResponse = await this.web.files.completeUploadExternal({
93
+ files: [{ id: file_id }],
94
+ channel_id: thread ? thread.channel : this.config.defaultChannel,
95
+ thread_ts: thread?.ts,
96
+ });
97
+ if (!completeResponse.ok)
98
+ throw HttpCodes._5XX.INTERNAL_SERVER_ERROR(`Failed at complete uploading: ${completeResponse.error}`);
99
+ return completeResponse;
100
+ }
101
+ async postStructuredMessage(message, thread) {
102
+ message.channel ??= this.config.defaultChannel;
103
+ message.text ??= generateHex(8);
104
+ const time = this.messageMap[message.text];
105
+ if (time && currentTimeMillis() - time < (this.config.throttlingTime || Minute))
106
+ return;
107
+ return await this.postMessageImpl(message, thread);
108
+ }
109
+ async postMessageImpl(message, threadPointer) {
110
+ try {
111
+ if (threadPointer) {
112
+ message.thread_ts = threadPointer.ts;
113
+ message.channel = threadPointer.channel;
114
+ }
115
+ message.unfurl_links = this.config.unfurl_links;
116
+ message.unfurl_media = this.config.unfurl_media;
117
+ this.logDebug(`Sending message in ${threadPointer ? 'thread' : 'channel'}`, message);
118
+ const res = await this.web.chat.postMessage(message);
119
+ //Add message to map
120
+ this.messageMap[md5(message.text)] = currentTimeMillis();
121
+ this.logDebug(`A message was posted to channel: ${message.channel} with message id ${res.ts} which contains the message ${message.text}`);
122
+ return { ts: res.ts, channel: res.channel };
123
+ }
124
+ catch (err) {
125
+ throw HttpCodes._5XX.INTERNAL_SERVER_ERROR(postSlackMessageErrorHandler(err, message.channel));
126
+ }
127
+ }
128
+ uploadFile = async (file, name, tp) => {
129
+ const channel = tp?.channel || this.config.defaultChannel;
130
+ const fileUploadBlob = {
131
+ channels: channel,
132
+ file: file,
133
+ filename: name,
134
+ };
135
+ return await this.web.files.upload(fileUploadBlob);
136
+ };
137
+ async getUserIdByEmail(email) {
138
+ const result = await this.web.users.lookupByEmail({ email });
139
+ if (result.ok)
140
+ // @ts-ignore
141
+ return result.user.id;
142
+ return undefined;
143
+ }
144
+ async openDM(userIds) {
145
+ const users = userIds.join(',');
146
+ const result = await this.web.conversations.open({ users });
147
+ if (result.ok) { // @ts-ignore
148
+ return result.channel.id;
149
+ }
150
+ }
151
+ getDefaultChannel = () => {
152
+ return this.config.defaultChannel;
153
+ };
154
+ }
155
+ export const ModuleBE_Slack = new ModuleBE_Slack_Class();
@@ -0,0 +1,9 @@
1
+ import { ThreadPointer } from './ModuleBE_Slack.js';
2
+ import { BaseSlackBuilder, SlackBlock } from '@nu-art/slack-shared';
3
+ export declare class SlackBuilderBE extends BaseSlackBuilder {
4
+ constructor(channel?: string, blocks?: SlackBlock[], replies?: SlackBlock[][]);
5
+ private convertLongSectionBlocks;
6
+ protected sendMessage: () => Promise<ThreadPointer | undefined>;
7
+ protected sendFiles: (tp: ThreadPointer) => Promise<void>;
8
+ protected sendReplies: (tp: ThreadPointer) => Promise<void>;
9
+ }
@@ -0,0 +1,53 @@
1
+ import { ModuleBE_Slack } from './ModuleBE_Slack.js';
2
+ import { BaseSlackBuilder } from '@nu-art/slack-shared';
3
+ import { __stringify, currentTimeMillis, formatTimestamp, generateHex } from '@nu-art/ts-common';
4
+ export class SlackBuilderBE extends BaseSlackBuilder {
5
+ // ######################## Builder Steps ########################
6
+ constructor(channel, blocks, replies) {
7
+ super(channel, blocks, replies);
8
+ }
9
+ // ######################## Internal Logic ########################
10
+ convertLongSectionBlocks = () => {
11
+ const convertBlock = (block) => {
12
+ if (block.type !== 'section')
13
+ return;
14
+ //@ts-ignore - text does exist on the block at this point
15
+ const text = block.text.text;
16
+ if (text.length < 3000)
17
+ return;
18
+ //Convert the text into a file
19
+ const fileName = `${formatTimestamp('DD/MM/YYYY_HH:mm', currentTimeMillis())}_${generateHex(8)}`;
20
+ const buffer = Buffer.from(__stringify(text, true), 'utf-8');
21
+ this.addFiles({
22
+ title: fileName,
23
+ fileName: fileName,
24
+ file: buffer
25
+ });
26
+ // @ts-ignore
27
+ block.text.text = `Message was too long, converted to file "${fileName}"`;
28
+ };
29
+ this.blocks.forEach(convertBlock);
30
+ this.replies.forEach(reply => reply.forEach(convertBlock));
31
+ };
32
+ sendMessage = async () => {
33
+ this.convertLongSectionBlocks();
34
+ return ModuleBE_Slack.postStructuredMessage({
35
+ channel: this.channel,
36
+ blocks: this.blocks
37
+ });
38
+ };
39
+ sendFiles = async (tp) => {
40
+ await Promise.all(this.files.map(async (file) => {
41
+ const response = await ModuleBE_Slack.postFile(file.file, file.fileName, tp);
42
+ if (!response.ok)
43
+ return this.logError(response?.error);
44
+ }));
45
+ };
46
+ sendReplies = async (tp) => {
47
+ for (const reply of this.replies) {
48
+ await ModuleBE_Slack.postStructuredMessage({
49
+ blocks: reply
50
+ }, tp);
51
+ }
52
+ };
53
+ }
@@ -0,0 +1,14 @@
1
+ import { CustomException, ErrorMessage, Module, ServerErrorSeverity } from '@nu-art/ts-common';
2
+ type Config = {
3
+ exclude: string[];
4
+ minLevel: ServerErrorSeverity;
5
+ };
6
+ export declare class Slack_ServerApiError_Class extends Module<Config> {
7
+ constructor();
8
+ protected init(): void;
9
+ __processApplicationError(errorLevel: ServerErrorSeverity, module: Module, message: ErrorMessage): Promise<void>;
10
+ __processExceptionError(errorLevel: ServerErrorSeverity, exception: CustomException): Promise<void>;
11
+ private sendMessage;
12
+ }
13
+ export declare const Slack_ServerApiError: Slack_ServerApiError_Class;
14
+ export {};
@@ -0,0 +1,80 @@
1
+ /*
2
+ * Storm contains a list of utility functions.. this project
3
+ * might be broken down into more smaller projects in the future.
4
+ *
5
+ * Copyright (C) 2020 Adam van der Kruk aka TacB0sS
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ import { Module, ServerErrorSeverity, ServerErrorSeverity_Ordinal } from '@nu-art/ts-common';
20
+ import { ModuleBE_Slack } from './ModuleBE_Slack.js';
21
+ export class Slack_ServerApiError_Class extends Module {
22
+ constructor() {
23
+ super();
24
+ this.setDefaultConfig({ exclude: [], minLevel: ServerErrorSeverity.Info });
25
+ }
26
+ init() {
27
+ }
28
+ async __processApplicationError(errorLevel, module, message) {
29
+ if (ServerErrorSeverity_Ordinal.indexOf(errorLevel) < ServerErrorSeverity_Ordinal.indexOf(this.config.minLevel))
30
+ return;
31
+ const threadPointer = await this.sendMessage(message.message);
32
+ if (!threadPointer)
33
+ return;
34
+ for (const innerMessage of (message.innerMessages || [])) {
35
+ await this.sendMessage(innerMessage, threadPointer);
36
+ }
37
+ }
38
+ // public composeSlackStructuredMessage = (exception: CustomException, channel?: string): ChatPostMessageArguments => {
39
+ // let dataMessage = `No message composer defined for type ${exception.exceptionType}`;
40
+ //
41
+ // if (exception.isInstanceOf(ApiException))
42
+ // dataMessage = Composer_ApiException(exception as ApiException);
43
+ // else if (exception.isInstanceOf(BadImplementationException))
44
+ // dataMessage = Composer_BadImplementationException(exception);
45
+ // else if (exception.isInstanceOf(ThisShouldNotHappenException))
46
+ // dataMessage = Composer_ThisShouldNotHappenException(exception);
47
+ //
48
+ // return {
49
+ // text: Composer_NotificationText(exception),
50
+ // channel: channel!,
51
+ // blocks: SlackBuilder_TextSectionWithTitle(':octagonal_sign: *API Error*', dataMessage)
52
+ // };
53
+ // };
54
+ async __processExceptionError(errorLevel, exception) {
55
+ if (ServerErrorSeverity_Ordinal.indexOf(errorLevel) < ServerErrorSeverity_Ordinal.indexOf(this.config.minLevel))
56
+ return;
57
+ // const message = this.composeSlackStructuredMessage(exception);
58
+ // const thread = await ModuleBE_Slack.postStructuredMessage(message);
59
+ // if (!thread)
60
+ // return;
61
+ //Send a full stack reply in thread
62
+ // const stackSection: ChatPostMessageArguments = {
63
+ // blocks: [
64
+ // SlackBuilder_TextSection(''),
65
+ // SlackBuilder_Divider(),
66
+ // SlackBuilder_TextSection(`\`\`\`${exception.stack}\`\`\``),
67
+ // ]
68
+ // } as ChatPostMessageArguments;
69
+ // if (stackSection)
70
+ // await ModuleBE_Slack.postStructuredMessage(stackSection, thread);
71
+ }
72
+ sendMessage(message, threadPointer) {
73
+ for (const key of this.config.exclude || []) {
74
+ if (message.includes(key))
75
+ return;
76
+ }
77
+ return ModuleBE_Slack.postMessage(message, undefined, threadPointer);
78
+ }
79
+ }
80
+ export const Slack_ServerApiError = new Slack_ServerApiError_Class();
package/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './ModuleBE_Slack.js';
2
+ export * from './Slack_ServerApiError.js';
3
+ export * from './SlackBuilderBE.js';
package/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Storm contains a list of utility functions.. this project
3
+ * might be broken down into more smaller projects in the future.
4
+ *
5
+ * Copyright (C) 2020 Adam van der Kruk aka TacB0sS
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ export * from './ModuleBE_Slack.js';
20
+ export * from './Slack_ServerApiError.js';
21
+ export * from './SlackBuilderBE.js';
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@nu-art/slack-backend",
3
+ "version": "0.400.5",
4
+ "description": "Storm - Express & Typescript based backend framework Backend",
5
+ "keywords": [
6
+ "TacB0sS",
7
+ "infra",
8
+ "nu-art",
9
+ "storm",
10
+ "thunderstorm",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://github.com/nu-art-js/storm",
14
+ "bugs": {
15
+ "url": "https://github.com/nu-art-js/storm/issues"
16
+ },
17
+ "publishConfig": {
18
+ "directory": "dist",
19
+ "linkDirectory": true
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+ssh://git@github.com:nu-art-js/storm.git"
24
+ },
25
+ "license": "Apache-2.0",
26
+ "author": "TacB0sS",
27
+ "files": [
28
+ "**/*"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc"
32
+ },
33
+ "dependencies": {
34
+ "@nu-art/slack-shared": "0.400.5",
35
+ "@nu-art/thunderstorm-backend": "0.400.5",
36
+ "@nu-art/thunderstorm-shared": "0.400.5",
37
+ "@nu-art/ts-common": "0.400.5",
38
+ "@slack/events-api": "^2.3.0",
39
+ "@slack/web-api": "7.9.3",
40
+ "firebase": "^11.9.0",
41
+ "firebase-admin": "13.4.0",
42
+ "firebase-functions": "6.3.2",
43
+ "google-auth-library": "^10.0.0",
44
+ "moment": "^2.29.4",
45
+ "react": "^18.0.0",
46
+ "react-dom": "^18.0.0",
47
+ "react-router-dom": "^6.9.0",
48
+ "request": "^2.88.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/react": "^18.0.0",
52
+ "@types/react-dom": "^18.0.0",
53
+ "@types/react-router": "^5.1.20",
54
+ "@types/react-router-dom": "^5.3.3"
55
+ },
56
+ "unitConfig": {
57
+ "type": "typescript-lib"
58
+ },
59
+ "type": "module",
60
+ "exports": {
61
+ ".": {
62
+ "types": "./index.d.ts",
63
+ "import": "./index.js"
64
+ },
65
+ "./*": {
66
+ "types": "./*.d.ts",
67
+ "import": "./*.js"
68
+ }
69
+ }
70
+ }
package/utils.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Receives slack error and returns a human-readable error message
3
+ * @param error The error received from the api call
4
+ */
5
+ export declare const postSlackMessageErrorHandler: (error: any, channel?: string) => string;
package/utils.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Receives slack error and returns a human-readable error message
3
+ * @param error The error received from the api call
4
+ */
5
+ export const postSlackMessageErrorHandler = (error, channel) => {
6
+ const prefix = 'Slack error! ';
7
+ if (error.data && error.data.error) {
8
+ switch (error.data.error) {
9
+ case 'channel_not_found':
10
+ return `${prefix}The specified channel (Channel '${channel}') does not exist or is not accessible by your access token. Please double check defined channel in config and adjust if needed`;
11
+ case 'not_in_channel':
12
+ return `${prefix}The bot or user is not a member of the specified channel. (Channel '${channel}')`;
13
+ case 'is_archived':
14
+ return `${prefix}The channel has been archived and cannot be written to. (Channel '${channel}')`;
15
+ case 'msg_too_long':
16
+ return `${prefix}The message text exceeds the maximum allowed length.`;
17
+ case 'no_text':
18
+ return `${prefix}The message text was provided empty.`;
19
+ case 'rate_limited':
20
+ return `${prefix}Too many messages have been sent in a short period. Please try again later.`;
21
+ case 'not_authed':
22
+ return `${prefix}No authentication token was provided. Please provide a valid token.`;
23
+ case 'invalid_auth':
24
+ return `${prefix}The authentication token provided is invalid. Please check your token settings.`;
25
+ case 'access_denied':
26
+ return `${prefix}Access has been denied. The token does not have the necessary permissions to perform this operation.`;
27
+ case 'account_inactive':
28
+ return `${prefix}The user account associated with the token is inactive.`;
29
+ case 'token_revoked':
30
+ return `${prefix}The token has been revoked.`;
31
+ case 'no_permission':
32
+ return `${prefix}The token does not have the necessary permission to post messages in the specified channel. (Channel '${channel}')`;
33
+ case 'user_is_bot':
34
+ return `${prefix}The user associated with the token is a bot, which cannot post messages.`;
35
+ case 'team_access_not_granted':
36
+ return `${prefix}The application has not been granted access to post messages in the specified team. (Channel '${channel}')`;
37
+ default:
38
+ return `${prefix}An unexpected error occurred: ${error.data.error}`;
39
+ }
40
+ }
41
+ else {
42
+ return `${prefix}An unknown error occurred. Please check your network and try again.`;
43
+ }
44
+ };