From 70a9f9d7d4d0879457e7c1228afb7739e60b3197 Mon Sep 17 00:00:00 2001
From: Dima Rosmait <dima.rosmait@holi.team>
Date: Wed, 8 Nov 2023 09:50:14 +0000
Subject: [PATCH] HOLI-6369 - Update space handler

---
 src/handlers/space-updated-handler.ts | 35 ++++++++++++
 src/helpers/createSpace.ts            | 10 ++--
 src/helpers/rooms.ts                  | 76 ++++++++++++++++++++++-----
 src/index.ts                          | 13 +++--
 src/types.ts                          | 14 +++++
 5 files changed, 125 insertions(+), 23 deletions(-)
 create mode 100644 src/handlers/space-updated-handler.ts

diff --git a/src/handlers/space-updated-handler.ts b/src/handlers/space-updated-handler.ts
new file mode 100644
index 0000000..3fca20f
--- /dev/null
+++ b/src/handlers/space-updated-handler.ts
@@ -0,0 +1,35 @@
+import { logPhase } from '../logger'
+import { SpaceUpdatedDataPayload } from '../types'
+import { startChatClient, stopChatClient } from '../helpers/_chatClient'
+import {
+  getHoliSpaceRoomsSharedData,
+  hasRoomUpdatedValues,
+  isSpaceGeneralRoomSharedData,
+  setRoomSharedData,
+} from '../helpers/rooms'
+
+// Handles the SpaceCreated event from Google Cloud Pub/Sub.
+export const spaceUpdatedHandler = async (messageId: string, payload: SpaceUpdatedDataPayload): Promise<void> => {
+  await startChatClient()
+
+  try {
+    await logPhase(
+      messageId,
+      'spaceUpdatedHandler'
+    )(async () => {
+      const { space: spacePayload } = payload
+      const [roomId, sharedData] = Object.entries(await getHoliSpaceRoomsSharedData(spacePayload.id, 'general')).find(
+        ([_, data]) => isSpaceGeneralRoomSharedData(data)
+      )
+
+      if (!isSpaceGeneralRoomSharedData(sharedData)) return
+      if (!hasRoomUpdatedValues(sharedData, spacePayload)) return
+
+      await setRoomSharedData(roomId, sharedData.parentSpaceRoomId, spacePayload)
+    })
+  } catch (error) {
+    throw new Error(`Failed to create space ${error}`)
+  } finally {
+    await stopChatClient()
+  }
+}
diff --git a/src/helpers/createSpace.ts b/src/helpers/createSpace.ts
index 856b339..d09987f 100644
--- a/src/helpers/createSpace.ts
+++ b/src/helpers/createSpace.ts
@@ -4,6 +4,7 @@ import { chatClient } from './_chatClient'
 import { CHAT_SERVER_URL } from '../constants'
 import { SpacePayload } from '../types'
 import { ChatRoomEvent } from './type'
+import { setRoomSharedData } from './rooms'
 
 export const createSpaceContainer = async ({ id, name }: SpacePayload, inviteUsers: string[]): Promise<string> => {
   const { room_id: parentSpaceRoomId } = await chatClient.createRoom({
@@ -38,15 +39,10 @@ export const createSpaceGeneralRoom = async (inviteUsers: string[]): Promise<str
 export const addGeneralRoomToSpaceContainer = async (
   parentSpaceRoomId: string,
   generalSpaceRoomId: string,
-  { id, name, avatarDefaultColor }: SpacePayload
+  spacePayload: SpacePayload
 ): Promise<void> => {
   // 1. add custom data into general room
-  await chatClient.sendStateEvent(generalSpaceRoomId, ChatRoomEvent.SharedData, {
-    holiSpaceId: id,
-    spaceName: name,
-    avatarDefaultColor,
-    parentSpaceRoomId,
-  })
+  await setRoomSharedData(generalSpaceRoomId, parentSpaceRoomId, spacePayload)
 
   // 2. add general room into space room
   await chatClient.sendStateEvent(
diff --git a/src/helpers/rooms.ts b/src/helpers/rooms.ts
index 1fc72d4..60e7aa7 100644
--- a/src/helpers/rooms.ts
+++ b/src/helpers/rooms.ts
@@ -1,4 +1,6 @@
 import { CHAT_ADMIN_ACCESS_TOKEN, CHAT_SERVER_NAME, CHAT_SERVER_URL } from '../constants'
+import { SpacePayload } from '../types'
+import { chatClient } from './_chatClient'
 import { ChatRoomEvent } from './type'
 
 export const getMatrixUserId = (holiIdentity: string) => `@${holiIdentity}:${CHAT_SERVER_NAME}`
@@ -52,31 +54,69 @@ interface RoomSharedDataEvent {
 const isRoomSharedDataEvent = (event: RoomSharedDataEvent | StateEvent): event is RoomSharedDataEvent =>
   event.type === ChatRoomEvent.SharedData
 
-interface SpaceSharedData {
+export const isSpaceGeneralRoomSharedData = (data: SpaceSharedData): data is SpaceGeneralRoomSharedData => {
+  return 'parentSpaceRoomId' in data && 'spaceName' in data
+}
+
+interface SpaceRoomSharedData {
   holiSpaceId: string
+}
+
+interface SpaceGeneralRoomSharedData extends SpaceRoomSharedData {
   parentSpaceRoomId: string
+  spaceName: string
+  avatar: string
+  avatarDefaultColor: string
 }
 
-/**
- * Delete room helpers
- */
-export const getRoomIdsForHoliSpace = async (holiSpaceId: string, searchTerm?: string) => {
-  const rooms: { room_id: string }[] = await getRooms(searchTerm)
+export type SpaceSharedData = SpaceRoomSharedData | SpaceGeneralRoomSharedData
+
+export const getHoliSpaceRoomsSharedData = async (
+  holiSpaceId: string,
+  searchTerm?: string
+): Promise<Record<string, SpaceSharedData>> => {
+  const rooms: { room_id: string; name: string }[] = await getRooms(searchTerm)
 
-  return rooms.reduce<Promise<string[]>>(async (accPromise, room) => {
+  return rooms.reduce<Promise<Record<string, SpaceSharedData>>>(async (accPromise, room) => {
     let acc = await accPromise
     const { room_id } = room
     const state = await getRoomSharedData(room_id)
     const { content } = state.find(isRoomSharedDataEvent) ?? {}
 
-    if (content && content.holiSpaceId === holiSpaceId) {
-      acc = Array.from(new Set<string>([...acc, room_id, content.parentSpaceRoomId].filter((id) => !!id)))
-    }
+    return content && content.holiSpaceId === holiSpaceId
+      ? {
+          ...acc,
+          [room_id]: content,
+        }
+      : acc
+  }, Promise.resolve({}))
+}
+
+export const getRoomIdsForHoliSpace = async (holiSpaceId: string, searchTerm?: string): Promise<string[]> => {
+  const holiSpaceRoomsRecord = await getHoliSpaceRoomsSharedData(holiSpaceId, searchTerm)
+  return Object.keys(holiSpaceRoomsRecord)
+}
 
-    return acc
-  }, Promise.resolve([]))
+/**
+ * Create room helpers
+ */
+export const setRoomSharedData = async (
+  roomId: string,
+  parentSpaceRoomId: string,
+  { id, name, avatar, avatarDefaultColor }: SpacePayload
+) => {
+  await chatClient.sendStateEvent(roomId, ChatRoomEvent.SharedData, {
+    holiSpaceId: id,
+    spaceName: name,
+    avatar,
+    avatarDefaultColor,
+    parentSpaceRoomId,
+  })
 }
 
+/**
+ * Delete room helpers
+ */
 interface DeleteStatus {
   status: 'active' | 'shutting_down' | 'purging' | 'complete' | 'failed'
 }
@@ -120,3 +160,15 @@ export const initRoomDeletion = async (roomId: string) => {
 
   return delete_id
 }
+
+/**
+ * Update room helpers
+ */
+export const hasRoomUpdatedValues = (
+  oldSharedData: SpaceGeneralRoomSharedData,
+  spacePayload: SpacePayload
+): boolean => {
+  const { spaceName, avatar, avatarDefaultColor } = oldSharedData
+  const { name: newSpaceName, avatar: newAvatar, avatarDefaultColor: newAvatarDefaultColor } = spacePayload
+  return spaceName !== newSpaceName || avatar !== newAvatar || avatarDefaultColor !== newAvatarDefaultColor
+}
diff --git a/src/index.ts b/src/index.ts
index 471c9c6..e20a219 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,7 @@
 // import fetch from 'node-fetch'
 import { spaceCreatedHandler } from './handlers/space-created-handler'
 import { spaceDeletedHandler } from './handlers/space-deleted-handler'
+import { spaceUpdatedHandler } from './handlers/space-updated-handler'
 import { spaceUserAddedHandler } from './handlers/space-user-added-handler'
 import { spaceUserLeftHandler } from './handlers/space-user-left-handler'
 import { spaceUserRemovedHandler } from './handlers/space-user-removed-handler'
@@ -39,6 +40,14 @@ export const receiveEvent = async (request, response): Promise<void> => {
         await logPhase(messageId, 'spaceCreatedHandler')(() => spaceCreatedHandler(messageId, event))
         response.sendStatus(201)
         break
+      case EventType.SPACE_UPDATED:
+        await logPhase(messageId, 'spaceUpdatedHandler')(() => spaceUpdatedHandler(messageId, event))
+        response.sendStatus(201)
+        break
+      case EventType.SPACE_DELETED:
+        await logPhase(messageId, 'spaceDeletedHandler')(() => spaceDeletedHandler(messageId, event))
+        response.sendStatus(204)
+        break
       case EventType.SPACE_USER_ADDED:
         await logPhase(messageId, 'spaceUserAddedHandler')(() => spaceUserAddedHandler(messageId, event))
         response.sendStatus(200)
@@ -47,10 +56,6 @@ export const receiveEvent = async (request, response): Promise<void> => {
         await logPhase(messageId, 'userNameUpdatedHandler')(() => userNameUpdatedHandler(messageId, event))
         response.sendStatus(200)
         break
-      case EventType.SPACE_DELETED:
-        await logPhase(messageId, 'spaceDeletedHandler')(() => spaceDeletedHandler(messageId, event))
-        response.sendStatus(204)
-        break
       case EventType.USER_DELETED:
         await logPhase(messageId, 'userDeletedHandler')(() => userDeletedHandler(messageId, event))
         response.sendStatus(204)
diff --git a/src/types.ts b/src/types.ts
index 18e8706..b1538a6 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -6,6 +6,7 @@
  */
 export enum EventType {
   SPACE_CREATED = 'SpaceCreated',
+  SPACE_UPDATED = 'SpaceUpdated',
   SPACE_USER_ADDED = 'SpaceUserAdded',
   USER_NAME_UPDATED = 'UserNameUpdated',
   SPACE_DELETED = 'SpaceDeleted',
@@ -42,6 +43,7 @@ export type SpacePayload = {
   id: string
   name: string
   slug: string
+  avatar?: string
   avatarDefaultColor: string
 }
 
@@ -57,6 +59,18 @@ export type SpaceCreatedDataPayload = {
   space: SpacePayload
 }
 
+/**
+ * Payload for data created when a space is updated
+ *
+ * @typedef {Object} SpaceCreatedDataPayload
+ * @property {UserPayload} creator - The user who created the space
+ * @property {SpacePayload} space - The updated space
+ */
+export type SpaceUpdatedDataPayload = {
+  creator: UserPayload
+  space: SpacePayload
+}
+
 /**
  * Payload for data created when a user is added to a space
  *
-- 
GitLab