diff --git a/src/back/routes/user.ts b/src/back/routes/user.ts index 10abd4879..f0cace271 100644 --- a/src/back/routes/user.ts +++ b/src/back/routes/user.ts @@ -1,23 +1,17 @@ import { WithAuth, auth } from 'decentraland-gatsby/dist/entities/Auth/middleware' -import RequestError from 'decentraland-gatsby/dist/entities/Route/error' import handleAPI from 'decentraland-gatsby/dist/entities/Route/handle' import routes from 'decentraland-gatsby/dist/entities/Route/routes' import { Request } from 'express' -import { GATSBY_DISCOURSE_CONNECT_THREAD } from '../../entities/User/constants' -import { ValidationComment } from '../../entities/User/types' import { validateAccountTypes } from '../../entities/User/utils' -import { DiscourseService } from '../../services/DiscourseService' -import { ErrorService } from '../../services/ErrorService' -import { DiscordService } from '../services/discord' import { UserService } from '../services/user' import { validateAddress } from '../utils/validations' export default routes((route) => { const withAuth = auth() route.get('/user/validate', withAuth, handleAPI(getValidationMessage)) - route.post('/user/validate/forum', withAuth, handleAPI(checkForumValidationMessage)) - route.post('/user/validate/discord', withAuth, handleAPI(checkDiscordValidationMessage)) + route.post('/user/validate/forum', withAuth, handleAPI(validateForumUser)) + route.post('/user/validate/discord', withAuth, handleAPI(validateDiscordUser)) route.post('/user/discord-active', withAuth, handleAPI(updateDiscordStatus)) route.get('/user/discord-active', withAuth, handleAPI(getIsDiscordActive)) route.get('/user/discord-linked', withAuth, handleAPI(isDiscordLinked)) @@ -32,139 +26,42 @@ async function getValidationMessage(req: WithAuth) { return UserService.getValidationMessage(address, account) } -async function checkForumValidationMessage(req: WithAuth) { +async function validateForumUser(req: WithAuth) { const user = req.auth! - try { - const comments = await DiscourseService.getPostComments(Number(GATSBY_DISCOURSE_CONNECT_THREAD)) - const formattedComments = comments.comments.map((comment) => ({ - id: '', - userId: String(comment.user_forum_id), - content: comment.cooked, - timestamp: new Date(comment.created_at).getTime(), - })) - - const validationComment = await UserService.checkForumValidationMessage(user, formattedComments) - return { - valid: !!validationComment, - } - } catch (error) { - throw new Error("Couldn't validate the user. " + error) - } + return UserService.validateForumUser(user) } -async function checkDiscordValidationMessage(req: WithAuth) { +async function validateDiscordUser(req: WithAuth) { const user = req.auth! - try { - const messages = await DiscordService.getProfileVerificationMessages() - const formattedMessages = messages.map((message) => ({ - id: message.id, - userId: message.author.id, - content: message.content, - timestamp: message.createdTimestamp, - })) - - const validationComment = await UserService.checkDiscordValidationMessage(user, formattedMessages) - if (validationComment) { - await DiscordService.deleteVerificationMessage(validationComment.id) - DiscordService.sendDirectMessage(validationComment.userId, { - title: 'Profile verification completed ✅', - action: `You have been verified as ${user}\n\nFrom now on you will receive important notifications for you through this channel.`, - fields: [], - }) - } - - return { - valid: !!validationComment, - } - } catch (error) { - throw new Error("Couldn't validate the user. " + error) - } + return await UserService.validateDiscordUser(user) } async function updateDiscordStatus(req: WithAuth) { const address = req.auth! const { is_discord_notifications_active } = req.body - const enabledMessage = - 'You have enabled the notifications through Discord, from now on you will receive notifications that may concern you through this channel.' - const disabledMessage = - 'You have disabled the notifications through Discord, from now on you will no longer receive notifications through this channel.' - - try { - if (typeof is_discord_notifications_active !== 'boolean') { - throw new Error('Invalid discord status') - } - const account = await UserService.updateDiscordActiveStatus(address, is_discord_notifications_active) - if (account) { - if (account.is_discord_notifications_active) { - DiscordService.sendDirectMessage(account.discord_id, { - title: 'Notifications enabled ✅', - action: enabledMessage, - fields: [], - }) - } else { - DiscordService.sendDirectMessage(account.discord_id, { - title: 'Notifications disabled ❌', - action: disabledMessage, - fields: [], - }) - } - } - } catch (error) { - throw new Error(`Error while updating discord status. ${error}`) + if (typeof is_discord_notifications_active !== 'boolean') { + throw new Error('Invalid discord status') } + await UserService.updateDiscordStatus(address, is_discord_notifications_active) } async function getIsDiscordActive(req: WithAuth) { const address = req.auth! - try { - return await UserService.getIsDiscordActive(address) - } catch (error) { - throw new Error(`Error while fetching discord status. ${error}`) - } + return await UserService.getIsDiscordActive(address) } async function isDiscordLinked(req: WithAuth) { const address = req.auth! - try { - return await UserService.isDiscordLinked(address) - } catch (error) { - throw new Error(`Error while fetching discord status. ${error}`) - } + return await UserService.isDiscordLinked(address) } async function isValidated(req: Request) { const address = validateAddress(req.params.address) const accounts = validateAccountTypes(req.query.account as string | string[] | undefined) - try { - return await UserService.isValidated(address, new Set(accounts)) - } catch (error) { - const message = 'Error while fetching validation data' - ErrorService.report(message, { error: `${error}` }) - throw new Error(`${message}. ${error}`) - } + return await UserService.isValidated(address, new Set(accounts)) } async function getProfile(req: Request) { const address = validateAddress(req.params.address) - try { - const user = await UserService.getProfile(address) - if (!user) { - throw new RequestError('User not found', RequestError.NotFound) - } - const { forum_id } = user - - return { - forum_id, - forum_username: forum_id ? (await DiscourseService.getUserById(forum_id))?.username : null, - } - } catch (error: unknown) { - if (error instanceof Error) { - throw new RequestError( - `Error while fetching profile data: ${error.message}. Stack: ${error.stack}`, - RequestError.InternalServerError - ) - } else { - throw new RequestError(`An unexpected error occurred ${error}`, RequestError.InternalServerError) - } - } + return await UserService.getProfile(address) } diff --git a/src/back/services/user.ts b/src/back/services/user.ts index d06912d22..e3862cce2 100644 --- a/src/back/services/user.ts +++ b/src/back/services/user.ts @@ -2,7 +2,7 @@ import { ChainId } from '@dcl/schemas/dist/dapps/chain-id' import { PUSH_CHANNEL_ID } from '../../constants' import { isSameAddress } from '../../entities/Snapshot/utils' -import { MESSAGE_TIMEOUT_TIME } from '../../entities/User/constants' +import { GATSBY_DISCOURSE_CONNECT_THREAD, MESSAGE_TIMEOUT_TIME } from '../../entities/User/constants' import UserModel from '../../entities/User/model' import { AccountType, UserAttributes, ValidationComment, ValidationMessage } from '../../entities/User/types' import { @@ -11,9 +11,13 @@ import { toAccountType, validateComment, } from '../../entities/User/utils' +import { DiscourseService } from '../../services/DiscourseService' +import { ErrorService } from '../../services/ErrorService' import { isProdEnv } from '../../utils/governanceEnvs' import { getCaipAddress, getPushNotificationsEnv } from '../../utils/notifications' +import { DiscordService } from './discord' + import PushAPI = require('@pushprotocol/restapi') export class UserService { @@ -42,6 +46,25 @@ export class UserService { return formatValidationMessage(address, timestamp, toAccountType(account)) } + static async validateForumUser(user: string) { + try { + const comments = await DiscourseService.getPostComments(Number(GATSBY_DISCOURSE_CONNECT_THREAD)) + const formattedComments = comments.comments.map((comment) => ({ + id: '', + userId: String(comment.user_forum_id), + content: comment.cooked, + timestamp: new Date(comment.created_at).getTime(), + })) + + const validationComment = await this.checkForumValidationMessage(user, formattedComments) + return { + valid: !!validationComment, + } + } catch (error) { + throw new Error("Couldn't validate the user. " + error) + } + } + static async checkForumValidationMessage(user: string, validationComments: ValidationComment[]) { const messageProperties = this.VALIDATIONS_IN_PROGRESS[user] if (!messageProperties) { @@ -64,6 +87,33 @@ export class UserService { return validationComment } + static async validateDiscordUser(user: string) { + try { + const messages = await DiscordService.getProfileVerificationMessages() + const formattedMessages = messages.map((message) => ({ + id: message.id, + userId: message.author.id, + content: message.content, + timestamp: message.createdTimestamp, + })) + + const validationComment = await this.checkDiscordValidationMessage(user, formattedMessages) + if (validationComment) { + await DiscordService.deleteVerificationMessage(validationComment.id) + DiscordService.sendDirectMessage(validationComment.userId, { + title: 'Profile verification completed ✅', + action: `You have been verified as ${user}\n\nFrom now on you will receive important notifications for you through this channel.`, + fields: [], + }) + } + + return { + valid: !!validationComment, + } + } catch (error) { + throw new Error("Couldn't validate the user. " + error) + } + } static async checkDiscordValidationMessage(user: string, validationComments: ValidationComment[]) { const messageProperties = this.VALIDATIONS_IN_PROGRESS[user] if (!messageProperties) { @@ -93,41 +143,99 @@ export class UserService { return account.length > 0 ? account[0] : null } + static async updateDiscordStatus(address: string, isDiscordNotificationsActive: boolean) { + try { + const account = await this.updateDiscordActiveStatus(address, isDiscordNotificationsActive) + if (account) { + if (account.is_discord_notifications_active) { + const enabledMessage = + 'You have enabled the notifications through Discord, from now on you will receive notifications that may concern you through this channel.' + DiscordService.sendDirectMessage(account.discord_id, { + title: 'Notifications enabled ✅', + action: enabledMessage, + fields: [], + }) + } else { + const disabledMessage = + 'You have disabled the notifications through Discord, from now on you will no longer receive notifications through this channel.' + DiscordService.sendDirectMessage(account.discord_id, { + title: 'Notifications disabled ❌', + action: disabledMessage, + fields: [], + }) + } + } + } catch (error) { + throw new Error(`Error while updating discord status. ${error}`) + } + } + static async getIsDiscordActive(address: string) { - const account = await UserModel.getDiscordIds([address]) - return account.length > 0 ? account[0].is_discord_notifications_active : false + try { + const account = await UserModel.getDiscordIds([address]) + return account.length > 0 ? account[0].is_discord_notifications_active : false + } catch (error) { + throw new Error(`Error while fetching discord status. ${error}`) + } } static async isDiscordLinked(address: string) { - return await UserModel.isValidated(address, new Set([AccountType.Discord])) + try { + return await UserModel.isValidated(address, new Set([AccountType.Discord])) + } catch (error) { + throw new Error(`Error while fetching discord status. ${error}`) + } } static async isValidated(address: string, accounts: Set): Promise { - if (!accounts.has(AccountType.Push)) { - return await UserModel.isValidated(address, accounts) - } + try { + if (!accounts.has(AccountType.Push)) { + return await UserModel.isValidated(address, accounts) + } - const chainId = isProdEnv() ? ChainId.ETHEREUM_MAINNET : ChainId.ETHEREUM_SEPOLIA - const env = getPushNotificationsEnv(chainId) + const chainId = isProdEnv() ? ChainId.ETHEREUM_MAINNET : ChainId.ETHEREUM_SEPOLIA + const env = getPushNotificationsEnv(chainId) - const pushSubscriptions = await PushAPI.user.getSubscriptions({ - user: getCaipAddress(address, chainId), - env, - }) + const pushSubscriptions = await PushAPI.user.getSubscriptions({ + user: getCaipAddress(address, chainId), + env, + }) - const isSubscribedToPush = !!pushSubscriptions?.find((item: { channel: string }) => - isSameAddress(item.channel, PUSH_CHANNEL_ID) - ) - accounts.delete(AccountType.Push) + const isSubscribedToPush = !!pushSubscriptions?.find((item: { channel: string }) => + isSameAddress(item.channel, PUSH_CHANNEL_ID) + ) + accounts.delete(AccountType.Push) - if (accounts.size === 0) { - return isSubscribedToPush + if (accounts.size === 0) { + return isSubscribedToPush + } + return isSubscribedToPush && (await UserModel.isValidated(address, accounts)) + } catch (error) { + const message = 'Error while fetching validation data' + ErrorService.report(message, { error: `${error}` }) + throw new Error(`${message}. ${error}`) } - - return isSubscribedToPush && (await UserModel.isValidated(address, accounts)) } static async getProfile(address: string) { - return await UserModel.findOne({ address: address.toLowerCase() }) + try { + const user = await UserModel.findOne({ address: address.toLowerCase() }) + if (!user) { + const emptyProfile: UserAttributes = { address } + return emptyProfile + } + const { forum_id } = user + + return { + forum_id, + forum_username: forum_id ? (await DiscourseService.getUserById(forum_id))?.username : null, + } + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`Error while fetching profile data: ${error.message}. Stack: ${error.stack}`) + } else { + throw new Error(`Unexpected error while fetching profile data ${error}`) + } + } } }