Skip to content

Commit

Permalink
feat: app notifications (#149)
Browse files Browse the repository at this point in the history
* wip

* feat: subscribe to topic on client and send to topic on server

* feat: fcm registration token table

* feat: send registration token to backend every time and store it in database

* deps: pnpm v9.15.1

* feat: send notifications when creating assignments in scheduler

* fix: dont initialize app in firebaseMessagingBackgroundHandler

* fix: initialize firebase admin app only if required

* fix: dont send firebase messages on CI

* fix: adjust messages

* fix: only do firebase stuff if supported platform

* fix: stuff from self-code-review

* fix: remove unused import
  • Loading branch information
invertedEcho authored Dec 23, 2024
1 parent 1a3df99 commit 9ec4825
Show file tree
Hide file tree
Showing 44 changed files with 1,946 additions and 153 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:

- uses: pnpm/action-setup@v4
with:
version: 9
version: 9.15.1

- name: Install deps
run: pnpm i
Expand Down
3 changes: 3 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ JWT_SECRET=****
DATABASE_URL=postgres://postgres:changeme@localhost:5432/postgres
NODE_ENV=development
DB_PASSWORD=changeme

# a oneline string containing valid json of the firebase service account (needed for firebase cloud messaging - app notifications)
FIREBASE_SERVICE_ACCOUNT_JSON_CONTENT=
6 changes: 3 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"private": true,
"license": "UNLICENSED",
"engines": {
"pnpm": "~9"
"pnpm": "9.15.1"
},
"scripts": {
"build": "nest build",
Expand Down Expand Up @@ -34,6 +34,7 @@
"@nestjs/serve-static": "4.0.2",
"bcrypt": "5.1.1",
"drizzle-orm": "0.37.0",
"firebase-admin": "^13.0.1",
"pg": "8.11.5",
"postgres": "3.4.4",
"reflect-metadata": "0.2.0",
Expand Down Expand Up @@ -92,6 +93,5 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"packageManager": "[email protected]+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1"
}
}
738 changes: 731 additions & 7 deletions backend/pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ import { AssignmentController } from './assignment/assignment.controller';
import { TaskGroupController } from './tasks/task-group.controller';
import { EventsGateway } from './shopping-list/events.gateway';
import { ShoppingListController } from './shopping-list/shopping-list.controller';
import { NotificationController } from './notifications/notification.controller';
import { NotificationModule } from './notifications/notification.module';

const rootPathStatic = join(__dirname, '../../src/client/public/');

@Module({
imports: [
AuthModule,
// needed to active job scheduling from @nestjs/schedule
ScheduleModule.forRoot(),
AssignmentsModule,
NotificationModule,
ServeStaticModule.forRoot({
rootPath: rootPathStatic,
exclude: ['/api/(.*)'],
Expand All @@ -31,6 +35,7 @@ const rootPathStatic = join(__dirname, '../../src/client/public/');
TaskGroupController,
UserGroupController,
ShoppingListController,
NotificationController,
],
providers: [EventsGateway],
})
Expand Down
25 changes: 24 additions & 1 deletion backend/src/assignment/assignment-scheduler.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { Cron, CronExpression } from '@nestjs/schedule';
import { dbInsertAssignments } from 'src/db/functions/assignment';
import { hydrateTaskGroupsToAssignToAssignments } from './util';
import { dbGetTaskGroupsToAssignForCurrentInterval } from 'src/db/functions/task-group';
import { dbGetFCMRegistrationTokensByUserIds } from 'src/db/functions/notification';
import { sendFirebaseMessages } from 'src/notifications/notification';
import { Message } from 'firebase-admin/messaging';

@Injectable()
export class AssignmentSchedulerService {
@Cron(CronExpression.EVERY_10_SECONDS)
// TODO: optimally, this is not run this often, but just once in a day, and on task creation we immediately create assignments as needed.
@Cron(CronExpression.EVERY_30_SECONDS)
async handleCreateAssignmentsCron() {
const taskGroupsToAssign = await dbGetTaskGroupsToAssignForCurrentInterval({
overrideNow: undefined,
Expand All @@ -29,6 +33,25 @@ export class AssignmentSchedulerService {
2,
)}`,
);

if (process.env.CI === 'true') {
return;
}

const userIds = assignmentsToCreate.map(
(assignment) => assignment.userId,
);
const tokens = await dbGetFCMRegistrationTokensByUserIds(userIds);
const messages = tokens.map((token) => {
return {
token,
notification: {
title: '🚀 New Assignments Waiting!',
body: "You have new assignments waiting for you! Click to learn more. Let's get them done!",
},
} satisfies Message;
});
await sendFirebaseMessages({ messages });
}
}
}
6 changes: 0 additions & 6 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { userTable } from 'src/db/schema';
import { AuthService } from './auth.service';
import {
dbAddUserToUserGroup,
dbGetUserGroupOfUser,
dbGetUserGroupByInviteCode,
} from 'src/db/functions/user-group';
import { eq } from 'drizzle-orm';
Expand Down Expand Up @@ -115,13 +114,8 @@ export class AuthController {
'The access token seemed valid, but the user id included in the jwt token could not be found in the database.',
);
}
const userGroup = await dbGetUserGroupOfUser(req.user.sub);
return {
userId: req.user.sub,
userGroup: {
id: userGroup?.userGroup.name,
name: userGroup?.userGroup.name,
},
email: user.email,
username: user.username,
};
Expand Down
13 changes: 13 additions & 0 deletions backend/src/db/functions/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { inArray } from 'drizzle-orm';
import { db } from '..';
import { userFcmRegistrationTokenMappingTable } from '../schema';

export async function dbGetFCMRegistrationTokensByUserIds(userIds: number[]) {
const result = await db
.select({
token: userFcmRegistrationTokenMappingTable.fcmRegistrationToken,
})
.from(userFcmRegistrationTokenMappingTable)
.where(inArray(userFcmRegistrationTokenMappingTable.userId, userIds));
return result.map((row) => row.token);
}
13 changes: 13 additions & 0 deletions backend/src/db/migrations/0013_loving_satana.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS "user_fcm_registration_token_mapping" (
"user_id" integer NOT NULL,
"fcm_registration_token" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_fcm_registration_token_mapping_user_id_fcm_registration_token_pk" PRIMARY KEY("user_id","fcm_registration_token")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "user_fcm_registration_token_mapping" ADD CONSTRAINT "user_fcm_registration_token_mapping_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
Loading

0 comments on commit 9ec4825

Please sign in to comment.