Skip to content

Commit

Permalink
refactor: move service logic out of route
Browse files Browse the repository at this point in the history
  • Loading branch information
1emu committed Jun 13, 2024
1 parent d76c1cb commit f034e10
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 138 deletions.
129 changes: 13 additions & 116 deletions src/back/routes/user.ts
Original file line number Diff line number Diff line change
@@ -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))
Expand All @@ -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<ValidationComment>((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<ValidationComment>((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)
}
152 changes: 130 additions & 22 deletions src/back/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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<ValidationComment>((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) {
Expand All @@ -64,6 +87,33 @@ export class UserService {
return validationComment
}

static async validateDiscordUser(user: string) {
try {
const messages = await DiscordService.getProfileVerificationMessages()
const formattedMessages = messages.map<ValidationComment>((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) {
Expand Down Expand Up @@ -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<AccountType>): Promise<boolean> {
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<UserAttributes>({ address: address.toLowerCase() })
try {
const user = await UserModel.findOne<UserAttributes>({ 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}`)
}
}
}
}

0 comments on commit f034e10

Please sign in to comment.