Add Chat collections to Hasura/Postgres

This commit is contained in:
Gustavo Trott 2023-03-29 09:55:41 -03:00
parent 7b7d2636ad
commit 7f88d2efa4
17 changed files with 505 additions and 4 deletions

View File

@ -20,7 +20,7 @@ trait SendMessageToBreakoutRoomInternalMsgHdlr {
} yield {
val groupChatMsgFromUser = GroupChatMsgFromUser(sender.id, sender.copy(name = msg.senderName), true, msg.msg)
val gcm = GroupChatApp.toGroupChatMessage(sender.copy(name = msg.senderName), groupChatMsgFromUser)
val gcs = GroupChatApp.addGroupChatMessage(chat, state.groupChats, gcm)
val gcs = GroupChatApp.addGroupChatMessage(liveMeeting.props.meetingProp.intId, chat, state.groupChats, gcm)
val event = buildGroupChatMessageBroadcastEvtMsg(
liveMeeting.props.meetingProp.intId,

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.groupchats
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.ChatDAO
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models.GroupChat
import org.bigbluebutton.core.running.LiveMeeting
@ -33,6 +34,7 @@ trait CreateDefaultPublicGroupChat {
bus.outGW.send(respMsg)
val groupChats = state.groupChats.add(groupChat)
ChatDAO.insert(liveMeeting.props.meetingProp.intId, groupChat)
state.update(groupChats)
}
}

View File

@ -7,6 +7,7 @@ import org.bigbluebutton.core.models.GroupChat
import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.core.apps.PermissionCheck
import org.bigbluebutton.SystemConfiguration
import org.bigbluebutton.core.db.ChatDAO
import org.bigbluebutton.core.models.Users2x
import org.bigbluebutton.core.models.Roles
import org.bigbluebutton.core2.MeetingStatus2x
@ -66,6 +67,7 @@ trait CreateGroupChatReqMsgHdlr extends SystemConfiguration {
sendMessages(msg, gc, liveMeeting, bus)
val groupChats = state.groupChats.add(gc)
ChatDAO.insert(liveMeeting.props.meetingProp.intId, gc)
state.update(groupChats)
}

View File

@ -1,6 +1,7 @@
package org.bigbluebutton.core.apps.groupchats
import org.bigbluebutton.common2.msgs.{ GroupChatAccess, GroupChatMsgFromUser, GroupChatMsgToUser, GroupChatUser }
import org.bigbluebutton.core.db.ChatMessageDAO
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.models._
import org.bigbluebutton.core.running.LiveMeeting
@ -26,8 +27,9 @@ object GroupChatApp {
sender = msg.sender, chatEmphasizedText = msg.chatEmphasizedText, message = msg.message)
}
def addGroupChatMessage(chat: GroupChat, chats: GroupChats,
def addGroupChatMessage(meetingId: String, chat: GroupChat, chats: GroupChats,
msg: GroupChatMessage): GroupChats = {
ChatMessageDAO.insert(meetingId, chat.id, msg)
val c = chat.add(msg)
chats.update(c)
}
@ -71,7 +73,7 @@ object GroupChatApp {
} yield {
val gcm1 = GroupChatApp.toGroupChatMessage(sender, msg)
val gcs1 = GroupChatApp.addGroupChatMessage(chat, state.groupChats, gcm1)
val gcs1 = GroupChatApp.addGroupChatMessage(liveMeeting.props.meetingProp.intId, chat, state.groupChats, gcm1)
state.update(gcs1)
}

View File

@ -49,7 +49,7 @@ trait SendGroupChatMessageMsgHdlr extends HandlerHelpers {
if ((chatIsPrivate && userIsAParticipant) || !chatIsPrivate) {
val gcm = GroupChatApp.toGroupChatMessage(sender, msg.body.msg)
val gcs = GroupChatApp.addGroupChatMessage(chat, state.groupChats, gcm)
val gcs = GroupChatApp.addGroupChatMessage(liveMeeting.props.meetingProp.intId, chat, state.groupChats, gcm)
val event = buildGroupChatMessageBroadcastEvtMsg(
liveMeeting.props.meetingProp.intId,

View File

@ -0,0 +1,56 @@
package org.bigbluebutton.core.db
import slick.jdbc.PostgresProfile.api._
import org.bigbluebutton.core.models.GroupChat
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{ Failure, Success }
case class ChatDbModel(
chatId: String,
meetingId: String,
access: String,
createdBy: String,
// participants: List[String],
// users: List[String]
)
class ChatDbTableDef(tag: Tag) extends Table[ChatDbModel](tag, None, "chat") {
val chatId = column[String]("chatId", O.PrimaryKey)
val meetingId = column[String]("meetingId", O.PrimaryKey)
val access = column[String]("access")
val createdBy = column[String]("createdBy")
// val participants = column[List[String]]("participants")
// val users = column[List[String]]("users")
// val meeting = foreignKey("chat_meeting_fk", (meetingId), ChatDbTableDef.meetings)(_.meetingId, onDelete = ForeignKeyAction.Cascade)
override def * = (chatId, meetingId, access, createdBy) <> (ChatDbModel.tupled, ChatDbModel.unapply)
}
object ChatDAO {
def insert(meetingId: String, groupChat: GroupChat) = {
DatabaseConnection.db.run(
TableQuery[ChatDbTableDef].insertOrUpdate(
ChatDbModel(
chatId = groupChat.id,
meetingId = meetingId,
access = groupChat.access,
createdBy = groupChat.createdBy.id,
// participants = groupChat.users.map(u => u.id).toList,
// users = groupChat.users.map(u => u.id).toList
)
)
).onComplete {
case Success(rowsAffected) => {
println(s"$rowsAffected row(s) inserted on Chat table!")
for {
user <- groupChat.users
} yield {
ChatUserDAO.insert(meetingId, groupChat.id, user)
}
}
case Failure(e) => println(s"Error inserting Chat: $e")
}
}
}

View File

@ -0,0 +1,61 @@
package org.bigbluebutton.core.db
import slick.jdbc.PostgresProfile.api._
import org.bigbluebutton.core.models.GroupChatMessage
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{ Failure, Success }
case class ChatMessageDbModel(
messageId: String,
chatId: String,
meetingId: String,
correlationId: String,
createdTime: Long,
chatEmphasizedText: Boolean,
message: String,
senderId: String,
senderName: String,
senderRole: String
)
class ChatMessageDbTableDef(tag: Tag) extends Table[ChatMessageDbModel](tag, None, "chat_message") {
val messageId = column[String]("messageId", O.PrimaryKey)
val chatId = column[String]("chatId")
val meetingId = column[String]("meetingId")
val correlationId = column[String]("correlationId")
val createdTime = column[Long]("createdTime")
val chatEmphasizedText = column[Boolean]("chatEmphasizedText")
val message = column[String]("message")
val senderId = column[String]("senderId")
val senderName = column[String]("senderName")
val senderRole = column[String]("senderRole")
// val chat = foreignKey("chat_message_chat_fk", (chatId, meetingId), ChatTable.chats)(c => (c.chatId, c.meetingId), onDelete = ForeignKeyAction.Cascade)
// val sender = foreignKey("chat_message_sender_fk", senderId, UserTable.users)(_.userId, onDelete = ForeignKeyAction.SetNull)
override def * = (messageId, chatId, meetingId, correlationId, createdTime, chatEmphasizedText, message, senderId, senderName, senderRole) <> (ChatMessageDbModel.tupled, ChatMessageDbModel.unapply)
}
object ChatMessageDAO {
def insert(meetingId: String, chatId: String, groupChatMessage: GroupChatMessage) = {
DatabaseConnection.db.run(
TableQuery[ChatMessageDbTableDef].insertOrUpdate(
ChatMessageDbModel(
messageId = groupChatMessage.id,
chatId = chatId,
meetingId = meetingId,
correlationId = groupChatMessage.correlationId,
createdTime = groupChatMessage.timestamp,
chatEmphasizedText = groupChatMessage.chatEmphasizedText,
message = groupChatMessage.message,
senderId = groupChatMessage.sender.id,
senderName = groupChatMessage.sender.name,
senderRole = groupChatMessage.sender.role,
)
)
).onComplete {
case Success(rowsAffected) => println(s"$rowsAffected row(s) inserted on ChatMessage table!")
case Failure(e) => println(s"Error inserting ChatMessage: $e")
}
}
}

View File

@ -0,0 +1,49 @@
package org.bigbluebutton.core.db
import slick.jdbc.PostgresProfile.api._
import org.bigbluebutton.common2.msgs.GroupChatUser
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{ Failure, Success }
case class ChatUserDbModel(
chatId: String,
meetingId: String,
userId: String,
lastSeenAt: Long,
userName: String,
userRole: String
)
class ChatUserDbTableDef(tag: Tag) extends Table[ChatUserDbModel](tag, None, "chat_user") {
val chatId = column[String]("chatId", O.PrimaryKey)
val meetingId = column[String]("meetingId", O.PrimaryKey)
val userId = column[String]("userId", O.PrimaryKey)
val lastSeenAt = column[Long]("lastSeenAt")
val userName = column[String]("userName")
val userRole = column[String]("userRole")
// val chat = foreignKey("chat_message_chat_fk", (chatId, meetingId), ChatTable.chats)(c => (c.chatId, c.meetingId), onDelete = ForeignKeyAction.Cascade)
// val sender = foreignKey("chat_message_sender_fk", senderId, UserTable.users)(_.userId, onDelete = ForeignKeyAction.SetNull)
override def * = (chatId, meetingId, userId, lastSeenAt, userName, userRole) <> (ChatUserDbModel.tupled, ChatUserDbModel.unapply)
}
object ChatUserDAO {
def insert(meetingId: String, chatId: String, groupChatUser: GroupChatUser) = {
DatabaseConnection.db.run(
TableQuery[ChatUserDbTableDef].insertOrUpdate(
ChatUserDbModel(
userId = groupChatUser.id,
chatId = chatId,
meetingId = meetingId,
lastSeenAt = 0,
userName = groupChatUser.name,
userRole = groupChatUser.role
)
)
).onComplete {
case Success(rowsAffected) => println(s"$rowsAffected row(s) inserted on ChatUser table!")
case Failure(e) => println(s"Error inserting ChatUser: $e")
}
}
}

View File

@ -10,10 +10,14 @@ import TotalOfModerators from './TotalOfModerators';
import TotalOfViewers from './TotalOfViewers';
import TotalOfUsersTalking from './TotalOfUsersTalking';
import TotalOfUniqueNames from './TotalOfUniqueNames';
import ChatMessages from "./ChatMessages";
import ChatsInfo from "./ChatsInfo";
import ChatPublicMessages from "./ChatPublicMessages";
function App() {
const [sessionToken, setSessionToken] = useState(null);
const [userId, setUserId] = useState(null);
const [joining, setJoining] = useState(false);
const [graphqlClient, setGraphqlClient] = useState(null);
const [enterApiResponse, setEnterApiResponse] = useState('');
@ -33,6 +37,9 @@ function App() {
.then((json) => {
console.log(json.response);
setEnterApiResponse(json.response.returncode);
if(json?.response?.internalUserID) {
setUserId(json.response.internalUserID);
}
});
}
@ -80,8 +87,17 @@ function App() {
}}
>
<ApolloProvider client={graphqlClient}>
User Id: {userId}
<MeetingInfo />
<br />
<UserList />
<br />
<ChatsInfo />
<br />
<ChatMessages />
<br />
<ChatPublicMessages />
<br />
<TotalOfUsers />
<TotalOfModerators />
<TotalOfViewers />

View File

@ -0,0 +1,52 @@
import {useSubscription, gql, useQuery} from '@apollo/client';
import React, { useState } from "react";
export default function ChatMessages() {
const { loading, error, data } = useSubscription(
gql`subscription {
chat_message_private(order_by: {createdTime: asc}) {
chatEmphasizedText
chatId
correlationId
createdTime
createdTimeAsDate
meetingId
message
messageId
senderId
senderName
senderRole
}
}`
);
return !loading && !error &&
(<table border="1">
<thead>
<tr>
<th colSpan="4">Private Chat Messages</th>
</tr>
<tr>
<th>ChatId</th>
<th>Sender</th>
<th>Message</th>
<th>Sent At</th>
</tr>
</thead>
<tbody>
{data.chat_message_private.map((curr) => {
console.log('message', curr);
return (
<tr key={curr.messageId}>
{/*<td>{user.userId}</td>*/}
<td>{curr.chatId}</td>
<td>{curr.senderName}</td>
<td>{curr.message}</td>
<td>{curr.createdTimeAsDate} ({curr.createdTime})</td>
</tr>
);
})}
</tbody>
</table>);
}

View File

@ -0,0 +1,51 @@
import {useSubscription, gql, useQuery} from '@apollo/client';
import React, { useState } from "react";
export default function ChatPublicMessages() {
const { loading, error, data } = useSubscription(
gql`subscription {
chat_message_public(order_by: {createdTime: asc}) {
chatId
chatEmphasizedText
correlationId
createdTime
createdTimeAsDate
meetingId
message
messageId
senderId
senderName
senderRole
}
}`
);
return !loading && !error &&
(<table border="1">
<thead>
<tr>
<th colSpan="4">Public Chat Messages</th>
</tr>
<tr>
<th>ChatId</th>
<th>Sender</th>
<th>Message</th>
<th>Sent At</th>
</tr>
</thead>
<tbody>
{data.chat_message_public.map((curr) => {
console.log('message', curr);
return (
<tr key={curr.messageId}>
<td>{curr.chatId}</td>
<td>{curr.senderName}</td>
<td>{curr.message}</td>
<td>{curr.createdTimeAsDate} ({curr.createdTime})</td>
</tr>
);
})}
</tbody>
</table>);
}

View File

@ -0,0 +1,48 @@
import {useSubscription, gql, useQuery} from '@apollo/client';
import React, { useState } from "react";
export default function ChatsInfo() {
const { loading, error, data } = useSubscription(
gql`subscription {
chat {
chatId
meetingId
participantsId
participantsName
totalMessages
totalUnread
}
}`
);
return !loading && !error &&
(<table border="1">
<thead>
<tr>
<th colSpan="5">Chats available</th>
</tr>
<tr>
<th>Id</th>
<th>Meeting</th>
<th>Participants</th>
<th>Total Mgs</th>
<th>Unread</th>
</tr>
</thead>
<tbody>
{data.chat.map((curr) => {
console.log('chat', curr);
return (
<tr key={curr.chatId}>
<td>{curr.chatId}</td>
<td>{curr.meetingId}</td>
<td>{curr.participantsName}</td>
<td>{curr.totalMessages}</td>
<td>{curr.totalUnread}</td>
</tr>
);
})}
</tbody>
</table>);
}

View File

@ -223,3 +223,83 @@ SELECT
"user_breakoutRoom" .*
FROM "user_breakoutRoom"
JOIN "user" u ON u."userId" = "user_breakoutRoom"."userId";
-- ===================== CHAT TABLES
DROP VIEW IF EXISTS "v_chat";
DROP VIEW IF EXISTS "v_chat_message_public";
DROP VIEW IF EXISTS "v_chat_message_private";
DROP TABLE IF EXISTS "chat_user";
DROP TABLE IF EXISTS "chat_message";
DROP TABLE IF EXISTS "chat";
CREATE TABLE "chat" (
"chatId" varchar(100),
"meetingId" varchar(100) REFERENCES "meeting"("meetingId") ON DELETE CASCADE,
"access" varchar(20),
"createdBy" varchar(25),
CONSTRAINT "chat_pkey" PRIMARY KEY ("chatId","meetingId")
);
CREATE INDEX "chat_meetingId" ON "chat"("meetingId");
CREATE TABLE "chat_user" (
"chatId" varchar(100),
"meetingId" varchar(100),
"userId" varchar(100),
"userName" varchar(255),
"userRole" varchar(20),
"lastSeenAt" bigint,
CONSTRAINT "chat_user_pkey" PRIMARY KEY ("chatId","meetingId","userId"),
CONSTRAINT chat_fk FOREIGN KEY ("chatId", "meetingId") REFERENCES "chat"("chatId", "meetingId") ON DELETE CASCADE
);
CREATE INDEX "chat_user_chatId" ON "chat_user"("chatId","meetingId");
CREATE TABLE "chat_message" (
"messageId" varchar(100) PRIMARY KEY,
"chatId" varchar(100),
"meetingId" varchar(100),
"correlationId" varchar(100),
"createdTime" bigint,
"chatEmphasizedText" boolean,
"message" TEXT,
"senderId" varchar(100),
"senderName" varchar(255),
"senderRole" varchar(20),
CONSTRAINT chat_fk FOREIGN KEY ("chatId", "meetingId") REFERENCES "chat"("chatId", "meetingId") ON DELETE CASCADE
);
CREATE INDEX "chat_message_chatId" ON "chat_message"("chatId","meetingId");
CREATE OR REPLACE VIEW "v_chat" AS
SELECT cu."userId",
chat."meetingId",
chat."chatId",
array_remove(array_agg(DISTINCT chat_with."userId"),NULL) "participantsId",
string_agg(DISTINCT chat_with."userName" ,', ') AS "participantsName",
count(DISTINCT cm."messageId") "totalMessages",
sum(CASE WHEN cm."createdTime" > cu."lastSeenAt" THEN 1 ELSE 0 end) "totalUnread"
FROM "user"
LEFT JOIN "chat_user" cu ON cu."meetingId" = "user"."meetingId" AND cu."userId" = "user"."userId"
JOIN "chat" ON cu."meetingId" = chat."meetingId" AND (cu."chatId" = chat."chatId" OR chat."chatId" = 'MAIN-PUBLIC-GROUP-CHAT')
LEFT JOIN "chat_user" chat_with ON chat_with."meetingId" = chat."meetingId" AND chat_with."chatId" = chat."chatId" AND chat_with."userId" != cu."userId"
LEFT JOIN chat_message cm ON cm."meetingId" = chat."meetingId" AND cm."chatId" = chat."chatId"
GROUP BY cu."userId", chat."meetingId", chat."chatId";
CREATE OR REPLACE VIEW "v_chat_message_public" AS
SELECT cm.*, to_timestamp("createdTime" / 1000) AS "createdTimeAsDate"
FROM chat_message cm
WHERE cm."chatId" = 'MAIN-PUBLIC-GROUP-CHAT';
CREATE OR REPLACE VIEW "v_chat_message_private" AS
SELECT cu."userId", cm.*, to_timestamp("createdTime" / 1000) AS "createdTimeAsDate"
FROM chat_message cm
JOIN chat_user cu ON cu."meetingId" = cm."meetingId" AND cu."chatId" = cm."chatId";

View File

@ -0,0 +1,24 @@
table:
name: v_chat
schema: public
configuration:
column_config: {}
custom_column_names: {}
custom_name: chat
custom_root_fields: {}
select_permissions:
- role: bbb_client
permission:
columns:
- chatId
- meetingId
- participantsId
- participantsName
- totalMessages
- totalUnread
filter:
_and:
- meetingId:
_eq: X-Hasura-MeetingId
- userId:
_eq: X-Hasura-UserId

View File

@ -0,0 +1,29 @@
table:
name: v_chat_message_private
schema: public
configuration:
column_config: {}
custom_column_names: {}
custom_name: chat_message_private
custom_root_fields: {}
select_permissions:
- role: bbb_client
permission:
columns:
- chatEmphasizedText
- chatId
- correlationId
- createdTime
- createdTimeAsDate
- meetingId
- message
- messageId
- senderId
- senderName
- senderRole
filter:
_and:
- meetingId:
_eq: X-Hasura-MeetingId
- userId:
_eq: X-Hasura-UserId

View File

@ -0,0 +1,26 @@
table:
name: v_chat_message_public
schema: public
configuration:
column_config: {}
custom_column_names: {}
custom_name: chat_message_public
custom_root_fields: {}
select_permissions:
- role: bbb_client
permission:
columns:
- createdTime
- chatEmphasizedText
- chatId
- correlationId
- meetingId
- messageId
- senderId
- senderName
- senderRole
- message
- createdTimeAsDate
filter:
meetingId:
_eq: X-Hasura-MeetingId

View File

@ -2,6 +2,9 @@
- "!include public_user.yaml"
- "!include public_user_camera.yaml"
- "!include public_user_voice.yaml"
- "!include public_v_chat.yaml"
- "!include public_v_chat_message_private.yaml"
- "!include public_v_chat_message_public.yaml"
- "!include public_v_user_breakoutRoom.yaml"
- "!include public_v_user_camera.yaml"
- "!include public_v_user_voice.yaml"