import { prismaClient as prisma } from '../../prismaClient' import GeneralUtils from '../../utils/general' import { logger } from '../../utils/logger' import * as NotificationsUtils from '../../utils/notification' import * as SendBirdUtils from '../../utils/sendbird' import * as Slack from '../../utils/slack' import { putGoodResidentsInPropertyChannel, sendMessage } from '.' type SendBirdWebhookData = { category: string sender: { nickname: string user_id: string profile_url: string metadata: Record } custom_type: string mention_type: string mentioned_users: string[] app_id: string members: SendBirdUtils.ChannelMember[] type: string payload: { url: string custom_type: string created_at: number message: string data: string message_id: number } channel: SendBirdUtils.Channel } export const getWalterUsers = (userIds: string[]) => { return prisma.user.findMany({ where: { OR: [ { id: { in: userIds, }, }, { managingCompany: { id: { in: userIds, }, }, }, { serviceProvider: { id: { in: userIds, }, }, }, ], }, select: { id: true, role: true, hasPrivateChat: true, managingCompany: { select: { id: true, shortName: true, longName: true, automaticChatMessage: true, automaticChatMessageFr: true, phone: { select: { number: true, extension: true, }, }, emergencyPhone: { select: { number: true, extension: true, }, }, businessHours: { select: { dayOfWeek: true, openAtMS: true, closeAtMS: true, openAt: true, closeAt: true, isClosed: true, }, }, }, }, currentProject: { select: { id: true, name: true, building: { select: { id: true, }, }, }, }, serviceProvider: { select: { id: true, type: true, serviceProjects: { select: { id: true, businessHours: { select: { dayOfWeek: true, openAtMS: true, closeAtMS: true, openAt: true, closeAt: true, isClosed: true, }, }, project: { select: { id: true, }, }, }, }, }, }, }, }) } export const handleWebhook = async (req, res) => { try { res.status(200).send(true) const webhookData: SendBirdWebhookData = req.body const { category, members, sender: sendBirdSender, payload, channel } = webhookData // We only want to handle those webhooks for now if (category !== 'group_channel:message_send') { return } // If the message was only transfered to another channel, we don't want to send another notification try { if (payload.data) { const messageDataJson = JSON.parse(payload.data) if (messageDataJson.v2transfered) { // Because in the v1 of the resident app, for no reason, we decided to show the message.data // So it shows {v2Transfered: true}... -_- return SendBirdUtils.updateMessage({ channelUrl: channel.channel_url, messageId: payload.message_id, messageType: payload.url ? 'FILE' : 'MESG', data: '', }) } } } catch (error) { logger.warn(error) } // Helpers const getTemplate = (user) => { return user.role === 'RESIDENT' ? user.preferedLanguage === 'fr' ? 'NEW_MESSAGE_TO_RESIDENT_FR' : 'NEW_MESSAGE_TO_RESIDENT' : 'NEW_MESSAGE_TO_MANAGER' } const usersInChannelThatShouldBeNotified = await getWalterUsers( members.map(({ user_id }) => user_id) ) let sender const receivers = [] const isWalterSupport = members.some((member) => member.user_id === 'walter') usersInChannelThatShouldBeNotified.forEach((user) => { if (user.managingCompany && user.role === 'MANAGER') { if (user.hasPrivateChat && user.id === sendBirdSender.user_id) { sender = user return } else if (user.managingCompany.id === sendBirdSender.user_id) { sender = user return } } if (user?.id === sendBirdSender.user_id) { sender = user return } if (user.id === sendBirdSender.user_id) { sender = user return } receivers.push(user) }) if (!sender && isWalterSupport) { sender = { firstName: 'Walter', } } if (!sender) { throw new Error( `Can't find sender in channel: ${JSON.stringify( req.body, null, 2 )} and users/companies founded: ${JSON.stringify( usersInChannelThatShouldBeNotified, null, 2 )}` ) } const senderIsProvider = sender.role === 'PROVIDER' const senderIsManager = sender.role === 'MANAGER' const senderIsResident = sender.role === 'RESIDENT' const receiverIsProvider = receivers.some((u) => u.role === 'PROVIDER') const receiverIsManager = receivers.some((u) => u.role === 'MANAGER') const userResident = senderIsResident ? sender : receivers[0] const channelIsServiceProvider = senderIsProvider || receiverIsProvider // SERVICE PROVIDER - RESIDENT if (channelIsServiceProvider) { const receiverIsResident = receivers.some((u) => u.role === 'RESIDENT') const serviceProjects = receiverIsResident ? sender.serviceProvider.serviceProjects : receivers[0].serviceProvider.serviceProjects const project = receiverIsResident ? receivers[0].currentProject : sender.currentProject const serviceProject = serviceProjects.find((sP) => sP.project?.id === project.id) const fromName = senderIsResident ? GeneralUtils.getUserName(sender) : sender.serviceProvider.type // Disable for now... // Slack.send({ // channel: 'providerChat', // subject: `New chat message sent from (${fromName}) to (${receiverName})`, // text: `"${req.body.payload.message}"`, // }).catch(logger.error) return NotificationsUtils.send({ serviceProjectId: serviceProject.id, forceSend: true, // Provider needs to receive chat forceSMS: true, forceEmail: true, users: receivers, data: channel, title: `New message from ${fromName}`, message: payload.message || 'IMAGE', type: 'NEW_CHAT_MESSAGE', // TODO: Find out why this isn't rowing an error... // In utils/notification.js we have: // getEmailDataForUser?: (user: Partial) => Promise // So the property 'personalizations' should throw an error here?!?! getEmailDataForUser: (user) => Promise.resolve({ template: getTemplate(user), customVariables: { fromName, bodySplitted: GeneralUtils.splitTextWithLineBreak( GeneralUtils.truncate({ str: payload.message?.split('communication@parse.usewalter.com')[0] || 'New attachment', maxChars: 500, }) ), isProvider: receiverIsProvider, isResident: receiverIsResident, serviceProjectId: serviceProject?.id, channelUrl: channel.channel_url, }, }), }) } // WALTER SUPPORT - RESIDENT if (isWalterSupport) { const receiverIsResident = receivers.some((u) => u.role === 'RESIDENT') const project = senderIsResident ? sender.currentProject : receivers[0].currentProject const fromName = senderIsResident ? GeneralUtils.getUserName(sender) : 'Walter support' const receiverName = receiverIsResident ? GeneralUtils.getUserName(receivers[0]) : 'Walter support' // For us Slack.send({ channel: 'walterChat', subject: `New chat message sent from (${fromName}) to (${receiverName})`, text: `"${req.body.payload.message}"`, }).catch(logger.error) return NotificationsUtils.send({ projectId: project.id, isFromWalter: true, users: receivers, data: channel, title: `New message from ${fromName}`, message: payload.message || 'IMAGE', type: 'NEW_CHAT_MESSAGE', getEmailDataForUser: (user) => Promise.resolve({ template: getTemplate(user), customVariables: { fromName, bodySplitted: GeneralUtils.splitTextWithLineBreak( GeneralUtils.truncate({ str: payload.message?.split('communication@parse.usewalter.com')[0] || 'New attachment', maxChars: 500, }) ), isResident: receiverIsResident, projectId: project?.id, channelUrl: channel.channel_url, }, }), }) } const [projectId, propertyId, isPrivate] = channel.name.split('-') const property = await prisma.property.findOne({ where: { id: propertyId }, select: { id: true, address: { select: { apartmentNumber: true, }, }, building: { select: { id: true, project: { select: { id: true, name: true, managingCompany: { select: { id: true, }, }, }, }, }, }, owners: { select: { id: true, }, }, users: { select: { id: true, }, }, }, }) // MANAGER (PRIVATE) if (isPrivate === 'private') { const { mentioned_users } = req.body if (mentioned_users.length === 0) { return } const usersMentioned = await prisma.user.findMany({ where: { id: { in: mentioned_users.map((u) => u.user_id), }, }, select: { id: true, }, }) return NotificationsUtils.send({ projectId, users: usersMentioned, type: 'NEW_CHAT_PRIVATE_MENTIONED', noAppNotification: true, title: `${ GeneralUtils.getUserName(sender) || 'Someone' } mentioned you in the conversation of unit #${ property?.address?.apartmentNumber || 'N/A' } - ${property?.building?.project?.name || 'N/A'}`, message: payload.message || 'IMAGE', data: { channel_url: channel.channel_url, }, emailData: { template: 'PRIVATE_CONVERSATION_MENTIONED', customVariables: { channelUrl: channel.channel_url, projectId, }, }, }) } // MANAGER - RESIDENT (NEW) if (projectId && projectId !== 'chat' && property) { const senderName = senderIsResident ? `${GeneralUtils.getUserName(sender)} - #${property.address?.apartmentNumber || 'N/A'}` : sender.managingCompany?.shortName || sender.managingCompany?.longName || '' // Slack message for us, disable for now.. // const receiversManagers = receivers.filter((r) => r.role === 'MANAGER'). // const receiverName = senderIsResident // ? receiversManagers[0].managingCompany?.shortName || // receiversManagers[0].managingCompany?.longName || // '' // : `${GeneralUtils.getUserName(sender)} - #${property.address?.apartmentNumber || 'N/A'}` // Slack.send({ // channel: 'managerChat', // subject: `(V2) New chat message sent from (${senderName}) to (${receiverName})`, // text: `"${req.body.payload.message}"`, // }).catch(logger.error) const residentsOfProperties = GeneralUtils.uniq([...property.owners, ...property.users], 'id') // Protection to make sure users are in good channel const validResidents = receivers .filter((user) => user.role === 'RESIDENT') .filter((user) => residentsOfProperties.some((residentOfProperty) => residentOfProperty.id === user.id) ) if (validResidents.length !== residentsOfProperties.length) { putGoodResidentsInPropertyChannel({ projectId, propertyId }).catch(logger.error) } // Make sure we send a notificaiton to the good person. EX: if we put a wrong user in a conversation by mistake // Don't send the notification at least... We will still need to remove it from the channel tought const validReceivers = receivers.filter( (user) => user.role === 'MANAGER' || residentsOfProperties.some((residentOfProperty) => residentOfProperty.id === user.id) ) return NotificationsUtils.send({ projectId, users: validReceivers, data: { channel_url: channel.channel_url, }, title: `New message from ${senderName}`, message: payload.message || 'IMAGE', type: 'NEW_CHAT_MESSAGE', getEmailDataForUser: (user) => Promise.resolve({ template: getTemplate(user), customVariables: { fromName: senderName, projectId, bodySplitted: GeneralUtils.splitTextWithLineBreak( GeneralUtils.truncate({ str: payload.message?.split('communication@parse.usewalter.com')[0] || 'New attachment', maxChars: 500, }) ), isManager: receiverIsManager, isResident: user.role === 'RESIDENT', channelUrl: channel.channel_url, }, }), }) } // MANAGER - RESIDENT (OLD) if (senderIsManager || receiverIsManager) { // We need to transfer this message to all the unit conversations of the user const propertiesOfUser = await prisma.property.findMany({ where: { AND: [ { building: { project: { id: userResident.currentProject.id, }, }, }, { OR: [ { users: { some: { id: userResident.id, }, }, }, { owners: { some: { id: userResident.id, }, }, }, ], }, ], }, select: { id: true, address: { select: { apartmentNumber: true, }, }, }, }) await Promise.all( propertiesOfUser.map((property) => sendMessage({ fromSendbirdUserId: senderIsManager ? sender.managingCompany.id : sender.id, message: payload.message, attachments: payload.url ? [ { url: payload.url, }, ] : [], projectId: userResident.currentProject.id, propertyId: property.id, messageData: JSON.stringify({ v2transfered: true }), }) ) ) const fromName = senderIsResident ? await GeneralUtils.getUserNameAndUnits(sender, sender.currentProject) : sender.managingCompany?.shortName || sender.managingCompany?.longName || '' return NotificationsUtils.send({ projectId: userResident.currentProject.id, users: receivers, data: { channel_url: channel.channel_url, }, title: `New message from ${fromName}`, message: payload.message || 'IMAGE', type: 'NEW_CHAT_MESSAGE', getEmailDataForUser: (user) => Promise.resolve({ template: getTemplate(user), customVariables: { fromName, projectId: userResident.currentProject.id, bodySplitted: GeneralUtils.splitTextWithLineBreak( GeneralUtils.truncate({ str: payload.message?.split('communication@parse.usewalter.com')[0] || 'New attachment', maxChars: 500, }) ), isManager: receiverIsManager, isResident: user.role === 'RESIDENT', channelUrl: channel.channel_url, }, }), }) } // If the process didn't enter in any "if" and didn't return // it means the notification/message was not handled. throw new Error(`Invalid message: ${req.body}`) } catch (error) { logger.error(error) } }