Skip to content

Commit

Permalink
up to date
Browse files Browse the repository at this point in the history
  • Loading branch information
caiodasilva2005 committed Dec 20, 2024
2 parents 882fcde + 147b5dd commit 51c9a04
Show file tree
Hide file tree
Showing 33 changed files with 1,626 additions and 82 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
7 changes: 5 additions & 2 deletions src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +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 slackRouter from './src/routes/slack.routes';
import { slackEvents } from './src/routes/slack.routes';

const app = express();

Expand All @@ -41,6 +41,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 Expand Up @@ -69,7 +73,6 @@ app.use('/templates', workPackageTemplatesRouter);
app.use('/cars', carsRouter);
app.use('/organizations', organizationRouter);
app.use('/recruitment', recruitmentRouter);
app.use('/slack', slackRouter);
app.use('/', (_req, res) => {
res.status(200).json('Welcome to FinishLine');
});
Expand Down
32 changes: 29 additions & 3 deletions src/backend/src/controllers/users.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,39 @@ export default class UsersController {

static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) {
try {
const { userId } = req.params;
const { organization } = req;
const { organization, currentUser } = req;

const unreadNotifications = await UsersService.getUserUnreadNotifications(currentUser.userId, organization);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}

static async removeUserNotification(req: Request, res: Response, next: NextFunction) {
try {
const { notificationId } = req.params;
const { organization, currentUser } = req;

const unreadNotifications = await UsersService.getUserUnreadNotifications(userId, organization);
const unreadNotifications = await UsersService.removeUserNotification(
currentUser.userId,
notificationId,
organization
);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}

static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) {
try {
const { organization, currentUser } = req;

const unreadAnnouncements = await UsersService.getUserUnreadAnnouncements(currentUser.userId, organization);
res.status(200).json(unreadAnnouncements);
} catch (error: unknown) {
next(error);
}
}
}
84 changes: 84 additions & 0 deletions src/backend/src/integrations/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,88 @@ 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) {
throw new HttpException(500, 'Error getting members from a slack channel: ' + (error as any).data.error);
}
};

/**
* Given a slack channel id, prood.uces the name of the channel
* @param channelId the id of the slack channel
* @returns the name of the channel
*/
export const getChannelName = async (channelId: string) => {
const { SLACK_BOT_TOKEN } = process.env;
if (!SLACK_BOT_TOKEN) return channelId;

try {
const channelRes = await slack.conversations.info({ channel: channelId });
return channelRes.channel?.name || 'Unknown Channel';
} catch (error) {
throw new HttpException(500, 'Error getting slack channel name: ' + (error as any).data.error);
}
};

/**
* 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)
*/
export const getUserName = async (userId: string) => {
const { SLACK_BOT_TOKEN } = process.env;
if (!SLACK_BOT_TOKEN) return;

try {
const userRes = await slack.users.info({ user: userId });
return userRes.user?.profile?.display_name || userRes.user?.real_name || 'Unkown User';
} catch (error) {
throw new HttpException(500, 'Error getting slack user name: ' + (error as any).data.error);
}
};

/**
* Get the workspace id of the workspace this slack api is registered with
* @returns the id of the workspace
*/
export const getWorkspaceId = async () => {
const { SLACK_BOT_TOKEN } = process.env;
if (!SLACK_BOT_TOKEN) return;

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;

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
-- AlterTable
ALTER TABLE "Organization" ADD COLUMN "logoImageId" TEXT,
ADD COLUMN "slackWorkspaceId" TEXT;

-- AlterTable
ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT;

-- CreateTable
CREATE TABLE "Announcement" (
"announcementId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"dateCrated" TIMESTAMP(3) NOT NULL,
"userCreatedId" TEXT NOT NULL,
"dateCreated" TIMESTAMP(3) NOT NULL,
"dateDeleted" TIMESTAMP(3),
"senderName" TEXT NOT NULL,
"slackEventId" TEXT NOT NULL,
"slackChannelName" TEXT NOT NULL,

CONSTRAINT "Announcement_pkey" PRIMARY KEY ("announcementId")
);
Expand All @@ -13,6 +23,7 @@ CREATE TABLE "Notification" (
"notificationId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"iconName" TEXT NOT NULL,
"eventLink" TEXT,

CONSTRAINT "Notification_pkey" PRIMARY KEY ("notificationId")
);
Expand All @@ -29,6 +40,9 @@ CREATE TABLE "_userNotifications" (
"B" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "Announcement_slackEventId_key" ON "Announcement"("slackEventId");

-- CreateIndex
CREATE UNIQUE INDEX "_receivedAnnouncements_AB_unique" ON "_receivedAnnouncements"("A", "B");

Expand All @@ -42,7 +56,7 @@ CREATE UNIQUE INDEX "_userNotifications_AB_unique" ON "_userNotifications"("A",
CREATE INDEX "_userNotifications_B_index" ON "_userNotifications"("B");

-- AddForeignKey
ALTER TABLE "Announcement" ADD CONSTRAINT "Announcement_userCreatedId_fkey" FOREIGN KEY ("userCreatedId") REFERENCES "User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("organizationId") ON DELETE SET NULL 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
23 changes: 13 additions & 10 deletions src/backend/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,7 @@ model User {
deletedFrequentlyAskedQuestions FrequentlyAskedQuestion[] @relation(name: "frequentlyAskedQuestionDeleter")
createdMilestones Milestone[] @relation(name: "milestoneCreator")
deletedMilestones Milestone[] @relation(name: "milestoneDeleter")
receivedAnnouncements Announcement[] @relation(name: "receivedAnnouncements")
createdAnnouncements Announcement[] @relation(name: "createdAnnouncements")
unreadAnnouncements Announcement[] @relation(name: "receivedAnnouncements")
unreadNotifications Notification[] @relation(name: "userNotifications")
}

Expand Down Expand Up @@ -878,6 +877,7 @@ model Organization {
applyInterestImageId String? @unique
exploreAsGuestImageId String? @unique
logoImageId String?
slackWorkspaceId String?
// Relation references
wbsElements WBS_Element[]
Expand Down Expand Up @@ -932,17 +932,20 @@ model Milestone {
}

model Announcement {
announcementId String @id @default(uuid())
text String
usersReceived User[] @relation("receivedAnnouncements")
dateCrated DateTime
userCreatedId String
userCreated User @relation("createdAnnouncements", fields: [userCreatedId], references: [userId])
announcementId String @id @default(uuid())
text String
usersReceived User[] @relation("receivedAnnouncements")
dateCreated DateTime
dateDeleted DateTime?
senderName String
slackEventId String @unique
slackChannelName String
}

model Notification {
notificationId String @id @default(uuid())
notificationId String @id @default(uuid())
text String
iconName String
users User[] @relation("userNotifications")
users User[] @relation("userNotifications")
eventLink String?
}
12 changes: 10 additions & 2 deletions src/backend/src/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { writeFileSync } from 'fs';
import WorkPackageTemplatesService from '../services/work-package-template.services';
import RecruitmentServices from '../services/recruitment.services';
import OrganizationsService from '../services/organizations.services';
import NotificationsService from '../services/notifications.services';
import AnnouncementService from '../services/announcement.service';

const prisma = new PrismaClient();

Expand Down Expand Up @@ -1895,7 +1895,15 @@ const performSeed: () => Promise<void> = async () => {

await RecruitmentServices.createFaq(batman, 'How many developers are working on FinishLine?', '178 as of 2024', ner);

await NotificationsService.sendNotifcationToUsers('Admin!', 'star', [thomasEmrax.userId], ner.organizationId);
await AnnouncementService.createAnnouncement(
'Welcome to Finishline!',
[regina.userId],
new Date(),
'Thomas Emrax',
'1',
'software',
ner.organizationId
);
};

performSeed()
Expand Down
21 changes: 16 additions & 5 deletions src/backend/src/routes/slack.routes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import express from 'express';
import slackController from '../controllers/slack.controllers';
import { createEventAdapter } from '@slack/events-api';
import slackServices from '../services/slack.services';
import OrganizationsService from '../services/organizations.services';
import { getWorkspaceId } from '../integrations/slack';

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

slackRouter.post('/', slackController.handleEvent);
slackEvents.on('message', async (event) => {
const organizations = await OrganizationsService.getAllOrganizations();
const nerSlackWorkspaceId = await getWorkspaceId();
const orgId = organizations.find((org) => org.slackWorkspaceId === nerSlackWorkspaceId)?.organizationId;
if (orgId) {
slackServices.processMessageSent(event, orgId);
}
});

export default slackRouter;
slackEvents.on('error', (error) => {
console.log(error.name);
});
4 changes: 3 additions & 1 deletion src/backend/src/routes/users.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ userRouter.post(
validateInputs,
UsersController.getManyUserTasks
);
userRouter.get('/:userId/notifications', UsersController.getUserUnreadNotifications);
userRouter.get('/notifications/current-user', UsersController.getUserUnreadNotifications);
userRouter.get('/announcements/current-user', UsersController.getUserUnreadAnnouncements);
userRouter.post('/notifications/:notificationId/remove', UsersController.removeUserNotification);

export default userRouter;
Loading

0 comments on commit 51c9a04

Please sign in to comment.