Update API version (#147)

* Bump VK Api version to 5.238
* Implemented new authorization flow (at the moment, without auto re-requesting token)
* Add support for sticker pack preview attachments
* Bump LongPoll to version 19
* Improved messages handling
* Fixed coloring issues
* Cache improvements
* Archive screen with full functionality
* Recomposition fixes
* Markdown support for messages bubbles
* Adjust app name font size based on screen width
* Navigation related improvements
* Add logout functionality
This commit is contained in:
2025-04-04 20:43:59 +03:00
committed by GitHub
parent add67b6f8d
commit 89748b72ed
237 changed files with 4896 additions and 3289 deletions
@@ -84,11 +84,12 @@ class MainViewModelImpl(
override fun onError(error: BaseError) { override fun onError(error: BaseError) {
when (error) { when (error) {
BaseError.SessionExpired -> { BaseError.SessionExpired,
BaseError.AccountBlocked -> {
isNeedToReplaceWithAuth.update { true } isNeedToReplaceWithAuth.update { true }
} }
is BaseError.SimpleError -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui else -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
} }
} }
@@ -2,7 +2,7 @@ package dev.meloda.fast.navigation
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import dev.meloda.fast.conversations.navigation.Conversations import dev.meloda.fast.conversations.navigation.ConversationsGraph
import dev.meloda.fast.friends.navigation.Friends import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.BottomNavigationItem import dev.meloda.fast.model.BottomNavigationItem
@@ -21,10 +21,10 @@ object Main
fun NavGraphBuilder.mainScreen( fun NavGraphBuilder.mainScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onSettingsButtonClicked: () -> Unit, onSettingsButtonClicked: () -> Unit,
onConversationClicked: (conversationId: Int) -> Unit, onNavigateToMessagesHistory: (conversationId: Long) -> Unit,
onPhotoClicked: (url: String) -> Unit, onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userId: Int) -> Unit, onMessageClicked: (userid: Long) -> Unit,
onCreateChatClicked: () -> Unit onNavigateToCreateChat: () -> Unit
) { ) {
val navigationItems = ImmutableList.of( val navigationItems = ImmutableList.of(
BottomNavigationItem( BottomNavigationItem(
@@ -37,7 +37,7 @@ fun NavGraphBuilder.mainScreen(
titleResId = UiR.string.title_conversations, titleResId = UiR.string.title_conversations,
selectedIconResId = UiR.drawable.baseline_chat_24, selectedIconResId = UiR.drawable.baseline_chat_24,
unselectedIconResId = UiR.drawable.outline_chat_24, unselectedIconResId = UiR.drawable.outline_chat_24,
route = Conversations route = ConversationsGraph
), ),
BottomNavigationItem( BottomNavigationItem(
titleResId = UiR.string.title_profile, titleResId = UiR.string.title_profile,
@@ -52,10 +52,10 @@ fun NavGraphBuilder.mainScreen(
navigationItems = navigationItems, navigationItems = navigationItems,
onError = onError, onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked,
onConversationItemClicked = onConversationClicked, onNavigateToMessagesHistory = onNavigateToMessagesHistory,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked, onMessageClicked = onMessageClicked,
onCreateChatClicked = onCreateChatClicked onNavigateToCreateChat = onNavigateToCreateChat
) )
} }
} }
@@ -37,7 +37,7 @@ import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.conversations.navigation.Conversations import dev.meloda.fast.conversations.navigation.Conversations
import dev.meloda.fast.conversations.navigation.conversationsScreen import dev.meloda.fast.conversations.navigation.conversationsGraph
import dev.meloda.fast.friends.navigation.Friends import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.friends.navigation.friendsScreen import dev.meloda.fast.friends.navigation.friendsScreen
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
@@ -46,7 +46,8 @@ import dev.meloda.fast.navigation.MainGraph
import dev.meloda.fast.profile.navigation.profileScreen import dev.meloda.fast.profile.navigation.profileScreen
import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalBottomPadding
import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalScrollToTop import dev.meloda.fast.ui.theme.LocalNavController
import dev.meloda.fast.ui.theme.LocalReselectedTab
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.theme.LocalUser import dev.meloda.fast.ui.theme.LocalUser
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
@@ -57,10 +58,10 @@ fun MainScreen(
navigationItems: ImmutableList<BottomNavigationItem>, navigationItems: ImmutableList<BottomNavigationItem>,
onError: (BaseError) -> Unit = {}, onError: (BaseError) -> Unit = {},
onSettingsButtonClicked: () -> Unit = {}, onSettingsButtonClicked: () -> Unit = {},
onConversationItemClicked: (conversationId: Int) -> Unit = {}, onNavigateToMessagesHistory: (conversationId: Long) -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {},
onMessageClicked: (userId: Int) -> Unit = {}, onMessageClicked: (userid: Long) -> Unit = {},
onCreateChatClicked: () -> Unit = {} onNavigateToCreateChat: () -> Unit = {}
) { ) {
val theme = LocalThemeConfig.current val theme = LocalThemeConfig.current
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
@@ -75,7 +76,7 @@ fun MainScreen(
derivedStateOf { user?.photo100 } derivedStateOf { user?.photo100 }
} }
var scrollToTop by remember { var tabReselected by remember {
mutableStateOf( mutableStateOf(
navigationItems.associate { navigationItems.associate {
it.route to false it.route to false
@@ -113,7 +114,7 @@ fun MainScreen(
} }
} }
} else { } else {
scrollToTop = scrollToTop.toMutableMap().also { tabReselected = tabReselected.toMutableMap().also {
it[navigationItems[index].route] = true it[navigationItems[index].route] = true
} }
} }
@@ -164,7 +165,8 @@ fun MainScreen(
CompositionLocalProvider( CompositionLocalProvider(
LocalHazeState provides hazeState, LocalHazeState provides hazeState,
LocalBottomPadding provides padding.calculateBottomPadding(), LocalBottomPadding provides padding.calculateBottomPadding(),
LocalScrollToTop provides scrollToTop LocalReselectedTab provides tabReselected,
LocalNavController provides navController
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
@@ -182,18 +184,17 @@ fun MainScreen(
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked, onMessageClicked = onMessageClicked,
onScrolledToTop = { onScrolledToTop = {
scrollToTop = scrollToTop.toMutableMap().also { tabReselected = tabReselected.toMutableMap().also {
it[Friends] = false it[Friends] = false
} }
}, },
) )
conversationsScreen( conversationsGraph(
onError = onError, onError = onError,
onConversationItemClicked = onConversationItemClicked, onNavigateToMessagesHistory = onNavigateToMessagesHistory,
onCreateChatClicked = onCreateChatClicked, onNavigateToCreateChat = onNavigateToCreateChat,
navController = navController,
onScrolledToTop = { onScrolledToTop = {
scrollToTop = scrollToTop.toMutableMap().also { tabReselected = tabReselected.toMutableMap().also {
it[Conversations] = false it[Conversations] = false
} }
} }
@@ -201,8 +202,7 @@ fun MainScreen(
profileScreen( profileScreen(
onError = onError, onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked, onSettingsButtonClicked = onSettingsButtonClicked,
onPhotoClicked = onPhotoClicked, onPhotoClicked = onPhotoClicked
navController = navController
) )
} }
} }
@@ -10,6 +10,7 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -38,6 +39,8 @@ import dev.meloda.fast.photoviewer.navigation.photoViewScreen
import dev.meloda.fast.settings.navigation.navigateToSettings import dev.meloda.fast.settings.navigation.navigateToSettings
import dev.meloda.fast.settings.navigation.settingsScreen import dev.meloda.fast.settings.navigation.settingsScreen
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.theme.LocalNavController
import dev.meloda.fast.ui.theme.LocalNavRootController
@Composable @Composable
fun RootScreen( fun RootScreen(
@@ -111,53 +114,59 @@ fun RootScreen(
} }
if (startDestination != null) { if (startDestination != null) {
NavHost( CompositionLocalProvider(
navController = navController, LocalNavRootController provides navController,
startDestination = requireNotNull(startDestination), LocalNavController provides navController
enterTransition = { fadeIn(animationSpec = tween(200)) },
exitTransition = { fadeOut(animationSpec = tween(200)) }
) { ) {
authNavGraph( NavHost(
onNavigateToMain = { navController = navController,
viewModel.onUserAuthenticated() startDestination = requireNotNull(startDestination),
navController.navigateToMain() enterTransition = { fadeIn(animationSpec = tween(200)) },
}, exitTransition = { fadeOut(animationSpec = tween(200)) }
navController = navController ) {
) authNavGraph(
mainScreen( onNavigateToMain = {
onError = viewModel::onError, viewModel.onUserAuthenticated()
onSettingsButtonClicked = navController::navigateToSettings, navController.navigateToMain()
onConversationClicked = navController::navigateToMessagesHistory, },
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }, navController = navController
onMessageClicked = navController::navigateToMessagesHistory, )
onCreateChatClicked = navController::navigateToCreateChat
)
messagesHistoryScreen( mainScreen(
onError = viewModel::onError, onError = viewModel::onError,
onBack = navController::navigateUp, onSettingsButtonClicked = navController::navigateToSettings,
onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
) onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
chatMaterialsScreen( onMessageClicked = navController::navigateToMessagesHistory,
onBack = navController::navigateUp, onNavigateToCreateChat = navController::navigateToCreateChat
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) } )
)
createChatScreen(
onChatCreated = { conversationId ->
navController.popBackStack()
navController.navigateToMessagesHistory(conversationId)
},
navController = navController
)
settingsScreen( messagesHistoryScreen(
onBack = navController::navigateUp, onError = viewModel::onError,
onLogOutButtonClicked = { navController.navigateToAuth(true) }, onBack = navController::navigateUp,
onLanguageItemClicked = navController::navigateToLanguagePicker onNavigateToChatMaterials = navController::navigateToChatMaterials
) )
languagePickerScreen(onBack = navController::navigateUp) chatMaterialsScreen(
onBack = navController::navigateUp,
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
)
createChatScreen(
onChatCreated = { conversationId ->
navController.popBackStack()
navController.navigateToMessagesHistory(conversationId)
},
navController = navController
)
photoViewScreen(onBack = navController::navigateUp) settingsScreen(
onBack = navController::navigateUp,
onLogOutButtonClicked = { navController.navigateToAuth(true) },
onLanguageItemClicked = navController::navigateToLanguagePicker
)
languagePickerScreen(onBack = navController::navigateUp)
photoViewScreen(onBack = navController::navigateUp)
}
} }
} }
} }
@@ -4,7 +4,7 @@ object AppConstants {
const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive" const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive"
const val API_VERSION = "5.173" const val API_VERSION = "5.238"
const val URL_OAUTH = "https://oauth.vk.com" const val URL_OAUTH = "https://oauth.vk.com"
const val URL_API = "https://api.vk.com/method" const val URL_API = "https://api.vk.com/method"
@@ -5,12 +5,12 @@ object VkConstants {
const val GROUP_FIELDS = "description,members_count,counters,status,verified" const val GROUP_FIELDS = "description,members_count,counters,status,verified"
const val USER_FIELDS = const val USER_FIELDS =
"photo_50,photo_100,photo_200,photo_400_orig,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info,bdate" "photo_50,photo_100,photo_200,photo_400_orig,status,screen_name,online_info,last_seen,verified,sex,bdate"
const val ALL_FIELDS = const val ALL_FIELDS =
"$USER_FIELDS,$GROUP_FIELDS" "$USER_FIELDS,$GROUP_FIELDS"
const val LP_VERSION = 10 const val LP_VERSION = 19
const val VK_APP_ID = "2274003" const val VK_APP_ID = "2274003"
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH" const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
@@ -18,6 +18,11 @@ object VkConstants {
const val FAST_GROUP_ID = -119516304 const val FAST_GROUP_ID = -119516304
const val FAST_APP_ID = "6964679" const val FAST_APP_ID = "6964679"
const val MESSENGER_APP_ID = 51453752
const val MESSENGER_APP_SECRET = "4UyuCUsdK8pVCNoeQuGi"
const val MESSENGER_APP_SCOPE = 1454174
object Auth { object Auth {
const val SCOPE = "notify," + const val SCOPE = "notify," +
"friends," + "friends," +
@@ -75,6 +75,11 @@ fun <T> MutableStateFlow<T>.setValue(function: (T) -> T) {
update { newValue } update { newValue }
} }
fun <T> MutableStateFlow<T>.updateValue(block: T.() -> T) {
val newValue = block(value)
update { newValue }
}
fun Any.asInt(): Int { fun Any.asInt(): Int {
return when (this) { return when (this) {
is Number -> this.toInt() is Number -> this.toInt()
@@ -83,6 +88,14 @@ fun Any.asInt(): Int {
} }
} }
fun Any.asLong(): Long {
return when(this) {
is Number -> this.toLong()
else -> throw IllegalArgumentException("Object is not numeric")
}
}
fun <T> Any.toList(mapper: (old: Any) -> T): List<T> { fun <T> Any.toList(mapper: (old: Any) -> T): List<T> {
return when (this) { return when (this) {
is List<*> -> this.mapNotNull { it?.run(mapper) } is List<*> -> this.mapNotNull { it?.run(mapper) }
@@ -25,8 +25,6 @@ sealed class State<out T> {
data object InternalError : Error() data object InternalError : Error()
data class OAuthError(val error: OAuthErrorDomain) : Error() data class OAuthError(val error: OAuthErrorDomain) : Error()
data class TestError(val message: String) : Error()
} }
fun isLoading(): Boolean = this is Loading fun isLoading(): Boolean = this is Loading
@@ -38,8 +36,8 @@ sealed class State<out T> {
} }
inline fun <T> State<T>.processState( inline fun <T> State<T>.processState(
error: (error: State.Error) -> (Unit), error: (error: State.Error) -> Unit,
success: (data: T) -> (Unit), success: (data: T) -> Unit,
idle: (() -> (Unit)) = {}, idle: (() -> (Unit)) = {},
loading: (() -> (Unit)) = {}, loading: (() -> (Unit)) = {},
any: () -> Unit = {} any: () -> Unit = {}
@@ -61,11 +59,41 @@ inline fun <T> State<T>.processState(
} }
} }
fun OAuthErrorDomain?.toStateApiError(): State.Error {
if (this == null) return State.Error.ConnectionError
return State.Error.OAuthError(this)
}
fun RestApiErrorDomain?.toStateApiError(): State.Error = when (this) { fun RestApiErrorDomain?.toStateApiError(): State.Error = when (this) {
null -> State.Error.ConnectionError null -> State.Error.ConnectionError
else -> State.Error.ApiError(VkErrorCode.parse(code), message) else -> State.Error.ApiError(VkErrorCode.parse(code), message)
} }
fun <T : Any> ApiResult<T, OAuthErrorDomain>.asState() = when (this) {
is ApiResult.Success -> State.Success(this.value)
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
}
fun <T : Any, N> ApiResult<T, OAuthErrorDomain>.asState(successMapper: (T) -> N) =
when (this) {
is ApiResult.Success -> State.Success(successMapper(this.value))
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
}
fun <T : Any, E : Any> ApiResult<T, E>.success(): T =
when (this) {
is ApiResult.Success -> value
else -> throw IllegalArgumentException()
}
fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) { fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
is ApiResult.Success -> State.Success(this.value) is ApiResult.Success -> State.Success(this.value)
@@ -6,17 +6,18 @@ object UserConfig {
private const val ARG_CURRENT_USER_ID = "current_user_id" private const val ARG_CURRENT_USER_ID = "current_user_id"
var currentUserId: Int = -1 var currentUserId: Long = -1
get() = AppSettings.getInt(ARG_CURRENT_USER_ID, -1) get() = AppSettings.getLong(ARG_CURRENT_USER_ID, -1)
set(value) { set(value) {
field = value field = value
AppSettings.edit { putInt(ARG_CURRENT_USER_ID, value) } AppSettings.edit { putLong(ARG_CURRENT_USER_ID, value) }
} }
var userId: Int = -1 var userId: Long = -1
var accessToken: String = "" var accessToken: String = ""
var fastToken: String? = "" var fastToken: String? = ""
var trustedHash: String? = null var trustedHash: String? = null
var exchangeToken: String? = null
fun clear() { fun clear() {
currentUserId = -1 currentUserId = -1
@@ -10,7 +10,7 @@ class VkGroupsMap(
private val groups: List<VkGroupDomain> private val groups: List<VkGroupDomain>
) { ) {
private val map: HashMap<Int, VkGroupDomain> by lazy { private val map: HashMap<Long, VkGroupDomain> by lazy {
HashMap(groups.associateBy(VkGroupDomain::id)) HashMap(groups.associateBy(VkGroupDomain::id))
} }
@@ -36,7 +36,7 @@ class VkGroupsMap(
if (message.fromId >= 0) null if (message.fromId >= 0) null
else map[abs(message.fromId)] else map[abs(message.fromId)]
fun group(groupId: Int): VkGroupDomain? = map[abs(groupId)] fun group(groupId: Long): VkGroupDomain? = map[abs(groupId)]
companion object { companion object {
@@ -1,5 +1,6 @@
package dev.meloda.fast.data package dev.meloda.fast.data
import dev.meloda.fast.data.UserConfig.userId
import dev.meloda.fast.model.api.domain.VkContactDomain import dev.meloda.fast.model.api.domain.VkContactDomain
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkGroupDomain
@@ -9,11 +10,11 @@ import kotlin.math.abs
object VkMemoryCache { object VkMemoryCache {
private val users: HashMap<Int, VkUser> = hashMapOf() private val users: HashMap<Long, VkUser> = hashMapOf()
private val groups: HashMap<Int, VkGroupDomain> = hashMapOf() private val groups: HashMap<Long, VkGroupDomain> = hashMapOf()
private val messages: HashMap<Int, VkMessage> = hashMapOf() private val messages: HashMap<Long, VkMessage> = hashMapOf()
private val conversations: HashMap<Int, VkConversation> = hashMapOf() private val conversations: HashMap<Long, VkConversation> = hashMapOf()
private val contacts: HashMap<Int, VkContactDomain> = hashMapOf() private val contacts: HashMap<Long, VkContactDomain> = hashMapOf()
fun appendUsers(users: List<VkUser>) { fun appendUsers(users: List<VkUser>) {
users.forEach { user -> VkMemoryCache.users[user.id] = user } users.forEach { user -> VkMemoryCache.users[user.id] = user }
@@ -37,83 +38,83 @@ object VkMemoryCache {
contacts.forEach { contact -> VkMemoryCache.contacts[contact.userId] = contact } contacts.forEach { contact -> VkMemoryCache.contacts[contact.userId] = contact }
} }
operator fun set(userId: Int, user: VkUser) { operator fun set(userid: Long, user: VkUser) {
users[userId] = user users[userId] = user
} }
operator fun set(groupId: Int, group: VkGroupDomain) { operator fun set(groupId: Long, group: VkGroupDomain) {
groups[groupId] = group groups[groupId] = group
} }
operator fun set(messageId: Int, message: VkMessage) { operator fun set(messageId: Long, message: VkMessage) {
messages[messageId] = message messages[messageId] = message
} }
operator fun set(conversationId: Int, conversation: VkConversation) { operator fun set(conversationId: Long, conversation: VkConversation) {
conversations[conversationId] = conversation conversations[conversationId] = conversation
} }
operator fun set(contactId: Int, contact: VkContactDomain) { operator fun set(contactId: Long, contact: VkContactDomain) {
contacts[contactId] = contact contacts[contactId] = contact
} }
fun getUser(id: Int): VkUser? { fun getUser(id: Long): VkUser? {
return getUsers(id).firstOrNull() return getUsers(id).firstOrNull()
} }
fun getUsers(vararg ids: Int): List<VkUser> { fun getUsers(vararg ids: Long): List<VkUser> {
return getUsers(ids.toList()) return getUsers(ids.toList())
} }
fun getUsers(ids: List<Int>): List<VkUser> { fun getUsers(ids: List<Long>): List<VkUser> {
return ids.mapNotNull { id -> users[id] } return ids.mapNotNull { id -> users[id] }
} }
fun getGroup(id: Int): VkGroupDomain? { fun getGroup(id: Long): VkGroupDomain? {
return getGroups(id).firstOrNull() return getGroups(id).firstOrNull()
} }
fun getGroups(vararg ids: Int): List<VkGroupDomain> { fun getGroups(vararg ids: Long): List<VkGroupDomain> {
return getGroups(ids.toList()) return getGroups(ids.toList())
} }
fun getGroups(ids: List<Int>): List<VkGroupDomain> { fun getGroups(ids: List<Long>): List<VkGroupDomain> {
return ids.mapNotNull { id -> groups[id] } return ids.mapNotNull { id -> groups[id] }
} }
fun getMessage(id: Int): VkMessage? { fun getMessage(id: Long): VkMessage? {
return getMessages(id).firstOrNull() return getMessages(id).firstOrNull()
} }
fun getMessages(vararg ids: Int): List<VkMessage> { fun getMessages(vararg ids: Long): List<VkMessage> {
return getMessages(ids.toList()) return getMessages(ids.toList())
} }
fun getMessages(ids: List<Int>): List<VkMessage> { fun getMessages(ids: List<Long>): List<VkMessage> {
return ids.mapNotNull { id -> messages[id] } return ids.mapNotNull { id -> messages[id] }
} }
fun getConversation(id: Int): VkConversation? { fun getConversation(id: Long): VkConversation? {
return getConversations(id).firstOrNull() return getConversations(id).firstOrNull()
} }
fun getConversations(vararg ids: Int): List<VkConversation> { fun getConversations(vararg ids: Long): List<VkConversation> {
return getConversations(ids.toList()) return getConversations(ids.toList())
} }
fun getConversations(ids: List<Int>): List<VkConversation> { fun getConversations(ids: List<Long>): List<VkConversation> {
return ids.mapNotNull { id -> conversations[id] } return ids.mapNotNull { id -> conversations[id] }
} }
fun getContact(id: Int): VkContactDomain? { fun getContact(id: Long): VkContactDomain? {
return getContacts(id).firstOrNull() return getContacts(id).firstOrNull()
} }
fun getContacts(vararg ids: Int): List<VkContactDomain> { fun getContacts(vararg ids: Long): List<VkContactDomain> {
return getContacts(ids.toList()) return getContacts(ids.toList())
} }
fun getContacts(ids: List<Int>): List<VkContactDomain> { fun getContacts(ids: List<Long>): List<VkContactDomain> {
return ids.mapNotNull { id -> contacts[id] } return ids.mapNotNull { id -> contacts[id] }
} }
} }
@@ -1,5 +1,6 @@
package dev.meloda.fast.data package dev.meloda.fast.data
import dev.meloda.fast.data.UserConfig.userId
import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
@@ -9,7 +10,7 @@ class VkUsersMap(
private val users: List<VkUser> private val users: List<VkUser>
) { ) {
private val map: HashMap<Int, VkUser> by lazy { private val map: HashMap<Long, VkUser> by lazy {
HashMap(users.associateBy(VkUser::id)) HashMap(users.associateBy(VkUser::id))
} }
@@ -35,7 +36,7 @@ class VkUsersMap(
if (message.fromId > 0) map[message.fromId] if (message.fromId > 0) map[message.fromId]
else null else null
fun user(userId: Int): VkUser? = map[userId] fun user(userid: Long): VkUser? = map[userId]
companion object { companion object {
@@ -0,0 +1,34 @@
package dev.meloda.fast.data
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.network.VkErrorCode
object VkUtils {
fun parseError(error: State.Error): BaseError? {
return when (error) {
is State.Error.ApiError -> {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
if (error.errorMessage.startsWith(
"User authorization failed: user is blocked."
)
) {
BaseError.AccountBlocked
} else {
BaseError.SessionExpired
}
}
else -> BaseError.SimpleError(message = error.errorMessage)
}
}
State.Error.ConnectionError -> BaseError.ConnectionError
State.Error.InternalError -> BaseError.InternalError
State.Error.UnknownError -> BaseError.UnknownError
else -> null
}
}
}
@@ -1,12 +1,32 @@
package dev.meloda.fast.data.api.auth package dev.meloda.fast.data.api.auth
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse
import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse
import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse
import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import com.slack.eithernet.ApiResult
interface AuthRepository { interface AuthRepository {
suspend fun logout(): ApiResult<Int, RestApiErrorDomain>
suspend fun validatePhone( suspend fun validatePhone(
validationSid: String validationSid: String
): ApiResult<ValidatePhoneResponse, RestApiErrorDomain> ): ApiResult<ValidatePhoneResponse, RestApiErrorDomain>
suspend fun getAnonymToken(
clientId: String,
clientSecret: String
): ApiResult<GetAnonymTokenResponse, RestApiErrorDomain>
suspend fun exchangeSilentToken(
anonymToken: String,
silentToken: String,
silentUuid: String
): ApiResult<ExchangeSilentTokenResponse, RestApiErrorDomain>
suspend fun getExchangeToken(
accessToken: String
): ApiResult<GetExchangeTokenResponse, RestApiErrorDomain>
} }
@@ -1,10 +1,17 @@
package dev.meloda.fast.data.api.auth package dev.meloda.fast.data.api.auth
import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.model.api.requests.ExchangeSilentTokenRequest
import dev.meloda.fast.model.api.requests.GetAnonymTokenRequest
import dev.meloda.fast.model.api.requests.GetExchangeTokenRequest
import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse
import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse
import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse
import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiDefault
import dev.meloda.fast.network.service.auth.AuthService import dev.meloda.fast.network.service.auth.AuthService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -12,9 +19,50 @@ class AuthRepositoryImpl(
private val service: AuthService private val service: AuthService
) : AuthRepository { ) : AuthRepository {
override suspend fun logout(): ApiResult<Int, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
service.logout(
clientId = VkConstants.MESSENGER_APP_ID.toString(),
clientSecret = VkConstants.MESSENGER_APP_SECRET
).mapApiDefault()
}
override suspend fun validatePhone( override suspend fun validatePhone(
validationSid: String validationSid: String
): ApiResult<ValidatePhoneResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<ValidatePhoneResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
service.validatePhone(validationSid).mapApiDefault() service.validatePhone(validationSid).mapApiDefault()
} }
override suspend fun getAnonymToken(
clientId: String,
clientSecret: String
): ApiResult<GetAnonymTokenResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = GetAnonymTokenRequest(
clientId = clientId,
clientSecret = clientSecret
)
service.getAnonymToken(requestModel.map).mapApiDefault()
}
override suspend fun exchangeSilentToken(
anonymToken: String,
silentToken: String,
silentUuid: String
): ApiResult<ExchangeSilentTokenResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ExchangeSilentTokenRequest(
anonymToken = anonymToken,
silentToken = silentToken,
silentUuid = silentUuid
)
service.exchangeSilentToken(requestModel.map).mapApiDefault()
}
override suspend fun getExchangeToken(
accessToken: String
): ApiResult<GetExchangeTokenResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = GetExchangeTokenRequest(accessToken = accessToken)
service.getExchangeToken(requestModel.map).mapApiDefault()
}
} }
@@ -1,22 +1,30 @@
package dev.meloda.fast.data.api.conversations package dev.meloda.fast.data.api.conversations
import com.slack.eithernet.ApiResult import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
interface ConversationsRepository { interface ConversationsRepository {
suspend fun storeConversations(conversations: List<VkConversation>)
suspend fun getConversations( suspend fun getConversations(
count: Int?, count: Int?,
offset: Int? offset: Int?,
filter: ConversationsFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain> ): ApiResult<List<VkConversation>, RestApiErrorDomain>
suspend fun getConversationsById( suspend fun getConversationsById(
peerIds: List<Int> peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): ApiResult<List<VkConversation>, RestApiErrorDomain> ): ApiResult<List<VkConversation>, RestApiErrorDomain>
suspend fun storeConversations(conversations: List<VkConversation>) suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain>
suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain> suspend fun pin(peerId: Long): ApiResult<Int, RestApiErrorDomain>
suspend fun pin(peerId: Int): ApiResult<Int, RestApiErrorDomain> suspend fun unpin(peerId: Long): ApiResult<Int, RestApiErrorDomain>
suspend fun unpin(peerId: Int): ApiResult<Int, RestApiErrorDomain> suspend fun reorderPinned(peerIds: List<Long>): ApiResult<Int, RestApiErrorDomain>
suspend fun archive(peerId: Long): ApiResult<Int, RestApiErrorDomain>
suspend fun unarchive(peerId: Long): ApiResult<Int, RestApiErrorDomain>
} }
@@ -6,37 +6,50 @@ import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.VkUsersMap import dev.meloda.fast.data.VkUsersMap
import dev.meloda.fast.database.dao.ConversationDao import dev.meloda.fast.database.dao.ConversationDao
import dev.meloda.fast.database.dao.GroupDao
import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkContactData
import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.data.asDomain import dev.meloda.fast.model.api.data.asDomain
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkGroupDomain
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.model.api.domain.asEntity import dev.meloda.fast.model.api.domain.asEntity
import dev.meloda.fast.model.api.requests.ConversationsDeleteRequest
import dev.meloda.fast.model.api.requests.ConversationsGetRequest import dev.meloda.fast.model.api.requests.ConversationsGetRequest
import dev.meloda.fast.model.api.requests.ConversationsPinRequest
import dev.meloda.fast.model.api.requests.ConversationsUnpinRequest
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiDefault
import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.conversations.ConversationsService import dev.meloda.fast.network.service.conversations.ConversationsService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ConversationsRepositoryImpl( class ConversationsRepositoryImpl(
private val conversationsService: ConversationsService, private val conversationsService: ConversationsService,
private val messageDao: MessageDao,
private val userDao: UserDao,
private val groupDao: GroupDao,
private val conversationDao: ConversationDao private val conversationDao: ConversationDao
) : ConversationsRepository { ) : ConversationsRepository {
override suspend fun storeConversations(conversations: List<VkConversation>) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
}
override suspend fun getConversations( override suspend fun getConversations(
count: Int?, count: Int?,
offset: Int? offset: Int?,
filter: ConversationsFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsGetRequest( val requestModel = ConversationsGetRequest(
count = count, count = count,
offset = offset, offset = offset,
fields = VkConstants.ALL_FIELDS, fields = VkConstants.ALL_FIELDS,
filter = "all", filter = filter,
extended = true, extended = true,
startMessageId = null startMessageId = null
) )
@@ -56,7 +69,7 @@ class ConversationsRepositoryImpl(
VkMemoryCache.appendGroups(groupsList) VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList) VkMemoryCache.appendContacts(contactsList)
response.items.map { item -> val conversations = response.items.map { item ->
val lastMessage = item.lastMessage?.asDomain()?.let { message -> val lastMessage = item.lastMessage?.asDomain()?.let { message ->
message.copy( message.copy(
user = usersMap.messageUser(message), user = usersMap.messageUser(message),
@@ -72,6 +85,17 @@ class ConversationsRepositoryImpl(
).also { VkMemoryCache[conversation.id] = it } ).also { VkMemoryCache[conversation.id] = it }
} }
} }
val messages = conversations.mapNotNull(VkConversation::lastMessage)
launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
conversations
}, },
errorMapper = { error -> errorMapper = { error ->
error?.toDomain() error?.toDomain()
@@ -80,13 +104,16 @@ class ConversationsRepositoryImpl(
} }
override suspend fun getConversationsById( override suspend fun getConversationsById(
peerIds: List<Int> peerIds: List<Long>,
extended: Boolean?,
fields: String?
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestParams = mapOf( val requestParams = mutableMapOf(
"peer_ids" to peerIds.joinToString(separator = ","), "peer_ids" to peerIds.joinToString(separator = ",")
"extended" to "1", ).apply {
"fields" to VkConstants.ALL_FIELDS extended?.let { this["extended"] = if (it) "1" else "0" }
) fields?.let { this["fields"] = it }
}
conversationsService.getConversationsById(requestParams).mapApiResult( conversationsService.getConversationsById(requestParams).mapApiResult(
successMapper = { apiResponse -> successMapper = { apiResponse ->
@@ -99,11 +126,7 @@ class ConversationsRepositoryImpl(
val usersMap = VkUsersMap.forUsers(profilesList) val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList) val groupsMap = VkGroupsMap.forGroups(groupsList)
VkMemoryCache.appendUsers(profilesList) val conversations = response.items.map { item ->
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
response.items.map { item ->
item.asDomain().let { conversation -> item.asDomain().let { conversation ->
conversation.copy( conversation.copy(
user = usersMap.conversationUser(conversation), user = usersMap.conversationUser(conversation),
@@ -111,6 +134,18 @@ class ConversationsRepositoryImpl(
).also { VkMemoryCache[conversation.id] = it } ).also { VkMemoryCache[conversation.id] = it }
} }
} }
launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
conversations
}, },
errorMapper = { error -> errorMapper = { error ->
error?.toDomain() error?.toDomain()
@@ -118,31 +153,43 @@ class ConversationsRepositoryImpl(
) )
} }
override suspend fun storeConversations(conversations: List<VkConversation>) { override suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain> =
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
}
override suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val requestModel = ConversationsDeleteRequest(peerId = peerId) conversationsService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult(
conversationsService.delete(requestModel.map).mapApiResult(
successMapper = { response -> response.requireResponse().lastDeletedId }, successMapper = { response -> response.requireResponse().lastDeletedId },
errorMapper = { error -> error?.toDomain() } errorMapper = { error -> error?.toDomain() }
) )
} }
override suspend fun pin( override suspend fun pin(
peerId: Int peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsPinRequest(peerId = peerId) conversationsService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
conversationsService.pin(requestModel.map).mapApiDefault()
} }
override suspend fun unpin( override suspend fun unpin(
peerId: Int peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsUnpinRequest(peerId = peerId) conversationsService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
conversationsService.unpin(requestModel.map).mapApiDefault() }
override suspend fun reorderPinned(
peerIds: List<Long>
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService
.reorderPinned(mapOf("peer_ids" to peerIds.joinToString(",")))
.mapApiDefault()
}
override suspend fun archive(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
}
override suspend fun unarchive(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
} }
} }
@@ -16,7 +16,7 @@ class FilesRepository(
// AUDIO_MESSAGE("audio_message") // AUDIO_MESSAGE("audio_message")
// } // }
// //
// suspend fun getMessagesUploadServer(peerId: Int, type: FileType) = // suspend fun getMessagesUploadServer(peerid: Long, type: FileType) =
// filesService.getUploadServer( // filesService.getUploadServer(
// mapOf( // mapOf(
// "peer_id" to peerId.toString(), // "peer_id" to peerId.toString(),
@@ -22,7 +22,7 @@ interface FriendsRepository {
suspend fun getOnlineFriends( suspend fun getOnlineFriends(
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<List<Int>, RestApiErrorDomain> ): ApiResult<List<Long>, RestApiErrorDomain>
suspend fun storeUsers(users: List<VkUser>) suspend fun storeUsers(users: List<VkUser>)
} }
@@ -2,7 +2,7 @@ package dev.meloda.fast.data.api.friends
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.database.dao.UsersDao import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.model.FriendsInfo import dev.meloda.fast.model.FriendsInfo
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
@@ -21,7 +21,7 @@ import kotlinx.coroutines.withContext
class FriendsRepositoryImpl( class FriendsRepositoryImpl(
private val service: FriendsService, private val service: FriendsService,
private val dao: UsersDao private val dao: UserDao
) : FriendsRepository { ) : FriendsRepository {
override suspend fun getAllFriends( override suspend fun getAllFriends(
@@ -69,7 +69,7 @@ class FriendsRepositoryImpl(
override suspend fun getOnlineFriends( override suspend fun getOnlineFriends(
count: Int?, count: Int?,
offset: Int? offset: Int?
): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<Long>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = GetOnlineFriendsRequest( val requestModel = GetOnlineFriendsRequest(
order = "hints", order = "hints",
count = count, count = count,
@@ -1,91 +1,112 @@
package dev.meloda.fast.data.api.messages package dev.meloda.fast.data.api.messages
import com.slack.eithernet.ApiResult import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.data.VkChatData
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
interface MessagesRepository { interface MessagesRepository {
suspend fun storeMessages(messages: List<VkMessage>)
suspend fun getHistory( suspend fun getHistory(
conversationId: Int, conversationId: Long,
offset: Int?, offset: Int?,
count: Int? count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> ): ApiResult<MessagesHistoryInfo, RestApiErrorDomain>
suspend fun getById( suspend fun getById(
messagesIds: List<Int>, peerCmIds: List<Long>?,
peerId: Long?,
messagesIds: List<Long>?,
cmIds: List<Long>?,
extended: Boolean?, extended: Boolean?,
fields: String? fields: String?
): ApiResult<List<VkMessage>, RestApiErrorDomain> ): ApiResult<List<VkMessage>, RestApiErrorDomain>
suspend fun send( suspend fun send(
peerId: Int, peerId: Long,
randomId: Int, randomId: Long,
message: String?, message: String?,
replyTo: Int?, replyTo: Long?,
attachments: List<VkAttachment>? attachments: List<VkAttachment>?
): ApiResult<Int, RestApiErrorDomain> ): ApiResult<MessagesSendResponse, RestApiErrorDomain>
suspend fun markAsRead( suspend fun markAsRead(
peerId: Int, peerId: Long,
startMessageId: Int? startMessageId: Long?
): ApiResult<Int, RestApiErrorDomain> ): ApiResult<Int, RestApiErrorDomain>
suspend fun getHistoryAttachments( suspend fun getHistoryAttachments(
peerId: Int, peerId: Long,
count: Int?, count: Int?,
offset: Int?, offset: Int?,
attachmentTypes: List<String>, attachmentTypes: List<String>,
conversationMessageId: Int cmId: Long
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> ): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain>
suspend fun createChat( suspend fun createChat(
userIds: List<Int>?, userIds: List<Long>?,
title: String? title: String?
): ApiResult<Int, RestApiErrorDomain> ): ApiResult<Long, RestApiErrorDomain>
suspend fun pin( suspend fun pin(
peerId: Int, peerId: Long,
messageId: Int?, messageId: Long? = null,
conversationMessageId: Int? cmId: Long? = null
): ApiResult<VkMessage, RestApiErrorDomain> ): ApiResult<VkMessage, RestApiErrorDomain>
suspend fun unpin( suspend fun unpin(
peerId: Int peerId: Long
): ApiResult<Int, RestApiErrorDomain> ): ApiResult<Int, RestApiErrorDomain>
suspend fun markAsImportant( suspend fun markAsImportant(
peerId: Int, peerId: Long,
messageIds: List<Int>?, messageIds: List<Long>? = null,
conversationMessageIds: List<Int>?, cmIds: List<Long>? = null,
important: Boolean important: Boolean
): ApiResult<List<Int>, RestApiErrorDomain> ): ApiResult<List<Long>, RestApiErrorDomain>
suspend fun delete( suspend fun delete(
peerId: Int, peerId: Long,
messageIds: List<Int>?, messageIds: List<Long>?,
conversationMessageIds: List<Int>?, cmIds: List<Long>?,
spam: Boolean, spam: Boolean,
deleteForAll: Boolean deleteForAll: Boolean
): ApiResult<List<Any>, RestApiErrorDomain> ): ApiResult<List<Any>, RestApiErrorDomain>
suspend fun storeMessages(messages: List<VkMessage>) suspend fun edit(
// peerId: Long,
// suspend fun edit( messageId: Long? = null,
// params: MessagesEditRequest cmId: Long? = null,
// ): ApiResult<Int, RestApiErrorDomain> message: String? = null,
// lat: Float? = null,
// suspend fun getChat( long: Float? = null,
// params: MessagesGetChatRequest attachments: List<VkAttachment>? = null,
// ): ApiResult<VkChatData, RestApiErrorDomain> notParseLinks: Boolean = false,
// keepSnippets: Boolean = true,
// suspend fun getConversationMembers( keepForwardedMessages: Boolean = true
// params: MessagesGetConversationMembersRequest ): ApiResult<Int, RestApiErrorDomain>
// ): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain>
// suspend fun getChat(
// suspend fun removeChatUser( chatId: Long,
// params: MessagesRemoveChatUserRequest fields: String? = null
// ): ApiResult<Int, RestApiErrorDomain> ): ApiResult<VkChatData, RestApiErrorDomain>
suspend fun getConversationMembers(
peerId: Long,
offset: Int? = null,
count: Int? = null,
extended: Boolean? = null,
fields: String? = null
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain>
suspend fun removeChatUser(
chatId: Long,
memberId: Long
): ApiResult<Int, RestApiErrorDomain>
} }
@@ -5,40 +5,57 @@ import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.VkUsersMap import dev.meloda.fast.data.VkUsersMap
import dev.meloda.fast.database.dao.ConversationDao
import dev.meloda.fast.database.dao.GroupDao
import dev.meloda.fast.database.dao.MessageDao import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData
import dev.meloda.fast.model.api.data.VkChatData
import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkContactData
import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.data.asDomain import dev.meloda.fast.model.api.data.asDomain
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkGroupDomain
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.model.api.domain.asEntity import dev.meloda.fast.model.api.domain.asEntity
import dev.meloda.fast.model.api.requests.MessagesCreateChatRequest import dev.meloda.fast.model.api.requests.MessagesCreateChatRequest
import dev.meloda.fast.model.api.requests.MessagesDeleteRequest import dev.meloda.fast.model.api.requests.MessagesDeleteRequest
import dev.meloda.fast.model.api.requests.MessagesEditRequest
import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest
import dev.meloda.fast.model.api.requests.MessagesGetChatRequest
import dev.meloda.fast.model.api.requests.MessagesGetConversationMembersRequest
import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest
import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest
import dev.meloda.fast.model.api.requests.MessagesMarkAsImportantRequest import dev.meloda.fast.model.api.requests.MessagesMarkAsImportantRequest
import dev.meloda.fast.model.api.requests.MessagesMarkAsReadRequest import dev.meloda.fast.model.api.requests.MessagesMarkAsReadRequest
import dev.meloda.fast.model.api.requests.MessagesPinMessageRequest import dev.meloda.fast.model.api.requests.MessagesPinMessageRequest
import dev.meloda.fast.model.api.requests.MessagesRemoveChatUserRequest
import dev.meloda.fast.model.api.requests.MessagesSendRequest import dev.meloda.fast.model.api.requests.MessagesSendRequest
import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiDefault
import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.messages.MessagesService import dev.meloda.fast.network.service.messages.MessagesService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class MessagesRepositoryImpl( class MessagesRepositoryImpl(
private val messagesService: MessagesService, private val messagesService: MessagesService,
private val messageDao: MessageDao, private val messageDao: MessageDao,
private val userDao: UserDao,
private val groupDao: GroupDao,
private val conversationDao: ConversationDao
) : MessagesRepository { ) : MessagesRepository {
override suspend fun getHistory( override suspend fun getHistory(
conversationId: Int, conversationId: Long,
offset: Int?, offset: Int?,
count: Int? count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
@@ -89,6 +106,13 @@ class MessagesRepositoryImpl(
} }
} }
launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
MessagesHistoryInfo( MessagesHistoryInfo(
messages = messages, messages = messages,
conversations = conversations conversations = conversations
@@ -101,12 +125,18 @@ class MessagesRepositoryImpl(
} }
override suspend fun getById( override suspend fun getById(
messagesIds: List<Int>, peerCmIds: List<Long>?,
peerId: Long?,
messagesIds: List<Long>?,
cmIds: List<Long>?,
extended: Boolean?, extended: Boolean?,
fields: String? fields: String?
): ApiResult<List<VkMessage>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkMessage>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetByIdRequest( val requestModel = MessagesGetByIdRequest(
peerCmIds = peerCmIds,
peerId = peerId,
messagesIds = messagesIds, messagesIds = messagesIds,
cmIds = cmIds,
extended = extended, extended = extended,
fields = fields fields = fields
) )
@@ -116,12 +146,15 @@ class MessagesRepositoryImpl(
val response = apiResponse.requireResponse() val response = apiResponse.requireResponse()
val messages = response.items val messages = response.items
val usersMap =
VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain))
val groupsMap =
VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain))
messages.map { message -> val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
val domainMessages = messages.map { message ->
message.asDomain().copy( message.asDomain().copy(
user = usersMap.messageUser(message), user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message), group = groupsMap.messageGroup(message),
@@ -129,18 +162,30 @@ class MessagesRepositoryImpl(
actionGroup = groupsMap.messageActionGroup(message) actionGroup = groupsMap.messageActionGroup(message)
) )
} }
launch(Dispatchers.IO) {
messageDao.insertAll(domainMessages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
domainMessages
}, },
errorMapper = { error -> error?.toDomain() } errorMapper = { error -> error?.toDomain() }
) )
} }
override suspend fun send( override suspend fun send(
peerId: Int, peerId: Long,
randomId: Int, randomId: Long,
message: String?, message: String?,
replyTo: Int?, replyTo: Long?,
attachments: List<VkAttachment>? attachments: List<VkAttachment>?
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesSendRequest( val requestModel = MessagesSendRequest(
peerId = peerId, peerId = peerId,
randomId = randomId, randomId = randomId,
@@ -153,8 +198,8 @@ class MessagesRepositoryImpl(
} }
override suspend fun markAsRead( override suspend fun markAsRead(
peerId: Int, peerId: Long,
startMessageId: Int? startMessageId: Long?
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesMarkAsReadRequest( val requestModel = MessagesMarkAsReadRequest(
peerId = peerId, peerId = peerId,
@@ -165,11 +210,11 @@ class MessagesRepositoryImpl(
} }
override suspend fun getHistoryAttachments( override suspend fun getHistoryAttachments(
peerId: Int, peerId: Long,
count: Int?, count: Int?,
offset: Int?, offset: Int?,
attachmentTypes: List<String>, attachmentTypes: List<String>,
conversationMessageId: Int cmId: Long
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> = ): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryAttachmentsRequest( val requestModel = MessagesGetHistoryAttachmentsRequest(
@@ -179,7 +224,7 @@ class MessagesRepositoryImpl(
offset = offset, offset = offset,
preserveOrder = true, preserveOrder = true,
attachmentTypes = attachmentTypes, attachmentTypes = attachmentTypes,
conversationMessageId = conversationMessageId, conversationMessageId = cmId,
fields = VkConstants.ALL_FIELDS fields = VkConstants.ALL_FIELDS
) )
@@ -195,6 +240,11 @@ class MessagesRepositoryImpl(
VkMemoryCache.appendGroups(groupsList) VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList) VkMemoryCache.appendContacts(contactsList)
launch(Dispatchers.IO) {
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
response.items.map(VkAttachmentHistoryMessageData::toDomain) response.items.map(VkAttachmentHistoryMessageData::toDomain)
}, },
errorMapper = { error -> errorMapper = { error ->
@@ -204,9 +254,9 @@ class MessagesRepositoryImpl(
} }
override suspend fun createChat( override suspend fun createChat(
userIds: List<Int>?, userIds: List<Long>?,
title: String? title: String?
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Long, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesCreateChatRequest( val requestModel = MessagesCreateChatRequest(
userIds = userIds, userIds = userIds,
title = title title = title
@@ -221,14 +271,14 @@ class MessagesRepositoryImpl(
} }
override suspend fun pin( override suspend fun pin(
peerId: Int, peerId: Long,
messageId: Int?, messageId: Long?,
conversationMessageId: Int? cmId: Long?
): ApiResult<VkMessage, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<VkMessage, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesPinMessageRequest( val requestModel = MessagesPinMessageRequest(
peerId = peerId, peerId = peerId,
messageId = messageId, messageId = messageId,
conversationMessageId = conversationMessageId conversationMessageId = cmId
) )
messagesService.pin(requestModel.map).mapApiResult( messagesService.pin(requestModel.map).mapApiResult(
@@ -240,18 +290,18 @@ class MessagesRepositoryImpl(
} }
override suspend fun unpin( override suspend fun unpin(
peerId: Int peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesUnpinMessageRequest(peerId = peerId) val requestModel = MessagesUnpinMessageRequest(peerId = peerId)
messagesService.unpin(requestModel.map).mapApiDefault() messagesService.unpin(requestModel.map).mapApiDefault()
} }
override suspend fun markAsImportant( override suspend fun markAsImportant(
peerId: Int, peerId: Long,
messageIds: List<Int>?, messageIds: List<Long>?,
conversationMessageIds: List<Int>?, cmIds: List<Long>?,
important: Boolean important: Boolean
): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<Long>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesMarkAsImportantRequest( val requestModel = MessagesMarkAsImportantRequest(
messagesIds = messageIds.orEmpty(), messagesIds = messageIds.orEmpty(),
important = important important = important
@@ -260,16 +310,16 @@ class MessagesRepositoryImpl(
} }
override suspend fun delete( override suspend fun delete(
peerId: Int, peerId: Long,
messageIds: List<Int>?, messageIds: List<Long>?,
conversationMessageIds: List<Int>?, cmIds: List<Long>?,
spam: Boolean, spam: Boolean,
deleteForAll: Boolean deleteForAll: Boolean
): ApiResult<List<Any>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<Any>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesDeleteRequest( val requestModel = MessagesDeleteRequest(
peerId = peerId, peerId = peerId,
messagesIds = messageIds, messagesIds = messageIds,
conversationsMessagesIds = conversationMessageIds, conversationsMessagesIds = cmIds,
isSpam = spam, isSpam = spam,
deleteForAll = deleteForAll deleteForAll = deleteForAll
) )
@@ -280,58 +330,74 @@ class MessagesRepositoryImpl(
messageDao.insertAll(messages.map(VkMessage::asEntity)) messageDao.insertAll(messages.map(VkMessage::asEntity))
} }
// override suspend fun markAsImportant( override suspend fun edit(
// params: MessagesMarkAsImportantRequest peerId: Long,
// ): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) { messageId: Long?,
// messagesService.markAsImportant(params.map).mapResult( cmId: Long?,
// successMapper = { response -> response.requireResponse() }, message: String?,
// errorMapper = { error -> error?.toDomain() } lat: Float?,
// ) long: Float?,
// } attachments: List<VkAttachment>?,
// notParseLinks: Boolean,
// override suspend fun delete( keepSnippets: Boolean,
// params: MessagesDeleteRequest keepForwardedMessages: Boolean
// ): ApiResult<Unit, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.delete(params.map).mapResult( val requestModel = MessagesEditRequest(
// successMapper = {}, peerId = peerId,
// errorMapper = { error -> error?.toDomain() } messageId = messageId,
// ) cmId = cmId,
// } message = message,
// lat = lat,
// override suspend fun edit( long = long,
// params: MessagesEditRequest attachments = attachments,
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { notParseLinks = notParseLinks,
// messagesService.edit(params.map).mapResult( keepSnippets = keepSnippets,
// successMapper = { response -> response.requireResponse() }, keepForwardedMessages = keepForwardedMessages
// errorMapper = { error -> error?.toDomain() } )
// )
// } messagesService.edit(requestModel.map).mapApiDefault()
// }
// override suspend fun getChat(
// params: MessagesGetChatRequest override suspend fun getChat(
// ): ApiResult<VkChatData, RestApiErrorDomain> = withContext(Dispatchers.IO) { chatId: Long,
// messagesService.getChat(params.map).mapResult( fields: String?
// successMapper = { response -> response.requireResponse() }, ): ApiResult<VkChatData, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// errorMapper = { error -> error?.toDomain() } val requestModel = MessagesGetChatRequest(
// ) chatId = chatId,
// } fields = fields
// )
// override suspend fun getConversationMembers(
// params: MessagesGetConversationMembersRequest messagesService.getChat(requestModel.map).mapApiDefault()
// ): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> = }
// withContext(Dispatchers.IO) {
// messagesService.getConversationMembers(params.map).mapResult( override suspend fun getConversationMembers(
// successMapper = { response -> response.requireResponse() }, peerId: Long,
// errorMapper = { error -> error?.toDomain() } offset: Int?,
// ) count: Int?,
// } extended: Boolean?,
// fields: String?
// override suspend fun removeChatUser( ): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> =
// params: MessagesRemoveChatUserRequest withContext(Dispatchers.IO) {
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { val requestModel = MessagesGetConversationMembersRequest(
// messagesService.removeChatUser(params.map).mapResult( peerId = peerId,
// successMapper = { response -> response.requireResponse() }, offset = offset,
// errorMapper = { error -> error?.toDomain() } count = count,
// ) extended = extended,
// } fields = fields
)
messagesService.getConversationMembers(requestModel.map).mapApiDefault()
}
override suspend fun removeChatUser(
chatId: Long,
memberId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesRemoveChatUserRequest(
chatId = chatId,
memberId = memberId
)
messagesService.removeChatUser(requestModel.map).mapApiDefault()
}
} }
@@ -1,6 +1,9 @@
package dev.meloda.fast.data.api.oauth package dev.meloda.fast.data.api.oauth
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.responses.AuthDirectResponse import dev.meloda.fast.model.api.responses.AuthDirectResponse
import dev.meloda.fast.model.api.responses.GetSilentTokenResponse
import dev.meloda.fast.network.OAuthErrorDomain
interface OAuthRepository { interface OAuthRepository {
@@ -11,5 +14,14 @@ interface OAuthRepository {
validationCode: String?, validationCode: String?,
captchaSid: String?, captchaSid: String?,
captchaKey: String? captchaKey: String?
): AuthDirectResponse ): ApiResult<AuthDirectResponse, OAuthErrorDomain>
suspend fun getSilentToken(
login: String,
password: String,
forceSms: Boolean,
validationCode: String?,
captchaSid: String?,
captchaKey: String?,
): ApiResult<GetSilentTokenResponse, OAuthErrorDomain>
} }
@@ -1,10 +1,16 @@
package dev.meloda.fast.data.api.oauth package dev.meloda.fast.data.api.oauth
import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.model.api.requests.AuthDirectRequest import dev.meloda.fast.model.api.requests.AuthDirectRequest
import dev.meloda.fast.model.api.responses.AuthDirectResponse import dev.meloda.fast.model.api.responses.AuthDirectResponse
import dev.meloda.fast.model.api.responses.GetSilentTokenResponse
import dev.meloda.fast.network.OAuthErrorDomain
import dev.meloda.fast.network.ValidationType
import dev.meloda.fast.network.VkOAuthError
import dev.meloda.fast.network.VkOAuthErrorType
import dev.meloda.fast.network.mapResult
import dev.meloda.fast.network.service.oauth.OAuthService import dev.meloda.fast.network.service.oauth.OAuthService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -18,37 +24,190 @@ class OAuthRepositoryImpl(
forceSms: Boolean, forceSms: Boolean,
validationCode: String?, validationCode: String?,
captchaSid: String?, captchaSid: String?,
captchaKey: String? captchaKey: String?,
): AuthDirectResponse = withContext(Dispatchers.IO) { ): ApiResult<AuthDirectResponse, OAuthErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = AuthDirectRequest( val requestModel = AuthDirectRequest(
grantType = VkConstants.Auth.GrantType.PASSWORD, grantType = VkConstants.Auth.GrantType.PASSWORD,
clientId = VkConstants.VK_APP_ID, clientId = VkConstants.MESSENGER_APP_ID.toString(),
clientSecret = VkConstants.VK_SECRET, clientSecret = VkConstants.MESSENGER_APP_SECRET,
username = login, username = login,
password = password, password = password,
scope = VkConstants.Auth.SCOPE, scope = VkConstants.MESSENGER_APP_SCOPE.toString(),
validationForceSms = forceSms, validationForceSms = forceSms,
validationCode = validationCode, validationCode = validationCode,
captchaSid = captchaSid, captchaSid = captchaSid,
captchaKey = captchaKey, captchaKey = captchaKey,
) )
when (val result = oAuthService.auth(requestModel.map)) { oAuthService.auth(requestModel.map).mapResult(
is ApiResult.Success -> result.value successMapper = {
it
},
errorMapper = { response ->
val error = response?.error?.let(VkOAuthError::parse)
val errorType = response?.errorType?.let(VkOAuthErrorType::parse)
is ApiResult.Failure.HttpFailure -> { when (error) {
requireNotNull(result.error) null -> OAuthErrorDomain.UnknownError
VkOAuthError.FLOOD_CONTROL -> OAuthErrorDomain.TooManyTriesError
VkOAuthError.NEED_VALIDATION -> {
if (response.banInfo != null) {
val info = requireNotNull(response.banInfo)
OAuthErrorDomain.UserBannedError(
memberName = info.memberName,
message = info.message,
accessToken = info.accessToken,
restoreUrl = info.restoreUrl
)
} else {
OAuthErrorDomain.ValidationRequiredError(
description = response.errorDescription.orEmpty(),
validationType = response.validationType.orEmpty()
.let(ValidationType::parse),
validationSid = response.validationSid.orEmpty(),
phoneMask = response.phoneMask.orEmpty(),
redirectUri = response.redirectUri.orEmpty(),
validationResend = response.validationResend,
restoreIfCannotGetCode = response.restoreIfCannotGetCode
)
}
}
VkOAuthError.NEED_CAPTCHA -> {
OAuthErrorDomain.CaptchaRequiredError(
captchaSid = response.captchaSid.orEmpty(),
captchaImageUrl = response.captchaImage.orEmpty()
)
}
VkOAuthError.INVALID_CLIENT -> {
OAuthErrorDomain.InvalidCredentialsError
}
VkOAuthError.INVALID_REQUEST -> {
when (errorType) {
null -> OAuthErrorDomain.UnknownError
VkOAuthErrorType.WRONG_OTP -> {
OAuthErrorDomain.WrongValidationCode
}
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
OAuthErrorDomain.WrongValidationCodeFormat
}
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
OAuthErrorDomain.TooManyTriesError
}
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
OAuthErrorDomain.InvalidCredentialsError
}
}
}
VkOAuthError.UNKNOWN -> OAuthErrorDomain.UnknownError
}
} }
)
is ApiResult.Failure.ApiFailure -> TODO()
is ApiResult.Failure.NetworkFailure -> {
// TODO: 13/07/2024, Danil Nikolaev: implement showing network error
TODO()
}
is ApiResult.Failure.UnknownFailure -> TODO()
else -> throw IllegalStateException("Unknown result")
}
} }
override suspend fun getSilentToken(
login: String,
password: String,
forceSms: Boolean,
validationCode: String?,
captchaSid: String?,
captchaKey: String?,
): ApiResult<GetSilentTokenResponse, OAuthErrorDomain> =
withContext(Dispatchers.IO) {
val requestModel = AuthDirectRequest(
grantType = VkConstants.Auth.GrantType.PASSWORD,
clientId = VkConstants.MESSENGER_APP_ID.toString(),
clientSecret = VkConstants.MESSENGER_APP_SECRET,
username = login,
password = password,
scope = VkConstants.MESSENGER_APP_SCOPE.toString(),
validationForceSms = forceSms,
validationCode = validationCode,
captchaSid = captchaSid,
captchaKey = captchaKey,
)
oAuthService.getSilentToken(requestModel.map).mapResult(
successMapper = { it },
errorMapper = { response ->
val error = response?.error?.let(VkOAuthError::parse)
val errorType = response?.errorType?.let(VkOAuthErrorType::parse)
when (error) {
null -> OAuthErrorDomain.UnknownError
VkOAuthError.FLOOD_CONTROL -> OAuthErrorDomain.TooManyTriesError
VkOAuthError.NEED_VALIDATION -> {
if (response.banInfo != null) {
val info = requireNotNull(response.banInfo)
OAuthErrorDomain.UserBannedError(
memberName = info.memberName,
message = info.message,
accessToken = info.accessToken,
restoreUrl = info.restoreUrl
)
} else {
OAuthErrorDomain.ValidationRequiredError(
description = response.errorDescription.orEmpty(),
validationType = response.validationType.orEmpty()
.let(ValidationType::parse),
validationSid = response.validationSid.orEmpty(),
phoneMask = response.phoneMask.orEmpty(),
redirectUri = response.redirectUri.orEmpty(),
validationResend = response.validationResend,
restoreIfCannotGetCode = response.restoreIfCannotGetCode
)
}
}
VkOAuthError.NEED_CAPTCHA -> {
OAuthErrorDomain.CaptchaRequiredError(
captchaSid = response.captchaSid.orEmpty(),
captchaImageUrl = response.captchaImage.orEmpty()
)
}
VkOAuthError.INVALID_CLIENT -> {
OAuthErrorDomain.InvalidCredentialsError
}
VkOAuthError.INVALID_REQUEST -> {
when (errorType) {
null -> OAuthErrorDomain.UnknownError
VkOAuthErrorType.WRONG_OTP -> {
OAuthErrorDomain.WrongValidationCode
}
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
OAuthErrorDomain.WrongValidationCodeFormat
}
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
OAuthErrorDomain.TooManyTriesError
}
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
OAuthErrorDomain.InvalidCredentialsError
}
}
}
VkOAuthError.UNKNOWN -> OAuthErrorDomain.UnknownError
}
}
)
}
} }
@@ -8,7 +8,7 @@ class PhotosRepository(
private val photosService: PhotosService private val photosService: PhotosService
) { ) {
suspend fun getMessagesUploadServer(peerId: Int) = suspend fun getMessagesUploadServer(peerId: Long) =
photosService.getUploadServer(mapOf("peer_id" to peerId.toString())) photosService.getUploadServer(mapOf("peer_id" to peerId.toString()))
suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) = suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) =
@@ -1,18 +1,18 @@
package dev.meloda.fast.data.api.users package dev.meloda.fast.data.api.users
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import com.slack.eithernet.ApiResult
interface UsersRepository { interface UsersRepository {
suspend fun get( suspend fun get(
userIds: List<Int>?, userIds: List<Long>?,
fields: String?, fields: String?,
nomCase: String? nomCase: String?
): ApiResult<List<VkUser>, RestApiErrorDomain> ): ApiResult<List<VkUser>, RestApiErrorDomain>
suspend fun getLocalUsers(userIds: List<Int>): List<VkUser> suspend fun getLocalUsers(userIds: List<Long>): List<VkUser>
suspend fun storeUsers(users: List<VkUser>) suspend fun storeUsers(users: List<VkUser>)
} }
@@ -1,7 +1,8 @@
package dev.meloda.fast.data.api.users package dev.meloda.fast.data.api.users
import com.slack.eithernet.ApiResult
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.database.dao.UsersDao import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.model.api.domain.asEntity import dev.meloda.fast.model.api.domain.asEntity
@@ -11,18 +12,17 @@ import dev.meloda.fast.model.database.asExternalModel
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.users.UsersService import dev.meloda.fast.network.service.users.UsersService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class UsersRepositoryImpl( class UsersRepositoryImpl(
private val service: UsersService, private val service: UsersService,
private val dao: UsersDao private val dao: UserDao
) : UsersRepository { ) : UsersRepository {
override suspend fun get( override suspend fun get(
userIds: List<Int>?, userIds: List<Long>?,
fields: String?, fields: String?,
nomCase: String? nomCase: String?
): ApiResult<List<VkUser>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkUser>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
@@ -38,7 +38,9 @@ class UsersRepositoryImpl(
val users = response.map(VkUserData::mapToDomain) val users = response.map(VkUserData::mapToDomain)
launch { storeUsers(users) } launch(Dispatchers.IO) {
storeUsers(users)
}
VkMemoryCache.appendUsers(users) VkMemoryCache.appendUsers(users)
@@ -51,7 +53,7 @@ class UsersRepositoryImpl(
} }
override suspend fun getLocalUsers( override suspend fun getLocalUsers(
userIds: List<Int> userIds: List<Long>
): List<VkUser> = withContext(Dispatchers.IO) { ): List<VkUser> = withContext(Dispatchers.IO) {
dao.getAllByIds(userIds).map(VkUserEntity::asExternalModel) dao.getAllByIds(userIds).map(VkUserEntity::asExternalModel)
} }
@@ -6,7 +6,7 @@ interface AccountsRepository {
suspend fun getAccounts(): List<AccountEntity> suspend fun getAccounts(): List<AccountEntity>
suspend fun getAccountById(userId: Int): AccountEntity? suspend fun getAccountById(userId: Long): AccountEntity?
suspend fun storeAccounts(accounts: List<AccountEntity>) suspend fun storeAccounts(accounts: List<AccountEntity>)
} }
@@ -9,7 +9,7 @@ class AccountsRepositoryImpl(
override suspend fun getAccounts(): List<AccountEntity> = accountDao.getAll() override suspend fun getAccounts(): List<AccountEntity> = accountDao.getAll()
override suspend fun getAccountById(userId: Int): AccountEntity? = override suspend fun getAccountById(userId: Long): AccountEntity? =
accountDao.getById(userId) accountDao.getById(userId)
override suspend fun storeAccounts( override suspend fun storeAccounts(
@@ -65,7 +65,6 @@ val dataModule = module {
singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class
// TODO: 11/08/2024, Danil Nikolaev: find a better solution
single<Interceptor>(named("token_interceptor")) { single<Interceptor>(named("token_interceptor")) {
AccessTokenInterceptor() AccessTokenInterceptor()
} }
@@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 2, "version": 2,
"identityHash": "3ebd234270e36902d3d461af38664869", "identityHash": "ca007bca2ab4a9b901662792042770ad",
"entities": [ "entities": [
{ {
"tableName": "accounts", "tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, PRIMARY KEY(`userId`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, `exchangeToken` TEXT, PRIMARY KEY(`userId`))",
"fields": [ "fields": [
{ {
"fieldPath": "userId", "fieldPath": "userId",
@@ -31,6 +31,12 @@
"columnName": "trustedHash", "columnName": "trustedHash",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
},
{
"fieldPath": "exchangeToken",
"columnName": "exchangeToken",
"affinity": "TEXT",
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@@ -46,7 +52,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3ebd234270e36902d3d461af38664869')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ca007bca2ab4a9b901662792042770ad')"
] ]
} }
} }
@@ -7,7 +7,7 @@ import dev.meloda.fast.model.database.AccountEntity
@Database( @Database(
entities = [AccountEntity::class], entities = [AccountEntity::class],
version = 2 version = 3
) )
abstract class AccountsDatabase : RoomDatabase() { abstract class AccountsDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao abstract fun accountDao(): AccountDao
@@ -6,7 +6,7 @@ import androidx.room.TypeConverters
import dev.meloda.fast.database.dao.ConversationDao import dev.meloda.fast.database.dao.ConversationDao
import dev.meloda.fast.database.dao.GroupDao import dev.meloda.fast.database.dao.GroupDao
import dev.meloda.fast.database.dao.MessageDao import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.database.dao.UsersDao import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.database.typeconverters.Converters import dev.meloda.fast.database.typeconverters.Converters
import dev.meloda.fast.model.database.VkConversationEntity import dev.meloda.fast.model.database.VkConversationEntity
import dev.meloda.fast.model.database.VkGroupEntity import dev.meloda.fast.model.database.VkGroupEntity
@@ -21,11 +21,11 @@ import dev.meloda.fast.model.database.VkUserEntity
VkConversationEntity::class VkConversationEntity::class
], ],
version = 8 version = 10
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class CacheDatabase : RoomDatabase() { abstract class CacheDatabase : RoomDatabase() {
abstract fun userDao(): UsersDao abstract fun userDao(): UserDao
abstract fun groupDao(): GroupDao abstract fun groupDao(): GroupDao
abstract fun messageDao(): MessageDao abstract fun messageDao(): MessageDao
abstract fun conversationDao(): ConversationDao abstract fun conversationDao(): ConversationDao
@@ -11,8 +11,8 @@ abstract class AccountDao : EntityDao<AccountEntity> {
abstract suspend fun getAll(): List<AccountEntity> abstract suspend fun getAll(): List<AccountEntity>
@Query("SELECT * FROM accounts WHERE userId = :userId") @Query("SELECT * FROM accounts WHERE userId = :userId")
abstract suspend fun getById(userId: Int): AccountEntity? abstract suspend fun getById(userId: Long): AccountEntity?
@Query("DELETE FROM accounts WHERE userId = :userId") @Query("DELETE FROM accounts WHERE userId = :userId")
abstract suspend fun deleteById(userId: Int) abstract suspend fun deleteById(userId: Long)
} }
@@ -16,11 +16,11 @@ abstract class ConversationDao : EntityDao<VkConversationEntity> {
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConversationEntity> abstract suspend fun getAllByIds(ids: List<Int>): List<VkConversationEntity>
@Query("SELECT * FROM conversations WHERE id IS (:id)") @Query("SELECT * FROM conversations WHERE id IS (:id)")
abstract suspend fun getById(id: Int): VkConversationEntity? abstract suspend fun getById(id: Long): VkConversationEntity?
@Transaction @Transaction
@Query("SELECT * FROM conversations WHERE id IS (:id)") @Query("SELECT * FROM conversations WHERE id IS (:id)")
abstract suspend fun getByIdWithMessage(id: Int): ConversationWithMessage? abstract suspend fun getByIdWithMessage(id: Long): ConversationWithMessage?
@Query("DELETE FROM conversations WHERE rowid IN (:ids)") @Query("DELETE FROM conversations WHERE rowid IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int abstract suspend fun deleteByIds(ids: List<Int>): Int
@@ -11,13 +11,13 @@ abstract class MessageDao : EntityDao<VkMessageEntity> {
abstract suspend fun getAll(): List<VkMessageEntity> abstract suspend fun getAll(): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE peerId IS (:conversationId)") @Query("SELECT * FROM messages WHERE peerId IS (:conversationId)")
abstract suspend fun getAll(conversationId: Int): List<VkMessageEntity> abstract suspend fun getAll(conversationId: Long): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE id IN (:ids)") @Query("SELECT * FROM messages WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity> abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE id IS (:messageId)") @Query("SELECT * FROM messages WHERE id IS (:messageId)")
abstract suspend fun getById(messageId: Int): VkMessageEntity? abstract suspend fun getById(messageId: Long): VkMessageEntity?
@Query("DELETE FROM messages WHERE id IN (:ids)") @Query("DELETE FROM messages WHERE id IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int abstract suspend fun deleteByIds(ids: List<Int>): Int
@@ -5,14 +5,14 @@ import androidx.room.Query
import dev.meloda.fast.model.database.VkUserEntity import dev.meloda.fast.model.database.VkUserEntity
@Dao @Dao
abstract class UsersDao : EntityDao<VkUserEntity> { abstract class UserDao : EntityDao<VkUserEntity> {
@Query("SELECT * FROM users") @Query("SELECT * FROM users")
abstract suspend fun getAll(): List<VkUserEntity> abstract suspend fun getAll(): List<VkUserEntity>
@Query("SELECT * FROM users WHERE id IN (:ids)") @Query("SELECT * FROM users WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkUserEntity> abstract suspend fun getAllByIds(ids: List<Long>): List<VkUserEntity>
@Query("DELETE FROM users WHERE id IN (:ids)") @Query("DELETE FROM users WHERE id IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int abstract suspend fun deleteByIds(ids: List<Long>): Int
} }
@@ -3,12 +3,15 @@ package dev.meloda.fast.database.di
import androidx.room.Room import androidx.room.Room
import dev.meloda.fast.database.AccountsDatabase import dev.meloda.fast.database.AccountsDatabase
import dev.meloda.fast.database.CacheDatabase import dev.meloda.fast.database.CacheDatabase
import dev.meloda.fast.database.di.migration.migrationFrom2To3
import org.koin.core.scope.Scope import org.koin.core.scope.Scope
import org.koin.dsl.module import org.koin.dsl.module
val databaseModule = module { val databaseModule = module {
single { single {
Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts").build() Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts")
.addMigrations(migrationFrom2To3)
.build()
} }
single { get<AccountsDatabase>().accountDao() } single { get<AccountsDatabase>().accountDao() }
@@ -0,0 +1,14 @@
package dev.meloda.fast.database.di.migration
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val migrationFrom2To3 = object : Migration(
startVersion = 2,
endVersion = 3
) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE accounts ADD COLUMN exchangeToken TEXT DEFAULT null")
}
}
@@ -13,6 +13,15 @@ class Converters {
.split(", ") .split(", ")
.mapNotNull(String::toIntOrNull) .mapNotNull(String::toIntOrNull)
@TypeConverter
fun longListToString(list: List<Long>): String = list.joinToString()
@TypeConverter
fun stringToLongList(string: String): List<Long> =
string
.split(", ")
.mapNotNull(String::toLongOrNull)
@TypeConverter @TypeConverter
fun stringListToString(list: List<String>): String = list.joinToString() fun stringListToString(list: List<String>): String = list.joinToString()
@@ -45,7 +45,7 @@ object SettingsKeys {
const val KEY_ENABLE_HAPTIC = "enable_haptic" const val KEY_ENABLE_HAPTIC = "enable_haptic"
const val DEFAULT_ENABLE_HAPTIC = true const val DEFAULT_ENABLE_HAPTIC = true
const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level" const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level"
const val DEFAULT_NETWORK_LOG_LEVEL = 0 const val DEFAULT_NETWORK_LOG_LEVEL = 3
const val KEY_USE_SYSTEM_FONT = "use_system_font" const val KEY_USE_SYSTEM_FONT = "use_system_font"
const val DEFAULT_USE_SYSTEM_FONT = false const val DEFAULT_USE_SYSTEM_FONT = false
const val KEY_MORE_ANIMATIONS = "more_animations" const val KEY_MORE_ANIMATIONS = "more_animations"
@@ -53,5 +53,5 @@ object SettingsKeys {
const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category"
const val ID_DMITRY = 37610580 const val ID_DMITRY = 37610580L
} }
@@ -1,12 +1,32 @@
package dev.meloda.fast.domain package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse
import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse
import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse
import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AuthUseCase { interface AuthUseCase : BaseUseCase {
fun logout(): Flow<State<Int>>
fun validatePhone( fun validatePhone(
validationSid: String validationSid: String
): Flow<State<ValidatePhoneResponse>> ): Flow<State<ValidatePhoneResponse>>
suspend fun getAnonymToken(
clientId: String,
clientSecret: String
): Flow<State<GetAnonymTokenResponse>>
suspend fun exchangeSilentToken(
anonymToken: String,
silentToken: String,
silentUuid: String
): Flow<State<ExchangeSilentTokenResponse>>
suspend fun getExchangeToken(
accessToken: String
): Flow<State<GetExchangeTokenResponse>>
} }
@@ -3,16 +3,44 @@ package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.auth.AuthRepository import dev.meloda.fast.data.api.auth.AuthRepository
import dev.meloda.fast.data.mapToState import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.api.responses.ExchangeSilentTokenResponse
import dev.meloda.fast.model.api.responses.GetAnonymTokenResponse
import dev.meloda.fast.model.api.responses.GetExchangeTokenResponse
import dev.meloda.fast.model.api.responses.ValidatePhoneResponse import dev.meloda.fast.model.api.responses.ValidatePhoneResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class AuthUseCaseImpl(private val repository: AuthRepository) : AuthUseCase { class AuthUseCaseImpl(private val repository: AuthRepository) : AuthUseCase {
override fun validatePhone(validationSid: String): Flow<State<ValidatePhoneResponse>> = flow { override fun logout(): Flow<State<Int>> = flowNewState { repository.logout().mapToState() }
emit(State.Loading)
val newState = repository.validatePhone(validationSid).mapToState() override fun validatePhone(validationSid: String): Flow<State<ValidatePhoneResponse>> =
emit(newState) flowNewState { repository.validatePhone(validationSid = validationSid).mapToState() }
override suspend fun getAnonymToken(
clientId: String,
clientSecret: String
): Flow<State<GetAnonymTokenResponse>> = flowNewState {
repository.getAnonymToken(
clientId = clientId,
clientSecret = clientSecret
).mapToState()
}
override suspend fun exchangeSilentToken(
anonymToken: String,
silentToken: String,
silentUuid: String
): Flow<State<ExchangeSilentTokenResponse>> = flowNewState {
repository.exchangeSilentToken(
anonymToken = anonymToken,
silentToken = silentToken,
silentUuid = silentUuid
).mapToState()
}
override suspend fun getExchangeToken(
accessToken: String
): Flow<State<GetExchangeTokenResponse>> = flowNewState {
repository.getExchangeToken(accessToken = accessToken).mapToState()
} }
} }
@@ -0,0 +1,16 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
interface BaseUseCase {
suspend fun <T> FlowCollector<State<T>>.emitState(stateBlock: suspend () -> State<T>) {
emit(State.Loading)
emit(stateBlock())
}
fun <T> flowNewState(stateBlock: suspend () -> State<T>) =
flow { emitState(stateBlock) }
}
@@ -1,19 +1,29 @@
package dev.meloda.fast.domain package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ConversationsUseCase { interface ConversationsUseCase : BaseUseCase {
fun getConversations(
count: Int?,
offset: Int?,
): Flow<State<List<VkConversation>>>
fun delete(peerId: Int): Flow<State<Int>>
fun changePinState(peerId: Int, pin: Boolean): Flow<State<Int>>
suspend fun storeConversations(conversations: List<VkConversation>) suspend fun storeConversations(conversations: List<VkConversation>)
fun getConversations(
count: Int? = null,
offset: Int? = null,
filter: ConversationsFilter
): Flow<State<List<VkConversation>>>
fun getById(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): Flow<State<List<VkConversation>>>
fun delete(peerId: Long): Flow<State<Long>>
fun changePinState(peerId: Long, pin: Boolean): Flow<State<Int>>
fun changeArchivedState(peerId: Long, archive: Boolean): Flow<State<Int>>
} }
@@ -3,116 +3,69 @@ package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.conversations.ConversationsRepository import dev.meloda.fast.data.api.conversations.ConversationsRepository
import dev.meloda.fast.data.mapToState import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ConversationsUseCaseImpl( class ConversationsUseCaseImpl(
private val repository: ConversationsRepository, private val repository: ConversationsRepository,
) : ConversationsUseCase { ) : ConversationsUseCase {
// override fun getConversations(
// count: Int?,
// offset: Int?,
// fields: String,
// filter: String,
// extended: Boolean?,
// startMessageId: Int?
// ): Flow<dev.meloda.fast.network.State<ConversationsResponseDomain>> = flow {
// emit(dev.meloda.fast.network.State.Loading)
//
// val newState = conversationsRepository.getConversations(
// params = ConversationsGetRequest(
// count = count,
// offset = offset,
// fields = fields,
// filter = filter,
// extended = extended,
// startMessageId = startMessageId
// )
// ).fold(
// onSuccess = { response -> dev.meloda.fast.network.State.Success(response.toDomain()) },
// onNetworkFailure = { dev.meloda.fast.network.State.Error.ConnectionError },
// onUnknownFailure = { dev.meloda.fast.network.State.UNKNOWN_ERROR },
// onHttpFailure = { result -> result.error.toStateApiError() },
// onApiFailure = { result -> result.error.toStateApiError() }
// )
// emit(newState)
// }
//
//
// override fun pin(peerId: Int): Flow<dev.meloda.fast.network.State<Unit>> = flow {
// emit(dev.meloda.fast.network.State.Loading)
//
// val newState = conversationsRepository.pin(
// ConversationsPinRequest(peerId = peerId)
// ).fold(
// onSuccess = { dev.meloda.fast.network.State.Success(Unit) },
// onNetworkFailure = { dev.meloda.fast.network.State.Error.ConnectionError },
// onUnknownFailure = { dev.meloda.fast.network.State.UNKNOWN_ERROR },
// onHttpFailure = { result -> result.error.toStateApiError() },
// onApiFailure = { result -> result.error.toStateApiError() }
// )
// emit(newState)
// }
//
// override fun unpin(peerId: Int): Flow<dev.meloda.fast.network.State<Unit>> = flow {
// emit(dev.meloda.fast.network.State.Loading)
//
// val newState = conversationsRepository.unpin(
// ConversationsUnpinRequest(peerId = peerId)
// ).fold(
// onSuccess = { dev.meloda.fast.network.State.Success(Unit) },
// onNetworkFailure = { dev.meloda.fast.network.State.Error.ConnectionError },
// onUnknownFailure = { dev.meloda.fast.network.State.UNKNOWN_ERROR },
// onHttpFailure = { result -> result.error.toStateApiError() },
// onApiFailure = { result -> result.error.toStateApiError() }
// )
// emit(newState)
// }
//
// override suspend fun storeConversations(conversations: List<VkConversationDomain>) {
// conversationsDao.insertAll(conversations.map(VkConversationDomain::mapToDb))
// }
//
// override suspend fun storeGroups(groups: List<VkGroupDomain>) {
// groupsDao.insertAll(groups.map(VkGroupDomain::mapToDB))
// }
override fun getConversations(
count: Int?,
offset: Int?
): Flow<State<List<VkConversation>>> = flow {
emit(State.Loading)
val newState = repository.getConversations(count, offset).mapToState()
emit(newState)
}
override suspend fun storeConversations( override suspend fun storeConversations(
conversations: List<VkConversation> conversations: List<VkConversation>
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
repository.storeConversations(conversations) repository.storeConversations(conversations)
} }
override fun delete(peerId: Int): Flow<State<Int>> = flow { override fun getConversations(
emit(State.Loading) count: Int?,
offset: Int?,
val newState = repository.delete(peerId = peerId).mapToState() filter: ConversationsFilter
emit(newState) ): Flow<State<List<VkConversation>>> = flowNewState {
repository.getConversations(
count = count,
offset = offset,
filter = filter
).mapToState()
} }
override fun changePinState(peerId: Int, pin: Boolean): Flow<State<Int>> = flow { override fun getById(
emit(State.Loading) peerIds: List<Long>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkConversation>>> = flowNewState {
repository.getConversationsById(
peerIds = peerIds,
extended = extended,
fields = fields
).mapToState()
}
val newState = if (pin) { override fun delete(peerId: Long): Flow<State<Long>> = flowNewState {
repository.delete(peerId = peerId).mapToState()
}
override fun changePinState(
peerId: Long,
pin: Boolean
): Flow<State<Int>> = flowNewState {
if (pin) {
repository.pin(peerId) repository.pin(peerId)
} else { } else {
repository.unpin(peerId) repository.unpin(peerId)
}.mapToState() }.mapToState()
}
emit(newState) override fun changeArchivedState(
peerId: Long,
archive: Boolean
): Flow<State<Int>> = flowNewState {
if (archive) {
repository.archive(peerId)
} else {
repository.unarchive(peerId)
}.mapToState()
} }
} }
@@ -22,7 +22,7 @@ interface FriendsUseCase {
fun getOnlineFriends( fun getOnlineFriends(
count: Int?, count: Int?,
offset: Int? offset: Int?
): Flow<State<List<Int>>> ): Flow<State<List<Long>>>
suspend fun storeUsers(users: List<VkUser>) suspend fun storeUsers(users: List<VkUser>)
} }
@@ -36,7 +36,7 @@ class FriendsUseCaseImpl(private val repository: FriendsRepository) :
override fun getOnlineFriends( override fun getOnlineFriends(
count: Int?, offset: Int? count: Int?, offset: Int?
): Flow<State<List<Int>>> = flow { ): Flow<State<List<Long>>> = flow {
emit(State.Loading) emit(State.Loading)
val newState = repository.getOnlineFriends(count, offset).mapToState() val newState = repository.getOnlineFriends(count, offset).mapToState()
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.flow
class GetLocalUserByIdUseCase(private val repository: UsersRepository) { class GetLocalUserByIdUseCase(private val repository: UsersRepository) {
operator fun invoke(userId: Int): Flow<State<VkUser?>> = flow { operator fun invoke(userId: Long): Flow<State<VkUser?>> = flow {
emit(State.Loading) emit(State.Loading)
val newState = kotlin.runCatching { val newState = kotlin.runCatching {
@@ -21,7 +21,7 @@ class GetLocalUserByIdUseCase(private val repository: UsersRepository) {
emit(newState) emit(newState)
} }
suspend fun proceed(userId: Int): VkUser? { suspend fun proceed(userId: Long): VkUser? {
return repository.getLocalUsers(userIds = listOf(userId)).singleOrNull() return repository.getLocalUsers(userIds = listOf(userId)).singleOrNull()
} }
} }
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.flow
class GetLocalUsersByIdsUseCase(private val repository: UsersRepository) { class GetLocalUsersByIdsUseCase(private val repository: UsersRepository) {
operator fun invoke(userIds: List<Int>): Flow<State<List<VkUser>>> = flow { operator fun invoke(userIds: List<Long>): Flow<State<List<VkUser>>> = flow {
emit(State.Loading) emit(State.Loading)
val newState = kotlin.runCatching { val newState = kotlin.runCatching {
@@ -5,19 +5,21 @@ import dev.meloda.fast.data.api.conversations.ConversationsRepository
import dev.meloda.fast.data.mapToState import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class LoadConversationsByIdUseCase( class LoadConversationsByIdUseCase(
private val conversationsRepository: ConversationsRepository private val conversationsRepository: ConversationsRepository
) { ) : BaseUseCase {
operator fun invoke(peerIds: List<Int>): Flow<State<List<VkConversation>>> = flow { operator fun invoke(
emit(State.Loading) peerIds: List<Long>,
extended: Boolean? = null,
val newState = conversationsRepository fields: String? = null
.getConversationsById(peerIds = peerIds) ): Flow<State<List<VkConversation>>> = flowNewState {
.mapToState() conversationsRepository
.getConversationsById(
emit(newState) peerIds = peerIds,
extended = extended,
fields = fields,
).mapToState()
} }
} }
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.flow
class LoadUserByIdUseCase(private val repository: UsersRepository) { class LoadUserByIdUseCase(private val repository: UsersRepository) {
operator fun invoke( operator fun invoke(
userId: Int?, userId: Long?,
fields: String = VkConstants.USER_FIELDS, fields: String = VkConstants.USER_FIELDS,
nomCase: String? = null nomCase: String? = null
): Flow<State<VkUser?>> = flow { ): Flow<State<VkUser?>> = flow {
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.flow
class LoadUsersByIdsUseCase(private val repository: UsersRepository) { class LoadUsersByIdsUseCase(private val repository: UsersRepository) {
operator fun invoke( operator fun invoke(
userIds: List<Int>?, userIds: List<Long>?,
fields: String = VkConstants.USER_FIELDS, fields: String = VkConstants.USER_FIELDS,
nomCase: String? = null nomCase: String? = null
): Flow<State<List<VkUser>>> = flow { ): Flow<State<List<VkUser>>> = flow {
@@ -3,21 +3,24 @@ package dev.meloda.fast.domain
import android.util.Log import android.util.Log
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.asInt import dev.meloda.fast.common.extensions.asInt
import dev.meloda.fast.common.extensions.asLong
import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.toList import dev.meloda.fast.common.extensions.toList
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.model.ApiEvent import dev.meloda.fast.model.ApiEvent
import dev.meloda.fast.model.ConversationFlags
import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.InteractionType
import dev.meloda.fast.model.LongPollEvent import dev.meloda.fast.model.LongPollEvent
import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.LongPollParsedEvent
import dev.meloda.fast.model.MessageFlags import dev.meloda.fast.model.MessageFlags
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@@ -25,6 +28,7 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class LongPollUpdatesParser( class LongPollUpdatesParser(
private val conversationsUseCase: ConversationsUseCase,
private val messagesUseCase: MessagesUseCase private val messagesUseCase: MessagesUseCase
) { ) {
private val job = SupervisorJob() private val job = SupervisorJob()
@@ -68,6 +72,458 @@ class LongPollUpdatesParser(
ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event) ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event)
ApiEvent.UNREAD_COUNT_UPDATE -> parseUnreadCounterUpdate(eventType, event) ApiEvent.UNREAD_COUNT_UPDATE -> parseUnreadCounterUpdate(eventType, event)
ApiEvent.MESSAGE_UPDATED -> parseMessageUpdated(eventType, event)
ApiEvent.MESSAGE_CACHE_CLEAR -> parseMessageCacheClear(eventType, event)
}
}
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val cmId = event[1].asLong()
val flags = event[2].asInt()
val peerId = event[3].asLong()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = MessageFlags.parse(flags)
parsedFlags.forEach { flag ->
when (flag) {
MessageFlags.IMPORTANT -> {
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
peerId = peerId,
cmId = cmId,
marked = true
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.SPAM -> {
val eventToSend = LongPollParsedEvent.MessageMarkedAsSpam(
peerId = peerId,
cmId = cmId
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_SPAM]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsSpam>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.DELETED -> {
val eventToSend =
if (parsedFlags.contains(MessageFlags.DELETED_FOR_ALL)) {
LongPollParsedEvent.MessageDeleted(
peerId = peerId,
cmId = cmId,
forAll = true
)
} else {
LongPollParsedEvent.MessageDeleted(
peerId = peerId,
cmId = cmId,
forAll = false
)
}
eventsToSend += eventToSend
listenersMap[LongPollEvent.MESSAGE_DELETED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageDeleted>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.AUDIO_LISTENED -> {
val eventToSend = LongPollParsedEvent.AudioMessageListened(
peerId = peerId,
cmId = cmId
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.AudioMessageListened>)
?.onEvent(eventToSend)
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent(eventToSend)
}
}
}
}
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val cmId = event[1].asLong()
val flags = event[2].asInt()
val peerId = event[3].asLong()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = MessageFlags.parse(flags)
coroutineScope.launch {
parsedFlags.forEach { flag ->
when (flag) {
MessageFlags.IMPORTANT -> {
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
peerId = peerId,
cmId = cmId,
marked = false
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.SPAM -> {
if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) {
withContext(Dispatchers.IO) {
val message = loadMessage(
peerId = peerId,
cmId = cmId
)
message?.let {
val eventToSend =
LongPollParsedEvent.MessageMarkedAsNotSpam(message = message)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsNotSpam>)
?.onEvent(eventToSend)
}
}
}
}
}
}
MessageFlags.DELETED -> {
withContext(Dispatchers.IO) {
val message = loadMessage(
peerId = peerId,
cmId = cmId
)
message?.let {
val eventToSend =
LongPollParsedEvent.MessageRestored(message = message)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MESSAGE_RESTORED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageRestored>)
?.onEvent(eventToSend)
}
}
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
vkEventCallback.onEvent(eventToSend)
}
}
}
}
}
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val cmId = event[1].asLong()
val peerId = event[4].asLong()
coroutineScope.launch(Dispatchers.IO) {
val message =
async { loadMessage(peerId = peerId, cmId = cmId) }.await()
val conversation =
async {
loadConversation(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
)
}.await()
message?.let {
listenersMap[LongPollEvent.MESSAGE_NEW]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.NewMessage>)
.onEvent(
LongPollParsedEvent.NewMessage(
message = message,
inArchive = conversation?.isArchived == true
// TODO: 03-Apr-25, Danil Nikolaev:
// load user settings about restoring chats with
// enabled notifications from archive
)
)
}
}
}
}
}
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val cmId = event[1].asLong()
val peerId = event[3].asLong()
coroutineScope.launch(Dispatchers.IO) {
loadMessage(
peerId = peerId,
cmId = cmId
)?.let { message ->
listenersMap[LongPollEvent.MESSAGE_EDITED]?.let {
it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageEdited>)
.onEvent(LongPollParsedEvent.MessageEdited(message))
}
}
}
}
}
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val cmId = event[2].asLong()
val unreadCount = event[3].asInt()
listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.IncomingMessageRead>)
.onEvent(
LongPollParsedEvent.IncomingMessageRead(
peerId = peerId,
cmId = cmId,
unreadCount = unreadCount
)
)
}
}
}
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val cmId = event[2].asLong()
val unreadCount = event[3].asInt()
listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.OutgoingMessageRead>)
.onEvent(
LongPollParsedEvent.OutgoingMessageRead(
peerId = peerId,
cmId = cmId,
unreadCount = unreadCount
)
)
}
}
}
private fun parseChatClearFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val flags = event[2].asInt()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = ConversationFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag ->
when (flag) {
ConversationFlags.ARCHIVED -> {
val conversation = loadConversation(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
) ?: return@forEach
val message = loadMessage(
peerId = peerId,
cmId = conversation.lastCmId
)
val eventToSend = LongPollParsedEvent.ChatArchived(
conversation = conversation.copy(lastMessage = message),
archived = false
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.ChatArchived>)
?.onEvent(eventToSend)
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.CHAT_CLEAR_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent(
eventToSend
)
}
}
}
}
}
private fun parseChatSetFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val flags = event[2].asInt()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = ConversationFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag ->
when (flag) {
ConversationFlags.ARCHIVED -> {
val conversation = loadConversation(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
) ?: return@forEach
val message = loadMessage(
peerId = peerId,
cmId = conversation.lastCmId
)
val eventToSend = LongPollParsedEvent.ChatArchived(
conversation = conversation.copy(lastMessage = message),
archived = true
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.CHAT_ARCHIVED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.ChatArchived>)
?.onEvent(eventToSend)
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.CHAT_SET_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent(
eventToSend
)
}
}
}
}
}
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val cmId = event[2].asLong()
listenersMap[LongPollEvent.CHAT_CLEARED]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatCleared>)
.onEvent(
LongPollParsedEvent.ChatCleared(
peerId = peerId,
toCmId = cmId
)
)
}
}
}
private fun parseChatMajorChanged(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val majorId = event[2].asInt()
listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMajorChanged>)
.onEvent(
LongPollParsedEvent.ChatMajorChanged(
peerId = peerId,
majorId = majorId,
)
)
}
}
}
private fun parseChatMinorChanged(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asLong()
val minorId = event[2].asInt()
listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMinorChanged>)
.onEvent(
LongPollParsedEvent.ChatMinorChanged(
peerId = peerId,
minorId = minorId,
)
)
}
} }
} }
@@ -92,8 +548,8 @@ class LongPollUpdatesParser(
else -> return else -> return
} }
val peerId = event[1].asInt() val peerId = event[1].asLong()
val userIds = event[2].toList(Any::asInt).filter { it != UserConfig.userId } val userIds = event[2].toList(Any::asLong).filter { it != UserConfig.userId }
val totalCount = event[3].asInt() val totalCount = event[3].asInt()
val timestamp = event[4].asInt() val timestamp = event[4].asInt()
@@ -145,325 +601,57 @@ class LongPollUpdatesParser(
} }
} }
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) { private fun parseMessageUpdated(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event") Log.d("LongPollUpdatesParser", "$eventType $event")
val messageId = event[1].asInt() val cmId = event[1].asLong()
val flags = event[2].asInt() val peerId = event[4].asLong()
val peerId = event[3].asInt()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = MessageFlags.parse(flags)
parsedFlags.forEach { flag ->
when (flag) {
MessageFlags.IMPORTANT -> { // marked as important
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
peerId = peerId,
messageId = messageId,
marked = true
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.SPAM -> { // marked as spam
val eventToSend = LongPollParsedEvent.MessageMarkedAsSpam(
peerId = peerId,
messageId = messageId
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_SPAM]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsSpam>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.DELETED -> {
val eventToSend =
if (parsedFlags.contains(MessageFlags.DELETED_FOR_ALL)) { // deleted for all
LongPollParsedEvent.MessageDeleted(
peerId = peerId,
messageId = messageId,
forAll = true
)
} else { // deleted only for me
LongPollParsedEvent.MessageDeleted(
peerId = peerId,
messageId = messageId,
forAll = false
)
}
eventsToSend += eventToSend
listenersMap[LongPollEvent.MESSAGE_DELETED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageDeleted>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.AUDIO_LISTENED -> { // audio message listened
val eventToSend = LongPollParsedEvent.AudioMessageListened(
peerId = peerId,
messageId = messageId
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.AudioMessageListened>)
?.onEvent(eventToSend)
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent(eventToSend)
}
}
}
}
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt()
val flags = event[2].asInt()
val peerId = event[3].asInt()
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = MessageFlags.parse(flags)
coroutineScope.launch {
parsedFlags.forEach { flag ->
when (flag) {
MessageFlags.IMPORTANT -> { // not important anymore
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
peerId = peerId,
messageId = messageId,
marked = false
)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
?.onEvent(eventToSend)
}
}
}
MessageFlags.SPAM -> {
if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) { // not spam anymore
withContext(Dispatchers.IO) {
val message = loadMessage(messageId)
message?.let {
val eventToSend =
LongPollParsedEvent.MessageMarkedAsNotSpam(message = message)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsNotSpam>)
?.onEvent(eventToSend)
}
}
}
}
}
}
MessageFlags.DELETED -> { // restored
withContext(Dispatchers.IO) {
val message = loadMessage(messageId)
message?.let {
val eventToSend =
LongPollParsedEvent.MessageRestored(message = message)
eventsToSend += eventToSend
listenersMap[LongPollEvent.MESSAGE_RESTORED]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageRestored>)
?.onEvent(eventToSend)
}
}
}
}
}
else -> Unit
}
}
eventsToSend.forEach { eventToSend ->
listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.let { listeners ->
listeners.map { vkEventCallback ->
vkEventCallback.onEvent(eventToSend)
}
}
}
}
}
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val messageId = event[1].asInt()
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
loadMessage(messageId)?.let { message -> loadMessage(
listenersMap[LongPollEvent.MESSAGE_NEW]?.let { peerId = peerId,
cmId = cmId
)?.let { message ->
listenersMap[LongPollEvent.MESSAGE_UPDATED]?.let {
it.map { vkEventCallback -> it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.NewMessage>) (vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageUpdated>)
.onEvent(LongPollParsedEvent.NewMessage(message)) .onEvent(LongPollParsedEvent.MessageUpdated(message))
} }
} }
} }
} }
} }
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) { private fun parseMessageCacheClear(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event") Log.d("LongPollUpdatesParser", "$eventType $event")
val messageId = event[1].asInt()
val messageId = event[1].asLong()
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
loadMessage(messageId)?.let { message -> loadMessage(messageId = messageId)?.let { message ->
listenersMap[LongPollEvent.MESSAGE_EDITED]?.let { listenersMap[LongPollEvent.MESSAGE_CACHE_CLEAR]?.let {
it.map { vkEventCallback -> it.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageEdited>) (vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageCacheClear>)
.onEvent(LongPollParsedEvent.MessageEdited(message)) .onEvent(LongPollParsedEvent.MessageCacheClear(message))
} }
} }
} }
} }
} }
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) { private suspend fun loadMessage(
Log.d("LongPollUpdatesParser", "$eventType: $event") peerId: Long? = null,
val peerId = event[1].asInt() cmId: Long? = null,
val messageId = event[2].asInt() messageId: Long? = null
val unreadCount = event[3].asInt() ): VkMessage? = suspendCoroutine { continuation ->
require((peerId != null && cmId != null) || messageId != null)
listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.IncomingMessageRead>)
.onEvent(
LongPollParsedEvent.IncomingMessageRead(
peerId = peerId,
messageId = messageId,
unreadCount = unreadCount
)
)
}
}
}
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt()
val messageId = event[2].asInt()
val unreadCount = event[3].asInt()
listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners ->
listeners.map { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.OutgoingMessageRead>)
.onEvent(
LongPollParsedEvent.OutgoingMessageRead(
peerId = peerId,
messageId = messageId,
unreadCount = unreadCount
)
)
}
}
}
private fun parseChatClearFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseChatSetFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt()
val messageId = event[2].asInt()
listenersMap[LongPollEvent.CHAT_CLEARED]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatCleared>)
.onEvent(
LongPollParsedEvent.ChatCleared(
peerId = peerId,
toMessageId = messageId
)
)
}
}
}
private fun parseChatMajorChanged(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt()
val majorId = event[2].asInt()
listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMajorChanged>)
.onEvent(
LongPollParsedEvent.ChatMajorChanged(
peerId = peerId,
majorId = majorId,
)
)
}
}
}
private fun parseChatMinorChanged(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollUpdatesParser", "$eventType: $event")
val peerId = event[1].asInt()
val minorId = event[2].asInt()
listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.let { listeners ->
listeners.forEach { vkEventCallback ->
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMinorChanged>)
.onEvent(
LongPollParsedEvent.ChatMinorChanged(
peerId = peerId,
minorId = minorId,
)
)
}
}
}
private suspend fun loadMessage(messageId: Int): VkMessage? = suspendCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
messagesUseCase.getById( messagesUseCase.getById(
messageIds = listOf(messageId), peerCmIds = null,
peerId = peerId,
messageIds = messageId?.let(::listOf),
cmIds = cmId?.let(::listOf),
extended = true, extended = true,
fields = VkConstants.ALL_FIELDS fields = VkConstants.ALL_FIELDS
).listenValue(this) { state -> ).listenValue(this) { state ->
@@ -478,9 +666,6 @@ class LongPollUpdatesParser(
return@listenValue return@listenValue
} }
VkMemoryCache[message.id] = message
messagesUseCase.storeMessage(message)
continuation.resume(message) continuation.resume(message)
} }
) )
@@ -488,6 +673,35 @@ class LongPollUpdatesParser(
} }
} }
private suspend fun loadConversation(
peerId: Long,
extended: Boolean = false,
fields: String? = null
): VkConversation? = suspendCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) {
conversationsUseCase.getById(
peerIds = listOf(peerId),
extended = extended,
fields = fields
).listenValue(coroutineScope) { state ->
state.processState(
error = { error ->
Log.e("LongPollUpdatesParser", "loadConversation: error: $error")
continuation.resume(null)
},
success = { response ->
val conversation = response.singleOrNull() ?: run {
continuation.resume(null)
return@listenValue
}
continuation.resume(conversation)
}
)
}
}
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun <T : LongPollParsedEvent> registerListener( private fun <T : LongPollParsedEvent> registerListener(
eventType: LongPollEvent, eventType: LongPollEvent,
@@ -564,6 +778,10 @@ class LongPollUpdatesParser(
registerListener(LongPollEvent.CHAT_MINOR_CHANGED, assembleEventCallback(block)) registerListener(LongPollEvent.CHAT_MINOR_CHANGED, assembleEventCallback(block))
} }
fun onChatArchived(block: (LongPollParsedEvent.ChatArchived) -> Unit) {
registerListener(LongPollEvent.CHAT_ARCHIVED, assembleEventCallback(block))
}
fun onInteractions(block: (LongPollParsedEvent.Interaction) -> Unit) { fun onInteractions(block: (LongPollParsedEvent.Interaction) -> Unit) {
registerListeners( registerListeners(
eventTypes = listOf( eventTypes = listOf(
@@ -576,10 +794,6 @@ class LongPollUpdatesParser(
listener = assembleEventCallback(block) listener = assembleEventCallback(block)
) )
} }
fun clearListeners() {
listenersMap.clear()
}
} }
internal inline fun <R : LongPollParsedEvent> assembleEventCallback( internal inline fun <R : LongPollParsedEvent> assembleEventCallback(
@@ -5,71 +5,77 @@ import dev.meloda.fast.data.api.messages.MessagesHistoryInfo
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface MessagesUseCase { interface MessagesUseCase : BaseUseCase {
suspend fun storeMessage(message: VkMessage)
suspend fun storeMessages(messages: List<VkMessage>)
fun getMessagesHistory( fun getMessagesHistory(
conversationId: Int, conversationId: Long,
count: Int?, count: Int?,
offset: Int? offset: Int?
): Flow<State<MessagesHistoryInfo>> ): Flow<State<MessagesHistoryInfo>>
fun getById( fun getById(
messageIds: List<Int>, peerCmIds: List<Long>?,
peerId: Long?,
messageIds: List<Long>?,
cmIds: List<Long>?,
extended: Boolean?, extended: Boolean?,
fields: String? fields: String?
): Flow<State<List<VkMessage>>> ): Flow<State<List<VkMessage>>>
fun sendMessage( fun sendMessage(
peerId: Int, peerId: Long,
randomId: Int, randomId: Long,
message: String?, message: String?,
replyTo: Int?, replyTo: Long?,
attachments: List<VkAttachment>? attachments: List<VkAttachment>?
): Flow<State<Int>> ): Flow<State<MessagesSendResponse>>
fun markAsRead( fun markAsRead(
peerId: Int, peerId: Long,
startMessageId: Int startMessageId: Long
): Flow<State<Int>> ): Flow<State<Int>>
fun getHistoryAttachments( fun getHistoryAttachments(
peerId: Int, peerId: Long,
count: Int?, count: Int? = null,
offset: Int?, offset: Int? = null,
attachmentTypes: List<String>, attachmentTypes: List<String>,
conversationMessageId: Int cmId: Long
): Flow<State<List<VkAttachmentHistoryMessage>>> ): Flow<State<List<VkAttachmentHistoryMessage>>>
fun createChat( fun createChat(
userIds: List<Int>?, userIds: List<Long>? = null,
title: String? title: String
): Flow<State<Int>> ): Flow<State<Long>>
fun pin( fun pin(
peerId: Int, peerId: Long,
messageId: Int?, messageId: Long? = null,
conversationMessageId: Int? cmId: Long? = null
): Flow<State<VkMessage>> ): Flow<State<VkMessage>>
fun unpin( fun unpin(
peerId: Int peerId: Long
): Flow<State<Int>> ): Flow<State<Int>>
fun markAsImportant( fun markAsImportant(
peerId: Int, peerId: Long,
messageIds: List<Int>, messageIds: List<Long>? = null,
cmIds: List<Long>? = null,
important: Boolean important: Boolean
): Flow<State<List<Int>>> ): Flow<State<List<Long>>>
fun delete( fun delete(
peerId: Int, peerId: Long,
messageIds: List<Int>, messageIds: List<Long>? = null,
cmIds: List<Long>? = null,
spam: Boolean = false, spam: Boolean = false,
deleteForAll: Boolean = false deleteForAll: Boolean = false
): Flow<State<List<Any>>> ): Flow<State<List<Any>>>
suspend fun storeMessage(message: VkMessage)
suspend fun storeMessages(messages: List<VkMessage>)
} }
@@ -7,163 +7,13 @@ import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class MessagesUseCaseImpl( class MessagesUseCaseImpl(
private val repository: MessagesRepository private val repository: MessagesRepository,
) : MessagesUseCase { ) : MessagesUseCase {
override fun getMessagesHistory(
conversationId: Int,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryInfo>> = flow {
emit(State.Loading)
val newState = repository.getHistory(
conversationId = conversationId,
offset = offset,
count = count
).mapToState()
emit(newState)
}
override fun getById(
messageIds: List<Int>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkMessage>>> = flow {
emit(State.Loading)
val newState = repository.getById(
messagesIds = messageIds,
extended = extended,
fields = fields
).mapToState()
emit(newState)
}
override fun sendMessage(
peerId: Int,
randomId: Int,
message: String?,
replyTo: Int?,
attachments: List<VkAttachment>?
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = repository.send(
peerId = peerId,
randomId = randomId,
message = message,
replyTo = replyTo,
attachments = attachments
).mapToState()
emit(newState)
}
override fun markAsRead(
peerId: Int,
startMessageId: Int
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = repository.markAsRead(
peerId = peerId,
startMessageId = startMessageId
).mapToState()
emit(newState)
}
override fun getHistoryAttachments(
peerId: Int,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
conversationMessageId: Int
): Flow<State<List<VkAttachmentHistoryMessage>>> = flow {
emit(State.Loading)
val newState = repository.getHistoryAttachments(
peerId = peerId,
count = count,
offset = offset,
attachmentTypes = attachmentTypes,
conversationMessageId = conversationMessageId
).mapToState()
emit(newState)
}
override fun createChat(userIds: List<Int>?, title: String?): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = repository.createChat(userIds, title).mapToState()
emit(newState)
}
override fun pin(
peerId: Int,
messageId: Int?,
conversationMessageId: Int?
): Flow<State<VkMessage>> = flow {
emit(State.Loading)
val newState = repository.pin(
peerId = peerId,
messageId = messageId,
conversationMessageId = conversationMessageId
).mapToState()
emit(newState)
}
override fun unpin(peerId: Int): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = repository.unpin(peerId = peerId).mapToState()
emit(newState)
}
override fun markAsImportant(
peerId: Int,
messageIds: List<Int>,
important: Boolean
): Flow<State<List<Int>>> = flow {
emit(State.Loading)
val newState = repository.markAsImportant(
peerId = peerId,
messageIds = messageIds,
conversationMessageIds = null,
important = important
).mapToState()
emit(newState)
}
override fun delete(
peerId: Int,
messageIds: List<Int>,
spam: Boolean,
deleteForAll: Boolean
): Flow<State<List<Any>>> = flow {
emit(State.Loading)
val newState = repository.delete(
peerId = peerId,
messageIds = messageIds,
conversationMessageIds = null,
spam = spam,
deleteForAll = deleteForAll
).mapToState()
emit(newState)
}
override suspend fun storeMessage(message: VkMessage) { override suspend fun storeMessage(message: VkMessage) {
repository.storeMessages(listOf(message)) repository.storeMessages(listOf(message))
} }
@@ -171,4 +21,129 @@ class MessagesUseCaseImpl(
override suspend fun storeMessages(messages: List<VkMessage>) { override suspend fun storeMessages(messages: List<VkMessage>) {
repository.storeMessages(messages) repository.storeMessages(messages)
} }
override fun getMessagesHistory(
conversationId: Long,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryInfo>> = flowNewState {
repository.getHistory(
conversationId = conversationId,
offset = offset,
count = count
).mapToState()
}
override fun getById(
peerCmIds: List<Long>?,
peerId: Long?,
messageIds: List<Long>?,
cmIds: List<Long>?,
extended: Boolean?,
fields: String?
): Flow<State<List<VkMessage>>> = flowNewState {
repository.getById(
peerCmIds = peerCmIds,
peerId = peerId,
messagesIds = messageIds,
cmIds = cmIds,
extended = extended,
fields = fields
).mapToState()
}
override fun sendMessage(
peerId: Long,
randomId: Long,
message: String?,
replyTo: Long?,
attachments: List<VkAttachment>?
): Flow<State<MessagesSendResponse>> = flowNewState {
repository.send(
peerId = peerId,
randomId = randomId,
message = message,
replyTo = replyTo,
attachments = attachments
).mapToState()
}
override fun markAsRead(
peerId: Long,
startMessageId: Long
): Flow<State<Int>> = flowNewState {
repository.markAsRead(
peerId = peerId,
startMessageId = startMessageId
).mapToState()
}
override fun getHistoryAttachments(
peerId: Long,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
cmId: Long
): Flow<State<List<VkAttachmentHistoryMessage>>> = flowNewState {
repository.getHistoryAttachments(
peerId = peerId,
count = count,
offset = offset,
attachmentTypes = attachmentTypes,
cmId = cmId
).mapToState()
}
override fun createChat(
userIds: List<Long>?,
title: String
): Flow<State<Long>> = flowNewState {
repository.createChat(userIds, title).mapToState()
}
override fun pin(
peerId: Long,
messageId: Long?,
cmId: Long?
): Flow<State<VkMessage>> = flowNewState {
repository.pin(
peerId = peerId,
messageId = messageId,
cmId = cmId
).mapToState()
}
override fun unpin(peerId: Long): Flow<State<Int>> = flowNewState {
repository.unpin(peerId = peerId).mapToState()
}
override fun markAsImportant(
peerId: Long,
messageIds: List<Long>?,
cmIds: List<Long>?,
important: Boolean
): Flow<State<List<Long>>> = flowNewState {
repository.markAsImportant(
peerId = peerId,
messageIds = messageIds,
cmIds = cmIds,
important = important
).mapToState()
}
override fun delete(
peerId: Long,
messageIds: List<Long>?,
cmIds: List<Long>?,
spam: Boolean,
deleteForAll: Boolean
): Flow<State<List<Any>>> = flowNewState {
repository.delete(
peerId = peerId,
messageIds = messageIds,
cmIds = cmIds,
spam = spam,
deleteForAll = deleteForAll
).mapToState()
}
} }
@@ -2,6 +2,7 @@ package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.model.AuthInfo import dev.meloda.fast.model.AuthInfo
import dev.meloda.fast.model.api.responses.GetSilentTokenResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface OAuthUseCase { interface OAuthUseCase {
@@ -14,4 +15,13 @@ interface OAuthUseCase {
captchaSid: String?, captchaSid: String?,
captchaKey: String? captchaKey: String?
): Flow<State<AuthInfo>> ): Flow<State<AuthInfo>>
fun getSilentToken(
login: String,
password: String,
forceSms: Boolean,
validationCode: String?,
captchaSid: String?,
captchaKey: String?
): Flow<State<GetSilentTokenResponse>>
} }
@@ -2,11 +2,9 @@ package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.oauth.OAuthRepository import dev.meloda.fast.data.api.oauth.OAuthRepository
import dev.meloda.fast.data.asState
import dev.meloda.fast.model.AuthInfo import dev.meloda.fast.model.AuthInfo
import dev.meloda.fast.network.OAuthErrorDomain import dev.meloda.fast.model.api.responses.GetSilentTokenResponse
import dev.meloda.fast.network.ValidationType
import dev.meloda.fast.network.VkOAuthError
import dev.meloda.fast.network.VkOAuthErrorType
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@@ -24,109 +22,45 @@ class OAuthUseCaseImpl(
): Flow<State<AuthInfo>> = flow { ): Flow<State<AuthInfo>> = flow {
emit(State.Loading) emit(State.Loading)
val response = oAuthRepository.auth( val newState = oAuthRepository.auth(
login = login, login = login,
password = password, password = password,
forceSms = forceSms,
validationCode = validationCode, validationCode = validationCode,
captchaSid = captchaSid, captchaSid = captchaSid,
captchaKey = captchaKey, captchaKey = captchaKey
forceSms = forceSms ).asState(
) successMapper = {
AuthInfo(
kotlin.runCatching { userId = it.userId!!,
val error = response.error?.let(VkOAuthError::parse) accessToken = it.accessToken!!,
val errorType = response.errorType?.let(VkOAuthErrorType::parse) validationHash = it.validationHash!!
)
val newState = when (error) {
null -> {
State.Success(
AuthInfo(
userId = response.userId,
accessToken = response.accessToken,
validationHash = response.validationHash
)
)
}
VkOAuthError.FLOOD_CONTROL -> {
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
}
VkOAuthError.NEED_VALIDATION -> {
if (response.banInfo != null) {
val info = requireNotNull(response.banInfo)
State.Error.OAuthError(
OAuthErrorDomain.UserBannedError(
memberName = info.memberName,
message = info.message,
accessToken = info.accessToken,
restoreUrl = info.restoreUrl
)
)
} else {
State.Error.OAuthError(
OAuthErrorDomain.ValidationRequiredError(
description = response.errorDescription.orEmpty(),
validationType = response.validationType.orEmpty()
.let(ValidationType::parse),
validationSid = response.validationSid.orEmpty(),
phoneMask = response.phoneMask.orEmpty(),
redirectUri = response.redirectUri.orEmpty(),
validationResend = response.validationResend,
restoreIfCannotGetCode = response.restoreIfCannotGetCode
)
)
}
}
VkOAuthError.NEED_CAPTCHA -> {
State.Error.OAuthError(
OAuthErrorDomain.CaptchaRequiredError(
captchaSid = response.captchaSid.orEmpty(),
captchaImageUrl = response.captchaImage.orEmpty()
)
)
}
VkOAuthError.INVALID_CLIENT -> {
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
}
VkOAuthError.INVALID_REQUEST -> {
when (errorType) {
null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError)
VkOAuthErrorType.WRONG_OTP -> {
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode)
}
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat)
}
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
}
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
}
}
}
VkOAuthError.UNKNOWN -> {
State.Error.OAuthError(OAuthErrorDomain.UnknownError)
}
}
emit(newState)
}.fold(
onSuccess = {
},
onFailure = {
emit(State.Error.TestError(it.stackTraceToString()))
} }
) )
emit(newState)
}
override fun getSilentToken(
login: String,
password: String,
forceSms: Boolean,
validationCode: String?,
captchaSid: String?,
captchaKey: String?
): Flow<State<GetSilentTokenResponse>> = flow {
emit(State.Loading)
val newState = oAuthRepository.getSilentToken(
login = login,
password = password,
forceSms = forceSms,
validationCode = validationCode,
captchaSid = captchaSid,
captchaKey = captchaKey
).asState()
emit(newState)
} }
} }
@@ -1,15 +1,17 @@
package dev.meloda.fast.model package dev.meloda.fast.model
enum class ApiEvent(val value: Int) { enum class ApiEvent(val value: Int) {
MESSAGE_SET_FLAGS(2), MESSAGE_SET_FLAGS(10002),
MESSAGE_CLEAR_FLAGS(3), MESSAGE_CLEAR_FLAGS(10003),
MESSAGE_NEW(4), MESSAGE_NEW(10004),
MESSAGE_EDIT(5), MESSAGE_EDIT(10005),
MESSAGE_READ_INCOMING(6), MESSAGE_READ_INCOMING(10006),
MESSAGE_READ_OUTGOING(7), MESSAGE_READ_OUTGOING(10007),
CHAT_CLEAR_FLAGS(10), CHAT_CLEAR_FLAGS(10),
CHAT_SET_FLAGS(12), CHAT_SET_FLAGS(12),
MESSAGES_DELETED(13), MESSAGES_DELETED(10013),
MESSAGE_UPDATED(10018),
MESSAGE_CACHE_CLEAR(10019),
CHAT_MAJOR_CHANGED(20), CHAT_MAJOR_CHANGED(20),
CHAT_MINOR_CHANGED(21), CHAT_MINOR_CHANGED(21),
TYPING(63), TYPING(63),
@@ -1,7 +1,7 @@
package dev.meloda.fast.model package dev.meloda.fast.model
data class AuthInfo( data class AuthInfo(
val userId: Int?, val userId: Long,
val accessToken: String?, val accessToken: String,
val validationHash: String? val validationHash: String
) )
@@ -6,6 +6,10 @@ import androidx.compose.runtime.Immutable
sealed class BaseError { sealed class BaseError {
data object SessionExpired : BaseError() data object SessionExpired : BaseError()
data object AccountBlocked : BaseError()
data object ConnectionError : BaseError()
data object InternalError : BaseError()
data object UnknownError : BaseError()
data class SimpleError(val message: String) : BaseError() data class SimpleError(val message: String) : BaseError()
} }
@@ -13,5 +13,20 @@ enum class ConversationFlags(val value: Int) {
DO_NOT_NOTIFY_ALL_MENTIONS(524288), DO_NOT_NOTIFY_ALL_MENTIONS(524288),
MARKED_AS_UNREAD(1048576), MARKED_AS_UNREAD(1048576),
ARCHIVED(8388608), ARCHIVED(8388608),
CALL_IN_PROGRESS(16777216), CALL_IN_PROGRESS(16777216);
companion object {
fun parse(mask: Int): List<ConversationFlags> {
val flags = mutableListOf<ConversationFlags>()
ConversationFlags.entries.forEach { flag ->
if (mask and flag.value > 0) {
flags.add(flag)
}
}
return flags
}
}
} }
@@ -0,0 +1,5 @@
package dev.meloda.fast.model
enum class ConversationsFilter {
ALL, UNREAD, ARCHIVE, BUSINESS_NOTIFY
}
@@ -4,5 +4,5 @@ import dev.meloda.fast.model.api.domain.VkUser
data class FriendsInfo( data class FriendsInfo(
val friends: List<VkUser>, val friends: List<VkUser>,
val onlineFriendsIds: List<Int> val onlineFriendsIds: List<Long>
) )
@@ -21,7 +21,10 @@ enum class LongPollEvent {
MARKED_AS_SPAM, MARKED_AS_SPAM,
MARKED_AS_NOT_SPAM, MARKED_AS_NOT_SPAM,
MESSAGE_DELETED, MESSAGE_DELETED,
MESSAGE_UPDATED,
MESSAGE_CACHE_CLEAR,
MESSAGE_RESTORED, MESSAGE_RESTORED,
AUDIO_MESSAGE_LISTENED, AUDIO_MESSAGE_LISTENED,
CHAT_CLEARED CHAT_CLEARED,
CHAT_ARCHIVED
} }
@@ -1,39 +1,47 @@
package dev.meloda.fast.model package dev.meloda.fast.model
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
sealed interface LongPollParsedEvent { sealed interface LongPollParsedEvent {
data class NewMessage(val message: VkMessage) : LongPollParsedEvent data class NewMessage(
val message: VkMessage,
val inArchive: Boolean
) : LongPollParsedEvent
data class MessageEdited(val message: VkMessage) : LongPollParsedEvent data class MessageEdited(val message: VkMessage) : LongPollParsedEvent
data class MessageUpdated(val message: VkMessage) : LongPollParsedEvent
data class MessageCacheClear(val message: VkMessage) : LongPollParsedEvent
data class IncomingMessageRead( data class IncomingMessageRead(
val peerId: Int, val peerId: Long,
val messageId: Int, val cmId: Long,
val unreadCount: Int, val unreadCount: Int,
) : LongPollParsedEvent ) : LongPollParsedEvent
data class OutgoingMessageRead( data class OutgoingMessageRead(
val peerId: Int, val peerId: Long,
val messageId: Int, val cmId: Long,
val unreadCount: Int, val unreadCount: Int,
) : LongPollParsedEvent ) : LongPollParsedEvent
data class ChatMajorChanged( data class ChatMajorChanged(
val peerId: Int, val peerId: Long,
val majorId: Int, val majorId: Int,
) : LongPollParsedEvent ) : LongPollParsedEvent
data class ChatMinorChanged( data class ChatMinorChanged(
val peerId: Int, val peerId: Long,
val minorId: Int val minorId: Int
) : LongPollParsedEvent ) : LongPollParsedEvent
data class Interaction( data class Interaction(
val interactionType: InteractionType, val interactionType: InteractionType,
val peerId: Int, val peerId: Long,
val userIds: List<Int>, val userIds: List<Long>,
val totalCount: Int, val totalCount: Int,
val timestamp: Int val timestamp: Int
) : LongPollParsedEvent ) : LongPollParsedEvent
@@ -49,14 +57,14 @@ sealed interface LongPollParsedEvent {
) : LongPollParsedEvent ) : LongPollParsedEvent
data class MessageMarkedAsImportant( data class MessageMarkedAsImportant(
val peerId: Int, val peerId: Long,
val messageId: Int, val cmId: Long,
val marked: Boolean val marked: Boolean
) : LongPollParsedEvent ) : LongPollParsedEvent
data class MessageMarkedAsSpam( data class MessageMarkedAsSpam(
val peerId: Int, val peerId: Long,
val messageId: Int val cmId: Long
) : LongPollParsedEvent ) : LongPollParsedEvent
data class MessageMarkedAsNotSpam( data class MessageMarkedAsNotSpam(
@@ -64,8 +72,8 @@ sealed interface LongPollParsedEvent {
) : LongPollParsedEvent ) : LongPollParsedEvent
data class MessageDeleted( data class MessageDeleted(
val peerId: Int, val peerId: Long,
val messageId: Int, val cmId: Long,
val forAll: Boolean val forAll: Boolean
) : LongPollParsedEvent ) : LongPollParsedEvent
@@ -74,12 +82,17 @@ sealed interface LongPollParsedEvent {
) : LongPollParsedEvent ) : LongPollParsedEvent
data class AudioMessageListened( data class AudioMessageListened(
val peerId: Int, val peerId: Long,
val messageId: Int val cmId: Long
) : LongPollParsedEvent ) : LongPollParsedEvent
data class ChatCleared( data class ChatCleared(
val peerId: Int, val peerId: Long,
val toMessageId: Int val toCmId: Long
): LongPollParsedEvent ) : LongPollParsedEvent
data class ChatArchived(
val conversation: VkConversation,
val archived: Boolean
) : LongPollParsedEvent
} }
@@ -27,7 +27,11 @@ enum class AttachmentType(var value: String) {
AUDIO_PLAYLIST("audio_playlist"), AUDIO_PLAYLIST("audio_playlist"),
PODCAST("podcast"), PODCAST("podcast"),
NARRATIVE("narrative"), NARRATIVE("narrative"),
ARTICLE("article"); ARTICLE("article"),
VIDEO_MESSAGE("video_message"),
GROUP_CHAT_STICKER("ugc_sticker"),
STICKER_PACK_PREVIEW("sticker_pack_preview")
;
fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE) fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE)
@@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkArticleData( data class VkArticleData(
@Json(name = "id") val id: Int @Json(name = "id") val id: Long
) : VkAttachmentData { ) : VkAttachmentData {
fun toDomain(): VkArticleDomain = VkArticleDomain( fun toDomain(): VkArticleDomain = VkArticleDomain(
@@ -1,15 +1,15 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAttachmentHistoryMessageData( data class VkAttachmentHistoryMessageData(
@Json(name = "message_id") val messageId: Int, @Json(name = "message_id") val messageId: Long,
@Json(name = "date") val date: Int, @Json(name = "date") val date: Int,
@Json(name = "cmid") val conversationMessageId: Int, @Json(name = "cmid") val conversationMessageId: Long,
@Json(name = "from_id") val fromId: Int, @Json(name = "from_id") val fromId: Long,
@Json(name = "position") val position: Int, @Json(name = "position") val position: Int,
@Json(name = "attachment") val attachment: VkAttachmentItemData @Json(name = "attachment") val attachment: VkAttachmentItemData
) { ) {
@@ -1,9 +1,9 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkUnknownAttachment
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkUnknownAttachment
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAttachmentItemData( data class VkAttachmentItemData(
@@ -32,7 +32,10 @@ data class VkAttachmentItemData(
@Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?, @Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?,
@Json(name = "podcast") val podcast: VkPodcastData?, @Json(name = "podcast") val podcast: VkPodcastData?,
@Json(name = "narrative") val narrative: VkNarrativeData?, @Json(name = "narrative") val narrative: VkNarrativeData?,
@Json(name = "article") val article: VkArticleData? @Json(name = "article") val article: VkArticleData?,
@Json(name = "video_message") val videoMessage: VkVideoMessageData?,
@Json(name = "ugc_sticker") val groupSticker: VkGroupStickerData?,
@Json(name = "sticker_pack_preview") val stickerPackPreview: VkStickerPackPreviewData?
) { ) {
fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) { fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) {
AttachmentType.UNKNOWN -> VkUnknownAttachment AttachmentType.UNKNOWN -> VkUnknownAttachment
@@ -60,5 +63,8 @@ data class VkAttachmentItemData(
AttachmentType.PODCAST -> podcast?.toDomain() AttachmentType.PODCAST -> podcast?.toDomain()
AttachmentType.NARRATIVE -> narrative?.toDomain() AttachmentType.NARRATIVE -> narrative?.toDomain()
AttachmentType.ARTICLE -> article?.toDomain() AttachmentType.ARTICLE -> article?.toDomain()
AttachmentType.VIDEO_MESSAGE -> videoMessage?.toDomain()
AttachmentType.GROUP_CHAT_STICKER -> groupSticker?.toDomain()
AttachmentType.STICKER_PACK_PREVIEW -> stickerPackPreview?.toDomain()
} ?: VkUnknownAttachment } ?: VkUnknownAttachment
} }
@@ -1,31 +1,31 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkAudioDomain
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkAudioDomain
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAudioData( data class VkAudioData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "artist") val artist: String, @Json(name = "artist") val artist: String,
@Json(name = "duration") val duration: Int, @Json(name = "duration") val duration: Int,
@Json(name = "url") val url: String, @Json(name = "url") val url: String,
@Json(name = "date") val date: Int, @Json(name = "date") val date: Int,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "access_key") val accessKey: String?, @Json(name = "access_key") val accessKey: String?,
@Json(name = "is_explicit") val isExplicit: Boolean, @Json(name = "is_explicit") val isExplicit: Boolean,
@Json(name = "is_focus_track") val isFocusTrack: Boolean, @Json(name = "is_focus_track") val isFocusTrack: Boolean,
@Json(name = "is_licensed") val isLicensed: Boolean?, @Json(name = "is_licensed") val isLicensed: Boolean?,
@Json(name = "genre_id") val genreId: Int?, @Json(name = "genre_id") val genreId: Long?,
@Json(name = "album") val album: Album?, @Json(name = "album") val album: Album?,
) : VkAttachmentData { ) : VkAttachmentData {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Album( data class Album(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "access_key") val accessKey: String, @Json(name = "access_key") val accessKey: String,
@Json(name = "thumb") val thumb: Thumb? @Json(name = "thumb") val thumb: Thumb?
) { ) {
@@ -6,8 +6,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAudioMessageData( data class VkAudioMessageData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "duration") val duration: Int, @Json(name = "duration") val duration: Int,
@Json(name = "waveform") val waveform: List<Int>, @Json(name = "waveform") val waveform: List<Int>,
@Json(name = "link_ogg") val linkOgg: String, @Json(name = "link_ogg") val linkOgg: String,
@@ -6,8 +6,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkAudioPlaylistData( data class VkAudioPlaylistData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "type") val type: Int, @Json(name = "type") val type: Int,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "description") val description: String, @Json(name = "description") val description: String,
@@ -6,8 +6,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkCallData( data class VkCallData(
@Json(name = "initiator_id") val initiatorId: Int, @Json(name = "initiator_id") val initiatorId: Long,
@Json(name = "receiver_id") val receiverId: Int, @Json(name = "receiver_id") val receiverId: Long,
@Json(name = "state") val state: String, @Json(name = "state") val state: String,
@Json(name = "time") val time: Int, @Json(name = "time") val time: Int,
@Json(name = "duration") val duration: Int, @Json(name = "duration") val duration: Int,
@@ -8,9 +8,9 @@ import com.squareup.moshi.JsonClass
data class VkChatData( data class VkChatData(
@Json(name = "type") val type: String, @Json(name = "type") val type: String,
@Json(name = "val title") val title: String, @Json(name = "val title") val title: String,
@Json(name = "admin_id") val adminId: Int, @Json(name = "admin_id") val adminId: Long,
@Json(name = "members_count") val membersCount: Int, @Json(name = "members_count") val membersCount: Int,
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "photo_50") val photo50: String, @Json(name = "photo_50") val photo50: String,
@Json(name = "photo_100") val photo100: String, @Json(name = "photo_100") val photo100: String,
@Json(name = "photo_200") val photo200: String, @Json(name = "photo_200") val photo200: String,
@@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkChatMemberData( data class VkChatMemberData(
@Json(name = "member_id") val memberId: Int, @Json(name = "member_id") val memberId: Long,
@Json(name = "invited_by") val invitedBy: Int, @Json(name = "invited_by") val invitedBy: Int,
@Json(name = "join_date") val joinDate: Int, @Json(name = "join_date") val joinDate: Int,
@Json(name = "is_admin") val isAdmin: Boolean?, @Json(name = "is_admin") val isAdmin: Boolean?,
@@ -6,10 +6,10 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkContactData( data class VkContactData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "can_write") val canWrite: Boolean, @Json(name = "can_write") val canWrite: Boolean,
@Json(name = "user_id") val userId: Int, @Json(name = "user_id") val userId: Long,
@Json(name = "last_seen_status") val lastSeenStatus: String?, @Json(name = "last_seen_status") val lastSeenStatus: String?,
@Json(name = "photo_50") val photo50: String?, @Json(name = "photo_50") val photo50: String?,
@Json(name = "calls_id") val callsId: String @Json(name = "calls_id") val callsId: String
@@ -1,21 +1,21 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.PeerType import dev.meloda.fast.model.api.PeerType
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkConversationData( data class VkConversationData(
@Json(name = "peer") val peer: Peer, @Json(name = "peer") val peer: Peer,
@Json(name = "last_message_id") val lastMessageId: Int?, @Json(name = "last_message_id") val lastMessageId: Long?,
@Json(name = "in_read") val inRead: Int, @Json(name = "in_read") val inRead: Long,
@Json(name = "out_read") val outRead: Int, @Json(name = "out_read") val outRead: Long,
@Json(name = "in_read_cmid") val inReadConversationMessageId: Int, @Json(name = "in_read_cmid") val inReadConversationMessageId: Long,
@Json(name = "out_read_cmid") val outReadConversationMessageId: Int, @Json(name = "out_read_cmid") val outReadConversationMessageId: Long,
@Json(name = "sort_id") val sortId: SortId, @Json(name = "sort_id") val sortId: SortId,
@Json(name = "last_conversation_message_id") val lastConversationMessageId: Int, @Json(name = "last_conversation_message_id") val lastConversationMessageId: Long,
@Json(name = "is_marked_unread") val isMarkedUnread: Boolean, @Json(name = "is_marked_unread") val isMarkedUnread: Boolean,
@Json(name = "important") val important: Boolean, @Json(name = "important") val important: Boolean,
@Json(name = "push_settings") val pushSettings: PushSettings?, @Json(name = "push_settings") val pushSettings: PushSettings?,
@@ -25,13 +25,14 @@ data class VkConversationData(
@Json(name = "chat_settings") val chatSettings: ChatSettings?, @Json(name = "chat_settings") val chatSettings: ChatSettings?,
@Json(name = "call_in_progress") val callInProgress: CallInProgress?, @Json(name = "call_in_progress") val callInProgress: CallInProgress?,
@Json(name = "unread_count") val unreadCount: Int?, @Json(name = "unread_count") val unreadCount: Int?,
@Json(name = "is_archived") val isArchived: Boolean?
) { ) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Peer( data class Peer(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "type") val type: String, @Json(name = "type") val type: String,
@Json(name = "local_id") val localId: Int, @Json(name = "local_id") val localId: Long,
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@@ -55,7 +56,7 @@ data class VkConversationData(
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ChatSettings( data class ChatSettings(
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "state") val state: String, @Json(name = "state") val state: String,
@Json(name = "acl") val acl: Acl, @Json(name = "acl") val acl: Acl,
@@ -119,7 +120,7 @@ data class VkConversationData(
photo200 = chatSettings?.photo?.photo200, photo200 = chatSettings?.photo?.photo200,
isCallInProgress = callInProgress != null, isCallInProgress = callInProgress != null,
isPhantom = chatSettings?.isDisappearing == true, isPhantom = chatSettings?.isDisappearing == true,
lastConversationMessageId = lastConversationMessageId, lastCmId = lastConversationMessageId,
inRead = inRead, inRead = inRead,
outRead = outRead, outRead = outRead,
lastMessageId = lastMessageId, lastMessageId = lastMessageId,
@@ -140,5 +141,6 @@ data class VkConversationData(
pinnedMessage = chatSettings?.pinnedMessage?.mapToDomain(), pinnedMessage = chatSettings?.pinnedMessage?.mapToDomain(),
user = null, user = null,
group = null, group = null,
isArchived = isArchived == true
) )
} }
@@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkCuratorData( data class VkCuratorData(
val id: Int, val id: Long,
val name: String, val name: String,
val description: String, val description: String,
val url: String, val url: String,
@@ -7,7 +7,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkEventData( data class VkEventData(
@Json(name = "button_text") val buttonText: String, @Json(name = "button_text") val buttonText: String,
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "is_favorite") val isFavorite: Boolean, @Json(name = "is_favorite") val isFavorite: Boolean,
@Json(name = "text") val text: String, @Json(name = "text") val text: String,
@Json(name = "address") val address: String, @Json(name = "address") val address: String,
@@ -1,13 +1,13 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkFileDomain
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkFileDomain
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkFileData( data class VkFileData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "size") val size: Int, @Json(name = "size") val size: Int,
@Json(name = "ext") val extension: String, @Json(name = "ext") val extension: String,
@@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkGiftData( data class VkGiftData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "thumb_256") val thumb256: String?, @Json(name = "thumb_256") val thumb256: String?,
@Json(name = "thumb_96") val thumb96: String?, @Json(name = "thumb_96") val thumb96: String?,
@Json(name = "thumb_48") val thumb48: String @Json(name = "thumb_48") val thumb48: String
@@ -1,13 +1,13 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkGraffitiDomain
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkGraffitiDomain
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkGraffitiData( data class VkGraffitiData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "url") val url: String, @Json(name = "url") val url: String,
@Json(name = "width") val width: Int, @Json(name = "width") val width: Int,
@Json(name = "height") val height: Int, @Json(name = "height") val height: Int,
@@ -1,12 +1,12 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkGroupCallDomain
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkGroupCallDomain
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkGroupCallData( data class VkGroupCallData(
@Json(name = "initiator_id") val initiatorId: Int, @Json(name = "initiator_id") val initiatorId: Long,
@Json(name = "join_link") val joinLink: String, @Json(name = "join_link") val joinLink: String,
@Json(name = "participants") val participants: Participants @Json(name = "participants") val participants: Participants
) { ) {
@@ -7,7 +7,7 @@ import kotlin.math.abs
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkGroupData( data class VkGroupData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "screen_name") val screenName: String, @Json(name = "screen_name") val screenName: String,
@Json(name = "is_closed") val isClosed: Int?, @Json(name = "is_closed") val isClosed: Int?,
@@ -0,0 +1,27 @@
package dev.meloda.fast.model.api.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkGroupStickerDomain
@JsonClass(generateAdapter = true)
data class VkGroupStickerData(
val id: Long,
val owner_id: Long,
val pack_id: Long?,
val status: String?,
val is_deleted: Boolean?,
val images: List<Image>?
): VkAttachmentData {
@JsonClass(generateAdapter = true)
data class Image(
@Json(name = "width") val width: Int,
@Json(name = "height") val height: Int,
@Json(name = "url") val url: String
)
fun toDomain(): VkGroupStickerDomain = VkGroupStickerDomain(
id = id
)
}
@@ -2,22 +2,23 @@ package dev.meloda.fast.model.api.data
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.FormatDataType
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkMessageData( data class VkMessageData(
@Json(name = "id") val id: Int?, @Json(name = "id") val id: Long?,
@Json(name = "peer_id") val peerId: Int?, @Json(name = "peer_id") val peerId: Long?,
@Json(name = "date") val date: Int, @Json(name = "date") val date: Int,
@Json(name = "from_id") val fromId: Int, @Json(name = "from_id") val fromId: Long,
@Json(name = "out") val out: Int?, @Json(name = "out") val out: Int?,
@Json(name = "text") val text: String, @Json(name = "text") val text: String,
@Json(name = "conversation_message_id") val conversationMessageId: Int, @Json(name = "conversation_message_id") val cmId: Long,
@Json(name = "fwd_messages") val fwdMessages: List<VkMessageData>? = emptyList(), @Json(name = "fwd_messages") val fwdMessages: List<VkMessageData>? = emptyList(),
@Json(name = "important") val important: Boolean = false, @Json(name = "important") val important: Boolean?,
@Json(name = "random_id") val randomId: Int = 0, @Json(name = "random_id") val randomId: Long?,
@Json(name = "attachments") val attachments: List<VkAttachmentItemData> = emptyList(), @Json(name = "attachments") val attachments: List<VkAttachmentItemData> = emptyList(),
@Json(name = "is_hidden") val isHidden: Boolean = false, @Json(name = "is_hidden") val isHidden: Boolean?,
@Json(name = "payload") val payload: String?, @Json(name = "payload") val payload: String?,
@Json(name = "geo") val geo: Geo?, @Json(name = "geo") val geo: Geo?,
@Json(name = "action") val action: Action?, @Json(name = "action") val action: Action?,
@@ -25,7 +26,8 @@ data class VkMessageData(
@Json(name = "reply_message") val replyMessage: VkMessageData?, @Json(name = "reply_message") val replyMessage: VkMessageData?,
@Json(name = "update_time") val updateTime: Int?, @Json(name = "update_time") val updateTime: Int?,
@Json(name = "is_pinned") val isPinned: Boolean?, @Json(name = "is_pinned") val isPinned: Boolean?,
@Json(name = "pinned_at") val pinnedAt: Int? @Json(name = "pinned_at") val pinnedAt: Int?,
@Json(name = "format_data") val formatData: FormatData?
) { ) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@@ -52,29 +54,58 @@ data class VkMessageData(
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Action( data class Action(
@Json(name = "type") val type: String, @Json(name = "type") val type: String,
@Json(name = "member_id") val memberId: Int?, @Json(name = "member_id") val memberId: Long?,
@Json(name = "text") val text: String?, @Json(name = "text") val text: String?,
@Json(name = "conversation_message_id") val conversationMessageId: Int?, @Json(name = "conversation_message_id") val conversationMessageId: Long?,
@Json(name = "message") val message: String? @Json(name = "message") val message: String?
) )
@JsonClass(generateAdapter = true)
data class FormatData(
@Json(name = "version") val version: String,
@Json(name = "items") val items: List<Item>
) {
@JsonClass(generateAdapter = true)
data class Item(
@Json(name = "offset") val offset: Int,
@Json(name = "length") val length: Int,
@Json(name = "type") val type: String,
@Json(name = "url") val url: String?
)
fun asDomain(): VkMessage.FormatData = VkMessage.FormatData(
version = version,
items = items.mapNotNull { item ->
FormatDataType.parse(item.type)?.let { type ->
VkMessage.FormatData.Item(
offset = item.offset,
length = item.length,
type = type,
url = item.url
)
}
}
)
}
} }
fun VkMessageData.asDomain(): VkMessage = VkMessage( fun VkMessageData.asDomain(): VkMessage = VkMessage(
id = id ?: -1, id = id ?: -1,
conversationMessageId = conversationMessageId, cmId = cmId,
text = text.ifBlank { null }, text = text.ifBlank { null },
isOut = out == 1, isOut = out == 1,
peerId = peerId ?: -1, peerId = peerId ?: -1,
fromId = fromId, fromId = fromId,
date = date, date = date,
randomId = randomId, randomId = randomId ?: 0,
action = VkMessage.Action.parse(action?.type), action = VkMessage.Action.parse(action?.type),
actionMemberId = action?.memberId, actionMemberId = action?.memberId,
actionText = action?.text, actionText = action?.text,
actionConversationMessageId = action?.conversationMessageId, actionConversationMessageId = action?.conversationMessageId,
actionMessage = action?.message, actionMessage = action?.message,
geoType = geo?.type, geoType = geo?.type,
isImportant = important, isImportant = important ?: false,
updateTime = updateTime, updateTime = updateTime,
forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain), forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain),
attachments = attachments.map(VkAttachmentItemData::toDomain), attachments = attachments.map(VkAttachmentItemData::toDomain),
@@ -84,5 +115,7 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
actionUser = null, actionUser = null,
actionGroup = null, actionGroup = null,
pinnedAt = pinnedAt, pinnedAt = pinnedAt,
isPinned = isPinned == true isPinned = isPinned == true,
formatData = formatData?.asDomain(),
isSpam = false
) )
@@ -16,9 +16,9 @@ data class VkMiniAppData(
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class App( data class App(
@Json(name = "type") val type: String, @Json(name = "type") val type: String,
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "author_owner_id") val authorOwnerId: Int, @Json(name = "author_owner_id") val authorOwnerid: Long,
@Json(name = "is_favorite") val isFavorite: Boolean, @Json(name = "is_favorite") val isFavorite: Boolean,
@Json(name = "share_url") val shareUrl: String, @Json(name = "share_url") val shareUrl: String,
@Json(name = "webview_url") val webViewUrl: String, @Json(name = "webview_url") val webViewUrl: String,
@@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkNarrativeData( data class VkNarrativeData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "title") val title: String? @Json(name = "title") val title: String?
) : VkAttachmentData { ) : VkAttachmentData {
@@ -6,18 +6,18 @@ import dev.meloda.fast.model.api.domain.VkPhotoDomain
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkPhotoData( data class VkPhotoData(
@Json(name = "album_id") val albumId: Int, @Json(name = "album_id") val albumId: Long,
@Json(name = "date") val date: Int?, @Json(name = "date") val date: Int?,
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "has_tags") val hasTags: Boolean?, @Json(name = "has_tags") val hasTags: Boolean?,
@Json(name = "access_key") val accessKey: String?, @Json(name = "access_key") val accessKey: String?,
@Json(name = "sizes") val sizes: List<Size>, @Json(name = "sizes") val sizes: List<Size>,
@Json(name = "text") val text: String?, @Json(name = "text") val text: String?,
@Json(name = "user_id") val userId: Int?, @Json(name = "user_id") val userId: Long?,
@Json(name = "lat") val lat: Double?, @Json(name = "lat") val lat: Double?,
@Json(name = "long") val long: Double?, @Json(name = "long") val long: Double?,
@Json(name = "post_id") val postId: Int? @Json(name = "post_id") val postId: Long?
) : VkAttachmentData { ) : VkAttachmentData {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@@ -1,21 +1,21 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkMessage
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkMessage
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkPinnedMessageData( data class VkPinnedMessageData(
@Json(name = "id") val id: Int?, @Json(name = "id") val id: Long?,
@Json(name = "peer_id") val peerId: Int?, @Json(name = "peer_id") val peerId: Long?,
@Json(name = "date") val date: Int, @Json(name = "date") val date: Int,
@Json(name = "from_id") val fromId: Int, @Json(name = "from_id") val fromId: Long,
@Json(name = "out") val out: Boolean?, @Json(name = "out") val out: Boolean?,
@Json(name = "text") val text: String, @Json(name = "text") val text: String,
@Json(name = "conversation_message_id") val conversationMessageId: Int, @Json(name = "conversation_message_id") val conversationMessageId: Long,
@Json(name = "fwd_messages") val forwards: List<VkMessageData>?, @Json(name = "fwd_messages") val forwards: List<VkMessageData>?,
@Json(name = "important") val important: Boolean = false, @Json(name = "important") val important: Boolean = false,
@Json(name = "random_id") val randomId: Int = 0, @Json(name = "random_id") val randomId: Long = 0,
@Json(name = "attachments") val attachments: List<VkAttachmentItemData>?, @Json(name = "attachments") val attachments: List<VkAttachmentItemData>?,
@Json(name = "is_hidden") val isHidden: Boolean = false, @Json(name = "is_hidden") val isHidden: Boolean = false,
@Json(name = "payload") val payload: String?, @Json(name = "payload") val payload: String?,
@@ -28,7 +28,7 @@ data class VkPinnedMessageData(
fun mapToDomain(): VkMessage = VkMessage( fun mapToDomain(): VkMessage = VkMessage(
id = id ?: -1, id = id ?: -1,
conversationMessageId = conversationMessageId, cmId = conversationMessageId,
text = text.ifBlank { null }, text = text.ifBlank { null },
isOut = out == true, isOut = out == true,
peerId = peerId ?: -1, peerId = peerId ?: -1,
@@ -54,5 +54,7 @@ data class VkPinnedMessageData(
actionGroup = null, actionGroup = null,
pinnedAt = null, pinnedAt = null,
isPinned = true, isPinned = true,
isSpam = false,
formatData = null,
) )
} }
@@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkPodcastData( data class VkPodcastData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "artist") val artist: String, @Json(name = "artist") val artist: String,
// ... other fields // ... other fields
@@ -1,13 +1,13 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkPollDomain
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkPollDomain
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkPollData( data class VkPollData(
val multiple: Boolean, val multiple: Boolean,
val id: Int, val id: Long,
val votes: Int, val votes: Int,
val anonymous: Boolean?, val anonymous: Boolean?,
val closed: Boolean, val closed: Boolean,
@@ -18,24 +18,24 @@ data class VkPollData(
@Json(name = "can_report") val canReport: Boolean, @Json(name = "can_report") val canReport: Boolean,
@Json(name = "can_share") val canShare: Boolean, @Json(name = "can_share") val canShare: Boolean,
val created: Int, val created: Int,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
val question: String, val question: String,
@Json(name = "disable_unvote") val disableUnvote: Boolean, @Json(name = "disable_unvote") val disableUnvote: Boolean,
val friends: List<Friend>?, val friends: List<Friend>?,
@Json(name = "embed_hash") val embedHash: String, @Json(name = "embed_hash") val embedHash: String,
val answers: List<Answer>, val answers: List<Answer>,
@Json(name = "author_id") val authorId: Int?, @Json(name = "author_id") val authorId: Long?,
val background: Background? val background: Background?
) { ) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Friend( data class Friend(
val id: Int val id: Long
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Answer( data class Answer(
val id: Int, val id: Long,
val rate: Double, val rate: Double,
val text: String, val text: String,
val votes: Int val votes: Int
@@ -45,7 +45,7 @@ data class VkPollData(
data class Background( data class Background(
val angle: Int, val angle: Int,
val color: String, val color: String,
val id: Int, val id: Long,
val name: String, val name: String,
val type: String, val type: String,
val points: List<Point> val points: List<Point>
@@ -1,15 +1,15 @@
package dev.meloda.fast.model.api.data package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkStickerDomain
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkStickerDomain
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkStickerData( data class VkStickerData(
@Json(name = "product_id") val productId: Int, @Json(name = "product_id") val productId: Long,
@Json(name = "sticker_id") val stickerId: Int, @Json(name = "sticker_id") val stickerId: Long,
@Json(name = "images") val images: List<Image>, @Json(name = "images") val images: List<Image>?,
@Json(name = "images_with_background") val imagesWithBackground: List<Image>, @Json(name = "images_with_background") val imagesWithBackground: List<Image>?,
@Json(name = "animation_url") val animationUrl: String?, @Json(name = "animation_url") val animationUrl: String?,
@Json(name = "animations") val animations: List<Animation>? @Json(name = "animations") val animations: List<Animation>?
) { ) {
@@ -0,0 +1,33 @@
package dev.meloda.fast.model.api.data
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkStickerPackPreviewDomain
@JsonClass(generateAdapter = true)
data class VkStickerPackPreviewData(
val id: Long,
val title: String,
val description: String?,
val author: String?,
val icon: Icon?,
val price: Price?,
val can_purchase: Boolean,
val url: String
) : VkAttachmentData {
@JsonClass(generateAdapter = true)
data class Icon(
val base_url: String
)
@JsonClass(generateAdapter = true)
data class Price(
val current: Long,
val regular: Long
)
fun toDomain(): VkStickerPackPreviewDomain = VkStickerPackPreviewDomain(
id = id
)
}
@@ -5,8 +5,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkStoryData( data class VkStoryData(
val id: Int, val id: Long,
val owner_id: Int, val owner_id: Long,
val access_key: String?, val access_key: String?,
val can_comment: Int?, val can_comment: Int?,
val can_reply: Int?, val can_reply: Int?,
@@ -7,7 +7,7 @@ import dev.meloda.fast.model.api.domain.VkUser
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkUserData( data class VkUserData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "first_name") val firstName: String, @Json(name = "first_name") val firstName: String,
@Json(name = "last_name") val lastName: String, @Json(name = "last_name") val lastName: String,
@Json(name = "can_access_closed") val canAccessClosed: Boolean, @Json(name = "can_access_closed") val canAccessClosed: Boolean,
@@ -18,8 +18,8 @@ data class VkUserData(
@Json(name = "photo_100") val photo100: String?, @Json(name = "photo_100") val photo100: String?,
@Json(name = "photo_200") val photo200: String?, @Json(name = "photo_200") val photo200: String?,
@Json(name = "photo_400_orig") val photo400Orig: String?, @Json(name = "photo_400_orig") val photo400Orig: String?,
@Json(name = "online") val online: Int?,
@Json(name = "online_info") val onlineInfo: OnlineInfo?, @Json(name = "online_info") val onlineInfo: OnlineInfo?,
@Json(name = "last_seen") val lastSeen: LastSeen?,
@Json(name = "screen_name") val screenName: String, @Json(name = "screen_name") val screenName: String,
@Json(name = "bdate") val birthday: String? @Json(name = "bdate") val birthday: String?
//...other fields //...other fields
@@ -31,25 +31,26 @@ data class VkUserData(
@Json(name = "status") val status: String?, @Json(name = "status") val status: String?,
@Json(name = "last_seen") val lastSeen: Int?, @Json(name = "last_seen") val lastSeen: Int?,
@Json(name = "is_online") val isOnline: Boolean?, @Json(name = "is_online") val isOnline: Boolean?,
@Json(name = "online_mobile") val onlineMobile: Boolean?, @Json(name = "online_mobile") val isOnlineMobile: Boolean?,
@Json(name = "app_id") val appId: Int? @Json(name = "app_id") val appId: Long?
)
@JsonClass(generateAdapter = true)
data class LastSeen(
@Json(name = "platform") val platform: Int,
@Json(name = "time") val time: Int
) )
fun mapToDomain() = VkUser( fun mapToDomain() = VkUser(
id = id, id = id,
firstName = firstName, firstName = firstName,
lastName = lastName, lastName = lastName,
// TODO: 05/05/2024, Danil Nikolaev: improve onlineStatus = parseUserOnlineState(
onlineStatus = when { isOnline = onlineInfo?.isOnline,
online != 1 -> OnlineStatus.Offline isOnlineMobile = onlineInfo?.isOnlineMobile,
onlineInfo?.onlineMobile == true -> { status = onlineInfo?.status,
OnlineStatus.OnlineMobile(appId = onlineInfo.appId) appId = onlineInfo?.appId
} ),
else -> {
OnlineStatus.Online(appId = onlineInfo?.appId)
}
},
photo50 = photo50, photo50 = photo50,
photo100 = photo100, photo100 = photo100,
photo200 = photo200, photo200 = photo200,
@@ -59,3 +60,26 @@ data class VkUserData(
birthday = birthday birthday = birthday
) )
} }
fun parseUserOnlineState(
isOnline: Boolean?,
isOnlineMobile: Boolean?,
status: String?,
appId: Long?
): OnlineStatus {
return when {
isOnlineMobile == true -> OnlineStatus.OnlineMobile(appId)
isOnline == true -> OnlineStatus.Online(appId)
status != null -> {
when (status) {
"last_week" -> OnlineStatus.LastWeek
"last_month" -> OnlineStatus.LastMonth
else -> OnlineStatus.Recently
}
}
else -> OnlineStatus.Offline
}
}
@@ -6,7 +6,7 @@ import dev.meloda.fast.model.api.domain.VkVideoDomain
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkVideoData( data class VkVideoData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "title") val title: String, @Json(name = "title") val title: String,
@Json(name = "width") val width: Int?, @Json(name = "width") val width: Int?,
@Json(name = "height") val height: Int?, @Json(name = "height") val height: Int?,
@@ -19,7 +19,7 @@ data class VkVideoData(
@Json(name = "type") val type: String, @Json(name = "type") val type: String,
@Json(name = "views") val views: Int, @Json(name = "views") val views: Int,
@Json(name = "access_key") val accessKey: String?, @Json(name = "access_key") val accessKey: String?,
@Json(name = "owner_id") val ownerId: Int, @Json(name = "owner_id") val ownerId: Long,
@Json(name = "is_favorite") val isFavorite: Boolean?, @Json(name = "is_favorite") val isFavorite: Boolean?,
@Json(name = "image") val image: List<Image>?, @Json(name = "image") val image: List<Image>?,
@Json(name = "first_frame") val firstFrame: List<FirstFrame>?, @Json(name = "first_frame") val firstFrame: List<FirstFrame>?,
@@ -0,0 +1,78 @@
package dev.meloda.fast.model.api.data
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkVideoMessageDomain
@JsonClass(generateAdapter = true)
data class VkVideoMessageData(
val id: Long,
val access_key: String?,
val can_add: Int?,
val can_dislike: Int?,
val can_download: Int?,
val can_play_in_background: Int?,
val date: Int?,
val description: String?,
val duration: Int?,
val files: Files?,
val first_frame: List<FirstFrame>?,
val height: Int?,
val image: List<Image>?,
val is_author: Boolean?,
val is_favorite: Boolean?,
val is_from_message: Int?,
val need_mediascope_stat: Boolean?,
val ov_id: String?,
val owner_id: Long?,
val player: String?,
val processing: Int?,
val repeat: Int?,
val response_type: String?,
val shape_id: Long?,
val timeline_thumbs: TimelineThumbs?,
val title: String?,
val track_code: String?,
val transcript_state: String?,
val type: String?,
val views: Int?,
val width: Int?,
) : VkAttachmentData {
@JsonClass(generateAdapter = true)
data class Files(
val failover_host: String?,
val mp4_240: String?,
val mp4_480: String?,
)
@JsonClass(generateAdapter = true)
data class FirstFrame(
val height: Int?,
val url: String?,
val width: Int?,
)
@JsonClass(generateAdapter = true)
data class Image(
val height: Int?,
val url: String?,
val width: Int?,
val with_padding: Int?,
)
@JsonClass(generateAdapter = true)
data class TimelineThumbs(
val count_per_image: Int?,
val count_per_row: Int?,
val count_total: Int?,
val frame_height: Int?,
val frame_width: Double?,
val frequency: Int?,
val is_uv: Boolean?,
val links: List<String>?,
)
fun toDomain(): VkVideoMessageDomain = VkVideoMessageDomain(
id = id
)
}
@@ -6,9 +6,9 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkWallData( data class VkWallData(
@Json(name = "id") val id: Int, @Json(name = "id") val id: Long,
@Json(name = "from_id") val from_id: Int, @Json(name = "from_id") val from_id: Long,
@Json(name = "to_id") val to_id: Int, @Json(name = "to_id") val to_id: Long,
@Json(name = "date") val date: Int, @Json(name = "date") val date: Int,
@Json(name = "text") val text: String, @Json(name = "text") val text: String,
@Json(name = "attachments") val attachments: List<VkAttachmentItemData>?, @Json(name = "attachments") val attachments: List<VkAttachmentItemData>?,

Some files were not shown because too many files have changed in this diff Show More