From 2d57406d06ad528fd5a77c2fac20f8ba28b5a0e7 Mon Sep 17 00:00:00 2001 From: priyanshu Date: Tue, 1 Apr 2025 01:57:23 +0530 Subject: [PATCH 1/3] feat: Default replies for new app users - ensures that both admin and users gets it - tried to make it most optimised(not sure) --- QuickRepliesApp.ts | 123 ++++++++++++++++++++++++++++++++++++- src/data/DefaultReplies.ts | 34 ++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/data/DefaultReplies.ts diff --git a/QuickRepliesApp.ts b/QuickRepliesApp.ts index 48a0d53..be30a82 100644 --- a/QuickRepliesApp.ts +++ b/QuickRepliesApp.ts @@ -1,5 +1,6 @@ import { IAppAccessors, + IAppInstallationContext, IConfigurationExtend, IEnvironmentRead, IHttp, @@ -9,7 +10,7 @@ import { IRead, } from '@rocket.chat/apps-engine/definition/accessors'; import { App } from '@rocket.chat/apps-engine/definition/App'; -import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import { IAppInfo, RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; import { QuickCommand } from './src/commands/QuickCommand'; import { IUIKitResponse, @@ -32,10 +33,15 @@ import { import { ActionButton } from './src/enum/modals/common/ActionButtons'; import { ExecuteActionButtonHandler } from './src/handlers/ExecuteActionButtonHandler'; import { settings } from './src/config/settings'; +import { ReplyStorage } from './src/storage/ReplyStorage'; +import { getDefaultReplies } from './src/data/DefaultReplies'; +import { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { Language } from './src/lib/Translation/translation'; export class QuickRepliesApp extends App { private elementBuilder: ElementBuilder; private blockBuilder: BlockBuilder; + constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } @@ -91,6 +97,113 @@ export class QuickRepliesApp extends App { blockBuilder: this.blockBuilder, }; } + + /** + * Get the association records for tracking user initialization status + */ + private getInitAssociations(userId: string): RocketChatAssociationRecord[] { + return [ + new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + userId, + ), + new RocketChatAssociationRecord( + RocketChatAssociationModel.MISC, + 'initialized_replies' + ), + ]; + } + + /** + * Check if a user has been initialized with default replies + */ + private async isUserInitialized(user: IUser, read: IRead): Promise { + try { + const association = this.getInitAssociations(user.id); + const result = await read.getPersistenceReader().readByAssociations(association); + return result && result.length > 0; + } catch (error) { + this.getLogger().error(`Error checking initialization status: ${error}`); + return false; + } + } + + /** + * Mark a user as initialized in persistent storage + */ + private async markUserAsInitialized(user: IUser, persistence: IPersistence): Promise { + try { + const association = this.getInitAssociations(user.id); + await persistence.updateByAssociations( + association, + { initialized: true, timestamp: new Date().toISOString() }, + true + ); + this.getLogger().debug(`User ${user.id} marked as initialized in persistence`); + } catch (error) { + this.getLogger().error(`Error marking user as initialized: ${error}`); + } + } + + /** + * Initialize default quick replies for a user who hasn't used the app before + + */ + public async initializeDefaultRepliesForUser( + user: IUser, + read: IRead, + persistence: IPersistence + ): Promise { + try { + // Check if the user has already been initialized using persistent storage + if (await this.isUserInitialized(user, read)) { + this.getLogger().debug(`User ${user.id} already initialized, skipping`); + return; + } + + const replyStorage = new ReplyStorage(persistence, read.getPersistenceReader()); + const existingReplies = await replyStorage.getReplyForUser(user); + + // Only initialize if the user doesn't have any replies yet + if (existingReplies.length === 0) { + const defaultReplies = getDefaultReplies(user.id); + + for (const reply of defaultReplies) { + await replyStorage.createReply( + user, + reply.name, + reply.body, + Language.en + ); + } + + this.getLogger().info(`Initialized default quick replies for user: ${user.id}`); + } + + // Mark the user as initialized regardless of whether we added replies + // This prevents checking the replies again in the future + await this.markUserAsInitialized(user, persistence); + } catch (error) { + this.getLogger().error(`Error initializing default replies for user: ${error}`); + } + } + + public async onInstall( + context: IAppInstallationContext, + read: IRead, + http: IHttp, + persistence: IPersistence, + modify: IModify + ): Promise { + try { + // Initialize for the admin/installer user + await this.initializeDefaultRepliesForUser(context.user, read, persistence); + this.getLogger().info('Successfully initialized default replies for admin during installation'); + } catch (error) { + this.getLogger().error(`Error in onInstall: ${error}`); + } + } + public async executeViewSubmitHandler( context: UIKitViewSubmitInteractionContext, read: IRead, @@ -98,6 +211,7 @@ export class QuickRepliesApp extends App { persistence: IPersistence, modify: IModify, ) { + const handler = new ExecuteViewSubmitHandler( this, read, @@ -109,6 +223,7 @@ export class QuickRepliesApp extends App { return await handler.handleActions(); } + public async executeViewClosedHandler( context: UIKitViewCloseInteractionContext, read: IRead, @@ -135,6 +250,9 @@ export class QuickRepliesApp extends App { persistence: IPersistence, modify: IModify, ): Promise { + // Check and initialize default replies for the user + await this.initializeDefaultRepliesForUser(context.getInteractionData().user, read, persistence); + const handler = new ExecuteBlockActionHandler( this, read, @@ -154,6 +272,9 @@ export class QuickRepliesApp extends App { persistence: IPersistence, modify: IModify, ): Promise { + // Check and initialize default replies for the user + await this.initializeDefaultRepliesForUser(context.getInteractionData().user, read, persistence); + const handler = new ExecuteActionButtonHandler( this, read, diff --git a/src/data/DefaultReplies.ts b/src/data/DefaultReplies.ts new file mode 100644 index 0000000..ed978f6 --- /dev/null +++ b/src/data/DefaultReplies.ts @@ -0,0 +1,34 @@ +import { IReply } from '../definition/reply/IReply'; + +/** + * Collection of pre-built default quick replies that will be added for new users + */ +export const getDefaultReplies = (userId: string): IReply[] => { + return [ + { + name: 'Greeting', + body: 'Hello! How may I assist you today?', + id: `${userId}-${(Date.now() - 10).toString(36)}`, + }, + { + name: 'Acknowledgment', + body: 'Thank you for reaching out. I will get back to you shortly.', + id: `${userId}-${(Date.now() - 5).toString(36)}`, + }, + { + name: 'Follow-up', + body: 'I wanted to follow up on our previous discussion. Please let me know how you\'d like to proceed.', + id: `${userId}-${Date.now().toString(36)}`, + }, + { + name: 'Apology', + body: 'I sincerely apologize for any inconvenience. We are looking into this and will resolve it as soon as possible.', + id: `${userId}-${(Date.now() + 5).toString(36)}`, + }, + { + name: 'Closing', + body: 'It was a pleasure assisting you. Please feel free to reach out for any further queries.', + id: `${userId}-${(Date.now() + 10).toString(36)}`, + }, + ]; +}; From 48758141887695339b3b2bd0959e171dffe5c6b4 Mon Sep 17 00:00:00 2001 From: priyanshu Date: Tue, 1 Apr 2025 02:05:27 +0530 Subject: [PATCH 2/3] Delete excess comments --- QuickRepliesApp.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/QuickRepliesApp.ts b/QuickRepliesApp.ts index be30a82..1d8465a 100644 --- a/QuickRepliesApp.ts +++ b/QuickRepliesApp.ts @@ -180,8 +180,6 @@ export class QuickRepliesApp extends App { this.getLogger().info(`Initialized default quick replies for user: ${user.id}`); } - // Mark the user as initialized regardless of whether we added replies - // This prevents checking the replies again in the future await this.markUserAsInitialized(user, persistence); } catch (error) { this.getLogger().error(`Error initializing default replies for user: ${error}`); From 76a98b8a826992be6311caae17a756fe1ace18a7 Mon Sep 17 00:00:00 2001 From: priyanshu Date: Wed, 23 Apr 2025 19:49:10 +0530 Subject: [PATCH 3/3] refactor: user initialization logic into separate UserDefaultReplies handler --- QuickRepliesApp.ts | 93 +++----------------------- app.json | 104 ++++++++++++++--------------- src/handlers/UserDefaultReplies.ts | 95 ++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 137 deletions(-) create mode 100644 src/handlers/UserDefaultReplies.ts diff --git a/QuickRepliesApp.ts b/QuickRepliesApp.ts index 1d8465a..24578e7 100644 --- a/QuickRepliesApp.ts +++ b/QuickRepliesApp.ts @@ -10,7 +10,7 @@ import { IRead, } from '@rocket.chat/apps-engine/definition/accessors'; import { App } from '@rocket.chat/apps-engine/definition/App'; -import { IAppInfo, RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; +import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import { QuickCommand } from './src/commands/QuickCommand'; import { IUIKitResponse, @@ -33,10 +33,8 @@ import { import { ActionButton } from './src/enum/modals/common/ActionButtons'; import { ExecuteActionButtonHandler } from './src/handlers/ExecuteActionButtonHandler'; import { settings } from './src/config/settings'; -import { ReplyStorage } from './src/storage/ReplyStorage'; -import { getDefaultReplies } from './src/data/DefaultReplies'; import { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { Language } from './src/lib/Translation/translation'; +import { UserInitStorage } from './src/handlers/UserDefaultReplies'; export class QuickRepliesApp extends App { private elementBuilder: ElementBuilder; @@ -98,92 +96,18 @@ export class QuickRepliesApp extends App { }; } - /** - * Get the association records for tracking user initialization status - */ - private getInitAssociations(userId: string): RocketChatAssociationRecord[] { - return [ - new RocketChatAssociationRecord( - RocketChatAssociationModel.USER, - userId, - ), - new RocketChatAssociationRecord( - RocketChatAssociationModel.MISC, - 'initialized_replies' - ), - ]; - } - - /** - * Check if a user has been initialized with default replies - */ - private async isUserInitialized(user: IUser, read: IRead): Promise { - try { - const association = this.getInitAssociations(user.id); - const result = await read.getPersistenceReader().readByAssociations(association); - return result && result.length > 0; - } catch (error) { - this.getLogger().error(`Error checking initialization status: ${error}`); - return false; - } - } - - /** - * Mark a user as initialized in persistent storage - */ - private async markUserAsInitialized(user: IUser, persistence: IPersistence): Promise { - try { - const association = this.getInitAssociations(user.id); - await persistence.updateByAssociations( - association, - { initialized: true, timestamp: new Date().toISOString() }, - true - ); - this.getLogger().debug(`User ${user.id} marked as initialized in persistence`); - } catch (error) { - this.getLogger().error(`Error marking user as initialized: ${error}`); - } - } - - /** - * Initialize default quick replies for a user who hasn't used the app before - - */ public async initializeDefaultRepliesForUser( user: IUser, read: IRead, persistence: IPersistence ): Promise { - try { - // Check if the user has already been initialized using persistent storage - if (await this.isUserInitialized(user, read)) { - this.getLogger().debug(`User ${user.id} already initialized, skipping`); - return; - } - - const replyStorage = new ReplyStorage(persistence, read.getPersistenceReader()); - const existingReplies = await replyStorage.getReplyForUser(user); - - // Only initialize if the user doesn't have any replies yet - if (existingReplies.length === 0) { - const defaultReplies = getDefaultReplies(user.id); - - for (const reply of defaultReplies) { - await replyStorage.createReply( - user, - reply.name, - reply.body, - Language.en - ); - } - - this.getLogger().info(`Initialized default quick replies for user: ${user.id}`); - } + const userInitStorage = new UserInitStorage( + persistence, + read.getPersistenceReader(), + this.getLogger() + ); - await this.markUserAsInitialized(user, persistence); - } catch (error) { - this.getLogger().error(`Error initializing default replies for user: ${error}`); - } + await userInitStorage.initializeDefaultRepliesForUser(user); } public async onInstall( @@ -191,7 +115,6 @@ export class QuickRepliesApp extends App { read: IRead, http: IHttp, persistence: IPersistence, - modify: IModify ): Promise { try { // Initialize for the admin/installer user diff --git a/app.json b/app.json index ac25ae3..5e1fa09 100644 --- a/app.json +++ b/app.json @@ -1,54 +1,54 @@ { - "id": "e664d2cb-7beb-413a-837a-80fd840c387b", - "version": "0.0.1", - "requiredApiVersion": "^1.44.0", - "iconFile": "icon.png", - "author": { - "name": "Vipin Chaudhary", - "homepage": "https://github.com/RocketChat/Apps.QuickReplies", - "support": "https://github.com/RocketChat/Apps.QuickReplies/issues" - }, - "name": "QuickReplies", - "nameSlug": "quickreplies", - "classFile": "QuickRepliesApp.ts", - "description": "Instantly craft and send customizable responses within Rocket.Chat.", - "implements": [], - "permissions": [ - { - "name": "ui.registerButtons" - }, - { - "name": "api" - }, - { - "name": "slashcommand" - }, - { - "name": "server-setting.read" - }, - { - "name": "room.read" - }, - { - "name": "persistence" - }, - { - "name": "ui.interact" - }, - { - "name": "networking" - }, - { - "name": "message.write" - }, - { - "name": "user.read" - }, - { - "name": "room.write" - }, - { - "name": "message.read" - } - ] + "id": "e664d2cb-7beb-413a-837a-80fd840c387b", + "version": "0.0.1", + "requiredApiVersion": "^1.44.0", + "iconFile": "icon.png", + "author": { + "name": "Vipin Chaudhary", + "homepage": "https://github.com/RocketChat/Apps.QuickReplies", + "support": "https://github.com/RocketChat/Apps.QuickReplies/issues" + }, + "name": "QuickReplies", + "nameSlug": "quickreplies", + "classFile": "QuickRepliesApp.ts", + "description": "Instantly craft and send customizable responses within Rocket.Chat.", + "implements": [], + "permissions": [ + { + "name": "ui.registerButtons" + }, + { + "name": "api" + }, + { + "name": "slashcommand" + }, + { + "name": "server-setting.read" + }, + { + "name": "room.read" + }, + { + "name": "persistence" + }, + { + "name": "ui.interact" + }, + { + "name": "networking" + }, + { + "name": "message.write" + }, + { + "name": "user.read" + }, + { + "name": "room.write" + }, + { + "name": "message.read" + } + ] } \ No newline at end of file diff --git a/src/handlers/UserDefaultReplies.ts b/src/handlers/UserDefaultReplies.ts new file mode 100644 index 0000000..2f6c23e --- /dev/null +++ b/src/handlers/UserDefaultReplies.ts @@ -0,0 +1,95 @@ +import { + RocketChatAssociationModel, + RocketChatAssociationRecord, +} from '@rocket.chat/apps-engine/definition/metadata'; +import { + IPersistence, + IPersistenceRead, + ILogger, +} from '@rocket.chat/apps-engine/definition/accessors'; +import { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { ReplyStorage } from '../storage/ReplyStorage'; +import { getDefaultReplies } from '../data/DefaultReplies'; +import { Language } from '../lib/Translation/translation'; + +export class UserInitStorage { + constructor( + private readonly persistence: IPersistence, + private readonly persistenceRead: IPersistenceRead, + private readonly logger: ILogger, + ) {} + + /** + * Check if a user has been initialized with default replies + */ + public async isUserInitialized(user: IUser): Promise { + try { + const association = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + `${user.id}#initialized_replies`, + ); + const result = await this.persistenceRead.readByAssociation(association); + + return result && result.length > 0; + } catch (error) { + this.logger.error(`Error checking initialization status: ${error}`); + return false; + } + } + + /** + * Mark a user as initialized in persistent storage + */ + public async markUserAsInitialized(user: IUser): Promise { + try { + const association = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + `${user.id}#initialized_replies`, + ); + await this.persistence.updateByAssociation( + association, + { initialized: true, timestamp: new Date().toISOString() }, + true + ); + this.logger.debug(`User ${user.id} marked as initialized in persistence`); + } catch (error) { + this.logger.error(`Error marking user as initialized: ${error}`); + } + } + + /** + * Initialize default quick replies for a user who hasn't used the app before + */ + public async initializeDefaultRepliesForUser(user: IUser): Promise { + try { + // Check if the user has already been initialized + if (await this.isUserInitialized(user)) { + this.logger.debug(`User ${user.id} already initialized, skipping`); + return; + } + + const replyStorage = new ReplyStorage(this.persistence, this.persistenceRead); + const existingReplies = await replyStorage.getReplyForUser(user); + + // Only initialize if the user doesn't have any replies yet + if (existingReplies.length === 0) { + const defaultReplies = getDefaultReplies(user.id); + + for (const reply of defaultReplies) { + await replyStorage.createReply( + user, + reply.name, + reply.body, + Language.en + ); + } + + this.logger.info(`Initialized default quick replies for user: ${user.id}`); + } + + await this.markUserAsInitialized(user); + } catch (error) { + this.logger.error(`Error initializing default replies for user: ${error}`); + } + } +}