diff --git a/.gitignore b/.gitignore index 5d6f093..7b0f2e4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ node_modules/ package-lock.json yarn.lock +*.DS_Store \ No newline at end of file diff --git a/src/api/controllers/NotifController.ts b/src/api/controllers/NotifController.ts index 6a3f79f..1b3dcfc 100644 --- a/src/api/controllers/NotifController.ts +++ b/src/api/controllers/NotifController.ts @@ -1,5 +1,5 @@ -import { JsonController, Post } from 'routing-controllers'; -import { ExpoPushMessage, PushTicket } from 'src/types'; +import { Body, CurrentUser, Delete, Get, JsonController, Params, Post } from 'routing-controllers'; +import { ExpoPushMessage, PushTicket, FindTokensRequest } from 'src/types'; import { NotifService } from '../../services/NotifService'; @JsonController('notif/') @@ -11,7 +11,8 @@ export class NotifController { } @Post() - async sendPost( notifRequest : ExpoPushMessage ) { - return this.notifService.sendNotifs(notifRequest, {}); + async sendNotif(@Body() findTokensRequest: FindTokensRequest) { + return this.notifService.sendNotifs(findTokensRequest); } -} \ No newline at end of file +} + diff --git a/src/api/controllers/PostController.ts b/src/api/controllers/PostController.ts index 21707de..e8fedb2 100644 --- a/src/api/controllers/PostController.ts +++ b/src/api/controllers/PostController.ts @@ -98,8 +98,13 @@ export class PostController { return { isSaved: await this.postService.isSavedPost(user, params) }; } - @Post('edit/postID/:id/') + @Post('edit/postId/:id/') async editPrice(@Body() editPriceRequest: EditPostPriceRequest, @CurrentUser() user: UserModel, @Params() params: UuidParam): Promise { return { new_price: await (await this.postService.editPostPrice(user, params, editPriceRequest)).altered_price }; } + + @Get('similar/postId/:id/') + async similarPosts(@Params() params: UuidParam): Promise { + return { posts: await this.postService.similarPosts(params) }; + } } \ No newline at end of file diff --git a/src/api/controllers/UserController.ts b/src/api/controllers/UserController.ts index 0c95b02..810580e 100644 --- a/src/api/controllers/UserController.ts +++ b/src/api/controllers/UserController.ts @@ -2,7 +2,7 @@ import { Body, CurrentUser, Get, JsonController, Param, Params, Post } from 'rou import { UserModel } from '../../models/UserModel'; import { UserService } from '../../services/UserService'; -import { EditProfileRequest, GetUserByEmailRequest, GetUserResponse, GetUsersResponse, SetAdminByEmailRequest } from '../../types'; +import { EditProfileRequest, GetUserByEmailRequest, GetUserResponse, GetUsersResponse, SaveTokenRequest, SetAdminByEmailRequest } from '../../types'; import { UuidParam } from '../validators/GenericRequests'; @JsonController('user/') diff --git a/src/api/controllers/index.ts b/src/api/controllers/index.ts index 88c0feb..859e787 100644 --- a/src/api/controllers/index.ts +++ b/src/api/controllers/index.ts @@ -5,11 +5,13 @@ import { PostController } from './PostController'; import { RequestController } from './RequestController'; import { UserController } from './UserController'; import { UserReviewController } from './UserReviewController'; +import { NotifController } from './NotifController' export const controllers = [ AuthController, FeedbackController, ImageController, + NotifController, PostController, RequestController, UserController, diff --git a/src/api/validators/AuthControllerRequests.ts b/src/api/validators/AuthControllerRequests.ts index 608e1ff..c4c521b 100644 --- a/src/api/validators/AuthControllerRequests.ts +++ b/src/api/validators/AuthControllerRequests.ts @@ -8,4 +8,7 @@ export class LoginRequest implements AuthRequest { @IsDefined() user: GoogleLoginUser; + + @IsDefined() + deviceToken: string; } \ No newline at end of file diff --git a/src/migrations/0001_AddDeviceTokenForUser.ts b/src/migrations/0001_AddDeviceTokenForUser.ts new file mode 100644 index 0000000..39d29ca --- /dev/null +++ b/src/migrations/0001_AddDeviceTokenForUser.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; + +const TABLE_NAME = "User"; + +export class AddDeviceTokenForUser1682226973547 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // add device token for user table + await queryRunner.addColumn( + TABLE_NAME, + new TableColumn({ + name: "deviceTokens", + type: "text[]", + default: 'array[]::text[]', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn(TABLE_NAME, "deviceTokens"); + } +} \ No newline at end of file diff --git a/src/migrations/0003_AddDeviceTokenToSession.ts b/src/migrations/0003_AddDeviceTokenToSession.ts new file mode 100644 index 0000000..f34b6e7 --- /dev/null +++ b/src/migrations/0003_AddDeviceTokenToSession.ts @@ -0,0 +1,22 @@ +import {MigrationInterface, QueryRunner, TableColumn} from "typeorm"; + +const TABLE_NAME = "UserSession" + +export class AddDeviceTokenToSession1682461409919 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + TABLE_NAME, + new TableColumn({ + name: "deviceToken", + type: "text", + default: "''" + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn(TABLE_NAME, "deviceToken"); + } + +} diff --git a/src/migrations/0004_RemoveDeviceTokenForUser.ts b/src/migrations/0004_RemoveDeviceTokenForUser.ts new file mode 100644 index 0000000..8645088 --- /dev/null +++ b/src/migrations/0004_RemoveDeviceTokenForUser.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +const TABLE_NAME = "User"; + +export class RemoveDeviceTokenForUser1683322987029 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn(TABLE_NAME, "deviceTokens"); + } + + public async down(queryRunner: QueryRunner): Promise { + // add device token for user table + await queryRunner.addColumn( + TABLE_NAME, + new TableColumn({ + name: "deviceTokens", + type: "text[]", + default: 'array[]::text[]', + }) + ); + } +} diff --git a/src/models/UserSessionModel.ts b/src/models/UserSessionModel.ts index 3031a78..9c91ece 100644 --- a/src/models/UserSessionModel.ts +++ b/src/models/UserSessionModel.ts @@ -19,6 +19,9 @@ export class UserSessionModel { @Column() refreshToken: string; + @Column("text", { default: "", nullable: false, }) + deviceToken: string; + @ManyToOne(() => UserModel, user => user.sessions, { onDelete: "CASCADE" }) @JoinColumn({ name: 'user' }) user: UserModel; @@ -43,6 +46,7 @@ export class UserSessionModel { active: this.expiresAt.getTime() > Date.now(), expiresAt: this.expiresAt.getTime(), refreshToken: this.refreshToken, + deviceToken: this.deviceToken }; } diff --git a/src/repositories/UserRepository.ts b/src/repositories/UserRepository.ts index d0379d2..22cdb4b 100644 --- a/src/repositories/UserRepository.ts +++ b/src/repositories/UserRepository.ts @@ -102,7 +102,7 @@ export class UserRepository extends AbstractRepository { username: string | undefined, photoUrl: string | undefined, venmoHandle: string | undefined, - bio: string | undefined + bio: string | undefined, ): Promise { const existingUser = this.repository .createQueryBuilder("user") diff --git a/src/repositories/UserSessionRepository.ts b/src/repositories/UserSessionRepository.ts index 7b6d5f1..d46d964 100644 --- a/src/repositories/UserSessionRepository.ts +++ b/src/repositories/UserSessionRepository.ts @@ -100,6 +100,11 @@ export class UserSessionRepository extends AbstractRepository return session; } + public async updateSessionDeviceToken(session: UserSessionModel, deviceToken: string): Promise { + session.deviceToken = deviceToken; + return await this.repository.save(session); + } + public async verifySession(accessToken: string): Promise { const session = await this.repository .createQueryBuilder("UserSessionModel") diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index babdf3a..e269b1f 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -68,8 +68,9 @@ export class AuthService { user = await userRepository.createUser(netid, netid, newUser.givenName, newUser.familyName, newUser.photoUrl, newUser.email, userId); } - // since they're logging in, create a new session for them - const session = sessionsRepository.createSession(user); + //add device token + const session = await sessionsRepository.createSession(user); + sessionsRepository.updateSessionDeviceToken(session, authRequest.deviceToken) return session; }); } diff --git a/src/services/NotifService.ts b/src/services/NotifService.ts index 1b7cf11..184d7cf 100644 --- a/src/services/NotifService.ts +++ b/src/services/NotifService.ts @@ -1,50 +1,84 @@ +import { NotFoundError} from 'routing-controllers'; import { Service } from 'typedi'; - -import { ExpoPushMessage, PushTicket } from '../types'; +import { ExpoPushMessage, PushTicket, FindTokensRequest, NotifSent } from '../types'; import { Expo } from 'expo-server-sdk'; - +import { UserRepository } from 'src/repositories/UserRepository'; +import Repositories, { TransactionsManager } from '../repositories'; +import { EntityManager } from 'typeorm'; +import { InjectManager } from 'typeorm-typedi-extensions'; var accessToken = process.env['EXPO_ACCESS_TOKEN'] const expoServer = new Expo({ accessToken: accessToken }); + @Service() export class NotifService { -// /** + private transactions: TransactionsManager; + + constructor(@InjectManager() entityManager: EntityManager) { + this.transactions = new TransactionsManager(entityManager); + } + + // /** // * Takes an array of notifications and sends them to users in batches (called chunks) // * @param {NotifObject[]} notifs an array of notification objects // * @param {Object} expoServer the server object to connect with // */ - public sendNotifChunks = async (notifs : ExpoPushMessage[], expoServer : Expo) => { - let chunks = expoServer.chunkPushNotifications(notifs); - let tickets = []; - for (let chunk of chunks) { - try { - let ticketChunk = await expoServer.sendPushNotificationsAsync(chunk); - // store tickets to check for notif status later - tickets.push(...ticketChunk); - } catch (err) { - console.log("Error while sending notif chunk"); + public sendNotifChunks = async (notifs : ExpoPushMessage[], expoServer : Expo) => { + let chunks = expoServer.chunkPushNotifications(notifs); + let tickets = []; + + for (let chunk of chunks) { + try { + let ticketChunk = await expoServer.sendPushNotificationsAsync(chunk); + // store tickets to check for notif status later + tickets.push(...ticketChunk); + } catch (err) { + console.log("Error while sending notif chunk"); + } + console.log(tickets); } - console.log(tickets); } -} -public sendNotifs = (notif : ExpoPushMessage, json = {}) => { - try { - let notifs : ExpoPushMessage[] = []; - notif.to.forEach(token => { - notifs.push({ - to: notif.to, - sound: notif.sound, - title: notif.title, - body: notif.body, - data: notif.data - }) + public async sendNotifs(request: FindTokensRequest) { + return this.transactions.readWrite(async (transactionalEntityManager) => { + const userRepository = Repositories.user(transactionalEntityManager); + const userSessionRepository = Repositories.session(transactionalEntityManager); + let user = await userRepository.getUserByEmail(request.email); + if (!user) { + throw new NotFoundError("User not found!"); + } + const allDeviceTokens = []; + const allsessions = await userSessionRepository.getSessionsByUserId(user.id); + for (var sess of allsessions) { + if (sess.deviceToken) { + allDeviceTokens.push(sess.deviceToken); } + } + let notif: ExpoPushMessage= + { + to: allDeviceTokens, + sound: 'default', + title: request.title, + body: request.body, + data: request.data + } + try { + let notifs : ExpoPushMessage[] = []; + notif.to.forEach(token => { + notifs.push({ + to: [token], + sound: notif.sound, + title: notif.title, + body: notif.body, + data: notif.data + }) + }) + this.sendNotifChunks(notifs, expoServer) + } + + // Simply do nothing if the user has no tokens + catch (err) { + console.log(err) } }) - - this.sendNotifChunks(notifs, expoServer) } - // Simply do nothing if the user has no tokens - catch (err) { console.log(err) } -} } \ No newline at end of file diff --git a/src/services/PostService.ts b/src/services/PostService.ts index 23ae2c2..ec232e7 100644 --- a/src/services/PostService.ts +++ b/src/services/PostService.ts @@ -223,5 +223,33 @@ export class PostService { }, 0); return result; } + + public async similarPosts(params: UuidParam): Promise { + return this.transactions.readOnly(async (transactionalEntityManager) => { + const postRepository = Repositories.post(transactionalEntityManager); + const post = await postRepository.getPostById(params.id); + if (!post) throw new NotFoundError('Post not found!'); + const allPosts = await postRepository.getAllPosts(); + let posts: PostModel[] = [] + const model = await getLoadedModel(); + for (const p of allPosts) { + if (post.id != p.id) { + const sentences = [ + post.title, + p.title + ]; + await model.embed(sentences).then(async (embeddings: any) => { + embeddings = embeddings.arraySync() + const a = embeddings[0]; + const b = embeddings[1]; + if (this.similarity(a, b) >= 0.5) { + posts.push(p) + } + }); + } + } + return posts + }); + } } diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 36150da..2277ccd 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -6,7 +6,7 @@ import { InjectManager } from 'typeorm-typedi-extensions'; import { UuidParam } from '../api/validators/GenericRequests'; import { UserModel } from '../models/UserModel'; import Repositories, { TransactionsManager } from '../repositories'; -import { EditProfileRequest, SetAdminByEmailRequest } from '../types'; +import { EditProfileRequest, SaveTokenRequest, SetAdminByEmailRequest } from '../types'; import { uploadImage } from '../utils/Requests'; @Service() diff --git a/src/types/ApiRequests.ts b/src/types/ApiRequests.ts index 0bbbb63..074e195 100644 --- a/src/types/ApiRequests.ts +++ b/src/types/ApiRequests.ts @@ -16,6 +16,7 @@ export interface GoogleLoginUser { export interface AuthRequest { idToken: string; user: GoogleLoginUser; + deviceToken: string; } export interface EditProfileRequest { @@ -114,9 +115,21 @@ export interface CreateUserReviewRequest { // NOTIFICATION export interface ExpoPushMessage { to: string[]; - sound: 'default' | null; + //special type for ExpoPushMessage + sound: 'default'; title: string; body: string; data: JSON; } +export interface SaveTokenRequest { + token: string; + userId: Uuid; +} + +export interface FindTokensRequest { + email: string; + title: string; + body: string; + data: JSON; +} diff --git a/src/types/ApiResponses.ts b/src/types/ApiResponses.ts index 0f3b297..e653c1a 100644 --- a/src/types/ApiResponses.ts +++ b/src/types/ApiResponses.ts @@ -156,21 +156,20 @@ export interface GetUserReviewsResponse { export interface PushTicketData { status: string, id: string, - message: string - details: JSON + message: string, + details: JSON, } export interface PushTicketErrorData { code: string, - message: string + message: string, } export interface PushTicket { data: PushTicketData[], - errors: PushTicketErrorData[] + errors: PushTicketErrorData[], } -export interface SaveTokenRequest { - token: string, - email: string -} \ No newline at end of file +export interface NotifSent { + status: string, +} diff --git a/src/types/Miscellaneous.ts b/src/types/Miscellaneous.ts index b02c5d6..379627e 100644 --- a/src/types/Miscellaneous.ts +++ b/src/types/Miscellaneous.ts @@ -4,6 +4,7 @@ export type APIUserSession = { refreshToken: string, active: boolean, expiresAt: number, + deviceToken: string } export enum Category {