import Constants from '../../constants/general' import GeneralUtils from '../../utils/general' import { logger } from '../../utils/logger' import * as SendBirdUtils from '../../utils/sendbird' import { ChannelUpdateRequest } from '../../utils/sendbird' import { prismaClient as prisma } from '../../prismaClient' type AttachmentType = { url: string name?: string mimetype?: string } type SendMessageInputType = { fromSendbirdUserId: string message?: string attachments?: AttachmentType[] projectId?: string propertyId?: string channelUrl?: string serviceId?: string otherUserId?: string isPrivateChannel?: boolean isSupport?: boolean messageData?: string createdAt?: number } export const sendMessage = async ({ message, fromSendbirdUserId, attachments, projectId, propertyId, channelUrl, serviceId, otherUserId, isPrivateChannel, isSupport, messageData, createdAt = Date.now(), }: SendMessageInputType) => { try { if (!fromSendbirdUserId) { throw new Error("Can't send message when we don't know from who it's coming from") } logger.info(`fromSendbirdUserId: ${fromSendbirdUserId}`) // Don't send the empty message but it's still valid I guess... // The verification should be made in the caller's logic maybe if (!message && !attachments) { return Promise.resolve(true) } if ((!projectId || !propertyId) && !serviceId && !channelUrl) { throw new Error( `Miss informations to send to the good channel (${projectId}, ${propertyId}, ${serviceId}, ${channelUrl})` ) } let channelUrlToSendTo = channelUrl // Making sure that if we called the function sendMessage for a property, service, ... // That the channel is created otherwise create it if (!channelUrlToSendTo) { if (isSupport) { const channel = await createOrGetWalterSupportChannel({ otherUserId, }) channelUrlToSendTo = channel.channel_url } else if (serviceId) { const channel = await createOrGetServiceProviderChannel({ serviceId, otherUserId }) channelUrlToSendTo = channel.channel_url } else { const [publicChannel, privateChannel] = await createOrGetManagerChannel({ propertyId, projectId, }) if (isPrivateChannel) { channelUrlToSendTo = privateChannel.channel_url } else { channelUrlToSendTo = publicChannel.channel_url } } } // Sendbird wants Unix in milliseconds if (createdAt && createdAt.toString().length === 10) { createdAt = createdAt * 1000 } if (attachments?.length > 0) { // Still send the message after if this crashed for some reason try { await Promise.all( attachments.map((attachement) => SendBirdUtils.sendFileMessage({ messageData, channelUrl, createdAt, attachmentUrl: attachement.url, attachmentName: attachement.name, attachementType: attachement.mimetype, userId: fromSendbirdUserId, }) ) ) } catch (error) { logger.error(error) } } if (message) { await SendBirdUtils.sendMessage({ message, channelUrl, createdAt, messageData, userId: fromSendbirdUserId, }) } } catch (err) { logger.error(err) } } export const getChannels = ({ all = false, ...rest }: SendBirdUtils.GetChannelInputType): Promise => { let channelsToReturn = [] async function helper(token: string) { const { channels, next } = await SendBirdUtils.getChannels({ token, ...rest }) channelsToReturn = [...channelsToReturn, ...channels] if (all && next) { return helper(next) } return channelsToReturn } return helper(null) } export const getChannelByUrl = ({ channelUrl, show_member = false, order = '', showReadReceipt = false, showDeliveryReceipt = false, }): Promise => { if (!channelUrl) { throw new Error("Can't get channel by url without url...") } return SendBirdUtils.getChannelByUrl({ channelUrl, order, show_member: show_member, show_read_receipt: showReadReceipt, show_delivery_receipt: showDeliveryReceipt, }) } // SendBird doesn't have a get channel by name but we found a way by fetching // all channels and only getting the first 1 with the specified name export const getChannelByName = async ({ name, show_member = false, ...rest }): Promise => { if (!name) { throw new Error("Can't get channel by name without name...") } const channels = await getChannels({ name, limit: 1, show_member: show_member, ...rest, }) return channels[0] } export function createUser({ userId, nickname, profileUrl, }: { userId: string nickname: string profileUrl: string }) { return SendBirdUtils.createUser({ userId, nickname, profileUrl, }) } export const updateUserInfoIfExist = async ({ userId, nickname, profileUrl, }: { userId: string nickname: string profileUrl?: string }) => { if (!nickname) { return } try { const existingUser = await SendBirdUtils.getUser(userId) if (existingUser) { return SendBirdUtils.updateUser({ userId, nickname, profileUrl }) } } catch (err) { logger.warn(`can't find or update user ${userId}`) } } export const getManagerChannel = ({ propertyId, projectId, isPrivateChannel = false }) => { if (!propertyId && !projectId) { throw new Error("Can't create or get a channel without property id and project id") } return getChannelByName({ name: `${projectId}-${propertyId}${isPrivateChannel ? '-private' : ''}`, show_member: true, }) } export const getWalterSupportChannel = async ({ otherUserId }) => { if (!otherUserId) { throw new Error("Can't create or get a walter channel without otherUserId") } const channel = await getChannelByName({ name: `${otherUserId}-walter`, show_member: true, }) // There's 2 way we could've named the channel if (!channel) { return getChannelByName({ name: `walter-${otherUserId}`, show_member: true, }) } } export const getServiceProviderChannel = async ({ serviceId, otherUserId }) => { if (!serviceId || !otherUserId) { throw new Error("Can't get a service channel without service id or other member id") } try { const channels = await getChannels({ members_exactly_in: [serviceId, otherUserId].join(','), limit: 1, }) return channels[0] } catch (err) { logger.warn(`getting channels failed: ${err.message}`) } return null } export const createOrGetWalterSupportChannel = async ({ otherUserId }) => { if (!otherUserId) { throw new Error("Can't create or get a service channel without service id or other members ids") } const existingChannel = await getWalterSupportChannel({ otherUserId }) if (existingChannel) { return existingChannel } return createWalterSupportChannel({ otherUserId }) } export const createOrGetServiceProviderChannel = async ({ serviceId, otherUserId }) => { if (!serviceId || !otherUserId) { throw new Error("Can't create or get a service channel without service id or other members ids") } const existingChannel = await getServiceProviderChannel({ serviceId, otherUserId }) if (existingChannel) { return existingChannel } return createServiceProviderChannel({ serviceId, otherUserId }) } export const createServiceProviderChannel = async ({ serviceId, otherUserId }) => { const [user, service] = await Promise.all([ prisma.user.findOne({ where: { id: otherUserId }, select: { id: true, avatar: { select: { url: true, }, }, }, }), prisma.service.findOne({ where: { id: serviceId }, }), ]) // Create users if they didn't exist yet await Promise.all([ createSendBirdUser({ userId: user.id, nickname: GeneralUtils.getUserName(user), profileUrl: user?.avatar?.url, }), createSendBirdUser({ userId: service.id, nickname: service.type, }), ]) return SendBirdUtils.createChannel({ name: `${serviceId}-${user.id}`, userIds: [serviceId, user.id], }) } export const createWalterSupportChannel = async ({ otherUserId }) => { const user = await prisma.user.findOne({ where: { id: otherUserId }, select: { id: true, avatar: { select: { url: true, }, }, }, }) await Promise.all([ createSendBirdUser({ userId: user.id, nickname: GeneralUtils.getUserName(user), profileUrl: user?.avatar?.url, }), createSendBirdUser({ userId: 'walter', nickname: 'Walter support', }), ]) return SendBirdUtils.createChannel({ name: `${otherUserId}-walter`, userIds: [otherUserId, 'walter'], }) } export async function addManagerToPrivatePropertyChannelIfExist({ managerUserId, projectId, propertyId, }) { if (!projectId || !propertyId) { throw new Error("Can't add managers to channel without projectId or propertyId") } const channel = await getManagerChannel({ propertyId, projectId, isPrivateChannel: true, }) if (channel) { return addUsersToChannel({ userIds: [managerUserId], channelUrl: channel.channel_url, }) } } export function addUsersToPropertyChannelIfExists({ userIds, propertyId }) { if (!userIds || !propertyId) { throw new Error("Can't add users to channel without userIds or propertyId") } return Promise.all( userIds.map(async (userId) => { const projectId = ( await prisma.property .findOne({ where: { id: propertyId } }) .building() .project({ select: { id: true, }, }) )?.id const propertyChannel = await getManagerChannel({ propertyId: propertyId, projectId: projectId, }) if (propertyChannel) { return addUsersToChannel({ userIds: [userId], channelUrl: propertyChannel.channel_url, }) } }) ) } export function addUsersToPropertiesChannelOfProject({ userIds, projectId }) { if (!userIds || !projectId) { throw new Error("Can't add users to channel without userIds or projectId") } return Promise.all( userIds.map(async (userId) => { const propertiesOfUserForProject = await prisma.property.findMany({ where: { AND: [ { building: { project: { id: projectId, }, }, }, { OR: [ { users: { some: { id: userId, }, }, owners: { some: { id: userId, }, }, }, ], }, ], }, }) await Promise.all( propertiesOfUserForProject.map(async (property) => { const propertyChannel = await getManagerChannel({ propertyId: property.id, projectId: projectId, }) if (propertyChannel) { return addUsersToChannel({ userIds: [userId], channelUrl: propertyChannel.channel_url, }) } }) ) }) ) } export const createManagerChannel = async ({ propertyId, projectId, isPrivateChannel = false }) => { let userIdsToAddToChannel = [] if (isPrivateChannel) { const users = await prisma.user.findMany({ where: { managingCompany: { projects: { some: { id: projectId, }, }, }, }, include: { avatar: true, }, }) await Promise.all( users.map((user) => createSendBirdUser({ userId: user.id, nickname: GeneralUtils.getUserName(user), profileUrl: user?.avatar?.url, }) ) ) userIdsToAddToChannel = users.map(({ id }) => id) } else { const [ managingCompaniesAssociatedWithProjectId, usersAssociatedWithProperty, ] = await Promise.all([ prisma.managingCompany.findMany({ where: { projects: { some: { id: projectId, }, }, }, include: { logo: true, }, }), prisma.user.findMany({ where: { OR: [ { property: { id: propertyId, }, }, { properties: { some: { id: projectId, }, }, }, ], }, include: { avatar: true, }, }), ]) // Create the resident users await Promise.all( usersAssociatedWithProperty.map((user) => createSendBirdUser({ userId: user.id, nickname: GeneralUtils.getUserName(user), profileUrl: user?.avatar?.url, }) ) ) // Create the managingCompany users await Promise.all( managingCompaniesAssociatedWithProjectId.map((managingCompany) => createSendBirdUser({ userId: managingCompany.id, nickname: managingCompany.shortName || '', profileUrl: managingCompany?.logo?.url, }) ) ) userIdsToAddToChannel = [ ...usersAssociatedWithProperty, ...managingCompaniesAssociatedWithProjectId, ].map(({ id }) => id) } // Make sure we don't create the channel again... const existingChannel = await getManagerChannel({ propertyId, projectId, isPrivateChannel, }) if (existingChannel) { return existingChannel } return SendBirdUtils.createChannel({ name: `${projectId}-${propertyId}${isPrivateChannel ? '-private' : ''}`, userIds: userIdsToAddToChannel, }) } export const createOrGetManagerChannel = async ({ propertyId, projectId }) => { if (!propertyId && !projectId) { throw new Error("Can't create or get a channel without property id and project id") } const [existingPublicChannel, existingPrivateChannel] = await Promise.all([ getManagerChannel({ projectId, propertyId }), getManagerChannel({ projectId, propertyId, isPrivateChannel: true }), ]) if (!existingPublicChannel && !existingPrivateChannel) { return Promise.all([ createManagerChannel({ propertyId, projectId }), createManagerChannel({ propertyId, projectId, isPrivateChannel: true }), ]) } if (!existingPrivateChannel) { return Promise.all([ Promise.resolve(existingPublicChannel), createManagerChannel({ propertyId, projectId, isPrivateChannel: true }), ]) } return Promise.all([ Promise.resolve(existingPublicChannel), Promise.resolve(existingPrivateChannel), ]) } export async function addUsersToChannel({ userIds, channelUrl }) { if (!channelUrl || !userIds) { throw new Error("Can't add user to channel without userIds or channelUrl") } const users = await prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, firstName: true, lastName: true, avatar: { select: { url: true, }, }, }, }) await Promise.all( users.map((user) => createSendBirdUser({ userId: user.id, nickname: GeneralUtils.getUserName(user), profileUrl: user.avatar?.url, }) ) ) logger.info(`Adding ${userIds.join('-')} to channel: ${channelUrl}`) return SendBirdUtils.inviteUsersToChannel({ channelUrl, userIds }) } export function removeUsersFromChannel({ userIds, channelUrl }) { if (!channelUrl || !userIds) { throw new Error("Can't remove users to channel without userIds or channelUrl") } return SendBirdUtils.removeUsersFromChannel({ channelUrl, userIds }) } export const getChannelsThatNameStartWith = ({ nameStartsWith }) => { if (!nameStartsWith) { throw new Error("Can't get channels that name start with without an empty string") } return getChannels({ all: true, name_startswith: nameStartsWith }) } export const getChannelsThatNameContains = ({ nameContains }) => { if (!nameContains) { throw new Error("Can't get channels that name contains with without an empty string") } return getChannels({ all: true, name_contains: nameContains }) } export const getChannelsForUserId = async ({ all = false, userId, ...rest }: SendBirdUtils.GetChannelsForUserIdInputType): Promise => { const existingUser = await SendBirdUtils.getUser(userId) if (!existingUser) { return [] } let channelsToReturn = [] const helper = async (token = null) => { const { channels, next } = await SendBirdUtils.getChannelsForUserId({ userId, token, ...rest, }) channelsToReturn = [...channelsToReturn, ...channels] if (all && next) { return helper(next) } return channelsToReturn } return helper() } export async function removeUsersFromPropertyChannel({ userIds, propertyId }) { if (!propertyId) { throw new Error("Can't remove users to channel without propertyId") } if (!userIds || userIds.length === 0) { throw new Error("Can't remove users to channel without userIds") } const property = await prisma.property.findOne({ where: { id: propertyId }, select: { id: true, building: { select: { id: true, project: { select: { id: true, }, }, }, }, }, }) if (property?.building?.project?.id) { const channel = await getManagerChannel({ propertyId, projectId: property?.building?.project?.id, }) if (channel) { await removeUsersFromChannel({ channelUrl: channel.channel_url, userIds, }) } } } export function removeUsersFromPropertiesChannelOfProject({ userIds, projectId, }: { userIds: string[] projectId: string }) { if (!userIds || !projectId) { throw new Error("Can't remove users to channel without userIds or projectId") } return Promise.all( userIds.map(async (userId: string) => { try { if (await sendBirdUserExist(userId)) { const { channels: userChannels } = await SendBirdUtils.getChannelsForUserId({ userId, }) await Promise.all( userChannels.map(async (channel) => { if (channel.name.includes('walter')) { return } if (channel.name.includes('-private')) { return } const [channelProjectId, channelPropertyId] = channel.name.split('-') if (channelProjectId !== projectId) { return } const [project, property] = await Promise.all([ prisma.project.findOne({ where: { id: channelProjectId }, select: { id: true } }), prisma.property.findOne({ where: { id: channelPropertyId }, select: { id: true } }), ]) if (project && property) { await removeUsersFromChannel({ userIds: [userId], channelUrl: channel.channel_url }) } }) ) } } catch (error) { // Catch error here because we still want to process all userIds even if someone failed logger.error(error) } }) ) } export async function putGoodResidentsInPropertyChannel({ propertyId, projectId }) { const channel = await getManagerChannel({ propertyId, projectId }) if (!channel) { return } const [residentsInChannel, residentsInProperty] = await Promise.all([ prisma.user.findMany({ where: { role: 'RESIDENT', id: { in: channel.members.map((m) => m.user_id), }, }, select: { id: true, firstName: true, property: { select: { id: true, }, }, properties: { select: { id: true, }, }, }, }), prisma.user.findMany({ where: { OR: [{ property: { id: propertyId } }, { properties: { some: { id: propertyId } } }], }, select: { id: true, }, }), ]) const residentsThatAreNotSupposedToBeInChannel = residentsInChannel.filter((resident) => { const residentStillHasProperty = [resident.property, ...resident.properties].some( (property) => property?.id === propertyId ) if (residentStillHasProperty) { return false } return true }) if (residentsThatAreNotSupposedToBeInChannel.length) { logger.error( `Removing ${residentsThatAreNotSupposedToBeInChannel.length} residents from channel` ) await removeUsersFromPropertyChannel({ propertyId, userIds: residentsThatAreNotSupposedToBeInChannel.map(({ id }) => id), }) } const residentsThatAreNotInChannelButAreSupposedToBe = residentsInProperty.filter( (propertyResident) => !residentsInChannel.some((channelResident) => channelResident.id === propertyResident.id) ) if (residentsThatAreNotInChannelButAreSupposedToBe.length) { logger.error( `Adding ${residentsThatAreNotInChannelButAreSupposedToBe.length} residents from channel` ) await addUsersToChannel({ userIds: residentsThatAreNotInChannelButAreSupposedToBe.map(({ id }) => id), channelUrl: channel.channel_url, }) } } export const createSendBirdUser = async ({ userId, nickname, profileUrl = Constants.DEFAULT_AVATAR, }) => { try { const existingUser = await SendBirdUtils.getUser(userId) if (existingUser) { return existingUser } } catch (err) { logger.error(err.message) try { const user = await SendBirdUtils.createUser({ nickname, profileUrl, userId, }) return user } catch (err) { logger.error(err.message) } } } export const updatechannel = ({ channelUrl, data, }: { channelUrl: string data: ChannelUpdateRequest }) => { return SendBirdUtils.updateChannel({ channelUrl, ...data, }) } export const sendBirdUserExist = async (userId: string) => { try { const existingUser = await SendBirdUtils.getUser(userId) if (existingUser) { return true } } catch (err) { logger.error(err.message) } return false } export const deleteChannel = ({ channelUrl }: { channelUrl: string }) => { return SendBirdUtils.deleteChannel({ channelUrl }) }