Skip to content
This repository was archived by the owner on Oct 30, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package chat.rocket.core.internal.model

data class UserAndRoomIdPayload(
val userId: String,
val roomId: String
)
43 changes: 29 additions & 14 deletions core/src/main/kotlin/chat/rocket/core/internal/rest/ChatRoom.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
package chat.rocket.core.internal.rest

import chat.rocket.common.RocketChatApiException
import chat.rocket.common.model.BaseResult
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.User
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.RestResult
import chat.rocket.core.internal.model.ChatRoomAnnouncementPayload
import chat.rocket.core.internal.model.ChatRoomDescriptionPayload
import chat.rocket.core.internal.model.ChatRoomFavoritePayload
import chat.rocket.core.internal.model.ChatRoomInvitePayload
import chat.rocket.core.internal.model.ChatRoomJoinCodePayload
import chat.rocket.core.internal.model.ChatRoomKickPayload
import chat.rocket.core.internal.model.ChatRoomNamePayload
import chat.rocket.core.internal.model.ChatRoomPayload
import chat.rocket.core.internal.model.ChatRoomReadOnlyPayload
import chat.rocket.core.internal.model.ChatRoomTopicPayload
import chat.rocket.core.internal.model.ChatRoomTypePayload
import chat.rocket.core.internal.model.ChatRoomUnreadPayload
import chat.rocket.core.internal.model.ChatRoomUserIgnorePayload
import chat.rocket.core.internal.model.RoomIdPayload
import chat.rocket.core.internal.model.*
import chat.rocket.core.model.ChatRoomRole
import chat.rocket.core.model.Message
import chat.rocket.core.model.PagedResult
Expand Down Expand Up @@ -343,6 +331,33 @@ suspend fun RocketChatClient.leaveChat(
return@withContext handleRestCall<BaseResult>(request, BaseResult::class.java).success
}

/**
* Invite a user to a chat room.
*
* @param userId The ID of the user.
* @param roomId The ID of the room.
* @param roomType The type of the room.
*
* @return Whether the task was successful or not.
* @throws RocketChatApiException if user oder room are not known or something else went wrong
*/
suspend fun RocketChatClient.invite(
userId: String,
roomId: String,
roomType: RoomType
): Boolean = withContext(Dispatchers.IO) {
val payload = UserAndRoomIdPayload(userId, roomId)
val adapter = moshi.adapter(UserAndRoomIdPayload::class.java)
val payloadBody = adapter.toJson(payload)

val body = RequestBody.create(MEDIA_TYPE_JSON, payloadBody)

val url = requestUrl(restUrl, getRestApiMethodNameByRoomType(roomType, "invite")).build()
val request = requestBuilderForAuthenticatedMethods(url).post(body).build()

return@withContext handleRestCall<BaseResult>(request, BaseResult::class.java).success
}

/**
* Renames a chat room
*
Expand Down
70 changes: 56 additions & 14 deletions core/src/main/kotlin/chat/rocket/core/internal/rest/User.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package chat.rocket.core.internal.rest

import chat.rocket.common.RocketChatApiException
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.BaseResult
import chat.rocket.common.model.RoomType
Expand All @@ -24,6 +25,7 @@ import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
Expand Down Expand Up @@ -53,11 +55,11 @@ suspend fun RocketChatClient.me(): Myself {
* @return An [User] with an updated profile.
*/
suspend fun RocketChatClient.updateProfile(
userId: String,
email: String? = null,
name: String? = null,
password: String? = null,
username: String? = null
userId: String,
email: String? = null,
name: String? = null,
password: String? = null,
username: String? = null
): User {
val payload = UserPayload(userId, UserPayloadData(name, password, username, email), null)
val adapter = moshi.adapter(UserPayload::class.java)
Expand All @@ -83,14 +85,14 @@ suspend fun RocketChatClient.updateProfile(
* @return An [User] with an updated profile.
*/
suspend fun RocketChatClient.updateOwnBasicInformation(
email: String? = null,
currentPassword: String? = null,
newPassword: String? = null,
username: String? = null,
name: String? = null
email: String? = null,
currentPassword: String? = null,
newPassword: String? = null,
username: String? = null,
name: String? = null
): User {
val payload =
OwnBasicInformationPayload(OwnBasicInformationPayloadData(email, currentPassword, newPassword, username, name))
OwnBasicInformationPayload(OwnBasicInformationPayloadData(email, currentPassword, newPassword, username, name))
val adapter = moshi.adapter(OwnBasicInformationPayload::class.java)

val payloadBody = adapter.toJson(payload)
Expand Down Expand Up @@ -222,9 +224,9 @@ suspend fun RocketChatClient.roles(): UserRole = withContext(Dispatchers.IO) {
}

internal fun RocketChatClient.combine(
rooms: RestMultiResult<List<Room>, List<Removed>>,
subscriptions: RestMultiResult<List<Subscription>, List<Removed>>,
filterCustom: Boolean
rooms: RestMultiResult<List<Room>, List<Removed>>,
subscriptions: RestMultiResult<List<Subscription>, List<Removed>>,
filterCustom: Boolean
): RestMultiResult<List<ChatRoom>, List<Removed>> {
val update = combine(rooms.update, subscriptions.update, filterCustom)
val remove = combineRemoved(rooms.remove, subscriptions.remove, filterCustom)
Expand Down Expand Up @@ -334,3 +336,43 @@ internal suspend fun RocketChatClient.listRooms(timestamp: Long? = null): RestMu
)
return handleRestCall(request, type)
}


/**
* Returns the user for a given username.
*
* @param username the username to be looked up
* @return the corresponding [User] or null if the username is unkown
* @throws RocketChatApiException if anything goes wrong calling the API
*/
suspend fun RocketChatClient.getUserByUsername(username: String) =
getUser { builder -> builder.addQueryParameter("username", username) }

/**
* Returns the user for a given user id.
*
* @param username the username to be looked up
* @return the corresponding [User] or null if the userId is unkown
* @throws RocketChatApiException if anything goes wrong calling the API
*/
suspend fun RocketChatClient.getUserById(id: String) =
getUser { builder -> builder.addQueryParameter("userId", id) }


private suspend fun RocketChatClient.getUser(builder: (HttpUrl.Builder) -> Unit): User? {
val userInfoUrlBuilder = requestUrl(restUrl, "users.info")
builder.invoke(userInfoUrlBuilder)
val httpUrl = userInfoUrlBuilder.build()
val request = requestBuilderForAuthenticatedMethods(httpUrl).get().build()

val responseType = Types.newParameterizedType(RestResult::class.java, User::class.java)
try {
val response = handleRestCall<RestResult<User>>(request, responseType)
return response.result()
} catch (ex: RocketChatApiException) {
if (ex.errorType == "error-invalid-user")
return null
else
throw ex
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package chat.rocket.core.internal.rest

import chat.rocket.common.RocketChatApiException
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.Token
Expand Down Expand Up @@ -462,4 +463,46 @@ class ChatRoomTest {
sut.searchMessages(roomId = "GENERAL", searchText = "test")
}
}

@Test
fun `invite() should succeed without throwing`() {
mockServer.expect()
.post()
.withPath("/api/v1/channels.invite")
.andReturn(200, SUCCESS)
.once()

runBlocking {
sut.invite(userId = "someUserUuid", roomId = "someRoomUuid", roomType = roomTypeOf(RoomType.CHANNEL))
}
}

@Test(expected = RocketChatApiException::class)
fun `invite() should throw if room does not exist`() {
mockServer.expect()
.post()
.withPath("/api/v1/channels.invite")
.andReturn(400, ERROR_INVALID_ROOM)
.once()

runBlocking {
sut.invite(userId = "someUserUuid", roomId = "someRoomUuid", roomType = roomTypeOf(RoomType.CHANNEL))
}
}

@Test(expected = RocketChatApiException::class)
fun `invite() should throw if user does not exist`() {
mockServer.expect()
.post()
.withPath("/api/v1/channels.invite")
.andReturn(400, ERROR_INVALID_USER)
.once()

runBlocking {
sut.invite(userId = "someUserUuid", roomId = "someRoomUuid", roomType = roomTypeOf(RoomType.CHANNEL))
}
}



}
39 changes: 39 additions & 0 deletions core/src/test/kotlin/chat/rocket/core/internal/rest/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,42 @@ const val MESSAGE_WITHOUT_REACTION = " {\n" +
" ],\n" +
" \"channels\": []\n" +
" }"

const val USER_INFO = """
{
"user": {
"_id": "nSYqWzZ4GsKTX4dyK",
"type": "user",
"status": "offline",
"active": true,
"name": "Example User",
"utcOffset": 0,
"username": "example"
},
"success": true
}
"""

const val ERROR_INVALID_USER = """
{
"success": false,
"error": "The required \"userId\" or \"username\" param provided does not match any users [error-invalid-user]",
"errorType": "error-invalid-user"
}
"""

const val ERROR_GENERIC = """
{
"success": false,
"error": "Something went wrong",
"errorType": "error-generic-for-test"
}
"""

const val ERROR_INVALID_ROOM = """
{
"success": false,
"error": "The required \"roomId\" or \"roomName\" param provided does not match any channel [error-room-not-found]",
"errorType": "error-room-not-found"
}
"""
109 changes: 109 additions & 0 deletions core/src/test/kotlin/chat/rocket/core/internal/rest/UserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,115 @@ class UserTest {
}
}

@Test
fun `getUserByUsername() should return user information for an existing user`() {
mockServer.expect()
.get()
.withPath("/api/v1/users.info?username=example")
.andReturn(200, USER_INFO)
.once()

runBlocking {
val user = sut.getUserByUsername("example")

assert(user != null)
assertThat(user?.username, isEqualTo("example"))
assertThat(user?.name, isEqualTo("Example User"))
assert(user?.status is UserStatus.Offline)
assertThat(user?.utcOffset, isEqualTo((0).toFloat()))
assert(user?.emails == null)
}
}

@Test
fun `getUserByUsername() should return null for non existing user`() {
mockServer.expect()
.get()
.withPath("/api/v1/users.info?username=nobody")
.andReturn(400, ERROR_INVALID_USER)
.once()

runBlocking {
val user = sut.getUserByUsername("nobody")

assert(user == null)
}
}

@Test
fun `getUserByUsername() should throw an APIException if something went wrong`() {
mockServer.expect()
.get()
.withPath("/api/v1/users.info?username=nobody")
.andReturn(500, ERROR_GENERIC)
.once()

runBlocking {
try {
sut.getUserByUsername("nobody")
throw RuntimeException("unreachable code")
} catch (ex: RocketChatApiException) {
assertThat(ex.message, isEqualTo("Something went wrong"))
assertThat(ex.errorType, isEqualTo("error-generic-for-test"))
}
}
}


@Test
fun `getUserById() should return user information for an existing user`() {
mockServer.expect()
.get()
.withPath("/api/v1/users.info?userId=example")
.andReturn(200, USER_INFO)
.once()

runBlocking {
val user = sut.getUserById("example")

assert(user != null)
assertThat(user?.username, isEqualTo("example"))
assertThat(user?.name, isEqualTo("Example User"))
assert(user?.status is UserStatus.Offline)
assertThat(user?.utcOffset, isEqualTo((0).toFloat()))
assert(user?.emails == null)
}
}

@Test
fun `getUserById() should return null for non existing user`() {
mockServer.expect()
.get()
.withPath("/api/v1/users.info?userId=nobody")
.andReturn(400, ERROR_INVALID_USER)
.once()

runBlocking {
val user = sut.getUserById("nobody")

assert(user == null)
}
}

@Test
fun `getUserById() should throw an APIException if something went wrong`() {
mockServer.expect()
.get()
.withPath("/api/v1/users.info?userId=nobody")
.andReturn(500, ERROR_GENERIC)
.once()

runBlocking {
try {
sut.getUserById("nobody")
throw RuntimeException("unreachable code")
} catch (ex: RocketChatApiException) {
assertThat(ex.message, isEqualTo("Something went wrong"))
assertThat(ex.errorType, isEqualTo("error-generic-for-test"))
}
}
}

@After
fun shutdown() {
mockServer.shutdown()
Expand Down