Skip to content

Commit

Permalink
feat: elevated slack client
Browse files Browse the repository at this point in the history
  • Loading branch information
solaris007 committed Feb 1, 2024
1 parent 209de00 commit 22b327b
Show file tree
Hide file tree
Showing 9 changed files with 366 additions and 175 deletions.
91 changes: 83 additions & 8 deletions packages/spacecat-shared-slack-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,38 @@ To configure tokens for your application, follow these steps:

4. For information on the scopes needed for each Slack API method, refer to the [documentation](https://api.slack.com/methods). The required scopes for each API method are listed in the "Bot tokens" row. As an example, to use the `postMessage` API method, the required scope is `chat:write`, as documented in [https://api.slack.com/methods/chat.postMessage](https://api.slack.com/methods/chat.postMessage).

### Scopes required for the current implementation
All Bot Tokens:
```
chat:write
files:read
files:write
team:read
```

Scopes needed for elevated Bot:
```
channels:manage (for public channels)
channels:read (check if user is in a channel or a channel exists)
channels:write.invites
channels:write.topic
groups:read (check if user is in a channel or a channel exists)
groups:write (for private channels)
groups:write.invites
groups:write.topic
users:read (to lookup users, required by users:read.email)
users:read.email (to lookup users by their emails)
```

### Creating and instance from Helix UniversalContext

```js
import createFrom from '@adobe/spacecat-shared-slack-client';

const context = {}; // Your Helix UniversalContext object
const target = 'ADOBE_INTERNAL';
const slackClient = SlackClient.createFrom(context, target);
const isElevated = false; // optional, defaults to false
const slackClient = createFrom(context, target, isElevated);
```

**Required env variables in Helix UniversalContext**
Expand All @@ -40,28 +65,78 @@ SLACK_TOKEN_ADOBE_INTERNAL="slack bot token for the adobe internal org"
SLACK_TOKEN_ADOBE_EXTERNAL="slack bot token for the adobe external org"
```

Additionally, when using the elevated slack client, the following environment variables are required:

```
SLACK_TOKEN_ADOBE_INTERNAL_ELEVATED="slack bot token for the adobe internal org"
SLACK_TOKEN_ADOBE_EXTERNAL_ELEVATED="slack bot token for the adobe external org"
SLACK_OPS_CHANNEL_ADOBE_INTERNAL="slack channel id for the ops channel to which status and action required messages are sent"
SLACK_OPS_CHANNEL_ADOBE_EXTERNAL="slack channel id for the ops channel to which status and action required messages are sent"
SLACK_OPS_ADMINS_ADOBE_INTERNAL="comma separated list of slack user ids who are invited to created channels"
SLACK_OPS_ADMINS_ADOBE_EXTERNAL="comma separated list of slack user ids who are invited to created channels"
```

**Note**: if Helix UniversalContext object already contains a `slackClients` field, then `createFrom` factory method returns the previously created instance instead of creating a new one.

### Constructor

`SlackClient` class needs a slack bot token and a logger object:
`ElevatedSlackClient` or `BaseSlackClient` need a slack bot token, an ops config and a logger object:

```js
const token = 'slack bot token';
const slackClient = new SlackClient(token, console);
const opsConfig = {
channel: 'mandatory slack channel id for the ops channel to which status and action required messages are sent',
admins: 'optional comma separated list of slack user ids who are invited to created channels',
};
const slackClient = new SlackClient(token, opsConfig, console);
```

### Channel Creation && Invitation

#### Creating a channel

```js
import createFrom, { SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';

const elevatedClient = createFrom(context, SLACK_TARGETS.ADOBE_EXTERNAL, true);
const channel = await elevatedClient.createChannel(
channelName,
'This is a test topic',
'This is a test description',
false, // public vs private channel
);
```

#### Inviting a user to a channel

```js
import createFrom, { SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';

const elevatedClient = createFrom(context, SLACK_TARGETS.ADOBE_EXTERNAL, true);

const result = await elevatedClient.inviteUsersByEmail(channel.getId(), [
{
email: '[email protected]',
realName: 'User 1',
},
{
email: '[email protected]',
realName: 'User 2',
},
]);
```

### Posting a message

#### Posting a text message

```js
import { SlackClient, SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';
import createFrom, { SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';

const channelId = 'channel-id'; // channel to send the message to
const threadId = 'thread-id'; // thread id to send the message under (optional)

const internalSlackClient = SlackClient.createFrom(context, SLACK_TARGETS.ADOBE_INTERNAL);
const internalSlackClient = createFrom(context, SLACK_TARGETS.ADOBE_INTERNAL);

await internalSlackClient.postMessage({
text: 'HELLO WORLD!',
Expand All @@ -73,7 +148,7 @@ await internalSlackClient.postMessage({
#### Posting a simple text message using Slack Block Builder (recommended)

```js
import { SlackClient, SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';
import createFrom, { SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';
import { Message, Blocks, Elements } from 'slack-block-builder';

const channelId = 'channel-id'; // channel to send the message to
Expand All @@ -96,7 +171,7 @@ await internalSlackClient.postMessage(message);
#### Posting a non-trivial message using Slack Block Builder (recommended)

```js
import { SlackClient, SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';
import createFrom, { SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';
import { Message, Blocks, Elements } from 'slack-block-builder';

const channelId = 'channel-id'; // channel to send the message to
Expand Down Expand Up @@ -136,7 +211,7 @@ await internalSlackClient.postMessage(message);
### Uploading a file

```js
import { SlackClient, SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';
import createFrom, { SLACK_TARGETS } from '@adobe/spacecat-shared-slack-client';

const channelId = 'channel-id'; // channel to send the message to
const threadId = 'thread-id'; // thread id to send the message under (optional)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default class ElevatedSlackClient extends BaseSlackClient {
const response = await this._apiCall('team.info');
return SlackTeam.create(response.team);
} catch (e) {
this.log.error('Failed to retrieve workspace information', e);
this.log.error('Failed to retrieve team information', e);
throw e;
}
}
Expand All @@ -133,7 +133,6 @@ export default class ElevatedSlackClient extends BaseSlackClient {
*/
async #initialize() {
if (this.isInitialized) {
this.log.debug('Slack client already initialized');
return;
}

Expand Down Expand Up @@ -183,12 +182,6 @@ export default class ElevatedSlackClient extends BaseSlackClient {
* or null if no user with the specified email address was found.
*/
async #findUserByEmail(email) {
if (!hasText(email)) {
throw new Error('Email is required');
}

await this.#initialize();

try {
const response = await this._apiCall('users.lookupByEmail', { email });
return SlackUser.create(response.user);
Expand All @@ -210,12 +203,6 @@ export default class ElevatedSlackClient extends BaseSlackClient {
* @returns {Promise<Array<SlackChannel>>} A promise resolving to an array of Channel objects.
*/
async #getUserChannels(userId) {
if (!hasText(userId)) {
throw new Error('User ID is required');
}

await this.#initialize();

let channels = [];
let cursor = '';
do {
Expand Down Expand Up @@ -253,7 +240,7 @@ export default class ElevatedSlackClient extends BaseSlackClient {
.then((status) => ({ email: user.email, status }))
.catch((error) => ({
email: user.email,
status: 'Failed to invite',
status: SLACK_STATUSES.GENERAL_ERROR,
error,
})));

Expand Down Expand Up @@ -316,11 +303,6 @@ export default class ElevatedSlackClient extends BaseSlackClient {
* @return {Promise<void>} A promise that resolves when the message is posted.
*/
async #postMessageToOpsChannel(message) {
if (!hasText(this.opsConfig.opsChannelId)) {
this.log.warn('No ops channel configured, cannot post message');
return;
}

try {
const result = await this.postMessage({
channel: this.opsConfig.opsChannelId,
Expand Down
29 changes: 0 additions & 29 deletions packages/spacecat-shared-slack-client/src/models/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,6 @@ export interface SlackChannel {
* @returns {string} The channel's name.
*/
getName(): string,

/**
* Checks if the channel is public.
* @returns {boolean} True if the channel is public, false otherwise.
*/
isPublic(): boolean,

/**
* Checks if the channel is archived.
* @returns {boolean} True if the channel is archived, false otherwise.
*/
isArchived(): boolean,
}

export interface SlackTeam {
Expand All @@ -53,23 +41,6 @@ export interface SlackTeam {
* @returns {string} The team's name.
*/
getName(): string,

/**
* Retrieves the URL of the team.
* @returns {string} The team's URL.
*/
getURL(): string,

/**
* Retrieves the domain of the team.
* @returns {string} The team's domain.
*/
getDomain(): string,

/**
* Indicates whether the team is an enterprise grid.
*/
isEnterprise(): boolean,
}

/**
Expand Down
12 changes: 0 additions & 12 deletions packages/spacecat-shared-slack-client/src/models/slack-channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,11 @@ export default class SlackChannel {
* @param {object} channelData - channel data
* @param {string} channelData.id - channel id
* @param {string} channelData.name - channel name
* @param {boolean} channelData.is_private - is channel private
* @param {boolean} channelData.is_archived - is channel archived
* @constructor
*/
constructor(channelData) {
this.id = channelData.id;
this.name = channelData.name;
this.is_private = channelData.is_private;
this.is_archived = channelData.is_archived;
}

static create(channelData) {
Expand All @@ -42,12 +38,4 @@ export default class SlackChannel {
getName() {
return this.name;
}

isPublic() {
return !this.is_private;
}

isArchived() {
return this.is_archived;
}
}
17 changes: 0 additions & 17 deletions packages/spacecat-shared-slack-client/src/models/slack-team.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
* governing permissions and limitations under the License.
*/

import { hasText } from '@adobe/spacecat-shared-utils';

/**
* Represents a Slack team
*/
Expand All @@ -22,9 +20,6 @@ export default class SlackTeam {
* @param {object} teamData - team data
* @param {string} teamData.id - team id
* @param {string} teamData.name - team name
* @param {string} teamData.url - team url
* @param {string} teamData.domain - team domain
* @param {string} teamData.enterprise_id - team enterprise id
* @constructor
*/
constructor(teamData) {
Expand All @@ -46,16 +41,4 @@ export default class SlackTeam {
getName() {
return this.name;
}

getURL() {
return this.url;
}

getDomain() {
return this.domain;
}

isEnterprise() {
return hasText(this.enterpriseId);
}
}
33 changes: 0 additions & 33 deletions packages/spacecat-shared-slack-client/src/models/slack-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,8 @@ export default class SlackUser {
*/
constructor(userData) {
this.id = userData.id;
this.teamId = userData.team_id;
this.name = userData.name;
this.realName = userData.profile?.real_name;
this.email = userData.profile?.email;
this.isAdmin = userData.is_admin;
this.isOwner = userData.is_owner;
this.isBot = userData.is_bot;
this.isRestricted = userData.is_restricted;
this.isUltraRestricted = userData.is_ultra_restricted;
}
Expand All @@ -53,38 +48,10 @@ export default class SlackUser {
return this.id;
}

getTeamId() {
return this.teamId;
}

getHandle() {
return this.name;
}

getRealName() {
return this.realName;
}

getEmail() {
return this.email;
}

isAdminUser() {
return this.isAdmin;
}

isOwnerUser() {
return this.isOwner;
}

isBotUser() {
return this.isBot;
}

isMultiChannelGuestUser() {
return this.isRestricted && !this.isUltraRestricted;
}

isSingleChannelGuestUser() {
return this.isRestricted && this.isUltraRestricted;
}
Expand Down
Loading

0 comments on commit 22b327b

Please sign in to comment.