From 83b4f9c2bd138f994751e169d1b9bf93efc4e581 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Wed, 31 May 2023 18:32:41 +0530 Subject: [PATCH 01/29] feat: credentials settings for authorization --- NotionApp.ts | 14 ++++++++++++++ config/settings.ts | 37 +++++++++++++++++++++++++++++++++++++ i18n/en.json | 7 +++++++ 3 files changed, 58 insertions(+) create mode 100644 config/settings.ts create mode 100644 i18n/en.json diff --git a/NotionApp.ts b/NotionApp.ts index 613ac8a..8ff9d7e 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -1,12 +1,26 @@ import { IAppAccessors, + IConfigurationExtend, + IEnvironmentRead, ILogger, } 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 { settings } from "./config/settings"; export class NotionApp extends App { constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } + + public async initialize( + configurationExtend: IConfigurationExtend, + environmentRead: IEnvironmentRead + ): Promise { + await Promise.all( + settings.map((setting) => { + configurationExtend.settings.provideSetting(setting); + }) + ); + } } diff --git a/config/settings.ts b/config/settings.ts new file mode 100644 index 0000000..2f1db51 --- /dev/null +++ b/config/settings.ts @@ -0,0 +1,37 @@ +import { + ISetting, + SettingType, +} from "@rocket.chat/apps-engine/definition/settings"; + +// The settings that will be available for the App +enum OAuth2Setting { + CLIENT_ID = "notion-client-id", + CLIENT_SECRET = "notion-client-secret", +} + +export const settings: Array = [ + { + id: OAuth2Setting.CLIENT_ID, + type: SettingType.STRING, + packageValue: "", + required: true, + public: false, + section: "CredentialSettings", + i18nLabel: "ClientIdLabel", + i18nPlaceholder: "ClientIdPlaceholder", + hidden: false, + multiline: false, + }, + { + id: OAuth2Setting.CLIENT_SECRET, + type: SettingType.PASSWORD, + packageValue: "", + required: true, + public: false, + section: "CredentialSettings", + i18nLabel: "ClientSecretLabel", + i18nPlaceholder: "ClientSecretPlaceholder", + hidden: false, + multiline: false, + }, +]; diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..795fb9d --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,7 @@ +{ + "ClientIdLabel": "notion-client-id", + "ClientIdPlaceholder": "paste your clientId here", + "ClientSecretLabel": "notion-client-secret", + "ClientSecretPlaceholder": "Shhh! This is super secret", + "CredentialsSettings": "Authorization Settings" +} From fa0f9f4aa16eab80742911f133e28bbc5d4d44af Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 1 Jun 2023 13:00:31 +0530 Subject: [PATCH 02/29] feat(task): created IOAuth2Client and OAuth2Client --- NotionApp.ts | 8 +++++++ definition/authorization/IOAuthClient.ts | 25 ++++++++++++++++++++ src/authorization/OAuth2Client.ts | 30 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 definition/authorization/IOAuthClient.ts create mode 100644 src/authorization/OAuth2Client.ts diff --git a/NotionApp.ts b/NotionApp.ts index 8ff9d7e..0be457a 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -7,8 +7,10 @@ import { import { App } from "@rocket.chat/apps-engine/definition/App"; import { IAppInfo } from "@rocket.chat/apps-engine/definition/metadata"; import { settings } from "./config/settings"; +import { OAuth2Client } from "./src/authorization/OAuth2Client"; export class NotionApp extends App { + private oAuth2Client: OAuth2Client; constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } @@ -22,5 +24,11 @@ export class NotionApp extends App { configurationExtend.settings.provideSetting(setting); }) ); + + this.oAuth2Client = new OAuth2Client(this); + } + + public getOAuth2Client(): OAuth2Client { + return this.oAuth2Client; } } diff --git a/definition/authorization/IOAuthClient.ts b/definition/authorization/IOAuthClient.ts new file mode 100644 index 0000000..dafb9f2 --- /dev/null +++ b/definition/authorization/IOAuthClient.ts @@ -0,0 +1,25 @@ +import { + IHttp, + IModify, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; + +export interface IOAuth2Client { + connect( + context: SlashCommandContext, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence + ): Promise; + + disconnect( + context: SlashCommandContext, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence + ): Promise; +} diff --git a/src/authorization/OAuth2Client.ts b/src/authorization/OAuth2Client.ts new file mode 100644 index 0000000..7793beb --- /dev/null +++ b/src/authorization/OAuth2Client.ts @@ -0,0 +1,30 @@ +import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; +import { NotionApp } from "../../NotionApp"; +import { + IHttp, + IModify, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { IOAuth2Client } from "../../definition/authorization/IOAuthClient"; + +export class OAuth2Client implements IOAuth2Client { + constructor(private readonly app: NotionApp) {} + public async connect( + context: SlashCommandContext, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence + ) {} + + public async disconnect( + context: SlashCommandContext, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence + ) {} + + private async getCredentials(read: IRead) {} +} From b8f2337486ff26c1a16657398ae9410f2e594273 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 1 Jun 2023 13:44:28 +0530 Subject: [PATCH 03/29] feat(task): created CommandUtility and Registered Command --- NotionApp.ts | 4 ++ definition/command/ICommandUtility.ts | 35 +++++++++++ enum/CommandParam.ts | 4 ++ i18n/en.json | 4 +- src/commands/CommandUtility.ts | 85 +++++++++++++++++++++++++++ src/commands/NotionCommand.ts | 42 +++++++++++++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 definition/command/ICommandUtility.ts create mode 100644 enum/CommandParam.ts create mode 100644 src/commands/CommandUtility.ts create mode 100644 src/commands/NotionCommand.ts diff --git a/NotionApp.ts b/NotionApp.ts index 0be457a..eeadcfb 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -8,6 +8,7 @@ import { App } from "@rocket.chat/apps-engine/definition/App"; import { IAppInfo } from "@rocket.chat/apps-engine/definition/metadata"; import { settings } from "./config/settings"; import { OAuth2Client } from "./src/authorization/OAuth2Client"; +import { NotionCommand } from "./src/commands/NotionCommand"; export class NotionApp extends App { private oAuth2Client: OAuth2Client; @@ -19,6 +20,9 @@ export class NotionApp extends App { configurationExtend: IConfigurationExtend, environmentRead: IEnvironmentRead ): Promise { + await configurationExtend.slashCommands.provideSlashCommand( + new NotionCommand(this) + ); await Promise.all( settings.map((setting) => { configurationExtend.settings.provideSetting(setting); diff --git a/definition/command/ICommandUtility.ts b/definition/command/ICommandUtility.ts new file mode 100644 index 0000000..c62d774 --- /dev/null +++ b/definition/command/ICommandUtility.ts @@ -0,0 +1,35 @@ +import { + IHttp, + IModify, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { NotionApp } from "../../NotionApp"; +import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; + +export interface ICommandUtility { + app: NotionApp; + context: SlashCommandContext; + params: Array; + sender: IUser; + room: IRoom; + read: IRead; + modify: IModify; + http: IHttp; + persis: IPersistence; + triggerId?: string; + threadId?: string; + + resolveCommand(): Promise; +} + +export interface ICommandUtilityParams { + context: SlashCommandContext; + read: IRead; + modify: IModify; + http: IHttp; + persis: IPersistence; + app: NotionApp; +} diff --git a/enum/CommandParam.ts b/enum/CommandParam.ts new file mode 100644 index 0000000..7f34d19 --- /dev/null +++ b/enum/CommandParam.ts @@ -0,0 +1,4 @@ +export enum CommandParam { + CONNECT = "connect", + DISCONNECT = "disconnect", +} diff --git a/i18n/en.json b/i18n/en.json index 795fb9d..dca28f1 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3,5 +3,7 @@ "ClientIdPlaceholder": "paste your clientId here", "ClientSecretLabel": "notion-client-secret", "ClientSecretPlaceholder": "Shhh! This is super secret", - "CredentialsSettings": "Authorization Settings" + "CredentialsSettings": "Authorization Settings", + "NotionCommandParams": "connect | disconnect | workspace | create | schema | comment", + "NotionCommandDescription": "Create Notion pages and database from Rocket.Chat" } diff --git a/src/commands/CommandUtility.ts b/src/commands/CommandUtility.ts new file mode 100644 index 0000000..6b0a8aa --- /dev/null +++ b/src/commands/CommandUtility.ts @@ -0,0 +1,85 @@ +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { NotionApp } from "../../NotionApp"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { + IHttp, + IModify, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { + ICommandUtility, + ICommandUtilityParams, +} from "../../definition/command/ICommandUtility"; +import { CommandParam } from "../../enum/CommandParam"; +import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; + +export class CommandUtility implements ICommandUtility { + public app: NotionApp; + public context: SlashCommandContext; + public params: Array; + public sender: IUser; + public room: IRoom; + public read: IRead; + public modify: IModify; + public http: IHttp; + public persis: IPersistence; + public triggerId?: string; + public threadId?: string; + + constructor(props: ICommandUtilityParams) { + this.app = props.app; + this.context = props.context; + this.read = props.read; + this.modify = props.modify; + this.http = props.http; + this.persis = props.persis; + this.params = props.context.getArguments(); + this.sender = props.context.getSender(); + this.room = props.context.getRoom(); + this.triggerId = props.context.getTriggerId(); + this.threadId = props.context.getThreadId(); + } + + public async resolveCommand(): Promise { + switch (this.params.length) { + case 0: { + break; + } + case 1: { + await this.handleSingleParam(); + break; + } + default: { + } + } + } + + private async handleSingleParam(): Promise { + const oAuth2ClientInstance = await this.app.getOAuth2Client(); + switch (this.params[0]) { + case CommandParam.CONNECT: { + await oAuth2ClientInstance.connect( + this.context, + this.read, + this.modify, + this.http, + this.persis + ); + break; + } + case CommandParam.DISCONNECT: { + await oAuth2ClientInstance.disconnect( + this.context, + this.read, + this.modify, + this.http, + this.persis + ); + break; + } + default: { + } + } + } +} diff --git a/src/commands/NotionCommand.ts b/src/commands/NotionCommand.ts new file mode 100644 index 0000000..72ba186 --- /dev/null +++ b/src/commands/NotionCommand.ts @@ -0,0 +1,42 @@ +import { + ISlashCommand, + SlashCommandContext, +} from "@rocket.chat/apps-engine/definition/slashcommands"; +import { NotionApp } from "../../NotionApp"; +import { + IHttp, + IModify, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { ICommandUtilityParams } from "../../definition/command/ICommandUtility"; +import { CommandUtility } from "./CommandUtility"; + +export class NotionCommand implements ISlashCommand { + constructor(private readonly app: NotionApp) {} + + public command: string = "notion"; + public i18nParamsExample: string = "NotionCommandParams"; + public i18nDescription: string = "NotionCommandDescription"; + public providesPreview: boolean = false; + + public async executor( + context: SlashCommandContext, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence + ): Promise { + const commandUtilityParams: ICommandUtilityParams = { + context, + read, + modify, + http, + persis, + app: this.app, + }; + + const commandUtility = new CommandUtility(commandUtilityParams); + await commandUtility.resolveCommand(); + } +} From fbe424ef3e6bf664ce3c4641e54a7089de5d9694 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 1 Jun 2023 15:52:51 +0530 Subject: [PATCH 04/29] feat(error): Created IError and Common ErrorHandler --- definition/errors/IError.ts | 4 ++ enum/Error.ts | 8 ++++ errors/Error.ts | 95 +++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 definition/errors/IError.ts create mode 100644 enum/Error.ts create mode 100644 errors/Error.ts diff --git a/definition/errors/IError.ts b/definition/errors/IError.ts new file mode 100644 index 0000000..c585b39 --- /dev/null +++ b/definition/errors/IError.ts @@ -0,0 +1,4 @@ +export interface IError extends Error { + status: number; + additionalInfo?: string; +} diff --git a/enum/Error.ts b/enum/Error.ts new file mode 100644 index 0000000..e6bdb3a --- /dev/null +++ b/enum/Error.ts @@ -0,0 +1,8 @@ +export enum ErrorName { + BAD_REQUEST = "Bad Request", + UNAUTHORIZED = "Unauthorized", + SERVER_ERROR = "Internal Server Error", + FORBIDDEN = "Forbidden", + MANY_REQUESTS = "Too Many Requests", + NOT_FOUND = "Not Found", +} diff --git a/errors/Error.ts b/errors/Error.ts new file mode 100644 index 0000000..02fb594 --- /dev/null +++ b/errors/Error.ts @@ -0,0 +1,95 @@ +/* + - This file contains all the errors that can be handle by the application includes: 400, 401, 403, 404, 429, 500 + - The error name is the type of the error + - The message is the message that will be logged in the console + - The status is the status code of the error + - The additionalInfo is the additional information that will be logged in the console +*/ +import { IError } from "../definition/errors/IError"; +import { HttpStatusCode } from "@rocket.chat/apps-engine/definition/accessors"; +import { ErrorName } from "../enum/Error"; + +class Error implements IError { + name: string; + message: string; + status: number; + additionalInfo?: string; + + constructor( + name: string, + message: string, + status: number, + additionalInfo?: string + ) { + this.name = name; + this.message = message; + this.status = status; + this.additionalInfo = additionalInfo; + } +} + +export class ClientError extends Error { + constructor(message: string, additionalInfo?: string) { + super( + ErrorName.BAD_REQUEST, + message, + HttpStatusCode.BAD_REQUEST, + additionalInfo + ); + } +} + +export class NotFoundError extends Error { + constructor(message: string, additionalInfo?: string) { + super( + ErrorName.NOT_FOUND, + message, + HttpStatusCode.NOT_FOUND, + additionalInfo + ); + } +} + +export class UnAuthorizedError extends Error { + constructor(message: string, additionalInfo?: string) { + super( + ErrorName.UNAUTHORIZED, + message, + HttpStatusCode.UNAUTHORIZED, + additionalInfo + ); + } +} + +export class ForbiddenError extends Error { + constructor(message: string, additionalInfo?: string) { + super( + ErrorName.FORBIDDEN, + message, + HttpStatusCode.FORBIDDEN, + additionalInfo + ); + } +} + +export class ServerError extends Error { + constructor(message: string, additionalInfo?: string) { + super( + ErrorName.SERVER_ERROR, + message, + HttpStatusCode.INTERNAL_SERVER_ERROR, + additionalInfo + ); + } +} + +export class ManyRequestsError extends Error { + constructor(message: string, additionalInfo?: string) { + super( + ErrorName.MANY_REQUESTS, + message, + HttpStatusCode.TOO_MANY_REQUESTS, + additionalInfo + ); + } +} From 57c4fd0600da251eb34fa1fa27f8ee0c79d655a6 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Thu, 1 Jun 2023 16:21:17 +0530 Subject: [PATCH 05/29] feat: Created getCredential Helper for Authorization --- config/settings.ts | 3 ++- definition/authorization/ICredential.ts | 5 +++++ enum/Settings.ts | 3 +++ src/authorization/OAuth2Client.ts | 2 -- src/helper/getCredential.ts | 21 +++++++++++++++++++++ 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 definition/authorization/ICredential.ts create mode 100644 enum/Settings.ts create mode 100644 src/helper/getCredential.ts diff --git a/config/settings.ts b/config/settings.ts index 2f1db51..719022e 100644 --- a/config/settings.ts +++ b/config/settings.ts @@ -4,7 +4,8 @@ import { } from "@rocket.chat/apps-engine/definition/settings"; // The settings that will be available for the App -enum OAuth2Setting { +// warning(AppsEngine Error): Having OAuth2Setting in enums folder causing an error in deployment reason not known. +export enum OAuth2Setting { CLIENT_ID = "notion-client-id", CLIENT_SECRET = "notion-client-secret", } diff --git a/definition/authorization/ICredential.ts b/definition/authorization/ICredential.ts new file mode 100644 index 0000000..cbdb300 --- /dev/null +++ b/definition/authorization/ICredential.ts @@ -0,0 +1,5 @@ +export interface ICredential { + clientId: string; + clientSecret: string; + siteUrl: string; +} diff --git a/enum/Settings.ts b/enum/Settings.ts new file mode 100644 index 0000000..3699b7b --- /dev/null +++ b/enum/Settings.ts @@ -0,0 +1,3 @@ +export enum ServerSetting { + SITE_URL = "Site_Url", +} diff --git a/src/authorization/OAuth2Client.ts b/src/authorization/OAuth2Client.ts index 7793beb..28c40a9 100644 --- a/src/authorization/OAuth2Client.ts +++ b/src/authorization/OAuth2Client.ts @@ -25,6 +25,4 @@ export class OAuth2Client implements IOAuth2Client { http: IHttp, persis: IPersistence ) {} - - private async getCredentials(read: IRead) {} } diff --git a/src/helper/getCredential.ts b/src/helper/getCredential.ts new file mode 100644 index 0000000..04d091d --- /dev/null +++ b/src/helper/getCredential.ts @@ -0,0 +1,21 @@ +import { IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { ICredential } from "../../definition/authorization/ICredential"; +import { OAuth2Setting } from "../../config/settings"; +import { ServerSetting } from "../../enum/Settings"; + +async function getCredentials(read: IRead): Promise { + const clientId = (await read + .getEnvironmentReader() + .getSettings() + .getValueById(OAuth2Setting.CLIENT_ID)) as string; + const clientSecret = (await read + .getEnvironmentReader() + .getSettings() + .getValueById(OAuth2Setting.CLIENT_SECRET)) as string; + const siteUrl = (await read + .getEnvironmentReader() + .getServerSettings() + .getValueById(ServerSetting.SITE_URL)) as string; + + return { clientId, clientSecret, siteUrl }; +} From 7111cfd4b0696b9eafc7f298e01e1fefe9364071 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Fri, 2 Jun 2023 09:55:38 +0530 Subject: [PATCH 06/29] fix: getCredentials() to return siteUrlWithoutTrailingSlash --- src/helper/getCredential.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/helper/getCredential.ts b/src/helper/getCredential.ts index 04d091d..806de2c 100644 --- a/src/helper/getCredential.ts +++ b/src/helper/getCredential.ts @@ -3,7 +3,7 @@ import { ICredential } from "../../definition/authorization/ICredential"; import { OAuth2Setting } from "../../config/settings"; import { ServerSetting } from "../../enum/Settings"; -async function getCredentials(read: IRead): Promise { +export async function getCredentials(read: IRead): Promise { const clientId = (await read .getEnvironmentReader() .getSettings() @@ -12,10 +12,14 @@ async function getCredentials(read: IRead): Promise { .getEnvironmentReader() .getSettings() .getValueById(OAuth2Setting.CLIENT_SECRET)) as string; - const siteUrl = (await read + let siteUrl = (await read .getEnvironmentReader() .getServerSettings() .getValueById(ServerSetting.SITE_URL)) as string; + if (siteUrl.endsWith("/")) { + siteUrl = siteUrl.substring(0, siteUrl.length - 1); + } + return { clientId, clientSecret, siteUrl }; } From fef69d897b2d4f7fd307296c0c659af8c91e1628 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Fri, 2 Jun 2023 12:53:27 +0530 Subject: [PATCH 07/29] feat: created OAuth2Storage with Methods to connect, get and remove tokenInfo --- definition/authorization/IOAuth2Storage.ts | 39 +++++++++++ enum/Notion.ts | 17 +++++ src/authorization/OAuth2Storage.ts | 76 ++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 definition/authorization/IOAuth2Storage.ts create mode 100644 enum/Notion.ts create mode 100644 src/authorization/OAuth2Storage.ts diff --git a/definition/authorization/IOAuth2Storage.ts b/definition/authorization/IOAuth2Storage.ts new file mode 100644 index 0000000..dd5e330 --- /dev/null +++ b/definition/authorization/IOAuth2Storage.ts @@ -0,0 +1,39 @@ +import { NotionOwnerType, NotionTokenType } from "../../enum/Notion"; + +export interface IOAuth2Storage { + connectUserToWorkspace( + tokenInfo: ITokenInfo, + userId: string + ): Promise; + getCurrentWorkspace(userId: string): Promise; + disconnectUserFromCurrentWorkspace(userId: string): Promise; +} + +export interface ITokenInfo { + access_token: string; + token_type: NotionTokenType.TOKEN_TYPE; + bot_id: string; + workspace_icon: string | null; + workspace_id: string; + workspace_name: string | null; + owner: INotionOwner; + duplicated_template_id: string | null; +} + +interface INotionOwner { + type: NotionOwnerType.USER; + user: INotionUser; +} + +interface INotionUser { + object: NotionOwnerType.USER; + id: string; + name: string | null; + avatar_url: string | null; + type: NotionOwnerType.PERSON; + person: INotionPerson; +} + +interface INotionPerson { + email: string; +} diff --git a/enum/Notion.ts b/enum/Notion.ts new file mode 100644 index 0000000..53becfd --- /dev/null +++ b/enum/Notion.ts @@ -0,0 +1,17 @@ +export enum NotionTokenType { + TOKEN_TYPE = "bearer", + CURRENT_WORKSPACE = "notion_current_workspace", +} + +export enum NotionOwnerType { + USER = "user", + PERSON = "person", + BOT = "bot", +} + +export enum NotionApi { + BASE_URL = "https://api.notion.com/v1", + VERSION = "2022-06-28", + USER_AGENT = "Rocket.Chat-Apps-Engine", + CONTENT_TYPE = "application/json", +} diff --git a/src/authorization/OAuth2Storage.ts b/src/authorization/OAuth2Storage.ts new file mode 100644 index 0000000..8db7579 --- /dev/null +++ b/src/authorization/OAuth2Storage.ts @@ -0,0 +1,76 @@ +import { + IPersistence, + IPersistenceRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { + IOAuth2Storage, + ITokenInfo, +} from "../../definition/authorization/IOAuth2Storage"; +import { + RocketChatAssociationModel, + RocketChatAssociationRecord, +} from "@rocket.chat/apps-engine/definition/metadata"; +import { NotionTokenType } from "../../enum/Notion"; + +export class OAuth2Storage implements IOAuth2Storage { + constructor( + private readonly persistence: IPersistence, + private readonly persistenceRead: IPersistenceRead + ) {} + public async connectUserToWorkspace( + tokenInfo: ITokenInfo, + userId: string + ): Promise { + await this.persistence.updateByAssociations( + [ + new RocketChatAssociationRecord( // user association + RocketChatAssociationModel.USER, + userId + ), + new RocketChatAssociationRecord( + RocketChatAssociationModel.MISC, + NotionTokenType.CURRENT_WORKSPACE + ), + ], + tokenInfo, + true + ); + + return; + } + + public async getCurrentWorkspace( + userId: string + ): Promise { + const [tokenInfo] = (await this.persistenceRead.readByAssociations([ + new RocketChatAssociationRecord( // user association + RocketChatAssociationModel.USER, + userId + ), + new RocketChatAssociationRecord( + RocketChatAssociationModel.MISC, + NotionTokenType.CURRENT_WORKSPACE + ), + ])) as ITokenInfo[]; + + return tokenInfo; + } + + public async disconnectUserFromCurrentWorkspace( + userId: string + ): Promise { + const [removedTokenInfo] = (await this.persistence.removeByAssociations( + [ + new RocketChatAssociationRecord( // user association + RocketChatAssociationModel.USER, + userId + ), + new RocketChatAssociationRecord( + RocketChatAssociationModel.MISC, + NotionTokenType.CURRENT_WORKSPACE + ), + ] + )) as ITokenInfo[]; + return removedTokenInfo; + } +} From 955be9f085a31ca8d69c77b854a4bb2ed99890e4 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Fri, 2 Jun 2023 12:57:40 +0530 Subject: [PATCH 08/29] fix: changed property status to statusCode in IError --- definition/errors/IError.ts | 2 +- errors/Error.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/definition/errors/IError.ts b/definition/errors/IError.ts index c585b39..27989cf 100644 --- a/definition/errors/IError.ts +++ b/definition/errors/IError.ts @@ -1,4 +1,4 @@ export interface IError extends Error { - status: number; + statusCode: number; additionalInfo?: string; } diff --git a/errors/Error.ts b/errors/Error.ts index 02fb594..953979f 100644 --- a/errors/Error.ts +++ b/errors/Error.ts @@ -12,18 +12,18 @@ import { ErrorName } from "../enum/Error"; class Error implements IError { name: string; message: string; - status: number; + statusCode: number; additionalInfo?: string; constructor( name: string, message: string, - status: number, + statusCode: number, additionalInfo?: string ) { this.name = name; this.message = message; - this.status = status; + this.statusCode = statusCode; this.additionalInfo = additionalInfo; } } From 6c39dc161bb0ff3818354aec9ca39da0e063d4ab Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Fri, 2 Jun 2023 13:07:27 +0530 Subject: [PATCH 09/29] feat: created and Registered NotionSDK --- NotionApp.ts | 8 +++++ definition/lib/INotion.ts | 21 ++++++++++++ enum/OAuth2.ts | 29 ++++++++++++++++ src/lib/NotionSDK.ts | 69 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 definition/lib/INotion.ts create mode 100644 enum/OAuth2.ts create mode 100644 src/lib/NotionSDK.ts diff --git a/NotionApp.ts b/NotionApp.ts index eeadcfb..c750960 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -9,9 +9,11 @@ import { IAppInfo } from "@rocket.chat/apps-engine/definition/metadata"; import { settings } from "./config/settings"; import { OAuth2Client } from "./src/authorization/OAuth2Client"; import { NotionCommand } from "./src/commands/NotionCommand"; +import { NotionSDK } from "./src/lib/NotionSDK"; export class NotionApp extends App { private oAuth2Client: OAuth2Client; + private NotionSdk: NotionSDK; constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } @@ -30,9 +32,15 @@ export class NotionApp extends App { ); this.oAuth2Client = new OAuth2Client(this); + this.NotionSdk = new NotionSDK(); } public getOAuth2Client(): OAuth2Client { return this.oAuth2Client; } + public getUtils() { + return { + NotionSdk: this.NotionSdk, + }; + } } diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts new file mode 100644 index 0000000..2289768 --- /dev/null +++ b/definition/lib/INotion.ts @@ -0,0 +1,21 @@ +import { IHttp, IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { URL } from "url"; +import { ITokenInfo } from "../authorization/IOAuth2Storage"; +import { ClientError } from "../../errors/Error"; +import { NotionApi } from "../../enum/Notion"; + +export interface INotion { + baseUrl: string; + NotionVersion: string; +} + +export interface INotionSDK extends INotion { + getAuthorizationUrl(user: IUser, read: IRead): Promise; + createToken( + http: IHttp, + redirectUrl: URL, + code: string, + credentials: string + ): Promise; +} diff --git a/enum/OAuth2.ts b/enum/OAuth2.ts new file mode 100644 index 0000000..ae8f918 --- /dev/null +++ b/enum/OAuth2.ts @@ -0,0 +1,29 @@ +export enum OAuth2Locator { + authUri = "https://api.notion.com/v1/oauth/authorize?owner=user&response_type=code&", + accessTokenUrl = "https://api.notion.com/v1/oauth/token", + refreshTokenUrl = "https://api.notion.com/v1/oauth/token", + redirectUrlPath = "/api/apps/public/fb6e4e74-f99d-41b6-96da-2486e9aafea8/webhook", +} + +export enum OAuth2Content { + success = '
\ +

\ + Authorization went successfully
\ + You can close this tab now
\ +

\ +
', + failed = '
\ +

\ + Oops, something went wrong, please try again or in case it still does not work, contact the administrator.\ +

\ +
', +} + +export enum OAuth2Credential { + TYPE = "Basic", + GRANT_TYPE = "authorization_code", + FORMAT = "base64", + CLIENT_ID = "client_id", + REDIRECT_URI = "redirect_uri", + STATE = "state", +} diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts new file mode 100644 index 0000000..f8f2029 --- /dev/null +++ b/src/lib/NotionSDK.ts @@ -0,0 +1,69 @@ +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { INotionSDK } from "../../definition/lib/INotion"; +import { IHttp, IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { URL } from "url"; +import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; +import { ClientError } from "../../errors/Error"; +import { NotionApi } from "../../enum/Notion"; +import { OAuth2Credential, OAuth2Locator } from "../../enum/OAuth2"; +import { AppsEngineException } from "@rocket.chat/apps-engine/definition/exceptions"; +import { getCredentials } from "../helper/getCredential"; + +export class NotionSDK implements INotionSDK { + baseUrl: string; + NotionVersion: string; + constructor() { + this.baseUrl = NotionApi.BASE_URL; + this.NotionVersion = NotionApi.VERSION; + } + public async getAuthorizationUrl( + user: IUser, + read: IRead + ): Promise { + const userId = user.id; + const { clientId, siteUrl } = await getCredentials(read); + + const redirectUrl = new URL(OAuth2Locator.redirectUrlPath, siteUrl); + const authorizationUrl = new URL(OAuth2Locator.authUri); + authorizationUrl.searchParams.set(OAuth2Credential.CLIENT_ID, clientId); + authorizationUrl.searchParams.set( + OAuth2Credential.REDIRECT_URI, + redirectUrl.toString() + ); + authorizationUrl.searchParams.set(OAuth2Credential.STATE, userId); + + return authorizationUrl.toString(); + } + + public async createToken( + http: IHttp, + redirectUrl: URL, + code: string, + credentials: string + ): Promise { + try { + const response = await http.post(OAuth2Locator.accessTokenUrl, { + data: { + grant_type: OAuth2Credential.GRANT_TYPE, + redirect_uri: redirectUrl.toString(), + code, + }, + headers: { + Authorization: `Basic ${credentials}`, + "Content-Type": NotionApi.CONTENT_TYPE, + "User-Agent": NotionApi.USER_AGENT, + }, + }); + + if (!response.statusCode.toString().startsWith("2")) { + return new ClientError( + `Error while Creating token: ${response.statusCode}`, + response.content + ); + } + return response.data as ITokenInfo; + } catch (err) { + throw new AppsEngineException(err as string); + } + } +} From 8da505026cd1a3b77d2728855cfba86b2a24709c Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Sat, 3 Jun 2023 18:46:30 +0530 Subject: [PATCH 10/29] feat: created RoomInteraction Class for persisting current interaction room --- definition/lib/IRoomInteraction.ts | 5 +++ src/storage/RoomInteraction.ts | 49 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 definition/lib/IRoomInteraction.ts create mode 100644 src/storage/RoomInteraction.ts diff --git a/definition/lib/IRoomInteraction.ts b/definition/lib/IRoomInteraction.ts new file mode 100644 index 0000000..44b2e08 --- /dev/null +++ b/definition/lib/IRoomInteraction.ts @@ -0,0 +1,5 @@ +export interface IRoomInteractionStorage { + storeInteractionRoomId(userId: string, roomId: string): Promise; + getInteractionRoomId(userId: string): Promise; + clearInteractionRoomId(userId: string): Promise; +} diff --git a/src/storage/RoomInteraction.ts b/src/storage/RoomInteraction.ts new file mode 100644 index 0000000..58d241e --- /dev/null +++ b/src/storage/RoomInteraction.ts @@ -0,0 +1,49 @@ +import { + RocketChatAssociationModel, + RocketChatAssociationRecord, +} from "@rocket.chat/apps-engine/definition/metadata"; +import { IRoomInteractionStorage } from "../../definition/lib/IRoomInteraction"; +import { + IPersistence, + IPersistenceRead, +} from "@rocket.chat/apps-engine/definition/accessors"; + +export class RoomInteractionStorage implements IRoomInteractionStorage { + constructor( + private readonly persistence: IPersistence, + private readonly persistenceRead: IPersistenceRead + ) {} + public async storeInteractionRoomId( + userId: string, + roomId: string + ): Promise { + const association = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + `${userId}#RoomId` + ); + await this.persistence.updateByAssociation( + association, + { roomId: roomId }, + true + ); + } + + public async getInteractionRoomId(userId: string): Promise { + const association = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + `${userId}#RoomId` + ); + const [result] = (await this.persistenceRead.readByAssociation( + association + )) as Array<{ roomId: string }>; + return result.roomId; + } + + public async clearInteractionRoomId(userId: string): Promise { + const association = new RocketChatAssociationRecord( + RocketChatAssociationModel.USER, + `${userId}#RoomId` + ); + await this.persistence.removeByAssociation(association); + } +} From 2a40dd88b89e0044e1022da1b4f225f6c349bc6c Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Sun, 4 Jun 2023 10:39:19 +0530 Subject: [PATCH 11/29] fix: tranfered getAuthorizationUrl() to IOAuth2Client --- definition/authorization/IOAuthClient.ts | 3 +++ definition/lib/INotion.ts | 4 +--- src/authorization/OAuth2Client.ts | 23 +++++++++++++++++++++++ src/lib/NotionSDK.ts | 22 +--------------------- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/definition/authorization/IOAuthClient.ts b/definition/authorization/IOAuthClient.ts index dafb9f2..1a8989e 100644 --- a/definition/authorization/IOAuthClient.ts +++ b/definition/authorization/IOAuthClient.ts @@ -5,6 +5,7 @@ import { IRead, } from "@rocket.chat/apps-engine/definition/accessors"; import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; export interface IOAuth2Client { connect( @@ -22,4 +23,6 @@ export interface IOAuth2Client { http: IHttp, persis: IPersistence ): Promise; + + getAuthorizationUrl(user: IUser, read: IRead): Promise; } diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index 2289768..76db128 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -1,5 +1,4 @@ -import { IHttp, IRead } from "@rocket.chat/apps-engine/definition/accessors"; -import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { IHttp } from "@rocket.chat/apps-engine/definition/accessors"; import { URL } from "url"; import { ITokenInfo } from "../authorization/IOAuth2Storage"; import { ClientError } from "../../errors/Error"; @@ -11,7 +10,6 @@ export interface INotion { } export interface INotionSDK extends INotion { - getAuthorizationUrl(user: IUser, read: IRead): Promise; createToken( http: IHttp, redirectUrl: URL, diff --git a/src/authorization/OAuth2Client.ts b/src/authorization/OAuth2Client.ts index 28c40a9..e14c7cd 100644 --- a/src/authorization/OAuth2Client.ts +++ b/src/authorization/OAuth2Client.ts @@ -7,6 +7,10 @@ import { IRead, } from "@rocket.chat/apps-engine/definition/accessors"; import { IOAuth2Client } from "../../definition/authorization/IOAuthClient"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { getCredentials } from "../helper/getCredential"; +import { OAuth2Credential, OAuth2Locator } from "../../enum/OAuth2"; +import { URL } from "url"; export class OAuth2Client implements IOAuth2Client { constructor(private readonly app: NotionApp) {} @@ -25,4 +29,23 @@ export class OAuth2Client implements IOAuth2Client { http: IHttp, persis: IPersistence ) {} + + public async getAuthorizationUrl( + user: IUser, + read: IRead + ): Promise { + const userId = user.id; + const { clientId, siteUrl } = await getCredentials(read); + + const redirectUrl = new URL(OAuth2Locator.redirectUrlPath, siteUrl); + const authorizationUrl = new URL(OAuth2Locator.authUri); + authorizationUrl.searchParams.set(OAuth2Credential.CLIENT_ID, clientId); + authorizationUrl.searchParams.set( + OAuth2Credential.REDIRECT_URI, + redirectUrl.toString() + ); + authorizationUrl.searchParams.set(OAuth2Credential.STATE, userId); + + return authorizationUrl.toString(); + } } diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index f8f2029..5a84470 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -1,13 +1,11 @@ -import { IUser } from "@rocket.chat/apps-engine/definition/users"; import { INotionSDK } from "../../definition/lib/INotion"; -import { IHttp, IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { IHttp } from "@rocket.chat/apps-engine/definition/accessors"; import { URL } from "url"; import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; import { ClientError } from "../../errors/Error"; import { NotionApi } from "../../enum/Notion"; import { OAuth2Credential, OAuth2Locator } from "../../enum/OAuth2"; import { AppsEngineException } from "@rocket.chat/apps-engine/definition/exceptions"; -import { getCredentials } from "../helper/getCredential"; export class NotionSDK implements INotionSDK { baseUrl: string; @@ -16,24 +14,6 @@ export class NotionSDK implements INotionSDK { this.baseUrl = NotionApi.BASE_URL; this.NotionVersion = NotionApi.VERSION; } - public async getAuthorizationUrl( - user: IUser, - read: IRead - ): Promise { - const userId = user.id; - const { clientId, siteUrl } = await getCredentials(read); - - const redirectUrl = new URL(OAuth2Locator.redirectUrlPath, siteUrl); - const authorizationUrl = new URL(OAuth2Locator.authUri); - authorizationUrl.searchParams.set(OAuth2Credential.CLIENT_ID, clientId); - authorizationUrl.searchParams.set( - OAuth2Credential.REDIRECT_URI, - redirectUrl.toString() - ); - authorizationUrl.searchParams.set(OAuth2Credential.STATE, userId); - - return authorizationUrl.toString(); - } public async createToken( http: IHttp, From 8957c768e267a09693a15d777969c1347ffcd5b8 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 5 Jun 2023 11:59:21 +0530 Subject: [PATCH 12/29] feat: created BlockBuilder and ElementBuilder Class --- definition/ui-kit/Block/IActionBlock.ts | 3 ++ definition/ui-kit/Block/IBlockBuilder.ts | 8 ++++ definition/ui-kit/Block/ISectionBlock.ts | 6 +++ definition/ui-kit/Element/IButtonElement.ts | 5 +++ definition/ui-kit/Element/IElementBuilder.ts | 12 +++++ src/lib/BlockBuilder.ts | 46 ++++++++++++++++++++ src/lib/ElementBuilder.ts | 35 +++++++++++++++ 7 files changed, 115 insertions(+) create mode 100644 definition/ui-kit/Block/IActionBlock.ts create mode 100644 definition/ui-kit/Block/IBlockBuilder.ts create mode 100644 definition/ui-kit/Block/ISectionBlock.ts create mode 100644 definition/ui-kit/Element/IButtonElement.ts create mode 100644 definition/ui-kit/Element/IElementBuilder.ts create mode 100644 src/lib/BlockBuilder.ts create mode 100644 src/lib/ElementBuilder.ts diff --git a/definition/ui-kit/Block/IActionBlock.ts b/definition/ui-kit/Block/IActionBlock.ts new file mode 100644 index 0000000..107e3c5 --- /dev/null +++ b/definition/ui-kit/Block/IActionBlock.ts @@ -0,0 +1,3 @@ +import { ActionsBlock } from "@rocket.chat/ui-kit"; + +export type ActionBlockParam = Pick; diff --git a/definition/ui-kit/Block/IBlockBuilder.ts b/definition/ui-kit/Block/IBlockBuilder.ts new file mode 100644 index 0000000..d6f6513 --- /dev/null +++ b/definition/ui-kit/Block/IBlockBuilder.ts @@ -0,0 +1,8 @@ +import { SectionBlock, ActionsBlock } from "@rocket.chat/ui-kit"; +import { SectionBlockParam } from "./ISectionBlock"; +import { ActionBlockParam } from "./IActionBlock"; + +export interface IBlockBuilder { + createSectionBlock(param: SectionBlockParam): SectionBlock; + createActionBlock(param: ActionBlockParam): ActionsBlock; +} diff --git a/definition/ui-kit/Block/ISectionBlock.ts b/definition/ui-kit/Block/ISectionBlock.ts new file mode 100644 index 0000000..e1940f1 --- /dev/null +++ b/definition/ui-kit/Block/ISectionBlock.ts @@ -0,0 +1,6 @@ +import { SectionBlock } from "@rocket.chat/ui-kit"; + +export type SectionBlockParam = Pick & { + text?: string; + fields?: Array; +}; diff --git a/definition/ui-kit/Element/IButtonElement.ts b/definition/ui-kit/Element/IButtonElement.ts new file mode 100644 index 0000000..9d1c26e --- /dev/null +++ b/definition/ui-kit/Element/IButtonElement.ts @@ -0,0 +1,5 @@ +import { ButtonElement } from "@rocket.chat/ui-kit"; + +export type ButtonParam = Pick & { + text: string; +}; diff --git a/definition/ui-kit/Element/IElementBuilder.ts b/definition/ui-kit/Element/IElementBuilder.ts new file mode 100644 index 0000000..fff5431 --- /dev/null +++ b/definition/ui-kit/Element/IElementBuilder.ts @@ -0,0 +1,12 @@ +import { ButtonStyle } from "@rocket.chat/apps-engine/definition/uikit"; +import { ButtonElement } from "@rocket.chat/ui-kit"; +import { ButtonParam } from "./IButtonElement"; + +export interface IElementBuilder { + addButton( + param: ButtonParam, + interaction: ElementInteractionParam + ): ButtonElement; +} + +export type ElementInteractionParam = { blockId: string; actionId: string }; diff --git a/src/lib/BlockBuilder.ts b/src/lib/BlockBuilder.ts new file mode 100644 index 0000000..03b897c --- /dev/null +++ b/src/lib/BlockBuilder.ts @@ -0,0 +1,46 @@ +import { ActionBlockParam } from "../../definition/ui-kit/Block/IActionBlock"; +import { IBlockBuilder } from "../../definition/ui-kit/Block/IBlockBuilder"; +import { SectionBlockParam } from "../../definition/ui-kit/Block/ISectionBlock"; +import { + SectionBlock, + LayoutBlockType, + TextObjectType, + TextObject, + ActionsBlock, +} from "@rocket.chat/ui-kit"; + +export class BlockBuilder implements IBlockBuilder { + constructor(private readonly appId: string) {} + public createSectionBlock(param: SectionBlockParam): SectionBlock { + const { text, blockId, fields, accessory } = param; + const sectionBlock: SectionBlock = { + appId: this.appId, + blockId, + type: LayoutBlockType.SECTION, + text: { + type: TextObjectType.MRKDWN, + text: text ? text : "", + }, + accessory, + fields: fields ? this.createFields(fields) : undefined, + }; + return sectionBlock; + } + public createActionBlock(param: ActionBlockParam): ActionsBlock { + const { elements } = param; + const actionBlock: ActionsBlock = { + type: LayoutBlockType.ACTIONS, + elements: elements, + }; + return actionBlock; + } + + private createFields(fields: Array): Array { + return fields.map((field) => { + return { + type: TextObjectType.MRKDWN, + text: field, + }; + }); + } +} diff --git a/src/lib/ElementBuilder.ts b/src/lib/ElementBuilder.ts new file mode 100644 index 0000000..dc548a4 --- /dev/null +++ b/src/lib/ElementBuilder.ts @@ -0,0 +1,35 @@ +import { + ElementInteractionParam, + IElementBuilder, +} from "../../definition/ui-kit/Element/IElementBuilder"; +import { + ButtonElement, + BlockElementType, + TextObjectType, +} from "@rocket.chat/ui-kit"; +import { ButtonParam } from "../../definition/ui-kit/Element/IButtonElement"; + +export class ElementBuilder implements IElementBuilder { + constructor(private readonly appId: string) {} + public addButton( + param: ButtonParam, + interaction: ElementInteractionParam + ): ButtonElement { + const { text, url, value, style } = param; + const { blockId, actionId } = interaction; + const button: ButtonElement = { + type: BlockElementType.BUTTON, + text: { + type: TextObjectType.PLAIN_TEXT, + text, + }, + appId: this.appId, + blockId, + actionId, + url, + value, + style, + }; + return button; + } +} From 34c9aa738c5ae4006ab9d3b45718c2c41be7e7b1 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 5 Jun 2023 12:02:02 +0530 Subject: [PATCH 13/29] feat: created sendNotification() which notifies User --- src/helper/message.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/helper/message.ts diff --git a/src/helper/message.ts b/src/helper/message.ts new file mode 100644 index 0000000..634b1cd --- /dev/null +++ b/src/helper/message.ts @@ -0,0 +1,28 @@ +import { IModify, IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { Block } from "@rocket.chat/ui-kit"; + +export async function sendNotification( + read: IRead, + modify: IModify, + user: IUser, + room: IRoom, + content: { message?: string; blocks?: Array } +): Promise { + const appUser = (await read.getUserReader().getAppUser()) as IUser; + const { message, blocks } = content; + const messageBuilder = modify + .getCreator() + .startMessage() + .setSender(appUser) + .setRoom(room) + .setGroupable(false); + + if (message) { + messageBuilder.setText(message); + } else if (blocks) { + messageBuilder.setBlocks(blocks); + } + return read.getNotifier().notifyUser(user, messageBuilder.getMessage()); +} From ece7d2a331bf77391b545fbe27561d58c23da1a9 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 5 Jun 2023 12:13:39 +0530 Subject: [PATCH 14/29] feat(ui): created reusable connectBlock() --- enum/OAuth2.ts | 8 ++++++++ src/helper/getConnectBlock.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/helper/getConnectBlock.ts diff --git a/enum/OAuth2.ts b/enum/OAuth2.ts index ae8f918..9ed8ce0 100644 --- a/enum/OAuth2.ts +++ b/enum/OAuth2.ts @@ -27,3 +27,11 @@ export enum OAuth2Credential { REDIRECT_URI = "redirect_uri", STATE = "state", } + +export enum OAuth2Block { + CONNECT_TO_WORKSPACE = "connect-to-workspace-block", +} + +export enum OAuth2Action { + CONNECT_TO_WORKSPACE = "connect-to-workspace-action", +} diff --git a/src/helper/getConnectBlock.ts b/src/helper/getConnectBlock.ts new file mode 100644 index 0000000..4bc3433 --- /dev/null +++ b/src/helper/getConnectBlock.ts @@ -0,0 +1,31 @@ +import { ButtonStyle } from "@rocket.chat/apps-engine/definition/uikit"; +import { Block } from "@rocket.chat/ui-kit"; +import { NotionApp } from "../../NotionApp"; +import { OAuth2Action, OAuth2Block } from "../../enum/OAuth2"; + +export async function getConnectBlock( + app: NotionApp, + message: string, + url: string +): Promise> { + const { elementBuilder, blockBuilder } = app.getUtils(); + const buttonElement = elementBuilder.addButton( + { + text: "Connect to Workspace", + style: ButtonStyle.PRIMARY, + url, + }, + { + blockId: OAuth2Block.CONNECT_TO_WORKSPACE, + actionId: OAuth2Action.CONNECT_TO_WORKSPACE, + } + ); + const actionBlock = blockBuilder.createActionBlock({ + elements: [buttonElement], + }); + const textBlock = blockBuilder.createSectionBlock({ + text: message, + }); + + return [textBlock, actionBlock]; +} From 0dacc501c028f53493921e7baad3dfcd73a429fe Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 5 Jun 2023 12:18:49 +0530 Subject: [PATCH 15/29] feat(utils): registered AppUtils --- NotionApp.ts | 11 ++++++++++- definition/lib/IAppUtils.ts | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 definition/lib/IAppUtils.ts diff --git a/NotionApp.ts b/NotionApp.ts index c750960..4245861 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -10,10 +10,15 @@ import { settings } from "./config/settings"; import { OAuth2Client } from "./src/authorization/OAuth2Client"; import { NotionCommand } from "./src/commands/NotionCommand"; import { NotionSDK } from "./src/lib/NotionSDK"; +import { ElementBuilder } from "./src/lib/ElementBuilder"; +import { BlockBuilder } from "./src/lib/BlockBuilder"; +import { IAppUtils } from "./definition/lib/IAppUtils"; export class NotionApp extends App { private oAuth2Client: OAuth2Client; private NotionSdk: NotionSDK; + private elementBuilder: ElementBuilder; + private blockBuilder: BlockBuilder; constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } @@ -33,14 +38,18 @@ export class NotionApp extends App { this.oAuth2Client = new OAuth2Client(this); this.NotionSdk = new NotionSDK(); + this.elementBuilder = new ElementBuilder(this.getID()); + this.blockBuilder = new BlockBuilder(this.getID()); } public getOAuth2Client(): OAuth2Client { return this.oAuth2Client; } - public getUtils() { + public getUtils(): IAppUtils { return { NotionSdk: this.NotionSdk, + elementBuilder: this.elementBuilder, + blockBuilder: this.blockBuilder, }; } } diff --git a/definition/lib/IAppUtils.ts b/definition/lib/IAppUtils.ts new file mode 100644 index 0000000..7fdc5c8 --- /dev/null +++ b/definition/lib/IAppUtils.ts @@ -0,0 +1,9 @@ +import { BlockBuilder } from "../../src/lib/BlockBuilder"; +import { ElementBuilder } from "../../src/lib/ElementBuilder"; +import { NotionSDK } from "../../src/lib/NotionSDK"; + +export interface IAppUtils { + NotionSdk: NotionSDK; + elementBuilder: ElementBuilder; + blockBuilder: BlockBuilder; +} From cd867e7da7dbf3420c0e56464b64fe8055a2bf67 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 5 Jun 2023 12:28:31 +0530 Subject: [PATCH 16/29] feat: implemented Connect() and Handled Connect BlockAction --- NotionApp.ts | 33 +++++++++++++++++++++++++++++++ src/authorization/OAuth2Client.ts | 19 +++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/NotionApp.ts b/NotionApp.ts index 4245861..4d3d4b2 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -2,7 +2,11 @@ import { IAppAccessors, IConfigurationExtend, IEnvironmentRead, + IHttp, ILogger, + IModify, + IPersistence, + 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"; @@ -12,6 +16,12 @@ import { NotionCommand } from "./src/commands/NotionCommand"; import { NotionSDK } from "./src/lib/NotionSDK"; import { ElementBuilder } from "./src/lib/ElementBuilder"; import { BlockBuilder } from "./src/lib/BlockBuilder"; +import { + IUIKitResponse, + UIKitBlockInteractionContext, +} from "@rocket.chat/apps-engine/definition/uikit"; +import { RoomInteractionStorage } from "./src/storage/RoomInteraction"; +import { OAuth2Action } from "./enum/OAuth2"; import { IAppUtils } from "./definition/lib/IAppUtils"; export class NotionApp extends App { @@ -52,4 +62,27 @@ export class NotionApp extends App { blockBuilder: this.blockBuilder, }; } + + public async executeBlockActionHandler( + context: UIKitBlockInteractionContext, + read: IRead, + http: IHttp, + persistence: IPersistence, + modify: IModify + ): Promise { + // Todo[Week 2]: Make a Interface and Class + const { actionId, user, room } = context.getInteractionData(); + + if (actionId == OAuth2Action.CONNECT_TO_WORKSPACE) { + const persistenceRead = read.getPersistenceReader(); + const roomId = room?.id as string; + const roomInteraction = new RoomInteractionStorage( + persistence, + persistenceRead + ); + await roomInteraction.storeInteractionRoomId(user.id, roomId); + } + + return context.getInteractionResponder().successResponse(); + } } diff --git a/src/authorization/OAuth2Client.ts b/src/authorization/OAuth2Client.ts index e14c7cd..08db021 100644 --- a/src/authorization/OAuth2Client.ts +++ b/src/authorization/OAuth2Client.ts @@ -8,9 +8,11 @@ import { } from "@rocket.chat/apps-engine/definition/accessors"; import { IOAuth2Client } from "../../definition/authorization/IOAuthClient"; import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { sendNotification } from "../helper/message"; import { getCredentials } from "../helper/getCredential"; import { OAuth2Credential, OAuth2Locator } from "../../enum/OAuth2"; import { URL } from "url"; +import { getConnectBlock } from "../helper/getConnectBlock"; export class OAuth2Client implements IOAuth2Client { constructor(private readonly app: NotionApp) {} @@ -20,7 +22,22 @@ export class OAuth2Client implements IOAuth2Client { modify: IModify, http: IHttp, persis: IPersistence - ) {} + ) { + const { blockBuilder, elementBuilder } = this.app.getUtils(); + const user = context.getSender(); + const room = context.getRoom(); + const authorizationUrl = await this.getAuthorizationUrl(user, read); + const message = `HeyπŸ‘‹ ${user.username}!`; + const blocks = await getConnectBlock( + this.app, + message, + authorizationUrl + ); + + await sendNotification(read, modify, user, room, { + blocks, + }); + } public async disconnect( context: SlashCommandContext, From 66826b81ea92eaa68df8c2ef5128bc7adb01d939 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 5 Jun 2023 12:31:19 +0530 Subject: [PATCH 17/29] feat(connect): created, Registered and Implemented Webhook get() Auth Callback --- NotionApp.ts | 11 ++++ src/endpoints/webhook.ts | 112 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/endpoints/webhook.ts diff --git a/NotionApp.ts b/NotionApp.ts index 4d3d4b2..1911bf0 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -14,6 +14,11 @@ import { settings } from "./config/settings"; import { OAuth2Client } from "./src/authorization/OAuth2Client"; import { NotionCommand } from "./src/commands/NotionCommand"; import { NotionSDK } from "./src/lib/NotionSDK"; +import { + ApiSecurity, + ApiVisibility, +} from "@rocket.chat/apps-engine/definition/api"; +import { WebHookEndpoint } from "./src/endpoints/webhook"; import { ElementBuilder } from "./src/lib/ElementBuilder"; import { BlockBuilder } from "./src/lib/BlockBuilder"; import { @@ -46,6 +51,12 @@ export class NotionApp extends App { }) ); + await configurationExtend.api.provideApi({ + visibility: ApiVisibility.PUBLIC, + security: ApiSecurity.UNSECURE, + endpoints: [new WebHookEndpoint(this)], + }); + this.oAuth2Client = new OAuth2Client(this); this.NotionSdk = new NotionSDK(); this.elementBuilder = new ElementBuilder(this.getID()); diff --git a/src/endpoints/webhook.ts b/src/endpoints/webhook.ts new file mode 100644 index 0000000..5db1309 --- /dev/null +++ b/src/endpoints/webhook.ts @@ -0,0 +1,112 @@ +import { + HttpStatusCode, + IHttp, + IModify, + IPersistence, + IRead, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { + ApiEndpoint, + IApiEndpointInfo, + IApiRequest, + IApiResponse, +} from "@rocket.chat/apps-engine/definition/api"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { URL } from "url"; +import { + OAuth2Content, + OAuth2Credential, + OAuth2Locator, +} from "../../enum/OAuth2"; +import { ClientError } from "../../errors/Error"; +import { OAuth2Storage } from "../authorization/OAuth2Storage"; +import { getCredentials } from "../helper/getCredential"; +import { sendNotification } from "../helper/message"; +import { BlockBuilder } from "../lib/BlockBuilder"; +import { NotionSDK } from "../lib/NotionSDK"; +import { RoomInteractionStorage } from "../storage/RoomInteraction"; + +export class WebHookEndpoint extends ApiEndpoint { + public path: string = "webhook"; + public url_path: string = OAuth2Locator.redirectUrlPath; + public accessTokenUrl: string = OAuth2Locator.accessTokenUrl; + public async get( + request: IApiRequest, + endpoint: IApiEndpointInfo, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence + ): Promise { + const { code, state, error } = request.query; + + // incase when user leaves in between the auth process + if (error) { + this.app.getLogger().warn(error); + return { + status: HttpStatusCode.UNAUTHORIZED, + content: OAuth2Content.failed, + }; + } + + const user = await read.getUserReader().getById(state); + // incase when user changed the state in authUrl + if (!user) { + this.app + .getLogger() + .warn(`User not found before access token request`); + return { + status: HttpStatusCode.NON_AUTHORITATIVE_INFORMATION, + content: OAuth2Content.failed, + }; + } + + const { clientId, clientSecret, siteUrl } = await getCredentials(read); + const redirectUrl = new URL(this.url_path, siteUrl); + const credentials = new Buffer(`${clientId}:${clientSecret}`).toString( + OAuth2Credential.FORMAT + ); + + const notionSDK = new NotionSDK(); + const response = await notionSDK.createToken( + http, + redirectUrl, + code, + credentials + ); + + // incase there is some error in creation of Token from Notion + if (response instanceof ClientError) { + this.app.getLogger().warn(response.message); + return { + status: response.statusCode, + content: OAuth2Content.failed, + }; + } + + const persistenceRead = read.getPersistenceReader(); + const oAuth2Storage = new OAuth2Storage(persis, persistenceRead); + await oAuth2Storage.connectUserToWorkspace(response, state); + + const roomInteraction = new RoomInteractionStorage( + persis, + persistenceRead + ); + const roomId = await roomInteraction.getInteractionRoomId(user.id); + const room = (await read.getRoomReader().getById(roomId)) as IRoom; + + const workspaceName = response.workspace_name as string; + + const blockBuilder = new BlockBuilder(this.app.getID()); + const sectionBlock = blockBuilder.createSectionBlock({ + text: `πŸ‘‹You are connected to Workspace **${workspaceName}**`, + }); + + await sendNotification(read, modify, user, room, { + blocks: [sectionBlock], + }); + await roomInteraction.clearInteractionRoomId(user.id); + + return this.success(OAuth2Content.success); + } +} From 68cad40650ebf212cab15b845c8dae56be70f1cd Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 5 Jun 2023 12:34:18 +0530 Subject: [PATCH 18/29] feat: implemented disconnect() for workspace --- src/authorization/OAuth2Client.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/authorization/OAuth2Client.ts b/src/authorization/OAuth2Client.ts index 08db021..4735a7e 100644 --- a/src/authorization/OAuth2Client.ts +++ b/src/authorization/OAuth2Client.ts @@ -8,6 +8,7 @@ import { } from "@rocket.chat/apps-engine/definition/accessors"; import { IOAuth2Client } from "../../definition/authorization/IOAuthClient"; import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { OAuth2Storage } from "./OAuth2Storage"; import { sendNotification } from "../helper/message"; import { getCredentials } from "../helper/getCredential"; import { OAuth2Credential, OAuth2Locator } from "../../enum/OAuth2"; @@ -45,7 +46,34 @@ export class OAuth2Client implements IOAuth2Client { modify: IModify, http: IHttp, persis: IPersistence - ) {} + ) { + const persistenceRead = read.getPersistenceReader(); + const oAuthStorage = new OAuth2Storage(persis, persistenceRead); + const { blockBuilder, elementBuilder } = this.app.getUtils(); + + const room = context.getRoom(); + const user = context.getSender(); + const userId = user.id; + const tokenInfo = await oAuthStorage.getCurrentWorkspace(userId); + + if (tokenInfo) { + await oAuthStorage.disconnectUserFromCurrentWorkspace(userId); + const message = `you are being disconnected from the Workspace **${tokenInfo.workspace_name}**`; + await sendNotification(read, modify, user, room, { message }); + return; + } + + const authorizationUrl = await this.getAuthorizationUrl(user, read); + const message = `you are not connected to workspace!`; + const blocks = await getConnectBlock( + this.app, + message, + authorizationUrl + ); + await sendNotification(read, modify, user, room, { + blocks, + }); + } public async getAuthorizationUrl( user: IUser, From 26c59a64dab2e43b8d1fe8d75bbd7ebca80849b1 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 6 Jun 2023 11:57:04 +0530 Subject: [PATCH 19/29] feat(ui): enhance several auth notificiations --- src/authorization/OAuth2Client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/authorization/OAuth2Client.ts b/src/authorization/OAuth2Client.ts index 4735a7e..662d964 100644 --- a/src/authorization/OAuth2Client.ts +++ b/src/authorization/OAuth2Client.ts @@ -28,7 +28,7 @@ export class OAuth2Client implements IOAuth2Client { const user = context.getSender(); const room = context.getRoom(); const authorizationUrl = await this.getAuthorizationUrl(user, read); - const message = `HeyπŸ‘‹ ${user.username}!`; + const message = `Hey **${user.username}**!πŸ‘‹ Connect your Notion Workspace`; const blocks = await getConnectBlock( this.app, message, @@ -58,13 +58,13 @@ export class OAuth2Client implements IOAuth2Client { if (tokenInfo) { await oAuthStorage.disconnectUserFromCurrentWorkspace(userId); - const message = `you are being disconnected from the Workspace **${tokenInfo.workspace_name}**`; + const message = `πŸ‘‹ You are disconnected from the Workspace **${tokenInfo.workspace_name}**`; await sendNotification(read, modify, user, room, { message }); return; } const authorizationUrl = await this.getAuthorizationUrl(user, read); - const message = `you are not connected to workspace!`; + const message = `πŸ‘‹ You are not Connected to **Workspace**!`; const blocks = await getConnectBlock( this.app, message, From d128feed0f055a25a19725aa7d2feab7ae3745b1 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 6 Jun 2023 11:59:30 +0530 Subject: [PATCH 20/29] feat(ui): created IContextBlock, IPreviewBlock and ImageElement --- definition/ui-kit/Block/IBlockBuilder.ts | 14 +++++++++++++- definition/ui-kit/Block/IContextBlock.ts | 7 +++++++ definition/ui-kit/Block/IPreviewBlock.ts | 8 ++++++++ definition/ui-kit/Element/IElementBuilder.ts | 4 +++- definition/ui-kit/Element/IImageElement.ts | 3 +++ 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 definition/ui-kit/Block/IContextBlock.ts create mode 100644 definition/ui-kit/Block/IPreviewBlock.ts create mode 100644 definition/ui-kit/Element/IImageElement.ts diff --git a/definition/ui-kit/Block/IBlockBuilder.ts b/definition/ui-kit/Block/IBlockBuilder.ts index d6f6513..a796150 100644 --- a/definition/ui-kit/Block/IBlockBuilder.ts +++ b/definition/ui-kit/Block/IBlockBuilder.ts @@ -1,8 +1,20 @@ -import { SectionBlock, ActionsBlock } from "@rocket.chat/ui-kit"; +import { + SectionBlock, + ActionsBlock, + PreviewBlockBase, + PreviewBlockWithThumb, + ContextBlock, +} from "@rocket.chat/ui-kit"; import { SectionBlockParam } from "./ISectionBlock"; import { ActionBlockParam } from "./IActionBlock"; +import { PreviewBlockParam } from "./IPreviewBlock"; +import { ContextBlockParam } from "./IContextBlock"; export interface IBlockBuilder { createSectionBlock(param: SectionBlockParam): SectionBlock; createActionBlock(param: ActionBlockParam): ActionsBlock; + createPreviewBlock( + param: PreviewBlockParam + ): PreviewBlockBase | PreviewBlockWithThumb; + createContextBlock(param: ContextBlockParam): ContextBlock; } diff --git a/definition/ui-kit/Block/IContextBlock.ts b/definition/ui-kit/Block/IContextBlock.ts new file mode 100644 index 0000000..ca46b7a --- /dev/null +++ b/definition/ui-kit/Block/IContextBlock.ts @@ -0,0 +1,7 @@ +import { ImageParam } from "../Element/IImageElement"; +import { ImageElement } from "@rocket.chat/ui-kit"; + +export type ContextBlockParam = { + contextElements: Array; + blockId?: string; +}; diff --git a/definition/ui-kit/Block/IPreviewBlock.ts b/definition/ui-kit/Block/IPreviewBlock.ts new file mode 100644 index 0000000..8aaac79 --- /dev/null +++ b/definition/ui-kit/Block/IPreviewBlock.ts @@ -0,0 +1,8 @@ +import { PreviewBlockWithThumb } from "@rocket.chat/ui-kit"; + +export type PreviewBlockParam = Partial< + Pick +> & { + title: Array; + description: Array; +}; diff --git a/definition/ui-kit/Element/IElementBuilder.ts b/definition/ui-kit/Element/IElementBuilder.ts index fff5431..de7d947 100644 --- a/definition/ui-kit/Element/IElementBuilder.ts +++ b/definition/ui-kit/Element/IElementBuilder.ts @@ -1,12 +1,14 @@ import { ButtonStyle } from "@rocket.chat/apps-engine/definition/uikit"; -import { ButtonElement } from "@rocket.chat/ui-kit"; +import { ButtonElement, ImageElement } from "@rocket.chat/ui-kit"; import { ButtonParam } from "./IButtonElement"; +import { ImageParam } from "./IImageElement"; export interface IElementBuilder { addButton( param: ButtonParam, interaction: ElementInteractionParam ): ButtonElement; + addImage(param: ImageParam): ImageElement; } export type ElementInteractionParam = { blockId: string; actionId: string }; diff --git a/definition/ui-kit/Element/IImageElement.ts b/definition/ui-kit/Element/IImageElement.ts new file mode 100644 index 0000000..8f0769a --- /dev/null +++ b/definition/ui-kit/Element/IImageElement.ts @@ -0,0 +1,3 @@ +import { ImageElement } from "@rocket.chat/ui-kit"; + +export type ImageParam = Pick; From f5e53328442d4cd2f49844edf996b82b5d82e58b Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 6 Jun 2023 12:01:11 +0530 Subject: [PATCH 21/29] feat(ui): implemented addImage(), createPreviewblock() and createContextBlock() --- src/lib/BlockBuilder.ts | 47 +++++++++++++++++++++++++++++++++++++-- src/lib/ElementBuilder.ts | 13 +++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/lib/BlockBuilder.ts b/src/lib/BlockBuilder.ts index 03b897c..b399f1c 100644 --- a/src/lib/BlockBuilder.ts +++ b/src/lib/BlockBuilder.ts @@ -1,5 +1,7 @@ import { ActionBlockParam } from "../../definition/ui-kit/Block/IActionBlock"; import { IBlockBuilder } from "../../definition/ui-kit/Block/IBlockBuilder"; +import { ContextBlockParam } from "../../definition/ui-kit/Block/IContextBlock"; +import { PreviewBlockParam } from "../../definition/ui-kit/Block/IPreviewBlock"; import { SectionBlockParam } from "../../definition/ui-kit/Block/ISectionBlock"; import { SectionBlock, @@ -7,6 +9,9 @@ import { TextObjectType, TextObject, ActionsBlock, + PreviewBlockBase, + PreviewBlockWithThumb, + ContextBlock, } from "@rocket.chat/ui-kit"; export class BlockBuilder implements IBlockBuilder { @@ -22,7 +27,7 @@ export class BlockBuilder implements IBlockBuilder { text: text ? text : "", }, accessory, - fields: fields ? this.createFields(fields) : undefined, + fields: fields ? this.createTextObjects(fields) : undefined, }; return sectionBlock; } @@ -35,7 +40,7 @@ export class BlockBuilder implements IBlockBuilder { return actionBlock; } - private createFields(fields: Array): Array { + private createTextObjects(fields: Array): Array { return fields.map((field) => { return { type: TextObjectType.MRKDWN, @@ -43,4 +48,42 @@ export class BlockBuilder implements IBlockBuilder { }; }); } + + public createPreviewBlock( + param: PreviewBlockParam + ): PreviewBlockBase | PreviewBlockWithThumb { + const { title, description, footer, thumb } = param; + const previewBlock: PreviewBlockBase | PreviewBlockWithThumb = { + type: LayoutBlockType.PREVIEW, + title: this.createTextObjects(title), + description: this.createTextObjects(description), + footer, + thumb, + }; + + return previewBlock; + } + + public createContextBlock(param: ContextBlockParam): ContextBlock { + const { contextElements, blockId } = param; + + const elements = contextElements.map((element) => { + if (typeof element === "string") { + return { + type: TextObjectType.MRKDWN, + text: element, + } as TextObject; + } else { + return element; + } + }); + + const contextBlock: ContextBlock = { + type: LayoutBlockType.CONTEXT, + elements, + blockId, + }; + + return contextBlock; + } } diff --git a/src/lib/ElementBuilder.ts b/src/lib/ElementBuilder.ts index dc548a4..4088967 100644 --- a/src/lib/ElementBuilder.ts +++ b/src/lib/ElementBuilder.ts @@ -6,8 +6,10 @@ import { ButtonElement, BlockElementType, TextObjectType, + ImageElement, } from "@rocket.chat/ui-kit"; import { ButtonParam } from "../../definition/ui-kit/Element/IButtonElement"; +import { ImageParam } from "../../definition/ui-kit/Element/IImageElement"; export class ElementBuilder implements IElementBuilder { constructor(private readonly appId: string) {} @@ -32,4 +34,15 @@ export class ElementBuilder implements IElementBuilder { }; return button; } + + public addImage(param: ImageParam): ImageElement { + const { imageUrl, altText } = param; + const image: ImageElement = { + type: BlockElementType.IMAGE, + imageUrl, + altText, + }; + return image; + } + } From e6648e5e64a9a1cec88e484440e0fb5ad2069789 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 6 Jun 2023 12:02:39 +0530 Subject: [PATCH 22/29] feat(ui): created previewLayout for connected workspace --- enum/Notion.ts | 4 ++++ src/helper/getConnectLayout.ts | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/helper/getConnectLayout.ts diff --git a/enum/Notion.ts b/enum/Notion.ts index 53becfd..e0a6d66 100644 --- a/enum/Notion.ts +++ b/enum/Notion.ts @@ -15,3 +15,7 @@ export enum NotionApi { USER_AGENT = "Rocket.Chat-Apps-Engine", CONTENT_TYPE = "application/json", } + +export enum Notion { + WEBSITE_URL = "https://www.notion.so", +} diff --git a/src/helper/getConnectLayout.ts b/src/helper/getConnectLayout.ts new file mode 100644 index 0000000..f395000 --- /dev/null +++ b/src/helper/getConnectLayout.ts @@ -0,0 +1,42 @@ +import { ITokenInfo } from "../../definition/authorization/IOAuth2Storage"; +import { PreviewBlockWithPreview, PreviewBlock } from "@rocket.chat/ui-kit"; +import { ElementBuilder } from "../lib/ElementBuilder"; +import { BlockBuilder } from "../lib/BlockBuilder"; +import { Notion } from "../../enum/Notion"; + +export function getConnectPreview( + appId: string, + tokenInfo: ITokenInfo +): Exclude { + const { workspace_name, workspace_icon, owner } = tokenInfo; + const { name, avatar_url } = owner.user; + + const elementBuilder = new ElementBuilder(appId); + const blockBuilder = new BlockBuilder(appId); + + const workspace_icon_url = workspace_icon?.startsWith("/") + ? `${Notion.WEBSITE_URL}${workspace_icon}` + : workspace_icon?.startsWith("http") + ? `${workspace_icon}` + : undefined; + const thumb = workspace_icon_url ? { url: workspace_icon_url } : undefined; + const title = [`[**${workspace_name}**](${Notion.WEBSITE_URL})`]; + const description = ["**πŸ‘‹ You are Connected to Workspace**"]; + const avatarElement = elementBuilder.addImage({ + imageUrl: avatar_url as string, + altText: name as string, + }); + const avatarName = `**${name}**`; + const footer = blockBuilder.createContextBlock({ + contextElements: [avatarElement, avatarName], + }); + + const connectPreview = blockBuilder.createPreviewBlock({ + title, + description, + footer, + thumb, + }); + + return connectPreview; +} From 74b44b831d38f746dcb19f46caaf473b2e784933 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 6 Jun 2023 12:03:34 +0530 Subject: [PATCH 23/29] feat(ui): enhanced connected to workspace notification with previewLayout --- src/endpoints/webhook.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/endpoints/webhook.ts b/src/endpoints/webhook.ts index 5db1309..59e62e8 100644 --- a/src/endpoints/webhook.ts +++ b/src/endpoints/webhook.ts @@ -25,6 +25,7 @@ import { sendNotification } from "../helper/message"; import { BlockBuilder } from "../lib/BlockBuilder"; import { NotionSDK } from "../lib/NotionSDK"; import { RoomInteractionStorage } from "../storage/RoomInteraction"; +import { getConnectPreview } from "../helper/getConnectLayout"; export class WebHookEndpoint extends ApiEndpoint { public path: string = "webhook"; @@ -95,15 +96,9 @@ export class WebHookEndpoint extends ApiEndpoint { const roomId = await roomInteraction.getInteractionRoomId(user.id); const room = (await read.getRoomReader().getById(roomId)) as IRoom; - const workspaceName = response.workspace_name as string; - - const blockBuilder = new BlockBuilder(this.app.getID()); - const sectionBlock = blockBuilder.createSectionBlock({ - text: `πŸ‘‹You are connected to Workspace **${workspaceName}**`, - }); - + const connectPreview = getConnectPreview(this.app.getID(), response); await sendNotification(read, modify, user, room, { - blocks: [sectionBlock], + blocks: [connectPreview], }); await roomInteraction.clearInteractionRoomId(user.id); From e1069e91bec1c3e025be6adc44a46a9ab3066dc8 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Tue, 6 Jun 2023 16:37:44 +0530 Subject: [PATCH 24/29] feat(ui): created and registered Custom Authorization HTML Page --- enum/OAuth2.ts | 13 +------ src/endpoints/webhook.ts | 23 ++++++++++-- src/helper/getAuthPageTemplate.ts | 61 +++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 src/helper/getAuthPageTemplate.ts diff --git a/enum/OAuth2.ts b/enum/OAuth2.ts index 9ed8ce0..dd70e1b 100644 --- a/enum/OAuth2.ts +++ b/enum/OAuth2.ts @@ -6,17 +6,8 @@ export enum OAuth2Locator { } export enum OAuth2Content { - success = '
\ -

\ - Authorization went successfully
\ - You can close this tab now
\ -

\ -
', - failed = '
\ -

\ - Oops, something went wrong, please try again or in case it still does not work, contact the administrator.\ -

\ -
', + success = "https://github-production-user-asset-6210df.s3.amazonaws.com/65061890/243671111-9964efff-3b23-4223-aadd-5f4be441037c.svg", + failed = "https://open.rocket.chat/assets/logo.png", } export enum OAuth2Credential { diff --git a/src/endpoints/webhook.ts b/src/endpoints/webhook.ts index 59e62e8..b83143e 100644 --- a/src/endpoints/webhook.ts +++ b/src/endpoints/webhook.ts @@ -26,6 +26,7 @@ import { BlockBuilder } from "../lib/BlockBuilder"; import { NotionSDK } from "../lib/NotionSDK"; import { RoomInteractionStorage } from "../storage/RoomInteraction"; import { getConnectPreview } from "../helper/getConnectLayout"; +import { getAuthPageTemplate } from "../helper/getAuthPageTemplate"; export class WebHookEndpoint extends ApiEndpoint { public path: string = "webhook"; @@ -41,12 +42,19 @@ export class WebHookEndpoint extends ApiEndpoint { ): Promise { const { code, state, error } = request.query; + const failedTemplate = getAuthPageTemplate( + "Something Went Wrong", + OAuth2Content.failed, + "🚫 Something went wrong while Connecting to Workspace", + "PLEASE TRY AGAIN IN CASE IT STILL DOES NOT WORK, CONTACT ADMINISTRATOR" + ); + // incase when user leaves in between the auth process if (error) { this.app.getLogger().warn(error); return { status: HttpStatusCode.UNAUTHORIZED, - content: OAuth2Content.failed, + content: failedTemplate, }; } @@ -58,7 +66,7 @@ export class WebHookEndpoint extends ApiEndpoint { .warn(`User not found before access token request`); return { status: HttpStatusCode.NON_AUTHORITATIVE_INFORMATION, - content: OAuth2Content.failed, + content: failedTemplate, }; } @@ -81,10 +89,17 @@ export class WebHookEndpoint extends ApiEndpoint { this.app.getLogger().warn(response.message); return { status: response.statusCode, - content: OAuth2Content.failed, + content: failedTemplate, }; } + const successTemplate = getAuthPageTemplate( + "Connected to Workspace", + OAuth2Content.success, + `πŸ‘‹ Connected to ${response.workspace_name}❗`, + "YOU CAN NOW CLOSE THIS WINDOW" + ); + const persistenceRead = read.getPersistenceReader(); const oAuth2Storage = new OAuth2Storage(persis, persistenceRead); await oAuth2Storage.connectUserToWorkspace(response, state); @@ -102,6 +117,6 @@ export class WebHookEndpoint extends ApiEndpoint { }); await roomInteraction.clearInteractionRoomId(user.id); - return this.success(OAuth2Content.success); + return this.success(successTemplate); } } diff --git a/src/helper/getAuthPageTemplate.ts b/src/helper/getAuthPageTemplate.ts new file mode 100644 index 0000000..b7d5550 --- /dev/null +++ b/src/helper/getAuthPageTemplate.ts @@ -0,0 +1,61 @@ +export function getAuthPageTemplate( + title: string, + imgUrl: string, + message: string, + info: string +) { + const template = ` + + + ${title} + + + +
+ Image +

+ ${message} +

+

+ ${info} +

+
+ + + `; + + return template; +} From bcbfabb035a9d15903c12fe4022c249987cd1a9f Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Wed, 7 Jun 2023 12:21:06 +0530 Subject: [PATCH 25/29] fix: Interface Design ICommandUtility and IOAuth2Client to support any type of Interaction --- definition/authorization/IOAuthClient.ts | 8 +++++--- definition/command/ICommandUtility.ts | 10 +++++---- src/authorization/OAuth2Client.ts | 26 +++++++++++------------- src/commands/CommandUtility.ts | 19 ++++++++--------- src/commands/NotionCommand.ts | 12 ++++++++++- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/definition/authorization/IOAuthClient.ts b/definition/authorization/IOAuthClient.ts index 1a8989e..94ac9e2 100644 --- a/definition/authorization/IOAuthClient.ts +++ b/definition/authorization/IOAuthClient.ts @@ -4,12 +4,13 @@ import { IPersistence, IRead, } from "@rocket.chat/apps-engine/definition/accessors"; -import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; import { IUser } from "@rocket.chat/apps-engine/definition/users"; export interface IOAuth2Client { connect( - context: SlashCommandContext, + room: IRoom, + sender: IUser, read: IRead, modify: IModify, http: IHttp, @@ -17,7 +18,8 @@ export interface IOAuth2Client { ): Promise; disconnect( - context: SlashCommandContext, + room: IRoom, + sender: IUser, read: IRead, modify: IModify, http: IHttp, diff --git a/definition/command/ICommandUtility.ts b/definition/command/ICommandUtility.ts index c62d774..a9f58a4 100644 --- a/definition/command/ICommandUtility.ts +++ b/definition/command/ICommandUtility.ts @@ -7,11 +7,9 @@ import { import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; import { IUser } from "@rocket.chat/apps-engine/definition/users"; import { NotionApp } from "../../NotionApp"; -import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; export interface ICommandUtility { app: NotionApp; - context: SlashCommandContext; params: Array; sender: IUser; room: IRoom; @@ -26,10 +24,14 @@ export interface ICommandUtility { } export interface ICommandUtilityParams { - context: SlashCommandContext; + app: NotionApp; + params: Array; + sender: IUser; + room: IRoom; read: IRead; modify: IModify; http: IHttp; persis: IPersistence; - app: NotionApp; + triggerId?: string; + threadId?: string; } diff --git a/src/authorization/OAuth2Client.ts b/src/authorization/OAuth2Client.ts index 662d964..1f08e87 100644 --- a/src/authorization/OAuth2Client.ts +++ b/src/authorization/OAuth2Client.ts @@ -1,4 +1,3 @@ -import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; import { NotionApp } from "../../NotionApp"; import { IHttp, @@ -14,34 +13,35 @@ import { getCredentials } from "../helper/getCredential"; import { OAuth2Credential, OAuth2Locator } from "../../enum/OAuth2"; import { URL } from "url"; import { getConnectBlock } from "../helper/getConnectBlock"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; export class OAuth2Client implements IOAuth2Client { constructor(private readonly app: NotionApp) {} public async connect( - context: SlashCommandContext, + room: IRoom, + sender: IUser, read: IRead, modify: IModify, http: IHttp, persis: IPersistence ) { const { blockBuilder, elementBuilder } = this.app.getUtils(); - const user = context.getSender(); - const room = context.getRoom(); - const authorizationUrl = await this.getAuthorizationUrl(user, read); - const message = `Hey **${user.username}**!πŸ‘‹ Connect your Notion Workspace`; + const authorizationUrl = await this.getAuthorizationUrl(sender, read); + const message = `Hey **${sender.username}**!πŸ‘‹ Connect your Notion Workspace`; const blocks = await getConnectBlock( this.app, message, authorizationUrl ); - await sendNotification(read, modify, user, room, { + await sendNotification(read, modify, sender, room, { blocks, }); } public async disconnect( - context: SlashCommandContext, + room: IRoom, + sender: IUser, read: IRead, modify: IModify, http: IHttp, @@ -51,26 +51,24 @@ export class OAuth2Client implements IOAuth2Client { const oAuthStorage = new OAuth2Storage(persis, persistenceRead); const { blockBuilder, elementBuilder } = this.app.getUtils(); - const room = context.getRoom(); - const user = context.getSender(); - const userId = user.id; + const userId = sender.id; const tokenInfo = await oAuthStorage.getCurrentWorkspace(userId); if (tokenInfo) { await oAuthStorage.disconnectUserFromCurrentWorkspace(userId); const message = `πŸ‘‹ You are disconnected from the Workspace **${tokenInfo.workspace_name}**`; - await sendNotification(read, modify, user, room, { message }); + await sendNotification(read, modify, sender, room, { message }); return; } - const authorizationUrl = await this.getAuthorizationUrl(user, read); + const authorizationUrl = await this.getAuthorizationUrl(sender, read); const message = `πŸ‘‹ You are not Connected to **Workspace**!`; const blocks = await getConnectBlock( this.app, message, authorizationUrl ); - await sendNotification(read, modify, user, room, { + await sendNotification(read, modify, sender, room, { blocks, }); } diff --git a/src/commands/CommandUtility.ts b/src/commands/CommandUtility.ts index 6b0a8aa..fab7b71 100644 --- a/src/commands/CommandUtility.ts +++ b/src/commands/CommandUtility.ts @@ -12,11 +12,9 @@ import { ICommandUtilityParams, } from "../../definition/command/ICommandUtility"; import { CommandParam } from "../../enum/CommandParam"; -import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; export class CommandUtility implements ICommandUtility { public app: NotionApp; - public context: SlashCommandContext; public params: Array; public sender: IUser; public room: IRoom; @@ -29,16 +27,15 @@ export class CommandUtility implements ICommandUtility { constructor(props: ICommandUtilityParams) { this.app = props.app; - this.context = props.context; + this.params = props.params; + this.sender = props.sender; + this.room = props.room; this.read = props.read; this.modify = props.modify; this.http = props.http; this.persis = props.persis; - this.params = props.context.getArguments(); - this.sender = props.context.getSender(); - this.room = props.context.getRoom(); - this.triggerId = props.context.getTriggerId(); - this.threadId = props.context.getThreadId(); + this.triggerId = props.triggerId; + this.threadId = props.threadId; } public async resolveCommand(): Promise { @@ -60,7 +57,8 @@ export class CommandUtility implements ICommandUtility { switch (this.params[0]) { case CommandParam.CONNECT: { await oAuth2ClientInstance.connect( - this.context, + this.room, + this.sender, this.read, this.modify, this.http, @@ -70,7 +68,8 @@ export class CommandUtility implements ICommandUtility { } case CommandParam.DISCONNECT: { await oAuth2ClientInstance.disconnect( - this.context, + this.room, + this.sender, this.read, this.modify, this.http, diff --git a/src/commands/NotionCommand.ts b/src/commands/NotionCommand.ts index 72ba186..62886a5 100644 --- a/src/commands/NotionCommand.ts +++ b/src/commands/NotionCommand.ts @@ -27,8 +27,18 @@ export class NotionCommand implements ISlashCommand { http: IHttp, persis: IPersistence ): Promise { + const params = context.getArguments(); + const sender = context.getSender(); + const room = context.getRoom(); + const triggerId = context.getTriggerId(); + const threadId = context.getThreadId(); + const commandUtilityParams: ICommandUtilityParams = { - context, + params, + sender, + room, + triggerId, + threadId, read, modify, http, From c7287e18dd3ec143bf46ee181f567b4700307299 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Wed, 7 Jun 2023 13:32:09 +0530 Subject: [PATCH 26/29] fix(ui): connectPreviewLayout font-weight to black bold --- src/helper/getConnectLayout.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/helper/getConnectLayout.ts b/src/helper/getConnectLayout.ts index f395000..85b1e64 100644 --- a/src/helper/getConnectLayout.ts +++ b/src/helper/getConnectLayout.ts @@ -20,8 +20,11 @@ export function getConnectPreview( ? `${workspace_icon}` : undefined; const thumb = workspace_icon_url ? { url: workspace_icon_url } : undefined; - const title = [`[**${workspace_name}**](${Notion.WEBSITE_URL})`]; - const description = ["**πŸ‘‹ You are Connected to Workspace**"]; + const title = [ + `**πŸ“š [**${workspace_name}**](${Notion.WEBSITE_URL})**`, + "**πŸ‘‹ Connected to Workspace**", + ]; + const description = [""]; const avatarElement = elementBuilder.addImage({ imageUrl: avatar_url as string, altText: name as string, From 5bc037489a9f43a32b3c747c5f0135ce4c699afd Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Wed, 7 Jun 2023 16:19:59 +0530 Subject: [PATCH 27/29] feat: Empty Credentials in AppSettings Notifies Relevent message based on User Role. --- definition/authorization/IOAuthClient.ts | 7 ++++- enum/Settings.ts | 1 + src/authorization/OAuth2Client.ts | 38 ++++++++++++++++++++---- src/endpoints/webhook.ts | 27 +++++++++++------ src/helper/getCredential.ts | 36 ++++++++++++++++++++-- 5 files changed, 92 insertions(+), 17 deletions(-) diff --git a/definition/authorization/IOAuthClient.ts b/definition/authorization/IOAuthClient.ts index 94ac9e2..e42aac6 100644 --- a/definition/authorization/IOAuthClient.ts +++ b/definition/authorization/IOAuthClient.ts @@ -26,5 +26,10 @@ export interface IOAuth2Client { persis: IPersistence ): Promise; - getAuthorizationUrl(user: IUser, read: IRead): Promise; + getAuthorizationUrl( + user: IUser, + read: IRead, + modify: IModify, + room: IRoom + ): Promise; } diff --git a/enum/Settings.ts b/enum/Settings.ts index 3699b7b..3e8d9d1 100644 --- a/enum/Settings.ts +++ b/enum/Settings.ts @@ -1,3 +1,4 @@ export enum ServerSetting { SITE_URL = "Site_Url", + USER_ROLE_ADMIN = "admin", } diff --git a/src/authorization/OAuth2Client.ts b/src/authorization/OAuth2Client.ts index 1f08e87..f620806 100644 --- a/src/authorization/OAuth2Client.ts +++ b/src/authorization/OAuth2Client.ts @@ -26,7 +26,17 @@ export class OAuth2Client implements IOAuth2Client { persis: IPersistence ) { const { blockBuilder, elementBuilder } = this.app.getUtils(); - const authorizationUrl = await this.getAuthorizationUrl(sender, read); + const authorizationUrl = await this.getAuthorizationUrl( + sender, + read, + modify, + room + ); + + if (!authorizationUrl) { + return; + } + const message = `Hey **${sender.username}**!πŸ‘‹ Connect your Notion Workspace`; const blocks = await getConnectBlock( this.app, @@ -61,7 +71,17 @@ export class OAuth2Client implements IOAuth2Client { return; } - const authorizationUrl = await this.getAuthorizationUrl(sender, read); + const authorizationUrl = await this.getAuthorizationUrl( + sender, + read, + modify, + room + ); + + if (!authorizationUrl) { + return; + } + const message = `πŸ‘‹ You are not Connected to **Workspace**!`; const blocks = await getConnectBlock( this.app, @@ -75,10 +95,18 @@ export class OAuth2Client implements IOAuth2Client { public async getAuthorizationUrl( user: IUser, - read: IRead - ): Promise { + read: IRead, + modify: IModify, + room: IRoom + ): Promise { const userId = user.id; - const { clientId, siteUrl } = await getCredentials(read); + const credentials = await getCredentials(read, modify, user, room); + + if (!credentials) { + return null; + } + + const { clientId, siteUrl } = credentials; const redirectUrl = new URL(OAuth2Locator.redirectUrlPath, siteUrl); const authorizationUrl = new URL(OAuth2Locator.authUri); diff --git a/src/endpoints/webhook.ts b/src/endpoints/webhook.ts index b83143e..976b871 100644 --- a/src/endpoints/webhook.ts +++ b/src/endpoints/webhook.ts @@ -70,7 +70,24 @@ export class WebHookEndpoint extends ApiEndpoint { }; } - const { clientId, clientSecret, siteUrl } = await getCredentials(read); + const persistenceRead = read.getPersistenceReader(); + const roomInteraction = new RoomInteractionStorage( + persis, + persistenceRead + ); + const roomId = await roomInteraction.getInteractionRoomId(user.id); + const room = (await read.getRoomReader().getById(roomId)) as IRoom; + + const appCredentials = await getCredentials(read, modify, user, room); + // incase there is no credentials in between auth interaction + if (!appCredentials) { + return { + status: HttpStatusCode.UNAUTHORIZED, + content: failedTemplate, + }; + } + + const { clientId, clientSecret, siteUrl } = appCredentials; const redirectUrl = new URL(this.url_path, siteUrl); const credentials = new Buffer(`${clientId}:${clientSecret}`).toString( OAuth2Credential.FORMAT @@ -100,17 +117,9 @@ export class WebHookEndpoint extends ApiEndpoint { "YOU CAN NOW CLOSE THIS WINDOW" ); - const persistenceRead = read.getPersistenceReader(); const oAuth2Storage = new OAuth2Storage(persis, persistenceRead); await oAuth2Storage.connectUserToWorkspace(response, state); - const roomInteraction = new RoomInteractionStorage( - persis, - persistenceRead - ); - const roomId = await roomInteraction.getInteractionRoomId(user.id); - const room = (await read.getRoomReader().getById(roomId)) as IRoom; - const connectPreview = getConnectPreview(this.app.getID(), response); await sendNotification(read, modify, user, room, { blocks: [connectPreview], diff --git a/src/helper/getCredential.ts b/src/helper/getCredential.ts index 806de2c..7bd5c16 100644 --- a/src/helper/getCredential.ts +++ b/src/helper/getCredential.ts @@ -1,9 +1,17 @@ -import { IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { IModify, IRead } from "@rocket.chat/apps-engine/definition/accessors"; import { ICredential } from "../../definition/authorization/ICredential"; import { OAuth2Setting } from "../../config/settings"; import { ServerSetting } from "../../enum/Settings"; +import { sendNotification } from "./message"; +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; -export async function getCredentials(read: IRead): Promise { +export async function getCredentials( + read: IRead, + modify: IModify, + user: IUser, + room: IRoom +): Promise { const clientId = (await read .getEnvironmentReader() .getSettings() @@ -17,6 +25,30 @@ export async function getCredentials(read: IRead): Promise { .getServerSettings() .getValueById(ServerSetting.SITE_URL)) as string; + if ( + !( + clientId.trim().length && + clientSecret.trim().length && + siteUrl.trim().length + ) + ) { + // based on user role send relevant notification + let message: string; + if (user.roles.includes(ServerSetting.USER_ROLE_ADMIN)) { + message = `Please Configure the App and Ensure the \`SiteUrl\` is correct in the Server Settings. + \xa0\xa0β€’ Go to **NotionApp** Settings and add \`ClientId\` and \`ClientSecret\` Generated from a Notion Public Integration + `; + } else { + message = `🚫 Something Went Wrong, Please Contact the Admin!`; + } + + await sendNotification(read, modify, user, room, { + message, + }); + + return null; + } + if (siteUrl.endsWith("/")) { siteUrl = siteUrl.substring(0, siteUrl.length - 1); } From f7257e93cd60b80e614b54b2109f5205ddd1da7f Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Sun, 11 Jun 2023 22:31:00 +0530 Subject: [PATCH 28/29] feat: support param with any letter case --- src/commands/CommandUtility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/CommandUtility.ts b/src/commands/CommandUtility.ts index fab7b71..72815f0 100644 --- a/src/commands/CommandUtility.ts +++ b/src/commands/CommandUtility.ts @@ -54,7 +54,7 @@ export class CommandUtility implements ICommandUtility { private async handleSingleParam(): Promise { const oAuth2ClientInstance = await this.app.getOAuth2Client(); - switch (this.params[0]) { + switch (this.params[0].toLowerCase()) { case CommandParam.CONNECT: { await oAuth2ClientInstance.connect( this.room, From f9df28f2fb1dc3dd8d091162844d4e3545cb77c3 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras Date: Mon, 12 Jun 2023 01:41:14 +0530 Subject: [PATCH 29/29] fix: NotionSDK design to avoid IHttp param in methods --- NotionApp.ts | 2 +- definition/lib/INotion.ts | 2 +- src/endpoints/webhook.ts | 3 +-- src/lib/NotionSDK.ts | 7 ++++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/NotionApp.ts b/NotionApp.ts index 1911bf0..50da671 100644 --- a/NotionApp.ts +++ b/NotionApp.ts @@ -58,7 +58,7 @@ export class NotionApp extends App { }); this.oAuth2Client = new OAuth2Client(this); - this.NotionSdk = new NotionSDK(); + this.NotionSdk = new NotionSDK(this.getAccessors().http); this.elementBuilder = new ElementBuilder(this.getID()); this.blockBuilder = new BlockBuilder(this.getID()); } diff --git a/definition/lib/INotion.ts b/definition/lib/INotion.ts index 76db128..7a933a3 100644 --- a/definition/lib/INotion.ts +++ b/definition/lib/INotion.ts @@ -10,8 +10,8 @@ export interface INotion { } export interface INotionSDK extends INotion { + http: IHttp; createToken( - http: IHttp, redirectUrl: URL, code: string, credentials: string diff --git a/src/endpoints/webhook.ts b/src/endpoints/webhook.ts index 976b871..ab7e1fa 100644 --- a/src/endpoints/webhook.ts +++ b/src/endpoints/webhook.ts @@ -93,9 +93,8 @@ export class WebHookEndpoint extends ApiEndpoint { OAuth2Credential.FORMAT ); - const notionSDK = new NotionSDK(); + const notionSDK = new NotionSDK(http); const response = await notionSDK.createToken( - http, redirectUrl, code, credentials diff --git a/src/lib/NotionSDK.ts b/src/lib/NotionSDK.ts index 5a84470..1012fda 100644 --- a/src/lib/NotionSDK.ts +++ b/src/lib/NotionSDK.ts @@ -10,19 +10,20 @@ import { AppsEngineException } from "@rocket.chat/apps-engine/definition/excepti export class NotionSDK implements INotionSDK { baseUrl: string; NotionVersion: string; - constructor() { + http: IHttp; + constructor(http: IHttp) { this.baseUrl = NotionApi.BASE_URL; this.NotionVersion = NotionApi.VERSION; + this.http = http; } public async createToken( - http: IHttp, redirectUrl: URL, code: string, credentials: string ): Promise { try { - const response = await http.post(OAuth2Locator.accessTokenUrl, { + const response = await this.http.post(OAuth2Locator.accessTokenUrl, { data: { grant_type: OAuth2Credential.GRANT_TYPE, redirect_uri: redirectUrl.toString(),