Skip to content

Commit

Permalink
Merge pull request #3089 from Northeastern-Electric-Racing/#3044-slac…
Browse files Browse the repository at this point in the history
…k-endpoint-listener

#3044 slack endpoint listener
  • Loading branch information
Peyton-McKee authored Jan 2, 2025
2 parents ecc4548 + 1d118e0 commit 5a6990e
Show file tree
Hide file tree
Showing 26 changed files with 1,278 additions and 26 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@types/react-dom": "17.0.1"
},
"dependencies": {
"@slack/events-api": "^3.0.1",
"mitt": "^3.0.1",
"react-hook-form-persist": "^3.0.0",
"typescript": "^4.1.5"
Expand Down
5 changes: 5 additions & 0 deletions src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import workPackageTemplatesRouter from './src/routes/work-package-templates.rout
import carsRouter from './src/routes/cars.routes';
import organizationRouter from './src/routes/organizations.routes';
import recruitmentRouter from './src/routes/recruitment.routes';
import { slackEvents } from './src/routes/slack.routes';
import announcementsRouter from './src/routes/announcements.routes';
import onboardingRouter from './src/routes/onboarding.routes';
import popUpsRouter from './src/routes/pop-up.routes';
Expand All @@ -43,6 +44,10 @@ const options: cors.CorsOptions = {
allowedHeaders
};

// so we can listen to slack messages
// NOTE: must be done before using json
app.use('/slack', slackEvents.requestListener());

// so that we can use cookies and json
app.use(cookieParser());
app.use(express.json());
Expand Down
18 changes: 18 additions & 0 deletions src/backend/src/controllers/slack.controllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getWorkspaceId } from '../integrations/slack';
import OrganizationsService from '../services/organizations.services';
import SlackServices from '../services/slack.services';

export default class SlackController {
static async processMessageEvent(event: any) {
try {
const organizations = await OrganizationsService.getAllOrganizations();
const nerSlackWorkspaceId = await getWorkspaceId();
const relatedOrganization = organizations.find((org) => org.slackWorkspaceId === nerSlackWorkspaceId);
if (relatedOrganization) {
SlackServices.processMessageSent(event, relatedOrganization.organizationId);
}
} catch (error: unknown) {
console.log(error);
}
}
}
75 changes: 75 additions & 0 deletions src/backend/src/integrations/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,79 @@ const generateSlackTextBlock = (message: string, link?: string, linkButtonText?:
};
};

/**
* Given an id of a channel, produces the slack ids of all the users in that channel.
* @param channelId the id of the channel
* @returns an array of strings of all the slack ids of the users in the given channel
*/
export const getUsersInChannel = async (channelId: string) => {
let members: string[] = [];
let cursor: string | undefined;

try {
do {
const response = await slack.conversations.members({
channel: channelId,
cursor,
limit: 200
});

if (response.ok && response.members) {
members = members.concat(response.members);
cursor = response.response_metadata?.next_cursor;
} else {
throw new Error(`Failed to fetch members: ${response.error}`);
}
} while (cursor);

return members;
} catch (error) {
return members;
}
};

/**
* Given a slack channel id, produces the name of the channel
* @param channelId the id of the slack channel
* @returns the name of the channel or undefined if it cannot be found
*/
export const getChannelName = async (channelId: string) => {
try {
const channelRes = await slack.conversations.info({ channel: channelId });
return channelRes.channel?.name;
} catch (error) {
return undefined;
}
};

/**
* Given a slack user id, prood.uces the name of the channel
* @param userId the id of the slack user
* @returns the name of the user (real name if no display name), undefined if cannot be found
*/
export const getUserName = async (userId: string) => {
try {
const userRes = await slack.users.info({ user: userId });
return userRes.user?.profile?.display_name || userRes.user?.real_name;
} catch (error) {
return undefined;
}
};

/**
* Get the workspace id of the workspace this slack api is registered with
* @returns the id of the workspace
*/
export const getWorkspaceId = async () => {
try {
const response = await slack.auth.test();
if (response.ok) {
return response.team_id;
}
throw new Error(response.error);
} catch (error) {
throw new HttpException(500, 'Error getting slack workspace id: ' + (error as any).data.error);
}
};

export default slack;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ CREATE TABLE "Announcement" (
"senderName" TEXT NOT NULL,
"slackEventId" TEXT NOT NULL,
"slackChannelName" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,

CONSTRAINT "Announcement_pkey" PRIMARY KEY ("announcementId")
);
Expand All @@ -24,6 +25,7 @@ CREATE TABLE "PopUp" (
"text" TEXT NOT NULL,
"iconName" TEXT NOT NULL,
"eventLink" TEXT,
"organizationId" TEXT NOT NULL,

CONSTRAINT "PopUp_pkey" PRIMARY KEY ("popUpId")
);
Expand Down Expand Up @@ -58,6 +60,12 @@ CREATE INDEX "_userPopUps_B_index" ON "_userPopUps"("B");
-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Announcement" ADD CONSTRAINT "Announcement_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PopUp" ADD CONSTRAINT "PopUp_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_receivedAnnouncements" ADD CONSTRAINT "_receivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE;

Expand Down
24 changes: 15 additions & 9 deletions src/backend/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,8 @@ model Organization {
FrequentlyAskedQuestions FrequentlyAskedQuestion[]
Milestone Milestone[]
featuredProjects Project[]
PopUps PopUp[]
Announcements Announcement[]
}

model FrequentlyAskedQuestion {
Expand Down Expand Up @@ -932,20 +934,24 @@ model Milestone {
}

model Announcement {
announcementId String @id @default(uuid())
announcementId String @id @default(uuid())
text String
usersReceived User[] @relation("receivedAnnouncements")
dateMessageSent DateTime @default(now())
usersReceived User[] @relation("receivedAnnouncements")
dateMessageSent DateTime @default(now())
dateDeleted DateTime?
senderName String
slackEventId String @unique
slackEventId String @unique
slackChannelName String
organizationId String
organization Organization @relation(fields: [organizationId], references: [organizationId])
}

model PopUp {
popUpId String @id @default(uuid())
text String
iconName String
users User[] @relation("userPopUps")
eventLink String?
popUpId String @id @default(uuid())
text String
iconName String
users User[] @relation("userPopUps")
eventLink String?
organizationId String
organization Organization @relation(fields: [organizationId], references: [organizationId])
}
8 changes: 8 additions & 0 deletions src/backend/src/routes/slack.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createEventAdapter } from '@slack/events-api';
import SlackController from '../controllers/slack.controllers';

export const slackEvents = createEventAdapter(process.env.SLACK_SIGNING_SECRET || '');

slackEvents.on('message', SlackController.processMessageEvent);

slackEvents.on('error', console.log);
76 changes: 73 additions & 3 deletions src/backend/src/services/announcement.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { Announcement } from 'shared';
import prisma from '../prisma/prisma';
import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args';
import announcementTransformer from '../transformers/announcements.transformer';
import { HttpException, NotFoundException } from '../utils/errors.utils';
import { DeletedException, HttpException, NotFoundException } from '../utils/errors.utils';

export default class AnnouncementService {
/**
* Creates an announcement that is sent to users
* this data is populated from slack events
* @param text slack message text
* @param usersReceivedIds users to send announcements to
* @param dateCreated date created of slack message
* @param dateMessageSent date created of slack message
* @param senderName name of user who sent slack message
* @param slackEventId id of slack event (provided by slack api)
* @param slackChannelName name of channel message was sent in
Expand All @@ -37,6 +37,47 @@ export default class AnnouncementService {
dateMessageSent,
senderName,
slackEventId,
slackChannelName,
organizationId
},
...getAnnouncementQueryArgs(organizationId)
});

return announcementTransformer(announcement);
}

static async updateAnnouncement(
text: string,
usersReceivedIds: string[],
senderName: string,
slackEventId: string,
slackChannelName: string,
organizationId: string
): Promise<Announcement> {
const originalAnnouncement = await prisma.announcement.findUnique({
where: {
slackEventId
}
});

if (!originalAnnouncement) throw new NotFoundException('Announcement', slackEventId);

if (originalAnnouncement.dateDeleted) throw new DeletedException('Announcement', slackEventId);

if (originalAnnouncement.organizationId !== organizationId)
throw new HttpException(400, `Announcement is not apart of the current organization`);

const announcement = await prisma.announcement.update({
where: { announcementId: originalAnnouncement.announcementId },
data: {
text,
usersReceived: {
set: usersReceivedIds.map((id) => ({
userId: id
}))
},
slackEventId,
senderName,
slackChannelName
},
...getAnnouncementQueryArgs(organizationId)
Expand All @@ -45,6 +86,34 @@ export default class AnnouncementService {
return announcementTransformer(announcement);
}

static async deleteAnnouncement(slackEventId: string, organizationId: string): Promise<Announcement> {
const originalAnnouncement = await prisma.announcement.findUnique({
where: {
slackEventId
}
});

if (!originalAnnouncement) throw new NotFoundException('Announcement', slackEventId);

if (originalAnnouncement.dateDeleted) throw new DeletedException('Announcement', slackEventId);

if (originalAnnouncement.organizationId !== organizationId)
throw new HttpException(400, `Announcement is not apart of the current organization`);

const announcement = await prisma.announcement.update({
where: { slackEventId },
data: {
dateDeleted: new Date(),
usersReceived: {
set: []
}
},
...getAnnouncementQueryArgs(organizationId)
});

return announcementTransformer(announcement);
}

/**
* Gets all of a user's unread announcements
* @param userId id of the current user
Expand All @@ -57,7 +126,8 @@ export default class AnnouncementService {
dateDeleted: null,
usersReceived: {
some: { userId }
}
},
organizationId
},
...getAnnouncementQueryArgs(organizationId)
});
Expand Down
8 changes: 8 additions & 0 deletions src/backend/src/services/organizations.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import { getProjectQueryArgs } from '../prisma-query-args/projects.query-args';
import projectTransformer from '../transformers/projects.transformer';

export default class OrganizationsService {
/**
* Retrieve all the organizations
* @returns an array of every organization
*/
static async getAllOrganizations(): Promise<Organization[]> {
return prisma.organization.findMany();
}

/**
* Gets the current organization
* @param organizationId the organizationId to be fetched
Expand Down
6 changes: 4 additions & 2 deletions src/backend/src/services/pop-up.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export class PopUpService {
where: {
users: {
some: { userId }
}
},
organizationId
},
...getPopUpQueryArgs(organizationId)
});
Expand Down Expand Up @@ -70,7 +71,8 @@ export class PopUpService {
data: {
text,
iconName,
eventLink
eventLink,
organizationId
},
...getPopUpQueryArgs(organizationId)
});
Expand Down
Loading

0 comments on commit 5a6990e

Please sign in to comment.