Skip to content

Commit e196211

Browse files
authored
Merge pull request #2758 from simonredfern/develop
Chat room search to enable non duplicate DM room creation
2 parents e52171f + 3345194 commit e196211

File tree

4 files changed

+200
-18
lines changed

4 files changed

+200
-18
lines changed

obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala

Lines changed: 126 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12870,7 +12870,7 @@ trait APIMethods600 {
1287012870
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot add creator as participant", 400)
1287112871
}
1287212872
} yield {
12873-
(JSONFactory600.createChatRoomJson(room), HttpCode.`201`(callContext))
12873+
(JSONFactory600.createChatRoomJson(room, participantCount = computeParticipantCount(room.chatRoomId)), HttpCode.`201`(callContext))
1287412874
}
1287512875
}
1287612876
}
@@ -12941,7 +12941,7 @@ trait APIMethods600 {
1294112941
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot add creator as participant", 400)
1294212942
}
1294312943
} yield {
12944-
(JSONFactory600.createChatRoomJson(room), HttpCode.`201`(callContext))
12944+
(JSONFactory600.createChatRoomJson(room, participantCount = computeParticipantCount(room.chatRoomId)), HttpCode.`201`(callContext))
1294512945
}
1294612946
}
1294712947
}
@@ -12999,8 +12999,11 @@ trait APIMethods600 {
1299912999
unreadCounts <- Future {
1300013000
computeUnreadCounts(rooms, u.userId)
1300113001
}
13002+
participantCounts <- Future {
13003+
computeParticipantCounts(rooms)
13004+
}
1300213005
} yield {
13003-
(JSONFactory600.createChatRoomsJson(rooms, unreadCounts), HttpCode.`200`(callContext))
13006+
(JSONFactory600.createChatRoomsJson(rooms, unreadCounts, participantCounts), HttpCode.`200`(callContext))
1300413007
}
1300513008
}
1300613009
}
@@ -13058,8 +13061,11 @@ trait APIMethods600 {
1305813061
unreadCounts <- Future {
1305913062
computeUnreadCounts(rooms, u.userId)
1306013063
}
13064+
participantCounts <- Future {
13065+
computeParticipantCounts(rooms)
13066+
}
1306113067
} yield {
13062-
(JSONFactory600.createChatRoomsJson(rooms, unreadCounts), HttpCode.`200`(callContext))
13068+
(JSONFactory600.createChatRoomsJson(rooms, unreadCounts, participantCounts), HttpCode.`200`(callContext))
1306313069
}
1306413070
}
1306513071
}
@@ -13122,7 +13128,7 @@ trait APIMethods600 {
1312213128
x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
1312313129
}
1312413130
} yield {
13125-
(JSONFactory600.createChatRoomJson(room), HttpCode.`200`(callContext))
13131+
(JSONFactory600.createChatRoomJson(room, participantCount = computeParticipantCount(room.chatRoomId)), HttpCode.`200`(callContext))
1312613132
}
1312713133
}
1312813134
}
@@ -13185,7 +13191,7 @@ trait APIMethods600 {
1318513191
x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
1318613192
}
1318713193
} yield {
13188-
(JSONFactory600.createChatRoomJson(room), HttpCode.`200`(callContext))
13194+
(JSONFactory600.createChatRoomJson(room, participantCount = computeParticipantCount(room.chatRoomId)), HttpCode.`200`(callContext))
1318913195
}
1319013196
}
1319113197
}
@@ -13258,7 +13264,7 @@ trait APIMethods600 {
1325813264
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update chat room", 400)
1325913265
}
1326013266
} yield {
13261-
(JSONFactory600.createChatRoomJson(updatedRoom), HttpCode.`200`(callContext))
13267+
(JSONFactory600.createChatRoomJson(updatedRoom, participantCount = computeParticipantCount(updatedRoom.chatRoomId)), HttpCode.`200`(callContext))
1326213268
}
1326313269
}
1326413270
}
@@ -13331,7 +13337,7 @@ trait APIMethods600 {
1333113337
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update chat room", 400)
1333213338
}
1333313339
} yield {
13334-
(JSONFactory600.createChatRoomJson(updatedRoom), HttpCode.`200`(callContext))
13340+
(JSONFactory600.createChatRoomJson(updatedRoom, participantCount = computeParticipantCount(updatedRoom.chatRoomId)), HttpCode.`200`(callContext))
1333513341
}
1333613342
}
1333713343
}
@@ -13490,7 +13496,7 @@ trait APIMethods600 {
1349013496
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot archive chat room", 400)
1349113497
}
1349213498
} yield {
13493-
(JSONFactory600.createChatRoomJson(archivedRoom), HttpCode.`200`(callContext))
13499+
(JSONFactory600.createChatRoomJson(archivedRoom, participantCount = computeParticipantCount(archivedRoom.chatRoomId)), HttpCode.`200`(callContext))
1349413500
}
1349513501
}
1349613502
}
@@ -13555,7 +13561,7 @@ trait APIMethods600 {
1355513561
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot archive chat room", 400)
1355613562
}
1355713563
} yield {
13558-
(JSONFactory600.createChatRoomJson(archivedRoom), HttpCode.`200`(callContext))
13564+
(JSONFactory600.createChatRoomJson(archivedRoom, participantCount = computeParticipantCount(archivedRoom.chatRoomId)), HttpCode.`200`(callContext))
1355913565
}
1356013566
}
1356113567
}
@@ -13624,7 +13630,7 @@ trait APIMethods600 {
1362413630
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update chat room", 400)
1362513631
}
1362613632
} yield {
13627-
(JSONFactory600.createChatRoomJson(updatedRoom), HttpCode.`200`(callContext))
13633+
(JSONFactory600.createChatRoomJson(updatedRoom, participantCount = computeParticipantCount(updatedRoom.chatRoomId)), HttpCode.`200`(callContext))
1362813634
}
1362913635
}
1363013636
}
@@ -13693,7 +13699,7 @@ trait APIMethods600 {
1369313699
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update chat room", 400)
1369413700
}
1369513701
} yield {
13696-
(JSONFactory600.createChatRoomJson(updatedRoom), HttpCode.`200`(callContext))
13702+
(JSONFactory600.createChatRoomJson(updatedRoom, participantCount = computeParticipantCount(updatedRoom.chatRoomId)), HttpCode.`200`(callContext))
1369713703
}
1369813704
}
1369913705
}
@@ -16343,10 +16349,99 @@ trait APIMethods600 {
1634316349
}
1634416350
}
1634516351
}
16352+
participantCounts <- Future {
16353+
computeParticipantCounts(roomsAndCounts.map(_._1))
16354+
}
1634616355
} yield {
1634716356
val rooms = roomsAndCounts.map(_._1)
1634816357
val unreadCounts = roomsAndCounts.map { case (room, count) => room.chatRoomId -> count }.toMap
16349-
(JSONFactory600.createChatRoomsJson(rooms, unreadCounts), HttpCode.`200`(callContext))
16358+
(JSONFactory600.createChatRoomsJson(rooms, unreadCounts, participantCounts), HttpCode.`200`(callContext))
16359+
}
16360+
}
16361+
}
16362+
16363+
// 25b. searchChatRooms
16364+
staticResourceDocs += ResourceDoc(
16365+
searchChatRooms,
16366+
implementedInApiVersion,
16367+
nameOf(searchChatRooms),
16368+
"POST",
16369+
"/chat-rooms/search",
16370+
"Search Chat Rooms",
16371+
s"""Search chat rooms the current user is a participant of, filtered by the supplied criteria.
16372+
|
16373+
|Currently supports filtering by participant set:
16374+
|
16375+
|- `with_user_ids` (array of user_id strings, required): only return rooms where the current user
16376+
| AND every listed user_id are participants. Pass an empty list to match all of the current user's rooms.
16377+
|- `exact_participants` (boolean, optional, default `false`): if `true`, the room's participant set
16378+
| must equal exactly `{current user} ∪ with_user_ids` with no extras. Open rooms are excluded
16379+
| from exact-participant searches because their participant set is implicitly "everyone".
16380+
|
16381+
|Primary use case: a client looking up an existing 1-on-1 direct-message room before creating one,
16382+
|by calling with `with_user_ids: [<other user_id>]` and `exact_participants: true`.
16383+
|
16384+
|The response shape is the same as `Get My Chat Rooms`.
16385+
|
16386+
|Authentication is Required
16387+
|
16388+
|""".stripMargin,
16389+
ChatRoomSearchRequestJsonV600(
16390+
with_user_ids = List("user-id-123"),
16391+
exact_participants = Some(true)
16392+
),
16393+
ChatRoomsJsonV600(chat_rooms = List(ChatRoomJsonV600(
16394+
chat_room_id = "chat-room-id-123",
16395+
bank_id = "",
16396+
name = "DM with robert.x.0.gh",
16397+
description = "",
16398+
joining_key = "abc123key",
16399+
created_by = "user-id-456",
16400+
created_by_username = "alice",
16401+
created_by_provider = "https://github.com",
16402+
is_open_room = false,
16403+
is_archived = false,
16404+
last_message_at = Some(new java.util.Date()),
16405+
last_message_preview = Some("Hello!"),
16406+
last_message_sender = Some("alice"),
16407+
unread_count = Some(0),
16408+
created_at = new java.util.Date(),
16409+
updated_at = new java.util.Date()
16410+
))),
16411+
List(
16412+
$AuthenticatedUserIsRequired,
16413+
InvalidJsonFormat,
16414+
UnknownError
16415+
),
16416+
List(apiTagChat),
16417+
None
16418+
)
16419+
16420+
lazy val searchChatRooms: OBPEndpoint = {
16421+
case "chat-rooms" :: "search" :: Nil JsonPost json -> _ => {
16422+
cc => implicit val ec = EndpointContext(Some(cc))
16423+
for {
16424+
(Full(u), callContext) <- authenticatedAccess(cc)
16425+
postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $ChatRoomSearchRequestJsonV600", 400, callContext) {
16426+
json.extract[ChatRoomSearchRequestJsonV600]
16427+
}
16428+
rooms <- Future {
16429+
code.chat.ChatRoomTrait.chatRoomProvider.vend.searchChatRoomsForUserWithParticipants(
16430+
u.userId,
16431+
postJson.with_user_ids,
16432+
postJson.exact_participants.getOrElse(false)
16433+
)
16434+
} map {
16435+
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot search chat rooms", 400)
16436+
}
16437+
unreadCounts <- Future {
16438+
computeUnreadCounts(rooms, u.userId)
16439+
}
16440+
participantCounts <- Future {
16441+
computeParticipantCounts(rooms)
16442+
}
16443+
} yield {
16444+
(JSONFactory600.createChatRoomsJson(rooms, unreadCounts, participantCounts), HttpCode.`200`(callContext))
1635016445
}
1635116446
}
1635216447
}
@@ -16589,6 +16684,24 @@ trait APIMethods600 {
1658916684
}
1659016685
}
1659116686

16687+
/**
16688+
* Compute the participant count for a single chat room.
16689+
*/
16690+
private def computeParticipantCount(chatRoomId: String): Long = {
16691+
code.chat.ParticipantTrait.participantProvider.vend
16692+
.getParticipants(chatRoomId)
16693+
.map(_.length.toLong)
16694+
.openOr(0L)
16695+
}
16696+
16697+
/**
16698+
* Compute the participant count for each given room.
16699+
* One DB query per room — same N+1 pattern as `computeUnreadCounts`.
16700+
*/
16701+
private def computeParticipantCounts(rooms: List[code.chat.ChatRoomTrait]): Map[String, Long] = {
16702+
rooms.map(room => room.chatRoomId -> computeParticipantCount(room.chatRoomId)).toMap
16703+
}
16704+
1659216705
/**
1659316706
* Compute unread counts for a list of rooms for a given user.
1659416707
* For open rooms, counts only mentions. For private rooms, counts all unread messages.

obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,10 @@ case class InvestigationReportJsonV600(
11751175
// Chat / Messaging API case classes
11761176
case class PostChatRoomJsonV600(name: String, description: String)
11771177
case class PutChatRoomJsonV600(name: Option[String], description: Option[String])
1178+
case class ChatRoomSearchRequestJsonV600(
1179+
with_user_ids: List[String],
1180+
exact_participants: Option[Boolean] = Some(false)
1181+
)
11781182
case class PostParticipantJsonV600(user_id: Option[String], consumer_id: Option[String], permissions: Option[List[String]], webhook_url: Option[String])
11791183
case class PutParticipantPermissionsJsonV600(permissions: List[String])
11801184
case class PostChatMessageJsonV600(content: String, message_type: Option[String], mentioned_user_ids: Option[List[String]], reply_to_message_id: Option[String], thread_id: Option[String])
@@ -1197,7 +1201,8 @@ case class ChatRoomJsonV600(
11971201
last_message_sender: Option[String],
11981202
unread_count: Option[Long],
11991203
created_at: java.util.Date,
1200-
updated_at: java.util.Date
1204+
updated_at: java.util.Date,
1205+
participant_count: Long = 0L
12011206
)
12021207
case class ChatRoomsJsonV600(chat_rooms: List[ChatRoomJsonV600])
12031208

@@ -2968,7 +2973,11 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
29682973
}
29692974

29702975
// Chat / Messaging factory functions
2971-
def createChatRoomJson(room: code.chat.ChatRoomTrait, unreadCount: Option[Long] = None): ChatRoomJsonV600 = {
2976+
def createChatRoomJson(
2977+
room: code.chat.ChatRoomTrait,
2978+
unreadCount: Option[Long] = None,
2979+
participantCount: Long = 0L
2980+
): ChatRoomJsonV600 = {
29722981
val creator = code.users.Users.users.vend.getUserByUserId(room.createdBy)
29732982
val hasLastMessage = room.lastMessageAt.isDefined
29742983
ChatRoomJsonV600(
@@ -2987,11 +2996,18 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
29872996
last_message_sender = if (hasLastMessage) Some(room.lastMessageSender) else None,
29882997
unread_count = unreadCount,
29892998
created_at = room.createdDate,
2990-
updated_at = room.updatedDate
2999+
updated_at = room.updatedDate,
3000+
participant_count = participantCount
29913001
)
29923002
}
2993-
def createChatRoomsJson(rooms: List[code.chat.ChatRoomTrait], unreadCounts: Map[String, Long] = Map.empty): ChatRoomsJsonV600 = {
2994-
ChatRoomsJsonV600(rooms.map(r => createChatRoomJson(r, unreadCounts.get(r.chatRoomId))))
3003+
def createChatRoomsJson(
3004+
rooms: List[code.chat.ChatRoomTrait],
3005+
unreadCounts: Map[String, Long] = Map.empty,
3006+
participantCounts: Map[String, Long] = Map.empty
3007+
): ChatRoomsJsonV600 = {
3008+
ChatRoomsJsonV600(rooms.map(r =>
3009+
createChatRoomJson(r, unreadCounts.get(r.chatRoomId), participantCounts.getOrElse(r.chatRoomId, 0L))
3010+
))
29953011
}
29963012

29973013
def createParticipantJson(p: code.chat.ParticipantTrait): ParticipantJsonV600 = {

obp-api/src/main/scala/code/chat/ChatRoomTrait.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ trait ChatRoomProvider {
2323
def getChatRoomsByBankIdForUser(bankId: String, userId: String): Box[List[ChatRoomTrait]]
2424
def getChatRoomByJoiningKey(joiningKey: String): Box[ChatRoomTrait]
2525

26+
/**
27+
* Find chat rooms where the given user AND all of `requiredParticipantUserIds`
28+
* are participants. If `exactParticipants` is true, the room's participant set
29+
* must equal exactly `{userId} ∪ requiredParticipantUserIds` and open rooms
30+
* are excluded (their participant set is "everyone" so an exact match is
31+
* meaningless).
32+
*/
33+
def searchChatRoomsForUserWithParticipants(
34+
userId: String,
35+
requiredParticipantUserIds: List[String],
36+
exactParticipants: Boolean
37+
): Box[List[ChatRoomTrait]]
38+
2639
def updateChatRoom(
2740
chatRoomId: String,
2841
name: Option[String],

obp-api/src/main/scala/code/chat/MappedChatRoom.scala

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,46 @@ object MappedChatRoomProvider extends ChatRoomProvider {
6363
ChatRoom.find(By(ChatRoom.JoiningKey, joiningKey))
6464
}
6565

66+
override def searchChatRoomsForUserWithParticipants(
67+
userId: String,
68+
requiredParticipantUserIds: List[String],
69+
exactParticipants: Boolean
70+
): Box[List[ChatRoomTrait]] = {
71+
tryo {
72+
// 1. Find every room where the current user is an explicit participant.
73+
val myRoomIds = Participant.findAll(By(Participant.UserId, userId))
74+
.map(_.chatRoomId)
75+
.distinct
76+
val myRooms = if (myRoomIds.isEmpty) Nil
77+
else ChatRoom.findAll(ByList(ChatRoom.ChatRoomId, myRoomIds))
78+
79+
// 2. For each candidate room, fetch the full participant set and apply
80+
// the requested filters.
81+
val requiredSet = requiredParticipantUserIds.toSet
82+
val expectedExactSize = requiredSet.size + 1 // +1 for the current user
83+
84+
myRooms.filter { room =>
85+
// Open rooms have implicit participants ("everyone"), so an exact-match
86+
// query is meaningless against them — exclude them in that case.
87+
if (exactParticipants && room.isOpenRoom) {
88+
false
89+
} else {
90+
val participantUserIds = Participant.findAll(By(Participant.ChatRoomId, room.chatRoomId))
91+
.map(_.userId)
92+
.toSet
93+
val containsAllRequired = requiredSet.subsetOf(participantUserIds)
94+
if (!containsAllRequired) {
95+
false
96+
} else if (exactParticipants) {
97+
participantUserIds.size == expectedExactSize
98+
} else {
99+
true
100+
}
101+
}
102+
}
103+
}
104+
}
105+
66106
override def updateChatRoom(
67107
chatRoomId: String,
68108
name: Option[String],

0 commit comments

Comments
 (0)