diff --git a/flushall_build_and_run.sh b/flushall_build_and_run.sh
index 11356f45a1..d37c73782b 100755
--- a/flushall_build_and_run.sh
+++ b/flushall_build_and_run.sh
@@ -149,19 +149,23 @@ JAVA_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util.jar=ALL-UNNAMED \
--add-opens java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED"
+RUNTIME_LOG=/tmp/obp-api.log
+
if [ "$RUN_BACKGROUND" = true ]; then
- # Run in background with output to log file
- nohup java $JAVA_OPTS -jar obp-api/target/obp-api.jar > http4s-server.log 2>&1 &
+ # Run in background with output to log file (tee'd to /tmp as well)
+ nohup java $JAVA_OPTS -jar obp-api/target/obp-api.jar > >(tee "$RUNTIME_LOG") 2>&1 &
SERVER_PID=$!
echo "✓ HTTP4S server started in background"
echo " PID: $SERVER_PID"
- echo " Log: http4s-server.log"
+ echo " Log: http4s-server.log (also $RUNTIME_LOG)"
echo ""
echo "To stop the server: kill $SERVER_PID"
echo "To view logs: tail -f http4s-server.log"
else
- # Run in foreground (Ctrl+C to stop)
+ # Run in foreground (Ctrl+C to stop). Also tee output to /tmp so it can be
+ # tailed from another terminal without taking over this one.
echo "Press Ctrl+C to stop the server"
+ echo "Runtime log also written to: $RUNTIME_LOG"
echo ""
- java $JAVA_OPTS -jar obp-api/target/obp-api.jar
+ java $JAVA_OPTS -jar obp-api/target/obp-api.jar 2>&1 | tee "$RUNTIME_LOG"
fi
diff --git a/obp-api/pom.xml b/obp-api/pom.xml
index c107e3a3c7..6d859854a0 100644
--- a/obp-api/pom.xml
+++ b/obp-api/pom.xml
@@ -323,16 +323,25 @@
scalapb-runtime-grpc_${scala.version}
0.8.4
-
io.grpc
- grpc-all
+ grpc-netty-shaded
1.48.1
- io.netty
- netty-tcnative-boringssl-static
- 2.0.27.Final
+ io.grpc
+ grpc-protobuf
+ 1.48.1
+
+
+ io.grpc
+ grpc-stub
+ 1.48.1
+
+
+ io.grpc
+ grpc-services
+ 1.48.1
org.asynchttpclient
diff --git a/obp-api/src/main/protobuf/chat.proto b/obp-api/src/main/protobuf/chat.proto
new file mode 100644
index 0000000000..fed1bbf7f5
--- /dev/null
+++ b/obp-api/src/main/protobuf/chat.proto
@@ -0,0 +1,68 @@
+syntax = "proto3";
+package code.obp.grpc.chat.g1;
+
+import "google/protobuf/timestamp.proto";
+
+message StreamMessagesRequest {
+ string chat_room_id = 1;
+}
+
+// Fields match ChatMessageJsonV600 exactly, plus event_type for stream events
+message ChatMessageEvent {
+ string event_type = 1;
+ string chat_message_id = 2;
+ string chat_room_id = 3;
+ string sender_user_id = 4;
+ string sender_consumer_id = 5;
+ string sender_username = 6;
+ string sender_provider = 7;
+ string sender_consumer_name = 8;
+ string content = 9;
+ string message_type = 10;
+ repeated string mentioned_user_ids = 11;
+ string reply_to_message_id = 12;
+ string thread_id = 13;
+ bool is_deleted = 14;
+ google.protobuf.Timestamp created_at = 15;
+ google.protobuf.Timestamp updated_at = 16;
+}
+
+message TypingEvent {
+ string chat_room_id = 1;
+ bool is_typing = 2;
+}
+
+// Fields match TypingUserJsonV600
+message TypingIndicator {
+ string chat_room_id = 1;
+ string user_id = 2;
+ string username = 3;
+ string provider = 4;
+ bool is_typing = 5;
+}
+
+message StreamPresenceRequest {
+ string chat_room_id = 1;
+}
+
+message PresenceEvent {
+ string user_id = 1;
+ string username = 2;
+ string provider = 3;
+ bool is_online = 4;
+}
+
+message StreamUnreadCountsRequest {
+}
+
+message UnreadCountEvent {
+ string chat_room_id = 1;
+ int64 unread_count = 2;
+}
+
+service ChatStreamService {
+ rpc StreamMessages(StreamMessagesRequest) returns (stream ChatMessageEvent);
+ rpc StreamTyping(stream TypingEvent) returns (stream TypingIndicator);
+ rpc StreamPresence(StreamPresenceRequest) returns (stream PresenceEvent);
+ rpc StreamUnreadCounts(StreamUnreadCountsRequest) returns (stream UnreadCountEvent);
+}
diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template
index 699e92a6ec..0c5c811125 100644
--- a/obp-api/src/main/resources/props/sample.props.template
+++ b/obp-api/src/main/resources/props/sample.props.template
@@ -1196,10 +1196,13 @@ database_messages_scheduler_interval=3600
# GRPC
# the default GRPC is disabled
# grpc.server.enabled = false
-# If do not set this props, the grpc port will be set randomly when OBP starts.
-# And you can call `Get API Configuration` endpoint to see the `grpc_port` there.
-# When you set this props, need to make sure this port is available.
+# The default gRPC port is 50051. Override if needed.
# grpc.server.port = 50051
+# When gRPC is enabled, chat streaming services (StreamMessages, StreamTyping,
+# StreamPresence, StreamUnreadCounts) are available on the same port.
+# Clients authenticate via the "authorization" metadata key using the same
+# DirectLogin or OAuth tokens as the REST API.
+# See src/main/protobuf/chat.proto for the service contract.
# Create System Views At Boot -----------------------------------------------
# In case is not defined default value is true
diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
index a67bb12d19..5c79a617a7 100644
--- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
+++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
@@ -105,7 +105,7 @@ import code.migration.MigrationScriptLog
import code.model._
import code.model.dataAccess._
import code.model.dataAccess.internalMapping.AccountIdMapping
-import code.obp.grpc.HelloWorldServer
+import code.obp.grpc.ObpGrpcServer
import code.productAttributeattribute.MappedProductAttribute
import code.productcollection.MappedProductCollection
import code.productcollectionitem.MappedProductCollectionItem
@@ -131,6 +131,7 @@ import code.transaction_types.MappedTransactionType
import code.transactionattribute.MappedTransactionAttribute
import code.transactionrequests.{MappedTransactionRequest, MappedTransactionRequestTypeCharge, TransactionRequestReasons}
import code.usercustomerlinks.MappedUserCustomerLink
+import code.customerlinks.CustomerLink
import code.userlocks.UserLocks
import code.users._
import code.util.Helper.{MdcLoggable, ObpS, SILENCE_IS_GOLDEN}
@@ -761,6 +762,8 @@ class Boot extends MdcLoggable {
def schemifyAll() = {
Schemifier.schemify(true, Schemifier.infoF _, ToSchemify.models: _*)
+ // Create default system-level "general" chat room (all_users_are_participants = true)
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getOrCreateDefaultRoom()
}
private def showExceptionAtJson(error: Throwable): String = {
@@ -1161,6 +1164,7 @@ object ToSchemify {
MappedNarrative,
MappedCustomer,
MappedUserCustomerLink,
+ CustomerLink,
Consumer,
Token,
OpenIDConnectToken,
@@ -1203,12 +1207,16 @@ object ToSchemify {
CounterpartyAttributeMapper,
BankAccountBalance,
Group,
- AccountAccessRequest
+ AccountAccessRequest,
+ code.chat.ChatRoom,
+ code.chat.Participant,
+ code.chat.ChatMessage,
+ code.chat.Reaction
)
// start grpc server
if (APIUtil.getPropsAsBoolValue("grpc.server.enabled", false)) {
- val server = new HelloWorldServer(ExecutionContext.global)
+ val server = new ObpGrpcServer(ExecutionContext.global)
server.start()
LiftRules.unloadHooks.append(server.stop)
}
diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala
index 86848b97ae..2771c7ce76 100644
--- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala
+++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala
@@ -6103,6 +6103,75 @@ object SwaggerDefinitionsJSON {
List(counterpartyAttributeResponseJsonV600)
)
+ lazy val postCustomerLinkJsonV600 = PostCustomerLinkJsonV600(
+ customer_id = customerIdExample.value,
+ other_bank_id = bankIdExample.value,
+ other_customer_id = customerIdExample.value,
+ relationship_to = "spouse"
+ )
+
+ lazy val putCustomerLinkJsonV600 = PutCustomerLinkJsonV600(
+ relationship_to = "close_associate"
+ )
+
+ lazy val customerLinkJsonV600 = CustomerLinkJsonV600(
+ customer_link_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f",
+ bank_id = bankIdExample.value,
+ customer_id = customerIdExample.value,
+ other_bank_id = bankIdExample.value,
+ other_customer_id = customerIdExample.value,
+ relationship_to = "spouse",
+ date_inserted = DateWithDayExampleObject,
+ date_updated = DateWithDayExampleObject
+ )
+
+ lazy val customerLinksJsonV600 = CustomerLinksJsonV600(
+ List(customerLinkJsonV600)
+ )
+
+ lazy val investigationTransactionJsonV600 = InvestigationTransactionJsonV600(
+ transaction_id = transactionIdExample.value,
+ account_id = accountIdExample.value,
+ amount = "1250",
+ currency = currencyExample.value,
+ transaction_type = "DEBIT",
+ description = "Payment for consulting services",
+ start_date = DateWithDayExampleObject,
+ finish_date = DateWithDayExampleObject,
+ counterparty_name = "ACME Corp",
+ counterparty_account = "DE89370400440532013000",
+ counterparty_bank_name = "Deutsche Bank"
+ )
+
+ lazy val investigationAccountJsonV600 = InvestigationAccountJsonV600(
+ account_id = accountIdExample.value,
+ bank_id = bankIdExample.value,
+ currency = currencyExample.value,
+ balance = "150000",
+ account_name = "Current Account",
+ account_type = "CURRENT",
+ transactions = List(investigationTransactionJsonV600)
+ )
+
+ lazy val investigationCustomerLinkJsonV600 = InvestigationCustomerLinkJsonV600(
+ customer_link_id = "613c83ea-80f9-4560-8404-b9cd4ec42a7f",
+ other_customer_id = customerIdExample.value,
+ other_bank_id = bankIdExample.value,
+ relationship = "spouse",
+ other_legal_name = "Jane Doe"
+ )
+
+ lazy val investigationReportJsonV600 = InvestigationReportJsonV600(
+ customer_id = customerIdExample.value,
+ legal_name = "John Doe",
+ bank_id = bankIdExample.value,
+ accounts = List(investigationAccountJsonV600),
+ related_customers = List(investigationCustomerLinkJsonV600),
+ from_date = DateWithDayExampleObject,
+ to_date = DateWithDayExampleObject,
+ data_source = "mapped_database"
+ )
+
lazy val bankAccountBalanceRequestJsonV510 = BankAccountBalanceRequestJsonV510(
balance_type = balanceTypeExample.value,
balance_amount = balanceAmountExample.value
diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala
index a12bc5254a..026dbbf765 100644
--- a/obp-api/src/main/scala/code/api/util/ApiRole.scala
+++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala
@@ -317,7 +317,25 @@ object ApiRole extends MdcLoggable{
case class CanGetCustomerAccountLinks(requiresBankId: Boolean = true) extends ApiRole
lazy val canGetCustomerAccountLinks = CanGetCustomerAccountLinks()
-
+
+ case class CanCreateCustomerLink(requiresBankId: Boolean = true) extends ApiRole
+ lazy val canCreateCustomerLink = CanCreateCustomerLink()
+
+ case class CanUpdateCustomerLink(requiresBankId: Boolean = true) extends ApiRole
+ lazy val canUpdateCustomerLink = CanUpdateCustomerLink()
+
+ case class CanDeleteCustomerLink(requiresBankId: Boolean = true) extends ApiRole
+ lazy val canDeleteCustomerLink = CanDeleteCustomerLink()
+
+ case class CanGetCustomerLink(requiresBankId: Boolean = true) extends ApiRole
+ lazy val canGetCustomerLink = CanGetCustomerLink()
+
+ case class CanGetCustomerLinks(requiresBankId: Boolean = true) extends ApiRole
+ lazy val canGetCustomerLinks = CanGetCustomerLinks()
+
+ case class CanGetInvestigationReport(requiresBankId: Boolean = true) extends ApiRole
+ lazy val canGetInvestigationReport = CanGetInvestigationReport()
+
case class CanCreateBranch(requiresBankId: Boolean = true) extends ApiRole
lazy val canCreateBranch = CanCreateBranch()
@@ -1352,6 +1370,20 @@ object ApiRole extends MdcLoggable{
case class CanGetAccountDirectoryAtOneBank(requiresBankId: Boolean = true) extends ApiRole
lazy val canGetAccountDirectoryAtOneBank = CanGetAccountDirectoryAtOneBank()
+ // Chat Room roles
+ case class CanDeleteBankChatRoom(requiresBankId: Boolean = true) extends ApiRole
+ lazy val canDeleteBankChatRoom = CanDeleteBankChatRoom()
+ case class CanDeleteSystemChatRoom(requiresBankId: Boolean = false) extends ApiRole
+ lazy val canDeleteSystemChatRoom = CanDeleteSystemChatRoom()
+ case class CanArchiveBankChatRoom(requiresBankId: Boolean = true) extends ApiRole
+ lazy val canArchiveBankChatRoom = CanArchiveBankChatRoom()
+ case class CanArchiveSystemChatRoom(requiresBankId: Boolean = false) extends ApiRole
+ lazy val canArchiveSystemChatRoom = CanArchiveSystemChatRoom()
+ case class CanSetBankChatRoomAUAP(requiresBankId: Boolean = true) extends ApiRole
+ lazy val canSetBankChatRoomAUAP = CanSetBankChatRoomAUAP()
+ case class CanSetSystemChatRoomAUAP(requiresBankId: Boolean = false) extends ApiRole
+ lazy val canSetSystemChatRoomAUAP = CanSetSystemChatRoomAUAP()
+
private val dynamicApiRoles = new ConcurrentHashMap[String, ApiRole]
private case class DynamicApiRole(role: String, requiresBankId: Boolean = false) extends ApiRole{
diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala
index f014ad9dbf..fb92047c66 100644
--- a/obp-api/src/main/scala/code/api/util/ApiTag.scala
+++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala
@@ -89,6 +89,7 @@ object ApiTag {
val apiTagAggregateMetrics = ResourceDocTag("Aggregate-Metrics")
val apiTagSystemIntegrity = ResourceDocTag("System-Integrity")
val apiTagBalance = ResourceDocTag("Balance")
+ val apiTagChat = ResourceDocTag("Chat")
val apiTagGroup = ResourceDocTag("Group")
val apiTagWebhook = ResourceDocTag("Webhook")
val apiTagMockedData = ResourceDocTag("Mocked-Data")
@@ -164,6 +165,7 @@ object ApiTag {
val apiTagSignal = ResourceDocTag("Signal")
val apiTagSignalling = ResourceDocTag("Signalling")
val apiTagChannel = ResourceDocTag("Channel")
+ val apiTagFinancialCrime = ResourceDocTag("Financial-Crime")
private[this] val tagNameSymbolMapTag: MutableMap[String, ResourceDocTag] = MutableMap()
diff --git a/obp-api/src/main/scala/code/api/util/AuthHeaderParser.scala b/obp-api/src/main/scala/code/api/util/AuthHeaderParser.scala
new file mode 100644
index 0000000000..989b6eb6d2
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/util/AuthHeaderParser.scala
@@ -0,0 +1,87 @@
+package code.api.util
+
+import net.liftweb.common.{Box, Empty, Full}
+
+/**
+ * Transport-independent parsing of the HTTP `Authorization` header value
+ * into the subset of CallContext fields used by the authentication chain.
+ *
+ * The auth chain in [[APIUtil.getUserAndSessionContextFuture]] identifies which
+ * scheme to use (OAuth 2, OAuth 1.0a, DirectLogin, Gateway Login, DAuth) by
+ * reading CallContext.authReqHeaderField / directLoginParams / oAuthParams —
+ * *not* requestHeaders. Every transport that supports authentication (REST
+ * via http4s, gRPC, etc.) must populate these three fields identically,
+ * otherwise schemes will silently fail to match and the chain will fall through
+ * to "OBP-20080 Authorization Header format is not supported".
+ *
+ * This helper is the single source of truth for that parsing so that all
+ * transports stay in sync.
+ */
+object AuthHeaderParser {
+
+ /** Result of parsing an Authorization header value. */
+ final case class ParsedAuthHeader(
+ authReqHeaderField: Box[String],
+ directLoginParams: Map[String, String],
+ oAuthParams: Map[String, String]
+ )
+
+ private val EmptyParsed: ParsedAuthHeader =
+ ParsedAuthHeader(Empty, Map.empty, Map.empty)
+
+ private val DirectLoginAllowedParameters: List[String] =
+ List("consumer_key", "token", "username", "password")
+
+ /**
+ * Parse an Authorization header value (e.g. "Bearer eyJ...", "DirectLogin token=...",
+ * 'OAuth oauth_consumer_key="..."') into the auth-related CallContext fields.
+ *
+ * Returns empty fields when no header value is present.
+ */
+ def parseAuthorizationHeader(authHeaderValue: Option[String]): ParsedAuthHeader =
+ authHeaderValue match {
+ case None => EmptyParsed
+ case Some(value) =>
+ ParsedAuthHeader(
+ authReqHeaderField = Full(value),
+ directLoginParams = if (value.contains("DirectLogin")) parseDirectLoginHeader(value) else Map.empty,
+ oAuthParams = if (value.startsWith("OAuth ")) parseOAuthHeader(value) else Map.empty
+ )
+ }
+
+ /**
+ * Parse a DirectLogin header value into its named parameters.
+ * Accepts both:
+ * - `DirectLogin token="xxx", username="yyy"` (old Authorization header format, with prefix)
+ * - `token="xxx", username="yyy"` (new dedicated `DirectLogin:` header, no prefix)
+ *
+ * Only the whitelisted parameters (`consumer_key`, `token`, `username`, `password`)
+ * are kept. Mirrors Lift's getAllParameters in directlogin.scala.
+ */
+ def parseDirectLoginHeader(headerValue: String): Map[String, String] = {
+ val cleanedParameterList = headerValue.stripPrefix("DirectLogin").split(",").map(_.trim).toList
+ cleanedParameterList.flatMap { input =>
+ if (input.contains("=")) {
+ val split = input.split("=", 2)
+ val paramName = split(0).trim
+ val paramValue = split(1).replaceAll("^\"|\"$", "").trim
+ if (DirectLoginAllowedParameters.contains(paramName) && paramValue.nonEmpty)
+ Some(paramName -> paramValue)
+ else
+ None
+ } else {
+ None
+ }
+ }.toMap
+ }
+
+ /**
+ * Parse an OAuth 1.0a Authorization header value into its named parameters.
+ * Format: `OAuth oauth_consumer_key="xxx", oauth_token="yyy", ...`
+ */
+ def parseOAuthHeader(headerValue: String): Map[String, String] = {
+ val oauthPart = headerValue.stripPrefix("OAuth ").trim
+ val pattern = """(\w+)="([^"]*)"""".r
+ pattern.findAllMatchIn(oauthPart).map(m => m.group(1) -> m.group(2)).toMap
+ }
+}
diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
index 61bf2c577c..63d4033ebc 100644
--- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
+++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
@@ -389,6 +389,11 @@ object ErrorMessages {
val DefaultBankIdNotSet = "OBP-30044: Default BankId is not set on this instance. Please set defaultBank.bank_id in props files. "
val ExcludeParametersNotSupported = "OBP-30146: The exclude_* parameters are not supported in v6.0.0+. Please use the corresponding include_* parameters instead (include_app_names, include_url_patterns, include_implemented_by_partial_functions). "
+ val CustomerLinkNotFound = "OBP-30147: Customer Link not found. Please specify a valid value for CUSTOMER_LINK_ID."
+ val CreateCustomerLinkError = "OBP-30148: Could not create the Customer Link."
+ val UpdateCustomerLinkError = "OBP-30149: Could not update the Customer Link."
+ val InvestigationReportNotAvailable = "OBP-30150: Investigation Report is only available in mapped mode (connector=mapped)."
+
val CreateWebhookError = "OBP-30047: Cannot create Webhook"
val GetWebhooksError = "OBP-30048: Cannot get Webhooks"
val UpdateWebhookError = "OBP-30049: Cannot create Webhook"
@@ -692,6 +697,22 @@ object ErrorMessages {
val AbacRuleTooPermissive = "OBP-38010: ABAC rule is too permissive. The rule code contains a tautological expression (e.g. 'true', '1==1') that would always grant access. Please write a rule that checks specific attributes."
val AbacRuleStatisticallyTooPermissive = "OBP-38011: ABAC rule is statistically too permissive. When evaluated against a sample of system users with no resource context, the rule grants access to more than 50% of users. Please write a more selective rule that checks specific attributes."
+ // Chat / Messaging related messages (OBP-39XXX)
+ val ChatRoomNotFound = "OBP-39001: Chat Room not found. Please specify a valid value for CHAT_ROOM_ID."
+ val ChatRoomAlreadyExists = "OBP-39002: Chat Room with this name already exists in this bank."
+ val ChatRoomIsArchived = "OBP-39003: Chat Room is archived. No new messages or participants can be added."
+ val NotChatRoomParticipant = "OBP-39004: Current user is not a participant of this Chat Room."
+ val ChatMessageNotFound = "OBP-39005: Chat Message not found. Please specify a valid value for CHAT_MESSAGE_ID."
+ val ChatRoomParticipantAlreadyExists = "OBP-39006: User is already a participant of this Chat Room."
+ val ChatRoomParticipantNotFound = "OBP-39007: Participant not found in this Chat Room."
+ val InsufficientChatPermission = "OBP-39008: You do not have the required permission for this Chat Room action."
+ val CannotEditOthersMessage = "OBP-39009: You can only edit your own messages."
+ val CannotDeleteMessage = "OBP-39010: You do not have permission to delete this message."
+ val InvalidJoiningKey = "OBP-39011: Invalid joining key. The key may have been refreshed."
+ val ReactionAlreadyExists = "OBP-39012: You have already added this reaction to this message."
+ val ReactionNotFound = "OBP-39013: Reaction not found."
+ val MustSpecifyUserIdOrConsumerId = "OBP-39014: Must specify either user_id or consumer_id, but not both."
+
// Transaction Request related messages (OBP-40XXX)
val InvalidTransactionRequestType = "OBP-40001: Invalid value for TRANSACTION_REQUEST_TYPE"
val InsufficientAuthorisationToCreateTransactionRequest = "OBP-40002: Insufficient authorisation to create TransactionRequest. " +
@@ -916,7 +937,7 @@ object ErrorMessages {
MeetingApiKeyNotConfigured -> 403,
MeetingApiSecretNotConfigured -> 403,
EntitlementNotFound -> 404,
- EntitlementCannotBeDeleted -> 404,
+ EntitlementCannotBeDeleted -> 500,
ConsentStatusIssue -> 401,
ConsentDisabled -> 401,
InternalServerError -> 500,
diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala
index 2da89d8df3..3edca50c3f 100644
--- a/obp-api/src/main/scala/code/api/util/Glossary.scala
+++ b/obp-api/src/main/scala/code/api/util/Glossary.scala
@@ -5125,6 +5125,83 @@ object Glossary extends MdcLoggable {
|
""")
+ glossaryItems += GlossaryItem(
+ title = "Chat",
+ description =
+ s"""
+ |# Chat
+ |
+ |OBP provides a built-in Chat / Messaging API that allows users and applications to communicate within the platform.
+ |
+ |Chat Rooms can be scoped to a specific Bank (bank-level) or be system-wide (system-level).
+ |
+ |## Key Concepts
+ |
+ |### Chat Rooms
+ |A Chat Room is a named space where participants exchange messages.
+ |
+ |A system-level room called **general** is created automatically at startup with **all_users_are_participants = true** — meaning every authenticated user can read and send messages without needing an explicit Participant record.
+ |
+ |Each room has:
+ |- A unique **joining key** (UUID) that can be shared to invite others. The key can be refreshed to revoke access.
+ |- A **name** that is unique within its scope (per bank, or globally for system-level rooms).
+ |- An optional **bank_id** — if set, the room is scoped to that bank. If empty, it is a system-level room.
+ |- An **all_users_are_participants** flag — if true, all authenticated users are treated as implicit participants without needing a database record. They can read and send messages but have no special permissions.
+ |
+ |### Participants
+ |A Participant is a user or consumer (application/bot) that belongs to a Chat Room. Participants can:
+ |- Send and read messages.
+ |- Have a granular **permissions** list that controls what management actions they can perform.
+ |- Optionally specify a **webhook_url** to receive HTTP POST notifications for room events (new messages, mentions, etc.).
+ |
+ |Participants join rooms by presenting the room's joining key. The room creator automatically receives all permissions.
+ |
+ |### Participant Permissions
+ |Permissions are stored as a list on each Participant record. Possible values:
+ |- **can_delete_message** — delete any message in the room
+ |- **can_remove_participant** — remove other participants from the room
+ |- **can_refresh_joining_key** — regenerate the room's joining key
+ |- **can_update_room** — edit the room name and description
+ |- **can_manage_permissions** — grant or revoke permissions for other participants, and add participants directly
+ |
+ |Any participant can send messages, read messages, and add emoji reactions without special permissions. A participant can also remove themselves (leave the room) without needing the can_remove_participant permission.
+ |
+ |### OBP-Level Roles
+ |In addition to room-level permissions, OBP Roles provide platform-wide moderation:
+ |- **CanDeleteBankChatRoom** — delete any chat room within a bank
+ |- **CanDeleteSystemChatRoom** — delete any system-level chat room
+ |- **CanArchiveBankChatRoom** — archive any chat room within a bank
+ |- **CanArchiveSystemChatRoom** — archive any system-level chat room
+ |
+ |Bank-scoped roles apply per bank; system-scoped roles apply to system-level chat rooms. Both kinds apply regardless of room-level permissions.
+ |
+ |### Consumer / Bot Participation
+ |API Consumers (applications) can participate in chat rooms alongside human users. A Participant record stores either a user_id or a consumer_id (not both). This enables automated assistants, notification bots, and integrations.
+ |
+ |### Messages
+ |Messages support:
+ |- **@mentions** — the mentioned_user_ids field tracks which users are referenced in a message.
+ |- **Threading** — a message can reference a thread_id (the root message) to form a conversation thread.
+ |- **Editing** — only the sender can edit their own message.
+ |- **Soft deletion** — messages are marked as deleted rather than removed, preserving audit trails.
+ |- **Emoji reactions** — participants can react to messages with emoji. Each user can add a given emoji to a message only once.
+ |
+ |### Typing Indicators
+ |Typing state is ephemeral and stored in Redis with a short TTL (5 seconds). No database records are created.
+ |
+ |### Polling
+ |Clients retrieve new messages by polling the GET messages endpoint with a **since** parameter (timestamp). This avoids the complexity of WebSocket infrastructure while providing a simple, reliable mechanism for near-real-time updates.
+ |
+ |## API Endpoints
+ |
+ |All chat endpoints are available in two forms:
+ |- **Bank-scoped**: /banks/BANK_ID/chat-rooms/...
+ |- **System-level**: /chat-rooms/...
+ |
+ |See the API Explorer for the full list of Chat endpoints, tagged with **Chat**.
+ |
+""")
+
///////////////////////////////////////////////////////////////////
// NOTE! Some glossary items are generated in ExampleValue.scala
//////////////////////////////////////////////////////////////////
diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala
index 05dbfc2d64..d9e2337b43 100644
--- a/obp-api/src/main/scala/code/api/util/NewStyle.scala
+++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala
@@ -4401,6 +4401,36 @@ object NewStyle extends MdcLoggable{
i => (unboxFullOrFail(i._1, callContext, UpdateCustomerAccountLinkError), i._2)
}
+ def createCustomerLink(bankId: String, customerId: String, otherBankId: String, otherCustomerId: String, relationshipTo: String, callContext: Option[CallContext]): OBPReturnType[code.customerlinks.CustomerLinkTrait] =
+ Connector.connector.vend.createCustomerLink(bankId, customerId, otherBankId, otherCustomerId, relationshipTo, callContext) map {
+ i => (unboxFullOrFail(i._1, callContext, CreateCustomerLinkError), i._2)
+ }
+
+ def getCustomerLinkById(customerLinkId: String, callContext: Option[CallContext]): OBPReturnType[code.customerlinks.CustomerLinkTrait] =
+ Connector.connector.vend.getCustomerLinkById(customerLinkId, callContext) map {
+ i => (unboxFullOrFail(i._1, callContext, CustomerLinkNotFound), i._2)
+ }
+
+ def getCustomerLinksByBankId(bankId: String, callContext: Option[CallContext]): OBPReturnType[List[code.customerlinks.CustomerLinkTrait]] =
+ Connector.connector.vend.getCustomerLinksByBankId(bankId, callContext) map {
+ i => (unboxFullOrFail(i._1, callContext, CustomerLinkNotFound), i._2)
+ }
+
+ def getCustomerLinksByCustomerId(customerId: String, callContext: Option[CallContext]): OBPReturnType[List[code.customerlinks.CustomerLinkTrait]] =
+ Connector.connector.vend.getCustomerLinksByCustomerId(customerId, callContext) map {
+ i => (unboxFullOrFail(i._1, callContext, CustomerLinkNotFound), i._2)
+ }
+
+ def updateCustomerLinkById(customerLinkId: String, relationshipTo: String, callContext: Option[CallContext]): OBPReturnType[code.customerlinks.CustomerLinkTrait] =
+ Connector.connector.vend.updateCustomerLinkById(customerLinkId, relationshipTo, callContext) map {
+ i => (unboxFullOrFail(i._1, callContext, UpdateCustomerLinkError), i._2)
+ }
+
+ def deleteCustomerLinkById(customerLinkId: String, callContext: Option[CallContext]): OBPReturnType[Boolean] =
+ Connector.connector.vend.deleteCustomerLinkById(customerLinkId, callContext) map {
+ i => (unboxFullOrFail(i._1, callContext, CustomerLinkNotFound), i._2)
+ }
+
def getConsentImplicitSCA(user: User, callContext: Option[CallContext]): OBPReturnType[ConsentImplicitSCAT] =
Connector.connector.vend.getConsentImplicitSCA(user: User, callContext: Option[CallContext]) map {
i => (unboxFullOrFail(i._1, callContext, GetConsentImplicitSCAError), i._2)
diff --git a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala
index 89cae07d3e..c8b9a3941c 100644
--- a/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala
+++ b/obp-api/src/main/scala/code/api/util/http4s/Http4sSupport.scala
@@ -2,7 +2,7 @@ package code.api.util.http4s
import cats.effect._
import code.api.util.APIUtil.ResourceDoc
-import code.api.util.CallContext
+import code.api.util.{AuthHeaderParser, CallContext}
import com.openbankproject.commons.model.{Bank, User}
import net.liftweb.common.{Box, Empty, Full}
import net.liftweb.http.provider.HTTPParam
@@ -256,75 +256,36 @@ object Http4sCallContextBuilder {
}
/**
- * Extract DirectLogin header parameters if present
+ * Extract DirectLogin header parameters if present.
* Supports two formats:
- * 1. New format (2021): DirectLogin: token=xxx
- * 2. Old format (deprecated): Authorization: DirectLogin token=xxx
+ * 1. New format (2021): `DirectLogin: token=xxx` (dedicated header)
+ * 2. Old format (deprecated): `Authorization: DirectLogin token=xxx`
+ *
+ * Delegates the actual parsing of header values to [[AuthHeaderParser]] so
+ * it stays consistent with other transports (e.g. gRPC).
*/
private def extractDirectLoginParams(request: Request[IO]): Map[String, String] = {
// Try new format first: DirectLogin header
request.headers.get(CIString("DirectLogin"))
- .map(h => parseDirectLoginHeader(h.head.value))
+ .map(h => AuthHeaderParser.parseDirectLoginHeader(h.head.value))
.getOrElse {
// Fall back to old format: Authorization: DirectLogin token=xxx
request.headers.get(CIString("Authorization"))
.filter(_.head.value.contains("DirectLogin"))
- .map(h => parseDirectLoginHeader(h.head.value))
+ .map(h => AuthHeaderParser.parseDirectLoginHeader(h.head.value))
.getOrElse(Map.empty)
}
}
-
- /**
- * Parse DirectLogin header value into parameter map
- * Matches Lift's parsing logic in directlogin.scala getAllParameters
- * Supports formats:
- * - DirectLogin token="xxx"
- * - DirectLogin token=xxx
- * - token="xxx", username="yyy"
- */
- private def parseDirectLoginHeader(headerValue: String): Map[String, String] = {
- val directLoginPossibleParameters = List("consumer_key", "token", "username", "password")
-
- // Strip "DirectLogin" prefix and split by comma, then trim each part (matches Lift logic)
- val cleanedParameterList = headerValue.stripPrefix("DirectLogin").split(",").map(_.trim).toList
-
- cleanedParameterList.flatMap { input =>
- if (input.contains("=")) {
- val split = input.split("=", 2)
- val paramName = split(0).trim
- // Remove surrounding quotes if present
- val paramValue = split(1).replaceAll("^\"|\"$", "").trim
- if (directLoginPossibleParameters.contains(paramName) && paramValue.nonEmpty)
- Some(paramName -> paramValue)
- else
- None
- } else {
- None
- }
- }.toMap
- }
-
+
/**
- * Extract OAuth parameters from Authorization header if OAuth
+ * Extract OAuth 1.0a parameters from the Authorization header if it uses the OAuth scheme.
*/
private def extractOAuthParams(request: Request[IO]): Map[String, String] = {
request.headers.get(CIString("Authorization"))
.filter(_.head.value.startsWith("OAuth "))
- .map(h => parseOAuthHeader(h.head.value))
+ .map(h => AuthHeaderParser.parseOAuthHeader(h.head.value))
.getOrElse(Map.empty)
}
-
- /**
- * Parse OAuth Authorization header value into parameter map
- * Format: OAuth oauth_consumer_key="xxx", oauth_token="yyy", ...
- */
- private def parseOAuthHeader(headerValue: String): Map[String, String] = {
- val oauthPart = headerValue.stripPrefix("OAuth ").trim
- val pattern = """(\w+)="([^"]*)"""".r
- pattern.findAllMatchIn(oauthPart).map { m =>
- m.group(1) -> m.group(2)
- }.toMap
- }
}
/**
diff --git a/obp-api/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/obp-api/src/main/scala/code/api/v2_0_0/APIMethods200.scala
index 0e702593f7..2270a84946 100644
--- a/obp-api/src/main/scala/code/api/v2_0_0/APIMethods200.scala
+++ b/obp-api/src/main/scala/code/api/v2_0_0/APIMethods200.scala
@@ -1921,7 +1921,7 @@ trait APIMethods200 {
} map { unboxFull(_) }
_ <- Helper.booleanToFuture(UserDoesNotHaveEntitlement, cc=callContext) { entitlement.userId == userId }
deleted <- Future(Entitlement.entitlement.vend.deleteEntitlement(Some(entitlement))) map {
- x => fullBoxOrException(x ~> APIFailureNewStyle(EntitlementCannotBeDeleted, 404, callContext.map(_.toLight)))
+ x => fullBoxOrException(x ~> APIFailureNewStyle(EntitlementCannotBeDeleted, 500, callContext.map(_.toLight)))
} map { unboxFull(_) }
} yield (deleted, HttpCode.`204`(cc.callContext))
}
diff --git a/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala b/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala
index 0f25cb7c53..33361752e4 100644
--- a/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala
+++ b/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala
@@ -49,7 +49,7 @@ import code.entitlement.Entitlement
import code.loginattempts.BadLoginAttempt
import code.metrics.{TopApi, TopConsumer}
import code.model.{Consumer, ModeratedBankAccount, ModeratedBankAccountCore, UserX}
-import code.obp.grpc.HelloWorldServer
+import code.obp.grpc.ObpGrpcServer
import code.ratelimiting
import code.webhook.AccountWebhook
import com.openbankproject.commons.model.{AccountApplication, AmountOfMoneyJsonV121, CustomerAttribute, Product, ProductCollection, ProductCollectionItem, TaxResidence, User, UserAuthContextUpdate, _}
@@ -923,7 +923,7 @@ object JSONFactory310{
def getConfigInfoJSON(): ConfigurationJsonV310 = {
val configurationJson: ConfigurationJSON = JSONFactory220.getConfigInfoJSON()
val defaultBankId= APIUtil.defaultBankId
- val grpcPort = HelloWorldServer.port
+ val grpcPort = ObpGrpcServer.port
ConfigurationJsonV310(
defaultBankId,
configurationJson.akka,
diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
index e6e09ce346..b85da9cac8 100644
--- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
+++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
@@ -36,6 +36,7 @@ import code.api.v6_0_0.OBPAPI6_0_0
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
import code.mandate.{MappedMandateProvider}
import code.api.v6_0_0.JSONFactory600.{createMandateJsonV600, createMandatesJsonV600, createMandateProvisionJsonV600, createMandateProvisionsJsonV600, createSignatoryPanelJsonV600, createSignatoryPanelsJsonV600, createCounterpartyAttributeJson, createCounterpartyAttributesJson}
+// Chat case classes are at package level in JSONFactory6.0.0.scala, not inside JSONFactory600 object
import code.metrics.{APIMetrics, ConnectorCountsRedis, ConnectorTraceProvider}
import code.bankconnectors.{Connector, LocalMappedConnectorInternal}
import code.bankconnectors.storedprocedure.StoredProcedureUtils
@@ -12443,6 +12444,4015 @@ trait APIMethods600 {
}
}
+ staticResourceDocs += ResourceDoc(
+ createCustomerLink,
+ implementedInApiVersion,
+ nameOf(createCustomerLink),
+ "POST",
+ "/banks/BANK_ID/customer-links",
+ "Create Customer Link",
+ s"""Link a Customer to another Customer (e.g. spouse, parent, close_associate).
+ |
+ |Authentication is Required
+ |
+ |""",
+ postCustomerLinkJsonV600,
+ customerLinkJsonV600,
+ List(
+ $AuthenticatedUserIsRequired,
+ $BankNotFound,
+ InvalidJsonFormat,
+ CustomerNotFoundByCustomerId,
+ UserHasMissingRoles,
+ CreateCustomerLinkError,
+ UnknownError
+ ),
+ List(apiTagCustomer),
+ Some(List(canCreateCustomerLink)))
+
+ lazy val createCustomerLink: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "customer-links" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (_, _, callContext) <- SS.userBank
+ postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerLinkJsonV600 ", 400, callContext) {
+ json.extract[PostCustomerLinkJsonV600]
+ }
+ (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(postedData.customer_id, callContext)
+ _ <- Helper.booleanToFuture(s"Bank of the customer specified by the CUSTOMER_ID(${customer.bankId}) has to match BANK_ID(${bankId.value}) in URL", 400, callContext) {
+ customer.bankId == bankId.value
+ }
+ (_, callContext) <- NewStyle.function.getBank(BankId(postedData.other_bank_id), callContext)
+ (otherCustomer, callContext) <- NewStyle.function.getCustomerByCustomerId(postedData.other_customer_id, callContext)
+ _ <- Helper.booleanToFuture(s"Bank of the other customer specified by the OTHER_CUSTOMER_ID(${otherCustomer.bankId}) has to match OTHER_BANK_ID(${postedData.other_bank_id})", 400, callContext) {
+ otherCustomer.bankId == postedData.other_bank_id
+ }
+ (customerLink, callContext) <- NewStyle.function.createCustomerLink(bankId.value, postedData.customer_id, postedData.other_bank_id, postedData.other_customer_id, postedData.relationship_to, callContext)
+ } yield {
+ (JSONFactory600.createCustomerLinkJson(customerLink), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ staticResourceDocs += ResourceDoc(
+ getCustomerLinksByCustomerId,
+ implementedInApiVersion,
+ nameOf(getCustomerLinksByCustomerId),
+ "GET",
+ "/banks/BANK_ID/customers/CUSTOMER_ID/customer-links",
+ "Get Customer Links by CUSTOMER_ID",
+ s"""Get Customer Links by CUSTOMER_ID.
+ |
+ |Authentication is Required
+ |
+ |""",
+ EmptyBody,
+ customerLinksJsonV600,
+ List(
+ $AuthenticatedUserIsRequired,
+ $BankNotFound,
+ CustomerNotFoundByCustomerId,
+ UserHasMissingRoles,
+ UnknownError
+ ),
+ List(apiTagCustomer),
+ Some(List(canGetCustomerLinks)))
+
+ lazy val getCustomerLinksByCustomerId: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "customers" :: customerId :: "customer-links" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (_, _, callContext) <- SS.userBank
+ (_, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, callContext)
+ (customerLinks, callContext) <- NewStyle.function.getCustomerLinksByCustomerId(customerId, callContext)
+ } yield {
+ (JSONFactory600.createCustomerLinksJson(customerLinks), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ staticResourceDocs += ResourceDoc(
+ getCustomerLinksByBankId,
+ implementedInApiVersion,
+ nameOf(getCustomerLinksByBankId),
+ "GET",
+ "/banks/BANK_ID/customer-links",
+ "Get Customer Links at Bank",
+ s"""Get all Customer Links at a Bank.
+ |
+ |Authentication is Required
+ |
+ |""",
+ EmptyBody,
+ customerLinksJsonV600,
+ List(
+ $AuthenticatedUserIsRequired,
+ $BankNotFound,
+ UserHasMissingRoles,
+ UnknownError
+ ),
+ List(apiTagCustomer),
+ Some(List(canGetCustomerLinks)))
+
+ lazy val getCustomerLinksByBankId: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "customer-links" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (_, _, callContext) <- SS.userBank
+ (customerLinks, callContext) <- NewStyle.function.getCustomerLinksByBankId(bankId.value, callContext)
+ } yield {
+ (JSONFactory600.createCustomerLinksJson(customerLinks), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ staticResourceDocs += ResourceDoc(
+ getCustomerLinkById,
+ implementedInApiVersion,
+ nameOf(getCustomerLinkById),
+ "GET",
+ "/banks/BANK_ID/customer-links/CUSTOMER_LINK_ID",
+ "Get Customer Link by CUSTOMER_LINK_ID",
+ s"""Get Customer Link by CUSTOMER_LINK_ID.
+ |
+ |Authentication is Required
+ |
+ |""",
+ EmptyBody,
+ customerLinkJsonV600,
+ List(
+ $AuthenticatedUserIsRequired,
+ $BankNotFound,
+ CustomerLinkNotFound,
+ UserHasMissingRoles,
+ UnknownError
+ ),
+ List(apiTagCustomer),
+ Some(List(canGetCustomerLink)))
+
+ lazy val getCustomerLinkById: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "customer-links" :: customerLinkId :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (_, _, callContext) <- SS.userBank
+ (customerLink, callContext) <- NewStyle.function.getCustomerLinkById(customerLinkId, callContext)
+ } yield {
+ (JSONFactory600.createCustomerLinkJson(customerLink), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ staticResourceDocs += ResourceDoc(
+ updateCustomerLink,
+ implementedInApiVersion,
+ nameOf(updateCustomerLink),
+ "PUT",
+ "/banks/BANK_ID/customer-links/CUSTOMER_LINK_ID",
+ "Update Customer Link",
+ s"""Update an existing Customer Link.
+ |
+ |Authentication is Required
+ |
+ |""",
+ putCustomerLinkJsonV600,
+ customerLinkJsonV600,
+ List(
+ $AuthenticatedUserIsRequired,
+ $BankNotFound,
+ InvalidJsonFormat,
+ CustomerLinkNotFound,
+ UserHasMissingRoles,
+ UpdateCustomerLinkError,
+ UnknownError
+ ),
+ List(apiTagCustomer),
+ Some(List(canUpdateCustomerLink)))
+
+ lazy val updateCustomerLink: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "customer-links" :: customerLinkId :: Nil JsonPut json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (_, _, callContext) <- SS.userBank
+ postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PutCustomerLinkJsonV600 ", 400, callContext) {
+ json.extract[PutCustomerLinkJsonV600]
+ }
+ (customerLink, callContext) <- NewStyle.function.updateCustomerLinkById(customerLinkId, postedData.relationship_to, callContext)
+ } yield {
+ (JSONFactory600.createCustomerLinkJson(customerLink), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ staticResourceDocs += ResourceDoc(
+ deleteCustomerLink,
+ implementedInApiVersion,
+ nameOf(deleteCustomerLink),
+ "DELETE",
+ "/banks/BANK_ID/customer-links/CUSTOMER_LINK_ID",
+ "Delete Customer Link",
+ s"""Delete a Customer Link.
+ |
+ |Authentication is Required
+ |
+ |""",
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ $BankNotFound,
+ CustomerLinkNotFound,
+ UserHasMissingRoles,
+ UnknownError
+ ),
+ List(apiTagCustomer),
+ Some(List(canDeleteCustomerLink)))
+
+ lazy val deleteCustomerLink: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "customer-links" :: customerLinkId :: Nil JsonDelete _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (_, _, callContext) <- SS.userBank
+ (_, callContext) <- NewStyle.function.deleteCustomerLinkById(customerLinkId, callContext)
+ } yield {
+ (Full(true), HttpCode.`204`(callContext))
+ }
+ }
+ }
+
+
+ staticResourceDocs += ResourceDoc(
+ getCustomerInvestigationReport,
+ implementedInApiVersion,
+ nameOf(getCustomerInvestigationReport),
+ "GET",
+ "/banks/BANK_ID/customers/CUSTOMER_ID/investigation-report",
+ "Get Customer Investigation Report",
+ s"""Get a Customer Investigation Report for fraud detection, AML (Anti-Money Laundering), and financial crime analysis.
+ |
+ |This endpoint assembles a comprehensive data package for a customer in a single API call,
+ |designed for use by AI agents, compliance officers, and financial crime investigators.
+ |
+ |**Use Cases:**
+ |
+ |* Fraud Detection - identify suspicious transaction patterns
+ |* AML / Anti-Money Laundering - trace fund flows and flag anomalies
+ |* KYC Enhanced Due Diligence - deep-dive into customer activity
+ |* Suspicious Activity Report (SAR) preparation
+ |* Financial crime investigation and evidence gathering
+ |
+ |**Data Returned:**
+ |
+ |* Customer details (legal name, KYC status)
+ |* All accounts linked to the customer (with balances)
+ |* Transaction history for those accounts (within the specified date range)
+ |* Related customers (via customer links) — spouses, associates, business partners
+ |
+ |**Suspicious Patterns This Data Supports Detecting:**
+ |
+ |* Money flowing through intermediary companies (A to B to C patterns)
+ |* Payments inconsistent with known income or salary
+ |* Transfers to related parties (spouses, associates) shortly after large inflows
+ |* Round-tripping — money returning to origin via indirect paths
+ |* Vague or generic transaction descriptions on large amounts
+ |* Structuring — multiple transactions just below reporting thresholds
+ |* Rapid movement of funds across accounts (layering)
+ |
+ |**Query Parameters:**
+ |
+ |* from_date: Start date for transactions (ISO format, e.g. $DateWithMsExampleString). Defaults to 1 year ago.
+ |* to_date: End date for transactions (ISO format, e.g. $DateWithMsExampleString). Defaults to now.
+ |* limit: Maximum number of transactions per account (default 500).
+ |
+ |**Note:** This endpoint is only available in mapped mode (connector=mapped).
+ |For other connector configurations, use the individual endpoints to retrieve
+ |customer, account, transaction, and customer link data separately.
+ |
+ |Authentication is Required
+ |
+ |""",
+ EmptyBody,
+ investigationReportJsonV600,
+ List(
+ $AuthenticatedUserIsRequired,
+ $BankNotFound,
+ CustomerNotFoundByCustomerId,
+ InvestigationReportNotAvailable,
+ UserHasMissingRoles,
+ UnknownError
+ ),
+ List(apiTagCustomer, apiTagKyc, apiTagTransaction, apiTagAccount, apiTagFinancialCrime, apiTagAiAgent),
+ Some(List(canGetInvestigationReport)))
+
+ lazy val getCustomerInvestigationReport: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "customers" :: customerId :: "investigation-report" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (_, _, callContext) <- SS.userBank
+ // Check connector is mapped
+ connectorName = code.api.Constant.CONNECTOR.openOrThrowException("connector not set")
+ _ <- Helper.booleanToFuture(failMsg = InvestigationReportNotAvailable, cc = callContext) {
+ connectorName == "mapped"
+ }
+ // Validate customer exists
+ (customer, callContext) <- NewStyle.function.getCustomerByCustomerId(customerId, callContext)
+ _ <- Helper.booleanToFuture(failMsg = s"Customer bank (${customer.bankId}) does not match BANK_ID (${bankId.value})", 400, callContext) {
+ customer.bankId == bankId.value
+ }
+ // Parse query params
+ fromDateStr = ObpS.param("from_date")
+ toDateStr = ObpS.param("to_date")
+ limitStr = ObpS.param("limit")
+ fromDate = fromDateStr.flatMap(d => APIUtil.parseDate(d)).getOrElse {
+ new java.util.Date(System.currentTimeMillis() - 365L * 24 * 60 * 60 * 1000)
+ }
+ toDate = toDateStr.flatMap(d => APIUtil.parseDate(d)).getOrElse {
+ new java.util.Date()
+ }
+ limit = limitStr.flatMap(s => tryo(s.toInt)).getOrElse(500)
+ // Run Doobie queries
+ accounts <- Future {
+ code.investigation.DoobieInvestigationQueries.getAccountsForCustomer(customerId)
+ }
+ accountIds = accounts.map(_.accountId)
+ transactions <- Future {
+ code.investigation.DoobieInvestigationQueries.getTransactionsForAccounts(
+ accountIds, bankId.value,
+ new java.sql.Timestamp(fromDate.getTime),
+ new java.sql.Timestamp(toDate.getTime),
+ limit
+ )
+ }
+ customerLinks <- Future {
+ code.investigation.DoobieInvestigationQueries.getCustomerLinks(customerId)
+ }
+ customerRow = code.investigation.DoobieInvestigationQueries.CustomerRow(
+ customerId = customer.customerId,
+ legalName = customer.legalName,
+ email = customer.email,
+ mobileNumber = customer.mobileNumber,
+ kycStatus = customer.kycStatus
+ )
+ } yield {
+ (JSONFactory600.createInvestigationReportJson(
+ customerRow, bankId.value, accounts, transactions, customerLinks, fromDate, toDate
+ ), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // ============================================ CHAT / MESSAGING API ENDPOINTS ============================================
+
+ // ------ Batch A: Room CRUD ------
+
+ // 1a. createBankChatRoom
+ staticResourceDocs += ResourceDoc(
+ createBankChatRoom,
+ implementedInApiVersion,
+ nameOf(createBankChatRoom),
+ "POST",
+ "/banks/BANK_ID/chat-rooms",
+ "Create Bank Chat Room",
+ s"""Create a new chat room scoped to a bank.
+ |The creator is automatically added as a participant with all permissions.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostChatRoomJsonV600(name = "General Discussion", description = "A place to discuss general topics"),
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "gh.29.uk",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomAlreadyExists,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val createBankChatRoom: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostChatRoomJsonV600", 400, callContext) {
+ json.extract[PostChatRoomJsonV600]
+ }
+ existingRoom <- Future(code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoomByBankIdAndName(bankId.value, postJson.name))
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomAlreadyExists, cc = callContext) {
+ existingRoom.isEmpty
+ }
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.createChatRoom(bankId.value, postJson.name, postJson.description, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot create chat room", 400)
+ }
+ _ <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.addParticipant(room.chatRoomId, u.userId, "", code.chat.ChatPermissions.ALL_PERMISSIONS, "")
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot add creator as participant", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(room), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 1b. createSystemChatRoom
+ staticResourceDocs += ResourceDoc(
+ createSystemChatRoom,
+ implementedInApiVersion,
+ nameOf(createSystemChatRoom),
+ "POST",
+ "/chat-rooms",
+ "Create System Chat Room",
+ s"""Create a new system-level chat room (not scoped to a bank).
+ |The creator is automatically added as a participant with all permissions.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostChatRoomJsonV600(name = "General Discussion", description = "A place to discuss general topics"),
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomAlreadyExists,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val createSystemChatRoom: OBPEndpoint = {
+ case "chat-rooms" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostChatRoomJsonV600", 400, callContext) {
+ json.extract[PostChatRoomJsonV600]
+ }
+ existingRoom <- Future(code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoomByBankIdAndName("", postJson.name))
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomAlreadyExists, cc = callContext) {
+ existingRoom.isEmpty
+ }
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.createChatRoom("", postJson.name, postJson.description, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot create chat room", 400)
+ }
+ _ <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.addParticipant(room.chatRoomId, u.userId, "", code.chat.ChatPermissions.ALL_PERMISSIONS, "")
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot add creator as participant", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(room), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 2a. getBankChatRooms
+ staticResourceDocs += ResourceDoc(
+ getBankChatRooms,
+ implementedInApiVersion,
+ nameOf(getBankChatRooms),
+ "GET",
+ "/banks/BANK_ID/chat-rooms",
+ "Get Bank Chat Rooms",
+ s"""Get all chat rooms for the specified bank that the current user is a participant of.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatRoomsJsonV600(chat_rooms = List(ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "gh.29.uk",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getBankChatRooms: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ rooms <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoomsByBankIdForUser(bankId.value, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get chat rooms", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomsJson(rooms), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 2b. getSystemChatRooms
+ staticResourceDocs += ResourceDoc(
+ getSystemChatRooms,
+ implementedInApiVersion,
+ nameOf(getSystemChatRooms),
+ "GET",
+ "/chat-rooms",
+ "Get System Chat Rooms",
+ s"""Get all system-level chat rooms that the current user is a participant of.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatRoomsJsonV600(chat_rooms = List(ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getSystemChatRooms: OBPEndpoint = {
+ case "chat-rooms" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ rooms <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoomsByBankIdForUser("", u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get chat rooms", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomsJson(rooms), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 3a. getBankChatRoom
+ staticResourceDocs += ResourceDoc(
+ getBankChatRoom,
+ implementedInApiVersion,
+ nameOf(getBankChatRoom),
+ "GET",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID",
+ "Get Bank Chat Room",
+ s"""Get a specific chat room by ID within a bank. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "gh.29.uk",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getBankChatRoom: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(room), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 3b. getSystemChatRoom
+ staticResourceDocs += ResourceDoc(
+ getSystemChatRoom,
+ implementedInApiVersion,
+ nameOf(getSystemChatRoom),
+ "GET",
+ "/chat-rooms/CHAT_ROOM_ID",
+ "Get System Chat Room",
+ s"""Get a specific system-level chat room by ID. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getSystemChatRoom: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(room), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 4a. updateBankChatRoom
+ staticResourceDocs += ResourceDoc(
+ updateBankChatRoom,
+ implementedInApiVersion,
+ nameOf(updateBankChatRoom),
+ "PUT",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID",
+ "Update Bank Chat Room",
+ s"""Update the name and/or description of a chat room. Requires can_update_room permission.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PutChatRoomJsonV600(name = Some("Updated Name"), description = Some("Updated description")),
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "gh.29.uk",
+ name = "Updated Name",
+ description = "Updated description",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ InsufficientChatPermission,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val updateBankChatRoom: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: Nil JsonPut json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ putJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PutChatRoomJsonV600", 400, callContext) {
+ json.extract[PutChatRoomJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_UPDATE_ROOM)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ updatedRoom <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.updateChatRoom(chatRoomId, putJson.name, putJson.description)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update chat room", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(updatedRoom), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 4b. updateSystemChatRoom
+ staticResourceDocs += ResourceDoc(
+ updateSystemChatRoom,
+ implementedInApiVersion,
+ nameOf(updateSystemChatRoom),
+ "PUT",
+ "/chat-rooms/CHAT_ROOM_ID",
+ "Update System Chat Room",
+ s"""Update the name and/or description of a system-level chat room. Requires can_update_room permission.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PutChatRoomJsonV600(name = Some("Updated Name"), description = Some("Updated description")),
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "",
+ name = "Updated Name",
+ description = "Updated description",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ InsufficientChatPermission,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val updateSystemChatRoom: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: Nil JsonPut json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ putJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PutChatRoomJsonV600", 400, callContext) {
+ json.extract[PutChatRoomJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_UPDATE_ROOM)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ updatedRoom <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.updateChatRoom(chatRoomId, putJson.name, putJson.description)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update chat room", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(updatedRoom), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 5a. deleteBankChatRoom
+ staticResourceDocs += ResourceDoc(
+ deleteBankChatRoom,
+ implementedInApiVersion,
+ nameOf(deleteBankChatRoom),
+ "DELETE",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID",
+ "Delete Bank Chat Room",
+ s"""Delete a chat room. Requires the CanDeleteBankChatRoom role.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ UserHasMissingRoles,
+ ChatRoomNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ Some(List(canDeleteBankChatRoom))
+ )
+
+ lazy val deleteBankChatRoom: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: Nil JsonDelete _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, canDeleteBankChatRoom, callContext)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.deleteChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot delete chat room", 400)
+ }
+ } yield {
+ (EmptyBody, HttpCode.`204`(callContext))
+ }
+ }
+ }
+
+ // 5b. deleteSystemChatRoom
+ staticResourceDocs += ResourceDoc(
+ deleteSystemChatRoom,
+ implementedInApiVersion,
+ nameOf(deleteSystemChatRoom),
+ "DELETE",
+ "/chat-rooms/CHAT_ROOM_ID",
+ "Delete System Chat Room",
+ s"""Delete a system-level chat room. Requires the CanDeleteSystemChatRoom role.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ UserHasMissingRoles,
+ ChatRoomNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ Some(List(canDeleteSystemChatRoom))
+ )
+
+ lazy val deleteSystemChatRoom: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: Nil JsonDelete _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement("", u.userId, canDeleteSystemChatRoom, callContext)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.deleteChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot delete chat room", 400)
+ }
+ } yield {
+ (EmptyBody, HttpCode.`204`(callContext))
+ }
+ }
+ }
+
+ // 6a. archiveBankChatRoom
+ staticResourceDocs += ResourceDoc(
+ archiveBankChatRoom,
+ implementedInApiVersion,
+ nameOf(archiveBankChatRoom),
+ "PUT",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/archive-status",
+ "Archive Bank Chat Room",
+ s"""Archive a chat room. Archived rooms cannot receive new messages or participants.
+ |Requires the CanArchiveBankChatRoom role.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "gh.29.uk",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = true,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ UserHasMissingRoles,
+ ChatRoomNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ Some(List(canArchiveBankChatRoom))
+ )
+
+ lazy val archiveBankChatRoom: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "archive-status" :: Nil JsonPut _ -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, canArchiveBankChatRoom, callContext)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ archivedRoom <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.archiveChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot archive chat room", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(archivedRoom), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 6b. archiveSystemChatRoom
+ staticResourceDocs += ResourceDoc(
+ archiveSystemChatRoom,
+ implementedInApiVersion,
+ nameOf(archiveSystemChatRoom),
+ "PUT",
+ "/chat-rooms/CHAT_ROOM_ID/archive-status",
+ "Archive System Chat Room",
+ s"""Archive a system-level chat room. Archived rooms cannot receive new messages or participants.
+ |Requires the CanArchiveSystemChatRoom role.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = true,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ UserHasMissingRoles,
+ ChatRoomNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ Some(List(canArchiveSystemChatRoom))
+ )
+
+ lazy val archiveSystemChatRoom: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "archive-status" :: Nil JsonPut _ -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement("", u.userId, canArchiveSystemChatRoom, callContext)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ archivedRoom <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.archiveChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot archive chat room", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(archivedRoom), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 6c. setBankChatRoomAllUsersAreParticipants
+ staticResourceDocs += ResourceDoc(
+ setBankChatRoomAllUsersAreParticipants,
+ implementedInApiVersion,
+ nameOf(setBankChatRoomAllUsersAreParticipants),
+ "PUT",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/all-users-are-participants",
+ "Set Chat Room All Users Are Participants",
+ s"""Set whether all authenticated users are implicit participants of this chat room.
+ |
+ |If true, all users can read and send messages without needing an explicit Participant record.
+ |
+ |Requires the CanSetBankChatRoomAUAP role.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "gh.29.uk",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "username",
+ created_by_provider = "provider",
+ all_users_are_participants = true,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ UserHasMissingRoles,
+ ChatRoomNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ Some(List(canSetBankChatRoomAUAP))
+ )
+
+ lazy val setBankChatRoomAllUsersAreParticipants: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "all-users-are-participants" :: Nil JsonPut json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement(bankId.value, u.userId, canSetBankChatRoomAUAP, callContext)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ allUsersAreParticipants = (json \ "all_users_are_participants").extractOrElse[Boolean](false)
+ updatedRoom <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.setAllUsersAreParticipants(chatRoomId, allUsersAreParticipants)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update chat room", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(updatedRoom), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 6d. setSystemChatRoomAllUsersAreParticipants
+ staticResourceDocs += ResourceDoc(
+ setSystemChatRoomAllUsersAreParticipants,
+ implementedInApiVersion,
+ nameOf(setSystemChatRoomAllUsersAreParticipants),
+ "PUT",
+ "/chat-rooms/CHAT_ROOM_ID/all-users-are-participants",
+ "Set System Chat Room All Users Are Participants",
+ s"""Set whether all authenticated users are implicit participants of this system-level chat room.
+ |
+ |If true, all users can read and send messages without needing an explicit Participant record.
+ |
+ |Requires the CanSetSystemChatRoomAUAP role.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "username",
+ created_by_provider = "provider",
+ all_users_are_participants = true,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ UserHasMissingRoles,
+ ChatRoomNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ Some(List(canSetSystemChatRoomAUAP))
+ )
+
+ lazy val setSystemChatRoomAllUsersAreParticipants: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "all-users-are-participants" :: Nil JsonPut json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- NewStyle.function.hasEntitlement("", u.userId, canSetSystemChatRoomAUAP, callContext)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ allUsersAreParticipants = (json \ "all_users_are_participants").extractOrElse[Boolean](false)
+ updatedRoom <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.setAllUsersAreParticipants(chatRoomId, allUsersAreParticipants)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update chat room", 400)
+ }
+ } yield {
+ (JSONFactory600.createChatRoomJson(updatedRoom), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // ------ Batch B: Joining ------
+
+ // 7a. joinBankChatRoom
+ staticResourceDocs += ResourceDoc(
+ joinBankChatRoom,
+ implementedInApiVersion,
+ nameOf(joinBankChatRoom),
+ "POST",
+ "/banks/BANK_ID/chat-room-participants",
+ "Join Bank Chat Room",
+ s"""Join a chat room using a joining key (passed as joining_key in the JSON body).
+ |The user is added as a participant with no special permissions.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ParticipantJsonV600(
+ participant_id = "participant-id-123",
+ chat_room_id = "chat-room-id-123",
+ user_id = "user-id-123",
+ username = "robert.x.0.gh",
+ provider = "https://github.com",
+ consumer_id = "",
+ consumer_name = "",
+ permissions = List(),
+ webhook_url = "",
+ joined_at = new java.util.Date(),
+ last_read_at = new java.util.Date(),
+ is_muted = false
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJoiningKey,
+ ChatRoomIsArchived,
+ ChatRoomParticipantAlreadyExists,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val joinBankChatRoom: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-room-participants" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ joiningKey = (json \ "joining_key").extractOpt[String].getOrElse("")
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoomByJoiningKey(joiningKey)
+ } map {
+ x => unboxFullOrFail(x, callContext, InvalidJoiningKey, 404)
+ }
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomIsArchived, cc = callContext) {
+ !room.isArchived
+ }
+ existingParticipant <- Future(code.chat.ChatPermissions.isParticipant(room.chatRoomId, u.userId))
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomParticipantAlreadyExists, cc = callContext) {
+ existingParticipant.isEmpty
+ }
+ participant <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.addParticipant(room.chatRoomId, u.userId, "", List.empty, "")
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot join chat room", 400)
+ }
+ } yield {
+ (JSONFactory600.createParticipantJson(participant), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 7b. joinSystemChatRoom
+ staticResourceDocs += ResourceDoc(
+ joinSystemChatRoom,
+ implementedInApiVersion,
+ nameOf(joinSystemChatRoom),
+ "POST",
+ "/chat-room-participants",
+ "Join System Chat Room",
+ s"""Join a system-level chat room using a joining key (passed as joining_key in the JSON body).
+ |The user is added as a participant with no special permissions.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ParticipantJsonV600(
+ participant_id = "participant-id-123",
+ chat_room_id = "chat-room-id-123",
+ user_id = "user-id-123",
+ username = "robert.x.0.gh",
+ provider = "https://github.com",
+ consumer_id = "",
+ consumer_name = "",
+ permissions = List(),
+ webhook_url = "",
+ joined_at = new java.util.Date(),
+ last_read_at = new java.util.Date(),
+ is_muted = false
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJoiningKey,
+ ChatRoomIsArchived,
+ ChatRoomParticipantAlreadyExists,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val joinSystemChatRoom: OBPEndpoint = {
+ case "chat-room-participants" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ joiningKey = (json \ "joining_key").extractOpt[String].getOrElse("")
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoomByJoiningKey(joiningKey)
+ } map {
+ x => unboxFullOrFail(x, callContext, InvalidJoiningKey, 404)
+ }
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomIsArchived, cc = callContext) {
+ !room.isArchived
+ }
+ existingParticipant <- Future(code.chat.ChatPermissions.isParticipant(room.chatRoomId, u.userId))
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomParticipantAlreadyExists, cc = callContext) {
+ existingParticipant.isEmpty
+ }
+ participant <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.addParticipant(room.chatRoomId, u.userId, "", List.empty, "")
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot join chat room", 400)
+ }
+ } yield {
+ (JSONFactory600.createParticipantJson(participant), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 8a. refreshBankJoiningKey
+ staticResourceDocs += ResourceDoc(
+ refreshBankJoiningKey,
+ implementedInApiVersion,
+ nameOf(refreshBankJoiningKey),
+ "PUT",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/joining-key",
+ "Refresh Bank Chat Room Joining Key",
+ s"""Refresh the joining key for a chat room. The old key becomes invalid.
+ |Requires can_refresh_joining_key permission.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ JoiningKeyJsonV600(joining_key = "new-key-abc123"),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ InsufficientChatPermission,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val refreshBankJoiningKey: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "joining-key" :: Nil JsonPut _ -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_REFRESH_JOINING_KEY)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ updatedRoom <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.refreshJoiningKey(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot refresh joining key", 400)
+ }
+ } yield {
+ (JoiningKeyJsonV600(joining_key = updatedRoom.joiningKey), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 8b. refreshSystemJoiningKey
+ staticResourceDocs += ResourceDoc(
+ refreshSystemJoiningKey,
+ implementedInApiVersion,
+ nameOf(refreshSystemJoiningKey),
+ "PUT",
+ "/chat-rooms/CHAT_ROOM_ID/joining-key",
+ "Refresh System Chat Room Joining Key",
+ s"""Refresh the joining key for a system-level chat room. The old key becomes invalid.
+ |Requires can_refresh_joining_key permission.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ JoiningKeyJsonV600(joining_key = "new-key-abc123"),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ InsufficientChatPermission,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val refreshSystemJoiningKey: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "joining-key" :: Nil JsonPut _ -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_REFRESH_JOINING_KEY)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ updatedRoom <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.refreshJoiningKey(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot refresh joining key", 400)
+ }
+ } yield {
+ (JoiningKeyJsonV600(joining_key = updatedRoom.joiningKey), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // ------ Batch C: Participants ------
+
+ // 9a. addBankChatRoomParticipant
+ staticResourceDocs += ResourceDoc(
+ addBankChatRoomParticipant,
+ implementedInApiVersion,
+ nameOf(addBankChatRoomParticipant),
+ "POST",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/participants",
+ "Add Bank Chat Room Participant",
+ s"""Add a participant to a chat room. Requires can_manage_permissions permission.
+ |Specify either user_id or consumer_id, but not both.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostParticipantJsonV600(user_id = Some("user-id-456"), consumer_id = None, permissions = Some(List("can_delete_message")), webhook_url = None),
+ ParticipantJsonV600(
+ participant_id = "participant-id-456",
+ chat_room_id = "chat-room-id-123",
+ user_id = "user-id-456",
+ username = "ellie.y.1.gh",
+ provider = "https://github.com",
+ consumer_id = "",
+ consumer_name = "",
+ permissions = List("can_delete_message"),
+ webhook_url = "",
+ joined_at = new java.util.Date(),
+ last_read_at = new java.util.Date(),
+ is_muted = false
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ InsufficientChatPermission,
+ MustSpecifyUserIdOrConsumerId,
+ ChatRoomParticipantAlreadyExists,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val addBankChatRoomParticipant: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "participants" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostParticipantJsonV600", 400, callContext) {
+ json.extract[PostParticipantJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_MANAGE_PERMISSIONS)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ userId = postJson.user_id.getOrElse("")
+ consumerId = postJson.consumer_id.getOrElse("")
+ _ <- Helper.booleanToFuture(failMsg = MustSpecifyUserIdOrConsumerId, cc = callContext) {
+ (userId.nonEmpty || consumerId.nonEmpty) && !(userId.nonEmpty && consumerId.nonEmpty)
+ }
+ existingParticipant <- Future {
+ if (userId.nonEmpty) code.chat.ChatPermissions.isParticipant(chatRoomId, userId)
+ else code.chat.ChatPermissions.isParticipantByConsumerId(chatRoomId, consumerId)
+ }
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomParticipantAlreadyExists, cc = callContext) {
+ existingParticipant.isEmpty
+ }
+ participant <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.addParticipant(
+ chatRoomId, userId, consumerId,
+ postJson.permissions.getOrElse(List.empty),
+ postJson.webhook_url.getOrElse("")
+ )
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot add participant", 400)
+ }
+ } yield {
+ (JSONFactory600.createParticipantJson(participant), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 9b. addSystemChatRoomParticipant
+ staticResourceDocs += ResourceDoc(
+ addSystemChatRoomParticipant,
+ implementedInApiVersion,
+ nameOf(addSystemChatRoomParticipant),
+ "POST",
+ "/chat-rooms/CHAT_ROOM_ID/participants",
+ "Add System Chat Room Participant",
+ s"""Add a participant to a system-level chat room. Requires can_manage_permissions permission.
+ |Specify either user_id or consumer_id, but not both.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostParticipantJsonV600(user_id = Some("user-id-456"), consumer_id = None, permissions = Some(List("can_delete_message")), webhook_url = None),
+ ParticipantJsonV600(
+ participant_id = "participant-id-456",
+ chat_room_id = "chat-room-id-123",
+ user_id = "user-id-456",
+ username = "ellie.y.1.gh",
+ provider = "https://github.com",
+ consumer_id = "",
+ consumer_name = "",
+ permissions = List("can_delete_message"),
+ webhook_url = "",
+ joined_at = new java.util.Date(),
+ last_read_at = new java.util.Date(),
+ is_muted = false
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ InsufficientChatPermission,
+ MustSpecifyUserIdOrConsumerId,
+ ChatRoomParticipantAlreadyExists,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val addSystemChatRoomParticipant: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "participants" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostParticipantJsonV600", 400, callContext) {
+ json.extract[PostParticipantJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_MANAGE_PERMISSIONS)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ userId = postJson.user_id.getOrElse("")
+ consumerId = postJson.consumer_id.getOrElse("")
+ _ <- Helper.booleanToFuture(failMsg = MustSpecifyUserIdOrConsumerId, cc = callContext) {
+ (userId.nonEmpty || consumerId.nonEmpty) && !(userId.nonEmpty && consumerId.nonEmpty)
+ }
+ existingParticipant <- Future {
+ if (userId.nonEmpty) code.chat.ChatPermissions.isParticipant(chatRoomId, userId)
+ else code.chat.ChatPermissions.isParticipantByConsumerId(chatRoomId, consumerId)
+ }
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomParticipantAlreadyExists, cc = callContext) {
+ existingParticipant.isEmpty
+ }
+ participant <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.addParticipant(
+ chatRoomId, userId, consumerId,
+ postJson.permissions.getOrElse(List.empty),
+ postJson.webhook_url.getOrElse("")
+ )
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot add participant", 400)
+ }
+ } yield {
+ (JSONFactory600.createParticipantJson(participant), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 10a. getBankChatRoomParticipants
+ staticResourceDocs += ResourceDoc(
+ getBankChatRoomParticipants,
+ implementedInApiVersion,
+ nameOf(getBankChatRoomParticipants),
+ "GET",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/participants",
+ "Get Bank Chat Room Participants",
+ s"""Get all participants of a chat room. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ParticipantsJsonV600(participants = List(ParticipantJsonV600(
+ participant_id = "participant-id-123",
+ chat_room_id = "chat-room-id-123",
+ user_id = "user-id-123",
+ username = "robert.x.0.gh",
+ provider = "https://github.com",
+ consumer_id = "",
+ consumer_name = "",
+ permissions = List("can_update_room", "can_delete_message"),
+ webhook_url = "",
+ joined_at = new java.util.Date(),
+ last_read_at = new java.util.Date(),
+ is_muted = false
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getBankChatRoomParticipants: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "participants" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ participants <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.getParticipants(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get participants", 400)
+ }
+ } yield {
+ (JSONFactory600.createParticipantsJson(participants), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 10b. getSystemChatRoomParticipants
+ staticResourceDocs += ResourceDoc(
+ getSystemChatRoomParticipants,
+ implementedInApiVersion,
+ nameOf(getSystemChatRoomParticipants),
+ "GET",
+ "/chat-rooms/CHAT_ROOM_ID/participants",
+ "Get System Chat Room Participants",
+ s"""Get all participants of a system-level chat room. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ParticipantsJsonV600(participants = List(ParticipantJsonV600(
+ participant_id = "participant-id-123",
+ chat_room_id = "chat-room-id-123",
+ user_id = "user-id-123",
+ username = "robert.x.0.gh",
+ provider = "https://github.com",
+ consumer_id = "",
+ consumer_name = "",
+ permissions = List("can_update_room", "can_delete_message"),
+ webhook_url = "",
+ joined_at = new java.util.Date(),
+ last_read_at = new java.util.Date(),
+ is_muted = false
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getSystemChatRoomParticipants: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "participants" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ participants <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.getParticipants(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get participants", 400)
+ }
+ } yield {
+ (JSONFactory600.createParticipantsJson(participants), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 11a. updateBankParticipantPermissions
+ staticResourceDocs += ResourceDoc(
+ updateBankParticipantPermissions,
+ implementedInApiVersion,
+ nameOf(updateBankParticipantPermissions),
+ "PUT",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/participants/USER_ID",
+ "Update Bank Chat Room Participant Permissions",
+ s"""Update the permissions of a participant. Requires can_manage_permissions permission.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PutParticipantPermissionsJsonV600(permissions = List("can_delete_message", "can_update_room")),
+ ParticipantJsonV600(
+ participant_id = "participant-id-456",
+ chat_room_id = "chat-room-id-123",
+ user_id = "user-id-456",
+ username = "ellie.y.1.gh",
+ provider = "https://github.com",
+ consumer_id = "",
+ consumer_name = "",
+ permissions = List("can_delete_message", "can_update_room"),
+ webhook_url = "",
+ joined_at = new java.util.Date(),
+ last_read_at = new java.util.Date(),
+ is_muted = false
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ InsufficientChatPermission,
+ ChatRoomParticipantNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val updateBankParticipantPermissions: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "participants" :: targetUserId :: Nil JsonPut json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ putJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PutParticipantPermissionsJsonV600", 400, callContext) {
+ json.extract[PutParticipantPermissionsJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_MANAGE_PERMISSIONS)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, targetUserId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomParticipantNotFound, 404)
+ }
+ updatedParticipant <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.updateParticipantPermissions(chatRoomId, targetUserId, putJson.permissions)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update participant permissions", 400)
+ }
+ } yield {
+ (JSONFactory600.createParticipantJson(updatedParticipant), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 11b. updateSystemParticipantPermissions
+ staticResourceDocs += ResourceDoc(
+ updateSystemParticipantPermissions,
+ implementedInApiVersion,
+ nameOf(updateSystemParticipantPermissions),
+ "PUT",
+ "/chat-rooms/CHAT_ROOM_ID/participants/USER_ID",
+ "Update System Chat Room Participant Permissions",
+ s"""Update the permissions of a participant in a system-level chat room. Requires can_manage_permissions permission.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PutParticipantPermissionsJsonV600(permissions = List("can_delete_message", "can_update_room")),
+ ParticipantJsonV600(
+ participant_id = "participant-id-456",
+ chat_room_id = "chat-room-id-123",
+ user_id = "user-id-456",
+ username = "ellie.y.1.gh",
+ provider = "https://github.com",
+ consumer_id = "",
+ consumer_name = "",
+ permissions = List("can_delete_message", "can_update_room"),
+ webhook_url = "",
+ joined_at = new java.util.Date(),
+ last_read_at = new java.util.Date(),
+ is_muted = false
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ InsufficientChatPermission,
+ ChatRoomParticipantNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val updateSystemParticipantPermissions: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "participants" :: targetUserId :: Nil JsonPut json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ putJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PutParticipantPermissionsJsonV600", 400, callContext) {
+ json.extract[PutParticipantPermissionsJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_MANAGE_PERMISSIONS)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, targetUserId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomParticipantNotFound, 404)
+ }
+ updatedParticipant <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.updateParticipantPermissions(chatRoomId, targetUserId, putJson.permissions)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot update participant permissions", 400)
+ }
+ } yield {
+ (JSONFactory600.createParticipantJson(updatedParticipant), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 12a. removeBankChatRoomParticipant
+ staticResourceDocs += ResourceDoc(
+ removeBankChatRoomParticipant,
+ implementedInApiVersion,
+ nameOf(removeBankChatRoomParticipant),
+ "DELETE",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/participants/USER_ID",
+ "Remove Bank Chat Room Participant",
+ s"""Remove a participant from a chat room. Requires can_remove_participant permission, or the user can remove themselves.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ InsufficientChatPermission,
+ ChatRoomParticipantNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val removeBankChatRoomParticipant: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "participants" :: targetUserId :: Nil JsonDelete _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ // Self-removal is allowed; otherwise need can_remove_participant
+ _ <- if (u.userId == targetUserId) {
+ Future.successful(Full(()))
+ } else {
+ Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_REMOVE_PARTICIPANT)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, targetUserId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomParticipantNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.removeParticipant(chatRoomId, targetUserId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot remove participant", 400)
+ }
+ } yield {
+ (EmptyBody, HttpCode.`204`(callContext))
+ }
+ }
+ }
+
+ // 12b. removeSystemChatRoomParticipant
+ staticResourceDocs += ResourceDoc(
+ removeSystemChatRoomParticipant,
+ implementedInApiVersion,
+ nameOf(removeSystemChatRoomParticipant),
+ "DELETE",
+ "/chat-rooms/CHAT_ROOM_ID/participants/USER_ID",
+ "Remove System Chat Room Participant",
+ s"""Remove a participant from a system-level chat room. Requires can_remove_participant permission, or the user can remove themselves.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ InsufficientChatPermission,
+ ChatRoomParticipantNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val removeSystemChatRoomParticipant: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "participants" :: targetUserId :: Nil JsonDelete _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- if (u.userId == targetUserId) {
+ Future.successful(Full(()))
+ } else {
+ Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_REMOVE_PARTICIPANT)
+ } map {
+ x => unboxFullOrFail(x, callContext, InsufficientChatPermission, 403)
+ }
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, targetUserId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomParticipantNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.removeParticipant(chatRoomId, targetUserId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot remove participant", 400)
+ }
+ } yield {
+ (EmptyBody, HttpCode.`204`(callContext))
+ }
+ }
+ }
+
+ // ------ Batch D: Messages ------
+
+ // 13a. sendBankChatMessage
+ staticResourceDocs += ResourceDoc(
+ sendBankChatMessage,
+ implementedInApiVersion,
+ nameOf(sendBankChatMessage),
+ "POST",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages",
+ "Send Bank Chat Message",
+ s"""Send a message in a chat room. The current user must be a participant and the room must not be archived.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostChatMessageJsonV600(content = "Hello everyone!", message_type = Some("text"), mentioned_user_ids = None, reply_to_message_id = None, thread_id = None),
+ ChatMessageJsonV600(
+ chat_message_id = "msg-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "Hello everyone!",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatRoomIsArchived,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val sendBankChatMessage: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostChatMessageJsonV600", 400, callContext) {
+ json.extract[PostChatMessageJsonV600]
+ }
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomIsArchived, cc = callContext) {
+ !room.isArchived
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.createMessage(
+ chatRoomId,
+ u.userId,
+ "",
+ postJson.content,
+ postJson.message_type.getOrElse("text"),
+ postJson.mentioned_user_ids.getOrElse(List.empty),
+ postJson.reply_to_message_id.getOrElse(""),
+ postJson.thread_id.getOrElse("")
+ )
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot send message", 400)
+ }
+ } yield {
+ code.chat.ChatEventPublisher.afterCreate(msg, u.name, u.provider, "")
+ (JSONFactory600.createChatMessageJson(msg, List.empty), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 13b. sendSystemChatMessage
+ staticResourceDocs += ResourceDoc(
+ sendSystemChatMessage,
+ implementedInApiVersion,
+ nameOf(sendSystemChatMessage),
+ "POST",
+ "/chat-rooms/CHAT_ROOM_ID/messages",
+ "Send System Chat Message",
+ s"""Send a message in a system-level chat room. The current user must be a participant and the room must not be archived.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostChatMessageJsonV600(content = "Hello everyone!", message_type = Some("text"), mentioned_user_ids = None, reply_to_message_id = None, thread_id = None),
+ ChatMessageJsonV600(
+ chat_message_id = "msg-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "Hello everyone!",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatRoomIsArchived,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val sendSystemChatMessage: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostChatMessageJsonV600", 400, callContext) {
+ json.extract[PostChatMessageJsonV600]
+ }
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomIsArchived, cc = callContext) {
+ !room.isArchived
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.createMessage(
+ chatRoomId,
+ u.userId,
+ "",
+ postJson.content,
+ postJson.message_type.getOrElse("text"),
+ postJson.mentioned_user_ids.getOrElse(List.empty),
+ postJson.reply_to_message_id.getOrElse(""),
+ postJson.thread_id.getOrElse("")
+ )
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot send message", 400)
+ }
+ } yield {
+ code.chat.ChatEventPublisher.afterCreate(msg, u.name, u.provider, "")
+ (JSONFactory600.createChatMessageJson(msg, List.empty), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 14a. getBankChatMessages
+ staticResourceDocs += ResourceDoc(
+ getBankChatMessages,
+ implementedInApiVersion,
+ nameOf(getBankChatMessages),
+ "GET",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages",
+ "Get Bank Chat Messages",
+ s"""Get messages in a chat room.
+ |
+ |${getObpApiRoot}/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages?limit=50&offset=0&from_date=$DateWithMsExampleString&to_date=$DateWithMsExampleString
+ |
+ |The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatMessagesJsonV600(messages = List(ChatMessageJsonV600(
+ chat_message_id = "msg-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "Hello everyone!",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List(ReactionSummaryJsonV600(emoji = "thumbsup", count = 2, user_ids = List("user-1", "user-2")))
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getBankChatMessages: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ limitParam = ObpS.param("limit").map(_.toInt).getOrElse(50)
+ offsetParam = ObpS.param("offset").map(_.toInt).getOrElse(0)
+ fromDate = ObpS.param("from_date").flatMap(parseObpStandardDate(_).toOption).getOrElse(theEpochTime)
+ toDate = ObpS.param("to_date").flatMap(parseObpStandardDate(_).toOption).getOrElse(APIUtil.DefaultToDate)
+ (messageRows, reactionRows) = code.chat.DoobieChatMessageQueries.getMessagesWithReactions(chatRoomId, fromDate, toDate, limitParam, offsetParam)
+ } yield {
+ (JSONFactory600.createChatMessagesJsonFromRows(messageRows, reactionRows), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 14b. getSystemChatMessages
+ staticResourceDocs += ResourceDoc(
+ getSystemChatMessages,
+ implementedInApiVersion,
+ nameOf(getSystemChatMessages),
+ "GET",
+ "/chat-rooms/CHAT_ROOM_ID/messages",
+ "Get System Chat Messages",
+ s"""Get messages in a system-level chat room.
+ |
+ |${getObpApiRoot}/chat-rooms/CHAT_ROOM_ID/messages?limit=50&offset=0&from_date=$DateWithMsExampleString&to_date=$DateWithMsExampleString
+ |
+ |The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatMessagesJsonV600(messages = List(ChatMessageJsonV600(
+ chat_message_id = "msg-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "Hello everyone!",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List(ReactionSummaryJsonV600(emoji = "thumbsup", count = 2, user_ids = List("user-1", "user-2")))
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getSystemChatMessages: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ limitParam = ObpS.param("limit").map(_.toInt).getOrElse(50)
+ offsetParam = ObpS.param("offset").map(_.toInt).getOrElse(0)
+ fromDate = ObpS.param("from_date").flatMap(parseObpStandardDate(_).toOption).getOrElse(theEpochTime)
+ toDate = ObpS.param("to_date").flatMap(parseObpStandardDate(_).toOption).getOrElse(APIUtil.DefaultToDate)
+ (messageRows, reactionRows) = code.chat.DoobieChatMessageQueries.getMessagesWithReactions(chatRoomId, fromDate, toDate, limitParam, offsetParam)
+ } yield {
+ (JSONFactory600.createChatMessagesJsonFromRows(messageRows, reactionRows), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 15a. getBankChatMessage
+ staticResourceDocs += ResourceDoc(
+ getBankChatMessage,
+ implementedInApiVersion,
+ nameOf(getBankChatMessage),
+ "GET",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID",
+ "Get Bank Chat Message",
+ s"""Get a specific message by ID. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatMessageJsonV600(
+ chat_message_id = "msg-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "Hello everyone!",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getBankChatMessage: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ reactions <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.getReactions(chatMessageId).openOr(List.empty)
+ }
+ } yield {
+ (JSONFactory600.createChatMessageJson(msg, reactions), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 15b. getSystemChatMessage
+ staticResourceDocs += ResourceDoc(
+ getSystemChatMessage,
+ implementedInApiVersion,
+ nameOf(getSystemChatMessage),
+ "GET",
+ "/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID",
+ "Get System Chat Message",
+ s"""Get a specific message by ID in a system-level chat room. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatMessageJsonV600(
+ chat_message_id = "msg-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "Hello everyone!",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getSystemChatMessage: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ reactions <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.getReactions(chatMessageId).openOr(List.empty)
+ }
+ } yield {
+ (JSONFactory600.createChatMessageJson(msg, reactions), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 16a. editBankChatMessage
+ staticResourceDocs += ResourceDoc(
+ editBankChatMessage,
+ implementedInApiVersion,
+ nameOf(editBankChatMessage),
+ "PUT",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID",
+ "Edit Bank Chat Message",
+ s"""Edit a message. Only the sender can edit their own messages.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PutChatMessageJsonV600(content = "Updated message content"),
+ ChatMessageJsonV600(
+ chat_message_id = "msg-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "Updated message content",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ CannotEditOthersMessage,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val editBankChatMessage: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: Nil JsonPut json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ putJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PutChatMessageJsonV600", 400, callContext) {
+ json.extract[PutChatMessageJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ _ <- Helper.booleanToFuture(failMsg = CannotEditOthersMessage, cc = callContext) {
+ msg.senderUserId == u.userId
+ }
+ updatedMsg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.updateMessage(chatMessageId, putJson.content)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot edit message", 400)
+ }
+ reactions <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.getReactions(chatMessageId).openOr(List.empty)
+ }
+ } yield {
+ code.chat.ChatEventPublisher.afterUpdate(updatedMsg, u.name, u.provider, "")
+ (JSONFactory600.createChatMessageJson(updatedMsg, reactions), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 16b. editSystemChatMessage
+ staticResourceDocs += ResourceDoc(
+ editSystemChatMessage,
+ implementedInApiVersion,
+ nameOf(editSystemChatMessage),
+ "PUT",
+ "/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID",
+ "Edit System Chat Message",
+ s"""Edit a message in a system-level chat room. Only the sender can edit their own messages.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PutChatMessageJsonV600(content = "Updated message content"),
+ ChatMessageJsonV600(
+ chat_message_id = "msg-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "Updated message content",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ CannotEditOthersMessage,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val editSystemChatMessage: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: Nil JsonPut json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ putJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PutChatMessageJsonV600", 400, callContext) {
+ json.extract[PutChatMessageJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ _ <- Helper.booleanToFuture(failMsg = CannotEditOthersMessage, cc = callContext) {
+ msg.senderUserId == u.userId
+ }
+ updatedMsg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.updateMessage(chatMessageId, putJson.content)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot edit message", 400)
+ }
+ reactions <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.getReactions(chatMessageId).openOr(List.empty)
+ }
+ } yield {
+ code.chat.ChatEventPublisher.afterUpdate(updatedMsg, u.name, u.provider, "")
+ (JSONFactory600.createChatMessageJson(updatedMsg, reactions), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 17a. deleteBankChatMessage
+ staticResourceDocs += ResourceDoc(
+ deleteBankChatMessage,
+ implementedInApiVersion,
+ nameOf(deleteBankChatMessage),
+ "DELETE",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID",
+ "Delete Bank Chat Message",
+ s"""Soft-delete a message. The sender can delete their own messages, or a participant with can_delete_message permission can delete any message.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ CannotDeleteMessage,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val deleteBankChatMessage: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: Nil JsonDelete _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ _ <- if (msg.senderUserId == u.userId) {
+ Future.successful(Full(()))
+ } else {
+ Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_DELETE_MESSAGE)
+ } map {
+ x => unboxFullOrFail(x, callContext, CannotDeleteMessage, 403)
+ }
+ }
+ deletedMsg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.softDeleteMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot delete message", 400)
+ }
+ } yield {
+ code.chat.ChatEventPublisher.afterDelete(deletedMsg, u.name, u.provider, "")
+ (EmptyBody, HttpCode.`204`(callContext))
+ }
+ }
+ }
+
+ // 17b. deleteSystemChatMessage
+ staticResourceDocs += ResourceDoc(
+ deleteSystemChatMessage,
+ implementedInApiVersion,
+ nameOf(deleteSystemChatMessage),
+ "DELETE",
+ "/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID",
+ "Delete System Chat Message",
+ s"""Soft-delete a message in a system-level chat room. The sender can delete their own messages, or a participant with can_delete_message permission can delete any message.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ CannotDeleteMessage,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val deleteSystemChatMessage: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: Nil JsonDelete _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ _ <- if (msg.senderUserId == u.userId) {
+ Future.successful(Full(()))
+ } else {
+ Future {
+ code.chat.ChatPermissions.checkParticipantPermission(chatRoomId, u.userId, code.chat.ChatPermissions.CAN_DELETE_MESSAGE)
+ } map {
+ x => unboxFullOrFail(x, callContext, CannotDeleteMessage, 403)
+ }
+ }
+ deletedMsg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.softDeleteMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot delete message", 400)
+ }
+ } yield {
+ code.chat.ChatEventPublisher.afterDelete(deletedMsg, u.name, u.provider, "")
+ (EmptyBody, HttpCode.`204`(callContext))
+ }
+ }
+ }
+
+ // ------ Batch E: Threads ------
+
+ // 18a. getBankThreadReplies
+ staticResourceDocs += ResourceDoc(
+ getBankThreadReplies,
+ implementedInApiVersion,
+ nameOf(getBankThreadReplies),
+ "GET",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/thread",
+ "Get Bank Thread Replies",
+ s"""Get all replies in a message thread. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatMessagesJsonV600(messages = List(ChatMessageJsonV600(
+ chat_message_id = "reply-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-456",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "This is a reply",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "msg-id-123",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getBankThreadReplies: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "thread" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ replies <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getThreadReplies(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get thread replies", 400)
+ }
+ allReactions <- Future {
+ replies.map { msg =>
+ val reactions = code.chat.ReactionTrait.reactionProvider.vend.getReactions(msg.chatMessageId).openOr(List.empty)
+ (msg.chatMessageId, reactions)
+ }.toMap
+ }
+ } yield {
+ (JSONFactory600.createChatMessagesJson(replies, allReactions), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 18b. getSystemThreadReplies
+ staticResourceDocs += ResourceDoc(
+ getSystemThreadReplies,
+ implementedInApiVersion,
+ nameOf(getSystemThreadReplies),
+ "GET",
+ "/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/thread",
+ "Get System Thread Replies",
+ s"""Get all replies in a message thread in a system-level chat room. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatMessagesJsonV600(messages = List(ChatMessageJsonV600(
+ chat_message_id = "reply-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-456",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "This is a reply",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "msg-id-123",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getSystemThreadReplies: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "thread" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ replies <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getThreadReplies(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get thread replies", 400)
+ }
+ allReactions <- Future {
+ replies.map { msg =>
+ val reactions = code.chat.ReactionTrait.reactionProvider.vend.getReactions(msg.chatMessageId).openOr(List.empty)
+ (msg.chatMessageId, reactions)
+ }.toMap
+ }
+ } yield {
+ (JSONFactory600.createChatMessagesJson(replies, allReactions), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 19a. replyInBankThread
+ staticResourceDocs += ResourceDoc(
+ replyInBankThread,
+ implementedInApiVersion,
+ nameOf(replyInBankThread),
+ "POST",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/thread",
+ "Reply In Bank Thread",
+ s"""Reply to a message in a thread. The current user must be a participant and the room must not be archived.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostChatMessageJsonV600(content = "This is a thread reply", message_type = Some("text"), mentioned_user_ids = None, reply_to_message_id = None, thread_id = None),
+ ChatMessageJsonV600(
+ chat_message_id = "reply-id-456",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "This is a thread reply",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "msg-id-123",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatRoomIsArchived,
+ ChatMessageNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val replyInBankThread: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "thread" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostChatMessageJsonV600", 400, callContext) {
+ json.extract[PostChatMessageJsonV600]
+ }
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomIsArchived, cc = callContext) {
+ !room.isArchived
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.createMessage(
+ chatRoomId,
+ u.userId,
+ "",
+ postJson.content,
+ postJson.message_type.getOrElse("text"),
+ postJson.mentioned_user_ids.getOrElse(List.empty),
+ postJson.reply_to_message_id.getOrElse(""),
+ chatMessageId // threadId is the parent message ID
+ )
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot send thread reply", 400)
+ }
+ } yield {
+ code.chat.ChatEventPublisher.afterCreate(msg, u.name, u.provider, "")
+ (JSONFactory600.createChatMessageJson(msg, List.empty), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 19b. replyInSystemThread
+ staticResourceDocs += ResourceDoc(
+ replyInSystemThread,
+ implementedInApiVersion,
+ nameOf(replyInSystemThread),
+ "POST",
+ "/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/thread",
+ "Reply In System Thread",
+ s"""Reply to a message in a thread in a system-level chat room. The current user must be a participant and the room must not be archived.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostChatMessageJsonV600(content = "This is a thread reply", message_type = Some("text"), mentioned_user_ids = None, reply_to_message_id = None, thread_id = None),
+ ChatMessageJsonV600(
+ chat_message_id = "reply-id-456",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-123",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "This is a thread reply",
+ message_type = "text",
+ mentioned_user_ids = List(),
+ reply_to_message_id = "",
+ thread_id = "msg-id-123",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatRoomIsArchived,
+ ChatMessageNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val replyInSystemThread: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "thread" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostChatMessageJsonV600", 400, callContext) {
+ json.extract[PostChatMessageJsonV600]
+ }
+ room <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Helper.booleanToFuture(failMsg = ChatRoomIsArchived, cc = callContext) {
+ !room.isArchived
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ msg <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.createMessage(
+ chatRoomId,
+ u.userId,
+ "",
+ postJson.content,
+ postJson.message_type.getOrElse("text"),
+ postJson.mentioned_user_ids.getOrElse(List.empty),
+ postJson.reply_to_message_id.getOrElse(""),
+ chatMessageId // threadId is the parent message ID
+ )
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot send thread reply", 400)
+ }
+ } yield {
+ code.chat.ChatEventPublisher.afterCreate(msg, u.name, u.provider, "")
+ (JSONFactory600.createChatMessageJson(msg, List.empty), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // ------ Batch F: Reactions ------
+
+ // 20a. addBankReaction
+ staticResourceDocs += ResourceDoc(
+ addBankReaction,
+ implementedInApiVersion,
+ nameOf(addBankReaction),
+ "POST",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions",
+ "Add Bank Reaction",
+ s"""Add a reaction (emoji) to a message. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostReactionJsonV600(emoji = "thumbsup"),
+ ReactionJsonV600(
+ reaction_id = "reaction-id-123",
+ chat_message_id = "msg-id-123",
+ user_id = "user-id-123",
+ username = "robert.x.0.gh",
+ provider = "https://github.com",
+ emoji = "thumbsup",
+ created_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ ReactionAlreadyExists,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val addBankReaction: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "reactions" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostReactionJsonV600", 400, callContext) {
+ json.extract[PostReactionJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ existingReaction <- Future(code.chat.ReactionTrait.reactionProvider.vend.getReaction(chatMessageId, u.userId, postJson.emoji))
+ _ <- Helper.booleanToFuture(failMsg = ReactionAlreadyExists, cc = callContext) {
+ existingReaction.isEmpty
+ }
+ reaction <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.addReaction(chatMessageId, u.userId, postJson.emoji)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot add reaction", 400)
+ }
+ } yield {
+ (JSONFactory600.createReactionJson(reaction), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 20b. addSystemReaction
+ staticResourceDocs += ResourceDoc(
+ addSystemReaction,
+ implementedInApiVersion,
+ nameOf(addSystemReaction),
+ "POST",
+ "/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions",
+ "Add System Reaction",
+ s"""Add a reaction (emoji) to a message in a system-level chat room. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ PostReactionJsonV600(emoji = "thumbsup"),
+ ReactionJsonV600(
+ reaction_id = "reaction-id-123",
+ chat_message_id = "msg-id-123",
+ user_id = "user-id-123",
+ username = "robert.x.0.gh",
+ provider = "https://github.com",
+ emoji = "thumbsup",
+ created_at = new java.util.Date()
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ InvalidJsonFormat,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ ReactionAlreadyExists,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val addSystemReaction: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "reactions" :: Nil JsonPost json -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ postJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostReactionJsonV600", 400, callContext) {
+ json.extract[PostReactionJsonV600]
+ }
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ existingReaction <- Future(code.chat.ReactionTrait.reactionProvider.vend.getReaction(chatMessageId, u.userId, postJson.emoji))
+ _ <- Helper.booleanToFuture(failMsg = ReactionAlreadyExists, cc = callContext) {
+ existingReaction.isEmpty
+ }
+ reaction <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.addReaction(chatMessageId, u.userId, postJson.emoji)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot add reaction", 400)
+ }
+ } yield {
+ (JSONFactory600.createReactionJson(reaction), HttpCode.`201`(callContext))
+ }
+ }
+ }
+
+ // 21a. removeBankReaction
+ staticResourceDocs += ResourceDoc(
+ removeBankReaction,
+ implementedInApiVersion,
+ nameOf(removeBankReaction),
+ "DELETE",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions/EMOJI",
+ "Remove Bank Reaction",
+ s"""Remove your own reaction from a message.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ ReactionNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val removeBankReaction: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "reactions" :: emoji :: Nil JsonDelete _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ decodedEmoji = URLDecoder.decode(emoji, StandardCharsets.UTF_8.name())
+ existingReaction <- Future(code.chat.ReactionTrait.reactionProvider.vend.getReaction(chatMessageId, u.userId, decodedEmoji))
+ _ <- Helper.booleanToFuture(failMsg = ReactionNotFound, cc = callContext) {
+ existingReaction.isDefined
+ }
+ _ <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.removeReaction(chatMessageId, u.userId, decodedEmoji)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot remove reaction", 400)
+ }
+ } yield {
+ (EmptyBody, HttpCode.`204`(callContext))
+ }
+ }
+ }
+
+ // 21b. removeSystemReaction
+ staticResourceDocs += ResourceDoc(
+ removeSystemReaction,
+ implementedInApiVersion,
+ nameOf(removeSystemReaction),
+ "DELETE",
+ "/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions/EMOJI",
+ "Remove System Reaction",
+ s"""Remove your own reaction from a message in a system-level chat room.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ ReactionNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val removeSystemReaction: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "reactions" :: emoji :: Nil JsonDelete _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ decodedEmoji = URLDecoder.decode(emoji, StandardCharsets.UTF_8.name())
+ existingReaction <- Future(code.chat.ReactionTrait.reactionProvider.vend.getReaction(chatMessageId, u.userId, decodedEmoji))
+ _ <- Helper.booleanToFuture(failMsg = ReactionNotFound, cc = callContext) {
+ existingReaction.isDefined
+ }
+ _ <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.removeReaction(chatMessageId, u.userId, decodedEmoji)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot remove reaction", 400)
+ }
+ } yield {
+ (EmptyBody, HttpCode.`204`(callContext))
+ }
+ }
+ }
+
+ // 22a. getBankReactions
+ staticResourceDocs += ResourceDoc(
+ getBankReactions,
+ implementedInApiVersion,
+ nameOf(getBankReactions),
+ "GET",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions",
+ "Get Bank Reactions",
+ s"""Get all reactions for a message. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ReactionsJsonV600(reactions = List(ReactionJsonV600(
+ reaction_id = "reaction-id-123",
+ chat_message_id = "msg-id-123",
+ user_id = "user-id-123",
+ username = "robert.x.0.gh",
+ provider = "https://github.com",
+ emoji = "thumbsup",
+ created_at = new java.util.Date()
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getBankReactions: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "reactions" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ reactions <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.getReactions(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get reactions", 400)
+ }
+ } yield {
+ (JSONFactory600.createReactionsJson(reactions), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 22b. getSystemReactions
+ staticResourceDocs += ResourceDoc(
+ getSystemReactions,
+ implementedInApiVersion,
+ nameOf(getSystemReactions),
+ "GET",
+ "/chat-rooms/CHAT_ROOM_ID/messages/CHAT_MESSAGE_ID/reactions",
+ "Get System Reactions",
+ s"""Get all reactions for a message in a system-level chat room. The current user must be a participant.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ReactionsJsonV600(reactions = List(ReactionJsonV600(
+ reaction_id = "reaction-id-123",
+ chat_message_id = "msg-id-123",
+ user_id = "user-id-123",
+ username = "robert.x.0.gh",
+ provider = "https://github.com",
+ emoji = "thumbsup",
+ created_at = new java.util.Date()
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ ChatMessageNotFound,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getSystemReactions: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "messages" :: chatMessageId :: "reactions" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMessage(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatMessageNotFound, 404)
+ }
+ reactions <- Future {
+ code.chat.ReactionTrait.reactionProvider.vend.getReactions(chatMessageId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get reactions", 400)
+ }
+ } yield {
+ (JSONFactory600.createReactionsJson(reactions), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // ------ Batch G: Typing ------
+
+ // 23a. signalBankTyping
+ staticResourceDocs += ResourceDoc(
+ signalBankTyping,
+ implementedInApiVersion,
+ nameOf(signalBankTyping),
+ "PUT",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/typing-indicators",
+ "Signal Bank Typing",
+ s"""Signal that the current user is typing in a chat room. The typing indicator expires after 5 seconds.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val signalBankTyping: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "typing-indicators" :: Nil JsonPut _ -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ val key = s"chat_typing_${chatRoomId}_${u.userId}"
+ Redis.use(code.api.JedisMethod.SET, key, Some(5), Some("1"))
+ code.chat.ChatEventPublisher.afterTyping(chatRoomId, u.userId, u.name, u.provider, true)
+ }
+ } yield {
+ (EmptyBody, HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 23b. signalSystemTyping
+ staticResourceDocs += ResourceDoc(
+ signalSystemTyping,
+ implementedInApiVersion,
+ nameOf(signalSystemTyping),
+ "PUT",
+ "/chat-rooms/CHAT_ROOM_ID/typing-indicators",
+ "Signal System Typing",
+ s"""Signal that the current user is typing in a system-level chat room. The typing indicator expires after 5 seconds.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ EmptyBody,
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val signalSystemTyping: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "typing-indicators" :: Nil JsonPut _ -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ _ <- Future {
+ val key = s"chat_typing_${chatRoomId}_${u.userId}"
+ Redis.use(code.api.JedisMethod.SET, key, Some(5), Some("1"))
+ code.chat.ChatEventPublisher.afterTyping(chatRoomId, u.userId, u.name, u.provider, true)
+ }
+ } yield {
+ (EmptyBody, HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 24a. getBankTypingUsers
+ staticResourceDocs += ResourceDoc(
+ getBankTypingUsers,
+ implementedInApiVersion,
+ nameOf(getBankTypingUsers),
+ "GET",
+ "/banks/BANK_ID/chat-rooms/CHAT_ROOM_ID/typing-indicators",
+ "Get Bank Typing Users",
+ s"""Get the list of users currently typing in a chat room.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ TypingUsersJsonV600(users = List(TypingUserJsonV600(user_id = "user-id-123", username = "robert.x.0.gh", provider = "https://github.com"))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getBankTypingUsers: OBPEndpoint = {
+ case "banks" :: BankId(bankId) :: "chat-rooms" :: chatRoomId :: "typing-indicators" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ participants <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.getParticipants(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get participants", 400)
+ }
+ typingUsers <- Future {
+ participants.filter(_.userId.nonEmpty).flatMap { p =>
+ val key = s"chat_typing_${chatRoomId}_${p.userId}"
+ try {
+ Redis.use(code.api.JedisMethod.GET, key) match {
+ case Some(_) =>
+ val typingUser = code.users.Users.users.vend.getUserByUserId(p.userId)
+ Some(TypingUserJsonV600(user_id = p.userId, username = typingUser.map(_.name).getOrElse(""), provider = typingUser.map(_.provider).getOrElse("")))
+ case None => None
+ }
+ } catch {
+ case _: Throwable => None
+ }
+ }
+ }
+ } yield {
+ (TypingUsersJsonV600(users = typingUsers), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 24b. getSystemTypingUsers
+ staticResourceDocs += ResourceDoc(
+ getSystemTypingUsers,
+ implementedInApiVersion,
+ nameOf(getSystemTypingUsers),
+ "GET",
+ "/chat-rooms/CHAT_ROOM_ID/typing-indicators",
+ "Get System Typing Users",
+ s"""Get the list of users currently typing in a system-level chat room.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ TypingUsersJsonV600(users = List(TypingUserJsonV600(user_id = "user-id-123", username = "robert.x.0.gh", provider = "https://github.com"))),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getSystemTypingUsers: OBPEndpoint = {
+ case "chat-rooms" :: chatRoomId :: "typing-indicators" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ participants <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.getParticipants(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get participants", 400)
+ }
+ typingUsers <- Future {
+ participants.filter(_.userId.nonEmpty).flatMap { p =>
+ val key = s"chat_typing_${chatRoomId}_${p.userId}"
+ try {
+ Redis.use(code.api.JedisMethod.GET, key) match {
+ case Some(_) =>
+ val typingUser = code.users.Users.users.vend.getUserByUserId(p.userId)
+ Some(TypingUserJsonV600(user_id = p.userId, username = typingUser.map(_.name).getOrElse(""), provider = typingUser.map(_.provider).getOrElse("")))
+ case None => None
+ }
+ } catch {
+ case _: Throwable => None
+ }
+ }
+ }
+ } yield {
+ (TypingUsersJsonV600(users = typingUsers), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // ------ Batch H: User-Level ------
+
+ // 25. getMyChatRooms
+ staticResourceDocs += ResourceDoc(
+ getMyChatRooms,
+ implementedInApiVersion,
+ nameOf(getMyChatRooms),
+ "GET",
+ "/users/current/chat-rooms",
+ "Get My Chat Rooms",
+ s"""Get all chat rooms the current user is a participant of, across all banks and system-level rooms.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatRoomsJsonV600(chat_rooms = List(ChatRoomJsonV600(
+ chat_room_id = "chat-room-id-123",
+ bank_id = "gh.29.uk",
+ name = "General Discussion",
+ description = "A place to discuss general topics",
+ joining_key = "abc123key",
+ created_by = "user-id-123",
+ created_by_username = "robert.x.0.gh",
+ created_by_provider = "https://github.com",
+ all_users_are_participants = false,
+ is_archived = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date()
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getMyChatRooms: OBPEndpoint = {
+ case "users" :: "current" :: "chat-rooms" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ participantRecords <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.getParticipantRoomsByUserId(u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get participant records", 400)
+ }
+ rooms <- Future {
+ participantRecords.flatMap { p =>
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(p.chatRoomId).toList
+ }
+ }
+ } yield {
+ (JSONFactory600.createChatRoomsJson(rooms), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 26. getMyUnreadCounts
+ staticResourceDocs += ResourceDoc(
+ getMyUnreadCounts,
+ implementedInApiVersion,
+ nameOf(getMyUnreadCounts),
+ "GET",
+ "/users/current/chat-rooms/unread",
+ "Get My Unread Counts",
+ s"""Get unread message counts for all chat rooms the current user is a participant of.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ UnreadCountsJsonV600(unread_counts = List(UnreadCountJsonV600(chat_room_id = "chat-room-id-123", unread_count = 5))),
+ List(
+ $AuthenticatedUserIsRequired,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getMyUnreadCounts: OBPEndpoint = {
+ case "users" :: "current" :: "chat-rooms" :: "unread" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ participantRecords <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.getParticipantRoomsByUserId(u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get participant records", 400)
+ }
+ unreadCounts <- Future {
+ participantRecords.flatMap { p =>
+ val count = code.chat.ChatMessageTrait.chatMessageProvider.vend.getUnreadCount(p.chatRoomId, p.lastReadAt)
+ count.toList.map(c => UnreadCountJsonV600(chat_room_id = p.chatRoomId, unread_count = c))
+ }
+ }
+ } yield {
+ (UnreadCountsJsonV600(unread_counts = unreadCounts), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 27. markChatRoomRead
+ staticResourceDocs += ResourceDoc(
+ markChatRoomRead,
+ implementedInApiVersion,
+ nameOf(markChatRoomRead),
+ "PUT",
+ "/users/current/chat-rooms/CHAT_ROOM_ID/read-marker",
+ "Mark Chat Room Read",
+ s"""Mark all messages in a chat room as read for the current user by updating lastReadAt to now.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ParticipantJsonV600(
+ participant_id = "participant-id-123",
+ chat_room_id = "chat-room-id-123",
+ user_id = "user-id-123",
+ username = "robert.x.0.gh",
+ provider = "https://github.com",
+ consumer_id = "",
+ consumer_name = "",
+ permissions = List(),
+ webhook_url = "",
+ joined_at = new java.util.Date(),
+ last_read_at = new java.util.Date(),
+ is_muted = false
+ ),
+ List(
+ $AuthenticatedUserIsRequired,
+ ChatRoomNotFound,
+ NotChatRoomParticipant,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val markChatRoomRead: OBPEndpoint = {
+ case "users" :: "current" :: "chat-rooms" :: chatRoomId :: "read-marker" :: Nil JsonPut _ -> _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ _ <- Future {
+ code.chat.ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId)
+ } map {
+ x => unboxFullOrFail(x, callContext, ChatRoomNotFound, 404)
+ }
+ _ <- Future {
+ code.chat.ChatPermissions.isParticipant(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, NotChatRoomParticipant, 403)
+ }
+ updatedParticipant <- Future {
+ code.chat.ParticipantTrait.participantProvider.vend.updateLastReadAt(chatRoomId, u.userId)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot mark as read", 400)
+ }
+ } yield {
+ (JSONFactory600.createParticipantJson(updatedParticipant), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
+ // 28. getMyMentions
+ staticResourceDocs += ResourceDoc(
+ getMyMentions,
+ implementedInApiVersion,
+ nameOf(getMyMentions),
+ "GET",
+ "/users/current/mentions",
+ "Get My Mentions",
+ s"""Get messages where the current user is mentioned. Supports limit and offset query parameters.
+ |
+ |Authentication is Required
+ |
+ |""".stripMargin,
+ EmptyBody,
+ ChatMessagesJsonV600(messages = List(ChatMessageJsonV600(
+ chat_message_id = "msg-id-123",
+ chat_room_id = "chat-room-id-123",
+ sender_user_id = "user-id-456",
+ sender_consumer_id = "",
+ sender_username = "robert.x.0.gh",
+ sender_provider = "https://github.com",
+ sender_consumer_name = "My Banking App",
+ content = "Hey @user-id-123, check this out!",
+ message_type = "text",
+ mentioned_user_ids = List("user-id-123"),
+ reply_to_message_id = "",
+ thread_id = "",
+ is_deleted = false,
+ created_at = new java.util.Date(),
+ updated_at = new java.util.Date(),
+ reactions = List()
+ ))),
+ List(
+ $AuthenticatedUserIsRequired,
+ UnknownError
+ ),
+ List(apiTagChat),
+ None
+ )
+
+ lazy val getMyMentions: OBPEndpoint = {
+ case "users" :: "current" :: "mentions" :: Nil JsonGet _ => {
+ cc => implicit val ec = EndpointContext(Some(cc))
+ for {
+ (Full(u), callContext) <- authenticatedAccess(cc)
+ limitParam = ObpS.param("limit").map(_.toInt).getOrElse(50)
+ offsetParam = ObpS.param("offset").map(_.toInt).getOrElse(0)
+ messages <- Future {
+ code.chat.ChatMessageTrait.chatMessageProvider.vend.getMentionsForUser(u.userId, limitParam, offsetParam)
+ } map {
+ x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get mentions", 400)
+ }
+ allReactions <- Future {
+ messages.map { msg =>
+ val reactions = code.chat.ReactionTrait.reactionProvider.vend.getReactions(msg.chatMessageId).openOr(List.empty)
+ (msg.chatMessageId, reactions)
+ }.toMap
+ }
+ } yield {
+ (JSONFactory600.createChatMessagesJson(messages, allReactions), HttpCode.`200`(callContext))
+ }
+ }
+ }
+
}
}
diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
index 8b0288c740..a91c879955 100644
--- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
+++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
@@ -86,6 +86,32 @@ case class CounterpartyAttributesJsonV600(
attributes: List[CounterpartyAttributeResponseJsonV600]
)
+case class PostCustomerLinkJsonV600(
+ customer_id: String,
+ other_bank_id: String,
+ other_customer_id: String,
+ relationship_to: String
+)
+
+case class PutCustomerLinkJsonV600(
+ relationship_to: String
+)
+
+case class CustomerLinkJsonV600(
+ customer_link_id: String,
+ bank_id: String,
+ customer_id: String,
+ other_bank_id: String,
+ other_customer_id: String,
+ relationship_to: String,
+ date_inserted: Date,
+ date_updated: Date
+)
+
+case class CustomerLinksJsonV600(
+ customer_links: List[CustomerLinkJsonV600]
+)
+
case class CardanoPaymentJsonV600(
address: String,
amount: CardanoAmountJsonV600,
@@ -1102,6 +1128,131 @@ case class SignalChannelDeletedJsonV600(
deleted: Boolean
)
+// Investigation Report
+case class InvestigationTransactionJsonV600(
+ transaction_id: String,
+ account_id: String,
+ amount: String,
+ currency: String,
+ transaction_type: String,
+ description: String,
+ start_date: java.util.Date,
+ finish_date: java.util.Date,
+ counterparty_name: String,
+ counterparty_account: String,
+ counterparty_bank_name: String
+)
+
+case class InvestigationAccountJsonV600(
+ account_id: String,
+ bank_id: String,
+ currency: String,
+ balance: String,
+ account_name: String,
+ account_type: String,
+ transactions: List[InvestigationTransactionJsonV600]
+)
+
+case class InvestigationCustomerLinkJsonV600(
+ customer_link_id: String,
+ other_customer_id: String,
+ other_bank_id: String,
+ relationship: String,
+ other_legal_name: String
+)
+
+case class InvestigationReportJsonV600(
+ customer_id: String,
+ legal_name: String,
+ bank_id: String,
+ accounts: List[InvestigationAccountJsonV600],
+ related_customers: List[InvestigationCustomerLinkJsonV600],
+ from_date: java.util.Date,
+ to_date: java.util.Date,
+ data_source: String
+)
+
+// Chat / Messaging API case classes
+case class PostChatRoomJsonV600(name: String, description: String)
+case class PutChatRoomJsonV600(name: Option[String], description: Option[String])
+case class PostParticipantJsonV600(user_id: Option[String], consumer_id: Option[String], permissions: Option[List[String]], webhook_url: Option[String])
+case class PutParticipantPermissionsJsonV600(permissions: List[String])
+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])
+case class PutChatMessageJsonV600(content: String)
+case class PostReactionJsonV600(emoji: String)
+
+case class ChatRoomJsonV600(
+ chat_room_id: String,
+ bank_id: String,
+ name: String,
+ description: String,
+ joining_key: String,
+ created_by: String,
+ created_by_username: String,
+ created_by_provider: String,
+ all_users_are_participants: Boolean,
+ is_archived: Boolean,
+ created_at: java.util.Date,
+ updated_at: java.util.Date
+)
+case class ChatRoomsJsonV600(chat_rooms: List[ChatRoomJsonV600])
+
+case class ParticipantJsonV600(
+ participant_id: String,
+ chat_room_id: String,
+ user_id: String,
+ username: String,
+ provider: String,
+ consumer_id: String,
+ consumer_name: String,
+ permissions: List[String],
+ webhook_url: String,
+ joined_at: java.util.Date,
+ last_read_at: java.util.Date,
+ is_muted: Boolean
+)
+case class ParticipantsJsonV600(participants: List[ParticipantJsonV600])
+
+case class ChatMessageJsonV600(
+ chat_message_id: String,
+ chat_room_id: String,
+ sender_user_id: String,
+ sender_consumer_id: String,
+ sender_username: String,
+ sender_provider: String,
+ sender_consumer_name: String,
+ content: String,
+ message_type: String,
+ mentioned_user_ids: List[String],
+ reply_to_message_id: String,
+ thread_id: String,
+ is_deleted: Boolean,
+ created_at: java.util.Date,
+ updated_at: java.util.Date,
+ reactions: List[ReactionSummaryJsonV600]
+)
+case class ChatMessagesJsonV600(messages: List[ChatMessageJsonV600])
+
+case class ReactionJsonV600(
+ reaction_id: String,
+ chat_message_id: String,
+ user_id: String,
+ username: String,
+ provider: String,
+ emoji: String,
+ created_at: java.util.Date
+)
+case class ReactionsJsonV600(reactions: List[ReactionJsonV600])
+case class ReactionSummaryJsonV600(emoji: String, count: Int, user_ids: List[String])
+
+case class TypingUserJsonV600(user_id: String, username: String, provider: String)
+case class TypingUsersJsonV600(users: List[TypingUserJsonV600])
+
+case class UnreadCountJsonV600(chat_room_id: String, unread_count: Long)
+case class UnreadCountsJsonV600(unread_counts: List[UnreadCountJsonV600])
+
+case class JoiningKeyJsonV600(joining_key: String)
+
object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
def createRedisCallCountersJson(
@@ -2730,4 +2881,210 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
)
}
+ def createCustomerLinkJson(customerLink: code.customerlinks.CustomerLinkTrait): CustomerLinkJsonV600 = {
+ CustomerLinkJsonV600(
+ customer_link_id = customerLink.customerLinkId,
+ bank_id = customerLink.bankId,
+ customer_id = customerLink.customerId,
+ other_bank_id = customerLink.otherBankId,
+ other_customer_id = customerLink.otherCustomerId,
+ relationship_to = customerLink.relationshipTo,
+ date_inserted = customerLink.dateInserted,
+ date_updated = customerLink.dateUpdated
+ )
+ }
+
+ def createCustomerLinksJson(customerLinks: List[code.customerlinks.CustomerLinkTrait]): CustomerLinksJsonV600 = {
+ CustomerLinksJsonV600(
+ customerLinks.map(createCustomerLinkJson)
+ )
+ }
+
+ def createInvestigationReportJson(
+ customer: code.investigation.DoobieInvestigationQueries.CustomerRow,
+ bankId: String,
+ accounts: List[code.investigation.DoobieInvestigationQueries.AccountRow],
+ transactions: List[code.investigation.DoobieInvestigationQueries.TransactionRow],
+ customerLinks: List[code.investigation.DoobieInvestigationQueries.CustomerLinkRow],
+ fromDate: java.util.Date,
+ toDate: java.util.Date
+ ): InvestigationReportJsonV600 = {
+ val transactionsByAccount = transactions.groupBy(_.accountId)
+
+ val accountJsons = accounts.map { acc =>
+ val txns = transactionsByAccount.getOrElse(acc.accountId, Nil)
+ InvestigationAccountJsonV600(
+ account_id = acc.accountId,
+ bank_id = acc.bankId,
+ currency = acc.currency,
+ balance = acc.balance.toString,
+ account_name = acc.accountName,
+ account_type = acc.accountType,
+ transactions = txns.map { t =>
+ InvestigationTransactionJsonV600(
+ transaction_id = t.transactionId,
+ account_id = t.accountId,
+ amount = t.amount.toString,
+ currency = t.currency,
+ transaction_type = t.transactionType,
+ description = t.description,
+ start_date = t.startDate,
+ finish_date = t.finishDate,
+ counterparty_name = t.counterpartyName,
+ counterparty_account = t.counterpartyAccount,
+ counterparty_bank_name = t.counterpartyBankName
+ )
+ }
+ )
+ }
+
+ val relatedCustomerJsons = customerLinks.map { cl =>
+ InvestigationCustomerLinkJsonV600(
+ customer_link_id = cl.customerLinkId,
+ other_customer_id = cl.otherCustomerId,
+ other_bank_id = cl.otherBankId,
+ relationship = cl.relationship,
+ other_legal_name = cl.otherLegalName
+ )
+ }
+
+ InvestigationReportJsonV600(
+ customer_id = customer.customerId,
+ legal_name = customer.legalName,
+ bank_id = bankId,
+ accounts = accountJsons,
+ related_customers = relatedCustomerJsons,
+ from_date = fromDate,
+ to_date = toDate,
+ data_source = "mapped_database"
+ )
+ }
+
+ // Chat / Messaging factory functions
+ def createChatRoomJson(room: code.chat.ChatRoomTrait): ChatRoomJsonV600 = {
+ val creator = code.users.Users.users.vend.getUserByUserId(room.createdBy)
+ ChatRoomJsonV600(
+ chat_room_id = room.chatRoomId,
+ bank_id = room.bankId,
+ name = room.name,
+ description = room.description,
+ joining_key = room.joiningKey,
+ created_by = room.createdBy,
+ created_by_username = creator.map(_.name).getOrElse(""),
+ created_by_provider = creator.map(_.provider).getOrElse(""),
+ all_users_are_participants = room.allUsersAreParticipants,
+ is_archived = room.isArchived,
+ created_at = room.createdDate,
+ updated_at = room.updatedDate
+ )
+ }
+ def createChatRoomsJson(rooms: List[code.chat.ChatRoomTrait]): ChatRoomsJsonV600 = {
+ ChatRoomsJsonV600(rooms.map(createChatRoomJson))
+ }
+
+ def createParticipantJson(p: code.chat.ParticipantTrait): ParticipantJsonV600 = {
+ val user = code.users.Users.users.vend.getUserByUserId(p.userId)
+ val consumerName = if (p.consumerId.nonEmpty)
+ code.model.Consumer.find(By(code.model.Consumer.consumerId, p.consumerId)).map(_.name.get).getOrElse("")
+ else ""
+ ParticipantJsonV600(
+ participant_id = p.participantId,
+ chat_room_id = p.chatRoomId,
+ user_id = p.userId,
+ username = user.map(_.name).getOrElse(""),
+ provider = user.map(_.provider).getOrElse(""),
+ consumer_id = p.consumerId,
+ consumer_name = consumerName,
+ permissions = p.permissions,
+ webhook_url = p.webhookUrl,
+ joined_at = p.joinedAt,
+ last_read_at = p.lastReadAt,
+ is_muted = p.isMuted
+ )
+ }
+ def createParticipantsJson(participants: List[code.chat.ParticipantTrait]): ParticipantsJsonV600 = {
+ ParticipantsJsonV600(participants.map(createParticipantJson))
+ }
+
+ def createChatMessageJson(msg: code.chat.ChatMessageTrait, reactions: List[code.chat.ReactionTrait]): ChatMessageJsonV600 = {
+ val reactionSummaries = reactions.groupBy(_.emoji).map { case (emoji, rs) =>
+ ReactionSummaryJsonV600(emoji = emoji, count = rs.size, user_ids = rs.map(_.userId))
+ }.toList
+ val user = code.users.Users.users.vend.getUserByUserId(msg.senderUserId)
+ val consumerAppName = if (msg.senderConsumerId.nonEmpty)
+ code.model.Consumer.find(By(code.model.Consumer.consumerId, msg.senderConsumerId)).map(_.name.get).getOrElse("")
+ else ""
+ ChatMessageJsonV600(
+ chat_message_id = msg.chatMessageId,
+ chat_room_id = msg.chatRoomId,
+ sender_user_id = msg.senderUserId,
+ sender_consumer_id = msg.senderConsumerId,
+ sender_username = user.map(_.name).getOrElse(""),
+ sender_provider = user.map(_.provider).getOrElse(""),
+ sender_consumer_name = consumerAppName,
+ content = if (msg.isDeleted) "" else msg.content,
+ message_type = msg.messageType,
+ mentioned_user_ids = msg.mentionedUserIds,
+ reply_to_message_id = msg.replyToMessageId,
+ thread_id = msg.threadId,
+ is_deleted = msg.isDeleted,
+ created_at = msg.createdDate,
+ updated_at = msg.updatedDate,
+ reactions = reactionSummaries
+ )
+ }
+ def createChatMessagesJson(messages: List[code.chat.ChatMessageTrait], allReactions: Map[String, List[code.chat.ReactionTrait]]): ChatMessagesJsonV600 = {
+ ChatMessagesJsonV600(messages.map(msg => createChatMessageJson(msg, allReactions.getOrElse(msg.chatMessageId, List.empty))))
+ }
+
+ def createChatMessagesJsonFromRows(
+ messages: List[code.chat.DoobieChatMessageQueries.ChatMessageRow],
+ allReactions: Map[String, List[code.chat.DoobieChatMessageQueries.ReactionRow]]
+ ): ChatMessagesJsonV600 = {
+ ChatMessagesJsonV600(messages.map { msg =>
+ val reactions = allReactions.getOrElse(msg.chatMessageId, List.empty)
+ val reactionSummaries = reactions.groupBy(_.emoji).map { case (emoji, rs) =>
+ ReactionSummaryJsonV600(emoji = emoji, count = rs.size, user_ids = rs.map(_.userId))
+ }.toList
+ val mentionedIds = msg.mentionedUserIds match {
+ case Some(ids) if ids.nonEmpty => ids.split(",").map(_.trim).filter(_.nonEmpty).toList
+ case _ => List.empty
+ }
+ ChatMessageJsonV600(
+ chat_message_id = msg.chatMessageId,
+ chat_room_id = msg.chatRoomId,
+ sender_user_id = msg.senderUserId,
+ sender_consumer_id = msg.senderConsumerId,
+ sender_username = msg.senderUsername,
+ sender_provider = msg.senderProvider,
+ sender_consumer_name = msg.senderConsumerName,
+ content = if (msg.isDeleted) "" else msg.content,
+ message_type = msg.messageType,
+ mentioned_user_ids = mentionedIds,
+ reply_to_message_id = msg.replyToMessageId,
+ thread_id = msg.threadId,
+ is_deleted = msg.isDeleted,
+ created_at = msg.createdAt,
+ updated_at = msg.updatedAt,
+ reactions = reactionSummaries
+ )
+ })
+ }
+
+ def createReactionJson(r: code.chat.ReactionTrait): ReactionJsonV600 = {
+ val user = code.users.Users.users.vend.getUserByUserId(r.userId)
+ ReactionJsonV600(
+ reaction_id = r.reactionId,
+ chat_message_id = r.chatMessageId,
+ user_id = r.userId,
+ username = user.map(_.name).getOrElse(""),
+ provider = user.map(_.provider).getOrElse(""),
+ emoji = r.emoji,
+ created_at = r.createdDate
+ )
+ }
+ def createReactionsJson(reactions: List[code.chat.ReactionTrait]): ReactionsJsonV600 = {
+ ReactionsJsonV600(reactions.map(createReactionJson))
+ }
+
}
diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala
index 86e14edf90..d32b943ab1 100644
--- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala
@@ -1876,7 +1876,19 @@ trait Connector extends MdcLoggable {
def createAgentAccountLink(agentId: String, bankId: String, accountId: String, callContext: Option[CallContext]): OBPReturnType[Box[AgentAccountLinkTrait]] = Future{(Failure(setUnimplementedError(nameOf(createAgentAccountLink _))), callContext)}
def updateCustomerAccountLinkById(customerAccountLinkId: String, relationshipType: String, callContext: Option[CallContext]): OBPReturnType[Box[CustomerAccountLinkTrait]] = Future{(Failure(setUnimplementedError(nameOf(updateCustomerAccountLinkById _))), callContext)}
-
+
+ def createCustomerLink(bankId: String, customerId: String, otherBankId: String, otherCustomerId: String, relationshipTo: String, callContext: Option[CallContext]): OBPReturnType[Box[code.customerlinks.CustomerLinkTrait]] = Future{(Failure(setUnimplementedError(nameOf(createCustomerLink _))), callContext)}
+
+ def getCustomerLinkById(customerLinkId: String, callContext: Option[CallContext]): OBPReturnType[Box[code.customerlinks.CustomerLinkTrait]] = Future{(Failure(setUnimplementedError(nameOf(getCustomerLinkById _))), callContext)}
+
+ def getCustomerLinksByBankId(bankId: String, callContext: Option[CallContext]): OBPReturnType[Box[List[code.customerlinks.CustomerLinkTrait]]] = Future{(Failure(setUnimplementedError(nameOf(getCustomerLinksByBankId _))), callContext)}
+
+ def getCustomerLinksByCustomerId(customerId: String, callContext: Option[CallContext]): OBPReturnType[Box[List[code.customerlinks.CustomerLinkTrait]]] = Future{(Failure(setUnimplementedError(nameOf(getCustomerLinksByCustomerId _))), callContext)}
+
+ def updateCustomerLinkById(customerLinkId: String, relationshipTo: String, callContext: Option[CallContext]): OBPReturnType[Box[code.customerlinks.CustomerLinkTrait]] = Future{(Failure(setUnimplementedError(nameOf(updateCustomerLinkById _))), callContext)}
+
+ def deleteCustomerLinkById(customerLinkId: String, callContext: Option[CallContext]): OBPReturnType[Box[Boolean]] = Future{(Failure(setUnimplementedError(nameOf(deleteCustomerLinkById _))), callContext)}
+
def getConsentImplicitSCA(user: User, callContext: Option[CallContext]): OBPReturnType[Box[ConsentImplicitSCAT]] = Future{(Failure(setUnimplementedError(nameOf(getConsentImplicitSCA _))), callContext)}
def createOrUpdateCounterpartyLimit(
diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
index 4be9a1723c..cab71474e1 100644
--- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala
@@ -5508,6 +5508,30 @@ object LocalMappedConnector extends Connector with MdcLoggable {
}
}
+ override def createCustomerLink(bankId: String, customerId: String, otherBankId: String, otherCustomerId: String, relationshipTo: String, callContext: Option[CallContext]): OBPReturnType[Box[code.customerlinks.CustomerLinkTrait]] = Future{
+ (code.customerlinks.CustomerLinkX.customerLink.vend.createCustomerLink(bankId, customerId, otherBankId, otherCustomerId, relationshipTo), callContext)
+ }
+
+ override def getCustomerLinkById(customerLinkId: String, callContext: Option[CallContext]): OBPReturnType[Box[code.customerlinks.CustomerLinkTrait]] = Future{
+ (code.customerlinks.CustomerLinkX.customerLink.vend.getCustomerLinkById(customerLinkId), callContext)
+ }
+
+ override def getCustomerLinksByBankId(bankId: String, callContext: Option[CallContext]): OBPReturnType[Box[List[code.customerlinks.CustomerLinkTrait]]] = Future{
+ (code.customerlinks.CustomerLinkX.customerLink.vend.getCustomerLinksByBankId(bankId), callContext)
+ }
+
+ override def getCustomerLinksByCustomerId(customerId: String, callContext: Option[CallContext]): OBPReturnType[Box[List[code.customerlinks.CustomerLinkTrait]]] = Future{
+ (code.customerlinks.CustomerLinkX.customerLink.vend.getCustomerLinksByCustomerId(customerId), callContext)
+ }
+
+ override def updateCustomerLinkById(customerLinkId: String, relationshipTo: String, callContext: Option[CallContext]): OBPReturnType[Box[code.customerlinks.CustomerLinkTrait]] = Future{
+ (code.customerlinks.CustomerLinkX.customerLink.vend.updateCustomerLinkById(customerLinkId, relationshipTo), callContext)
+ }
+
+ override def deleteCustomerLinkById(customerLinkId: String, callContext: Option[CallContext]): OBPReturnType[Box[Boolean]] = {
+ code.customerlinks.CustomerLinkX.customerLink.vend.deleteCustomerLinkById(customerLinkId).map{(_, callContext)}
+ }
+
override def getConsentImplicitSCA(user: User, callContext: Option[CallContext]): OBPReturnType[Box[ConsentImplicitSCAT]] = Future {
//find the email from the user, and the OBP Implicit SCA is email
(Full(ConsentImplicitSCA(
diff --git a/obp-api/src/main/scala/code/bankconnectors/grpc/GrpcUtils.scala b/obp-api/src/main/scala/code/bankconnectors/grpc/GrpcUtils.scala
index 60b726be67..b6570ad126 100644
--- a/obp-api/src/main/scala/code/bankconnectors/grpc/GrpcUtils.scala
+++ b/obp-api/src/main/scala/code/bankconnectors/grpc/GrpcUtils.scala
@@ -6,8 +6,8 @@ import code.bankconnectors.Connector
import code.bankconnectors.grpc.api.{ObpConnectorRequest, ObpConnectorServiceGrpc}
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model.TopicTrait
-import io.grpc.netty.{GrpcSslContexts, NettyChannelBuilder}
-import io.netty.handler.ssl.SslContextBuilder
+import io.grpc.netty.shaded.io.grpc.netty.{GrpcSslContexts, NettyChannelBuilder}
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder
import net.liftweb.common.{Box, Empty}
import net.liftweb.json.Serialization.write
diff --git a/obp-api/src/main/scala/code/chat/ChatEventBus.scala b/obp-api/src/main/scala/code/chat/ChatEventBus.scala
new file mode 100644
index 0000000000..6af2f5453a
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/ChatEventBus.scala
@@ -0,0 +1,156 @@
+package code.chat
+
+import code.api.cache.Redis
+import code.api.util.APIUtil
+import code.util.Helper.MdcLoggable
+import io.grpc.stub.StreamObserver
+import net.liftweb.json
+import net.liftweb.json.{Extraction, JsonAST}
+import net.liftweb.json.Serialization.write
+import redis.clients.jedis.{Jedis, JedisPubSub}
+
+import java.util.concurrent.{ConcurrentHashMap, CopyOnWriteArrayList}
+import scala.collection.JavaConverters._
+
+/**
+ * Redis pub/sub event bus for chat real-time streaming.
+ *
+ * Bridges REST write operations (create/update/delete message, typing, etc.)
+ * to gRPC streaming clients via Redis pub/sub.
+ *
+ * Channel naming:
+ * - obp_chat:message:{chatRoomId} — new/updated/deleted messages
+ * - obp_chat:typing:{chatRoomId} — typing indicators
+ * - obp_chat:presence:{chatRoomId} — online/offline status
+ * - obp_chat:unread:{userId} — unread count updates
+ */
+object ChatEventBus extends MdcLoggable {
+
+ implicit val formats = json.DefaultFormats
+
+ private val CHANNEL_PREFIX = "obp_chat:"
+
+ // Local observer registry: channel key → list of StreamObservers
+ private val observers = new ConcurrentHashMap[String, CopyOnWriteArrayList[StreamObserver[String]]]()
+
+ // Dedicated subscriber connection and thread
+ @volatile private var subscriberThread: Thread = _
+ @volatile private var subscriberJedis: Jedis = _
+ @volatile private var pubSub: JedisPubSub = _
+ @volatile private var running = false
+
+ // --- Publish methods ---
+
+ def publishMessage(chatRoomId: String, payload: String): Unit = {
+ publish(s"message:$chatRoomId", payload)
+ }
+
+ def publishTyping(chatRoomId: String, payload: String): Unit = {
+ publish(s"typing:$chatRoomId", payload)
+ }
+
+ def publishPresence(chatRoomId: String, payload: String): Unit = {
+ publish(s"presence:$chatRoomId", payload)
+ }
+
+ def publishUnread(userId: String, payload: String): Unit = {
+ publish(s"unread:$userId", payload)
+ }
+
+ private def publish(channelSuffix: String, payload: String): Unit = {
+ val channel = CHANNEL_PREFIX + channelSuffix
+ var jedis: Jedis = null
+ try {
+ jedis = Redis.jedisPool.getResource
+ jedis.publish(channel, payload)
+ } catch {
+ case e: Throwable =>
+ logger.error(s"ChatEventBus says: Failed to publish to $channel: ${e.getMessage}")
+ } finally {
+ if (jedis != null) jedis.close()
+ }
+ }
+
+ // --- Subscribe/unsubscribe for local StreamObservers ---
+
+ def subscribe(channelSuffix: String, observer: StreamObserver[String]): Unit = {
+ val key = channelSuffix
+ observers.computeIfAbsent(key, _ => new CopyOnWriteArrayList[StreamObserver[String]]())
+ observers.get(key).add(observer)
+ logger.info(s"ChatEventBus says: Observer subscribed to $key (total: ${observers.get(key).size})")
+ }
+
+ def unsubscribe(channelSuffix: String, observer: StreamObserver[String]): Unit = {
+ val list = observers.get(channelSuffix)
+ if (list != null) {
+ list.remove(observer)
+ logger.info(s"ChatEventBus says: Observer unsubscribed from $channelSuffix (remaining: ${list.size})")
+ if (list.isEmpty) {
+ observers.remove(channelSuffix)
+ }
+ }
+ }
+
+ // --- Lifecycle ---
+
+ def start(): Unit = {
+ if (running) return
+ running = true
+
+ pubSub = new JedisPubSub {
+ override def onPMessage(pattern: String, channel: String, message: String): Unit = {
+ // channel is e.g. "obp_chat:message:room-123"
+ // strip prefix to get "message:room-123"
+ val key = channel.stripPrefix(CHANNEL_PREFIX)
+ val list = observers.get(key)
+ if (list != null) {
+ list.asScala.foreach { observer =>
+ try {
+ observer.synchronized {
+ observer.onNext(message)
+ }
+ } catch {
+ case e: Throwable =>
+ logger.warn(s"ChatEventBus says: Failed to deliver to observer on $key, removing: ${e.getMessage}")
+ list.remove(observer)
+ }
+ }
+ }
+ }
+ }
+
+ subscriberThread = new Thread(() => {
+ try {
+ // Dedicated connection for the subscriber (not from the pool)
+ subscriberJedis = new Jedis(Redis.url, Redis.port, Redis.timeout)
+ if (Redis.password != null) subscriberJedis.auth(Redis.password)
+ logger.info(s"ChatEventBus says: Redis subscriber started, pattern-subscribing to ${CHANNEL_PREFIX}*")
+ subscriberJedis.psubscribe(pubSub, s"${CHANNEL_PREFIX}*")
+ } catch {
+ case e: Throwable if running =>
+ logger.error(s"ChatEventBus says: Redis subscriber thread died: ${e.getMessage}")
+ case _: Throwable => // shutting down, ignore
+ }
+ }, "chat-event-bus-subscriber")
+ subscriberThread.setDaemon(true)
+ subscriberThread.start()
+
+ logger.info("ChatEventBus says: Started")
+ }
+
+ def stop(): Unit = {
+ running = false
+ try {
+ if (pubSub != null) pubSub.punsubscribe()
+ } catch {
+ case _: Throwable => // ignore
+ }
+ try {
+ if (subscriberJedis != null) subscriberJedis.close()
+ } catch {
+ case _: Throwable => // ignore
+ }
+ observers.clear()
+ logger.info("ChatEventBus says: Stopped")
+ }
+}
diff --git a/obp-api/src/main/scala/code/chat/ChatEventPublisher.scala b/obp-api/src/main/scala/code/chat/ChatEventPublisher.scala
new file mode 100644
index 0000000000..275194a6f8
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/ChatEventPublisher.scala
@@ -0,0 +1,112 @@
+package code.chat
+
+import code.util.Helper.MdcLoggable
+import net.liftweb.json
+import net.liftweb.json.Serialization.write
+
+/**
+ * Publishes chat events to ChatEventBus after REST operations.
+ *
+ * Called from APIMethods600 after createMessage, updateMessage,
+ * softDeleteMessage, and typing indicator operations.
+ */
+object ChatEventPublisher extends MdcLoggable {
+
+ implicit val formats = json.DefaultFormats
+
+ case class MessageEvent(
+ event_type: String,
+ chat_message_id: String,
+ chat_room_id: String,
+ sender_user_id: String,
+ sender_consumer_id: String,
+ sender_username: String,
+ sender_provider: String,
+ sender_consumer_name: String,
+ content: String,
+ message_type: String,
+ mentioned_user_ids: List[String],
+ reply_to_message_id: String,
+ thread_id: String,
+ is_deleted: Boolean,
+ created_at: String,
+ updated_at: String
+ )
+
+ case class TypingEvent(
+ chat_room_id: String,
+ user_id: String,
+ username: String,
+ provider: String,
+ is_typing: Boolean
+ )
+
+ case class PresenceEvent(
+ user_id: String,
+ username: String,
+ provider: String,
+ is_online: Boolean
+ )
+
+ case class UnreadEvent(
+ chat_room_id: String,
+ unread_count: Long
+ )
+
+ private val dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
+
+ def afterCreate(msg: ChatMessageTrait, senderUsername: String, senderProvider: String, senderConsumerName: String): Unit = {
+ publishMessageEvent("new", msg, senderUsername, senderProvider, senderConsumerName)
+ }
+
+ def afterUpdate(msg: ChatMessageTrait, senderUsername: String, senderProvider: String, senderConsumerName: String): Unit = {
+ publishMessageEvent("updated", msg, senderUsername, senderProvider, senderConsumerName)
+ }
+
+ def afterDelete(msg: ChatMessageTrait, senderUsername: String, senderProvider: String, senderConsumerName: String): Unit = {
+ publishMessageEvent("deleted", msg, senderUsername, senderProvider, senderConsumerName)
+ }
+
+ def afterTyping(chatRoomId: String, userId: String, username: String, provider: String, isTyping: Boolean): Unit = {
+ val event = TypingEvent(chatRoomId, userId, username, provider, isTyping)
+ ChatEventBus.publishTyping(chatRoomId, write(event))
+ }
+
+ def afterPresenceChange(chatRoomId: String, userId: String, username: String, provider: String, isOnline: Boolean): Unit = {
+ val event = PresenceEvent(userId, username, provider, isOnline)
+ ChatEventBus.publishPresence(chatRoomId, write(event))
+ }
+
+ def afterUnreadCountChange(userId: String, chatRoomId: String, unreadCount: Long): Unit = {
+ val event = UnreadEvent(chatRoomId, unreadCount)
+ ChatEventBus.publishUnread(userId, write(event))
+ }
+
+ private def publishMessageEvent(
+ eventType: String,
+ msg: ChatMessageTrait,
+ senderUsername: String,
+ senderProvider: String,
+ senderConsumerName: String
+ ): Unit = {
+ val event = MessageEvent(
+ event_type = eventType,
+ chat_message_id = msg.chatMessageId,
+ chat_room_id = msg.chatRoomId,
+ sender_user_id = msg.senderUserId,
+ sender_consumer_id = msg.senderConsumerId,
+ sender_username = senderUsername,
+ sender_provider = senderProvider,
+ sender_consumer_name = senderConsumerName,
+ content = if (msg.isDeleted) "" else msg.content,
+ message_type = msg.messageType,
+ mentioned_user_ids = msg.mentionedUserIds,
+ reply_to_message_id = msg.replyToMessageId,
+ thread_id = msg.threadId,
+ is_deleted = msg.isDeleted,
+ created_at = dateFormat.format(msg.createdDate),
+ updated_at = dateFormat.format(msg.updatedDate)
+ )
+ ChatEventBus.publishMessage(msg.chatRoomId, write(event))
+ }
+}
diff --git a/obp-api/src/main/scala/code/chat/ChatMessageTrait.scala b/obp-api/src/main/scala/code/chat/ChatMessageTrait.scala
new file mode 100644
index 0000000000..b6fe7bf05b
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/ChatMessageTrait.scala
@@ -0,0 +1,47 @@
+package code.chat
+
+import java.util.Date
+import net.liftweb.common.Box
+import net.liftweb.util.SimpleInjector
+
+object ChatMessageTrait extends SimpleInjector {
+ val chatMessageProvider = new Inject(buildOne _) {}
+ def buildOne: ChatMessageProvider = MappedChatMessageProvider
+}
+
+trait ChatMessageProvider {
+ def createMessage(
+ chatRoomId: String,
+ senderUserId: String,
+ senderConsumerId: String,
+ content: String,
+ messageType: String,
+ mentionedUserIds: List[String],
+ replyToMessageId: String,
+ threadId: String
+ ): Box[ChatMessageTrait]
+
+ def getMessage(chatMessageId: String): Box[ChatMessageTrait]
+ def getMessages(chatRoomId: String, limit: Int, offset: Int, fromDate: Date, toDate: Date): Box[List[ChatMessageTrait]]
+ def getThreadReplies(threadId: String): Box[List[ChatMessageTrait]]
+ def getMentionsForUser(userId: String, limit: Int, offset: Int): Box[List[ChatMessageTrait]]
+ def getUnreadCount(chatRoomId: String, sinceDate: Date): Box[Long]
+
+ def updateMessage(chatMessageId: String, content: String): Box[ChatMessageTrait]
+ def softDeleteMessage(chatMessageId: String): Box[ChatMessageTrait]
+}
+
+trait ChatMessageTrait {
+ def chatMessageId: String
+ def chatRoomId: String
+ def senderUserId: String
+ def senderConsumerId: String
+ def content: String
+ def messageType: String
+ def mentionedUserIds: List[String]
+ def replyToMessageId: String
+ def threadId: String
+ def isDeleted: Boolean
+ def createdDate: Date
+ def updatedDate: Date
+}
diff --git a/obp-api/src/main/scala/code/chat/ChatPermissions.scala b/obp-api/src/main/scala/code/chat/ChatPermissions.scala
new file mode 100644
index 0000000000..33f9cb5884
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/ChatPermissions.scala
@@ -0,0 +1,65 @@
+package code.chat
+
+import java.util.Date
+import net.liftweb.common.{Box, Empty, Failure, Full}
+
+/**
+ * A synthetic participant for rooms with allUsersAreParticipants = true.
+ * No database row exists — the user is implicitly a member with no special permissions.
+ */
+case class ImplicitParticipant(chatRoomId: String, userId: String) extends ParticipantTrait {
+ override def participantId: String = ""
+ override def consumerId: String = ""
+ override def permissions: List[String] = List.empty
+ override def webhookUrl: String = ""
+ override def joinedAt: Date = new Date()
+ override def lastReadAt: Date = new Date()
+ override def isMuted: Boolean = false
+}
+
+object ChatPermissions {
+ val CAN_DELETE_MESSAGE = "can_delete_message"
+ val CAN_REMOVE_PARTICIPANT = "can_remove_participant"
+ val CAN_REFRESH_JOINING_KEY = "can_refresh_joining_key"
+ val CAN_UPDATE_ROOM = "can_update_room"
+ val CAN_MANAGE_PERMISSIONS = "can_manage_permissions"
+
+ val ALL_PERMISSIONS: List[String] = List(
+ CAN_DELETE_MESSAGE,
+ CAN_REMOVE_PARTICIPANT,
+ CAN_REFRESH_JOINING_KEY,
+ CAN_UPDATE_ROOM,
+ CAN_MANAGE_PERMISSIONS
+ )
+
+ /**
+ * Check if user is a participant of the room. Returns the Participant record if found,
+ * or a synthetic participant (via the room's allUsersAreParticipants flag) with empty permissions.
+ */
+ def isParticipant(chatRoomId: String, userId: String): Box[ParticipantTrait] = {
+ ParticipantTrait.participantProvider.vend.getParticipant(chatRoomId, userId) match {
+ case Full(p) => Full(p)
+ case _ =>
+ // Check if room has allUsersAreParticipants = true
+ ChatRoomTrait.chatRoomProvider.vend.getChatRoom(chatRoomId) match {
+ case Full(room) if room.allUsersAreParticipants =>
+ Full(ImplicitParticipant(chatRoomId, userId))
+ case _ => Empty
+ }
+ }
+ }
+
+ def isParticipantByConsumerId(chatRoomId: String, consumerId: String): Box[ParticipantTrait] = {
+ ParticipantTrait.participantProvider.vend.getParticipantByConsumerId(chatRoomId, consumerId)
+ }
+
+ def checkParticipantPermission(chatRoomId: String, userId: String, requiredPermission: String): Box[ParticipantTrait] = {
+ isParticipant(chatRoomId, userId) match {
+ case Full(p) =>
+ if (p.permissions.contains(requiredPermission)) Full(p)
+ else Failure(s"Participant does not have permission: $requiredPermission")
+ case Empty => Empty
+ case f: Failure => f
+ }
+ }
+}
diff --git a/obp-api/src/main/scala/code/chat/ChatRoomTrait.scala b/obp-api/src/main/scala/code/chat/ChatRoomTrait.scala
new file mode 100644
index 0000000000..cf9a521d80
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/ChatRoomTrait.scala
@@ -0,0 +1,50 @@
+package code.chat
+
+import java.util.Date
+import net.liftweb.common.Box
+import net.liftweb.util.SimpleInjector
+
+object ChatRoomTrait extends SimpleInjector {
+ val chatRoomProvider = new Inject(buildOne _) {}
+ def buildOne: ChatRoomProvider = MappedChatRoomProvider
+}
+
+trait ChatRoomProvider {
+ def createChatRoom(
+ bankId: String,
+ name: String,
+ description: String,
+ createdBy: String
+ ): Box[ChatRoomTrait]
+
+ def getChatRoom(chatRoomId: String): Box[ChatRoomTrait]
+ def getChatRoomByBankIdAndName(bankId: String, name: String): Box[ChatRoomTrait]
+ def getChatRoomsByBankId(bankId: String): Box[List[ChatRoomTrait]]
+ def getChatRoomsByBankIdForUser(bankId: String, userId: String): Box[List[ChatRoomTrait]]
+ def getChatRoomByJoiningKey(joiningKey: String): Box[ChatRoomTrait]
+
+ def updateChatRoom(
+ chatRoomId: String,
+ name: Option[String],
+ description: Option[String]
+ ): Box[ChatRoomTrait]
+
+ def setAllUsersAreParticipants(chatRoomId: String, allUsersAreParticipants: Boolean): Box[ChatRoomTrait]
+ def archiveChatRoom(chatRoomId: String): Box[ChatRoomTrait]
+ def deleteChatRoom(chatRoomId: String): Box[Boolean]
+ def refreshJoiningKey(chatRoomId: String): Box[ChatRoomTrait]
+ def getOrCreateDefaultRoom(): Box[ChatRoomTrait]
+}
+
+trait ChatRoomTrait {
+ def chatRoomId: String
+ def bankId: String
+ def name: String
+ def description: String
+ def joiningKey: String
+ def createdBy: String
+ def allUsersAreParticipants: Boolean
+ def isArchived: Boolean
+ def createdDate: Date
+ def updatedDate: Date
+}
diff --git a/obp-api/src/main/scala/code/chat/DoobieChatMessageQueries.scala b/obp-api/src/main/scala/code/chat/DoobieChatMessageQueries.scala
new file mode 100644
index 0000000000..9c4d9ae768
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/DoobieChatMessageQueries.scala
@@ -0,0 +1,96 @@
+package code.chat
+
+import java.sql.Timestamp
+import java.util.Date
+
+import code.api.util.DoobieUtil
+import code.util.Helper.MdcLoggable
+import doobie._
+import doobie.implicits._
+import doobie.implicits.javasql._
+
+/**
+ * Doobie queries for chat message retrieval.
+ *
+ * Tables used:
+ * - chatmessage (Lift Mapper table: ChatMessage)
+ * - resourceuser (Lift Mapper table: ResourceUser) — joined for sender username/provider
+ * - consumer (Lift Mapper table: Consumer) — joined for sender consumer app name
+ * - reaction (Lift Mapper table: Reaction)
+ */
+object DoobieChatMessageQueries extends MdcLoggable {
+
+ case class ChatMessageRow(
+ chatMessageId: String,
+ chatRoomId: String,
+ senderUserId: String,
+ senderConsumerId: String,
+ senderUsername: String,
+ senderProvider: String,
+ senderConsumerName: String,
+ content: String,
+ messageType: String,
+ mentionedUserIds: Option[String],
+ replyToMessageId: String,
+ threadId: String,
+ isDeleted: Boolean,
+ createdAt: Timestamp,
+ updatedAt: Timestamp
+ )
+
+ case class ReactionRow(
+ reactionId: String,
+ chatMessageId: String,
+ userId: String,
+ emoji: String,
+ createdAt: Timestamp
+ )
+
+ def getMessagesWithReactions(
+ chatRoomId: String,
+ fromDate: Date,
+ toDate: Date,
+ limit: Int,
+ offset: Int
+ ): (List[ChatMessageRow], Map[String, List[ReactionRow]]) = {
+ val fromTs = new Timestamp(fromDate.getTime)
+ val toTs = new Timestamp(toDate.getTime)
+
+ val messagesQuery: ConnectionIO[List[ChatMessageRow]] =
+ sql"""SELECT m.chatmessageid, m.chatroomid, m.senderuserid, m.senderconsumerid,
+ COALESCE(u.name_, ''), COALESCE(u.provider_, ''),
+ COALESCE(c.name, ''),
+ m.content, m.messagetype, m.mentioneduserids, m.replytomessageid,
+ m.threadid, m.isdeleted, m.createdat, m.updatedat
+ FROM chatmessage m
+ LEFT JOIN resourceuser u ON u.userid_ = m.senderuserid
+ LEFT JOIN consumer c ON c.consumerid = m.senderconsumerid
+ WHERE m.chatroomid = $chatRoomId
+ AND m.createdat >= $fromTs
+ AND m.createdat <= $toTs
+ ORDER BY m.id ASC
+ LIMIT $limit OFFSET $offset"""
+ .query[ChatMessageRow]
+ .to[List]
+
+ val messages = DoobieUtil.runQuery(messagesQuery)
+
+ val reactions: Map[String, List[ReactionRow]] = if (messages.isEmpty) {
+ Map.empty
+ } else {
+ val messageIds = messages.map(_.chatMessageId)
+ val inClause = messageIds.map(id => fr"$id").reduceLeft((a, b) => a ++ fr"," ++ b)
+
+ val reactionsQuery: ConnectionIO[List[ReactionRow]] =
+ (fr"""SELECT reactionid, chatmessageid, userid, emoji, createdat
+ FROM reaction
+ WHERE chatmessageid IN (""" ++ inClause ++ fr")")
+ .query[ReactionRow]
+ .to[List]
+
+ DoobieUtil.runQuery(reactionsQuery).groupBy(_.chatMessageId)
+ }
+
+ (messages, reactions)
+ }
+}
diff --git a/obp-api/src/main/scala/code/chat/MappedChatMessage.scala b/obp-api/src/main/scala/code/chat/MappedChatMessage.scala
new file mode 100644
index 0000000000..f320b6cb10
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/MappedChatMessage.scala
@@ -0,0 +1,135 @@
+package code.chat
+
+import java.util.Date
+import code.util.MappedUUID
+import net.liftweb.common.Box
+import net.liftweb.mapper._
+import net.liftweb.util.Helpers.tryo
+
+object MappedChatMessageProvider extends ChatMessageProvider {
+
+ override def createMessage(
+ chatRoomId: String,
+ senderUserId: String,
+ senderConsumerId: String,
+ content: String,
+ messageType: String,
+ mentionedUserIds: List[String],
+ replyToMessageId: String,
+ threadId: String
+ ): Box[ChatMessageTrait] = {
+ tryo {
+ ChatMessage.create
+ .ChatRoomId(chatRoomId)
+ .SenderUserId(senderUserId)
+ .SenderConsumerId(senderConsumerId)
+ .Content(content)
+ .MessageType(messageType)
+ .MentionedUserIds(mentionedUserIds.mkString(","))
+ .ReplyToMessageId(replyToMessageId)
+ .ThreadId(threadId)
+ .IsDeleted(false)
+ .saveMe()
+ }
+ }
+
+ override def getMessage(chatMessageId: String): Box[ChatMessageTrait] = {
+ ChatMessage.find(By(ChatMessage.ChatMessageId, chatMessageId))
+ }
+
+ override def getMessages(chatRoomId: String, limit: Int, offset: Int, fromDate: Date, toDate: Date): Box[List[ChatMessageTrait]] = {
+ tryo {
+ ChatMessage.findAll(
+ By(ChatMessage.ChatRoomId, chatRoomId),
+ By_>=(ChatMessage.createdAt, fromDate),
+ By_<=(ChatMessage.createdAt, toDate),
+ OrderBy(ChatMessage.id, Ascending),
+ MaxRows[ChatMessage](limit),
+ StartAt[ChatMessage](offset)
+ )
+ }
+ }
+
+ override def getThreadReplies(threadId: String): Box[List[ChatMessageTrait]] = {
+ tryo {
+ ChatMessage.findAll(
+ By(ChatMessage.ThreadId, threadId),
+ OrderBy(ChatMessage.id, Ascending)
+ )
+ }
+ }
+
+ override def getMentionsForUser(userId: String, limit: Int, offset: Int): Box[List[ChatMessageTrait]] = {
+ tryo {
+ ChatMessage.findAll(
+ Like(ChatMessage.MentionedUserIds, s"%$userId%"),
+ OrderBy(ChatMessage.id, Descending),
+ MaxRows[ChatMessage](limit),
+ StartAt[ChatMessage](offset)
+ )
+ }
+ }
+
+ override def getUnreadCount(chatRoomId: String, sinceDate: Date): Box[Long] = {
+ tryo {
+ ChatMessage.count(
+ By(ChatMessage.ChatRoomId, chatRoomId),
+ By_>(ChatMessage.createdAt, sinceDate)
+ )
+ }
+ }
+
+ override def updateMessage(chatMessageId: String, content: String): Box[ChatMessageTrait] = {
+ ChatMessage.find(By(ChatMessage.ChatMessageId, chatMessageId)).flatMap { msg =>
+ tryo {
+ msg.Content(content).saveMe()
+ }
+ }
+ }
+
+ override def softDeleteMessage(chatMessageId: String): Box[ChatMessageTrait] = {
+ ChatMessage.find(By(ChatMessage.ChatMessageId, chatMessageId)).flatMap { msg =>
+ tryo {
+ msg.IsDeleted(true).saveMe()
+ }
+ }
+ }
+}
+
+class ChatMessage extends ChatMessageTrait with LongKeyedMapper[ChatMessage] with IdPK with CreatedUpdated {
+
+ def getSingleton = ChatMessage
+
+ object ChatMessageId extends MappedUUID(this)
+ object ChatRoomId extends MappedString(this, 36)
+ object SenderUserId extends MappedString(this, 36)
+ object SenderConsumerId extends MappedString(this, 36)
+ object Content extends MappedText(this)
+ object MessageType extends MappedString(this, 16)
+ object MentionedUserIds extends MappedText(this)
+ object ReplyToMessageId extends MappedString(this, 36)
+ object ThreadId extends MappedString(this, 36)
+ object IsDeleted extends MappedBoolean(this)
+
+ override def chatMessageId: String = ChatMessageId.get
+ override def chatRoomId: String = ChatRoomId.get
+ override def senderUserId: String = SenderUserId.get
+ override def senderConsumerId: String = SenderConsumerId.get
+ override def content: String = Content.get
+ override def messageType: String = MessageType.get
+ override def mentionedUserIds: List[String] = {
+ val ids = MentionedUserIds.get
+ if (ids == null || ids.isEmpty) List.empty
+ else ids.split(",").map(_.trim).filter(_.nonEmpty).toList
+ }
+ override def replyToMessageId: String = ReplyToMessageId.get
+ override def threadId: String = ThreadId.get
+ override def isDeleted: Boolean = IsDeleted.get
+ override def createdDate: Date = createdAt.get
+ override def updatedDate: Date = updatedAt.get
+}
+
+object ChatMessage extends ChatMessage with LongKeyedMetaMapper[ChatMessage] {
+ override def dbTableName = "ChatMessage"
+ override def dbIndexes = UniqueIndex(ChatMessageId) :: Index(ChatRoomId) :: Index(ThreadId) :: Index(SenderUserId) :: super.dbIndexes
+}
diff --git a/obp-api/src/main/scala/code/chat/MappedChatRoom.scala b/obp-api/src/main/scala/code/chat/MappedChatRoom.scala
new file mode 100644
index 0000000000..3a940ab3b8
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/MappedChatRoom.scala
@@ -0,0 +1,158 @@
+package code.chat
+
+import java.util.Date
+import code.api.util.APIUtil.generateUUID
+import code.util.MappedUUID
+import net.liftweb.common.{Box, Full}
+import net.liftweb.mapper._
+import net.liftweb.util.Helpers.tryo
+
+object MappedChatRoomProvider extends ChatRoomProvider {
+
+ override def createChatRoom(
+ bankId: String,
+ name: String,
+ description: String,
+ createdBy: String
+ ): Box[ChatRoomTrait] = {
+ tryo {
+ ChatRoom.create
+ .BankId(bankId)
+ .Name(name)
+ .Description(description)
+ .CreatedBy(createdBy)
+ .IsArchived(false)
+ .saveMe()
+ }
+ }
+
+ override def getChatRoom(chatRoomId: String): Box[ChatRoomTrait] = {
+ ChatRoom.find(By(ChatRoom.ChatRoomId, chatRoomId))
+ }
+
+ override def getChatRoomByBankIdAndName(bankId: String, name: String): Box[ChatRoomTrait] = {
+ ChatRoom.find(
+ By(ChatRoom.BankId, bankId),
+ By(ChatRoom.Name, name)
+ )
+ }
+
+ override def getChatRoomsByBankId(bankId: String): Box[List[ChatRoomTrait]] = {
+ tryo {
+ ChatRoom.findAll(By(ChatRoom.BankId, bankId))
+ }
+ }
+
+ override def getChatRoomsByBankIdForUser(bankId: String, userId: String): Box[List[ChatRoomTrait]] = {
+ tryo {
+ val participantRoomIds = Participant.findAll(By(Participant.UserId, userId))
+ .map(_.chatRoomId)
+ val explicitRooms = ChatRoom.findAll(
+ By(ChatRoom.BankId, bankId),
+ ByList(ChatRoom.ChatRoomId, participantRoomIds)
+ )
+ val openRooms = ChatRoom.findAll(
+ By(ChatRoom.BankId, bankId),
+ By(ChatRoom.AllUsersAreParticipants, true)
+ )
+ (explicitRooms ++ openRooms).groupBy(_.chatRoomId).values.map(_.head).toList
+ }
+ }
+
+ override def getChatRoomByJoiningKey(joiningKey: String): Box[ChatRoomTrait] = {
+ ChatRoom.find(By(ChatRoom.JoiningKey, joiningKey))
+ }
+
+ override def updateChatRoom(
+ chatRoomId: String,
+ name: Option[String],
+ description: Option[String]
+ ): Box[ChatRoomTrait] = {
+ ChatRoom.find(By(ChatRoom.ChatRoomId, chatRoomId)).flatMap { room =>
+ tryo {
+ name.foreach(n => room.Name(n))
+ description.foreach(d => room.Description(d))
+ room.saveMe()
+ }
+ }
+ }
+
+ override def setAllUsersAreParticipants(chatRoomId: String, allUsersAreParticipants: Boolean): Box[ChatRoomTrait] = {
+ ChatRoom.find(By(ChatRoom.ChatRoomId, chatRoomId)).flatMap { room =>
+ tryo {
+ room.AllUsersAreParticipants(allUsersAreParticipants).saveMe()
+ }
+ }
+ }
+
+ override def archiveChatRoom(chatRoomId: String): Box[ChatRoomTrait] = {
+ ChatRoom.find(By(ChatRoom.ChatRoomId, chatRoomId)).flatMap { room =>
+ tryo {
+ room.IsArchived(true).saveMe()
+ }
+ }
+ }
+
+ override def deleteChatRoom(chatRoomId: String): Box[Boolean] = {
+ ChatRoom.find(By(ChatRoom.ChatRoomId, chatRoomId)).flatMap { room =>
+ tryo {
+ room.delete_!
+ }
+ }
+ }
+
+ override def refreshJoiningKey(chatRoomId: String): Box[ChatRoomTrait] = {
+ ChatRoom.find(By(ChatRoom.ChatRoomId, chatRoomId)).flatMap { room =>
+ tryo {
+ room.JoiningKey(generateUUID()).saveMe()
+ }
+ }
+ }
+
+ override def getOrCreateDefaultRoom(): Box[ChatRoomTrait] = {
+ getChatRoomByBankIdAndName("", "general") match {
+ case Full(room) => Full(room)
+ case _ =>
+ tryo {
+ ChatRoom.create
+ .BankId("")
+ .Name("general")
+ .Description("Default system-wide chat room for all users")
+ .CreatedBy("system")
+ .AllUsersAreParticipants(true)
+ .IsArchived(false)
+ .saveMe()
+ }
+ }
+ }
+}
+
+class ChatRoom extends ChatRoomTrait with LongKeyedMapper[ChatRoom] with IdPK with CreatedUpdated {
+
+ def getSingleton = ChatRoom
+
+ object ChatRoomId extends MappedUUID(this)
+ object BankId extends MappedString(this, 255)
+ object Name extends MappedString(this, 255)
+ object Description extends MappedText(this)
+ object JoiningKey extends MappedUUID(this)
+ object CreatedBy extends MappedString(this, 36)
+ object AllUsersAreParticipants extends MappedBoolean(this)
+ object IsArchived extends MappedBoolean(this)
+
+ override def chatRoomId: String = ChatRoomId.get
+ override def bankId: String = BankId.get
+ override def name: String = Name.get
+ override def description: String = Description.get
+ override def joiningKey: String = JoiningKey.get
+ override def createdBy: String = CreatedBy.get
+ override def allUsersAreParticipants: Boolean = AllUsersAreParticipants.get
+ override def isArchived: Boolean = IsArchived.get
+ override def createdDate: Date = createdAt.get
+ override def updatedDate: Date = updatedAt.get
+}
+
+object ChatRoom extends ChatRoom with LongKeyedMetaMapper[ChatRoom] {
+ override def dbTableName = "ChatRoom"
+ override def dbIndexes = UniqueIndex(ChatRoomId) :: UniqueIndex(BankId, Name) :: Index(BankId) :: super.dbIndexes
+}
diff --git a/obp-api/src/main/scala/code/chat/MappedParticipant.scala b/obp-api/src/main/scala/code/chat/MappedParticipant.scala
new file mode 100644
index 0000000000..19ac0f1d3c
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/MappedParticipant.scala
@@ -0,0 +1,154 @@
+package code.chat
+
+import java.util.Date
+import code.util.MappedUUID
+import net.liftweb.common.Box
+import net.liftweb.mapper._
+import net.liftweb.util.Helpers.tryo
+
+object MappedParticipantProvider extends ParticipantProvider {
+
+ override def addParticipant(
+ chatRoomId: String,
+ userId: String,
+ consumerId: String,
+ permissions: List[String],
+ webhookUrl: String
+ ): Box[ParticipantTrait] = {
+ tryo {
+ Participant.create
+ .ChatRoomId(chatRoomId)
+ .UserId(userId)
+ .ConsumerId(consumerId)
+ .Permissions(permissions.mkString(","))
+ .WebhookUrl(webhookUrl)
+ .JoinedAt(new Date())
+ .LastReadAt(new Date())
+ .IsMuted(false)
+ .saveMe()
+ }
+ }
+
+ override def getParticipant(chatRoomId: String, userId: String): Box[ParticipantTrait] = {
+ Participant.find(
+ By(Participant.ChatRoomId, chatRoomId),
+ By(Participant.UserId, userId)
+ )
+ }
+
+ override def getParticipantByConsumerId(chatRoomId: String, consumerId: String): Box[ParticipantTrait] = {
+ Participant.find(
+ By(Participant.ChatRoomId, chatRoomId),
+ By(Participant.ConsumerId, consumerId)
+ )
+ }
+
+ override def getParticipants(chatRoomId: String): Box[List[ParticipantTrait]] = {
+ tryo {
+ Participant.findAll(By(Participant.ChatRoomId, chatRoomId))
+ }
+ }
+
+ override def getParticipantRoomsByUserId(userId: String): Box[List[ParticipantTrait]] = {
+ tryo {
+ Participant.findAll(By(Participant.UserId, userId))
+ }
+ }
+
+ override def updateParticipantPermissions(
+ chatRoomId: String,
+ userId: String,
+ permissions: List[String]
+ ): Box[ParticipantTrait] = {
+ Participant.find(
+ By(Participant.ChatRoomId, chatRoomId),
+ By(Participant.UserId, userId)
+ ).flatMap { p =>
+ tryo {
+ p.Permissions(permissions.mkString(",")).saveMe()
+ }
+ }
+ }
+
+ override def updateWebhookUrl(
+ chatRoomId: String,
+ userId: String,
+ webhookUrl: String
+ ): Box[ParticipantTrait] = {
+ Participant.find(
+ By(Participant.ChatRoomId, chatRoomId),
+ By(Participant.UserId, userId)
+ ).flatMap { p =>
+ tryo {
+ p.WebhookUrl(webhookUrl).saveMe()
+ }
+ }
+ }
+
+ override def updateLastReadAt(chatRoomId: String, userId: String): Box[ParticipantTrait] = {
+ Participant.find(
+ By(Participant.ChatRoomId, chatRoomId),
+ By(Participant.UserId, userId)
+ ).flatMap { p =>
+ tryo {
+ p.LastReadAt(new Date()).saveMe()
+ }
+ }
+ }
+
+ override def updateMuted(chatRoomId: String, userId: String, isMuted: Boolean): Box[ParticipantTrait] = {
+ Participant.find(
+ By(Participant.ChatRoomId, chatRoomId),
+ By(Participant.UserId, userId)
+ ).flatMap { p =>
+ tryo {
+ p.IsMuted(isMuted).saveMe()
+ }
+ }
+ }
+
+ override def removeParticipant(chatRoomId: String, userId: String): Box[Boolean] = {
+ Participant.find(
+ By(Participant.ChatRoomId, chatRoomId),
+ By(Participant.UserId, userId)
+ ).flatMap { p =>
+ tryo {
+ p.delete_!
+ }
+ }
+ }
+}
+
+class Participant extends ParticipantTrait with LongKeyedMapper[Participant] with IdPK {
+
+ def getSingleton = Participant
+
+ object ParticipantId extends MappedUUID(this)
+ object ChatRoomId extends MappedString(this, 36)
+ object UserId extends MappedString(this, 36)
+ object ConsumerId extends MappedString(this, 36)
+ object Permissions extends MappedText(this)
+ object WebhookUrl extends MappedString(this, 1024)
+ object JoinedAt extends MappedDateTime(this)
+ object LastReadAt extends MappedDateTime(this)
+ object IsMuted extends MappedBoolean(this)
+
+ override def participantId: String = ParticipantId.get
+ override def chatRoomId: String = ChatRoomId.get
+ override def userId: String = UserId.get
+ override def consumerId: String = ConsumerId.get
+ override def permissions: List[String] = {
+ val permsStr = Permissions.get
+ if (permsStr == null || permsStr.isEmpty) List.empty
+ else permsStr.split(",").map(_.trim).filter(_.nonEmpty).toList
+ }
+ override def webhookUrl: String = WebhookUrl.get
+ override def joinedAt: Date = JoinedAt.get
+ override def lastReadAt: Date = LastReadAt.get
+ override def isMuted: Boolean = IsMuted.get
+}
+
+object Participant extends Participant with LongKeyedMetaMapper[Participant] {
+ override def dbTableName = "Participant"
+ override def dbIndexes = UniqueIndex(ParticipantId) :: Index(ChatRoomId) :: Index(UserId) :: UniqueIndex(ChatRoomId, UserId) :: super.dbIndexes
+}
diff --git a/obp-api/src/main/scala/code/chat/MappedReaction.scala b/obp-api/src/main/scala/code/chat/MappedReaction.scala
new file mode 100644
index 0000000000..433938e08c
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/MappedReaction.scala
@@ -0,0 +1,67 @@
+package code.chat
+
+import java.util.Date
+import code.util.MappedUUID
+import net.liftweb.common.Box
+import net.liftweb.mapper._
+import net.liftweb.util.Helpers.tryo
+
+object MappedReactionProvider extends ReactionProvider {
+
+ override def addReaction(chatMessageId: String, userId: String, emoji: String): Box[ReactionTrait] = {
+ tryo {
+ Reaction.create
+ .ChatMessageId(chatMessageId)
+ .UserId(userId)
+ .Emoji(emoji)
+ .saveMe()
+ }
+ }
+
+ override def removeReaction(chatMessageId: String, userId: String, emoji: String): Box[Boolean] = {
+ Reaction.find(
+ By(Reaction.ChatMessageId, chatMessageId),
+ By(Reaction.UserId, userId),
+ By(Reaction.Emoji, emoji)
+ ).flatMap { r =>
+ tryo {
+ r.delete_!
+ }
+ }
+ }
+
+ override def getReactions(chatMessageId: String): Box[List[ReactionTrait]] = {
+ tryo {
+ Reaction.findAll(By(Reaction.ChatMessageId, chatMessageId))
+ }
+ }
+
+ override def getReaction(chatMessageId: String, userId: String, emoji: String): Box[ReactionTrait] = {
+ Reaction.find(
+ By(Reaction.ChatMessageId, chatMessageId),
+ By(Reaction.UserId, userId),
+ By(Reaction.Emoji, emoji)
+ )
+ }
+}
+
+class Reaction extends ReactionTrait with LongKeyedMapper[Reaction] with IdPK with CreatedUpdated {
+
+ def getSingleton = Reaction
+
+ object ReactionId extends MappedUUID(this)
+ object ChatMessageId extends MappedString(this, 36)
+ object UserId extends MappedString(this, 36)
+ object Emoji extends MappedString(this, 64)
+
+ override def reactionId: String = ReactionId.get
+ override def chatMessageId: String = ChatMessageId.get
+ override def userId: String = UserId.get
+ override def emoji: String = Emoji.get
+ override def createdDate: Date = createdAt.get
+}
+
+object Reaction extends Reaction with LongKeyedMetaMapper[Reaction] {
+ override def dbTableName = "Reaction"
+ override def dbIndexes = UniqueIndex(ReactionId) :: Index(ChatMessageId) :: UniqueIndex(ChatMessageId, UserId, Emoji) :: super.dbIndexes
+}
diff --git a/obp-api/src/main/scala/code/chat/ParticipantTrait.scala b/obp-api/src/main/scala/code/chat/ParticipantTrait.scala
new file mode 100644
index 0000000000..f68f653f6b
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/ParticipantTrait.scala
@@ -0,0 +1,53 @@
+package code.chat
+
+import java.util.Date
+import net.liftweb.common.Box
+import net.liftweb.util.SimpleInjector
+
+object ParticipantTrait extends SimpleInjector {
+ val participantProvider = new Inject(buildOne _) {}
+ def buildOne: ParticipantProvider = MappedParticipantProvider
+}
+
+trait ParticipantProvider {
+ def addParticipant(
+ chatRoomId: String,
+ userId: String,
+ consumerId: String,
+ permissions: List[String],
+ webhookUrl: String
+ ): Box[ParticipantTrait]
+
+ def getParticipant(chatRoomId: String, userId: String): Box[ParticipantTrait]
+ def getParticipantByConsumerId(chatRoomId: String, consumerId: String): Box[ParticipantTrait]
+ def getParticipants(chatRoomId: String): Box[List[ParticipantTrait]]
+ def getParticipantRoomsByUserId(userId: String): Box[List[ParticipantTrait]]
+
+ def updateParticipantPermissions(
+ chatRoomId: String,
+ userId: String,
+ permissions: List[String]
+ ): Box[ParticipantTrait]
+
+ def updateWebhookUrl(
+ chatRoomId: String,
+ userId: String,
+ webhookUrl: String
+ ): Box[ParticipantTrait]
+
+ def updateLastReadAt(chatRoomId: String, userId: String): Box[ParticipantTrait]
+ def updateMuted(chatRoomId: String, userId: String, isMuted: Boolean): Box[ParticipantTrait]
+ def removeParticipant(chatRoomId: String, userId: String): Box[Boolean]
+}
+
+trait ParticipantTrait {
+ def participantId: String
+ def chatRoomId: String
+ def userId: String
+ def consumerId: String
+ def permissions: List[String]
+ def webhookUrl: String
+ def joinedAt: Date
+ def lastReadAt: Date
+ def isMuted: Boolean
+}
diff --git a/obp-api/src/main/scala/code/chat/ReactionTrait.scala b/obp-api/src/main/scala/code/chat/ReactionTrait.scala
new file mode 100644
index 0000000000..d924353369
--- /dev/null
+++ b/obp-api/src/main/scala/code/chat/ReactionTrait.scala
@@ -0,0 +1,25 @@
+package code.chat
+
+import java.util.Date
+import net.liftweb.common.Box
+import net.liftweb.util.SimpleInjector
+
+object ReactionTrait extends SimpleInjector {
+ val reactionProvider = new Inject(buildOne _) {}
+ def buildOne: ReactionProvider = MappedReactionProvider
+}
+
+trait ReactionProvider {
+ def addReaction(chatMessageId: String, userId: String, emoji: String): Box[ReactionTrait]
+ def removeReaction(chatMessageId: String, userId: String, emoji: String): Box[Boolean]
+ def getReactions(chatMessageId: String): Box[List[ReactionTrait]]
+ def getReaction(chatMessageId: String, userId: String, emoji: String): Box[ReactionTrait]
+}
+
+trait ReactionTrait {
+ def reactionId: String
+ def chatMessageId: String
+ def userId: String
+ def emoji: String
+ def createdDate: Date
+}
diff --git a/obp-api/src/main/scala/code/customerlinks/CustomerLink.scala b/obp-api/src/main/scala/code/customerlinks/CustomerLink.scala
new file mode 100644
index 0000000000..837168d85f
--- /dev/null
+++ b/obp-api/src/main/scala/code/customerlinks/CustomerLink.scala
@@ -0,0 +1,38 @@
+package code.customerlinks
+
+import java.util.Date
+
+import net.liftweb.common.Box
+import net.liftweb.util.SimpleInjector
+
+import scala.concurrent.Future
+
+
+object CustomerLinkX extends SimpleInjector {
+
+ val customerLink = new Inject(buildOne _) {}
+
+ def buildOne: CustomerLinkProvider = MappedCustomerLinkProvider
+
+}
+
+trait CustomerLinkProvider {
+ def createCustomerLink(bankId: String, customerId: String, otherBankId: String, otherCustomerId: String, relationshipTo: String): Box[CustomerLinkTrait]
+ def getCustomerLinkById(customerLinkId: String): Box[CustomerLinkTrait]
+ def getCustomerLinksByBankId(bankId: String): Box[List[CustomerLinkTrait]]
+ def getCustomerLinksByCustomerId(customerId: String): Box[List[CustomerLinkTrait]]
+ def updateCustomerLinkById(customerLinkId: String, relationshipTo: String): Box[CustomerLinkTrait]
+ def deleteCustomerLinkById(customerLinkId: String): Future[Box[Boolean]]
+ def bulkDeleteCustomerLinks(): Boolean
+}
+
+trait CustomerLinkTrait {
+ def customerLinkId: String
+ def bankId: String
+ def customerId: String
+ def otherBankId: String
+ def otherCustomerId: String
+ def relationshipTo: String
+ def dateInserted: Date
+ def dateUpdated: Date
+}
diff --git a/obp-api/src/main/scala/code/customerlinks/MappedCustomerLink.scala b/obp-api/src/main/scala/code/customerlinks/MappedCustomerLink.scala
new file mode 100644
index 0000000000..31ab41c85e
--- /dev/null
+++ b/obp-api/src/main/scala/code/customerlinks/MappedCustomerLink.scala
@@ -0,0 +1,94 @@
+package code.customerlinks
+
+import java.util.Date
+
+import code.api.util.ErrorMessages
+import code.util.{MappedUUID, UUIDString}
+import net.liftweb.common.{Box, Empty, Failure, Full}
+import net.liftweb.mapper._
+import net.liftweb.util.Helpers.tryo
+
+import scala.concurrent.Future
+import com.openbankproject.commons.ExecutionContext.Implicits.global
+
+object MappedCustomerLinkProvider extends CustomerLinkProvider {
+ override def createCustomerLink(bankId: String, customerId: String, otherBankId: String, otherCustomerId: String, relationshipTo: String): Box[CustomerLinkTrait] = {
+ tryo {
+ CustomerLink.create
+ .BankId(bankId)
+ .CustomerId(customerId)
+ .OtherBankId(otherBankId)
+ .OtherCustomerId(otherCustomerId)
+ .RelationshipTo(relationshipTo)
+ .saveMe()
+ }
+ }
+
+ override def getCustomerLinkById(customerLinkId: String): Box[CustomerLinkTrait] = {
+ CustomerLink.find(
+ By(CustomerLink.CustomerLinkId, customerLinkId)
+ )
+ }
+
+ override def getCustomerLinksByBankId(bankId: String): Box[List[CustomerLinkTrait]] = {
+ tryo {
+ CustomerLink.findAll(
+ By(CustomerLink.BankId, bankId))
+ }
+ }
+
+ override def getCustomerLinksByCustomerId(customerId: String): Box[List[CustomerLinkTrait]] = {
+ tryo {
+ CustomerLink.findAll(
+ By(CustomerLink.CustomerId, customerId))
+ }
+ }
+
+ override def updateCustomerLinkById(customerLinkId: String, relationshipTo: String): Box[CustomerLinkTrait] = {
+ CustomerLink.find(By(CustomerLink.CustomerLinkId, customerLinkId)) match {
+ case Full(t) => Full(t.RelationshipTo(relationshipTo).saveMe())
+ case Empty => Empty ?~! ErrorMessages.CustomerLinkNotFound
+ case Failure(msg, exception, chain) => Failure(msg, exception, chain)
+ }
+ }
+
+ override def deleteCustomerLinkById(customerLinkId: String): Future[Box[Boolean]] = {
+ Future {
+ CustomerLink.find(By(CustomerLink.CustomerLinkId, customerLinkId)) match {
+ case Full(t) => Full(t.delete_!)
+ case Empty => Empty ?~! ErrorMessages.CustomerLinkNotFound
+ case Failure(msg, exception, chain) => Failure(msg, exception, chain)
+ }
+ }
+ }
+
+ override def bulkDeleteCustomerLinks(): Boolean = {
+ CustomerLink.bulkDelete_!!()
+ }
+}
+
+class CustomerLink extends CustomerLinkTrait with LongKeyedMapper[CustomerLink] with IdPK with CreatedUpdated {
+
+ def getSingleton = CustomerLink
+
+ object CustomerLinkId extends MappedUUID(this)
+ object BankId extends MappedString(this, 255)
+ object CustomerId extends UUIDString(this)
+ object OtherBankId extends MappedString(this, 255)
+ object OtherCustomerId extends UUIDString(this)
+ object RelationshipTo extends MappedString(this, 255)
+
+ override def customerLinkId: String = CustomerLinkId.get
+ override def bankId: String = BankId.get
+ override def customerId: String = CustomerId.get
+ override def otherBankId: String = OtherBankId.get
+ override def otherCustomerId: String = OtherCustomerId.get
+ override def relationshipTo: String = RelationshipTo.get
+ override def dateInserted: Date = createdAt.get
+ override def dateUpdated: Date = updatedAt.get
+}
+
+object CustomerLink extends CustomerLink with LongKeyedMetaMapper[CustomerLink] {
+ override def dbTableName = "CustomerLink"
+ override def dbIndexes = UniqueIndex(CustomerLinkId) :: Index(CustomerId) :: Index(OtherCustomerId) :: super.dbIndexes
+}
diff --git a/obp-api/src/main/scala/code/investigation/DoobieInvestigationQueries.scala b/obp-api/src/main/scala/code/investigation/DoobieInvestigationQueries.scala
new file mode 100644
index 0000000000..21f5aa6ce1
--- /dev/null
+++ b/obp-api/src/main/scala/code/investigation/DoobieInvestigationQueries.scala
@@ -0,0 +1,176 @@
+package code.investigation
+
+import java.sql.Timestamp
+
+import code.api.util.DoobieUtil
+import code.util.Helper.MdcLoggable
+import doobie._
+import doobie.implicits._
+import doobie.implicits.javasql._
+
+/**
+ * Doobie queries for the Customer Investigation Report endpoint.
+ *
+ * Each query maps to a logical data retrieval step that could potentially
+ * be replaced by a connector call in the future. Keeping them separate
+ * makes the endpoint easier to refactor for non-mapped connectors.
+ *
+ * Tables used:
+ * - mappedcustomer
+ * - customeraccountlink
+ * - mappedbankaccount
+ * - mappedtransaction
+ * - mappedcustomerlink
+ */
+object DoobieInvestigationQueries extends MdcLoggable {
+
+ // Result case classes — these are internal to the query layer,
+ // not the JSON response classes.
+
+ case class CustomerRow(
+ customerId: String,
+ legalName: String,
+ email: String,
+ mobileNumber: String,
+ kycStatus: Boolean
+ )
+
+ case class AccountRow(
+ accountId: String,
+ bankId: String,
+ currency: String,
+ balance: Long,
+ accountName: String,
+ accountType: String
+ )
+
+ case class TransactionRow(
+ transactionId: String,
+ bankId: String,
+ accountId: String,
+ amount: Long,
+ currency: String,
+ transactionType: String,
+ description: String,
+ startDate: Timestamp,
+ finishDate: Timestamp,
+ counterpartyName: String,
+ counterpartyAccount: String,
+ counterpartyBankName: String
+ )
+
+ case class CustomerLinkRow(
+ customerLinkId: String,
+ otherCustomerId: String,
+ otherBankId: String,
+ relationship: String,
+ otherLegalName: String
+ )
+
+ case class AccountLinkRow(
+ customerId: String,
+ accountId: String,
+ bankId: String,
+ relationshipType: String
+ )
+
+ /**
+ * Get customer details by customer ID at a specific bank.
+ */
+ def getCustomerAtBank(customerId: String, bankId: String): Option[CustomerRow] = {
+ logger.info(s"getCustomerAtBank says: customerId=$customerId bankId=$bankId")
+ val query: ConnectionIO[Option[CustomerRow]] =
+ sql"""SELECT mcustomerid, mlegalname, memail, mmobilenumber, mkycstatus
+ FROM mappedcustomer
+ WHERE mcustomerid = $customerId
+ AND mbank = $bankId"""
+ .query[CustomerRow]
+ .option
+
+ DoobieUtil.runQuery(query)
+ }
+
+ /**
+ * Get all accounts linked to a customer via customeraccountlink.
+ */
+ def getAccountsForCustomer(customerId: String): List[AccountRow] = {
+ logger.info(s"getAccountsForCustomer says: customerId=$customerId")
+ val query: ConnectionIO[List[AccountRow]] =
+ sql"""SELECT a.theaccountid, a.bank, a.accountcurrency, a.accountbalance, a.accountname, a.kind
+ FROM mappedbankaccount a
+ JOIN customeraccountlink cal ON cal.accountid = a.theaccountid AND cal.bankid = a.bank
+ WHERE cal.customerid = $customerId"""
+ .query[AccountRow]
+ .to[List]
+
+ DoobieUtil.runQuery(query)
+ }
+
+ /**
+ * Get transactions for a list of accounts at a bank within a date range.
+ */
+ def getTransactionsForAccounts(
+ accountIds: List[String],
+ bankId: String,
+ fromDate: Timestamp,
+ toDate: Timestamp,
+ limit: Int
+ ): List[TransactionRow] = {
+ logger.info(s"getTransactionsForAccounts says: accountIds=${accountIds.size} bankId=$bankId limit=$limit")
+ if (accountIds.isEmpty) return Nil
+
+ val accountIdFragments = accountIds.map(id => fr"$id")
+ val inClause = accountIdFragments.reduceLeft((a, b) => a ++ fr"," ++ b)
+
+ val query: ConnectionIO[List[TransactionRow]] =
+ (fr"""SELECT transactionid, bank, account, amount, currency, transactiontype,
+ description, tstartdate, tfinishdate,
+ counterpartyaccountholder,
+ cpotheraccountroutingaddress,
+ counterpartybankname
+ FROM mappedtransaction
+ WHERE bank = $bankId
+ AND account IN (""" ++ inClause ++ fr""")
+ AND tstartdate >= $fromDate
+ AND tstartdate <= $toDate
+ ORDER BY tstartdate DESC
+ LIMIT $limit""")
+ .query[TransactionRow]
+ .to[List]
+
+ DoobieUtil.runQuery(query)
+ }
+
+ /**
+ * Get customer links (related customers) for a customer,
+ * joined with mappedcustomer to get the related customer's legal name.
+ */
+ def getCustomerLinks(customerId: String): List[CustomerLinkRow] = {
+ logger.info(s"getCustomerLinks says: customerId=$customerId")
+ val query: ConnectionIO[List[CustomerLinkRow]] =
+ sql"""SELECT cl.mcustomerlinkid, cl.mothercustomerid, cl.motherbankid, cl.mrelationshipto,
+ COALESCE(c.mlegalname, '')
+ FROM mappedcustomerlink cl
+ LEFT JOIN mappedcustomer c ON c.mcustomerid = cl.mothercustomerid
+ WHERE cl.mcustomerid = $customerId"""
+ .query[CustomerLinkRow]
+ .to[List]
+
+ DoobieUtil.runQuery(query)
+ }
+
+ /**
+ * Get account links for a customer — which accounts they own/have access to.
+ */
+ def getAccountLinksForCustomer(customerId: String): List[AccountLinkRow] = {
+ logger.info(s"getAccountLinksForCustomer says: customerId=$customerId")
+ val query: ConnectionIO[List[AccountLinkRow]] =
+ sql"""SELECT customerid, accountid, bankid, relationshiptype
+ FROM customeraccountlink
+ WHERE customerid = $customerId"""
+ .query[AccountLinkRow]
+ .to[List]
+
+ DoobieUtil.runQuery(query)
+ }
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/Client.scala b/obp-api/src/main/scala/code/obp/grpc/Client.scala
index c8209f6c16..08fdecbd31 100644
--- a/obp-api/src/main/scala/code/obp/grpc/Client.scala
+++ b/obp-api/src/main/scala/code/obp/grpc/Client.scala
@@ -6,7 +6,7 @@ import com.google.protobuf.empty.Empty
import io.grpc.{ManagedChannel, ManagedChannelBuilder}
object Client extends App {
- private val channelBuilder = ManagedChannelBuilder.forAddress("demo.openbankproject.com", HelloWorldServer.port)
+ private val channelBuilder = ManagedChannelBuilder.forAddress("demo.openbankproject.com", ObpGrpcServer.port)
.usePlaintext()
.asInstanceOf[ManagedChannelBuilder[_]]
val channel: ManagedChannel = channelBuilder.build()
diff --git a/obp-api/src/main/scala/code/obp/grpc/HelloWorldServer.scala b/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala
similarity index 85%
rename from obp-api/src/main/scala/code/obp/grpc/HelloWorldServer.scala
rename to obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala
index c77752fc56..04ff8875a8 100644
--- a/obp-api/src/main/scala/code/obp/grpc/HelloWorldServer.scala
+++ b/obp-api/src/main/scala/code/obp/grpc/ObpGrpcServer.scala
@@ -23,28 +23,36 @@ import net.liftweb.json.{Extraction, JArray}
import scala.concurrent.{ExecutionContext, Future}
/**
- * [[https://github.com/grpc/grpc-java/blob/v0.15.0/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java]]
+ * OBP gRPC server — serves banking RPCs (ObpService) and chat streaming RPCs (ChatStreamService).
+ * Enable via grpc.server.enabled=true in props.
*/
-object HelloWorldServer {
+object ObpGrpcServer {
def main(args: Array[String] = Array.empty): Unit = {
- val server = new HelloWorldServer(ExecutionContext.global)
+ val server = new ObpGrpcServer(ExecutionContext.global)
server.start()
server.blockUntilShutdown()
}
- val port = APIUtil.getPropsAsIntValue("grpc.server.port", Helper.findAvailablePort())
+ val port = APIUtil.getPropsAsIntValue("grpc.server.port", 50051)
}
-class HelloWorldServer(executionContext: ExecutionContext) extends MdcLoggable { self =>
+class ObpGrpcServer(executionContext: ExecutionContext) extends MdcLoggable { self =>
private[this] var server: Server = null
def start(): Unit = {
- val serverBuilder = ServerBuilder.forPort(HelloWorldServer.port)
+ // Start chat event bus for Redis pub/sub streaming
+ code.chat.ChatEventBus.start()
+
+ val serverBuilder = ServerBuilder.forPort(ObpGrpcServer.port)
.addService(ObpServiceGrpc.bindService(ObpServiceImpl, executionContext))
+ .addService(code.obp.grpc.chat.api.ChatStreamServiceGrpc.bindService(
+ code.obp.grpc.chat.ChatStreamServiceImpl, executionContext))
+ .addService(io.grpc.protobuf.services.ProtoReflectionService.newInstance())
+ .intercept(new code.obp.grpc.chat.AuthInterceptor())
.asInstanceOf[ServerBuilder[_]]
server = serverBuilder.build.start;
- logger.info("Server started, listening on " + HelloWorldServer.port)
+ logger.info("Server started, listening on " + ObpGrpcServer.port)
sys.addShutdownHook {
System.err.println("*** shutting down gRPC server since JVM is shutting down")
self.stop()
@@ -53,6 +61,7 @@ class HelloWorldServer(executionContext: ExecutionContext) extends MdcLoggable {
}
def stop(): Unit = {
+ code.chat.ChatEventBus.stop()
if (server != null) {
server.shutdown()
server = null
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/AuthInterceptor.scala b/obp-api/src/main/scala/code/obp/grpc/chat/AuthInterceptor.scala
new file mode 100644
index 0000000000..2cdcf22d77
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/AuthInterceptor.scala
@@ -0,0 +1,97 @@
+package code.obp.grpc.chat
+
+import code.api.util.{APIUtil, AuthHeaderParser, CallContext}
+import code.util.Helper.MdcLoggable
+import com.openbankproject.commons.model.User
+import io.grpc._
+import net.liftweb.common.Full
+import net.liftweb.http.provider.HTTPParam
+
+import scala.concurrent.Await
+import scala.concurrent.duration._
+
+/**
+ * gRPC ServerInterceptor that authenticates requests using OBP's existing auth chain.
+ *
+ * Reads the "authorization" key from gRPC Metadata (same format as HTTP headers:
+ * "DirectLogin token=..." or "Bearer ..."), validates it using the same auth logic
+ * as REST endpoints, and stores the authenticated User in gRPC Context.
+ *
+ * Token is validated once at stream open, not per-message.
+ */
+object AuthInterceptor {
+ val USER_CONTEXT_KEY: Context.Key[User] = Context.key("obp-user")
+ val CALL_CONTEXT_KEY: Context.Key[CallContext] = Context.key("obp-call-context")
+
+ private val AUTH_METADATA_KEY: Metadata.Key[String] =
+ Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER)
+}
+
+class AuthInterceptor extends ServerInterceptor with MdcLoggable {
+
+ import AuthInterceptor._
+
+ override def interceptCall[ReqT, RespT](
+ call: ServerCall[ReqT, RespT],
+ headers: Metadata,
+ next: ServerCallHandler[ReqT, RespT]
+ ): ServerCall.Listener[ReqT] = {
+
+ // Skip auth for gRPC reflection — allow unauthenticated service discovery
+ val serviceName = call.getMethodDescriptor.getServiceName
+ if (serviceName == "grpc.reflection.v1alpha.ServerReflection" || serviceName == "grpc.reflection.v1.ServerReflection") {
+ return Contexts.interceptCall(Context.current(), call, headers, next)
+ }
+
+ val authHeader = Option(headers.get(AUTH_METADATA_KEY))
+
+ authHeader match {
+ case None =>
+ logger.info("AuthInterceptor says: No authorization header in gRPC metadata")
+ call.close(Status.UNAUTHENTICATED.withDescription("Missing authorization header"), new Metadata())
+ new ServerCall.Listener[ReqT]() {}
+
+ case Some(authValue) =>
+ try {
+ // Populate the auth-related CallContext fields via the shared parser
+ // so the gRPC auth path matches what the REST (http4s) path produces.
+ // The downstream auth chain reads authReqHeaderField / directLoginParams
+ // / oAuthParams — not requestHeaders — to pick a scheme.
+ val parsed = AuthHeaderParser.parseAuthorizationHeader(Some(authValue))
+ val cc = CallContext(
+ requestHeaders = List(HTTPParam("Authorization", List(authValue))),
+ authReqHeaderField = parsed.authReqHeaderField,
+ directLoginParams = parsed.directLoginParams,
+ oAuthParams = parsed.oAuthParams,
+ verb = "GET",
+ url = "/grpc/chat",
+ implementedInVersion = "v6.0.0",
+ correlationId = APIUtil.generateUUID()
+ )
+
+ val future = APIUtil.getUserAndSessionContextFuture(cc)
+ val (userBox, callContextOption) = Await.result(future, 30.seconds)
+
+ userBox match {
+ case Full(user) =>
+ val updatedCallContext = callContextOption.getOrElse(cc)
+ val ctx = Context.current()
+ .withValue(USER_CONTEXT_KEY, user)
+ .withValue(CALL_CONTEXT_KEY, updatedCallContext)
+ Contexts.interceptCall(ctx, call, headers, next)
+
+ case _ =>
+ logger.info("AuthInterceptor says: Auth validation returned no user")
+ call.close(Status.UNAUTHENTICATED.withDescription("Invalid or expired token"), new Metadata())
+ new ServerCall.Listener[ReqT]() {}
+ }
+
+ } catch {
+ case e: Throwable =>
+ logger.error(s"AuthInterceptor says: Auth validation failed: ${e.getMessage}")
+ call.close(Status.UNAUTHENTICATED.withDescription("Authentication failed"), new Metadata())
+ new ServerCall.Listener[ReqT]() {}
+ }
+ }
+ }
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/ChatStreamServiceImpl.scala b/obp-api/src/main/scala/code/obp/grpc/chat/ChatStreamServiceImpl.scala
new file mode 100644
index 0000000000..74a0ace2de
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/ChatStreamServiceImpl.scala
@@ -0,0 +1,324 @@
+package code.obp.grpc.chat
+
+import code.chat.{ChatEventBus, ChatPermissions, ParticipantTrait}
+import code.obp.grpc.chat.api._
+import code.util.Helper.MdcLoggable
+import com.google.protobuf.timestamp.Timestamp
+import io.grpc.Status
+import io.grpc.stub.{ServerCallStreamObserver, StreamObserver}
+import net.liftweb.json
+import net.liftweb.json.JsonAST.JValue
+
+import java.time.Instant
+import scala.util.Try
+
+/**
+ * gRPC service implementation for chat streaming.
+ *
+ * All methods require authentication via AuthInterceptor.
+ * User is retrieved from gRPC Context.
+ */
+object ChatStreamServiceImpl extends ChatStreamServiceGrpc.ChatStreamService with MdcLoggable {
+
+ implicit val formats = json.DefaultFormats
+
+ // --- StreamMessages: server-side stream ---
+
+ override def streamMessages(
+ request: StreamMessagesRequest,
+ responseObserver: StreamObserver[ChatMessageEvent]
+ ): Unit = {
+ val user = AuthInterceptor.USER_CONTEXT_KEY.get()
+ if (user == null) {
+ responseObserver.onError(Status.UNAUTHENTICATED.withDescription("Not authenticated").asRuntimeException())
+ return
+ }
+
+ val chatRoomId = request.chatRoomId
+ val userId = user.userId
+
+ // Verify participant
+ if (ChatPermissions.isParticipant(chatRoomId, userId).isEmpty) {
+ responseObserver.onError(Status.PERMISSION_DENIED.withDescription("Not a participant of this chat room").asRuntimeException())
+ return
+ }
+
+ logger.info(s"ChatStreamServiceImpl says: User $userId subscribed to messages in room $chatRoomId")
+
+ // Create a bridge observer that parses JSON events into ChatMessageEvent protobuf messages
+ val jsonObserver = new StreamObserver[String] {
+ override def onNext(jsonPayload: String): Unit = {
+ try {
+ val jValue = json.parse(jsonPayload)
+ val event = jsonToChatMessageEvent(jValue)
+ responseObserver.onNext(event)
+ } catch {
+ case e: Throwable =>
+ logger.warn(s"ChatStreamServiceImpl says: Failed to parse message event: ${e.getMessage}")
+ }
+ }
+ override def onError(t: Throwable): Unit = responseObserver.onError(t)
+ override def onCompleted(): Unit = responseObserver.onCompleted()
+ }
+
+ ChatEventBus.subscribe(s"message:$chatRoomId", jsonObserver)
+
+ // Unsubscribe when client disconnects
+ responseObserver match {
+ case ssco: ServerCallStreamObserver[_] =>
+ ssco.setOnCancelHandler(() => {
+ ChatEventBus.unsubscribe(s"message:$chatRoomId", jsonObserver)
+ logger.info(s"ChatStreamServiceImpl says: User $userId unsubscribed from messages in room $chatRoomId")
+ })
+ case _ =>
+ }
+ }
+
+ // --- StreamTyping: bidi stream ---
+
+ override def streamTyping(
+ responseObserver: StreamObserver[TypingIndicator]
+ ): StreamObserver[TypingEvent] = {
+ val user = AuthInterceptor.USER_CONTEXT_KEY.get()
+ if (user == null) {
+ responseObserver.onError(Status.UNAUTHENTICATED.withDescription("Not authenticated").asRuntimeException())
+ return new StreamObserver[TypingEvent] {
+ override def onNext(value: TypingEvent): Unit = {}
+ override def onError(t: Throwable): Unit = {}
+ override def onCompleted(): Unit = {}
+ }
+ }
+
+ val userId = user.userId
+ val username = user.name
+ val provider = user.provider
+
+ // Track which rooms this client has subscribed to for typing
+ var subscribedRooms = Set.empty[String]
+ var jsonObservers = Map.empty[String, StreamObserver[String]]
+
+ // Return a request observer that handles incoming typing events from this client
+ new StreamObserver[TypingEvent] {
+ override def onNext(event: TypingEvent): Unit = {
+ val chatRoomId = event.chatRoomId
+
+ // Subscribe to typing channel for this room if not already
+ if (!subscribedRooms.contains(chatRoomId)) {
+ val jsonObserver = new StreamObserver[String] {
+ override def onNext(jsonPayload: String): Unit = {
+ try {
+ val jValue = json.parse(jsonPayload)
+ val indicator = jsonToTypingIndicator(jValue)
+ // Filter out own typing events
+ if (indicator.userId != userId) {
+ responseObserver.synchronized {
+ responseObserver.onNext(indicator)
+ }
+ }
+ } catch {
+ case e: Throwable =>
+ logger.warn(s"ChatStreamServiceImpl says: Failed to parse typing event: ${e.getMessage}")
+ }
+ }
+ override def onError(t: Throwable): Unit = {}
+ override def onCompleted(): Unit = {}
+ }
+ ChatEventBus.subscribe(s"typing:$chatRoomId", jsonObserver)
+ subscribedRooms += chatRoomId
+ jsonObservers += (chatRoomId -> jsonObserver)
+ }
+
+ // Publish this client's typing event
+ val payload = json.Serialization.write(
+ Map(
+ "chat_room_id" -> chatRoomId,
+ "user_id" -> userId,
+ "username" -> username,
+ "provider" -> provider,
+ "is_typing" -> event.isTyping
+ )
+ )
+ ChatEventBus.publishTyping(chatRoomId, payload)
+ }
+
+ override def onError(t: Throwable): Unit = cleanup()
+ override def onCompleted(): Unit = cleanup()
+
+ private def cleanup(): Unit = {
+ jsonObservers.foreach { case (room, obs) =>
+ ChatEventBus.unsubscribe(s"typing:$room", obs)
+ }
+ subscribedRooms = Set.empty
+ jsonObservers = Map.empty
+ }
+ }
+ }
+
+ // --- StreamPresence: server-side stream ---
+
+ override def streamPresence(
+ request: StreamPresenceRequest,
+ responseObserver: StreamObserver[PresenceEvent]
+ ): Unit = {
+ val user = AuthInterceptor.USER_CONTEXT_KEY.get()
+ if (user == null) {
+ responseObserver.onError(Status.UNAUTHENTICATED.withDescription("Not authenticated").asRuntimeException())
+ return
+ }
+
+ val chatRoomId = request.chatRoomId
+ val userId = user.userId
+
+ if (ChatPermissions.isParticipant(chatRoomId, userId).isEmpty) {
+ responseObserver.onError(Status.PERMISSION_DENIED.withDescription("Not a participant of this chat room").asRuntimeException())
+ return
+ }
+
+ logger.info(s"ChatStreamServiceImpl says: User $userId subscribed to presence in room $chatRoomId")
+
+ // Publish online event for this user
+ val onlinePayload = json.Serialization.write(
+ Map("user_id" -> userId, "username" -> user.name, "provider" -> user.provider, "is_online" -> true)
+ )
+ ChatEventBus.publishPresence(chatRoomId, onlinePayload)
+
+ // Subscribe to presence events
+ val jsonObserver = new StreamObserver[String] {
+ override def onNext(jsonPayload: String): Unit = {
+ try {
+ val jValue = json.parse(jsonPayload)
+ val event = jsonToPresenceEvent(jValue)
+ responseObserver.onNext(event)
+ } catch {
+ case e: Throwable =>
+ logger.warn(s"ChatStreamServiceImpl says: Failed to parse presence event: ${e.getMessage}")
+ }
+ }
+ override def onError(t: Throwable): Unit = responseObserver.onError(t)
+ override def onCompleted(): Unit = responseObserver.onCompleted()
+ }
+
+ ChatEventBus.subscribe(s"presence:$chatRoomId", jsonObserver)
+
+ // On disconnect: publish offline and unsubscribe
+ responseObserver match {
+ case ssco: ServerCallStreamObserver[_] =>
+ ssco.setOnCancelHandler(() => {
+ ChatEventBus.unsubscribe(s"presence:$chatRoomId", jsonObserver)
+ val offlinePayload = json.Serialization.write(
+ Map("user_id" -> userId, "username" -> user.name, "provider" -> user.provider, "is_online" -> false)
+ )
+ ChatEventBus.publishPresence(chatRoomId, offlinePayload)
+ logger.info(s"ChatStreamServiceImpl says: User $userId went offline in room $chatRoomId")
+ })
+ case _ =>
+ }
+ }
+
+ // --- StreamUnreadCounts: server-side stream ---
+
+ override def streamUnreadCounts(
+ request: StreamUnreadCountsRequest,
+ responseObserver: StreamObserver[UnreadCountEvent]
+ ): Unit = {
+ val user = AuthInterceptor.USER_CONTEXT_KEY.get()
+ if (user == null) {
+ responseObserver.onError(Status.UNAUTHENTICATED.withDescription("Not authenticated").asRuntimeException())
+ return
+ }
+
+ val userId = user.userId
+
+ logger.info(s"ChatStreamServiceImpl says: User $userId subscribed to unread counts")
+
+ // Subscribe to unread count updates for this user
+ // Initial counts can be fetched via the REST GET unread-counts endpoint.
+ // This stream pushes real-time updates as new messages arrive.
+ val jsonObserver = new StreamObserver[String] {
+ override def onNext(jsonPayload: String): Unit = {
+ try {
+ val jValue = json.parse(jsonPayload)
+ val event = jsonToUnreadCountEvent(jValue)
+ responseObserver.onNext(event)
+ } catch {
+ case e: Throwable =>
+ logger.warn(s"ChatStreamServiceImpl says: Failed to parse unread event: ${e.getMessage}")
+ }
+ }
+ override def onError(t: Throwable): Unit = responseObserver.onError(t)
+ override def onCompleted(): Unit = responseObserver.onCompleted()
+ }
+
+ ChatEventBus.subscribe(s"unread:$userId", jsonObserver)
+
+ responseObserver match {
+ case ssco: ServerCallStreamObserver[_] =>
+ ssco.setOnCancelHandler(() => {
+ ChatEventBus.unsubscribe(s"unread:$userId", jsonObserver)
+ logger.info(s"ChatStreamServiceImpl says: User $userId unsubscribed from unread counts")
+ })
+ case _ =>
+ }
+ }
+
+ // --- JSON to protobuf conversion helpers ---
+
+ private def jsonToChatMessageEvent(jv: JValue): ChatMessageEvent = {
+ ChatMessageEvent(
+ eventType = (jv \ "event_type").extractOrElse[String](""),
+ chatMessageId = (jv \ "chat_message_id").extractOrElse[String](""),
+ chatRoomId = (jv \ "chat_room_id").extractOrElse[String](""),
+ senderUserId = (jv \ "sender_user_id").extractOrElse[String](""),
+ senderConsumerId = (jv \ "sender_consumer_id").extractOrElse[String](""),
+ senderUsername = (jv \ "sender_username").extractOrElse[String](""),
+ senderProvider = (jv \ "sender_provider").extractOrElse[String](""),
+ senderConsumerName = (jv \ "sender_consumer_name").extractOrElse[String](""),
+ content = (jv \ "content").extractOrElse[String](""),
+ messageType = (jv \ "message_type").extractOrElse[String](""),
+ mentionedUserIds = (jv \ "mentioned_user_ids").extractOrElse[List[String]](List.empty),
+ replyToMessageId = (jv \ "reply_to_message_id").extractOrElse[String](""),
+ threadId = (jv \ "thread_id").extractOrElse[String](""),
+ isDeleted = (jv \ "is_deleted").extractOrElse[Boolean](false),
+ createdAt = parseIsoToProtoTimestamp((jv \ "created_at").extractOrElse[String]("")),
+ updatedAt = parseIsoToProtoTimestamp((jv \ "updated_at").extractOrElse[String](""))
+ )
+ }
+
+ /**
+ * Parse an ISO-8601 timestamp string (e.g. "2026-04-04T12:58:47Z") into a
+ * protobuf Timestamp. Returns None for empty or unparseable values so the
+ * wire format stays unchanged in the degenerate case.
+ */
+ private def parseIsoToProtoTimestamp(iso: String): Option[Timestamp] = {
+ if (iso.isEmpty) None
+ else Try(Instant.parse(iso)).toOption.map(i =>
+ Timestamp(seconds = i.getEpochSecond, nanos = i.getNano)
+ )
+ }
+
+ private def jsonToTypingIndicator(jv: JValue): TypingIndicator = {
+ TypingIndicator(
+ chatRoomId = (jv \ "chat_room_id").extractOrElse[String](""),
+ userId = (jv \ "user_id").extractOrElse[String](""),
+ username = (jv \ "username").extractOrElse[String](""),
+ provider = (jv \ "provider").extractOrElse[String](""),
+ isTyping = (jv \ "is_typing").extractOrElse[Boolean](false)
+ )
+ }
+
+ private def jsonToPresenceEvent(jv: JValue): PresenceEvent = {
+ PresenceEvent(
+ userId = (jv \ "user_id").extractOrElse[String](""),
+ username = (jv \ "username").extractOrElse[String](""),
+ provider = (jv \ "provider").extractOrElse[String](""),
+ isOnline = (jv \ "is_online").extractOrElse[Boolean](false)
+ )
+ }
+
+ private def jsonToUnreadCountEvent(jv: JValue): UnreadCountEvent = {
+ UnreadCountEvent(
+ chatRoomId = (jv \ "chat_room_id").extractOrElse[String](""),
+ unreadCount = (jv \ "unread_count").extractOrElse[Long](0L)
+ )
+ }
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/ChatMessageEvent.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/ChatMessageEvent.scala
new file mode 100644
index 0000000000..98268d973c
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/ChatMessageEvent.scala
@@ -0,0 +1,437 @@
+// Generated by the Scala Plugin for the Protocol Buffer Compiler.
+// Do not edit!
+//
+// Protofile syntax: PROTO3
+
+package code.obp.grpc.chat.api
+
+@SerialVersionUID(0L)
+final case class ChatMessageEvent(
+ eventType: _root_.scala.Predef.String = "",
+ chatMessageId: _root_.scala.Predef.String = "",
+ chatRoomId: _root_.scala.Predef.String = "",
+ senderUserId: _root_.scala.Predef.String = "",
+ senderConsumerId: _root_.scala.Predef.String = "",
+ senderUsername: _root_.scala.Predef.String = "",
+ senderProvider: _root_.scala.Predef.String = "",
+ senderConsumerName: _root_.scala.Predef.String = "",
+ content: _root_.scala.Predef.String = "",
+ messageType: _root_.scala.Predef.String = "",
+ mentionedUserIds: _root_.scala.Seq[_root_.scala.Predef.String] = _root_.scala.Seq.empty,
+ replyToMessageId: _root_.scala.Predef.String = "",
+ threadId: _root_.scala.Predef.String = "",
+ isDeleted: _root_.scala.Boolean = false,
+ createdAt: _root_.scala.Option[com.google.protobuf.timestamp.Timestamp] = _root_.scala.None,
+ updatedAt: _root_.scala.Option[com.google.protobuf.timestamp.Timestamp] = _root_.scala.None
+ ) extends scalapb.GeneratedMessage with scalapb.Message[ChatMessageEvent] with scalapb.lenses.Updatable[ChatMessageEvent] {
+ @transient
+ private[this] var __serializedSizeCachedValue: _root_.scala.Int = 0
+ private[this] def __computeSerializedValue(): _root_.scala.Int = {
+ var __size = 0
+ if (eventType != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(1, eventType) }
+ if (chatMessageId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(2, chatMessageId) }
+ if (chatRoomId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(3, chatRoomId) }
+ if (senderUserId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(4, senderUserId) }
+ if (senderConsumerId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(5, senderConsumerId) }
+ if (senderUsername != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(6, senderUsername) }
+ if (senderProvider != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(7, senderProvider) }
+ if (senderConsumerName != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(8, senderConsumerName) }
+ if (content != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(9, content) }
+ if (messageType != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(10, messageType) }
+ mentionedUserIds.foreach { __item =>
+ __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(11, __item)
+ }
+ if (replyToMessageId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(12, replyToMessageId) }
+ if (threadId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(13, threadId) }
+ if (isDeleted != false) { __size += _root_.com.google.protobuf.CodedOutputStream.computeBoolSize(14, isDeleted) }
+ if (createdAt.isDefined) {
+ val __v = createdAt.get
+ val __s = __v.serializedSize
+ __size += 1 + _root_.com.google.protobuf.CodedOutputStream.computeUInt32SizeNoTag(__s) + __s
+ }
+ if (updatedAt.isDefined) {
+ val __v = updatedAt.get
+ val __s = __v.serializedSize
+ __size += 2 + _root_.com.google.protobuf.CodedOutputStream.computeUInt32SizeNoTag(__s) + __s
+ }
+ __size
+ }
+ final override def serializedSize: _root_.scala.Int = {
+ var read = __serializedSizeCachedValue
+ if (read == 0) {
+ read = __computeSerializedValue()
+ __serializedSizeCachedValue = read
+ }
+ read
+ }
+ def writeTo(`_output__`: _root_.com.google.protobuf.CodedOutputStream): _root_.scala.Unit = {
+ {
+ val __v = eventType
+ if (__v != "") {
+ _output__.writeString(1, __v)
+ }
+ };
+ {
+ val __v = chatMessageId
+ if (__v != "") {
+ _output__.writeString(2, __v)
+ }
+ };
+ {
+ val __v = chatRoomId
+ if (__v != "") {
+ _output__.writeString(3, __v)
+ }
+ };
+ {
+ val __v = senderUserId
+ if (__v != "") {
+ _output__.writeString(4, __v)
+ }
+ };
+ {
+ val __v = senderConsumerId
+ if (__v != "") {
+ _output__.writeString(5, __v)
+ }
+ };
+ {
+ val __v = senderUsername
+ if (__v != "") {
+ _output__.writeString(6, __v)
+ }
+ };
+ {
+ val __v = senderProvider
+ if (__v != "") {
+ _output__.writeString(7, __v)
+ }
+ };
+ {
+ val __v = senderConsumerName
+ if (__v != "") {
+ _output__.writeString(8, __v)
+ }
+ };
+ {
+ val __v = content
+ if (__v != "") {
+ _output__.writeString(9, __v)
+ }
+ };
+ {
+ val __v = messageType
+ if (__v != "") {
+ _output__.writeString(10, __v)
+ }
+ };
+ mentionedUserIds.foreach { __v =>
+ _output__.writeString(11, __v)
+ };
+ {
+ val __v = replyToMessageId
+ if (__v != "") {
+ _output__.writeString(12, __v)
+ }
+ };
+ {
+ val __v = threadId
+ if (__v != "") {
+ _output__.writeString(13, __v)
+ }
+ };
+ {
+ val __v = isDeleted
+ if (__v != false) {
+ _output__.writeBool(14, __v)
+ }
+ };
+ createdAt.foreach { __v =>
+ _output__.writeTag(15, 2)
+ _output__.writeUInt32NoTag(__v.serializedSize)
+ __v.writeTo(_output__)
+ };
+ updatedAt.foreach { __v =>
+ _output__.writeTag(16, 2)
+ _output__.writeUInt32NoTag(__v.serializedSize)
+ __v.writeTo(_output__)
+ };
+ }
+ def mergeFrom(`_input__`: _root_.com.google.protobuf.CodedInputStream): code.obp.grpc.chat.api.ChatMessageEvent = {
+ var __eventType = this.eventType
+ var __chatMessageId = this.chatMessageId
+ var __chatRoomId = this.chatRoomId
+ var __senderUserId = this.senderUserId
+ var __senderConsumerId = this.senderConsumerId
+ var __senderUsername = this.senderUsername
+ var __senderProvider = this.senderProvider
+ var __senderConsumerName = this.senderConsumerName
+ var __content = this.content
+ var __messageType = this.messageType
+ val __mentionedUserIds = (_root_.scala.collection.immutable.Vector.newBuilder[_root_.scala.Predef.String] ++= this.mentionedUserIds)
+ var __replyToMessageId = this.replyToMessageId
+ var __threadId = this.threadId
+ var __isDeleted = this.isDeleted
+ var __createdAt = this.createdAt
+ var __updatedAt = this.updatedAt
+ var _done__ = false
+ while (!_done__) {
+ val _tag__ = _input__.readTag()
+ _tag__ match {
+ case 0 => _done__ = true
+ case 10 =>
+ __eventType = _input__.readString()
+ case 18 =>
+ __chatMessageId = _input__.readString()
+ case 26 =>
+ __chatRoomId = _input__.readString()
+ case 34 =>
+ __senderUserId = _input__.readString()
+ case 42 =>
+ __senderConsumerId = _input__.readString()
+ case 50 =>
+ __senderUsername = _input__.readString()
+ case 58 =>
+ __senderProvider = _input__.readString()
+ case 66 =>
+ __senderConsumerName = _input__.readString()
+ case 74 =>
+ __content = _input__.readString()
+ case 82 =>
+ __messageType = _input__.readString()
+ case 90 =>
+ __mentionedUserIds += _input__.readString()
+ case 98 =>
+ __replyToMessageId = _input__.readString()
+ case 106 =>
+ __threadId = _input__.readString()
+ case 112 =>
+ __isDeleted = _input__.readBool()
+ case 122 =>
+ __createdAt = _root_.scala.Option(_root_.scalapb.LiteParser.readMessage(_input__, __createdAt.getOrElse(com.google.protobuf.timestamp.Timestamp.defaultInstance)))
+ case 130 =>
+ __updatedAt = _root_.scala.Option(_root_.scalapb.LiteParser.readMessage(_input__, __updatedAt.getOrElse(com.google.protobuf.timestamp.Timestamp.defaultInstance)))
+ case tag => _input__.skipField(tag)
+ }
+ }
+ code.obp.grpc.chat.api.ChatMessageEvent(
+ eventType = __eventType,
+ chatMessageId = __chatMessageId,
+ chatRoomId = __chatRoomId,
+ senderUserId = __senderUserId,
+ senderConsumerId = __senderConsumerId,
+ senderUsername = __senderUsername,
+ senderProvider = __senderProvider,
+ senderConsumerName = __senderConsumerName,
+ content = __content,
+ messageType = __messageType,
+ mentionedUserIds = __mentionedUserIds.result(),
+ replyToMessageId = __replyToMessageId,
+ threadId = __threadId,
+ isDeleted = __isDeleted,
+ createdAt = __createdAt,
+ updatedAt = __updatedAt
+ )
+ }
+ def getCreatedAt: com.google.protobuf.timestamp.Timestamp = createdAt.getOrElse(com.google.protobuf.timestamp.Timestamp.defaultInstance)
+ def getUpdatedAt: com.google.protobuf.timestamp.Timestamp = updatedAt.getOrElse(com.google.protobuf.timestamp.Timestamp.defaultInstance)
+ def withEventType(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(eventType = __v)
+ def withChatMessageId(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(chatMessageId = __v)
+ def withChatRoomId(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(chatRoomId = __v)
+ def withSenderUserId(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(senderUserId = __v)
+ def withSenderConsumerId(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(senderConsumerId = __v)
+ def withSenderUsername(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(senderUsername = __v)
+ def withSenderProvider(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(senderProvider = __v)
+ def withSenderConsumerName(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(senderConsumerName = __v)
+ def withContent(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(content = __v)
+ def withMessageType(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(messageType = __v)
+ def withMentionedUserIds(__v: _root_.scala.Seq[_root_.scala.Predef.String]): ChatMessageEvent = copy(mentionedUserIds = __v)
+ def addMentionedUserIds(__vs: _root_.scala.Predef.String*): ChatMessageEvent = addAllMentionedUserIds(__vs)
+ def addAllMentionedUserIds(__vs: _root_.scala.Iterable[_root_.scala.Predef.String]): ChatMessageEvent = copy(mentionedUserIds = mentionedUserIds ++ __vs)
+ def withReplyToMessageId(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(replyToMessageId = __v)
+ def withThreadId(__v: _root_.scala.Predef.String): ChatMessageEvent = copy(threadId = __v)
+ def withIsDeleted(__v: _root_.scala.Boolean): ChatMessageEvent = copy(isDeleted = __v)
+ def clearCreatedAt: ChatMessageEvent = copy(createdAt = _root_.scala.None)
+ def withCreatedAt(__v: com.google.protobuf.timestamp.Timestamp): ChatMessageEvent = copy(createdAt = _root_.scala.Option(__v))
+ def clearUpdatedAt: ChatMessageEvent = copy(updatedAt = _root_.scala.None)
+ def withUpdatedAt(__v: com.google.protobuf.timestamp.Timestamp): ChatMessageEvent = copy(updatedAt = _root_.scala.Option(__v))
+ def getFieldByNumber(__fieldNumber: _root_.scala.Int): scala.Any = {
+ (__fieldNumber: @_root_.scala.unchecked) match {
+ case 1 => {
+ val __t = eventType
+ if (__t != "") __t else null
+ }
+ case 2 => {
+ val __t = chatMessageId
+ if (__t != "") __t else null
+ }
+ case 3 => {
+ val __t = chatRoomId
+ if (__t != "") __t else null
+ }
+ case 4 => {
+ val __t = senderUserId
+ if (__t != "") __t else null
+ }
+ case 5 => {
+ val __t = senderConsumerId
+ if (__t != "") __t else null
+ }
+ case 6 => {
+ val __t = senderUsername
+ if (__t != "") __t else null
+ }
+ case 7 => {
+ val __t = senderProvider
+ if (__t != "") __t else null
+ }
+ case 8 => {
+ val __t = senderConsumerName
+ if (__t != "") __t else null
+ }
+ case 9 => {
+ val __t = content
+ if (__t != "") __t else null
+ }
+ case 10 => {
+ val __t = messageType
+ if (__t != "") __t else null
+ }
+ case 11 => mentionedUserIds
+ case 12 => {
+ val __t = replyToMessageId
+ if (__t != "") __t else null
+ }
+ case 13 => {
+ val __t = threadId
+ if (__t != "") __t else null
+ }
+ case 14 => {
+ val __t = isDeleted
+ if (__t != false) __t else null
+ }
+ case 15 => createdAt.orNull
+ case 16 => updatedAt.orNull
+ }
+ }
+ def getField(__field: _root_.scalapb.descriptors.FieldDescriptor): _root_.scalapb.descriptors.PValue = {
+ require(__field.containingMessage eq companion.scalaDescriptor)
+ (__field.number: @_root_.scala.unchecked) match {
+ case 1 => _root_.scalapb.descriptors.PString(eventType)
+ case 2 => _root_.scalapb.descriptors.PString(chatMessageId)
+ case 3 => _root_.scalapb.descriptors.PString(chatRoomId)
+ case 4 => _root_.scalapb.descriptors.PString(senderUserId)
+ case 5 => _root_.scalapb.descriptors.PString(senderConsumerId)
+ case 6 => _root_.scalapb.descriptors.PString(senderUsername)
+ case 7 => _root_.scalapb.descriptors.PString(senderProvider)
+ case 8 => _root_.scalapb.descriptors.PString(senderConsumerName)
+ case 9 => _root_.scalapb.descriptors.PString(content)
+ case 10 => _root_.scalapb.descriptors.PString(messageType)
+ case 11 => _root_.scalapb.descriptors.PRepeated(mentionedUserIds.iterator.map(_root_.scalapb.descriptors.PString).toVector)
+ case 12 => _root_.scalapb.descriptors.PString(replyToMessageId)
+ case 13 => _root_.scalapb.descriptors.PString(threadId)
+ case 14 => _root_.scalapb.descriptors.PBoolean(isDeleted)
+ case 15 => createdAt.map(_.toPMessage).getOrElse(_root_.scalapb.descriptors.PEmpty)
+ case 16 => updatedAt.map(_.toPMessage).getOrElse(_root_.scalapb.descriptors.PEmpty)
+ }
+ }
+ def toProtoString: _root_.scala.Predef.String = _root_.scalapb.TextFormat.printToUnicodeString(this)
+ def companion = code.obp.grpc.chat.api.ChatMessageEvent
+}
+
+object ChatMessageEvent extends scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.ChatMessageEvent] {
+ implicit def messageCompanion: scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.ChatMessageEvent] = this
+ def fromFieldsMap(__fieldsMap: scala.collection.immutable.Map[_root_.com.google.protobuf.Descriptors.FieldDescriptor, scala.Any]): code.obp.grpc.chat.api.ChatMessageEvent = {
+ require(__fieldsMap.keys.forall(_.getContainingType() == javaDescriptor), "FieldDescriptor does not match message type.")
+ val __fields = javaDescriptor.getFields
+ code.obp.grpc.chat.api.ChatMessageEvent(
+ __fieldsMap.getOrElse(__fields.get(0), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(1), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(2), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(3), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(4), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(5), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(6), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(7), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(8), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(9), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(10), Nil).asInstanceOf[_root_.scala.Seq[_root_.scala.Predef.String]],
+ __fieldsMap.getOrElse(__fields.get(11), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(12), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(13), false).asInstanceOf[_root_.scala.Boolean],
+ __fieldsMap.get(__fields.get(14)).asInstanceOf[_root_.scala.Option[com.google.protobuf.timestamp.Timestamp]],
+ __fieldsMap.get(__fields.get(15)).asInstanceOf[_root_.scala.Option[com.google.protobuf.timestamp.Timestamp]]
+ )
+ }
+ implicit def messageReads: _root_.scalapb.descriptors.Reads[code.obp.grpc.chat.api.ChatMessageEvent] = _root_.scalapb.descriptors.Reads{
+ case _root_.scalapb.descriptors.PMessage(__fieldsMap) =>
+ require(__fieldsMap.keys.forall(_.containingMessage == scalaDescriptor), "FieldDescriptor does not match message type.")
+ code.obp.grpc.chat.api.ChatMessageEvent(
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(1).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(2).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(3).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(4).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(5).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(6).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(7).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(8).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(9).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(10).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(11).get).map(_.as[_root_.scala.Seq[_root_.scala.Predef.String]]).getOrElse(_root_.scala.Seq.empty),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(12).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(13).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(14).get).map(_.as[_root_.scala.Boolean]).getOrElse(false),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(15).get).flatMap(_.as[_root_.scala.Option[com.google.protobuf.timestamp.Timestamp]]),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(16).get).flatMap(_.as[_root_.scala.Option[com.google.protobuf.timestamp.Timestamp]])
+ )
+ case _ => throw new RuntimeException("Expected PMessage")
+ }
+ def javaDescriptor: _root_.com.google.protobuf.Descriptors.Descriptor = ChatProto.javaDescriptor.getMessageTypes.get(1)
+ def scalaDescriptor: _root_.scalapb.descriptors.Descriptor = throw new UnsupportedOperationException("scalaDescriptor not available")
+ def messageCompanionForFieldNumber(__number: _root_.scala.Int): _root_.scalapb.GeneratedMessageCompanion[_] = {
+ (__number: @_root_.scala.unchecked) match {
+ case 15 => com.google.protobuf.timestamp.Timestamp
+ case 16 => com.google.protobuf.timestamp.Timestamp
+ }
+ }
+ lazy val nestedMessagesCompanions: Seq[_root_.scalapb.GeneratedMessageCompanion[_]] = Seq.empty
+ def enumCompanionForFieldNumber(__fieldNumber: _root_.scala.Int): _root_.scalapb.GeneratedEnumCompanion[_] = throw new MatchError(__fieldNumber)
+ lazy val defaultInstance = code.obp.grpc.chat.api.ChatMessageEvent(
+ )
+ implicit class ChatMessageEventLens[UpperPB](_l: _root_.scalapb.lenses.Lens[UpperPB, code.obp.grpc.chat.api.ChatMessageEvent]) extends _root_.scalapb.lenses.ObjectLens[UpperPB, code.obp.grpc.chat.api.ChatMessageEvent](_l) {
+ def eventType: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.eventType)((c_, f_) => c_.copy(eventType = f_))
+ def chatMessageId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.chatMessageId)((c_, f_) => c_.copy(chatMessageId = f_))
+ def chatRoomId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.chatRoomId)((c_, f_) => c_.copy(chatRoomId = f_))
+ def senderUserId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.senderUserId)((c_, f_) => c_.copy(senderUserId = f_))
+ def senderConsumerId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.senderConsumerId)((c_, f_) => c_.copy(senderConsumerId = f_))
+ def senderUsername: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.senderUsername)((c_, f_) => c_.copy(senderUsername = f_))
+ def senderProvider: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.senderProvider)((c_, f_) => c_.copy(senderProvider = f_))
+ def senderConsumerName: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.senderConsumerName)((c_, f_) => c_.copy(senderConsumerName = f_))
+ def content: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.content)((c_, f_) => c_.copy(content = f_))
+ def messageType: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.messageType)((c_, f_) => c_.copy(messageType = f_))
+ def mentionedUserIds: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Seq[_root_.scala.Predef.String]] = field(_.mentionedUserIds)((c_, f_) => c_.copy(mentionedUserIds = f_))
+ def replyToMessageId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.replyToMessageId)((c_, f_) => c_.copy(replyToMessageId = f_))
+ def threadId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.threadId)((c_, f_) => c_.copy(threadId = f_))
+ def isDeleted: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Boolean] = field(_.isDeleted)((c_, f_) => c_.copy(isDeleted = f_))
+ def createdAt: _root_.scalapb.lenses.Lens[UpperPB, com.google.protobuf.timestamp.Timestamp] = field(_.getCreatedAt)((c_, f_) => c_.copy(createdAt = _root_.scala.Option(f_)))
+ def optionalCreatedAt: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Option[com.google.protobuf.timestamp.Timestamp]] = field(_.createdAt)((c_, f_) => c_.copy(createdAt = f_))
+ def updatedAt: _root_.scalapb.lenses.Lens[UpperPB, com.google.protobuf.timestamp.Timestamp] = field(_.getUpdatedAt)((c_, f_) => c_.copy(updatedAt = _root_.scala.Option(f_)))
+ def optionalUpdatedAt: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Option[com.google.protobuf.timestamp.Timestamp]] = field(_.updatedAt)((c_, f_) => c_.copy(updatedAt = f_))
+ }
+ final val EVENTTYPE_FIELD_NUMBER = 1
+ final val CHATMESSAGEID_FIELD_NUMBER = 2
+ final val CHATROOMID_FIELD_NUMBER = 3
+ final val SENDERUSERID_FIELD_NUMBER = 4
+ final val SENDERCONSUMERID_FIELD_NUMBER = 5
+ final val SENDERUSERNAME_FIELD_NUMBER = 6
+ final val SENDERPROVIDER_FIELD_NUMBER = 7
+ final val SENDERCONSUMERNAME_FIELD_NUMBER = 8
+ final val CONTENT_FIELD_NUMBER = 9
+ final val MESSAGETYPE_FIELD_NUMBER = 10
+ final val MENTIONEDUSERIDS_FIELD_NUMBER = 11
+ final val REPLYTOMESSAGEID_FIELD_NUMBER = 12
+ final val THREADID_FIELD_NUMBER = 13
+ final val ISDELETED_FIELD_NUMBER = 14
+ final val CREATEDAT_FIELD_NUMBER = 15
+ final val UPDATEDAT_FIELD_NUMBER = 16
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/ChatProto.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/ChatProto.scala
new file mode 100644
index 0000000000..e24b39e814
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/ChatProto.scala
@@ -0,0 +1,148 @@
+package code.obp.grpc.chat.api
+
+import com.google.protobuf.DescriptorProtos._
+import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.{Label, Type}
+
+/**
+ * Proto file descriptor for the chat streaming service.
+ * Built programmatically to support gRPC reflection (service discovery).
+ */
+object ChatProto {
+
+ lazy val javaDescriptor: com.google.protobuf.Descriptors.FileDescriptor = {
+ val fileProto = FileDescriptorProto.newBuilder()
+ .setName("chat.proto")
+ .setPackage("code.obp.grpc.chat.g1")
+ .setSyntax("proto3")
+ .addDependency("google/protobuf/timestamp.proto")
+ // StreamMessagesRequest
+ .addMessageType(DescriptorProto.newBuilder()
+ .setName("StreamMessagesRequest")
+ .addField(stringField("chat_room_id", 1))
+ )
+ // ChatMessageEvent
+ .addMessageType(DescriptorProto.newBuilder()
+ .setName("ChatMessageEvent")
+ .addField(stringField("event_type", 1))
+ .addField(stringField("chat_message_id", 2))
+ .addField(stringField("chat_room_id", 3))
+ .addField(stringField("sender_user_id", 4))
+ .addField(stringField("sender_consumer_id", 5))
+ .addField(stringField("sender_username", 6))
+ .addField(stringField("sender_provider", 7))
+ .addField(stringField("sender_consumer_name", 8))
+ .addField(stringField("content", 9))
+ .addField(stringField("message_type", 10))
+ .addField(repeatedStringField("mentioned_user_ids", 11))
+ .addField(stringField("reply_to_message_id", 12))
+ .addField(stringField("thread_id", 13))
+ .addField(boolField("is_deleted", 14))
+ .addField(messageField("created_at", 15, ".google.protobuf.Timestamp"))
+ .addField(messageField("updated_at", 16, ".google.protobuf.Timestamp"))
+ )
+ // TypingEvent
+ .addMessageType(DescriptorProto.newBuilder()
+ .setName("TypingEvent")
+ .addField(stringField("chat_room_id", 1))
+ .addField(boolField("is_typing", 2))
+ )
+ // TypingIndicator
+ .addMessageType(DescriptorProto.newBuilder()
+ .setName("TypingIndicator")
+ .addField(stringField("chat_room_id", 1))
+ .addField(stringField("user_id", 2))
+ .addField(stringField("username", 3))
+ .addField(stringField("provider", 4))
+ .addField(boolField("is_typing", 5))
+ )
+ // StreamPresenceRequest
+ .addMessageType(DescriptorProto.newBuilder()
+ .setName("StreamPresenceRequest")
+ .addField(stringField("chat_room_id", 1))
+ )
+ // PresenceEvent
+ .addMessageType(DescriptorProto.newBuilder()
+ .setName("PresenceEvent")
+ .addField(stringField("user_id", 1))
+ .addField(stringField("username", 2))
+ .addField(stringField("provider", 3))
+ .addField(boolField("is_online", 4))
+ )
+ // StreamUnreadCountsRequest
+ .addMessageType(DescriptorProto.newBuilder()
+ .setName("StreamUnreadCountsRequest")
+ )
+ // UnreadCountEvent
+ .addMessageType(DescriptorProto.newBuilder()
+ .setName("UnreadCountEvent")
+ .addField(stringField("chat_room_id", 1))
+ .addField(int64Field("unread_count", 2))
+ )
+ // ChatStreamService
+ .addService(ServiceDescriptorProto.newBuilder()
+ .setName("ChatStreamService")
+ .addMethod(MethodDescriptorProto.newBuilder()
+ .setName("StreamMessages")
+ .setInputType(".code.obp.grpc.chat.g1.StreamMessagesRequest")
+ .setOutputType(".code.obp.grpc.chat.g1.ChatMessageEvent")
+ .setServerStreaming(true)
+ )
+ .addMethod(MethodDescriptorProto.newBuilder()
+ .setName("StreamTyping")
+ .setInputType(".code.obp.grpc.chat.g1.TypingEvent")
+ .setOutputType(".code.obp.grpc.chat.g1.TypingIndicator")
+ .setClientStreaming(true)
+ .setServerStreaming(true)
+ )
+ .addMethod(MethodDescriptorProto.newBuilder()
+ .setName("StreamPresence")
+ .setInputType(".code.obp.grpc.chat.g1.StreamPresenceRequest")
+ .setOutputType(".code.obp.grpc.chat.g1.PresenceEvent")
+ .setServerStreaming(true)
+ )
+ .addMethod(MethodDescriptorProto.newBuilder()
+ .setName("StreamUnreadCounts")
+ .setInputType(".code.obp.grpc.chat.g1.StreamUnreadCountsRequest")
+ .setOutputType(".code.obp.grpc.chat.g1.UnreadCountEvent")
+ .setServerStreaming(true)
+ )
+ )
+ .build()
+
+ com.google.protobuf.Descriptors.FileDescriptor.buildFrom(
+ fileProto,
+ Array(com.google.protobuf.TimestampProto.getDescriptor)
+ )
+ }
+
+ private def stringField(name: String, number: Int): FieldDescriptorProto.Builder =
+ FieldDescriptorProto.newBuilder()
+ .setName(name).setNumber(number)
+ .setType(Type.TYPE_STRING)
+ .setLabel(Label.LABEL_OPTIONAL)
+
+ private def repeatedStringField(name: String, number: Int): FieldDescriptorProto.Builder =
+ FieldDescriptorProto.newBuilder()
+ .setName(name).setNumber(number)
+ .setType(Type.TYPE_STRING)
+ .setLabel(Label.LABEL_REPEATED)
+
+ private def boolField(name: String, number: Int): FieldDescriptorProto.Builder =
+ FieldDescriptorProto.newBuilder()
+ .setName(name).setNumber(number)
+ .setType(Type.TYPE_BOOL)
+ .setLabel(Label.LABEL_OPTIONAL)
+
+ private def int64Field(name: String, number: Int): FieldDescriptorProto.Builder =
+ FieldDescriptorProto.newBuilder()
+ .setName(name).setNumber(number)
+ .setType(Type.TYPE_INT64)
+ .setLabel(Label.LABEL_OPTIONAL)
+
+ private def messageField(name: String, number: Int, typeName: String): FieldDescriptorProto.Builder =
+ FieldDescriptorProto.newBuilder()
+ .setName(name).setNumber(number)
+ .setType(Type.TYPE_MESSAGE)
+ .setTypeName(typeName)
+ .setLabel(Label.LABEL_OPTIONAL)
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/ChatStreamServiceGrpc.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/ChatStreamServiceGrpc.scala
new file mode 100644
index 0000000000..64b5ef966b
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/ChatStreamServiceGrpc.scala
@@ -0,0 +1,114 @@
+// Generated by the Scala Plugin for the Protocol Buffer Compiler.
+// Do not edit!
+//
+// Protofile syntax: PROTO3
+
+package code.obp.grpc.chat.api
+
+object ChatStreamServiceGrpc {
+
+ val METHOD_STREAM_MESSAGES: _root_.io.grpc.MethodDescriptor[code.obp.grpc.chat.api.StreamMessagesRequest, code.obp.grpc.chat.api.ChatMessageEvent] =
+ _root_.io.grpc.MethodDescriptor.newBuilder()
+ .setType(_root_.io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
+ .setFullMethodName(_root_.io.grpc.MethodDescriptor.generateFullMethodName("code.obp.grpc.chat.g1.ChatStreamService", "StreamMessages"))
+ .setSampledToLocalTracing(true)
+ .setRequestMarshaller(new scalapb.grpc.Marshaller(code.obp.grpc.chat.api.StreamMessagesRequest))
+ .setResponseMarshaller(new scalapb.grpc.Marshaller(code.obp.grpc.chat.api.ChatMessageEvent))
+ .build()
+
+ val METHOD_STREAM_TYPING: _root_.io.grpc.MethodDescriptor[code.obp.grpc.chat.api.TypingEvent, code.obp.grpc.chat.api.TypingIndicator] =
+ _root_.io.grpc.MethodDescriptor.newBuilder()
+ .setType(_root_.io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)
+ .setFullMethodName(_root_.io.grpc.MethodDescriptor.generateFullMethodName("code.obp.grpc.chat.g1.ChatStreamService", "StreamTyping"))
+ .setSampledToLocalTracing(true)
+ .setRequestMarshaller(new scalapb.grpc.Marshaller(code.obp.grpc.chat.api.TypingEvent))
+ .setResponseMarshaller(new scalapb.grpc.Marshaller(code.obp.grpc.chat.api.TypingIndicator))
+ .build()
+
+ val METHOD_STREAM_PRESENCE: _root_.io.grpc.MethodDescriptor[code.obp.grpc.chat.api.StreamPresenceRequest, code.obp.grpc.chat.api.PresenceEvent] =
+ _root_.io.grpc.MethodDescriptor.newBuilder()
+ .setType(_root_.io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
+ .setFullMethodName(_root_.io.grpc.MethodDescriptor.generateFullMethodName("code.obp.grpc.chat.g1.ChatStreamService", "StreamPresence"))
+ .setSampledToLocalTracing(true)
+ .setRequestMarshaller(new scalapb.grpc.Marshaller(code.obp.grpc.chat.api.StreamPresenceRequest))
+ .setResponseMarshaller(new scalapb.grpc.Marshaller(code.obp.grpc.chat.api.PresenceEvent))
+ .build()
+
+ val METHOD_STREAM_UNREAD_COUNTS: _root_.io.grpc.MethodDescriptor[code.obp.grpc.chat.api.StreamUnreadCountsRequest, code.obp.grpc.chat.api.UnreadCountEvent] =
+ _root_.io.grpc.MethodDescriptor.newBuilder()
+ .setType(_root_.io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
+ .setFullMethodName(_root_.io.grpc.MethodDescriptor.generateFullMethodName("code.obp.grpc.chat.g1.ChatStreamService", "StreamUnreadCounts"))
+ .setSampledToLocalTracing(true)
+ .setRequestMarshaller(new scalapb.grpc.Marshaller(code.obp.grpc.chat.api.StreamUnreadCountsRequest))
+ .setResponseMarshaller(new scalapb.grpc.Marshaller(code.obp.grpc.chat.api.UnreadCountEvent))
+ .build()
+
+ val SERVICE: _root_.io.grpc.ServiceDescriptor =
+ _root_.io.grpc.ServiceDescriptor.newBuilder("code.obp.grpc.chat.g1.ChatStreamService")
+ .setSchemaDescriptor(new _root_.scalapb.grpc.ConcreteProtoFileDescriptorSupplier(code.obp.grpc.chat.api.ChatProto.javaDescriptor))
+ .addMethod(METHOD_STREAM_MESSAGES)
+ .addMethod(METHOD_STREAM_TYPING)
+ .addMethod(METHOD_STREAM_PRESENCE)
+ .addMethod(METHOD_STREAM_UNREAD_COUNTS)
+ .build()
+
+ trait ChatStreamService extends _root_.scalapb.grpc.AbstractService {
+ override def serviceCompanion = ChatStreamService
+
+ /** Server-side stream: pushes new/updated/deleted messages for a room */
+ def streamMessages(request: code.obp.grpc.chat.api.StreamMessagesRequest,
+ responseObserver: _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.ChatMessageEvent]): Unit
+
+ /** Bidi stream: client sends typing events, server broadcasts others' typing */
+ def streamTyping(responseObserver: _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.TypingIndicator]): _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.TypingEvent]
+
+ /** Server-side stream: online/offline status changes for room participants */
+ def streamPresence(request: code.obp.grpc.chat.api.StreamPresenceRequest,
+ responseObserver: _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.PresenceEvent]): Unit
+
+ /** Server-side stream: unread count updates for all user's rooms */
+ def streamUnreadCounts(request: code.obp.grpc.chat.api.StreamUnreadCountsRequest,
+ responseObserver: _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.UnreadCountEvent]): Unit
+ }
+
+ object ChatStreamService extends _root_.scalapb.grpc.ServiceCompanion[ChatStreamService] {
+ implicit def serviceCompanion: _root_.scalapb.grpc.ServiceCompanion[ChatStreamService] = this
+ def javaDescriptor: _root_.com.google.protobuf.Descriptors.ServiceDescriptor =
+ code.obp.grpc.chat.api.ChatProto.javaDescriptor.getServices().get(0)
+ }
+
+ def bindService(serviceImpl: ChatStreamService, executionContext: scala.concurrent.ExecutionContext): _root_.io.grpc.ServerServiceDefinition =
+ _root_.io.grpc.ServerServiceDefinition.builder(SERVICE)
+ .addMethod(
+ METHOD_STREAM_MESSAGES,
+ _root_.io.grpc.stub.ServerCalls.asyncServerStreamingCall(
+ new _root_.io.grpc.stub.ServerCalls.ServerStreamingMethod[code.obp.grpc.chat.api.StreamMessagesRequest, code.obp.grpc.chat.api.ChatMessageEvent] {
+ override def invoke(request: code.obp.grpc.chat.api.StreamMessagesRequest,
+ responseObserver: _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.ChatMessageEvent]): Unit =
+ serviceImpl.streamMessages(request, responseObserver)
+ }))
+ .addMethod(
+ METHOD_STREAM_TYPING,
+ _root_.io.grpc.stub.ServerCalls.asyncBidiStreamingCall(
+ new _root_.io.grpc.stub.ServerCalls.BidiStreamingMethod[code.obp.grpc.chat.api.TypingEvent, code.obp.grpc.chat.api.TypingIndicator] {
+ override def invoke(responseObserver: _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.TypingIndicator]): _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.TypingEvent] =
+ serviceImpl.streamTyping(responseObserver)
+ }))
+ .addMethod(
+ METHOD_STREAM_PRESENCE,
+ _root_.io.grpc.stub.ServerCalls.asyncServerStreamingCall(
+ new _root_.io.grpc.stub.ServerCalls.ServerStreamingMethod[code.obp.grpc.chat.api.StreamPresenceRequest, code.obp.grpc.chat.api.PresenceEvent] {
+ override def invoke(request: code.obp.grpc.chat.api.StreamPresenceRequest,
+ responseObserver: _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.PresenceEvent]): Unit =
+ serviceImpl.streamPresence(request, responseObserver)
+ }))
+ .addMethod(
+ METHOD_STREAM_UNREAD_COUNTS,
+ _root_.io.grpc.stub.ServerCalls.asyncServerStreamingCall(
+ new _root_.io.grpc.stub.ServerCalls.ServerStreamingMethod[code.obp.grpc.chat.api.StreamUnreadCountsRequest, code.obp.grpc.chat.api.UnreadCountEvent] {
+ override def invoke(request: code.obp.grpc.chat.api.StreamUnreadCountsRequest,
+ responseObserver: _root_.io.grpc.stub.StreamObserver[code.obp.grpc.chat.api.UnreadCountEvent]): Unit =
+ serviceImpl.streamUnreadCounts(request, responseObserver)
+ }))
+ .build()
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/PresenceEvent.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/PresenceEvent.scala
new file mode 100644
index 0000000000..bc2480d819
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/PresenceEvent.scala
@@ -0,0 +1,164 @@
+// Generated by the Scala Plugin for the Protocol Buffer Compiler.
+// Do not edit!
+//
+// Protofile syntax: PROTO3
+
+package code.obp.grpc.chat.api
+
+@SerialVersionUID(0L)
+final case class PresenceEvent(
+ userId: _root_.scala.Predef.String = "",
+ username: _root_.scala.Predef.String = "",
+ provider: _root_.scala.Predef.String = "",
+ isOnline: _root_.scala.Boolean = false
+ ) extends scalapb.GeneratedMessage with scalapb.Message[PresenceEvent] with scalapb.lenses.Updatable[PresenceEvent] {
+ @transient
+ private[this] var __serializedSizeCachedValue: _root_.scala.Int = 0
+ private[this] def __computeSerializedValue(): _root_.scala.Int = {
+ var __size = 0
+ if (userId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(1, userId) }
+ if (username != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(2, username) }
+ if (provider != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(3, provider) }
+ if (isOnline != false) { __size += _root_.com.google.protobuf.CodedOutputStream.computeBoolSize(4, isOnline) }
+ __size
+ }
+ final override def serializedSize: _root_.scala.Int = {
+ var read = __serializedSizeCachedValue
+ if (read == 0) {
+ read = __computeSerializedValue()
+ __serializedSizeCachedValue = read
+ }
+ read
+ }
+ def writeTo(`_output__`: _root_.com.google.protobuf.CodedOutputStream): _root_.scala.Unit = {
+ {
+ val __v = userId
+ if (__v != "") {
+ _output__.writeString(1, __v)
+ }
+ };
+ {
+ val __v = username
+ if (__v != "") {
+ _output__.writeString(2, __v)
+ }
+ };
+ {
+ val __v = provider
+ if (__v != "") {
+ _output__.writeString(3, __v)
+ }
+ };
+ {
+ val __v = isOnline
+ if (__v != false) {
+ _output__.writeBool(4, __v)
+ }
+ };
+ }
+ def mergeFrom(`_input__`: _root_.com.google.protobuf.CodedInputStream): code.obp.grpc.chat.api.PresenceEvent = {
+ var __userId = this.userId
+ var __username = this.username
+ var __provider = this.provider
+ var __isOnline = this.isOnline
+ var _done__ = false
+ while (!_done__) {
+ val _tag__ = _input__.readTag()
+ _tag__ match {
+ case 0 => _done__ = true
+ case 10 =>
+ __userId = _input__.readString()
+ case 18 =>
+ __username = _input__.readString()
+ case 26 =>
+ __provider = _input__.readString()
+ case 32 =>
+ __isOnline = _input__.readBool()
+ case tag => _input__.skipField(tag)
+ }
+ }
+ code.obp.grpc.chat.api.PresenceEvent(
+ userId = __userId,
+ username = __username,
+ provider = __provider,
+ isOnline = __isOnline
+ )
+ }
+ def withUserId(__v: _root_.scala.Predef.String): PresenceEvent = copy(userId = __v)
+ def withUsername(__v: _root_.scala.Predef.String): PresenceEvent = copy(username = __v)
+ def withProvider(__v: _root_.scala.Predef.String): PresenceEvent = copy(provider = __v)
+ def withIsOnline(__v: _root_.scala.Boolean): PresenceEvent = copy(isOnline = __v)
+ def getFieldByNumber(__fieldNumber: _root_.scala.Int): scala.Any = {
+ (__fieldNumber: @_root_.scala.unchecked) match {
+ case 1 => {
+ val __t = userId
+ if (__t != "") __t else null
+ }
+ case 2 => {
+ val __t = username
+ if (__t != "") __t else null
+ }
+ case 3 => {
+ val __t = provider
+ if (__t != "") __t else null
+ }
+ case 4 => {
+ val __t = isOnline
+ if (__t != false) __t else null
+ }
+ }
+ }
+ def getField(__field: _root_.scalapb.descriptors.FieldDescriptor): _root_.scalapb.descriptors.PValue = {
+ require(__field.containingMessage eq companion.scalaDescriptor)
+ (__field.number: @_root_.scala.unchecked) match {
+ case 1 => _root_.scalapb.descriptors.PString(userId)
+ case 2 => _root_.scalapb.descriptors.PString(username)
+ case 3 => _root_.scalapb.descriptors.PString(provider)
+ case 4 => _root_.scalapb.descriptors.PBoolean(isOnline)
+ }
+ }
+ def toProtoString: _root_.scala.Predef.String = _root_.scalapb.TextFormat.printToUnicodeString(this)
+ def companion = code.obp.grpc.chat.api.PresenceEvent
+}
+
+object PresenceEvent extends scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.PresenceEvent] {
+ implicit def messageCompanion: scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.PresenceEvent] = this
+ def fromFieldsMap(__fieldsMap: scala.collection.immutable.Map[_root_.com.google.protobuf.Descriptors.FieldDescriptor, scala.Any]): code.obp.grpc.chat.api.PresenceEvent = {
+ require(__fieldsMap.keys.forall(_.getContainingType() == javaDescriptor), "FieldDescriptor does not match message type.")
+ val __fields = javaDescriptor.getFields
+ code.obp.grpc.chat.api.PresenceEvent(
+ __fieldsMap.getOrElse(__fields.get(0), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(1), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(2), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(3), false).asInstanceOf[_root_.scala.Boolean]
+ )
+ }
+ implicit def messageReads: _root_.scalapb.descriptors.Reads[code.obp.grpc.chat.api.PresenceEvent] = _root_.scalapb.descriptors.Reads{
+ case _root_.scalapb.descriptors.PMessage(__fieldsMap) =>
+ require(__fieldsMap.keys.forall(_.containingMessage == scalaDescriptor), "FieldDescriptor does not match message type.")
+ code.obp.grpc.chat.api.PresenceEvent(
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(1).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(2).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(3).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(4).get).map(_.as[_root_.scala.Boolean]).getOrElse(false)
+ )
+ case _ => throw new RuntimeException("Expected PMessage")
+ }
+ def javaDescriptor: _root_.com.google.protobuf.Descriptors.Descriptor = ChatProto.javaDescriptor.getMessageTypes.get(5)
+ def scalaDescriptor: _root_.scalapb.descriptors.Descriptor = throw new UnsupportedOperationException("scalaDescriptor not available")
+ def messageCompanionForFieldNumber(__number: _root_.scala.Int): _root_.scalapb.GeneratedMessageCompanion[_] = throw new MatchError(__number)
+ lazy val nestedMessagesCompanions: Seq[_root_.scalapb.GeneratedMessageCompanion[_]] = Seq.empty
+ def enumCompanionForFieldNumber(__fieldNumber: _root_.scala.Int): _root_.scalapb.GeneratedEnumCompanion[_] = throw new MatchError(__fieldNumber)
+ lazy val defaultInstance = code.obp.grpc.chat.api.PresenceEvent(
+ )
+ implicit class PresenceEventLens[UpperPB](_l: _root_.scalapb.lenses.Lens[UpperPB, code.obp.grpc.chat.api.PresenceEvent]) extends _root_.scalapb.lenses.ObjectLens[UpperPB, code.obp.grpc.chat.api.PresenceEvent](_l) {
+ def userId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.userId)((c_, f_) => c_.copy(userId = f_))
+ def username: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.username)((c_, f_) => c_.copy(username = f_))
+ def provider: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.provider)((c_, f_) => c_.copy(provider = f_))
+ def isOnline: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Boolean] = field(_.isOnline)((c_, f_) => c_.copy(isOnline = f_))
+ }
+ final val USERID_FIELD_NUMBER = 1
+ final val USERNAME_FIELD_NUMBER = 2
+ final val PROVIDER_FIELD_NUMBER = 3
+ final val ISONLINE_FIELD_NUMBER = 4
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/StreamMessagesRequest.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/StreamMessagesRequest.scala
new file mode 100644
index 0000000000..87fec2d637
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/StreamMessagesRequest.scala
@@ -0,0 +1,98 @@
+// Generated by the Scala Plugin for the Protocol Buffer Compiler.
+// Do not edit!
+//
+// Protofile syntax: PROTO3
+
+package code.obp.grpc.chat.api
+
+@SerialVersionUID(0L)
+final case class StreamMessagesRequest(
+ chatRoomId: _root_.scala.Predef.String = ""
+ ) extends scalapb.GeneratedMessage with scalapb.Message[StreamMessagesRequest] with scalapb.lenses.Updatable[StreamMessagesRequest] {
+ @transient
+ private[this] var __serializedSizeCachedValue: _root_.scala.Int = 0
+ private[this] def __computeSerializedValue(): _root_.scala.Int = {
+ var __size = 0
+ if (chatRoomId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(1, chatRoomId) }
+ __size
+ }
+ final override def serializedSize: _root_.scala.Int = {
+ var read = __serializedSizeCachedValue
+ if (read == 0) {
+ read = __computeSerializedValue()
+ __serializedSizeCachedValue = read
+ }
+ read
+ }
+ def writeTo(`_output__`: _root_.com.google.protobuf.CodedOutputStream): _root_.scala.Unit = {
+ {
+ val __v = chatRoomId
+ if (__v != "") {
+ _output__.writeString(1, __v)
+ }
+ };
+ }
+ def mergeFrom(`_input__`: _root_.com.google.protobuf.CodedInputStream): code.obp.grpc.chat.api.StreamMessagesRequest = {
+ var __chatRoomId = this.chatRoomId
+ var _done__ = false
+ while (!_done__) {
+ val _tag__ = _input__.readTag()
+ _tag__ match {
+ case 0 => _done__ = true
+ case 10 =>
+ __chatRoomId = _input__.readString()
+ case tag => _input__.skipField(tag)
+ }
+ }
+ code.obp.grpc.chat.api.StreamMessagesRequest(
+ chatRoomId = __chatRoomId
+ )
+ }
+ def withChatRoomId(__v: _root_.scala.Predef.String): StreamMessagesRequest = copy(chatRoomId = __v)
+ def getFieldByNumber(__fieldNumber: _root_.scala.Int): scala.Any = {
+ (__fieldNumber: @_root_.scala.unchecked) match {
+ case 1 => {
+ val __t = chatRoomId
+ if (__t != "") __t else null
+ }
+ }
+ }
+ def getField(__field: _root_.scalapb.descriptors.FieldDescriptor): _root_.scalapb.descriptors.PValue = {
+ require(__field.containingMessage eq companion.scalaDescriptor)
+ (__field.number: @_root_.scala.unchecked) match {
+ case 1 => _root_.scalapb.descriptors.PString(chatRoomId)
+ }
+ }
+ def toProtoString: _root_.scala.Predef.String = _root_.scalapb.TextFormat.printToUnicodeString(this)
+ def companion = code.obp.grpc.chat.api.StreamMessagesRequest
+}
+
+object StreamMessagesRequest extends scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.StreamMessagesRequest] {
+ implicit def messageCompanion: scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.StreamMessagesRequest] = this
+ def fromFieldsMap(__fieldsMap: scala.collection.immutable.Map[_root_.com.google.protobuf.Descriptors.FieldDescriptor, scala.Any]): code.obp.grpc.chat.api.StreamMessagesRequest = {
+ require(__fieldsMap.keys.forall(_.getContainingType() == javaDescriptor), "FieldDescriptor does not match message type.")
+ val __fields = javaDescriptor.getFields
+ code.obp.grpc.chat.api.StreamMessagesRequest(
+ __fieldsMap.getOrElse(__fields.get(0), "").asInstanceOf[_root_.scala.Predef.String]
+ )
+ }
+ implicit def messageReads: _root_.scalapb.descriptors.Reads[code.obp.grpc.chat.api.StreamMessagesRequest] = _root_.scalapb.descriptors.Reads{
+ case _root_.scalapb.descriptors.PMessage(__fieldsMap) =>
+ require(__fieldsMap.keys.forall(_.containingMessage == scalaDescriptor), "FieldDescriptor does not match message type.")
+ code.obp.grpc.chat.api.StreamMessagesRequest(
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(1).get).map(_.as[_root_.scala.Predef.String]).getOrElse("")
+ )
+ case _ => throw new RuntimeException("Expected PMessage")
+ }
+ def javaDescriptor: _root_.com.google.protobuf.Descriptors.Descriptor = ChatProto.javaDescriptor.getMessageTypes.get(0)
+ def scalaDescriptor: _root_.scalapb.descriptors.Descriptor = throw new UnsupportedOperationException("scalaDescriptor not available")
+ def messageCompanionForFieldNumber(__number: _root_.scala.Int): _root_.scalapb.GeneratedMessageCompanion[_] = throw new MatchError(__number)
+ lazy val nestedMessagesCompanions: Seq[_root_.scalapb.GeneratedMessageCompanion[_]] = Seq.empty
+ def enumCompanionForFieldNumber(__fieldNumber: _root_.scala.Int): _root_.scalapb.GeneratedEnumCompanion[_] = throw new MatchError(__fieldNumber)
+ lazy val defaultInstance = code.obp.grpc.chat.api.StreamMessagesRequest(
+ )
+ implicit class StreamMessagesRequestLens[UpperPB](_l: _root_.scalapb.lenses.Lens[UpperPB, code.obp.grpc.chat.api.StreamMessagesRequest]) extends _root_.scalapb.lenses.ObjectLens[UpperPB, code.obp.grpc.chat.api.StreamMessagesRequest](_l) {
+ def chatRoomId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.chatRoomId)((c_, f_) => c_.copy(chatRoomId = f_))
+ }
+ final val CHATROOMID_FIELD_NUMBER = 1
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/StreamPresenceRequest.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/StreamPresenceRequest.scala
new file mode 100644
index 0000000000..bfec51377f
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/StreamPresenceRequest.scala
@@ -0,0 +1,98 @@
+// Generated by the Scala Plugin for the Protocol Buffer Compiler.
+// Do not edit!
+//
+// Protofile syntax: PROTO3
+
+package code.obp.grpc.chat.api
+
+@SerialVersionUID(0L)
+final case class StreamPresenceRequest(
+ chatRoomId: _root_.scala.Predef.String = ""
+ ) extends scalapb.GeneratedMessage with scalapb.Message[StreamPresenceRequest] with scalapb.lenses.Updatable[StreamPresenceRequest] {
+ @transient
+ private[this] var __serializedSizeCachedValue: _root_.scala.Int = 0
+ private[this] def __computeSerializedValue(): _root_.scala.Int = {
+ var __size = 0
+ if (chatRoomId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(1, chatRoomId) }
+ __size
+ }
+ final override def serializedSize: _root_.scala.Int = {
+ var read = __serializedSizeCachedValue
+ if (read == 0) {
+ read = __computeSerializedValue()
+ __serializedSizeCachedValue = read
+ }
+ read
+ }
+ def writeTo(`_output__`: _root_.com.google.protobuf.CodedOutputStream): _root_.scala.Unit = {
+ {
+ val __v = chatRoomId
+ if (__v != "") {
+ _output__.writeString(1, __v)
+ }
+ };
+ }
+ def mergeFrom(`_input__`: _root_.com.google.protobuf.CodedInputStream): code.obp.grpc.chat.api.StreamPresenceRequest = {
+ var __chatRoomId = this.chatRoomId
+ var _done__ = false
+ while (!_done__) {
+ val _tag__ = _input__.readTag()
+ _tag__ match {
+ case 0 => _done__ = true
+ case 10 =>
+ __chatRoomId = _input__.readString()
+ case tag => _input__.skipField(tag)
+ }
+ }
+ code.obp.grpc.chat.api.StreamPresenceRequest(
+ chatRoomId = __chatRoomId
+ )
+ }
+ def withChatRoomId(__v: _root_.scala.Predef.String): StreamPresenceRequest = copy(chatRoomId = __v)
+ def getFieldByNumber(__fieldNumber: _root_.scala.Int): scala.Any = {
+ (__fieldNumber: @_root_.scala.unchecked) match {
+ case 1 => {
+ val __t = chatRoomId
+ if (__t != "") __t else null
+ }
+ }
+ }
+ def getField(__field: _root_.scalapb.descriptors.FieldDescriptor): _root_.scalapb.descriptors.PValue = {
+ require(__field.containingMessage eq companion.scalaDescriptor)
+ (__field.number: @_root_.scala.unchecked) match {
+ case 1 => _root_.scalapb.descriptors.PString(chatRoomId)
+ }
+ }
+ def toProtoString: _root_.scala.Predef.String = _root_.scalapb.TextFormat.printToUnicodeString(this)
+ def companion = code.obp.grpc.chat.api.StreamPresenceRequest
+}
+
+object StreamPresenceRequest extends scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.StreamPresenceRequest] {
+ implicit def messageCompanion: scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.StreamPresenceRequest] = this
+ def fromFieldsMap(__fieldsMap: scala.collection.immutable.Map[_root_.com.google.protobuf.Descriptors.FieldDescriptor, scala.Any]): code.obp.grpc.chat.api.StreamPresenceRequest = {
+ require(__fieldsMap.keys.forall(_.getContainingType() == javaDescriptor), "FieldDescriptor does not match message type.")
+ val __fields = javaDescriptor.getFields
+ code.obp.grpc.chat.api.StreamPresenceRequest(
+ __fieldsMap.getOrElse(__fields.get(0), "").asInstanceOf[_root_.scala.Predef.String]
+ )
+ }
+ implicit def messageReads: _root_.scalapb.descriptors.Reads[code.obp.grpc.chat.api.StreamPresenceRequest] = _root_.scalapb.descriptors.Reads{
+ case _root_.scalapb.descriptors.PMessage(__fieldsMap) =>
+ require(__fieldsMap.keys.forall(_.containingMessage == scalaDescriptor), "FieldDescriptor does not match message type.")
+ code.obp.grpc.chat.api.StreamPresenceRequest(
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(1).get).map(_.as[_root_.scala.Predef.String]).getOrElse("")
+ )
+ case _ => throw new RuntimeException("Expected PMessage")
+ }
+ def javaDescriptor: _root_.com.google.protobuf.Descriptors.Descriptor = ChatProto.javaDescriptor.getMessageTypes.get(2)
+ def scalaDescriptor: _root_.scalapb.descriptors.Descriptor = throw new UnsupportedOperationException("scalaDescriptor not available")
+ def messageCompanionForFieldNumber(__number: _root_.scala.Int): _root_.scalapb.GeneratedMessageCompanion[_] = throw new MatchError(__number)
+ lazy val nestedMessagesCompanions: Seq[_root_.scalapb.GeneratedMessageCompanion[_]] = Seq.empty
+ def enumCompanionForFieldNumber(__fieldNumber: _root_.scala.Int): _root_.scalapb.GeneratedEnumCompanion[_] = throw new MatchError(__fieldNumber)
+ lazy val defaultInstance = code.obp.grpc.chat.api.StreamPresenceRequest(
+ )
+ implicit class StreamPresenceRequestLens[UpperPB](_l: _root_.scalapb.lenses.Lens[UpperPB, code.obp.grpc.chat.api.StreamPresenceRequest]) extends _root_.scalapb.lenses.ObjectLens[UpperPB, code.obp.grpc.chat.api.StreamPresenceRequest](_l) {
+ def chatRoomId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.chatRoomId)((c_, f_) => c_.copy(chatRoomId = f_))
+ }
+ final val CHATROOMID_FIELD_NUMBER = 1
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/StreamUnreadCountsRequest.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/StreamUnreadCountsRequest.scala
new file mode 100644
index 0000000000..5c569a0f1c
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/StreamUnreadCountsRequest.scala
@@ -0,0 +1,77 @@
+// Generated by the Scala Plugin for the Protocol Buffer Compiler.
+// Do not edit!
+//
+// Protofile syntax: PROTO3
+
+package code.obp.grpc.chat.api
+
+@SerialVersionUID(0L)
+final case class StreamUnreadCountsRequest(
+ ) extends scalapb.GeneratedMessage with scalapb.Message[StreamUnreadCountsRequest] with scalapb.lenses.Updatable[StreamUnreadCountsRequest] {
+ @transient
+ private[this] var __serializedSizeCachedValue: _root_.scala.Int = 0
+ private[this] def __computeSerializedValue(): _root_.scala.Int = {
+ var __size = 0
+ __size
+ }
+ final override def serializedSize: _root_.scala.Int = {
+ var read = __serializedSizeCachedValue
+ if (read == 0) {
+ read = __computeSerializedValue()
+ __serializedSizeCachedValue = read
+ }
+ read
+ }
+ def writeTo(`_output__`: _root_.com.google.protobuf.CodedOutputStream): _root_.scala.Unit = {
+ }
+ def mergeFrom(`_input__`: _root_.com.google.protobuf.CodedInputStream): code.obp.grpc.chat.api.StreamUnreadCountsRequest = {
+ var _done__ = false
+ while (!_done__) {
+ val _tag__ = _input__.readTag()
+ _tag__ match {
+ case 0 => _done__ = true
+ case tag => _input__.skipField(tag)
+ }
+ }
+ code.obp.grpc.chat.api.StreamUnreadCountsRequest(
+ )
+ }
+ def getFieldByNumber(__fieldNumber: _root_.scala.Int): scala.Any = {
+ (__fieldNumber: @_root_.scala.unchecked) match {
+ case _ => throw new MatchError(__fieldNumber)
+ }
+ }
+ def getField(__field: _root_.scalapb.descriptors.FieldDescriptor): _root_.scalapb.descriptors.PValue = {
+ require(__field.containingMessage eq companion.scalaDescriptor)
+ (__field.number: @_root_.scala.unchecked) match {
+ case _ => throw new MatchError(__field)
+ }
+ }
+ def toProtoString: _root_.scala.Predef.String = _root_.scalapb.TextFormat.printToUnicodeString(this)
+ def companion = code.obp.grpc.chat.api.StreamUnreadCountsRequest
+}
+
+object StreamUnreadCountsRequest extends scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.StreamUnreadCountsRequest] {
+ implicit def messageCompanion: scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.StreamUnreadCountsRequest] = this
+ def fromFieldsMap(__fieldsMap: scala.collection.immutable.Map[_root_.com.google.protobuf.Descriptors.FieldDescriptor, scala.Any]): code.obp.grpc.chat.api.StreamUnreadCountsRequest = {
+ require(__fieldsMap.keys.forall(_.getContainingType() == javaDescriptor), "FieldDescriptor does not match message type.")
+ code.obp.grpc.chat.api.StreamUnreadCountsRequest(
+ )
+ }
+ implicit def messageReads: _root_.scalapb.descriptors.Reads[code.obp.grpc.chat.api.StreamUnreadCountsRequest] = _root_.scalapb.descriptors.Reads{
+ case _root_.scalapb.descriptors.PMessage(__fieldsMap) =>
+ require(__fieldsMap.keys.forall(_.containingMessage == scalaDescriptor), "FieldDescriptor does not match message type.")
+ code.obp.grpc.chat.api.StreamUnreadCountsRequest(
+ )
+ case _ => throw new RuntimeException("Expected PMessage")
+ }
+ def javaDescriptor: _root_.com.google.protobuf.Descriptors.Descriptor = ChatProto.javaDescriptor.getMessageTypes.get(3)
+ def scalaDescriptor: _root_.scalapb.descriptors.Descriptor = throw new UnsupportedOperationException("scalaDescriptor not available")
+ def messageCompanionForFieldNumber(__number: _root_.scala.Int): _root_.scalapb.GeneratedMessageCompanion[_] = throw new MatchError(__number)
+ lazy val nestedMessagesCompanions: Seq[_root_.scalapb.GeneratedMessageCompanion[_]] = Seq.empty
+ def enumCompanionForFieldNumber(__fieldNumber: _root_.scala.Int): _root_.scalapb.GeneratedEnumCompanion[_] = throw new MatchError(__fieldNumber)
+ lazy val defaultInstance = code.obp.grpc.chat.api.StreamUnreadCountsRequest(
+ )
+ implicit class StreamUnreadCountsRequestLens[UpperPB](_l: _root_.scalapb.lenses.Lens[UpperPB, code.obp.grpc.chat.api.StreamUnreadCountsRequest]) extends _root_.scalapb.lenses.ObjectLens[UpperPB, code.obp.grpc.chat.api.StreamUnreadCountsRequest](_l) {
+ }
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/TypingEvent.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/TypingEvent.scala
new file mode 100644
index 0000000000..8613170b93
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/TypingEvent.scala
@@ -0,0 +1,120 @@
+// Generated by the Scala Plugin for the Protocol Buffer Compiler.
+// Do not edit!
+//
+// Protofile syntax: PROTO3
+
+package code.obp.grpc.chat.api
+
+@SerialVersionUID(0L)
+final case class TypingEvent(
+ chatRoomId: _root_.scala.Predef.String = "",
+ isTyping: _root_.scala.Boolean = false
+ ) extends scalapb.GeneratedMessage with scalapb.Message[TypingEvent] with scalapb.lenses.Updatable[TypingEvent] {
+ @transient
+ private[this] var __serializedSizeCachedValue: _root_.scala.Int = 0
+ private[this] def __computeSerializedValue(): _root_.scala.Int = {
+ var __size = 0
+ if (chatRoomId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(1, chatRoomId) }
+ if (isTyping != false) { __size += _root_.com.google.protobuf.CodedOutputStream.computeBoolSize(2, isTyping) }
+ __size
+ }
+ final override def serializedSize: _root_.scala.Int = {
+ var read = __serializedSizeCachedValue
+ if (read == 0) {
+ read = __computeSerializedValue()
+ __serializedSizeCachedValue = read
+ }
+ read
+ }
+ def writeTo(`_output__`: _root_.com.google.protobuf.CodedOutputStream): _root_.scala.Unit = {
+ {
+ val __v = chatRoomId
+ if (__v != "") {
+ _output__.writeString(1, __v)
+ }
+ };
+ {
+ val __v = isTyping
+ if (__v != false) {
+ _output__.writeBool(2, __v)
+ }
+ };
+ }
+ def mergeFrom(`_input__`: _root_.com.google.protobuf.CodedInputStream): code.obp.grpc.chat.api.TypingEvent = {
+ var __chatRoomId = this.chatRoomId
+ var __isTyping = this.isTyping
+ var _done__ = false
+ while (!_done__) {
+ val _tag__ = _input__.readTag()
+ _tag__ match {
+ case 0 => _done__ = true
+ case 10 =>
+ __chatRoomId = _input__.readString()
+ case 16 =>
+ __isTyping = _input__.readBool()
+ case tag => _input__.skipField(tag)
+ }
+ }
+ code.obp.grpc.chat.api.TypingEvent(
+ chatRoomId = __chatRoomId,
+ isTyping = __isTyping
+ )
+ }
+ def withChatRoomId(__v: _root_.scala.Predef.String): TypingEvent = copy(chatRoomId = __v)
+ def withIsTyping(__v: _root_.scala.Boolean): TypingEvent = copy(isTyping = __v)
+ def getFieldByNumber(__fieldNumber: _root_.scala.Int): scala.Any = {
+ (__fieldNumber: @_root_.scala.unchecked) match {
+ case 1 => {
+ val __t = chatRoomId
+ if (__t != "") __t else null
+ }
+ case 2 => {
+ val __t = isTyping
+ if (__t != false) __t else null
+ }
+ }
+ }
+ def getField(__field: _root_.scalapb.descriptors.FieldDescriptor): _root_.scalapb.descriptors.PValue = {
+ require(__field.containingMessage eq companion.scalaDescriptor)
+ (__field.number: @_root_.scala.unchecked) match {
+ case 1 => _root_.scalapb.descriptors.PString(chatRoomId)
+ case 2 => _root_.scalapb.descriptors.PBoolean(isTyping)
+ }
+ }
+ def toProtoString: _root_.scala.Predef.String = _root_.scalapb.TextFormat.printToUnicodeString(this)
+ def companion = code.obp.grpc.chat.api.TypingEvent
+}
+
+object TypingEvent extends scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.TypingEvent] {
+ implicit def messageCompanion: scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.TypingEvent] = this
+ def fromFieldsMap(__fieldsMap: scala.collection.immutable.Map[_root_.com.google.protobuf.Descriptors.FieldDescriptor, scala.Any]): code.obp.grpc.chat.api.TypingEvent = {
+ require(__fieldsMap.keys.forall(_.getContainingType() == javaDescriptor), "FieldDescriptor does not match message type.")
+ val __fields = javaDescriptor.getFields
+ code.obp.grpc.chat.api.TypingEvent(
+ __fieldsMap.getOrElse(__fields.get(0), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(1), false).asInstanceOf[_root_.scala.Boolean]
+ )
+ }
+ implicit def messageReads: _root_.scalapb.descriptors.Reads[code.obp.grpc.chat.api.TypingEvent] = _root_.scalapb.descriptors.Reads{
+ case _root_.scalapb.descriptors.PMessage(__fieldsMap) =>
+ require(__fieldsMap.keys.forall(_.containingMessage == scalaDescriptor), "FieldDescriptor does not match message type.")
+ code.obp.grpc.chat.api.TypingEvent(
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(1).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(2).get).map(_.as[_root_.scala.Boolean]).getOrElse(false)
+ )
+ case _ => throw new RuntimeException("Expected PMessage")
+ }
+ def javaDescriptor: _root_.com.google.protobuf.Descriptors.Descriptor = ChatProto.javaDescriptor.getMessageTypes.get(1)
+ def scalaDescriptor: _root_.scalapb.descriptors.Descriptor = throw new UnsupportedOperationException("scalaDescriptor not available")
+ def messageCompanionForFieldNumber(__number: _root_.scala.Int): _root_.scalapb.GeneratedMessageCompanion[_] = throw new MatchError(__number)
+ lazy val nestedMessagesCompanions: Seq[_root_.scalapb.GeneratedMessageCompanion[_]] = Seq.empty
+ def enumCompanionForFieldNumber(__fieldNumber: _root_.scala.Int): _root_.scalapb.GeneratedEnumCompanion[_] = throw new MatchError(__fieldNumber)
+ lazy val defaultInstance = code.obp.grpc.chat.api.TypingEvent(
+ )
+ implicit class TypingEventLens[UpperPB](_l: _root_.scalapb.lenses.Lens[UpperPB, code.obp.grpc.chat.api.TypingEvent]) extends _root_.scalapb.lenses.ObjectLens[UpperPB, code.obp.grpc.chat.api.TypingEvent](_l) {
+ def chatRoomId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.chatRoomId)((c_, f_) => c_.copy(chatRoomId = f_))
+ def isTyping: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Boolean] = field(_.isTyping)((c_, f_) => c_.copy(isTyping = f_))
+ }
+ final val CHATROOMID_FIELD_NUMBER = 1
+ final val ISTYPING_FIELD_NUMBER = 2
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/TypingIndicator.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/TypingIndicator.scala
new file mode 100644
index 0000000000..a4edb6cdc6
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/TypingIndicator.scala
@@ -0,0 +1,186 @@
+// Generated by the Scala Plugin for the Protocol Buffer Compiler.
+// Do not edit!
+//
+// Protofile syntax: PROTO3
+
+package code.obp.grpc.chat.api
+
+@SerialVersionUID(0L)
+final case class TypingIndicator(
+ chatRoomId: _root_.scala.Predef.String = "",
+ userId: _root_.scala.Predef.String = "",
+ username: _root_.scala.Predef.String = "",
+ provider: _root_.scala.Predef.String = "",
+ isTyping: _root_.scala.Boolean = false
+ ) extends scalapb.GeneratedMessage with scalapb.Message[TypingIndicator] with scalapb.lenses.Updatable[TypingIndicator] {
+ @transient
+ private[this] var __serializedSizeCachedValue: _root_.scala.Int = 0
+ private[this] def __computeSerializedValue(): _root_.scala.Int = {
+ var __size = 0
+ if (chatRoomId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(1, chatRoomId) }
+ if (userId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(2, userId) }
+ if (username != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(3, username) }
+ if (provider != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(4, provider) }
+ if (isTyping != false) { __size += _root_.com.google.protobuf.CodedOutputStream.computeBoolSize(5, isTyping) }
+ __size
+ }
+ final override def serializedSize: _root_.scala.Int = {
+ var read = __serializedSizeCachedValue
+ if (read == 0) {
+ read = __computeSerializedValue()
+ __serializedSizeCachedValue = read
+ }
+ read
+ }
+ def writeTo(`_output__`: _root_.com.google.protobuf.CodedOutputStream): _root_.scala.Unit = {
+ {
+ val __v = chatRoomId
+ if (__v != "") {
+ _output__.writeString(1, __v)
+ }
+ };
+ {
+ val __v = userId
+ if (__v != "") {
+ _output__.writeString(2, __v)
+ }
+ };
+ {
+ val __v = username
+ if (__v != "") {
+ _output__.writeString(3, __v)
+ }
+ };
+ {
+ val __v = provider
+ if (__v != "") {
+ _output__.writeString(4, __v)
+ }
+ };
+ {
+ val __v = isTyping
+ if (__v != false) {
+ _output__.writeBool(5, __v)
+ }
+ };
+ }
+ def mergeFrom(`_input__`: _root_.com.google.protobuf.CodedInputStream): code.obp.grpc.chat.api.TypingIndicator = {
+ var __chatRoomId = this.chatRoomId
+ var __userId = this.userId
+ var __username = this.username
+ var __provider = this.provider
+ var __isTyping = this.isTyping
+ var _done__ = false
+ while (!_done__) {
+ val _tag__ = _input__.readTag()
+ _tag__ match {
+ case 0 => _done__ = true
+ case 10 =>
+ __chatRoomId = _input__.readString()
+ case 18 =>
+ __userId = _input__.readString()
+ case 26 =>
+ __username = _input__.readString()
+ case 34 =>
+ __provider = _input__.readString()
+ case 40 =>
+ __isTyping = _input__.readBool()
+ case tag => _input__.skipField(tag)
+ }
+ }
+ code.obp.grpc.chat.api.TypingIndicator(
+ chatRoomId = __chatRoomId,
+ userId = __userId,
+ username = __username,
+ provider = __provider,
+ isTyping = __isTyping
+ )
+ }
+ def withChatRoomId(__v: _root_.scala.Predef.String): TypingIndicator = copy(chatRoomId = __v)
+ def withUserId(__v: _root_.scala.Predef.String): TypingIndicator = copy(userId = __v)
+ def withUsername(__v: _root_.scala.Predef.String): TypingIndicator = copy(username = __v)
+ def withProvider(__v: _root_.scala.Predef.String): TypingIndicator = copy(provider = __v)
+ def withIsTyping(__v: _root_.scala.Boolean): TypingIndicator = copy(isTyping = __v)
+ def getFieldByNumber(__fieldNumber: _root_.scala.Int): scala.Any = {
+ (__fieldNumber: @_root_.scala.unchecked) match {
+ case 1 => {
+ val __t = chatRoomId
+ if (__t != "") __t else null
+ }
+ case 2 => {
+ val __t = userId
+ if (__t != "") __t else null
+ }
+ case 3 => {
+ val __t = username
+ if (__t != "") __t else null
+ }
+ case 4 => {
+ val __t = provider
+ if (__t != "") __t else null
+ }
+ case 5 => {
+ val __t = isTyping
+ if (__t != false) __t else null
+ }
+ }
+ }
+ def getField(__field: _root_.scalapb.descriptors.FieldDescriptor): _root_.scalapb.descriptors.PValue = {
+ require(__field.containingMessage eq companion.scalaDescriptor)
+ (__field.number: @_root_.scala.unchecked) match {
+ case 1 => _root_.scalapb.descriptors.PString(chatRoomId)
+ case 2 => _root_.scalapb.descriptors.PString(userId)
+ case 3 => _root_.scalapb.descriptors.PString(username)
+ case 4 => _root_.scalapb.descriptors.PString(provider)
+ case 5 => _root_.scalapb.descriptors.PBoolean(isTyping)
+ }
+ }
+ def toProtoString: _root_.scala.Predef.String = _root_.scalapb.TextFormat.printToUnicodeString(this)
+ def companion = code.obp.grpc.chat.api.TypingIndicator
+}
+
+object TypingIndicator extends scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.TypingIndicator] {
+ implicit def messageCompanion: scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.TypingIndicator] = this
+ def fromFieldsMap(__fieldsMap: scala.collection.immutable.Map[_root_.com.google.protobuf.Descriptors.FieldDescriptor, scala.Any]): code.obp.grpc.chat.api.TypingIndicator = {
+ require(__fieldsMap.keys.forall(_.getContainingType() == javaDescriptor), "FieldDescriptor does not match message type.")
+ val __fields = javaDescriptor.getFields
+ code.obp.grpc.chat.api.TypingIndicator(
+ __fieldsMap.getOrElse(__fields.get(0), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(1), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(2), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(3), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(4), false).asInstanceOf[_root_.scala.Boolean]
+ )
+ }
+ implicit def messageReads: _root_.scalapb.descriptors.Reads[code.obp.grpc.chat.api.TypingIndicator] = _root_.scalapb.descriptors.Reads{
+ case _root_.scalapb.descriptors.PMessage(__fieldsMap) =>
+ require(__fieldsMap.keys.forall(_.containingMessage == scalaDescriptor), "FieldDescriptor does not match message type.")
+ code.obp.grpc.chat.api.TypingIndicator(
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(1).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(2).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(3).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(4).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(5).get).map(_.as[_root_.scala.Boolean]).getOrElse(false)
+ )
+ case _ => throw new RuntimeException("Expected PMessage")
+ }
+ def javaDescriptor: _root_.com.google.protobuf.Descriptors.Descriptor = ChatProto.javaDescriptor.getMessageTypes.get(3)
+ def scalaDescriptor: _root_.scalapb.descriptors.Descriptor = throw new UnsupportedOperationException("scalaDescriptor not available")
+ def messageCompanionForFieldNumber(__number: _root_.scala.Int): _root_.scalapb.GeneratedMessageCompanion[_] = throw new MatchError(__number)
+ lazy val nestedMessagesCompanions: Seq[_root_.scalapb.GeneratedMessageCompanion[_]] = Seq.empty
+ def enumCompanionForFieldNumber(__fieldNumber: _root_.scala.Int): _root_.scalapb.GeneratedEnumCompanion[_] = throw new MatchError(__fieldNumber)
+ lazy val defaultInstance = code.obp.grpc.chat.api.TypingIndicator(
+ )
+ implicit class TypingIndicatorLens[UpperPB](_l: _root_.scalapb.lenses.Lens[UpperPB, code.obp.grpc.chat.api.TypingIndicator]) extends _root_.scalapb.lenses.ObjectLens[UpperPB, code.obp.grpc.chat.api.TypingIndicator](_l) {
+ def chatRoomId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.chatRoomId)((c_, f_) => c_.copy(chatRoomId = f_))
+ def userId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.userId)((c_, f_) => c_.copy(userId = f_))
+ def username: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.username)((c_, f_) => c_.copy(username = f_))
+ def provider: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.provider)((c_, f_) => c_.copy(provider = f_))
+ def isTyping: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Boolean] = field(_.isTyping)((c_, f_) => c_.copy(isTyping = f_))
+ }
+ final val CHATROOMID_FIELD_NUMBER = 1
+ final val USERID_FIELD_NUMBER = 2
+ final val USERNAME_FIELD_NUMBER = 3
+ final val PROVIDER_FIELD_NUMBER = 4
+ final val ISTYPING_FIELD_NUMBER = 5
+}
diff --git a/obp-api/src/main/scala/code/obp/grpc/chat/api/UnreadCountEvent.scala b/obp-api/src/main/scala/code/obp/grpc/chat/api/UnreadCountEvent.scala
new file mode 100644
index 0000000000..47f4cf11a9
--- /dev/null
+++ b/obp-api/src/main/scala/code/obp/grpc/chat/api/UnreadCountEvent.scala
@@ -0,0 +1,120 @@
+// Generated by the Scala Plugin for the Protocol Buffer Compiler.
+// Do not edit!
+//
+// Protofile syntax: PROTO3
+
+package code.obp.grpc.chat.api
+
+@SerialVersionUID(0L)
+final case class UnreadCountEvent(
+ chatRoomId: _root_.scala.Predef.String = "",
+ unreadCount: _root_.scala.Long = 0L
+ ) extends scalapb.GeneratedMessage with scalapb.Message[UnreadCountEvent] with scalapb.lenses.Updatable[UnreadCountEvent] {
+ @transient
+ private[this] var __serializedSizeCachedValue: _root_.scala.Int = 0
+ private[this] def __computeSerializedValue(): _root_.scala.Int = {
+ var __size = 0
+ if (chatRoomId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(1, chatRoomId) }
+ if (unreadCount != 0L) { __size += _root_.com.google.protobuf.CodedOutputStream.computeInt64Size(2, unreadCount) }
+ __size
+ }
+ final override def serializedSize: _root_.scala.Int = {
+ var read = __serializedSizeCachedValue
+ if (read == 0) {
+ read = __computeSerializedValue()
+ __serializedSizeCachedValue = read
+ }
+ read
+ }
+ def writeTo(`_output__`: _root_.com.google.protobuf.CodedOutputStream): _root_.scala.Unit = {
+ {
+ val __v = chatRoomId
+ if (__v != "") {
+ _output__.writeString(1, __v)
+ }
+ };
+ {
+ val __v = unreadCount
+ if (__v != 0L) {
+ _output__.writeInt64(2, __v)
+ }
+ };
+ }
+ def mergeFrom(`_input__`: _root_.com.google.protobuf.CodedInputStream): code.obp.grpc.chat.api.UnreadCountEvent = {
+ var __chatRoomId = this.chatRoomId
+ var __unreadCount = this.unreadCount
+ var _done__ = false
+ while (!_done__) {
+ val _tag__ = _input__.readTag()
+ _tag__ match {
+ case 0 => _done__ = true
+ case 10 =>
+ __chatRoomId = _input__.readString()
+ case 16 =>
+ __unreadCount = _input__.readInt64()
+ case tag => _input__.skipField(tag)
+ }
+ }
+ code.obp.grpc.chat.api.UnreadCountEvent(
+ chatRoomId = __chatRoomId,
+ unreadCount = __unreadCount
+ )
+ }
+ def withChatRoomId(__v: _root_.scala.Predef.String): UnreadCountEvent = copy(chatRoomId = __v)
+ def withUnreadCount(__v: _root_.scala.Long): UnreadCountEvent = copy(unreadCount = __v)
+ def getFieldByNumber(__fieldNumber: _root_.scala.Int): scala.Any = {
+ (__fieldNumber: @_root_.scala.unchecked) match {
+ case 1 => {
+ val __t = chatRoomId
+ if (__t != "") __t else null
+ }
+ case 2 => {
+ val __t = unreadCount
+ if (__t != 0L) __t else null
+ }
+ }
+ }
+ def getField(__field: _root_.scalapb.descriptors.FieldDescriptor): _root_.scalapb.descriptors.PValue = {
+ require(__field.containingMessage eq companion.scalaDescriptor)
+ (__field.number: @_root_.scala.unchecked) match {
+ case 1 => _root_.scalapb.descriptors.PString(chatRoomId)
+ case 2 => _root_.scalapb.descriptors.PLong(unreadCount)
+ }
+ }
+ def toProtoString: _root_.scala.Predef.String = _root_.scalapb.TextFormat.printToUnicodeString(this)
+ def companion = code.obp.grpc.chat.api.UnreadCountEvent
+}
+
+object UnreadCountEvent extends scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.UnreadCountEvent] {
+ implicit def messageCompanion: scalapb.GeneratedMessageCompanion[code.obp.grpc.chat.api.UnreadCountEvent] = this
+ def fromFieldsMap(__fieldsMap: scala.collection.immutable.Map[_root_.com.google.protobuf.Descriptors.FieldDescriptor, scala.Any]): code.obp.grpc.chat.api.UnreadCountEvent = {
+ require(__fieldsMap.keys.forall(_.getContainingType() == javaDescriptor), "FieldDescriptor does not match message type.")
+ val __fields = javaDescriptor.getFields
+ code.obp.grpc.chat.api.UnreadCountEvent(
+ __fieldsMap.getOrElse(__fields.get(0), "").asInstanceOf[_root_.scala.Predef.String],
+ __fieldsMap.getOrElse(__fields.get(1), 0L).asInstanceOf[_root_.scala.Long]
+ )
+ }
+ implicit def messageReads: _root_.scalapb.descriptors.Reads[code.obp.grpc.chat.api.UnreadCountEvent] = _root_.scalapb.descriptors.Reads{
+ case _root_.scalapb.descriptors.PMessage(__fieldsMap) =>
+ require(__fieldsMap.keys.forall(_.containingMessage == scalaDescriptor), "FieldDescriptor does not match message type.")
+ code.obp.grpc.chat.api.UnreadCountEvent(
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(1).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""),
+ __fieldsMap.get(scalaDescriptor.findFieldByNumber(2).get).map(_.as[_root_.scala.Long]).getOrElse(0L)
+ )
+ case _ => throw new RuntimeException("Expected PMessage")
+ }
+ def javaDescriptor: _root_.com.google.protobuf.Descriptors.Descriptor = ChatProto.javaDescriptor.getMessageTypes.get(7)
+ def scalaDescriptor: _root_.scalapb.descriptors.Descriptor = throw new UnsupportedOperationException("scalaDescriptor not available")
+ def messageCompanionForFieldNumber(__number: _root_.scala.Int): _root_.scalapb.GeneratedMessageCompanion[_] = throw new MatchError(__number)
+ lazy val nestedMessagesCompanions: Seq[_root_.scalapb.GeneratedMessageCompanion[_]] = Seq.empty
+ def enumCompanionForFieldNumber(__fieldNumber: _root_.scala.Int): _root_.scalapb.GeneratedEnumCompanion[_] = throw new MatchError(__fieldNumber)
+ lazy val defaultInstance = code.obp.grpc.chat.api.UnreadCountEvent(
+ )
+ implicit class UnreadCountEventLens[UpperPB](_l: _root_.scalapb.lenses.Lens[UpperPB, code.obp.grpc.chat.api.UnreadCountEvent]) extends _root_.scalapb.lenses.ObjectLens[UpperPB, code.obp.grpc.chat.api.UnreadCountEvent](_l) {
+ def chatRoomId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.chatRoomId)((c_, f_) => c_.copy(chatRoomId = f_))
+ def unreadCount: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Long] = field(_.unreadCount)((c_, f_) => c_.copy(unreadCount = f_))
+ }
+ final val CHATROOMID_FIELD_NUMBER = 1
+ final val UNREADCOUNT_FIELD_NUMBER = 2
+}