├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── src ├── main │ └── kotlin │ │ ├── dev │ │ └── baseio │ │ │ └── slackserver │ │ │ ├── data │ │ │ ├── models │ │ │ │ ├── IDataMap.kt │ │ │ │ ├── SKEncryptedMessage.kt │ │ │ │ ├── SkAuthUser.kt │ │ │ │ ├── SKLastMessage.kt │ │ │ │ ├── SKUserPushToken.kt │ │ │ │ ├── SkWorkspace.kt │ │ │ │ ├── SkUser.kt │ │ │ │ ├── SKUserPublicKey.kt │ │ │ │ ├── SkMessage.kt │ │ │ │ └── SkChannel.kt │ │ │ ├── sources │ │ │ │ ├── AuthDataSource.kt │ │ │ │ ├── UserPushTokenDataSource.kt │ │ │ │ ├── ChannelMemberDataSource.kt │ │ │ │ ├── MessagesDataSource.kt │ │ │ │ ├── WorkspaceDataSource.kt │ │ │ │ ├── UsersDataSource.kt │ │ │ │ └── ChannelsDataSource.kt │ │ │ └── impl │ │ │ │ ├── UserPushTokenDataSourceImpl.kt │ │ │ │ ├── AuthDataSourceImpl.kt │ │ │ │ ├── UsersDataSourceImpl.kt │ │ │ │ ├── WorkspaceDataSourceImpl.kt │ │ │ │ ├── MessagesDataSourceImpl.kt │ │ │ │ ├── ChannelMemberDataSourceImpl.kt │ │ │ │ └── ChannelsDataSourceImpl.kt │ │ │ ├── services │ │ │ ├── SlackConstants.kt │ │ │ ├── IQrCodeGenerator.kt │ │ │ ├── AuthenticationDelegate.kt │ │ │ ├── AuthService.kt │ │ │ ├── interceptors │ │ │ │ └── AuthInterceptor.kt │ │ │ ├── MessagingService.kt │ │ │ ├── QrCodeService.kt │ │ │ ├── UserService.kt │ │ │ ├── WorkspaceService.kt │ │ │ └── ChannelService.kt │ │ │ ├── communications │ │ │ ├── NotificationType.kt │ │ │ ├── PNChannelMember.kt │ │ │ ├── PNMessages.kt │ │ │ ├── PNChannel.kt │ │ │ ├── PNSender.kt │ │ │ └── SlackEmailHelper.kt │ │ │ └── DataSourcesModule.kt │ │ └── Main.kt └── test │ └── kotlin │ ├── FakeQrCodeGenerator.kt │ └── TestQRCodeService.kt ├── README.md ├── .gitmodules ├── gradlew.bat ├── .gitignore └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/slack_multiplatform_grpc_server/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "slack_multiplatform_grpc_server" 2 | 3 | include(":slack_generate_protos") 4 | include(":slack_protos") 5 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/IDataMap.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | interface IDataMap { 4 | fun provideMap():Map 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/SKEncryptedMessage.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | data class SKEncryptedMessage(val first: String, val second: String) -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/SkAuthUser.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | data class SkAuthUser( 4 | val uuid: String, 5 | val userId: String, 6 | ) -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/SKLastMessage.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | data class SKLastMessage( 4 | val channel: SkChannel, 5 | val message: SkMessage 6 | ) -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/SKUserPushToken.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | data class SKUserPushToken(val uuid: String, val userId: String, val platform: Int, val token:String) 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/SkWorkspace.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | data class SkWorkspace( 4 | val uuid: String, 5 | val name: String, 6 | val domain: String, 7 | val picUrl: String?, 8 | val modifiedTime: Long 9 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slack Kotlin gRPC server for the multiplatform client! 2 | 3 | connection.mongodb=mongodb+srv://***:***@***/?retryWrites\=true&w\=majority; 4 | GOOGLE_APPLICATION_CREDENTIALS=~/adminsdk.json; 5 | email.username=anmol.verma4@gmail.com; 6 | email.password=****; 7 | email.from=****; 8 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/SlackConstants.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services 2 | 3 | class SlackConstants { 4 | companion object { 5 | const val CIPHERTEXT_KEY: String = "CIPHERTEXT_KEY" 6 | const val KEY_ALGORITHM_KEY = "KEY_ALGORITHM_KEY" 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/sources/AuthDataSource.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.sources 2 | 3 | import dev.baseio.slackserver.data.models.SkUser 4 | 5 | 6 | interface AuthDataSource { 7 | suspend fun register(email: String, user: SkUser): SkUser? 8 | suspend fun findUser(email: String, workspaceId: String): SkUser? 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/sources/UserPushTokenDataSource.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.sources 2 | 3 | import dev.baseio.slackserver.data.models.SKUserPushToken 4 | 5 | interface UserPushTokenDataSource { 6 | suspend fun getPushTokensFor(userIds: List): List 7 | suspend fun savePushToken(toSkUserPushToken: SKUserPushToken) 8 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "slack_generate_protos"] 2 | path = slack_generate_protos 3 | url = https://github.com/oianmol/slack_multiplatform_generate_protos 4 | [submodule "slack_protos"] 5 | path = slack_protos 6 | url = https://github.com/oianmol/slack_multiplatform_protos 7 | [submodule "capillary_generate_proto"] 8 | path = capillary_generate_proto 9 | url = https://github.com/oianmol/capillary_generate_proto 10 | [submodule "protos"] 11 | path = protos 12 | url = https://github.com/oianmol/capillary_protos 13 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/SkUser.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | data class SkUser( 4 | val uuid: String, 5 | val workspaceId: String, 6 | val gender: String?, 7 | val name: String, 8 | val location: String?, 9 | val email: String, 10 | val username: String, 11 | val userSince: Long, 12 | val phone: String, 13 | val avatarUrl: String, 14 | val publicKey: SKUserPublicKey 15 | ) { 16 | companion object { 17 | const val NAME = "skUser" 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/SKUserPublicKey.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | data class SKUserPublicKey( 4 | val keyBytes: ByteArray 5 | ) { 6 | override fun equals(other: Any?): Boolean { 7 | if (this === other) return true 8 | if (javaClass != other?.javaClass) return false 9 | 10 | other as SKUserPublicKey 11 | 12 | if (!keyBytes.contentEquals(other.keyBytes)) return false 13 | 14 | return true 15 | } 16 | 17 | override fun hashCode(): Int { 18 | return keyBytes.contentHashCode() 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/sources/ChannelMemberDataSource.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.sources 2 | 3 | import dev.baseio.slackserver.data.models.SkChannel 4 | import dev.baseio.slackserver.data.models.SkChannelMember 5 | 6 | interface ChannelMemberDataSource { 7 | suspend fun isChannelExistFor(sender: String, receiver: String): SkChannel? 8 | suspend fun addMembers(listOf: List) 9 | suspend fun getMembers(workspaceId: String, channelId: String): List 10 | suspend fun getChannelIdsForUserAndWorkspace(userId: String, workspaceId: String): List 11 | suspend fun isMember(userId: String, workspaceId: String, channelId: String): Boolean 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/sources/MessagesDataSource.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.sources 2 | 3 | import dev.baseio.slackdata.protos.SKWorkspaceChannelRequest 4 | import dev.baseio.slackserver.data.models.SkMessage 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface MessagesDataSource { 8 | suspend fun saveMessage(request: SkMessage): SkMessage 9 | suspend fun getMessages(workspaceId: String, channelId: String, limit: Int, offset: Int): List 10 | fun registerForChanges(request: SKWorkspaceChannelRequest): Flow> 11 | suspend fun updateMessage(request: SkMessage): SkMessage? 12 | suspend fun getMessage(uuid: String, workspaceId: String): SkMessage? 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/communications/NotificationType.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.communications 2 | 3 | enum class NotificationType(val titleMessage: String, val bodyMessage: String) { 4 | CHANNEL_CREATED( 5 | bodyMessage = "A new channel %s was created", 6 | titleMessage = "New Group Message Channel!" 7 | ), 8 | DM_CHANNEL_CREATED( 9 | bodyMessage = "A new conversation was initiated by %s", 10 | titleMessage = "New Direct Message Channel!" 11 | ), 12 | ADDED_CHANNEL( 13 | titleMessage = "Added to Channel", 14 | bodyMessage = "You were added to a slack channel by %s" 15 | ), 16 | NEW_MESSAGE(titleMessage = "New Message", bodyMessage = "You have received a new message. %s") 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/sources/WorkspaceDataSource.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.sources 2 | 3 | import dev.baseio.slackserver.data.models.SkWorkspace 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface WorkspaceDataSource { 7 | suspend fun getWorkspaces(): List 8 | suspend fun saveWorkspace(skWorkspace: SkWorkspace): SkWorkspace? 9 | suspend fun getWorkspace(workspaceId: String): SkWorkspace? 10 | suspend fun findWorkspacesForEmail(email: String): List 11 | suspend fun findWorkspaceForName(name: String): SkWorkspace? 12 | suspend fun updateWorkspace(toDBWorkspace: SkWorkspace): SkWorkspace? 13 | fun registerForChanges(uuid: String?): Flow> 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/sources/UsersDataSource.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.sources 2 | 3 | import dev.baseio.slackserver.data.models.SkUser 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface UsersDataSource { 7 | suspend fun saveUser(skUser: SkUser): SkUser? 8 | fun getChangeInUserFor(workspaceId: String): Flow> 9 | suspend fun getUsers(workspaceId: String): List 10 | suspend fun getUser(userId: String, workspaceId: String): SkUser? 11 | suspend fun updateUser(request: SkUser): SkUser? 12 | suspend fun getUserWithEmailId(emailId: String, workspaceId: String): SkUser? 13 | suspend fun getUserWithUsername(userName: String?, workspaceId: String): SkUser? 14 | suspend fun getUserWithUserId(userId: String,workspaceId: String):SkUser? 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/communications/PNChannelMember.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.communications 2 | 3 | import dev.baseio.slackserver.data.models.SKUserPushToken 4 | import dev.baseio.slackserver.data.models.SkChannelMember 5 | import dev.baseio.slackserver.data.models.SkUser 6 | import dev.baseio.slackserver.data.sources.UserPushTokenDataSource 7 | import dev.baseio.slackserver.data.sources.UsersDataSource 8 | 9 | class PNChannelMember( 10 | private val userPushTokenDataSource: UserPushTokenDataSource, 11 | private val usersDataSource: UsersDataSource 12 | ) : PNSender() { 13 | override suspend fun getSender(senderUserId: String, request: SkChannelMember): SkUser? { 14 | return usersDataSource.getUser(senderUserId, request.workspaceId) 15 | } 16 | 17 | override suspend fun getPushTokens(request: SkChannelMember): List { 18 | return userPushTokenDataSource.getPushTokensFor(listOf(request.memberId)) 19 | } 20 | } -------------------------------------------------------------------------------- /src/test/kotlin/FakeQrCodeGenerator.kt: -------------------------------------------------------------------------------- 1 | import dev.baseio.slackdata.common.SKByteArrayElement 2 | import dev.baseio.slackdata.common.sKByteArrayElement 3 | import dev.baseio.slackdata.protos.* 4 | import dev.baseio.slackserver.services.IQrCodeGenerator 5 | import java.nio.file.Path 6 | import java.util.* 7 | import kotlin.io.path.Path 8 | 9 | class FakeQrCodeGenerator : IQrCodeGenerator { 10 | val token = UUID.randomUUID().toString() 11 | override val inMemoryQrCodes: HashMap Unit>> = hashMapOf() 12 | 13 | override fun process(data: String): Pair { 14 | return Pair(sKQrCodeResponse { 15 | this.byteArray.addAll(mutableListOf().apply { 16 | add(sKByteArrayElement { 17 | this.byte = 1 18 | }) 19 | }) 20 | }, Path("somepath")) 21 | } 22 | 23 | override fun randomToken(): String { 24 | return token 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/impl/UserPushTokenDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.impl 2 | 3 | import dev.baseio.slackserver.data.models.SKUserPushToken 4 | import dev.baseio.slackserver.data.sources.UserPushTokenDataSource 5 | import org.litote.kmongo.coroutine.CoroutineDatabase 6 | import org.litote.kmongo.coroutine.insertOne 7 | import org.litote.kmongo.eq 8 | import org.litote.kmongo.`in` 9 | 10 | class UserPushTokenDataSourceImpl(private val coroutineDatabase: CoroutineDatabase) : UserPushTokenDataSource { 11 | override suspend fun getPushTokensFor(userIds: List): List { 12 | return coroutineDatabase.getCollection().find(SKUserPushToken::userId `in` userIds).toList() 13 | } 14 | 15 | override suspend fun savePushToken(toSkUserPushToken: SKUserPushToken) { 16 | val exists = 17 | coroutineDatabase.getCollection() 18 | .find(SKUserPushToken::token eq toSkUserPushToken.token).toList().isNotEmpty() 19 | if(!exists){ 20 | coroutineDatabase.getCollection().insertOne(toSkUserPushToken) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/IQrCodeGenerator.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services 2 | 3 | import dev.baseio.slackdata.protos.SKAuthResult 4 | import dev.baseio.slackdata.protos.SKQRAuthVerify 5 | import dev.baseio.slackdata.protos.SKQrCodeResponse 6 | import java.nio.file.Path 7 | import kotlin.io.path.deleteIfExists 8 | 9 | interface IQrCodeGenerator { 10 | val inMemoryQrCodes : HashMap Unit>> // TODO this is dirty! 11 | 12 | fun process(data: String): Pair 13 | 14 | fun notifyAuthenticated(result: SKAuthResult, request: SKQRAuthVerify) { 15 | inMemoryQrCodes[request.token]?.first?.deleteIfExists() 16 | inMemoryQrCodes[request.token]?.second?.invoke(result) 17 | inMemoryQrCodes.remove(request.token) 18 | } 19 | 20 | fun removeQrCode(data: String) { 21 | inMemoryQrCodes[data]?.first?.deleteIfExists() 22 | inMemoryQrCodes.remove(data) 23 | } 24 | 25 | fun find(token: String): Pair Unit>? { 26 | return inMemoryQrCodes[token] 27 | } 28 | 29 | fun put(data: String, result: Pair, function: (SKAuthResult) -> Unit) { 30 | inMemoryQrCodes[data] = Pair(result.second,function) 31 | } 32 | 33 | fun randomToken(): String 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/communications/PNMessages.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.communications 2 | 3 | import dev.baseio.slackserver.data.models.SKUserPushToken 4 | import dev.baseio.slackserver.data.models.SkMessage 5 | import dev.baseio.slackserver.data.models.SkUser 6 | import dev.baseio.slackserver.data.sources.ChannelMemberDataSource 7 | import dev.baseio.slackserver.data.sources.UserPushTokenDataSource 8 | import dev.baseio.slackserver.data.sources.UsersDataSource 9 | 10 | class PNMessages( 11 | private val channelMemberDataSource: ChannelMemberDataSource, 12 | private val userPushTokenDataSource: UserPushTokenDataSource, 13 | private val usersDataSource: UsersDataSource 14 | ) : PNSender() { 15 | 16 | override suspend fun getSender(senderUserId: String, request: SkMessage): SkUser { 17 | return usersDataSource.getUser(senderUserId, request.workspaceId)!! 18 | } 19 | 20 | override suspend fun getPushTokens(request: SkMessage): List { 21 | val tokens = mutableListOf() 22 | channelMemberDataSource.getMembers(request.workspaceId, request.channelId).map { it.memberId } 23 | .let { skChannelMembers -> 24 | tokens.addAll(userPushTokenDataSource.getPushTokensFor(skChannelMembers)) 25 | } 26 | return tokens 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/communications/PNChannel.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.communications 2 | 3 | import dev.baseio.slackserver.data.models.SKUserPushToken 4 | import dev.baseio.slackserver.data.models.SkChannel 5 | import dev.baseio.slackserver.data.models.SkUser 6 | import dev.baseio.slackserver.data.sources.ChannelMemberDataSource 7 | import dev.baseio.slackserver.data.sources.UserPushTokenDataSource 8 | import dev.baseio.slackserver.data.sources.UsersDataSource 9 | 10 | class PNChannel( 11 | private val usersDataSource: UsersDataSource, 12 | private val channelMemberDataSource: ChannelMemberDataSource, 13 | private val userPushTokenDataSource: UserPushTokenDataSource 14 | ) : 15 | PNSender() { 16 | 17 | override suspend fun getSender(senderUserId: String, request: SkChannel): SkUser { 18 | return usersDataSource.getUser(senderUserId, request.workspaceId)!! 19 | } 20 | 21 | override suspend fun getPushTokens(request: SkChannel): List { 22 | val tokens = mutableListOf() 23 | channelMemberDataSource.getMembers(request.workspaceId, request.channelId).map { it.memberId } 24 | .let { skChannelMembers -> 25 | tokens.addAll(userPushTokenDataSource.getPushTokensFor(skChannelMembers)) 26 | } 27 | return tokens 28 | } 29 | 30 | 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/sources/ChannelsDataSource.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.sources 2 | 3 | import dev.baseio.slackserver.data.models.SkChannel 4 | import dev.baseio.slackserver.data.models.SkChannelMember 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface ChannelsDataSource { 8 | fun getChannelChangeStream(workspaceId: String): Flow> 9 | fun getDMChannelChangeStream(workspaceId: String): Flow> 10 | suspend fun savePublicChannel(request: SkChannel.SkGroupChannel, adminId: String): SkChannel.SkGroupChannel? 11 | suspend fun saveDMChannel(request: SkChannel.SkDMChannel): SkChannel.SkDMChannel? 12 | suspend fun getAllChannels(workspaceId: String, userId: String): List 13 | suspend fun getAllDMChannels(workspaceId: String, userId: String): List 14 | suspend fun checkIfDMChannelExists(userId: String, receiverId: String?):SkChannel.SkDMChannel? 15 | suspend fun getChannelById(channelId: String, workspaceId: String): SkChannel? 16 | 17 | suspend fun getChannelByName(channelId: String, workspaceId: String): SkChannel? 18 | fun getChannelMemberChangeStream( 19 | workspaceId: String, 20 | memberId: String 21 | ): Flow> 22 | 23 | suspend fun checkIfGroupExisits(workspaceId: String?, name: String?): Boolean 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/AuthenticationDelegate.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services 2 | 3 | import dev.baseio.slackdata.protos.SKAuthUser 4 | import dev.baseio.slackserver.communications.SlackEmailHelper 5 | import dev.baseio.slackserver.data.sources.AuthDataSource 6 | import dev.baseio.slackserver.data.sources.UsersDataSource 7 | import io.grpc.Status 8 | import io.grpc.StatusException 9 | 10 | interface AuthenticationDelegate { 11 | suspend fun processRequestForEmail(request: SKAuthUser, workspaceId: String) 12 | } 13 | 14 | class AuthenticationDelegateImpl( 15 | private val authDataSource: AuthDataSource, 16 | private val usersDataSource: UsersDataSource 17 | ) : AuthenticationDelegate { 18 | 19 | override suspend fun processRequestForEmail(request: SKAuthUser, workspaceId: String) { 20 | kotlin.runCatching { 21 | val existingUser = usersDataSource.getUserWithEmailId(emailId = request.email, workspaceId = workspaceId) 22 | existingUser?.let { 23 | val authResult = skAuthResult(it) 24 | SlackEmailHelper.sendEmail(request.email, "slackclone://open/?token=${authResult.token}&workspaceId=$workspaceId") 25 | } ?: run { 26 | val generatedUser = authDataSource.register( 27 | request.email, 28 | request.user.toDBUser().copy(workspaceId = workspaceId, email = request.email) 29 | ) 30 | val authResult = skAuthResult(generatedUser) 31 | SlackEmailHelper.sendEmail(request.email, "slackclone://open/?token=${authResult.token}&workspaceId=$workspaceId") 32 | } 33 | }.exceptionOrNull()?.printStackTrace() 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/SkMessage.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | data class SkMessage( 4 | val uuid: String, 5 | val workspaceId: String, 6 | val channelId: String, 7 | val message: ByteArray, 8 | val sender: String, 9 | val createdDate: Long, 10 | val modifiedDate: Long, 11 | var isDeleted: Boolean = false 12 | ) :IDataMap{ 13 | override fun provideMap(): Map { 14 | return hashMapOf().apply { 15 | put("message", message.map { it }.toByteArray().decodeToString()) 16 | put("channelId", channelId) 17 | put("type", "message") 18 | } 19 | } 20 | 21 | override fun equals(other: Any?): Boolean { 22 | if (this === other) return true 23 | if (javaClass != other?.javaClass) return false 24 | 25 | other as SkMessage 26 | 27 | if (uuid != other.uuid) return false 28 | if (workspaceId != other.workspaceId) return false 29 | if (channelId != other.channelId) return false 30 | if (!message.contentEquals(other.message)) return false 31 | if (sender != other.sender) return false 32 | if (createdDate != other.createdDate) return false 33 | if (modifiedDate != other.modifiedDate) return false 34 | if (isDeleted != other.isDeleted) return false 35 | 36 | return true 37 | } 38 | 39 | override fun hashCode(): Int { 40 | var result = uuid.hashCode() 41 | result = 31 * result + workspaceId.hashCode() 42 | result = 31 * result + channelId.hashCode() 43 | result = 31 * result + message.contentHashCode() 44 | result = 31 * result + sender.hashCode() 45 | result = 31 * result + createdDate.hashCode() 46 | result = 31 * result + modifiedDate.hashCode() 47 | result = 31 * result + isDeleted.hashCode() 48 | return result 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/impl/AuthDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.impl 2 | 3 | import dev.baseio.slackserver.data.sources.AuthDataSource 4 | import dev.baseio.slackserver.data.models.SkAuthUser 5 | import dev.baseio.slackserver.data.models.SkUser 6 | import kotlinx.coroutines.reactive.awaitFirstOrNull 7 | import org.litote.kmongo.coroutine.CoroutineDatabase 8 | import org.litote.kmongo.eq 9 | import org.litote.kmongo.reactivestreams.findOne 10 | import java.util.* 11 | 12 | class AuthDataSourceImpl(private val slackCloneDB: CoroutineDatabase) : AuthDataSource { 13 | override suspend fun findUser(email: String, workspaceId: String): SkUser? { 14 | val user = slackCloneDB.getCollection().collection 15 | .findOne( 16 | SkUser::email eq email, 17 | SkUser::workspaceId eq workspaceId 18 | ) 19 | user.awaitFirstOrNull()?.let { user -> 20 | slackCloneDB.getCollection().collection 21 | .findOne(SkAuthUser::userId eq user.uuid) 22 | .awaitFirstOrNull() 23 | } 24 | return null 25 | } 26 | 27 | override suspend fun register(email: String, user: SkUser): SkUser? { 28 | //save the user details 29 | if (email.trim().isEmpty()) { 30 | throw Exception("email cannot be empty!") 31 | } 32 | if (user.uuid.trim().isEmpty()) { 33 | throw Exception("user uuid cannot be empty!") 34 | } 35 | slackCloneDB.getCollection().collection.insertOne( 36 | user 37 | ).awaitFirstOrNull() 38 | // save the auth 39 | 40 | slackCloneDB.getCollection().collection.insertOne( 41 | SkAuthUser(UUID.randomUUID().toString(), user.uuid) 42 | ).awaitFirstOrNull() 43 | 44 | return slackCloneDB.getCollection().collection.findOne(SkUser::uuid eq user.uuid).awaitFirstOrNull() 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/models/SkChannel.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.models 2 | 3 | import java.util.* 4 | 5 | sealed class SkChannel( 6 | val workspaceId: String, 7 | val channelId: String, 8 | val publicKey: SKUserPublicKey, 9 | ) : IDataMap { 10 | data class SkDMChannel( 11 | val uuid: String, 12 | val workId: String, 13 | var senderId: String, 14 | var receiverId: String, 15 | val createdDate: Long = System.currentTimeMillis(), 16 | val modifiedDate: Long = System.currentTimeMillis(), 17 | val deleted: Boolean, 18 | val channelPublicKey: SKUserPublicKey, 19 | ) : SkChannel(workId, uuid, channelPublicKey) { 20 | override fun provideMap(): Map { 21 | return hashMapOf().apply { 22 | put("uuid", uuid) 23 | put("workspaceId", workId) 24 | put("senderId", senderId) 25 | put("receiverId", receiverId) 26 | put("type", "") 27 | } 28 | } 29 | } 30 | 31 | data class SkGroupChannel( 32 | val uuid: String, 33 | val workId: String, 34 | var name: String, 35 | val createdDate: Long = System.currentTimeMillis(), 36 | val modifiedDate: Long = System.currentTimeMillis(), 37 | var avatarUrl: String?, 38 | val deleted: Boolean, 39 | val channelPublicKey: SKUserPublicKey, 40 | ) : SkChannel(workId, uuid, channelPublicKey) { 41 | override fun provideMap(): Map { 42 | return hashMapOf().apply { 43 | put("uuid", uuid) 44 | put("workspaceId", workId) 45 | put("name", name) 46 | } 47 | } 48 | } 49 | } 50 | 51 | 52 | data class SkChannelMember( 53 | val workspaceId: String, 54 | val channelId: String, 55 | val memberId: String, 56 | val channelEncryptedPrivateKey: SKEncryptedMessage? = null 57 | ) : IDataMap { 58 | var uuid: String = UUID.randomUUID().toString() 59 | override fun provideMap(): Map { 60 | return hashMapOf().apply { 61 | put("uuid", uuid) 62 | put("channelId", channelId) 63 | put("workspaceId", workspaceId) 64 | put("memberId", memberId) 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/AuthService.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services 2 | 3 | 4 | import dev.baseio.slackdata.common.Empty 5 | import dev.baseio.slackdata.common.empty 6 | import dev.baseio.slackdata.protos.* 7 | import dev.baseio.slackserver.data.models.SKUserPushToken 8 | import dev.baseio.slackserver.data.models.SkUser 9 | import dev.baseio.slackserver.data.sources.UserPushTokenDataSource 10 | import dev.baseio.slackserver.services.interceptors.* 11 | import io.jsonwebtoken.Jwts 12 | import io.jsonwebtoken.io.Decoders 13 | import io.jsonwebtoken.security.Keys 14 | import kotlinx.coroutines.Dispatchers 15 | import java.security.Key 16 | import java.time.Instant 17 | import java.util.* 18 | import java.util.concurrent.TimeUnit 19 | import kotlin.coroutines.CoroutineContext 20 | 21 | 22 | class AuthService( 23 | coroutineContext: CoroutineContext = Dispatchers.IO, 24 | private val pushTokenDataSource: UserPushTokenDataSource, 25 | authenticationDelegate: AuthenticationDelegate 26 | ) : 27 | AuthServiceGrpcKt.AuthServiceCoroutineImplBase(coroutineContext), AuthenticationDelegate by authenticationDelegate { 28 | 29 | override suspend fun savePushToken(request: SKPushToken): Empty { 30 | val authData = AUTH_CONTEXT_KEY.get() 31 | pushTokenDataSource.savePushToken(request.toSkUserPushToken(authData.userId)) 32 | return empty { } 33 | } 34 | } 35 | 36 | private fun SKPushToken.toSkUserPushToken(userId: String): SKUserPushToken { 37 | return SKUserPushToken( 38 | uuid = UUID.randomUUID().toString(), 39 | userId = userId, 40 | platform = this.platform, 41 | token = this.token 42 | ) 43 | } 44 | 45 | fun jwtTokenForUser( 46 | generatedUser: SkUser?, 47 | key: Key, addTime: Long = TimeUnit.DAYS.toMillis(365) 48 | ): String? = Jwts.builder() 49 | .setClaims(hashMapOf().apply { 50 | put(USER_ID, generatedUser?.uuid) 51 | put(WORKSPACE_ID, generatedUser?.workspaceId) 52 | }) 53 | .setExpiration(Date.from(Instant.now().plusMillis(addTime)))// valid for 5 days 54 | .signWith(key) 55 | .compact() 56 | 57 | fun skAuthResult(generatedUser: SkUser?): SKAuthResult { 58 | val keyBytes = 59 | Decoders.BASE64.decode(JWT_SIGNING_KEY)// TODO move this to env variables 60 | val key: Key = Keys.hmacShaKeyFor(keyBytes) 61 | val jws = jwtTokenForUser(generatedUser, key, TimeUnit.DAYS.toMillis(365)) 62 | return SKAuthResult.newBuilder() 63 | .setToken(jws) 64 | .build() 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/DataSourcesModule.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver 2 | 3 | import dev.baseio.slackserver.communications.PNChannel 4 | import dev.baseio.slackserver.communications.PNChannelMember 5 | import dev.baseio.slackserver.communications.PNMessages 6 | import dev.baseio.slackserver.communications.PNSender 7 | import dev.baseio.slackserver.data.impl.* 8 | import dev.baseio.slackserver.data.models.SkChannel 9 | import dev.baseio.slackserver.data.models.SkChannelMember 10 | import dev.baseio.slackserver.data.models.SkMessage 11 | import dev.baseio.slackserver.data.sources.* 12 | import dev.baseio.slackserver.services.AuthenticationDelegate 13 | import dev.baseio.slackserver.services.AuthenticationDelegateImpl 14 | import dev.baseio.slackserver.services.IQrCodeGenerator 15 | import dev.baseio.slackserver.services.QrCodeGenerator 16 | import org.koin.core.qualifier.named 17 | import org.koin.dsl.module 18 | import org.koin.java.KoinJavaComponent 19 | import org.litote.kmongo.coroutine.coroutine 20 | import org.litote.kmongo.reactivestreams.KMongo 21 | 22 | val dataSourcesModule = module { 23 | single { 24 | val client = KMongo.createClient(connectionString = System.getenv("connection.mongodb")) 25 | client.getDatabase("slackDB").coroutine //normal java driver usage 26 | } 27 | factory { WorkspaceDataSourceImpl(get()) } 28 | factory { UsersDataSourceImpl(get()) } 29 | 30 | factory { 31 | ChannelMemberDataSourceImpl(get()) 32 | } 33 | factory { 34 | ChannelsDataSourceImpl(get(), getKoin().get()) 35 | } 36 | factory { 37 | MessagesDataSourceImpl(get()) 38 | } 39 | factory { 40 | AuthDataSourceImpl(get()) 41 | } 42 | factory { 43 | AuthenticationDelegateImpl(KoinJavaComponent.getKoin().get(), KoinJavaComponent.getKoin().get()) 44 | } 45 | factory { QrCodeGenerator() } 46 | 47 | factory { 48 | UserPushTokenDataSourceImpl(get()) 49 | } 50 | factory>(named(SkChannel::class.java.name)) { 51 | PNChannel(get(), get(), get()) 52 | } 53 | factory>(named(SkMessage::class.java.name)) { 54 | PNMessages(get(), get(), get()) 55 | } 56 | factory>(named(SkChannelMember::class.java.name)) { 57 | PNChannelMember(get(), get()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/interceptors/AuthInterceptor.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services.interceptors 2 | 3 | import io.grpc.* 4 | import io.grpc.Metadata.ASCII_STRING_MARSHALLER 5 | import io.jsonwebtoken.Claims 6 | import io.jsonwebtoken.Jws 7 | import io.jsonwebtoken.JwtParser 8 | import io.jsonwebtoken.Jwts 9 | 10 | 11 | const val JWT_SIGNING_KEY = "L8hHXsaQOUjk5rg7XPGv4eL36anlCrkMz8CJ0i/8E/0=" 12 | const val USER_ID = "USER_ID" 13 | const val WORKSPACE_ID = "WORKSPACE_ID" 14 | const val BEARER_TYPE = "Bearer" 15 | val AUTHORIZATION_METADATA_KEY: Metadata.Key = Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER) 16 | val AUTH_CONTEXT_KEY: Context.Key = Context.key("credentials") 17 | 18 | data class AuthData(val userId: String, val workspaceId: String) 19 | 20 | class AuthInterceptor : ServerInterceptor { 21 | private val parser: JwtParser = Jwts.parserBuilder() 22 | .setSigningKey(JWT_SIGNING_KEY) 23 | .build() 24 | 25 | override fun interceptCall( 26 | serverCall: ServerCall?, 27 | metadata: Metadata?, 28 | serverCallHandler: ServerCallHandler? 29 | ): ServerCall.Listener { 30 | val value: String? = metadata?.get(AUTHORIZATION_METADATA_KEY) 31 | val status: Status = when { 32 | value == null -> { 33 | val ctx: Context = Context.current().withValue(AUTH_CONTEXT_KEY, AuthData("","")) 34 | return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler) 35 | //Status.UNAUTHENTICATED.withDescription("Authorization token is missing") 36 | } 37 | 38 | !value.startsWith(BEARER_TYPE) -> { 39 | Status.UNAUTHENTICATED.withDescription("Unknown authorization type") 40 | } 41 | 42 | else -> { 43 | try { 44 | val token: String = value.substring(BEARER_TYPE.length).trim { it <= ' ' } 45 | val claims: Jws = parser.parseClaimsJws(token) 46 | val authData = AuthData( 47 | userId = claims.body[USER_ID].toString(), 48 | workspaceId = claims.body[WORKSPACE_ID].toString() 49 | ) 50 | val ctx: Context = Context.current().withValue(AUTH_CONTEXT_KEY, authData) 51 | return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler) 52 | } catch (e: Exception) { 53 | e.printStackTrace() 54 | Status.UNAUTHENTICATED.withDescription(e.message).withCause(e) 55 | } 56 | } 57 | } 58 | serverCall?.close(status, metadata) 59 | return serverCallNoOp() 60 | } 61 | 62 | private fun serverCallNoOp(): ServerCall.Listener { 63 | return object : ServerCall.Listener() { 64 | // noop 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/impl/UsersDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.impl 2 | 3 | import com.mongodb.client.model.Filters 4 | import com.mongodb.client.model.changestream.OperationType 5 | import dev.baseio.slackserver.data.models.SkUser 6 | import dev.baseio.slackserver.data.sources.UsersDataSource 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.map 9 | import org.bson.Document 10 | import org.bson.conversions.Bson 11 | import org.litote.kmongo.coroutine.CoroutineDatabase 12 | import org.litote.kmongo.eq 13 | import org.litote.kmongo.match 14 | 15 | class UsersDataSourceImpl(private val slackCloneDB: CoroutineDatabase) : UsersDataSource { 16 | override suspend fun getUser(userId: String, workspaceId: String): SkUser? { 17 | return slackCloneDB.getCollection() 18 | .findOne(SkUser::uuid eq userId, SkUser::workspaceId eq workspaceId) 19 | } 20 | 21 | override suspend fun getUserWithEmailId(emailId: String, workspaceId: String): SkUser? { 22 | return slackCloneDB.getCollection() 23 | .findOne(SkUser::email eq emailId, SkUser::workspaceId eq workspaceId) 24 | } 25 | 26 | override suspend fun getUserWithUsername(userName: String?, workspaceId: String): SkUser? { 27 | return slackCloneDB.getCollection() 28 | .findOne(SkUser::username eq userName, SkUser::workspaceId eq workspaceId) 29 | } 30 | 31 | override suspend fun getUserWithUserId(userId: String, workspaceId: String): SkUser? { 32 | return slackCloneDB.getCollection().findOne(SkUser::uuid eq userId, SkUser::workspaceId eq workspaceId) 33 | } 34 | 35 | override suspend fun updateUser(request: SkUser): SkUser? { 36 | slackCloneDB.getCollection() 37 | .updateOne(SkUser::uuid eq request.uuid, request) 38 | return getUser(request.uuid, request.workspaceId) 39 | } 40 | 41 | override suspend fun saveUser(skUser: SkUser): SkUser? { 42 | slackCloneDB.getCollection() 43 | .insertOne(skUser) 44 | return slackCloneDB.getCollection().findOne(SkUser::uuid eq skUser.uuid) 45 | } 46 | 47 | override fun getChangeInUserFor(workspaceId: String): Flow> { 48 | val collection = slackCloneDB.getCollection() 49 | 50 | val pipeline: List = listOf( 51 | match( 52 | Document.parse("{'fullDocument.workspaceId': '$workspaceId'}"), 53 | Filters.`in`("operationType", OperationType.values().map { it.value }.toList()) 54 | ) 55 | ) 56 | 57 | return collection 58 | .watch(pipeline).toFlow().map { 59 | Pair(it.fullDocumentBeforeChange, it.fullDocument) 60 | } 61 | } 62 | 63 | override suspend fun getUsers(workspaceId: String): List { 64 | return slackCloneDB.getCollection() 65 | .find(SkUser::workspaceId eq workspaceId).toList() 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import com.google.auth.oauth2.GoogleCredentials 2 | import com.google.firebase.FirebaseApp 3 | import com.google.firebase.FirebaseOptions 4 | import dev.baseio.slackserver.data.models.SkChannel 5 | import dev.baseio.slackserver.data.models.SkChannelMember 6 | import dev.baseio.slackserver.data.models.SkMessage 7 | import dev.baseio.slackserver.dataSourcesModule 8 | import dev.baseio.slackserver.services.* 9 | import dev.baseio.slackserver.services.interceptors.AuthInterceptor 10 | import io.grpc.Server 11 | import io.grpc.ServerBuilder 12 | import org.bouncycastle.jce.provider.BouncyCastleProvider 13 | import org.koin.core.context.startKoin 14 | import org.koin.core.context.stopKoin 15 | import org.koin.core.qualifier.named 16 | import org.koin.java.KoinJavaComponent.getKoin 17 | import java.security.Security 18 | 19 | object SlackServer { 20 | 21 | init { 22 | Security.addProvider( 23 | BouncyCastleProvider() 24 | ) 25 | initializeFCM() 26 | 27 | initKoin() 28 | } 29 | 30 | fun start(): Server { 31 | return ServerBuilder.forPort(8081) 32 | //.useTransportSecurity(tlsCertFile, tlsPrivateKeyFile) // TODO enable this once the kmp library supports this. 33 | .addService( 34 | AuthService( 35 | pushTokenDataSource = getKoin().get(), 36 | authenticationDelegate = getKoin().get() 37 | ) 38 | ) 39 | .addService(QrCodeService(database = getKoin().get(), qrCodeGenerator = getKoin().get())) 40 | .addService( 41 | WorkspaceService( 42 | workspaceDataSource = getKoin().get(), 43 | authDelegate = getKoin().get() 44 | ) 45 | ) 46 | .addService( 47 | ChannelService( 48 | channelsDataSource = getKoin().get(), 49 | channelMemberDataSource = getKoin().get(), 50 | usersDataSource = getKoin().get(), 51 | channelMemberPNSender = getKoin().get(named(SkChannelMember::class.java.name)), 52 | channelPNSender = getKoin().get(named(SkChannel::class.java.name)) 53 | ) 54 | ) 55 | .addService( 56 | MessagingService( 57 | messagesDataSource = getKoin().get(), 58 | pushNotificationForMessages = getKoin().get(named(SkMessage::class.java.name)) 59 | ) 60 | ) 61 | .addService(UserService(usersDataSource = getKoin().get())) 62 | .intercept(AuthInterceptor()) 63 | .build() 64 | .start() 65 | } 66 | } 67 | 68 | fun main() { 69 | SlackServer.start().awaitTermination() 70 | stopKoin() 71 | } 72 | 73 | private fun initKoin() { 74 | startKoin { 75 | modules(dataSourcesModule) 76 | } 77 | } 78 | 79 | fun initializeFCM() { 80 | val options = FirebaseOptions.builder() 81 | .setCredentials(GoogleCredentials.getApplicationDefault()) 82 | .build() 83 | 84 | FirebaseApp.initializeApp(options) 85 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/impl/WorkspaceDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.impl 2 | 3 | import com.mongodb.client.model.Filters 4 | import com.mongodb.client.model.changestream.OperationType 5 | import dev.baseio.slackserver.data.models.SkMessage 6 | import dev.baseio.slackserver.data.models.SkUser 7 | import dev.baseio.slackserver.data.models.SkWorkspace 8 | import dev.baseio.slackserver.data.sources.WorkspaceDataSource 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.map 11 | import org.bson.Document 12 | import org.bson.conversions.Bson 13 | import org.litote.kmongo.coroutine.CoroutineDatabase 14 | import org.litote.kmongo.eq 15 | import org.litote.kmongo.`in` 16 | import org.litote.kmongo.match 17 | 18 | class WorkspaceDataSourceImpl(private val slackCloneDB: CoroutineDatabase) : WorkspaceDataSource { 19 | override suspend fun getWorkspaces(): List { 20 | return slackCloneDB.getCollection().find().toList() 21 | } 22 | 23 | override suspend fun findWorkspacesForEmail(email: String): List { 24 | val workspaceIds = slackCloneDB.getCollection() 25 | .find(SkUser::email eq email) 26 | .toList().map { 27 | it.workspaceId 28 | } 29 | return slackCloneDB.getCollection().find(SkWorkspace::uuid `in` workspaceIds) 30 | .toList() 31 | } 32 | 33 | override suspend fun findWorkspaceForName(name: String): SkWorkspace? { 34 | return slackCloneDB.getCollection().findOne(SkWorkspace::name eq name) 35 | } 36 | 37 | override suspend fun getWorkspace(workspaceId: String): SkWorkspace? { 38 | return slackCloneDB.getCollection().findOne(SkWorkspace::uuid eq workspaceId) 39 | } 40 | 41 | override suspend fun saveWorkspace(skWorkspace: SkWorkspace): SkWorkspace? { 42 | slackCloneDB.getCollection().insertOne(skWorkspace) 43 | return getWorkspace(skWorkspace.uuid) 44 | } 45 | 46 | override suspend fun updateWorkspace(toDBWorkspace: SkWorkspace): SkWorkspace? { 47 | slackCloneDB.getCollection() 48 | .updateOne(SkWorkspace::uuid eq toDBWorkspace.uuid, toDBWorkspace) 49 | return getWorkspace(toDBWorkspace.uuid) 50 | } 51 | 52 | override fun registerForChanges(uuid: String?): Flow> { 53 | val collection = slackCloneDB.getCollection() 54 | val pipeline: List = listOf( 55 | match( 56 | Document.parse("{'fullDocument.workspaceId': '${uuid}'}"), 57 | Filters.`in`("operationType", OperationType.values().map { it.value }.toList()) 58 | ) 59 | ) 60 | 61 | return collection 62 | .watch(pipeline).toFlow().map { 63 | Pair(it.fullDocumentBeforeChange, it.fullDocument) 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java template 2 | # Compiled class file 3 | *.class 4 | build 5 | build/ 6 | tools/build 7 | .idea 8 | .gradle 9 | .idea/ 10 | .gradle/ 11 | # Log file 12 | *.log 13 | # BlueJ files 14 | *.ctxt 15 | 16 | # Mobile Tools for Java (J2ME) 17 | .mtj.tmp/ 18 | 19 | # Package Files # 20 | *.jar 21 | *.war 22 | *.nar 23 | *.ear 24 | *.zip 25 | *.tar.gz 26 | *.rar 27 | 28 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 29 | hs_err_pid* 30 | 31 | ### JetBrains template 32 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 33 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 34 | 35 | # User-specific stuff 36 | .idea/**/workspace.xml 37 | .idea/**/tasks.xml 38 | .idea/**/usage.statistics.xml 39 | .idea/**/dictionaries 40 | .idea/**/shelf 41 | 42 | # Generated files 43 | .idea/**/contentModel.xml 44 | 45 | # Sensitive or high-churn files 46 | .idea/**/dataSources/ 47 | .idea/**/dataSources.ids 48 | .idea/**/dataSources.local.xml 49 | .idea/**/sqlDataSources.xml 50 | .idea/**/dynamic.xml 51 | .idea/**/uiDesigner.xml 52 | .idea/**/dbnavigator.xml 53 | 54 | # Gradle 55 | .idea/**/gradle.xml 56 | .idea/**/libraries 57 | 58 | # Gradle and Maven with auto-import 59 | # When using Gradle or Maven with auto-import, you should exclude module files, 60 | # since they will be recreated, and may cause churn. Uncomment if using 61 | # auto-import. 62 | # .idea/artifacts 63 | # .idea/compiler.xml 64 | # .idea/jarRepositories.xml 65 | # .idea/modules.xml 66 | # .idea/*.iml 67 | # .idea/modules 68 | # *.iml 69 | # *.ipr 70 | 71 | # CMake 72 | cmake-build-*/ 73 | 74 | # Mongo Explorer plugin 75 | .idea/**/mongoSettings.xml 76 | 77 | # File-based project format 78 | *.iws 79 | 80 | # IntelliJ 81 | out/ 82 | 83 | # mpeltonen/sbt-idea plugin 84 | .idea_modules/ 85 | 86 | # JIRA plugin 87 | atlassian-ide-plugin.xml 88 | 89 | # Cursive Clojure plugin 90 | .idea/replstate.xml 91 | 92 | # Crashlytics plugin (for Android Studio and IntelliJ) 93 | com_crashlytics_export_strings.xml 94 | crashlytics.properties 95 | crashlytics-build.properties 96 | fabric.properties 97 | 98 | # Editor-based Rest Client 99 | .idea/httpRequests 100 | 101 | # Android studio 3.1+ serialized cache file 102 | .idea/caches/build_file_checksums.ser 103 | 104 | ### Kotlin template 105 | # Compiled class file 106 | *.class 107 | 108 | # Log file 109 | *.log 110 | 111 | # BlueJ files 112 | *.ctxt 113 | 114 | # Mobile Tools for Java (J2ME) 115 | .mtj.tmp/ 116 | 117 | # Package Files # 118 | *.jar 119 | *.war 120 | *.nar 121 | *.ear 122 | *.zip 123 | *.tar.gz 124 | *.rar 125 | 126 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 127 | hs_err_pid* 128 | 129 | !/*.mv.db 130 | /slackdb.mv.db 131 | /src/main/resources/ecdsa/sender_signing_key.dat 132 | /android-key-path/sender_verification_key.dat 133 | /src/main/resources/tls/tls.key 134 | /src/main/resources/tls/tls.crt 135 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/impl/MessagesDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.impl 2 | 3 | import com.mongodb.client.model.Filters 4 | import com.mongodb.client.model.changestream.OperationType 5 | import dev.baseio.slackdata.protos.SKWorkspaceChannelRequest 6 | import dev.baseio.slackserver.data.sources.MessagesDataSource 7 | import dev.baseio.slackserver.data.models.SkMessage 8 | import dev.baseio.slackserver.data.models.SkUser 9 | import io.grpc.Status 10 | import io.grpc.StatusException 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.map 13 | import org.bson.Document 14 | import org.bson.conversions.Bson 15 | import org.litote.kmongo.coroutine.CoroutineDatabase 16 | import org.litote.kmongo.eq 17 | import org.litote.kmongo.match 18 | 19 | class MessagesDataSourceImpl(private val slackCloneDB: CoroutineDatabase) : MessagesDataSource { 20 | 21 | override suspend fun getMessage(uuid: String, workspaceId: String): SkMessage? { 22 | return messageCoroutineCollection() 23 | .findOne(SkMessage::uuid eq uuid, SkUser::workspaceId eq workspaceId) 24 | } 25 | 26 | override suspend fun updateMessage(request: SkMessage): SkMessage? { 27 | messageCoroutineCollection() 28 | .updateOne(SkMessage::uuid eq request.uuid, request) 29 | return getMessage(request.uuid, request.workspaceId) 30 | } 31 | 32 | override fun registerForChanges(request: SKWorkspaceChannelRequest): Flow> { 33 | val collection = messageCoroutineCollection() 34 | 35 | val pipeline: List = request.channelId.takeIf { !it.isNullOrEmpty() }?.let { 36 | Document.parse("{'fullDocument.channelId': '${request.channelId}'}") 37 | listOf( 38 | match( 39 | Document.parse("{'fullDocument.workspaceId': '${request.workspaceId}'}"), 40 | Document.parse("{'fullDocument.channelId': '${request.channelId}'}"), 41 | Filters.`in`("operationType", OperationType.values().map { it.value }.toList()) 42 | ) 43 | ) 44 | } ?: kotlin.run { 45 | listOf( 46 | match( 47 | Document.parse("{'fullDocument.workspaceId': '${request.workspaceId}'}"), 48 | Filters.`in`("operationType", OperationType.values().map { it.value }.toList()) 49 | ) 50 | ) 51 | } 52 | 53 | return collection 54 | .watch(pipeline).toFlow().map { 55 | Pair(it.fullDocumentBeforeChange, it.fullDocument) 56 | } 57 | } 58 | 59 | override suspend fun saveMessage(request: SkMessage): SkMessage { 60 | val collection = messageCoroutineCollection() 61 | collection.insertOne(request) 62 | return collection.findOne(SkMessage::uuid eq request.uuid) ?: throw StatusException(Status.CANCELLED) 63 | } 64 | 65 | override suspend fun getMessages(workspaceId: String, channelId: String, limit: Int, offset: Int): List { 66 | val collection = messageCoroutineCollection() 67 | return collection.find(SkMessage::workspaceId eq workspaceId, SkMessage::channelId eq channelId) 68 | .descendingSort(SkMessage::createdDate) 69 | .skip(offset) 70 | .limit(limit) 71 | .toList() 72 | } 73 | 74 | private fun messageCoroutineCollection() = slackCloneDB.getCollection() 75 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/communications/PNSender.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.communications 2 | 3 | import com.google.firebase.messaging.* 4 | import dev.baseio.slackserver.data.models.IDataMap 5 | import dev.baseio.slackserver.data.models.SKUserPushToken 6 | import dev.baseio.slackserver.data.models.SkUser 7 | import kotlinx.coroutines.* 8 | 9 | abstract class PNSender { 10 | private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) 11 | 12 | fun sendPushNotifications(request: T, senderUserId: String, notificationType: NotificationType) { 13 | coroutineScope.launch { 14 | val sender = getSender(senderUserId, request) 15 | val pushTokens = getPushTokens(request) 16 | sender?.let { it -> 17 | pushTokens.takeIf { it.isNotEmpty() }?.let { pushTokens -> 18 | sendMessagesNow(pushTokens, request, it, notificationType) 19 | } 20 | } 21 | 22 | } 23 | } 24 | 25 | abstract suspend fun getSender(senderUserId: String, request: T): SkUser? 26 | private fun toFirebaseMessage( 27 | model: T, 28 | userToken: String, 29 | resourceName: String, 30 | notificationType: NotificationType 31 | ): Message { 32 | val dataMap = model.provideMap() 33 | return Message.builder() 34 | .setToken(userToken) 35 | .setWebpushConfig( 36 | webpushConfig(notificationType, resourceName) 37 | ) 38 | .setAndroidConfig( 39 | androidConfig(dataMap, notificationType, resourceName) 40 | ) 41 | .setNotification( 42 | notification(notificationType, resourceName) 43 | ) 44 | .build() 45 | } 46 | 47 | private fun notification( 48 | notificationType: NotificationType, 49 | resourceName: String 50 | ): Notification? = Notification.builder() 51 | .setBody(notificationType.bodyMessage.format(resourceName)) 52 | .setTitle(notificationType.titleMessage).build() 53 | 54 | private fun androidConfig( 55 | dataMap: Map, 56 | notificationType: NotificationType, 57 | resourceName: String 58 | ): AndroidConfig? = AndroidConfig.builder().putAllData(dataMap).setNotification( 59 | AndroidNotification.builder() 60 | .setBody(notificationType.bodyMessage.format(resourceName)) 61 | .setTitle(notificationType.titleMessage) 62 | .build() 63 | ) 64 | .build() 65 | 66 | private fun apnsConfig(dataMap: Map): ApnsConfig? = 67 | ApnsConfig.builder() 68 | .putAllCustomData(dataMap) 69 | .build() 70 | 71 | private fun webpushConfig( 72 | notificationType: NotificationType, 73 | resourceName: String 74 | ): WebpushConfig? = WebpushConfig.builder() 75 | .setNotification( 76 | WebpushNotification.builder() 77 | .setBody(notificationType.bodyMessage.format(resourceName)) 78 | .setTitle(notificationType.titleMessage) 79 | .build() 80 | ) 81 | .build() 82 | 83 | private fun sendMessagesNow( 84 | pushTokens: List, 85 | request: T, 86 | sender: SkUser, 87 | notificationType: NotificationType 88 | ) { 89 | FirebaseMessaging.getInstance().sendAll(pushTokens.map { skUserPushToken -> 90 | toFirebaseMessage(request, skUserPushToken.token, sender.name, notificationType) 91 | }) 92 | } 93 | 94 | abstract suspend fun getPushTokens(request: T): List 95 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/MessagingService.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services 2 | 3 | import dev.baseio.slackdata.protos.* 4 | import dev.baseio.slackserver.communications.NotificationType 5 | import dev.baseio.slackserver.communications.PNSender 6 | import dev.baseio.slackserver.data.sources.MessagesDataSource 7 | import dev.baseio.slackserver.data.models.SkMessage 8 | import dev.baseio.slackserver.services.interceptors.AUTH_CONTEXT_KEY 9 | import io.grpc.Status 10 | import io.grpc.StatusException 11 | import kotlinx.coroutines.* 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.catch 14 | import kotlinx.coroutines.flow.map 15 | import java.util.UUID 16 | import kotlin.coroutines.CoroutineContext 17 | 18 | class MessagingService( 19 | coroutineContext: CoroutineContext = Dispatchers.IO, 20 | private val messagesDataSource: MessagesDataSource, 21 | private val pushNotificationForMessages: PNSender, 22 | 23 | ) : MessagesServiceGrpcKt.MessagesServiceCoroutineImplBase(coroutineContext) { 24 | 25 | 26 | override suspend fun updateMessage(request: SKMessage): SKMessage { 27 | val authData = AUTH_CONTEXT_KEY.get() 28 | return messagesDataSource.updateMessage(request.toDBMessage())?.toGrpc()?.also { 29 | pushNotificationForMessages.sendPushNotifications(request.toDBMessage(), authData.userId,NotificationType.NEW_MESSAGE) 30 | } 31 | ?: throw StatusException(Status.NOT_FOUND) 32 | } 33 | 34 | override suspend fun saveMessage(request: SKMessage): SKMessage { 35 | val authData = AUTH_CONTEXT_KEY.get() 36 | return messagesDataSource 37 | .saveMessage(request.toDBMessage()) 38 | .toGrpc().also { 39 | pushNotificationForMessages.sendPushNotifications( 40 | request = request.toDBMessage(), 41 | senderUserId = authData.userId, NotificationType.NEW_MESSAGE 42 | ) 43 | } 44 | } 45 | 46 | override fun registerChangeInMessage(request: SKWorkspaceChannelRequest): Flow { 47 | return messagesDataSource.registerForChanges(request).map { 48 | SKMessageChangeSnapshot.newBuilder() 49 | .apply { 50 | it.first?.toGrpc()?.let { skMessage -> 51 | previous = skMessage 52 | } 53 | it.second?.toGrpc()?.let { skMessage -> 54 | latest = skMessage 55 | } 56 | } 57 | .build() 58 | }.catch { 59 | it.printStackTrace() 60 | } 61 | } 62 | 63 | override suspend fun getMessages(request: SKWorkspaceChannelRequest): SKMessages { 64 | val messages = messagesDataSource.getMessages( 65 | workspaceId = request.workspaceId, 66 | channelId = request.channelId, 67 | request.paged.limit, 68 | request.paged.offset 69 | ).map { skMessage -> 70 | skMessage.toGrpc() 71 | } 72 | return SKMessages.newBuilder() 73 | .addAllMessages(messages) 74 | .build() 75 | } 76 | 77 | 78 | } 79 | 80 | private fun SkMessage.toGrpc(): SKMessage { 81 | return SKMessage.newBuilder() 82 | .setUuid(this.uuid) 83 | .setCreatedDate(this.createdDate) 84 | .setModifiedDate(this.modifiedDate) 85 | .setWorkspaceId(this.workspaceId) 86 | .setChannelId(this.channelId) 87 | .setSender(this.sender) 88 | .setText(SKEncryptedMessage.parseFrom(this.message)) 89 | .setIsDeleted(this.isDeleted) 90 | .build() 91 | } 92 | 93 | private fun SKMessage.toDBMessage(uuid: String = UUID.randomUUID().toString()): SkMessage { 94 | return SkMessage( 95 | uuid = this.uuid.takeIf { !it.isNullOrEmpty() } ?: uuid, 96 | workspaceId = this.workspaceId, 97 | channelId, 98 | text.toByteArray(), 99 | sender, 100 | createdDate, 101 | modifiedDate, 102 | isDeleted = this.isDeleted 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/impl/ChannelMemberDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.impl 2 | 3 | import com.mongodb.client.model.Filters 4 | import com.mongodb.client.model.changestream.OperationType 5 | import dev.baseio.slackserver.data.models.SkChannel 6 | import dev.baseio.slackserver.data.models.SkChannelMember 7 | import dev.baseio.slackserver.data.sources.ChannelMemberDataSource 8 | import org.bson.Document 9 | import org.litote.kmongo.coroutine.CoroutineDatabase 10 | import org.litote.kmongo.eq 11 | import org.litote.kmongo.match 12 | 13 | class ChannelMemberDataSourceImpl(private val database: CoroutineDatabase) : ChannelMemberDataSource { 14 | 15 | override suspend fun isMember(userId: String, workspaceId: String, channelId: String): Boolean { 16 | return database.getCollection() 17 | .find( 18 | SkChannelMember::workspaceId eq workspaceId, 19 | SkChannelMember::channelId eq channelId, 20 | SkChannelMember::memberId eq userId 21 | ).toList().isNotEmpty() 22 | } 23 | 24 | override suspend fun getChannelIdsForUserAndWorkspace(userId: String, workspaceId: String): List { 25 | return database.getCollection() 26 | .find( 27 | SkChannelMember::workspaceId eq workspaceId, 28 | SkChannelMember::memberId eq userId 29 | ).toList().map { 30 | it.channelId 31 | } 32 | } 33 | 34 | override suspend fun isChannelExistFor(sender: String, receiver: String): SkChannel? { 35 | val possible1 = database.getCollection() 36 | .findOne(SkChannelMember::channelId eq sender + receiver) 37 | val possible2 = database.getCollection() 38 | .findOne(SkChannelMember::channelId eq receiver + sender) 39 | possible1?.let { skChannelMember -> 40 | return skChannel(skChannelMember) 41 | } 42 | possible2?.let { skChannelMember -> 43 | return skChannel(skChannelMember) 44 | } 45 | return null 46 | 47 | } 48 | 49 | private suspend fun skChannel(skChannelMember: SkChannelMember) = 50 | database.getCollection() 51 | .findOne(SkChannel.SkDMChannel::uuid eq skChannelMember.channelId) 52 | ?: database.getCollection() 53 | .findOne(SkChannel.SkGroupChannel::uuid eq skChannelMember.channelId) 54 | 55 | override suspend fun getMembers(workspaceId: String, channelId: String): List { 56 | return database.getCollection() 57 | .find(SkChannelMember::channelId eq channelId, SkChannelMember::workspaceId eq workspaceId) 58 | .toList() 59 | } 60 | 61 | override suspend fun addMembers(listOf: List) { 62 | listOf.forEach { skChannelMember -> 63 | val channelMember = SkChannelMember( 64 | channelId = skChannelMember.channelId, memberId = skChannelMember.memberId, workspaceId = skChannelMember.workspaceId, 65 | channelEncryptedPrivateKey = skChannelMember.channelEncryptedPrivateKey 66 | ) 67 | val memberCollection = database.getCollection() 68 | memberCollection.findOne( 69 | SkChannelMember::channelId eq skChannelMember.channelId, SkChannelMember::memberId eq skChannelMember.memberId 70 | )?.let { existingChannelMember -> 71 | database.getCollection() 72 | .updateOne( 73 | match( 74 | Document.parse("{'fullDocument.memberId': '${existingChannelMember.memberId}'}"), 75 | Document.parse("{'fullDocument.channelId': '${existingChannelMember.channelId}'}"), 76 | Document.parse("{'fullDocument.workspaceId': '${existingChannelMember.workspaceId}'}"), 77 | ), channelMember 78 | ) 79 | } ?: kotlin.run { 80 | database.getCollection() 81 | .insertOne(channelMember) 82 | } 83 | } 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /src/test/kotlin/TestQRCodeService.kt: -------------------------------------------------------------------------------- 1 | import app.cash.turbine.test 2 | import dev.baseio.slackdata.protos.* 3 | import dev.baseio.slackserver.data.database.Database 4 | import dev.baseio.slackserver.data.impl.* 5 | import dev.baseio.slackserver.data.sources.UsersDataSource 6 | import dev.baseio.slackserver.services.* 7 | import dev.baseio.slackserver.services.interceptors.AUTHORIZATION_METADATA_KEY 8 | import dev.baseio.slackserver.services.interceptors.AuthInterceptor 9 | import io.grpc.CallOptions 10 | import io.grpc.ManagedChannel 11 | import io.grpc.Metadata 12 | import io.grpc.inprocess.InProcessChannelBuilder 13 | import io.grpc.inprocess.InProcessServerBuilder 14 | import io.grpc.testing.GrpcCleanupRule 15 | import kotlinx.coroutines.test.runTest 16 | import org.junit.Before 17 | import org.junit.Rule 18 | import org.junit.jupiter.api.Test 19 | import kotlin.test.BeforeTest 20 | import kotlin.time.Duration.Companion.seconds 21 | 22 | 23 | class TestQRCodeService { 24 | 25 | private lateinit var channel: ManagedChannel 26 | 27 | @Rule 28 | val grpcCleanup = GrpcCleanupRule() 29 | 30 | val iQrcodeGenerator : IQrCodeGenerator = FakeQrCodeGenerator() 31 | val workspaceDataSource = WorkspaceDataSourceImpl(Database.slackDB) 32 | val usersDataSource: UsersDataSource = UsersDataSourceImpl(Database.slackDB) 33 | val authDataSource = AuthDataSourceImpl(Database.slackDB) 34 | val authenticationDelegate: AuthenticationDelegate = AuthenticationDelegateImpl(authDataSource, usersDataSource) 35 | 36 | @BeforeTest 37 | fun before(){ 38 | val workspaceService = WorkspaceService(workspaceDataSource = workspaceDataSource, authDelegate = authenticationDelegate) 39 | val qrCodeService = QrCodeService(database = Database.slackDB, qrCodeGenerator = iQrcodeGenerator) 40 | 41 | val serverName: String = InProcessServerBuilder.generateName() 42 | grpcCleanup.register( 43 | InProcessServerBuilder 44 | .forName(serverName).directExecutor() 45 | .addService(workspaceService) 46 | .addService(qrCodeService ) 47 | .intercept(AuthInterceptor()) 48 | .build().start() 49 | ) 50 | channel = grpcCleanup.register( 51 | InProcessChannelBuilder.forName(serverName).directExecutor().build() 52 | ) 53 | } 54 | 55 | @Test 56 | fun `qr code is generated when requested and deleted once unsubscribed by consumer`(){ 57 | runTest{ 58 | val metadata = authorizeTestUser(channel) 59 | val qrCodeServiceClient = QrCodeServiceGrpcKt.QrCodeServiceCoroutineStub(channel) 60 | qrCodeServiceClient.generateQRCode(sKQrCodeGenerator {},headers = metadata).test(50.seconds) { 61 | val item = awaitItem().apply { 62 | this.byteArrayList 63 | } 64 | assert(item.byteArrayList.isNotEmpty()) 65 | qrCodeServiceClient.verifyQrCode(sKQRAuthVerify { 66 | this.token = iQrcodeGenerator.randomToken() 67 | }, headers = metadata) 68 | assert(awaitItem().hasAuthResult()) 69 | awaitComplete() 70 | 71 | assert(iQrcodeGenerator.find(iQrcodeGenerator.randomToken())==null) 72 | } 73 | } 74 | } 75 | 76 | private suspend fun authorizeTestUser(channel: ManagedChannel): Metadata { 77 | val workspaceClient = WorkspaceServiceGrpcKt.WorkspaceServiceCoroutineStub(channel) 78 | val result = workspaceClient.letMeIn( 79 | SKCreateWorkspaceRequest.newBuilder() 80 | .setUser(SKAuthUser.newBuilder().setEmail("anmol.verma4@gmail.com").setPassword("password")) 81 | .setWorkspace( 82 | SKWorkspace.newBuilder() 83 | .setName("gmail") 84 | .build() 85 | ) 86 | .build() 87 | ) 88 | val metadata = Metadata() 89 | metadata.put(AUTHORIZATION_METADATA_KEY, "Bearer " + result.token); 90 | return metadata 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/QrCodeService.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services 2 | 3 | import com.google.zxing.BarcodeFormat 4 | import com.google.zxing.MultiFormatWriter 5 | import com.google.zxing.client.j2se.MatrixToImageWriter 6 | import com.google.zxing.common.BitMatrix 7 | import dev.baseio.slackdata.common.sKByteArrayElement 8 | import dev.baseio.slackdata.protos.* 9 | import dev.baseio.slackserver.data.models.SkUser 10 | import dev.baseio.slackserver.services.interceptors.AUTH_CONTEXT_KEY 11 | import io.grpc.Status 12 | import io.grpc.StatusException 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.channels.awaitClose 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.channelFlow 17 | import kotlinx.coroutines.launch 18 | import org.litote.kmongo.coroutine.CoroutineDatabase 19 | import org.litote.kmongo.eq 20 | import java.io.File 21 | import java.nio.file.Path 22 | import java.time.LocalDateTime 23 | import java.util.* 24 | import kotlin.collections.HashMap 25 | import kotlin.coroutines.CoroutineContext 26 | import kotlin.io.path.deleteIfExists 27 | import kotlin.io.path.fileSize 28 | import kotlin.io.path.inputStream 29 | 30 | 31 | class QrCodeService( 32 | coroutineContext: CoroutineContext = Dispatchers.IO, 33 | private val database: CoroutineDatabase, 34 | private val qrCodeGenerator: IQrCodeGenerator, 35 | ) : QrCodeServiceGrpcKt.QrCodeServiceCoroutineImplBase(coroutineContext) { 36 | 37 | override fun generateQRCode(request: SKQrCodeGenerator): Flow { 38 | val user = AUTH_CONTEXT_KEY.get() 39 | return channelFlow { 40 | val data = user.userId // bad impl, try something secure 41 | val result = qrCodeGenerator.process(data) 42 | send(result.first) // first send the QR code! 43 | qrCodeGenerator.put(data,result) { // when authenticated send the auth result 44 | launch { 45 | send(sKQrCodeResponse { 46 | this.authResult = it 47 | }) 48 | close() 49 | } 50 | } 51 | awaitClose { 52 | qrCodeGenerator.removeQrCode(data) // remove the QR code and corr info! 53 | } 54 | } 55 | } 56 | 57 | 58 | override suspend fun verifyQrCode(request: SKQRAuthVerify): SKAuthResult { 59 | qrCodeGenerator.find(request.token)?.let { 60 | val skUser = database.getCollection().findOne(SkUser::uuid eq request.token) 61 | it.first.deleteIfExists() 62 | val result = skAuthResult(skUser) 63 | qrCodeGenerator.notifyAuthenticated(result, request) 64 | return result 65 | } 66 | throw StatusException(Status.NOT_FOUND) 67 | } 68 | } 69 | 70 | class QrCodeGenerator :IQrCodeGenerator { 71 | override val inMemoryQrCodes: HashMap Unit>> = hashMapOf() 72 | 73 | override fun process(data: String): Pair { 74 | with(generateImage(data)) { 75 | val ins = inputStream(java.nio.file.StandardOpenOption.READ) 76 | val bytes = ins.readAllBytes() 77 | val intBytes = bytes.map { it.toInt() } 78 | return Pair(sKQrCodeResponse { 79 | this.byteArray.addAll(intBytes.map { sKByteArrayElement { this.byte = it } }) 80 | this.totalSize = fileSize() 81 | }.also { 82 | ins.close() 83 | }, this) 84 | } 85 | } 86 | 87 | private fun generateImage(data: String): Path { 88 | val validTill = LocalDateTime.now().plusSeconds(120) 89 | val matrix: BitMatrix = MultiFormatWriter().encode( 90 | data, 91 | BarcodeFormat.QR_CODE, 512, 512 92 | ) 93 | val path = File.createTempFile(data, validTill.toString()).toPath() 94 | MatrixToImageWriter.writeToPath(matrix, "png", path).apply { 95 | return path 96 | } 97 | } 98 | 99 | override fun randomToken(): String { 100 | return UUID.randomUUID().toString() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/UserService.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services 2 | 3 | 4 | import dev.baseio.slackdata.common.Empty 5 | import dev.baseio.slackdata.common.sKByteArrayElement 6 | import dev.baseio.slackdata.protos.* 7 | import dev.baseio.slackserver.data.models.SKUserPublicKey 8 | import dev.baseio.slackserver.data.models.SkUser 9 | import dev.baseio.slackserver.data.sources.UsersDataSource 10 | import dev.baseio.slackserver.services.interceptors.AUTH_CONTEXT_KEY 11 | import io.grpc.Status 12 | import io.grpc.StatusException 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.map 16 | import java.util.UUID 17 | import kotlin.coroutines.CoroutineContext 18 | 19 | class UserService(coroutineContext: CoroutineContext = Dispatchers.IO, private val usersDataSource: UsersDataSource) : 20 | UsersServiceGrpcKt.UsersServiceCoroutineImplBase(coroutineContext) { 21 | override suspend fun updateSKUser(request: SKUser): SKUser { 22 | return usersDataSource.updateUser(request.toDBUser())?.toGrpc() 23 | ?: throw StatusException(Status.NOT_FOUND) 24 | } 25 | 26 | override suspend fun currentLoggedInUser(request: Empty): SKUser { 27 | val authData = AUTH_CONTEXT_KEY.get() ?: throw StatusException(Status.UNAUTHENTICATED) 28 | return usersDataSource.getUser(authData.userId, authData.workspaceId)?.toGrpc() 29 | ?: throw StatusException(Status.UNAUTHENTICATED) 30 | } 31 | 32 | override fun registerChangeInUsers(request: SKWorkspaceChannelRequest): Flow { 33 | return usersDataSource.getChangeInUserFor(request.workspaceId).map { skUser -> 34 | SKUserChangeSnapshot.newBuilder() 35 | .apply { 36 | skUser.first?.toGrpc()?.let { skMessage -> 37 | previous = skMessage 38 | } 39 | skUser.second?.toGrpc()?.let { skMessage -> 40 | latest = skMessage 41 | } 42 | } 43 | .build() 44 | } 45 | } 46 | 47 | override suspend fun getUsers(request: SKWorkspaceChannelRequest): SKUsers { 48 | return usersDataSource.getUsers(request.workspaceId).map { user -> 49 | user.toGrpc() 50 | }.run { 51 | SKUsers.newBuilder() 52 | .addAllUsers(this) 53 | .build() 54 | } 55 | } 56 | 57 | override suspend fun saveUser(request: SKUser): SKUser { 58 | return usersDataSource 59 | .saveUser(request.toDBUser()) 60 | ?.toGrpc() ?: throw StatusException(Status.ABORTED) 61 | } 62 | 63 | 64 | } 65 | 66 | fun SkUser.toGrpc(): SKUser { 67 | return SKUser.newBuilder() 68 | .setUuid(this.uuid) 69 | .setWorkspaceId(this.workspaceId) 70 | .setPhone(this.phone) 71 | .setAvatarUrl(this.avatarUrl) 72 | .setGender(this.gender) 73 | .setName(this.name) 74 | .setUserSince(this.userSince.toLong()) 75 | .setUsername(this.username) 76 | .setEmail(this.email) 77 | .setLocation(this.location) 78 | .setPublicKey( 79 | SlackKey.newBuilder() 80 | .addAllKeybytes(this.publicKey.keyBytes.map { 81 | sKByteArrayElement { 82 | this.byte = it.toInt() 83 | } 84 | }) 85 | .build() 86 | ) 87 | .build() 88 | } 89 | 90 | fun SKUser.toDBUser(userId: String = UUID.randomUUID().toString()): SkUser { 91 | return SkUser( 92 | this.uuid.takeIf { !it.isNullOrEmpty() } ?: userId, 93 | this.workspaceId, 94 | this.gender, 95 | this.name.takeIf { !it.isNullOrEmpty() } ?: this.email.split("@").first(), 96 | this.location, 97 | this.email, 98 | this.username.takeIf { !it.isNullOrEmpty() } ?: this.email.split("@").first(), 99 | this.userSince, 100 | this.phone, 101 | this.avatarUrl.takeIf { !it.isNullOrEmpty() } ?: "https://picsum.photos/300/300", 102 | SKUserPublicKey(keyBytes = this.publicKey.keybytesList.map { it.byte.toByte() }.toByteArray()) 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/WorkspaceService.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services 2 | 3 | import dev.baseio.slackdata.common.Empty 4 | import dev.baseio.slackdata.protos.* 5 | import dev.baseio.slackserver.data.models.SkWorkspace 6 | import dev.baseio.slackserver.data.sources.WorkspaceDataSource 7 | import dev.baseio.slackserver.services.interceptors.AUTH_CONTEXT_KEY 8 | import io.grpc.Status 9 | import io.grpc.StatusException 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.catch 13 | import kotlinx.coroutines.flow.map 14 | import java.util.UUID 15 | import kotlin.coroutines.CoroutineContext 16 | 17 | class WorkspaceService( 18 | coroutineContext: CoroutineContext = Dispatchers.IO, 19 | private val workspaceDataSource: WorkspaceDataSource, 20 | private val authDelegate: AuthenticationDelegate 21 | ) : 22 | WorkspaceServiceGrpcKt.WorkspaceServiceCoroutineImplBase(coroutineContext), AuthenticationDelegate by authDelegate { 23 | 24 | override suspend fun updateWorkspace(request: SKWorkspace): SKWorkspace { 25 | val authData = AUTH_CONTEXT_KEY.get() 26 | //todo authorize this request! 27 | return workspaceDataSource.updateWorkspace(request.toDBWorkspace())?.toGRPC() 28 | ?: throw StatusException(Status.NOT_FOUND) 29 | } 30 | 31 | override fun registerChangeInWorkspace(request: SKWorkspace): Flow { 32 | return workspaceDataSource.registerForChanges(request.uuid).map { 33 | SKWorkspaceChangeSnapshot.newBuilder() 34 | .apply { 35 | it.first?.toGRPC()?.let { skMessage -> 36 | previous = skMessage 37 | } 38 | it.second?.toGRPC()?.let { skMessage -> 39 | latest = skMessage 40 | } 41 | } 42 | .build() 43 | }.catch { 44 | it.printStackTrace() 45 | } 46 | } 47 | 48 | override suspend fun findWorkspaceForName(request: SKFindWorkspacesRequest): SKWorkspace { 49 | return workspaceDataSource.findWorkspaceForName(request.name)?.let { workspace -> 50 | sKWorkspace { 51 | uuid = workspace.uuid 52 | modifiedTime = workspace.modifiedTime 53 | picUrl = workspace.picUrl ?: "" 54 | domain = workspace.domain 55 | name = workspace.name 56 | } 57 | } ?: kotlin.run { 58 | throw StatusException(Status.NOT_FOUND) 59 | } 60 | } 61 | 62 | override suspend fun findWorkspacesForEmail(request: SKFindWorkspacesRequest): SKWorkspaces { 63 | val workspaces = workspaceDataSource.findWorkspacesForEmail(request.email) 64 | return SKWorkspaces.newBuilder() 65 | .addAllWorkspaces(workspaces.map { workspace -> 66 | sKWorkspace { 67 | uuid = workspace.uuid ?: "" 68 | modifiedTime = workspace.modifiedTime 69 | picUrl = workspace.picUrl ?: "" 70 | domain = workspace.domain ?: "" 71 | name = workspace.name ?: "" 72 | } 73 | }) 74 | .build() 75 | } 76 | 77 | override suspend fun letMeIn(request: SKCreateWorkspaceRequest): SKWorkspace { 78 | return workspaceDataSource.findWorkspaceForName(request.workspace.name)?.let { 79 | //if workspace exists then authenticateUser! 80 | processRequestForEmail(request.user, workspaceId = it.uuid) 81 | it.toGRPC() 82 | } ?: run { 83 | val savedWorkspace = workspaceDataSource 84 | .saveWorkspace(request.workspace.toDBWorkspace()) 85 | ?.toGRPC() ?: throw StatusException(Status.ABORTED) 86 | processRequestForEmail(request.user, workspaceId = savedWorkspace.uuid) 87 | savedWorkspace 88 | } 89 | } 90 | 91 | override suspend fun getWorkspaces(request: Empty): SKWorkspaces { 92 | val authData = AUTH_CONTEXT_KEY.get() 93 | return SKWorkspaces.newBuilder() 94 | .addWorkspaces(workspaceDataSource.getWorkspace(authData.workspaceId)?.toGRPC()) 95 | .build() 96 | } 97 | } 98 | 99 | fun SkWorkspace.toGRPC(): SKWorkspace { 100 | val dbWorkspace = this 101 | return SKWorkspace.newBuilder() 102 | .setUuid(dbWorkspace.uuid) 103 | .setName(dbWorkspace.name) 104 | .setDomain(dbWorkspace.domain) 105 | .setModifiedTime(dbWorkspace.modifiedTime) 106 | .setPicUrl(dbWorkspace.picUrl) 107 | .build() 108 | } 109 | 110 | fun SKWorkspace.toDBWorkspace(workspaceId: String = UUID.randomUUID().toString()): SkWorkspace { 111 | return SkWorkspace( 112 | this.uuid.takeIf { !it.isNullOrEmpty() } ?: workspaceId, 113 | this.name, 114 | this.domain.takeIf { !it.isNullOrEmpty() } ?: "$name.slack.com", 115 | this.picUrl.takeIf { !it.isNullOrEmpty() } ?: "https://picsum.photos/300/300", 116 | this.modifiedTime 117 | ) 118 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/data/impl/ChannelsDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.data.impl 2 | 3 | import com.mongodb.client.model.Filters 4 | import com.mongodb.client.model.changestream.OperationType 5 | import dev.baseio.slackserver.data.sources.ChannelsDataSource 6 | import dev.baseio.slackserver.data.models.SkChannel 7 | import dev.baseio.slackserver.data.models.SkChannelMember 8 | import dev.baseio.slackserver.data.sources.ChannelMemberDataSource 9 | import io.grpc.Status 10 | import io.grpc.StatusException 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.mapNotNull 13 | import org.bson.Document 14 | import org.bson.conversions.Bson 15 | import org.litote.kmongo.coroutine.CoroutineDatabase 16 | import org.litote.kmongo.eq 17 | import org.litote.kmongo.`in` 18 | import org.litote.kmongo.match 19 | 20 | class ChannelsDataSourceImpl( 21 | private val slackCloneDB: CoroutineDatabase, 22 | private val channelMemberDataSource: ChannelMemberDataSource 23 | ) : ChannelsDataSource { 24 | 25 | override suspend fun getAllChannels(workspaceId: String, userId: String): List { 26 | val userChannels = channelMemberDataSource.getChannelIdsForUserAndWorkspace(userId, workspaceId) 27 | return slackCloneDB.getCollection() 28 | .find(SkChannel.SkGroupChannel::uuid `in` userChannels) 29 | .toList() 30 | } 31 | 32 | override suspend fun getAllDMChannels(workspaceId: String, userId: String): List { 33 | val userChannels = channelMemberDataSource.getChannelIdsForUserAndWorkspace(userId, workspaceId) 34 | return slackCloneDB.getCollection() 35 | .find(SkChannel.SkDMChannel::uuid `in` userChannels) 36 | .toList() 37 | } 38 | 39 | override suspend fun checkIfGroupExisits(workspaceId: String?, name: String?): Boolean { 40 | return slackCloneDB.getCollection() 41 | .findOne(SkChannel.SkGroupChannel::workspaceId eq workspaceId, SkChannel.SkGroupChannel::name eq name) != null 42 | } 43 | 44 | override suspend fun checkIfDMChannelExists(userId: String, receiverId: String?): SkChannel.SkDMChannel? { 45 | return slackCloneDB.getCollection() 46 | .findOne(SkChannel.SkDMChannel::senderId eq userId, SkChannel.SkDMChannel::receiverId eq receiverId) 47 | ?: slackCloneDB.getCollection() 48 | .findOne(SkChannel.SkDMChannel::senderId eq receiverId, SkChannel.SkDMChannel::receiverId eq userId) 49 | } 50 | 51 | override suspend fun getChannelById(channelId: String, workspaceId: String): SkChannel? { 52 | return slackCloneDB.getCollection() 53 | .findOne(SkChannel.SkGroupChannel::uuid eq channelId, 54 | SkChannel.SkGroupChannel::workspaceId eq workspaceId) ?: slackCloneDB.getCollection() 55 | .findOne(SkChannel.SkGroupChannel::uuid eq channelId, 56 | SkChannel.SkGroupChannel::workspaceId eq workspaceId) 57 | } 58 | 59 | override suspend fun getChannelByName(channelId: String, workspaceId: String): SkChannel? { 60 | return slackCloneDB.getCollection() 61 | .findOne(SkChannel.SkGroupChannel::name eq channelId, 62 | SkChannel.SkGroupChannel::workspaceId eq workspaceId)?:slackCloneDB.getCollection() 63 | .findOne(SkChannel.SkGroupChannel::name eq channelId, 64 | SkChannel.SkGroupChannel::workspaceId eq workspaceId) 65 | } 66 | 67 | override suspend fun savePublicChannel( 68 | request: SkChannel.SkGroupChannel, 69 | adminId: String 70 | ): SkChannel.SkGroupChannel? { 71 | slackCloneDB.getCollection() 72 | .insertOne(request) 73 | return slackCloneDB.getCollection() 74 | .findOne(SkChannel.SkGroupChannel::uuid eq request.uuid) 75 | } 76 | 77 | override suspend fun saveDMChannel(request: SkChannel.SkDMChannel): SkChannel.SkDMChannel? { 78 | slackCloneDB.getCollection() 79 | .insertOne(request) 80 | return slackCloneDB.getCollection() 81 | .findOne(SkChannel.SkDMChannel::uuid eq request.uuid) 82 | } 83 | 84 | override fun getDMChannelChangeStream(workspaceId: String): Flow> { 85 | val collection = slackCloneDB.getCollection() 86 | 87 | val pipeline: List = listOf( 88 | match( 89 | Document.parse("{'fullDocument.workId': '$workspaceId'}"), 90 | Filters.`in`("operationType", OperationType.values().map { it.value }.toList()) 91 | ) 92 | ) 93 | 94 | return collection 95 | .watch(pipeline).toFlow().mapNotNull { 96 | Pair(it.fullDocumentBeforeChange, it.fullDocument) 97 | } 98 | } 99 | 100 | override fun getChannelChangeStream(workspaceId: String): Flow> { 101 | val flowGroupChannel = slackCloneDB.getCollection() 102 | .watch( 103 | listOf( 104 | match( 105 | Document.parse("{'fullDocument.workId': '$workspaceId'}"), 106 | Filters.`in`("operationType", OperationType.values().map { it.value }.toList()) 107 | ) 108 | ) 109 | ).toFlow() 110 | 111 | return flowGroupChannel.mapNotNull { 112 | Pair(it.fullDocumentBeforeChange, it.fullDocument) 113 | } 114 | } 115 | 116 | override fun getChannelMemberChangeStream( 117 | workspaceId: String, 118 | memberId: String 119 | ): Flow> { 120 | val flowGroupChannel = slackCloneDB.getCollection() 121 | .watch( 122 | listOf( 123 | match( 124 | Document.parse("{'fullDocument.workspaceId': '$workspaceId'}"), 125 | Document.parse("{'fullDocument.memberId': '$memberId'}"), 126 | Filters.`in`("operationType", OperationType.values().map { it.value }.toList()) 127 | ) 128 | ) 129 | ).toFlow() 130 | 131 | return flowGroupChannel.mapNotNull { 132 | Pair(it.fullDocumentBeforeChange, it.fullDocument) 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/communications/SlackEmailHelper.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.communications 2 | 3 | import java.util.* 4 | import javax.mail.* 5 | import javax.mail.internet.InternetAddress 6 | import javax.mail.internet.MimeMessage 7 | 8 | 9 | object SlackEmailHelper { 10 | fun sendEmail(toEmail: String,link:String) { 11 | // Get a Properties object 12 | val properties: Properties = System.getProperties() 13 | properties.put("mail.smtp.host", "smtp.gmail.com"); 14 | properties.put("mail.smtp.port", "587"); 15 | properties.put("mail.smtp.auth", "true"); 16 | properties.put("mail.smtp.starttls.enable", "true"); 17 | properties.put("mail.smtp.starttls.required", "true"); 18 | properties.put("mail.smtp.ssl.protocols", "TLSv1.2"); 19 | 20 | val username = System.getenv("email.from") 21 | val password = System.getenv("email.password") 22 | val session: Session = Session.getDefaultInstance(properties, object : Authenticator() { 23 | override fun getPasswordAuthentication(): PasswordAuthentication { 24 | return PasswordAuthentication(username, password) 25 | } 26 | }) 27 | 28 | // -- Create a new message -- 29 | val msg: Message = MimeMessage(session) 30 | 31 | // -- Set the FROM and TO fields -- 32 | msg.setFrom(InternetAddress(System.getenv("email.username"))) 33 | msg.setRecipients( 34 | Message.RecipientType.TO, 35 | InternetAddress.parse(toEmail, false) 36 | ) 37 | msg.subject = "Confirm your email address on SlackClone" 38 | msg.setContent(emailTemplate(toEmail,link).also { println(it) }, "text/html") 39 | msg.sentDate = Date() 40 | Transport.send(msg) 41 | } 42 | } 43 | 44 | fun emailTemplate(toEmail: String, link: String) = "\n" + 45 | "\n" + 46 | "\n" + 47 | "
\n" + 48 | " \n" + 49 | " \n" + 95 | "
\n" + 50 | "
\n" + 51 | "
\n" + 53 | "
\"slack
\n" + 57 | "

Confirm your email\n" + 58 | " address to get started on Slack

\n" + 59 | "

Once you’ve confirmed that $toEmail is your email address,\n" + 64 | " we’ll help you find your Slack workspaces or\n" + 65 | " create a new one.

\n" + 66 | "

\"\uD83D\uDCF1\"\n" From your mobile device, tap\n" + 70 | " the button below to confirm:

\n" + 71 | " \n" + 72 | " \n" + 73 | "Click here!"+ 74 | " \n" + 75 | " \n" + 85 | " \n" + 86 | " \n" + 87 | "
Confirm Email Address
\n" + 88 | "

If you didn’t request this email,\n" + 91 | " there’s nothing to worry about — you can safely ignore it.

\n" + 92 | "
\n" + 93 | "
\n" + 94 | "
\n" + 96 | "
\n" + 97 | "\n" + 98 | "\n" + 99 | "" -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/slackserver/services/ChannelService.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.slackserver.services 2 | 3 | import dev.baseio.security.CapillaryInstances 4 | import dev.baseio.security.EncryptedData 5 | import dev.baseio.security.JVMKeyStoreRsaUtils 6 | import dev.baseio.slackdata.common.sKByteArrayElement 7 | import dev.baseio.slackdata.protos.* 8 | import dev.baseio.slackserver.communications.NotificationType 9 | import dev.baseio.slackserver.communications.PNSender 10 | import dev.baseio.slackserver.data.models.SKUserPublicKey 11 | import dev.baseio.slackserver.data.sources.ChannelsDataSource 12 | import dev.baseio.slackserver.data.models.SkChannel 13 | import dev.baseio.slackserver.data.models.SkChannelMember 14 | import dev.baseio.slackserver.data.sources.ChannelMemberDataSource 15 | import dev.baseio.slackserver.data.sources.UsersDataSource 16 | import dev.baseio.slackserver.services.interceptors.AUTH_CONTEXT_KEY 17 | import dev.baseio.slackserver.services.interceptors.AuthData 18 | import io.grpc.Status 19 | import io.grpc.StatusException 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.flow.map 23 | import java.util.* 24 | import kotlin.coroutines.CoroutineContext 25 | 26 | class ChannelService( 27 | coroutineContext: CoroutineContext = Dispatchers.IO, 28 | private val channelsDataSource: ChannelsDataSource, 29 | private val channelMemberDataSource: ChannelMemberDataSource, 30 | private val usersDataSource: UsersDataSource, 31 | private val channelPNSender: PNSender, 32 | private val channelMemberPNSender: PNSender, 33 | ) : 34 | ChannelsServiceGrpcKt.ChannelsServiceCoroutineImplBase(coroutineContext) { 35 | 36 | override suspend fun inviteUserToChannel(request: SKInviteUserChannel): SKChannelMembers { 37 | val userData = AUTH_CONTEXT_KEY.get() 38 | return inviteUserWithAuthData(request, userData) 39 | } 40 | 41 | 42 | override suspend fun joinChannel(request: SKChannelMember): SKChannelMember { 43 | channelMemberDataSource.addMembers(listOf(request.toDBMember())) 44 | return request 45 | } 46 | 47 | override suspend fun channelMembers(request: SKWorkspaceChannelRequest): SKChannelMembers { 48 | return channelMemberDataSource.getMembers(request.workspaceId, request.channelId).run { 49 | SKChannelMembers.newBuilder() 50 | .addAllMembers(this.map { it.toGRPC() }) 51 | .build() 52 | } 53 | } 54 | 55 | override suspend fun getAllChannels(request: SKChannelRequest): SKChannels { 56 | val userData = AUTH_CONTEXT_KEY.get() 57 | return channelsDataSource.getAllChannels(request.workspaceId, userData.userId).run { 58 | SKChannels.newBuilder() 59 | .addAllChannels(this.map { it.toGRPC() }) 60 | .build() 61 | } 62 | } 63 | 64 | override suspend fun getAllDMChannels(request: SKChannelRequest): SKDMChannels { 65 | val userData = AUTH_CONTEXT_KEY.get() 66 | return channelsDataSource.getAllDMChannels(request.workspaceId, userData.userId).run { 67 | SKDMChannels.newBuilder() 68 | .addAllChannels(this.map { it.toGRPC() }) 69 | .build() 70 | } 71 | } 72 | 73 | override suspend fun savePublicChannel(request: SKChannel): SKChannel { 74 | val authData = AUTH_CONTEXT_KEY.get() 75 | val previousChannelExists = channelsDataSource.checkIfGroupExisits(request.workspaceId, request.name) 76 | if (previousChannelExists) { 77 | val groupChannel = 78 | channelsDataSource.getChannelByName(request.name, request.workspaceId) as SkChannel.SkGroupChannel 79 | return groupChannel.toGRPC() 80 | } 81 | 82 | val skGroupChannel = request.toDBChannel() 83 | 84 | val userPublicKey = skUserPublicKey(authData, request) 85 | val skChannelSlackKeyPair = with(CapillaryInstances.getInstance(skGroupChannel.uuid)) { 86 | val publicKeyChannel = publicKey().encoded //create the channel public key! 87 | // save the channel with the public key of channel 88 | val saved = channelsDataSource.savePublicChannel( 89 | skGroupChannel.copy(channelPublicKey = SKUserPublicKey(publicKeyChannel)), 90 | adminId = authData.userId 91 | )?.toGRPC() 92 | 93 | val channelPrivateKey = 94 | encrypt(privateKey().encoded, userPublicKey.toPublicKey()).toSlackKey() 95 | JVMKeyStoreRsaUtils.deleteKeyPair(keychainId) 96 | Pair(saved, channelPrivateKey) 97 | } 98 | 99 | // invite the user to his channel 100 | inviteUserWithAuthData(sKInviteUserChannel { 101 | this.channelId = skGroupChannel.uuid 102 | this.userId = authData.userId 103 | this.channelPrivateKey = skChannelSlackKeyPair.second 104 | }, authData) 105 | 106 | sendPushNotifyChannelCreated(skGroupChannel, authData) 107 | 108 | return skChannelSlackKeyPair.first ?: throw StatusException(Status.FAILED_PRECONDITION) 109 | 110 | } 111 | 112 | private fun sendPushNotifyChannelCreated( 113 | channel: SkChannel.SkGroupChannel, 114 | authData: AuthData 115 | ) { 116 | channelPNSender.sendPushNotifications( 117 | channel, 118 | authData.userId, 119 | NotificationType.CHANNEL_CREATED 120 | ) 121 | } 122 | 123 | private suspend fun skUserPublicKey( 124 | authData: AuthData, 125 | request: SKChannel 126 | ) = usersDataSource.getUser( 127 | authData.userId, 128 | request.workspaceId 129 | )!!.publicKey 130 | 131 | override suspend fun saveDMChannel(request: SKDMChannel): SKDMChannel { 132 | val authData = AUTH_CONTEXT_KEY.get() 133 | val previousChannel = channelsDataSource.checkIfDMChannelExists(request.senderId, request.receiverId) 134 | previousChannel?.let { 135 | return it.toGRPC() 136 | } ?: kotlin.run { 137 | val keyManager = CapillaryInstances.getInstance(request.uuid) 138 | val publicKeyChannel = keyManager.publicKey().encoded 139 | val channel = dbChannel(request, publicKeyChannel) 140 | val savedChannel = channelsDataSource.saveDMChannel(channel)?.toGRPC()!! 141 | inviteUserWithAuthData(sKInviteUserChannel { 142 | this.channelId = savedChannel.uuid 143 | this.userId = request.senderId 144 | 145 | val userPublicKey = usersDataSource.getUser( 146 | request.senderId, 147 | request.workspaceId 148 | )!!.publicKey 149 | this.channelPrivateKey = 150 | keyManager.encrypt(keyManager.privateKey().encoded, userPublicKey.toPublicKey()) 151 | .toSlackKey() 152 | }, authData) 153 | inviteUserWithAuthData(sKInviteUserChannel { 154 | this.channelId = savedChannel.uuid 155 | this.userId = request.receiverId 156 | 157 | val userPublicKey = usersDataSource.getUser( 158 | request.receiverId, 159 | request.workspaceId 160 | )!!.publicKey 161 | this.channelPrivateKey = 162 | keyManager.encrypt(keyManager.privateKey().encoded, userPublicKey.toPublicKey()) 163 | .toSlackKey() 164 | 165 | }, authData) 166 | channelPNSender.sendPushNotifications( 167 | channel, 168 | authData.userId, 169 | NotificationType.DM_CHANNEL_CREATED 170 | ) 171 | JVMKeyStoreRsaUtils.deleteKeyPair(keyManager.keychainId) 172 | return savedChannel 173 | } 174 | } 175 | 176 | private suspend fun inviteUserWithAuthData( 177 | request: SKInviteUserChannel, 178 | userData: AuthData 179 | ): SKChannelMembers { 180 | val user = usersDataSource.getUserWithUsername(userName = request.userId, userData.workspaceId) 181 | ?: usersDataSource.getUserWithUserId(userId = request.userId, userData.workspaceId) 182 | val channel = 183 | channelsDataSource.getChannelById(request.channelId, userData.workspaceId) 184 | ?: channelsDataSource.getChannelByName( 185 | request.channelId, 186 | userData.workspaceId 187 | ) 188 | user?.let { safeUser -> 189 | channel?.let { channel -> 190 | joinChannel(sKChannelMember { 191 | this.channelId = channel.channelId 192 | this.memberId = safeUser.uuid 193 | this.workspaceId = userData.workspaceId 194 | this.channelPrivateKey = request.channelPrivateKey 195 | }.also { 196 | channelMemberPNSender.sendPushNotifications( 197 | it.toDBMember(), 198 | userData.userId, 199 | NotificationType.ADDED_CHANNEL 200 | ) 201 | }) 202 | 203 | return channelMembers(sKWorkspaceChannelRequest { 204 | this.channelId = channel.channelId 205 | this.workspaceId = userData.workspaceId 206 | }) 207 | } ?: run { 208 | throw StatusException(Status.NOT_FOUND) 209 | } 210 | 211 | } ?: run { 212 | throw StatusException(Status.NOT_FOUND) 213 | } 214 | } 215 | 216 | private fun dbChannel( 217 | request: SKDMChannel, 218 | publicKeyChannel: ByteArray 219 | ): SkChannel.SkDMChannel { 220 | val channel = request.copy { 221 | uuid = request.uuid.takeIf { it.isNotEmpty() } ?: UUID.randomUUID().toString() 222 | createdDate = System.currentTimeMillis() 223 | modifiedDate = System.currentTimeMillis() 224 | publicKey = slackKey { 225 | this.keybytes.addAll(publicKeyChannel.map { 226 | sKByteArrayElement { 227 | this.byte = it.toInt() 228 | } 229 | }) 230 | } 231 | }.toDBChannel() 232 | return channel 233 | } 234 | 235 | override fun registerChangeInChannelMembers(request: SKChannelMember): Flow { 236 | return channelsDataSource.getChannelMemberChangeStream(request.workspaceId, request.memberId).map { skChannel -> 237 | SKChannelMemberChangeSnapshot.newBuilder() 238 | .apply { 239 | skChannel.first?.toGRPC()?.let { skChannel1 -> 240 | previous = skChannel1 241 | } 242 | skChannel.second?.toGRPC()?.let { skChannel1 -> 243 | latest = skChannel1 244 | } 245 | } 246 | .build() 247 | } 248 | } 249 | 250 | override fun registerChangeInChannels(request: SKChannelRequest): Flow { 251 | val authData = AUTH_CONTEXT_KEY.get() 252 | return channelsDataSource.getChannelChangeStream(request.workspaceId).map { skChannel -> 253 | SKChannelChangeSnapshot.newBuilder() 254 | .apply { 255 | skChannel.first?.toGRPC()?.let { skChannel1 -> 256 | val isMember = 257 | channelMemberDataSource.isMember(authData.userId, request.workspaceId, skChannel1.uuid) 258 | if (isMember) { 259 | previous = skChannel1 260 | } 261 | } 262 | skChannel.second?.toGRPC()?.let { skChannel1 -> 263 | val isMember = 264 | channelMemberDataSource.isMember(authData.userId, request.workspaceId, skChannel1.uuid) 265 | if (isMember) { 266 | latest = skChannel1 267 | } 268 | } 269 | } 270 | .build() 271 | } 272 | } 273 | 274 | override fun registerChangeInDMChannels(request: SKChannelRequest): Flow { 275 | return channelsDataSource.getDMChannelChangeStream(request.workspaceId).map { skChannel -> 276 | SKDMChannelChangeSnapshot.newBuilder() 277 | .apply { 278 | skChannel.first?.toGRPC()?.let { skMessage -> 279 | previous = skMessage 280 | } 281 | skChannel.second?.toGRPC()?.let { skMessage -> 282 | latest = skMessage 283 | } 284 | } 285 | .build() 286 | } 287 | } 288 | } 289 | 290 | private fun EncryptedData.toSlackKey(): SKEncryptedMessage { 291 | return SKEncryptedMessage.newBuilder() 292 | .setFirst(first) 293 | .setSecond(second) 294 | .build() 295 | } 296 | 297 | fun SKUserPublicKey.toPublicKey(): dev.baseio.security.PublicKey { 298 | return JVMKeyStoreRsaUtils.getPublicKeyFromBytes(this.keyBytes) 299 | } 300 | 301 | private fun SKChannelMember.toDBMember(): SkChannelMember { 302 | return SkChannelMember( 303 | this.workspaceId, 304 | this.channelId, 305 | this.memberId, 306 | this.channelPrivateKey.toSKEncryptedMessage() 307 | ).apply { 308 | this@toDBMember.uuid?.takeIf { it.isNotEmpty() }?.let { 309 | this.uuid = this@toDBMember.uuid 310 | } 311 | } 312 | } 313 | 314 | fun SKEncryptedMessage.toSKEncryptedMessage(): dev.baseio.slackserver.data.models.SKEncryptedMessage { 315 | return dev.baseio.slackserver.data.models.SKEncryptedMessage( 316 | this.first, 317 | this.second 318 | ) 319 | } 320 | 321 | 322 | private fun SlackKey.toSKUserPublicKey(): SKUserPublicKey { 323 | return SKUserPublicKey(this.keybytesList.map { it.byte.toByte() }.toByteArray()) 324 | } 325 | 326 | fun SkChannelMember.toGRPC(): SKChannelMember { 327 | val member = this 328 | return sKChannelMember { 329 | this.uuid = member.uuid 330 | this.channelId = member.channelId 331 | this.workspaceId = member.workspaceId 332 | this.memberId = member.memberId 333 | this.channelPrivateKey = sKEncryptedMessage { 334 | this.first = member.channelEncryptedPrivateKey!!.first 335 | this.second = member.channelEncryptedPrivateKey.second 336 | } 337 | } 338 | } 339 | 340 | fun SKDMChannel.toDBChannel( 341 | ): SkChannel.SkDMChannel { 342 | return SkChannel.SkDMChannel( 343 | this.uuid, 344 | this.workspaceId, 345 | this.senderId, 346 | this.receiverId, 347 | createdDate, 348 | modifiedDate, 349 | isDeleted, 350 | SKUserPublicKey(keyBytes = this.publicKey.keybytesList.map { it.byte.toByte() }.toByteArray()) 351 | ) 352 | } 353 | 354 | fun SKChannel.toDBChannel(): SkChannel.SkGroupChannel { 355 | return SkChannel.SkGroupChannel( 356 | this.uuid.takeIf { !it.isNullOrEmpty() } ?: UUID.randomUUID().toString(), 357 | this.workspaceId, 358 | this.name, 359 | createdDate, 360 | modifiedDate, 361 | avatarUrl, 362 | isDeleted, 363 | SKUserPublicKey(keyBytes = this.publicKey.keybytesList.map { it.byte.toByte() }.toByteArray()) 364 | ) 365 | } 366 | 367 | fun SkChannel.SkGroupChannel.toGRPC(): SKChannel { 368 | return SKChannel.newBuilder() 369 | .setUuid(this.uuid) 370 | .setAvatarUrl(this.avatarUrl ?: "") 371 | .setName(this.name) 372 | .setCreatedDate(this.createdDate) 373 | .setWorkspaceId(this.workspaceId) 374 | .setModifiedDate(this.modifiedDate) 375 | .setPublicKey(SlackKey.newBuilder().addAllKeybytes(this.publicKey.keyBytes.map { 376 | sKByteArrayElement { 377 | this.byte = it.toInt() 378 | } 379 | }).build()) 380 | .build() 381 | } 382 | 383 | fun SkChannel.SkDMChannel.toGRPC(): SKDMChannel { 384 | return SKDMChannel.newBuilder() 385 | .setUuid(this.uuid) 386 | .setCreatedDate(this.createdDate) 387 | .setModifiedDate(this.modifiedDate) 388 | .setIsDeleted(this.deleted) 389 | .setReceiverId(this.receiverId) 390 | .setSenderId(this.senderId) 391 | .setWorkspaceId(this.workspaceId) 392 | .setPublicKey(SlackKey.newBuilder().addAllKeybytes(this.publicKey.keyBytes.map { 393 | sKByteArrayElement { 394 | this.byte = it.toInt() 395 | } 396 | }).build()) 397 | .build() 398 | } 399 | --------------------------------------------------------------------------------