From 5b57812088c08b907ccb04d0644d07e240bbda75 Mon Sep 17 00:00:00 2001 From: Radek <104318242+radekm2000@users.noreply.github.com> Date: Thu, 30 May 2024 21:08:23 +0200 Subject: [PATCH] add receiving discord notifications when tracked user lists a new item --- server/ecommerce/src/app.module.ts | 2 + .../src/discord-bot/discord-bot.module.ts | 4 + .../notifiers/item-notifier.module.ts | 27 ++++ .../notifiers/item-notifier.service.ts | 24 ++++ .../src/commands/notifiers/item-notifier.ts | 18 +-- .../discord-notifications-bot.ts | 121 ++++++++++++++++++ .../discord-notifications.service.ts | 40 +++++- .../product-notification.module.ts | 11 +- .../ecommerce/src/products/products.module.ts | 4 + .../src/products/products.service.ts | 4 +- 10 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.module.ts create mode 100644 server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.service.ts create mode 100644 server/ecommerce/src/discord-notifications/discord-notifications-bot.ts diff --git a/server/ecommerce/src/app.module.ts b/server/ecommerce/src/app.module.ts index d5dc98d..5fa5531 100644 --- a/server/ecommerce/src/app.module.ts +++ b/server/ecommerce/src/app.module.ts @@ -19,6 +19,7 @@ import { AdminNotificationsModule } from './admin-notifications/admin-notificati import { FeedbacksModule } from './feedbacks/feedbacks.module'; import { DiscordBotModule } from './discord-bot/discord-bot.module'; import { DiscordNotificationsModule } from './discord-notifications/discord-notifications.module'; +import { ItemNotifierModule } from './discord-bot/src/commands/notifiers/item-notifier.module'; @Module({ imports: [ @@ -39,6 +40,7 @@ import { DiscordNotificationsModule } from './discord-notifications/discord-noti FeedbacksModule, DiscordBotModule, DiscordNotificationsModule, + ItemNotifierModule, ], controllers: [AppController], providers: [AppService], diff --git a/server/ecommerce/src/discord-bot/discord-bot.module.ts b/server/ecommerce/src/discord-bot/discord-bot.module.ts index 772ea74..6b578f4 100644 --- a/server/ecommerce/src/discord-bot/discord-bot.module.ts +++ b/server/ecommerce/src/discord-bot/discord-bot.module.ts @@ -13,6 +13,8 @@ import { Product } from 'src/utils/entities/product.entity'; import { ProductNotificationService } from 'src/product-notification/product-notification.service'; import { ProductNotification } from 'src/utils/entities/product-notification.entity'; import { FollowersService } from 'src/followers/followers.service'; +import { ItemNotifierService } from './src/commands/notifiers/item-notifier.service'; +import { DiscordNotificationsService } from 'src/discord-notifications/discord-notifications.service'; @Module({ controllers: [DiscordBotController], @@ -22,6 +24,8 @@ import { FollowersService } from 'src/followers/followers.service'; ProductsService, ProductNotificationService, FollowersService, + ItemNotifierService, + DiscordNotificationsService, ], imports: [ TypeOrmModule.forFeature([ diff --git a/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.module.ts b/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.module.ts new file mode 100644 index 0000000..c9fcdb6 --- /dev/null +++ b/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { ItemNotifierService } from './item-notifier.service'; +import { UsersService } from 'src/users/users.service'; +import { DiscordNotificationsService } from 'src/discord-notifications/discord-notifications.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Follow } from 'src/utils/entities/followers.entity'; +import { User } from 'discord.js'; +import { Profile } from 'src/utils/entities/profile.entity'; +import { Avatar } from 'src/utils/entities/avatar.entity'; +import { Product } from 'src/utils/entities/product.entity'; +import { ProductNotification } from 'src/utils/entities/product-notification.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Follow, + User, + Profile, + Avatar, + Product, + ProductNotification, + ]), + ], + providers: [ItemNotifierService, UsersService, DiscordNotificationsService], + exports: [ItemNotifierService], +}) +export class ItemNotifierModule {} diff --git a/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.service.ts b/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.service.ts new file mode 100644 index 0000000..3bb551b --- /dev/null +++ b/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { ItemNotifier } from './item-notifier'; +import { UsersService } from 'src/users/users.service'; +import { DiscordNotificationsService } from 'src/discord-notifications/discord-notifications.service'; +import { Product } from 'src/utils/entities/product.entity'; + +@Injectable() +export class ItemNotifierService { + private readonly itemNotifier: ItemNotifier; + + constructor( + private usersService: UsersService, + private discordNotificationsService: DiscordNotificationsService, + ) { + this.itemNotifier = new ItemNotifier({ + bot: this.discordNotificationsService.discordNotificationsBot, + usersService: this.usersService, + }); + } + + public notifyUsers = async (userId: number, product: Product) => { + return await this.itemNotifier.notifyUsers(userId, product); + }; +} diff --git a/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.ts b/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.ts index f1c52d0..d6ba3e3 100644 --- a/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.ts +++ b/server/ecommerce/src/discord-bot/src/commands/notifiers/item-notifier.ts @@ -1,5 +1,4 @@ import { Product } from 'src/utils/entities/product.entity'; -import { DiscordBot } from '../../discordbot'; import { UsersService } from 'src/users/users.service'; import { Follow } from 'src/utils/entities/followers.entity'; import { Profile } from 'src/utils/entities/profile.entity'; @@ -8,8 +7,9 @@ import { Avatar } from 'src/utils/entities/avatar.entity'; import { Message } from 'src/utils/entities/message.entity'; import { Review } from 'src/utils/entities/review.entity'; import { EmbedBuilder } from 'discord.js'; -import { DiscordBotService } from 'src/discord-bot/discord-bot.service'; import { Injectable } from '@nestjs/common'; +import { DiscordNotificationsService } from 'src/discord-notifications/discord-notifications.service'; +import { DiscordNotificationsBot } from 'src/discord-notifications/discord-notifications-bot'; type User = { followings: Follow[]; @@ -30,23 +30,25 @@ type User = { }; type Config = { - discordBotService: DiscordBotService; + bot: DiscordNotificationsBot; usersService: UsersService; }; @Injectable() export class ItemNotifier { - private readonly discordBotService: DiscordBotService; private readonly usersService: UsersService; + private readonly bot: DiscordNotificationsBot; constructor(config: Config) { - this.discordBotService = config.discordBotService; + this.bot = config.bot; this.usersService = config.usersService; } public notifyUsers = async (userId: number, product: Product) => { const user = await this.usersService.getUserInfo(userId); const usersToNotify = await this.getUsersToNotify(user); + console.log('ludzie do notifaj'); + console.log(usersToNotify); await this.notify(usersToNotify, product); }; @@ -59,11 +61,10 @@ export class ItemNotifier { }; private notify = async (usersToNotify: User[], product: Product) => { - const productImage = product.images[0].imageUrl; const productAuthor = product.user.username; await Promise.all( usersToNotify.map((user) => - this.discordBotService.discordBot.sendDM(user.discordId, { + this.bot.sendDM(user.discordId, { embeds: [ new EmbedBuilder() .setTitle(`**${productAuthor}** has listed a new item`) @@ -73,8 +74,7 @@ export class ItemNotifier { { name: 'Category', value: product.category, inline: true }, ]) .setAuthor({ name: productAuthor }) - .setColor('#58b9ff') - .setImage(productImage), + .setColor('#58b9ff'), ], }), ), diff --git a/server/ecommerce/src/discord-notifications/discord-notifications-bot.ts b/server/ecommerce/src/discord-notifications/discord-notifications-bot.ts new file mode 100644 index 0000000..8ad6a05 --- /dev/null +++ b/server/ecommerce/src/discord-notifications/discord-notifications-bot.ts @@ -0,0 +1,121 @@ +import { Logger } from '@nestjs/common'; +import { + AutocompleteInteraction, + ChatInputCommandInteraction, + Client, + Events, + MessageCreateOptions, + REST, + Routes, +} from 'discord.js'; +import { SlashCommand } from 'src/discord-bot/src/commands/slash-command'; +import { InteractionError } from 'src/discord-bot/src/errors/interactionError'; + +type Config = { + bot: Client; + botToken: string; + commands: SlashCommand[]; + botApplicationId: string; +}; + +export class DiscordNotificationsBot { + private readonly logger: Logger; + private readonly bot: Client; + private readonly botToken: string; + private readonly botApplicationId: string; + private readonly commands = new Map(); + + constructor(config: Config) { + this.logger = new Logger(DiscordNotificationsBot.name); + this.bot = config.bot; + this.botToken = config.botToken; + this.botApplicationId = config.botApplicationId; + config.commands.forEach((c) => this.commands.set(c.config.name, c)); + } + + public start = async () => { + await this.bot.login(this.botToken); + this.bot.on('ready', this.onReady); + }; + + private onReady = async () => { + await this.registerCommands(); + this.listenToInteractions(); + this.logger.log('DiscordNotificationsBot has been started'); + }; + + private registerCommands = async () => { + const rest = new REST().setToken(this.botToken); + const url = Routes.applicationCommands(this.botApplicationId); + await rest.put(url, { + body: Array.from(this.commands.values()).map((c) => c.config.toJSON()), + }); + }; + + private handleCommand = async (interaction: ChatInputCommandInteraction) => { + const command = this.commands.get(interaction.commandName); + if (!command) return; + + try { + await command.execute(interaction); + } catch (error) { + const isInteractionError = error instanceof InteractionError; + const msg = isInteractionError + ? error.message + : 'There was an error while executing this command!'; + + if (!isInteractionError) { + this.logger.error( + `Error when executing ${interaction.commandName} command`, + ); + } + if (interaction.replied || interaction.deferred) { + this.logger.error( + `Error when executing ${interaction.commandName} command`, + ); + await interaction.followUp({ + content: msg, + ephemeral: true, + }); + } else { + await interaction.reply({ + content: msg, + ephemeral: true, + }); + } + } + }; + + private listenToInteractions = () => { + this.bot.on(Events.InteractionCreate, async (interaction) => { + if (interaction.isChatInputCommand()) { + await this.handleCommand(interaction); + } else if (interaction.isAutocomplete()) { + await this.handleAutocomplete(interaction); + } + }); + }; + + private handleAutocomplete = async (interaction: AutocompleteInteraction) => { + const command = this.commands.get(interaction.commandName); + if (!command) return; + + try { + await command.autocomplete(interaction); + } catch (error) { + this.logger.error( + { error }, + `Error when executing the ${interaction.commandName} autocomplete`, + ); + } + }; + + public sendDM = async ( + userDiscordId: string, + message: MessageCreateOptions, + ) => { + try { + await this.bot.users.send(userDiscordId, message); + } catch (error) {} + }; +} diff --git a/server/ecommerce/src/discord-notifications/discord-notifications.service.ts b/server/ecommerce/src/discord-notifications/discord-notifications.service.ts index 7d98b63..6a46c4b 100644 --- a/server/ecommerce/src/discord-notifications/discord-notifications.service.ts +++ b/server/ecommerce/src/discord-notifications/discord-notifications.service.ts @@ -1,11 +1,37 @@ -import { Injectable } from '@nestjs/common'; -import { DiscordBotService } from 'src/discord-bot/discord-bot.service'; -import { ProductsService } from 'src/products/products.service'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Product } from 'src/utils/entities/product.entity'; +import { DiscordNotificationsBot } from './discord-notifications-bot'; +import { Client, IntentsBitField } from 'discord.js'; @Injectable() -export class DiscordNotificationsService { - public notify = async (userId: number, product: Product) => { - return; - }; +export class DiscordNotificationsService implements OnModuleInit { + public discordNotificationsBot: DiscordNotificationsBot; + private readonly bot: Client; + private readonly botToken: string; + private readonly botApplicationId: string; + + constructor() { + this.botToken = process.env.DISCORD_BOT_TOKEN; + this.botApplicationId = process.env.DISCORD_CLIENT_ID; + this.bot = new Client({ + intents: [ + IntentsBitField.Flags.Guilds, + IntentsBitField.Flags.GuildMessages, + ], + allowedMentions: { + parse: ['everyone', 'roles', 'users'], + }, + }); + + this.discordNotificationsBot = new DiscordNotificationsBot({ + commands: [], + bot: this.bot, + botToken: this.botToken, + botApplicationId: this.botApplicationId, + }); + } + + async onModuleInit() { + await this.discordNotificationsBot.start(); + } } diff --git a/server/ecommerce/src/product-notification/product-notification.module.ts b/server/ecommerce/src/product-notification/product-notification.module.ts index e6686cd..44b9a89 100644 --- a/server/ecommerce/src/product-notification/product-notification.module.ts +++ b/server/ecommerce/src/product-notification/product-notification.module.ts @@ -11,6 +11,9 @@ import { Avatar } from 'src/utils/entities/avatar.entity'; import { ProductsService } from 'src/products/products.service'; import { Product } from 'src/utils/entities/product.entity'; import { Image } from 'src/utils/entities/image.entity'; +import { ItemNotifier } from 'src/discord-bot/src/commands/notifiers/item-notifier'; +import { ItemNotifierService } from 'src/discord-bot/src/commands/notifiers/item-notifier.service'; +import { DiscordNotificationsService } from 'src/discord-notifications/discord-notifications.service'; @Module({ imports: [ @@ -25,7 +28,13 @@ import { Image } from 'src/utils/entities/image.entity'; ]), ], controllers: [ProductNotificationController], - providers: [ProductNotificationService, UsersService, ProductsService], + providers: [ + ProductNotificationService, + UsersService, + ProductsService, + ItemNotifierService, + DiscordNotificationsService, + ], exports: [ProductNotificationService], }) export class ProductNotificationModule {} diff --git a/server/ecommerce/src/products/products.module.ts b/server/ecommerce/src/products/products.module.ts index 1ad2115..c1e457c 100644 --- a/server/ecommerce/src/products/products.module.ts +++ b/server/ecommerce/src/products/products.module.ts @@ -15,6 +15,8 @@ import { ProductNotification } from 'src/utils/entities/product-notification.ent import { StripeService } from 'src/stripe/stripe.service'; import { NodemailerService } from 'src/nodemailer/nodemailer.service'; import { ItemNotifier } from 'src/discord-bot/src/commands/notifiers/item-notifier'; +import { DiscordNotificationsService } from 'src/discord-notifications/discord-notifications.service'; +import { ItemNotifierService } from 'src/discord-bot/src/commands/notifiers/item-notifier.service'; @Module({ controllers: [ProductsController], @@ -24,6 +26,8 @@ import { ItemNotifier } from 'src/discord-bot/src/commands/notifiers/item-notifi ProductNotificationService, StripeService, NodemailerService, + ItemNotifierService, + DiscordNotificationsService, ], imports: [ TypeOrmModule.forFeature([ diff --git a/server/ecommerce/src/products/products.service.ts b/server/ecommerce/src/products/products.service.ts index 878c0ba..524063f 100644 --- a/server/ecommerce/src/products/products.service.ts +++ b/server/ecommerce/src/products/products.service.ts @@ -19,6 +19,7 @@ import { randomUUID } from 'crypto'; import { UsersService } from 'src/users/users.service'; import 'dotenv/config'; import { ItemNotifier } from 'src/discord-bot/src/commands/notifiers/item-notifier'; +import { ItemNotifierService } from 'src/discord-bot/src/commands/notifiers/item-notifier.service'; const s3 = new S3Client({ region: process.env.BUCKET_REGION, @@ -37,6 +38,7 @@ export class ProductsService { @InjectRepository(Image) private readonly imageRepository: Repository, private usersService: UsersService, + private itemNotifierService: ItemNotifierService, ) { this.logger = new Logger(ProductsService.name); } @@ -324,7 +326,7 @@ export class ProductsService { newProduct, ); const command = new PutObjectCommand(paramsToS3); - + await this.itemNotifierService.notifyUsers(userId, newProduct); try { await s3.send(command); } catch (error) {