改包名com.aiosman.ravenow

This commit is contained in:
2024-11-17 20:07:42 +08:00
parent 914cfca6be
commit 074244c0f8
168 changed files with 897 additions and 970 deletions

View File

@@ -0,0 +1,556 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.AppConfig
import com.aiosman.ravenow.data.api.CaptchaInfo
import com.aiosman.ravenow.data.api.ChangePasswordRequestBody
import com.aiosman.ravenow.data.api.GoogleRegisterRequestBody
import com.aiosman.ravenow.data.api.LoginUserRequestBody
import com.aiosman.ravenow.data.api.RegisterMessageChannelRequestBody
import com.aiosman.ravenow.data.api.RegisterRequestBody
import com.aiosman.ravenow.data.api.ResetPasswordRequestBody
import com.aiosman.ravenow.data.api.TrtcSignResponseBody
import com.aiosman.ravenow.data.api.UnRegisterMessageChannelRequestBody
import com.aiosman.ravenow.data.api.UpdateNoticeRequestBody
import com.aiosman.ravenow.data.api.UpdateUserLangRequestBody
import com.aiosman.ravenow.entity.AccountFavouriteEntity
import com.aiosman.ravenow.entity.AccountLikeEntity
import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.NoticeCommentEntity
import com.aiosman.ravenow.entity.NoticePostEntity
import com.aiosman.ravenow.entity.NoticeUserEntity
import com.google.gson.annotations.SerializedName
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
/**
* 用户资料
*/
data class AccountProfile(
// 用户ID
val id: Int,
// 用户名
val username: String,
// 昵称
val nickname: String,
// 头像
val avatar: String,
// 关注数
val followingCount: Int,
// 粉丝数
val followerCount: Int,
// 是否关注
val isFollowing: Boolean,
// 个人简介
val bio: String,
// 主页背景图
val banner: String?,
// trtcUserId
val trtcUserId: String,
) {
/**
* 转换为Entity
*/
fun toAccountProfileEntity(): AccountProfileEntity {
return AccountProfileEntity(
id = id,
followerCount = followerCount,
followingCount = followingCount,
nickName = nickname,
avatar = "${ApiClient.BASE_SERVER}$avatar",
bio = bio,
country = "Worldwide",
isFollowing = isFollowing,
banner = banner.let {
if (!it.isNullOrEmpty()) {
return@let "${ApiClient.BASE_SERVER}$it"
}
null
},
trtcUserId = trtcUserId
)
}
}
/**
* 消息关联资料
*/
data class NoticePost(
// 动态ID
@SerializedName("id")
val id: Int,
// 动态内容
@SerializedName("textContent")
// 动态图片
val textContent: String,
// 动态图片
@SerializedName("images")
val images: List<Image>,
// 动态时间
@SerializedName("time")
val time: String,
) {
/**
* 转换为Entity
*/
fun toNoticePostEntity(): NoticePostEntity {
return NoticePostEntity(
id = id,
textContent = textContent,
images = images.map {
it.copy(
url = "${ApiClient.BASE_SERVER}${it.url}",
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
blurHash = it.blurHash,
width = it.width,
height = it.height
)
},
time = ApiClient.dateFromApiString(time)
)
}
}
//"comment": {
// "id": 103,
// "content": "ppp",
// "time": "2024-09-08 15:31:37"
//}
data class NoticeComment(
@SerializedName("id")
val id: Int,
@SerializedName("content")
val content: String,
@SerializedName("time")
val time: String,
@SerializedName("replyComment")
val replyComment: NoticeComment?,
@SerializedName("postId")
val postId: Int,
@SerializedName("post")
val post: NoticePost?,
) {
fun toNoticeCommentEntity(): NoticeCommentEntity {
return NoticeCommentEntity(
id = id,
content = content,
postId = postId,
time = ApiClient.dateFromApiString(time),
replyComment = replyComment?.toNoticeCommentEntity(),
post = post?.toNoticePostEntity()
)
}
}
/**
* 消息关联用户
*/
data class NoticeUser(
// 用户ID
@SerializedName("id")
val id: Int,
// 昵称
@SerializedName("nickName")
val nickName: String,
// 头像
@SerializedName("avatar")
val avatar: String,
) {
/**
* 转换为Entity
*/
fun toNoticeUserEntity(): NoticeUserEntity {
return NoticeUserEntity(
id = id,
nickName = nickName,
avatar = "${ApiClient.BASE_SERVER}$avatar",
)
}
}
/**
* 点赞消息通知
*/
data class AccountLike(
// 是否未读
@SerializedName("isUnread")
val isUnread: Boolean,
// 动态
@SerializedName("post")
val post: NoticePost?,
@SerializedName("comment")
val comment: NoticeComment?,
// 点赞用户
@SerializedName("user")
val user: NoticeUser,
// 点赞时间
@SerializedName("likeTime")
val likeTime: String,
// 动态ID
@SerializedName("postId")
val postId: Int,
) {
fun toAccountLikeEntity(): AccountLikeEntity {
return AccountLikeEntity(
post = post?.toNoticePostEntity(),
comment = comment?.toNoticeCommentEntity(),
user = user.toNoticeUserEntity(),
likeTime = ApiClient.dateFromApiString(likeTime),
postId = postId
)
}
}
data class AccountFavourite(
@SerializedName("isUnread")
val isUnread: Boolean,
@SerializedName("post")
val post: NoticePost,
@SerializedName("user")
val user: NoticeUser,
@SerializedName("favoriteTime")
val favouriteTime: String,
) {
fun toAccountFavouriteEntity(): AccountFavouriteEntity {
return AccountFavouriteEntity(
post = post.toNoticePostEntity(),
user = user.toNoticeUserEntity(),
favoriteTime = ApiClient.dateFromApiString(favouriteTime)
)
}
}
data class AccountFollow(
@SerializedName("id")
val id: Int,
@SerializedName("username")
val username: String,
@SerializedName("nickname")
val nickname: String,
@SerializedName("avatar")
val avatar: String,
@SerializedName("isUnread")
val isUnread: Boolean,
@SerializedName("userId")
val userId: Int,
@SerializedName("isFollowing")
val isFollowing: Boolean,
)
//{
// "likeCount": 0,
// "followCount": 0,
// "favoriteCount": 0
//}
data class AccountNotice(
@SerializedName("likeCount")
val likeCount: Int,
@SerializedName("followCount")
val followCount: Int,
@SerializedName("favoriteCount")
val favoriteCount: Int,
@SerializedName("commentCount")
val commentCount: Int,
)
interface AccountService {
/**
* 获取登录当前用户的资料
*/
suspend fun getMyAccountProfile(): AccountProfileEntity
/**
* 获取登录的用户认证信息
*/
suspend fun getMyAccount(): UserAuth
/**
* 使用用户名密码登录
* @param loginName 用户名
* @param password 密码
* @param captchaInfo 验证码信息
*/
suspend fun loginUserWithPassword(
loginName: String,
password: String,
captchaInfo: CaptchaInfo? = null
): UserAuth
/**
* 使用google登录
* @param googleId googleId
*/
suspend fun loginUserWithGoogle(googleId: String): UserAuth
/**
* 退出登录
*/
suspend fun logout()
/**
* 更新用户资料
* @param avatar 头像
* @param nickName 昵称
* @param bio 简介
* @param banner 主页背景图
*/
suspend fun updateProfile(
avatar: UploadImage?,
banner: UploadImage?,
nickName: String?,
bio: String?
)
/**
* 注册用户
* @param loginName 用户名
* @param password 密码
*/
suspend fun registerUserWithPassword(loginName: String, password: String)
/**
* 使用google账号注册
* @param idToken googleIdToken
*/
suspend fun regiterUserWithGoogleAccount(idToken: String)
/**
* 修改密码
* @param oldPassword 旧密码
* @param newPassword 新密码
*/
suspend fun changeAccountPassword(oldPassword: String, newPassword: String)
/**
* 获取我的点赞通知
* @param page 页码
* @param pageSize 每页数量
*/
suspend fun getMyLikeNotice(page: Int, pageSize: Int): ListContainer<AccountLike>
/**
* 获取我的关注通知
* @param page 页码
* @param pageSize 每页数量
*/
suspend fun getMyFollowNotice(page: Int, pageSize: Int): ListContainer<AccountFollow>
/**
* 获取我的收藏通知
* @param page 页码
* @param pageSize 每页数量
*/
suspend fun getMyFavouriteNotice(page: Int, pageSize: Int): ListContainer<AccountFavourite>
/**
* 获取我的通知信息
*/
suspend fun getMyNoticeInfo(): AccountNotice
/**
* 更新通知信息,更新最后一次查看时间
* @param payload 通知信息
*/
suspend fun updateNotice(payload: UpdateNoticeRequestBody)
/**
* 注册消息通道
*/
suspend fun registerMessageChannel(client: String, identifier: String)
/**
* 取消注册消息通道
*/
suspend fun unregisterMessageChannel(client: String, identifier: String)
/**
* 重置密码
*/
suspend fun resetPassword(email: String)
/**
* 更新用户额外信息
*/
suspend fun updateUserExtra(language: String, timeOffset: Int, timezone: String)
/**
* 获取腾讯云TRTC签名
*/
suspend fun getMyTrtcSign(): TrtcSignResponseBody
suspend fun getAppConfig(): AppConfig
}
class AccountServiceImpl : AccountService {
override suspend fun getMyAccountProfile(): AccountProfileEntity {
val resp = ApiClient.api.getMyAccount()
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body.data.toAccountProfileEntity()
}
override suspend fun getMyAccount(): UserAuth {
val resp = ApiClient.api.checkToken()
val body = resp.body() ?: throw ServiceException("Failed to get account")
AppState.UserId = body.id
return UserAuth(body.id)
}
override suspend fun loginUserWithPassword(
loginName: String,
password: String,
captchaInfo: CaptchaInfo?
): UserAuth {
val resp = ApiClient.api.login(LoginUserRequestBody(
username = loginName,
password = password,
captcha = captchaInfo,
))
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to register")
}
return UserAuth(0, resp.body()?.token)
}
override suspend fun loginUserWithGoogle(googleId: String): UserAuth {
val resp = ApiClient.api.login(LoginUserRequestBody(googleId = googleId))
val body = resp.body() ?: throw ServiceException("Failed to login")
return UserAuth(0, body.token)
}
override suspend fun regiterUserWithGoogleAccount(idToken: String) {
val resp = ApiClient.api.registerWithGoogle(GoogleRegisterRequestBody(idToken))
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to register")
}
}
override suspend fun logout() {
// do nothing
}
fun createMultipartBody(file: File, filename: String, name: String): MultipartBody.Part {
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
return MultipartBody.Part.createFormData(name, filename, requestFile)
}
override suspend fun updateProfile(
avatar: UploadImage?,
banner: UploadImage?,
nickName: String?,
bio: String?
) {
val nicknameField: RequestBody? = nickName?.toRequestBody("text/plain".toMediaTypeOrNull())
val bioField: RequestBody? = bio?.toRequestBody("text/plain".toMediaTypeOrNull())
val avatarField: MultipartBody.Part? = avatar?.let {
createMultipartBody(it.file, it.filename, "avatar")
}
val bannerField: MultipartBody.Part? = banner?.let {
createMultipartBody(it.file, it.filename, "banner")
}
ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
}
override suspend fun registerUserWithPassword(loginName: String, password: String) {
val resp = ApiClient.api.register(RegisterRequestBody(loginName, password))
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to register")
}
}
override suspend fun changeAccountPassword(oldPassword: String, newPassword: String) {
val resp = ApiClient.api.changePassword(ChangePasswordRequestBody(oldPassword, newPassword))
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to change password")
}
}
override suspend fun getMyLikeNotice(page: Int, pageSize: Int): ListContainer<AccountLike> {
val resp = ApiClient.api.getMyLikeNotices(page, pageSize)
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body
}
override suspend fun getMyFollowNotice(page: Int, pageSize: Int): ListContainer<AccountFollow> {
val resp = ApiClient.api.getMyFollowNotices(page, pageSize)
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body
}
override suspend fun getMyFavouriteNotice(
page: Int,
pageSize: Int
): ListContainer<AccountFavourite> {
val resp = ApiClient.api.getMyFavouriteNotices(page, pageSize)
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body
}
override suspend fun getMyNoticeInfo(): AccountNotice {
val resp = ApiClient.api.getMyNoticeInfo()
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body.data
}
override suspend fun updateNotice(payload: UpdateNoticeRequestBody) {
ApiClient.api.updateNoticeInfo(payload)
}
override suspend fun registerMessageChannel(client: String, identifier: String) {
ApiClient.api.registerMessageChannel(RegisterMessageChannelRequestBody(client, identifier))
}
override suspend fun unregisterMessageChannel(client: String, identifier: String) {
ApiClient.api.unRegisterMessageChannel(UnRegisterMessageChannelRequestBody(client, identifier))
}
override suspend fun resetPassword(email: String) {
val resp = ApiClient.api.resetPassword(
ResetPasswordRequestBody(
username = email
)
)
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to reset password")
}
}
override suspend fun updateUserExtra(language: String, timeOffset: Int, timezone: String) {
ApiClient.api.updateUserExtra(UpdateUserLangRequestBody(language, timeOffset, timezone))
}
override suspend fun getMyTrtcSign(): TrtcSignResponseBody {
val resp = ApiClient.api.getChatSign()
val body = resp.body() ?: throw ServiceException("Failed to get trtc sign")
return body.data
}
override suspend fun getAppConfig(): AppConfig {
val resp = ApiClient.api.getAppConfig()
val body = resp.body() ?: throw ServiceException("Failed to get app config")
return body.data
}
}

View File

@@ -0,0 +1,45 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CaptchaRequestBody
import com.aiosman.ravenow.data.api.CaptchaResponseBody
import com.aiosman.ravenow.data.api.CheckLoginCaptchaRequestBody
import com.aiosman.ravenow.data.api.GenerateLoginCaptchaRequestBody
interface CaptchaService {
suspend fun generateCaptcha(source: String): CaptchaResponseBody
suspend fun checkLoginCaptcha(username: String): Boolean
suspend fun generateLoginCaptcha(username: String): CaptchaResponseBody
}
class CaptchaServiceImpl : CaptchaService {
override suspend fun generateCaptcha(source: String): CaptchaResponseBody {
val resp = ApiClient.api.generateCaptcha(
CaptchaRequestBody(source)
)
val data = resp.body() ?: throw Exception("Failed to generate captcha")
return data.data.copy(
masterBase64 = data.data.masterBase64.replace("data:image/jpeg;base64,", ""),
thumbBase64 = data.data.thumbBase64.replace("data:image/png;base64,", "")
)
}
override suspend fun checkLoginCaptcha(username: String): Boolean {
val resp = ApiClient.api.checkLoginCaptcha(
CheckLoginCaptchaRequestBody(username)
)
return resp.body()?.data ?: true
}
override suspend fun generateLoginCaptcha(username: String): CaptchaResponseBody {
val resp = ApiClient.api.generateLoginCaptcha(
GenerateLoginCaptchaRequestBody(username)
)
val data = resp.body() ?: throw Exception("Failed to generate captcha")
return data.data.copy(
masterBase64 = data.data.masterBase64.replace("data:image/jpeg;base64,", ""),
thumbBase64 = data.data.thumbBase64.replace("data:image/png;base64,", "")
)
}
}

View File

@@ -0,0 +1,42 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.UpdateChatNotificationRequestBody
import com.aiosman.ravenow.entity.ChatNotification
interface ChatService {
suspend fun getChatNotifications(
targetTrtcId: String
): ChatNotification?
suspend fun updateChatNotification(
targetUserId: Int,
strategy: String
): ChatNotification
}
class ChatServiceImpl : ChatService {
override suspend fun getChatNotifications(
targetTrtcId: String
): ChatNotification? {
val resp = ApiClient.api.getChatNotification(targetTrtcId)
if (resp.isSuccessful) {
return resp.body()?.data
}
return null
}
override suspend fun updateChatNotification(
targetUserId: Int,
strategy: String
): ChatNotification {
val resp = ApiClient.api.updateChatNotification(UpdateChatNotificationRequestBody(
targetUserId = targetUserId,
strategy = strategy
))
if (resp.isSuccessful) {
return resp.body()?.data!!
}
throw Exception("update chat notification failed")
}
}

View File

@@ -0,0 +1,255 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.CommentRequestBody
import com.aiosman.ravenow.entity.CommentEntity
import com.google.gson.annotations.SerializedName
/**
* 评论相关 Service
*/
interface CommentService {
/**
* 获取动态
* @param pageNumber 页码
* @param postId 动态ID,过滤条件
* @param postUser 动态作者ID,获取某个用户所有动态下的评论
* @param selfNotice 是否是自己的通知
* @param order 排序
* @param parentCommentId 父评论ID
* @param pageSize 每页数量
* @return 评论列表
*/
suspend fun getComments(
pageNumber: Int,
postId: Int? = null,
postUser: Int? = null,
selfNotice: Boolean? = null,
order: String? = null,
parentCommentId: Int? = null,
pageSize: Int? = null
): ListContainer<CommentEntity>
/**
* 创建评论
* @param postId 动态ID
* @param content 评论内容
* @param parentCommentId 父评论ID
* @param replyUserId 回复用户ID
*/
suspend fun createComment(
postId: Int,
content: String,
parentCommentId: Int? = null,
replyUserId: Int? = null,
replyCommentId: Int? = null
): CommentEntity
/**
* 点赞评论
* @param commentId 评论ID
*/
suspend fun likeComment(commentId: Int)
/**
* 取消点赞评论
* @param commentId 评论ID
*/
suspend fun dislikeComment(commentId: Int)
/**
* 更新评论已读状态
* @param commentId 评论ID
*/
suspend fun updateReadStatus(commentId: Int)
/**
* 删除评论
* @param commentId 评论ID
*/
suspend fun DeleteComment(commentId: Int)
/**
* 获取评论
* @param commentId 评论ID
*/
suspend fun getCommentById(commentId: Int): CommentEntity
}
/**
* 评论
*/
data class Comment(
// 评论ID
@SerializedName("id")
val id: Int,
// 评论内容
@SerializedName("content")
val content: String,
// 评论用户
@SerializedName("user")
val user: User,
// 点赞数
@SerializedName("likeCount")
val likeCount: Int,
// 是否点赞
@SerializedName("isLiked")
val isLiked: Boolean,
// 创建时间
@SerializedName("createdAt")
val createdAt: String,
// 动态ID
@SerializedName("postId")
val postId: Int,
// 动态
@SerializedName("post")
val post: NoticePost?,
// 是否未读
@SerializedName("isUnread")
val isUnread: Boolean,
@SerializedName("reply")
val reply: List<Comment>,
@SerializedName("replyUser")
val replyUser: User?,
@SerializedName("parentCommentId")
val parentCommentId: Int?,
@SerializedName("replyCount")
val replyCount: Int
) {
/**
* 转换为Entity
*/
fun toCommentEntity(): CommentEntity {
return CommentEntity(
id = id,
name = user.nickName,
comment = content,
date = ApiClient.dateFromApiString(createdAt),
likes = likeCount,
postId = postId,
avatar = "${ApiClient.BASE_SERVER}${user.avatar}",
author = user.id,
liked = isLiked,
unread = isUnread,
post = post?.let {
it.copy(
images = it.images.map {
it.copy(
url = "${ApiClient.BASE_SERVER}${it.url}",
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}"
)
}
)
},
reply = reply.map { it.toCommentEntity() },
replyUserNickname = replyUser?.nickName,
replyUserId = replyUser?.id,
replyUserAvatar = replyUser?.avatar?.let { "${ApiClient.BASE_SERVER}$it" },
parentCommentId = parentCommentId,
replyCount = replyCount
)
}
}
class CommentRemoteDataSource(
private val commentService: CommentService,
) {
suspend fun getComments(
pageNumber: Int,
postId: Int?,
postUser: Int?,
selfNotice: Boolean?,
order: String?,
parentCommentId: Int?,
pageSize: Int? = 20
): ListContainer<CommentEntity> {
return commentService.getComments(
pageNumber,
postId,
postUser = postUser,
selfNotice = selfNotice,
order = order,
parentCommentId = parentCommentId,
pageSize = pageSize
)
}
}
class CommentServiceImpl : CommentService {
override suspend fun getComments(
pageNumber: Int,
postId: Int?,
postUser: Int?,
selfNotice: Boolean?,
order: String?,
parentCommentId: Int?,
pageSize: Int?
): ListContainer<CommentEntity> {
val resp = ApiClient.api.getComments(
page = pageNumber,
postId = postId,
postUser = postUser,
order = order,
selfNotice = selfNotice?.let {
if (it) 1 else 0
},
parentCommentId = parentCommentId,
pageSize = pageSize ?: 20
)
val body = resp.body() ?: throw ServiceException("Failed to get comments")
return ListContainer(
list = body.list.map { it.toCommentEntity() },
page = body.page,
total = body.total,
pageSize = body.pageSize
)
}
override suspend fun createComment(
postId: Int,
content: String,
parentCommentId: Int?,
replyUserId: Int?,
replyCommentId: Int?
): CommentEntity {
val resp = ApiClient.api.createComment(
postId,
CommentRequestBody(
content = content,
parentCommentId = parentCommentId,
replyUserId = replyUserId,
replyCommentId = replyCommentId
),
)
val body = resp.body() ?: throw ServiceException("Failed to create comment")
return body.data.toCommentEntity()
}
override suspend fun likeComment(commentId: Int) {
val resp = ApiClient.api.likeComment(commentId)
return
}
override suspend fun dislikeComment(commentId: Int) {
val resp = ApiClient.api.dislikeComment(commentId)
return
}
override suspend fun updateReadStatus(commentId: Int) {
val resp = ApiClient.api.updateReadStatus(commentId)
return
}
override suspend fun DeleteComment(commentId: Int) {
val resp = ApiClient.api.deleteComment(commentId)
return
}
override suspend fun getCommentById(commentId: Int): CommentEntity {
val resp = ApiClient.api.getComment(commentId)
val body = resp.body() ?: throw ServiceException("Failed to get comment")
return body.data.toCommentEntity()
}
}

View File

@@ -0,0 +1,8 @@
package com.aiosman.ravenow.data
/**
* 通用接口返回数据
*/
data class DataContainer<T>(
val data: T
)

View File

@@ -0,0 +1,19 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.DictItem
interface DictService {
/**
* 获取字典项
*/
suspend fun getDictByKey(key: String): DictItem
}
class DictServiceImpl : DictService {
override suspend fun getDictByKey(key: String): DictItem {
val resp = ApiClient.api.getDict(key)
return resp.body()?.data ?: throw Exception("failed to get dict")
}
}

View File

@@ -0,0 +1,47 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.data.api.ErrorCode
import com.aiosman.ravenow.data.api.toErrorCode
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import okhttp3.ResponseBody
/**
* 错误返回
*/
class ServiceException(
override val message: String,
val code: Int? = 0,
val data: Any? = null,
val error: String? = null,
val name: String? = null,
val errorType: ErrorCode = ErrorCode.UNKNOWN
) : Exception(
message
)
data class ApiErrorResponse(
@SerializedName("code")
val code: Int?,
@SerializedName("error")
val error: String?,
@SerializedName("message")
val name: String?,
) {
fun toServiceException(): ServiceException {
return ServiceException(
message = error ?: name ?: "",
code = code,
error = error,
name = name,
errorType = (code ?: 0).toErrorCode()
)
}
}
fun parseErrorResponse(errorBody: ResponseBody?): ApiErrorResponse? {
return errorBody?.let {
val gson = Gson()
gson.fromJson(it.charStream(), ApiErrorResponse::class.java)
}
}

View File

@@ -0,0 +1,22 @@
package com.aiosman.ravenow.data
import com.google.gson.annotations.SerializedName
/**
* 通用列表接口返回
*/
data class ListContainer<T>(
// 总数
@SerializedName("total")
val total: Int,
// 当前页
@SerializedName("page")
val page: Int,
// 每页数量
@SerializedName("pageSize")
val pageSize: Int,
// 列表
@SerializedName("list")
val list: List<T>
)

View File

@@ -0,0 +1,169 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentImageEntity
import com.google.gson.annotations.SerializedName
import java.io.File
data class Moment(
@SerializedName("id")
val id: Long,
@SerializedName("textContent")
val textContent: String,
@SerializedName("images")
val images: List<Image>,
@SerializedName("user")
val user: User,
@SerializedName("likeCount")
val likeCount: Long,
@SerializedName("isLiked")
val isLiked: Boolean,
@SerializedName("favoriteCount")
val favoriteCount: Long,
@SerializedName("isFavorite")
val isFavorite: Boolean,
@SerializedName("shareCount")
val isCommented: Boolean,
@SerializedName("commentCount")
val commentCount: Long,
@SerializedName("time")
val time: String,
@SerializedName("isFollowed")
val isFollowed: Boolean,
) {
fun toMomentItem(): MomentEntity {
return MomentEntity(
id = id.toInt(),
avatar = "${ApiClient.BASE_SERVER}${user.avatar}",
nickname = user.nickName,
location = "Worldwide",
time = ApiClient.dateFromApiString(time),
followStatus = isFollowed,
momentTextContent = textContent,
momentPicture = R.drawable.default_moment_img,
likeCount = likeCount.toInt(),
commentCount = commentCount.toInt(),
shareCount = 0,
favoriteCount = favoriteCount.toInt(),
images = images.map {
MomentImageEntity(
url = "${ApiClient.BASE_SERVER}${it.url}",
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
id = it.id,
blurHash = it.blurHash,
width = it.width,
height = it.height
)
},
authorId = user.id.toInt(),
liked = isLiked,
isFavorite = isFavorite,
)
}
}
data class Image(
@SerializedName("id")
val id: Long,
@SerializedName("url")
val url: String,
@SerializedName("thumbnail")
val thumbnail: String,
@SerializedName("blurHash")
val blurHash: String?,
@SerializedName("width")
val width: Int?,
@SerializedName("height")
val height: Int?
)
data class User(
@SerializedName("id")
val id: Long,
@SerializedName("nickName")
val nickName: String,
@SerializedName("avatar")
val avatar: String
)
data class UploadImage(
val file: File,
val filename: String,
val url: String,
val ext: String
)
interface MomentService {
/**
* 获取动态详情
* @param id 动态ID
*/
suspend fun getMomentById(id: Int): MomentEntity
/**
* 点赞动态
* @param id 动态ID
*/
suspend fun likeMoment(id: Int)
/**
* 取消点赞动态
* @param id 动态ID
*/
suspend fun dislikeMoment(id: Int)
/**
* 获取动态列表
* @param pageNumber 页码
* @param author 作者ID,过滤条件
* @param timelineId 用户时间线ID,指定用户 ID 的时间线
* @param contentSearch 内容搜索,过滤条件
* @param trend 是否趋势动态
* @param explore 是否探索动态
* @return 动态列表
*/
suspend fun getMoments(
pageNumber: Int,
author: Int? = null,
timelineId: Int? = null,
contentSearch: String? = null,
trend: Boolean? = false,
explore: Boolean? = false,
favoriteUserId: Int? = null
): ListContainer<MomentEntity>
/**
* 创建动态
* @param content 动态内容
* @param authorId 作者ID
* @param images 图片列表
* @param relPostId 关联动态ID
*/
suspend fun createMoment(
content: String,
authorId: Int,
images: List<UploadImage>,
relPostId: Int? = null
): MomentEntity
/**
* 收藏动态
* @param id 动态ID
*/
suspend fun favoriteMoment(id: Int)
/**
* 取消收藏动态
* @param id 动态ID
*/
suspend fun unfavoriteMoment(id: Int)
/**
* 删除动态
*/
suspend fun deleteMoment(id: Int)
}

View File

@@ -0,0 +1,100 @@
package com.aiosman.ravenow.data
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.AccountProfileEntity
data class UserAuth(
val id: Int,
val token: String? = null
)
/**
* 用户相关 Service
*/
interface UserService {
/**
* 获取用户信息
* @param id 用户ID
* @return 用户信息
*/
suspend fun getUserProfile(id: String): AccountProfileEntity
/**
* 关注用户
* @param id 用户ID
*/
suspend fun followUser(id: String)
/**
* 取消关注用户
* @param id 用户ID
*/
suspend fun unFollowUser(id: String)
/**
* 获取用户列表
* @param pageSize 分页大小
* @param page 页码
* @param nickname 昵称搜索
* @param followerId 粉丝ID,账号粉丝
* @param followingId 关注ID,账号关注
* @return 用户列表
*/
suspend fun getUsers(
pageSize: Int = 20,
page: Int = 1,
nickname: String? = null,
followerId: Int? = null,
followingId: Int? = null
): ListContainer<AccountProfileEntity>
suspend fun getUserProfileByTrtcUserId(id: String):AccountProfileEntity
}
class UserServiceImpl : UserService {
override suspend fun getUserProfile(id: String): AccountProfileEntity {
val resp = ApiClient.api.getAccountProfileById(id.toInt())
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body.data.toAccountProfileEntity()
}
override suspend fun followUser(id: String) {
val resp = ApiClient.api.followUser(id.toInt())
return
}
override suspend fun unFollowUser(id: String) {
val resp = ApiClient.api.unfollowUser(id.toInt())
return
}
override suspend fun getUsers(
pageSize: Int,
page: Int,
nickname: String?,
followerId: Int?,
followingId: Int?
): ListContainer<AccountProfileEntity> {
val resp = ApiClient.api.getUsers(
page = page,
pageSize = pageSize,
search = nickname,
followerId = followerId,
followingId = followingId
)
val body = resp.body() ?: throw ServiceException("Failed to get account")
return ListContainer<AccountProfileEntity>(
list = body.list.map { it.toAccountProfileEntity() },
page = body.page,
total = body.total,
pageSize = body.pageSize,
)
}
override suspend fun getUserProfileByTrtcUserId(id: String): AccountProfileEntity {
val resp = ApiClient.api.getAccountProfileByTrtcUserId(id)
val body = resp.body() ?: throw ServiceException("Failed to get account")
return body.data.toAccountProfileEntity()
}
}

View File

@@ -0,0 +1,140 @@
package com.aiosman.ravenow.data.api
import android.icu.text.SimpleDateFormat
import android.icu.util.TimeZone
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ConstVars
import com.auth0.android.jwt.JWT
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.security.cert.CertificateException
import java.util.Date
import java.util.Locale
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
fun getUnsafeOkHttpClient(
authInterceptor: AuthInterceptor? = null
): OkHttpClient {
return try {
// Create a trust manager that does not validate certificate chains
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
@Throws(CertificateException::class)
override fun checkClientTrusted(
chain: Array<java.security.cert.X509Certificate>,
authType: String
) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(
chain: Array<java.security.cert.X509Certificate>,
authType: String
) {
}
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> = arrayOf()
})
// Install the all-trusting trust manager
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
// Create an ssl socket factory with our all-trusting manager
val sslSocketFactory = sslContext.socketFactory
OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.apply {
authInterceptor?.let {
addInterceptor(it)
}
}
.build()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
class AuthInterceptor() : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
val token = AppStore.token
token?.let {
val jwt = JWT(token)
val expiresAt = jwt.expiresAt?.time?.minus(3000)
val currentTime = System.currentTimeMillis()
val isExpired = expiresAt != null && currentTime > expiresAt
if (isExpired) {
runBlocking {
val newToken = refreshToken()
if (newToken != null) {
AppStore.token = newToken
}
}
}
}
requestBuilder.addHeader("Authorization", "Bearer ${AppStore.token}")
val response = chain.proceed(requestBuilder.build())
return response
}
private suspend fun refreshToken(): String? {
val client = Retrofit.Builder()
.baseUrl(ApiClient.RETROFIT_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(getUnsafeOkHttpClient())
.build()
.create(RiderProAPI::class.java)
val resp = client.refreshToken(AppStore.token ?: "")
val newToken = resp.body()?.token
if (newToken != null) {
AppStore.token = newToken
}
return newToken
}
}
object ApiClient {
const val BASE_SERVER = ConstVars.BASE_SERVER
const val BASE_API_URL = "${BASE_SERVER}/api/v1"
const val RETROFIT_URL = "${BASE_API_URL}/"
const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"
private val okHttpClient: OkHttpClient by lazy {
getUnsafeOkHttpClient(authInterceptor = AuthInterceptor())
}
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(RETROFIT_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val api: RiderProAPI by lazy {
retrofit.create(RiderProAPI::class.java)
}
fun formatTime(date: Date): String {
val dateFormat = SimpleDateFormat(TIME_FORMAT, Locale.getDefault())
return dateFormat.format(date)
}
fun dateFromApiString(apiString: String): Date {
val timeFormat = TIME_FORMAT
val simpleDateFormat = SimpleDateFormat(timeFormat, Locale.getDefault())
simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC")
val date = simpleDateFormat.parse(apiString)
simpleDateFormat.timeZone = TimeZone.getDefault()
val localDateString = simpleDateFormat.format(date)
return simpleDateFormat.parse(localDateString)
}
}

View File

@@ -0,0 +1,42 @@
package com.aiosman.ravenow.data.api
import android.content.Context
import android.widget.Toast
import com.aiosman.ravenow.R
//
enum class ErrorCode(val code: Int) {
USER_EXIST(40001),
USER_NOT_EXIST(40002),
InvalidateCaptcha(40004),
IncorrectOldPassword(40005),
// 未知错误
UNKNOWN(99999)
}
fun ErrorCode.toErrorMessage(context: Context): String {
return context.getErrorMessageCode(code)
}
fun ErrorCode.showToast(context: Context) {
Toast.makeText(context, toErrorMessage(context), Toast.LENGTH_SHORT).show()
}
// code to ErrorCode
fun Int.toErrorCode(): ErrorCode {
return when (this) {
40001 -> ErrorCode.USER_EXIST
40002 -> ErrorCode.USER_NOT_EXIST
40004 -> ErrorCode.InvalidateCaptcha
40005 -> ErrorCode.IncorrectOldPassword
else -> ErrorCode.UNKNOWN
}
}
fun Context.getErrorMessageCode(code: Int?): String {
return when (code) {
40001 -> getString(R.string.error_10001_user_exist)
ErrorCode.IncorrectOldPassword.code -> getString(R.string.error_incorrect_old_password)
else -> getString(R.string.error_unknown)
}
}

View File

@@ -0,0 +1,428 @@
package com.aiosman.ravenow.data.api
import com.aiosman.ravenow.data.AccountFavourite
import com.aiosman.ravenow.data.AccountFollow
import com.aiosman.ravenow.data.AccountLike
import com.aiosman.ravenow.data.AccountNotice
import com.aiosman.ravenow.data.AccountProfile
import com.aiosman.ravenow.data.Comment
import com.aiosman.ravenow.data.DataContainer
import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.Moment
import com.aiosman.ravenow.entity.ChatNotification
import com.google.gson.annotations.SerializedName
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
data class RegisterRequestBody(
@SerializedName("username")
val username: String,
@SerializedName("password")
val password: String
)
data class LoginUserRequestBody(
@SerializedName("username")
val username: String? = null,
@SerializedName("password")
val password: String? = null,
@SerializedName("googleId")
val googleId: String? = null,
@SerializedName("captcha")
val captcha: CaptchaInfo? = null,
)
data class GoogleRegisterRequestBody(
@SerializedName("idToken")
val idToken: String
)
data class AuthResult(
@SerializedName("code")
val code: Int,
@SerializedName("expire")
val expire: String,
@SerializedName("token")
val token: String
)
data class ValidateTokenResult(
@SerializedName("id")
val id: Int,
)
data class CommentRequestBody(
@SerializedName("content")
val content: String,
@SerializedName("parentCommentId")
val parentCommentId: Int? = null,
@SerializedName("replyUserId")
val replyUserId: Int? = null,
@SerializedName("replyCommentId")
val replyCommentId: Int? = null,
)
data class ChangePasswordRequestBody(
@SerializedName("currentPassword")
val oldPassword: String = "",
@SerializedName("newPassword")
val newPassword: String = ""
)
data class UpdateNoticeRequestBody(
@SerializedName("lastLookLikeTime")
val lastLookLikeTime: String? = null,
@SerializedName("lastLookFollowTime")
val lastLookFollowTime: String? = null,
@SerializedName("lastLookFavoriteTime")
val lastLookFavouriteTime: String? = null
)
data class RegisterMessageChannelRequestBody(
@SerializedName("client")
val client: String,
@SerializedName("identifier")
val identifier: String,
)
data class UnRegisterMessageChannelRequestBody(
@SerializedName("client")
val client: String,
@SerializedName("identifier")
val identifier: String,
)
data class ResetPasswordRequestBody(
@SerializedName("username")
val username: String,
)
data class UpdateUserLangRequestBody(
@SerializedName("language")
val lang: String,
@SerializedName("timeOffset")
val timeOffset: Int,
@SerializedName("timezone")
val timezone: String,
)
data class TrtcSignResponseBody(
@SerializedName("sig")
val sig: String,
@SerializedName("userId")
val userId: String,
)
data class AppConfig(
@SerializedName("trtcAppId")
val trtcAppId: Int,
)
data class DictItem(
@SerializedName("key")
val key: String,
@SerializedName("value")
val value: String,
@SerializedName("desc")
val desc: String,
)
data class CaptchaRequestBody(
@SerializedName("source")
val source: String,
)
data class CaptchaResponseBody(
@SerializedName("id")
val id: Int,
@SerializedName("thumb_base64")
val thumbBase64: String,
@SerializedName("master_base64")
val masterBase64: String,
@SerializedName("count")
val count: Int,
)
data class CheckLoginCaptchaRequestBody(
@SerializedName("username")
val username: String,
)
data class GenerateLoginCaptchaRequestBody(
@SerializedName("username")
val username: String,
)
data class DotPosition(
@SerializedName("index")
val index: Int,
@SerializedName("x")
val x: Int,
@SerializedName("y")
val y: Int,
)
data class CaptchaInfo(
@SerializedName("id")
val id: Int,
@SerializedName("dot")
val dot: List<DotPosition>
)
data class UpdateChatNotificationRequestBody(
@SerializedName("targetUserId")
val targetUserId: Int,
@SerializedName("strategy")
val strategy: String,
)
interface RiderProAPI {
@POST("register")
suspend fun register(@Body body: RegisterRequestBody): Response<Unit>
@POST("login")
suspend fun login(@Body body: LoginUserRequestBody): Response<AuthResult>
@GET("auth/token")
suspend fun checkToken(): Response<ValidateTokenResult>
@GET("auth/refresh_token")
suspend fun refreshToken(
@Query("token") token: String
): Response<AuthResult>
@GET("posts")
suspend fun getPosts(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("timelineId") timelineId: Int? = null,
@Query("authorId") authorId: Int? = null,
@Query("contentSearch") contentSearch: String? = null,
@Query("postUser") postUser: Int? = null,
@Query("trend") trend: String? = null,
@Query("favouriteUserId") favouriteUserId: Int? = null,
@Query("explore") explore: String? = null,
): Response<ListContainer<Moment>>
@Multipart
@POST("posts")
suspend fun createPost(
@Part image: List<MultipartBody.Part>,
@Part("textContent") textContent: RequestBody,
): Response<DataContainer<Moment>>
@GET("post/{id}")
suspend fun getPost(
@Path("id") id: Int
): Response<DataContainer<Moment>>
@POST("post/{id}/like")
suspend fun likePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/dislike")
suspend fun dislikePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/favorite")
suspend fun favoritePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/unfavorite")
suspend fun unfavoritePost(
@Path("id") id: Int
): Response<Unit>
@POST("post/{id}/comment")
suspend fun createComment(
@Path("id") id: Int,
@Body body: CommentRequestBody
): Response<DataContainer<Comment>>
@POST("comment/{id}/like")
suspend fun likeComment(
@Path("id") id: Int
): Response<Unit>
@POST("comment/{id}/dislike")
suspend fun dislikeComment(
@Path("id") id: Int
): Response<Unit>
@POST("comment/{id}/read")
suspend fun updateReadStatus(
@Path("id") id: Int
): Response<Unit>
@GET("comments")
suspend fun getComments(
@Query("page") page: Int = 1,
@Query("postId") postId: Int? = null,
@Query("pageSize") pageSize: Int = 20,
@Query("postUser") postUser: Int? = null,
@Query("selfNotice") selfNotice: Int? = 0,
@Query("order") order: String? = null,
@Query("parentCommentId") parentCommentId: Int? = null,
): Response<ListContainer<Comment>>
@GET("account/my")
suspend fun getMyAccount(): Response<DataContainer<AccountProfile>>
@Multipart
@PATCH("account/my/profile")
suspend fun updateProfile(
@Part avatar: MultipartBody.Part?,
@Part banner: MultipartBody.Part?,
@Part("nickname") nickname: RequestBody?,
@Part("bio") bio: RequestBody?,
): Response<Unit>
@POST("account/my/password")
suspend fun changePassword(
@Body body: ChangePasswordRequestBody
): Response<Unit>
@GET("account/my/notice/like")
suspend fun getMyLikeNotices(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
): Response<ListContainer<AccountLike>>
@GET("account/my/notice/follow")
suspend fun getMyFollowNotices(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
): Response<ListContainer<AccountFollow>>
@GET("account/my/notice/favourite")
suspend fun getMyFavouriteNotices(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
): Response<ListContainer<AccountFavourite>>
@GET("account/my/notice")
suspend fun getMyNoticeInfo(): Response<DataContainer<AccountNotice>>
@POST("account/my/notice")
suspend fun updateNoticeInfo(
@Body body: UpdateNoticeRequestBody
): Response<Unit>
@POST("account/my/messaging")
suspend fun registerMessageChannel(
@Body body: RegisterMessageChannelRequestBody
): Response<Unit>
@POST("account/my/messaging/unregister")
suspend fun unRegisterMessageChannel(
@Body body: UnRegisterMessageChannelRequestBody
): Response<Unit>
@GET("profile/{id}")
suspend fun getAccountProfileById(
@Path("id") id: Int
): Response<DataContainer<AccountProfile>>
@GET("profile/trtc/{id}")
suspend fun getAccountProfileByTrtcUserId(
@Path("id") id: String
): Response<DataContainer<AccountProfile>>
@POST("user/{id}/follow")
suspend fun followUser(
@Path("id") id: Int
): Response<Unit>
@POST("user/{id}/unfollow")
suspend fun unfollowUser(
@Path("id") id: Int
): Response<Unit>
@GET("users")
suspend fun getUsers(
@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("nickname") search: String? = null,
@Query("followerId") followerId: Int? = null,
@Query("followingId") followingId: Int? = null,
): Response<ListContainer<AccountProfile>>
@POST("register/google")
suspend fun registerWithGoogle(@Body body: GoogleRegisterRequestBody): Response<AuthResult>
@DELETE("post/{id}")
suspend fun deletePost(
@Path("id") id: Int
): Response<Unit>
@DELETE("comment/{id}")
suspend fun deleteComment(
@Path("id") id: Int
): Response<Unit>
@POST("account/my/password/reset")
suspend fun resetPassword(
@Body body: ResetPasswordRequestBody
): Response<Unit>
@GET("comment/{id}")
suspend fun getComment(
@Path("id") id: Int
): Response<DataContainer<Comment>>
@PATCH("account/my/extra")
suspend fun updateUserExtra(
@Body body: UpdateUserLangRequestBody
): Response<Unit>
@GET("account/my/chat/sign")
suspend fun getChatSign(): Response<DataContainer<TrtcSignResponseBody>>
@GET("app/info")
suspend fun getAppConfig(): Response<DataContainer<AppConfig>>
@GET("dict")
suspend fun getDict(
@Query("key") key: String
): Response<DataContainer<DictItem>>
@POST("captcha/generate")
suspend fun generateCaptcha(
@Body body: CaptchaRequestBody
): Response<DataContainer<CaptchaResponseBody>>
@POST("login/needCaptcha")
suspend fun checkLoginCaptcha(
@Body body: CheckLoginCaptchaRequestBody
): Response<DataContainer<Boolean>>
@POST("captcha/login/generate")
suspend fun generateLoginCaptcha(
@Body body: GenerateLoginCaptchaRequestBody
): Response<DataContainer<CaptchaResponseBody>>
@GET("chat/notification")
suspend fun getChatNotification(
@Query("targetTrtcId") targetTrtcId: String
): Response<DataContainer<ChatNotification>>
@POST("chat/notification")
suspend fun updateChatNotification(
@Body body: UpdateChatNotificationRequestBody
): Response<DataContainer<ChatNotification>>
}