Skip to content

Commit

Permalink
#2823 slack service comments and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
chpy04 committed Dec 20, 2024
1 parent 35e2a27 commit e80337e
Show file tree
Hide file tree
Showing 6 changed files with 591 additions and 60 deletions.
42 changes: 0 additions & 42 deletions src/backend/src/controllers/slack.controllers.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/backend/src/services/announcement.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default class AnnouncementService {
return announcementTransformer(announcement);
}

static async UpdateAnnouncement(
static async updateAnnouncement(
text: string,
usersReceivedIds: string[],
dateCreated: Date,
Expand Down Expand Up @@ -70,7 +70,7 @@ export default class AnnouncementService {
return announcementTransformer(announcement);
}

static async DeleteAnnouncement(slackEventId: string, organizationId: string): Promise<Announcement> {
static async deleteAnnouncement(slackEventId: string, organizationId: string): Promise<Announcement> {
const announcement = await prisma.announcement.update({
where: { slackEventId },
data: {
Expand Down
109 changes: 93 additions & 16 deletions src/backend/src/services/slack.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@ import { User_Settings } from '@prisma/client';
import AnnouncementService from './announcement.service';
import { Announcement } from 'shared';

/**
* Represents a slack event for a message in a channel.
*/
export interface SlackMessageEvent {
type: 'message';
subtype?: string;
channel: string;
event_ts: string;
channel_type: string;
[key: string]: any;
}

/**
* Represents a slack message event for a standard sent message.
*/
export interface SlackMessage extends SlackMessageEvent {
user: string;
type: 'message';
client_msg_id: string;
text: string;
blocks: {
Expand All @@ -24,17 +30,27 @@ export interface SlackMessage extends SlackMessageEvent {
}[];
}

/**
* Represents a slack message event for a deleted message.
*/
export interface SlackDeletedMessage extends SlackMessageEvent {
subtype: 'message_deleted';
previous_message: SlackMessage;
}

/**
* Represents a slack message event for an edited message.
*/
export interface SlackUpdatedMessage extends SlackMessageEvent {
subtype: 'message_changed';
message: SlackMessage;
previous_message: SlackMessage;
}

/**
* Represents a block of information within a message. These blocks with an array
* make up all the information needed to represent the content of a message.
*/
export interface SlackRichTextBlock {
type: 'broadcast' | 'color' | 'channel' | 'date' | 'emoji' | 'link' | 'text' | 'user' | 'usergroup';
range?: string;
Expand All @@ -50,13 +66,19 @@ export interface SlackRichTextBlock {
}

export default class slackServices {
/**
* Converts a SlackRichTextBlock into a string representation for an announcement.
* @param block the block of information from slack
* @returns the string that will be combined with other block's strings to create the announcement
*/
private static async blockToString(block: SlackRichTextBlock): Promise<string> {
switch (block.type) {
case 'broadcast':
return '@' + block.range;
case 'color':
return block.value ?? '';
case 'channel':
//channels are represented as an id, get the name from the slack api
let channelName = block.channel_id;
try {
channelName = await getChannelName(block.channel_id ?? '');
Expand All @@ -67,8 +89,10 @@ export default class slackServices {
case 'date':
return new Date(block.timestamp ?? 0).toISOString();
case 'emoji':
//if the emoji is a unicode emoji, convert the unicode to a string,
//if it is a slack emoji just use the name of the emoji
if (block.unicode) {
return String.fromCharCode(parseInt(block.unicode, 16));
return String.fromCodePoint(parseInt(block.unicode, 16));
}
return 'emoji:' + block.name;
case 'link':
Expand All @@ -79,6 +103,7 @@ export default class slackServices {
case 'text':
return block.text ?? '';
case 'user':
//users are represented as an id, get the name of the user from the slack api
let userName: string = block.user_id ?? '';
try {
userName = (await getUserName(block.user_id ?? '')) ?? `Unknown User:${block.user_id}`;
Expand All @@ -91,6 +116,13 @@ export default class slackServices {
}
}

/**
* Gets the users notified in a specific SlackRichTextBlock.
* @param block the block that may contain mentioned user/users
* @param usersSettings the settings of all the users in prisma
* @param channelId the id of the channel that the block is being sent in
* @returns an array of prisma user ids of users to be notified
*/
private static async blockToMentionedUsers(
block: SlackRichTextBlock,
usersSettings: User_Settings[],
Expand All @@ -103,7 +135,11 @@ export default class slackServices {
return usersSettings.map((usersSettings) => usersSettings.userId);
case 'channel':
case 'here':
const slackIds = await getUsersInChannel(channelId);
//@here behaves the same as @channel; notifies all the users in that channel
let slackIds: string[] = [];
try {
slackIds = await getUsersInChannel(channelId);
} catch (ignored) {}
return usersSettings
.filter((userSettings) => {
return slackIds.some((slackId) => slackId === userSettings.slackId);
Expand All @@ -117,22 +153,38 @@ export default class slackServices {
.filter((userSettings) => userSettings.slackId === block.user_id)
.map((userSettings) => userSettings.userId);
default:
//only broadcasts and specific user mentions add recievers to announcements
return [];
}
}

/**
* Given a slack event representing a message in a channel,
* make the appropriate announcement change in prisma.
* @param event the slack event that will be processed
* @param organizationId the id of the organization represented by the slack api
* @returns an annoucement if an announcement was processed and created/modified/deleted
*/
static async processMessageSent(event: SlackMessageEvent, organizationId: string): Promise<Announcement | undefined> {
const slackChannelName = await getChannelName(event.channel);
let slackChannelName: string;
//get the name of the channel from the slack api
try {
slackChannelName = await getChannelName(event.channel);
} catch (error) {
slackChannelName = `Unknown_Channel:${event.channel}`;
}
const dateCreated = new Date(Number(event.event_ts));

//get the message that will be processed either as the event or within a subtype
let eventMessage: SlackMessage;

if (event.subtype) {
switch (event.subtype) {
case 'message_deleted':
//delete the message using the client_msg_id
eventMessage = (event as SlackDeletedMessage).previous_message;
try {
return AnnouncementService.DeleteAnnouncement(eventMessage.client_msg_id, organizationId);
return AnnouncementService.deleteAnnouncement(eventMessage.client_msg_id, organizationId);
} catch (ignored) {
return;
}
Expand All @@ -147,32 +199,41 @@ export default class slackServices {
eventMessage = event as SlackMessage;
}

//loop through the blocks of the meta data while accumulating the
//text and users notified
let messageText = '';
let userIdsToNotify: string[] = [];

//Get the settings of all users in this organization to compare slack ids
const users = await UsersService.getAllUsers();
const userSettings = await Promise.all(
users.map((user) => {
return UsersService.getUserSettings(user.userId);
})
);

//get the name of the user that sent the message from slack
let userName: string = '';
try {
userName = (await getUserName(eventMessage.user)) ?? '';
} catch (ignored) {}

//if slack could not produce the name of the user, look for their name in prisma
if (!userName) {
const userIdList = userSettings
.filter((userSetting) => userSetting.slackId === eventMessage.user)
.map((userSettings) => userSettings.userId);
if (userIdList.length !== 0) {
userName = users.find((user) => user.userId === userIdList[0])?.firstName ?? 'Unknown User:' + eventMessage.user;
const prismaUserName = users.find((user) => user.userId === userIdList[0]);
userName = prismaUserName
? `${prismaUserName?.firstName} ${prismaUserName?.lastName}`
: 'Unknown User:' + eventMessage.user;
} else {
userName = 'Unknown_User:' + eventMessage.user;
}
}

//pull out the blocks of data from the metadata within the message event
const richTextBlocks = eventMessage.blocks?.filter((eventBlock: any) => eventBlock.type === 'rich_text');

if (richTextBlocks && richTextBlocks.length === 1) {
Expand All @@ -186,9 +247,20 @@ export default class slackServices {
return;
}

//get rid of duplicates within the users to notify
userIdsToNotify = [...new Set(userIdsToNotify)];

//if no users are notified, disregard the message
if (userIdsToNotify.length === 0) {
return;
}

console.log('processed event');

if (event.subtype === 'message_changed') {
//try to edit the announcement, if no announcement with that id exists create a new announcement
try {
return AnnouncementService.UpdateAnnouncement(
return await AnnouncementService.updateAnnouncement(
messageText,
userIdsToNotify,
dateCreated,
Expand All @@ -199,14 +271,19 @@ export default class slackServices {
);
} catch (ignored) {}
}
return AnnouncementService.createAnnouncement(
messageText,
userIdsToNotify,
dateCreated,
userName,
eventMessage.client_msg_id,
slackChannelName,
organizationId
);
try {
return await AnnouncementService.createAnnouncement(
messageText,
userIdsToNotify,
dateCreated,
userName,
eventMessage.client_msg_id,
slackChannelName,
organizationId
);
} catch (error) {
//if announcement does not have unique cient_msg_id disregard it
return;
}
}
}
Loading

0 comments on commit e80337e

Please sign in to comment.