Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • api-redesign
  • main
  • 0.10.0
  • 0.10.1
  • 0.10.2
  • 0.7.0
  • 0.8.0
  • 0.8.1
  • 0.9.0
  • 0.9.1
  • 0.9.2
11 results

Target

Select target project
  • justJanne/libquassel
1 result
Select Git revision
  • api-redesign
  • main
  • 0.10.0
  • 0.10.1
  • 0.10.2
  • 0.7.0
  • 0.8.0
  • 0.8.1
  • 0.9.0
  • 0.9.1
  • 0.9.2
11 results
Show changes
Showing
with 360 additions and 282 deletions
......@@ -9,9 +9,13 @@
package de.justjanne.libquassel.client
import androidx.room.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import dagger.Binds
import dagger.Component
import dagger.Module
import dagger.Provides
import de.justjanne.bitflags.of
import de.justjanne.libquassel.backend.AliasManagerPersister
import de.justjanne.libquassel.backend.BacklogManagerPersister
import de.justjanne.libquassel.backend.BufferSyncerPersister
......@@ -28,8 +32,29 @@ import de.justjanne.libquassel.backend.IrcUserPersister
import de.justjanne.libquassel.backend.NetworkConfigPersister
import de.justjanne.libquassel.backend.NetworkPersister
import de.justjanne.libquassel.backend.RpcPersister
import de.justjanne.libquassel.connection.ChannelConnection
import de.justjanne.libquassel.connection.ClientHandshakeHandler
import de.justjanne.libquassel.connection.ClientSessionHandler
import de.justjanne.libquassel.connection.MagicHandler
import de.justjanne.libquassel.persistence.AliasEntity
import de.justjanne.libquassel.persistence.AppDatabase
import de.justjanne.libquassel.protocol.api.ObjectName
import de.justjanne.libquassel.protocol.api.client.*
import de.justjanne.libquassel.protocol.api.client.AliasManagerClientApi
import de.justjanne.libquassel.protocol.api.client.BacklogManagerClientApi
import de.justjanne.libquassel.protocol.api.client.BufferSyncerClientApi
import de.justjanne.libquassel.protocol.api.client.BufferViewConfigClientApi
import de.justjanne.libquassel.protocol.api.client.BufferViewManagerClientApi
import de.justjanne.libquassel.protocol.api.client.CertManagerClientApi
import de.justjanne.libquassel.protocol.api.client.CoreInfoClientApi
import de.justjanne.libquassel.protocol.api.client.HighlightRuleManagerClientApi
import de.justjanne.libquassel.protocol.api.client.IdentityClientApi
import de.justjanne.libquassel.protocol.api.client.IgnoreListManagerClientApi
import de.justjanne.libquassel.protocol.api.client.IrcChannelClientApi
import de.justjanne.libquassel.protocol.api.client.IrcListHelperClientApi
import de.justjanne.libquassel.protocol.api.client.IrcUserClientApi
import de.justjanne.libquassel.protocol.api.client.NetworkClientApi
import de.justjanne.libquassel.protocol.api.client.NetworkConfigClientApi
import de.justjanne.libquassel.protocol.api.client.RpcClientApi
import de.justjanne.libquassel.protocol.api.dispatcher.ClientDispatcherModule
import de.justjanne.libquassel.protocol.api.dispatcher.RpcDispatcher
import de.justjanne.libquassel.protocol.api.dispatcher.SyncHandler
......@@ -47,18 +72,40 @@ import de.justjanne.libquassel.protocol.api.server.IgnoreListManagerServerApi
import de.justjanne.libquassel.protocol.api.server.IrcListHelperServerApi
import de.justjanne.libquassel.protocol.api.server.NetworkConfigServerApi
import de.justjanne.libquassel.protocol.api.server.NetworkServerApi
import de.justjanne.libquassel.protocol.connection.ClientHeader
import de.justjanne.libquassel.protocol.connection.ProtocolFeature
import de.justjanne.libquassel.protocol.connection.ProtocolMeta
import de.justjanne.libquassel.protocol.connection.ProtocolVersion
import de.justjanne.libquassel.protocol.exceptions.RpcInvocationFailedException
import de.justjanne.libquassel.protocol.features.FeatureSet
import de.justjanne.libquassel.protocol.io.CoroutineChannel
import de.justjanne.libquassel.protocol.models.HandshakeMessage
import de.justjanne.libquassel.protocol.models.ids.BufferId
import de.justjanne.libquassel.protocol.models.ids.MsgId
import de.justjanne.libquassel.protocol.models.types.QtType
import de.justjanne.libquassel.protocol.variant.QVariantList
import de.justjanne.libquassel.protocol.variant.QVariant_
import de.justjanne.libquassel.protocol.variant.qVariant
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.net.InetSocketAddress
import java.nio.channels.ClosedByInterruptException
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.test.assertFails
import kotlin.test.assertFailsWith
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@Singleton
class ProxyImpl @Inject constructor() : Proxy {
......@@ -92,6 +139,22 @@ interface ClientModule {
@Binds fun bindProxy(impl: ProxyImpl): Proxy
}
@Module
class DatabaseModule {
@Singleton
@Provides
fun database(): AppDatabase =
Room.inMemoryDatabaseBuilder<AppDatabase>()
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
@Provides
fun provideAliasRepository(database: AppDatabase) = database.alias()
@Provides
fun provideMessageRepository(database: AppDatabase) = database.message()
}
@Singleton
class QuasselApiClient @Inject constructor(
val aliasManager: AliasManagerServerApi,
......@@ -109,8 +172,9 @@ class QuasselApiClient @Inject constructor(
)
@Singleton
@Component(modules = [ClientModule::class, ClientDispatcherModule::class, ClientProxyModule::class])
@Component(modules = [ClientModule::class, DatabaseModule::class, ClientDispatcherModule::class, ClientProxyModule::class])
interface ClientComponent {
fun db(): AppDatabase
fun sync(): SyncHandler
fun rpc(): RpcDispatcher
fun api(): QuasselApiClient
......@@ -118,7 +182,7 @@ interface ClientComponent {
class QuasselApiTest {
@Test
fun test() {
fun test() = runBlocking {
val client = DaggerClientComponent.builder().build()
client.sync().invoke(
"AliasManager",
......@@ -146,6 +210,69 @@ class QuasselApiTest {
client.api().certManager.requestUpdate(ObjectName(""), emptyMap())
client.api().highlightRuleManager.requestRemoveHighlightRule(5)
client.api().identity.requestUpdate(ObjectName(""), emptyMap())
println("hi!")
}
@Test
fun testDb() = runBlocking {
val db = Room.inMemoryDatabaseBuilder<AppDatabase>()
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
val aliasRepo = db.alias()
val job = CoroutineScope(Dispatchers.Default).launch {
aliasRepo.collectAll().collectLatest {
println("alias list changed: $it")
}
}
delay(10.milliseconds)
aliasRepo.insert(
AliasEntity(0, "foo", "bar"),
)
delay(10.milliseconds)
aliasRepo.insert(
AliasEntity(1, "bar", "baz"),
)
delay(10.milliseconds)
aliasRepo.insert(
AliasEntity(0, "foo", "foo"),
AliasEntity(2, "baz", "foo"),
)
delay(10.milliseconds)
job.cancelAndJoin()
}
@Test
fun testConnection() = runBlocking {
val di = DaggerClientComponent.builder().build()
val channel = CoroutineChannel()
withTimeout(500.milliseconds) {
channel.connect(InetSocketAddress("decentralised.chat", 4242), keepAlive = true)
}
val connection = ChannelConnection(channel)
val connectionSetup = withTimeout(500.milliseconds) {
MagicHandler(
ClientHeader(
features = ProtocolFeature.of(ProtocolFeature.Compression, ProtocolFeature.TLS),
versions = listOf(ProtocolMeta(ProtocolVersion.Datastream, 0u))
),
).handle(connection)
}.getOrThrow()
val handshake = withTimeout(500.milliseconds) {
ClientHandshakeHandler(
HandshakeMessage.ClientInit(clientVersion = "", buildDate = "", featureSet = FeatureSet.none()),
HandshakeMessage.ClientLogin(Const.user, Const.pass),
).handle(connection)
}.getOrThrow()
val sessionHandler = ClientSessionHandler(handshake, di.sync(), di.rpc())
withTimeout(2.seconds) {
launch {
sessionHandler.handle(connection)
}
sessionHandler.toInit.first { it != null && it.isEmpty() }
channel.close()
}
println(di.db().alias().getAll())
Unit
}
}
......@@ -67,7 +67,7 @@ class DispatcherGenerator : RpcModelVisitor<KotlinModel?> {
.addAnnotation(TYPENAME_GENERATED)
.addFunction(
FunSpec.builder("invoke")
.addModifiers(KModifier.OVERRIDE, KModifier.OPERATOR)
.addModifiers(KModifier.OVERRIDE, KModifier.OPERATOR, KModifier.SUSPEND)
.addAnnotation(TYPENAME_GENERATED)
.let {
if (model.rpcName.isNotEmpty()) {
......
......@@ -94,7 +94,7 @@ class AliasManagerClientApiProxy @Inject constructor(
.addFunctions(
model.methods.map { method ->
FunSpec.builder(method.name)
.addModifiers(KModifier.OVERRIDE)
.addModifiers(KModifier.OVERRIDE, KModifier.SUSPEND)
.let {
if (!method.static) {
it.addParameter(
......
/*
* libquassel
* Copyright (c) 2024 Janne Mareike Koschinski
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*/
plugins {
id("justjanne.kotlin")
id("justjanne.publication")
}
repositories {
google()
mavenCentral()
}
dependencies {
api(project(":libquassel-protocol"))
api(project(":libquassel-api"))
implementation(libs.dagger.core)
ksp(libs.dagger.compiler)
implementation(libs.room.runtime)
ksp(libs.room.compiler)
implementation(libs.slf4j)
}
/*
* libquassel
* Copyright (c) 2025 Janne Mareike Koschinski
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*/
package de.justjanne.libquassel.persistence
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class AliasEntity(
@PrimaryKey
val id: Int,
val name: String,
val expansion: String,
)
/*
* libquassel
* Copyright (c) 2024 Janne Mareike Koschinski
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*/
package de.justjanne.libquassel.persistence
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import de.justjanne.libquassel.protocol.models.ids.BufferId
import kotlinx.coroutines.flow.Flow
@Dao
interface AliasRepository {
@Query("SELECT * FROM AliasEntity")
suspend fun getAll(): List<AliasEntity>
@Query("SELECT * FROM AliasEntity")
fun collectAll(): Flow<List<AliasEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg item: AliasEntity)
@Query("DELETE FROM AliasEntity")
suspend fun clear()
@Transaction
suspend fun sync(data: List<AliasEntity>) {
clear()
for (item in data) {
insert(item)
}
}
}
@Dao
interface MessageRepository {
@Query("SELECT * FROM MessageEntity WHERE buffer = :buffer ORDER BY messageId ASC")
suspend fun getAll(buffer: BufferId): List<MessageEntity>
@Query("SELECT * FROM MessageEntity WHERE buffer = :buffer ORDER BY messageId ASC")
fun collectAll(buffer: BufferId): Flow<List<MessageEntity>>
@Query("SELECT * FROM messageentity WHERE buffer = :buffer ORDER BY messageId DESC LIMIT 1")
suspend fun getLast(buffer: BufferId): MessageEntity?
@Query("SELECT * FROM messageentity WHERE buffer = :buffer ORDER BY messageId DESC LIMIT 1")
suspend fun collectLast(buffer: BufferId): MessageEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg item: MessageEntity)
@Query("DELETE FROM MessageEntity WHERE buffer = :buffer")
suspend fun clear(buffer: BufferId)
@Transaction
suspend fun sync(buffer: BufferId, data: List<MessageEntity>) {
val newMessages = data.map(MessageEntity::messageId).toSet()
val last = getLast(buffer)
if (last != null && !newMessages.contains(last.messageId)) {
clear(buffer)
}
for (item in data) {
insert(item)
}
}
}
/*
* libquassel
* Copyright (c) 2024 Janne Mareike Koschinski
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*/
package de.justjanne.libquassel.persistence
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(
entities = [
AliasEntity::class,
MessageEntity::class,
],
version = 1,
)
@TypeConverters(InstantConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun alias(): AliasRepository
abstract fun message(): MessageRepository
}
/*
* libquassel
* Copyright (c) 2025 Janne Mareike Koschinski
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*/
package de.justjanne.libquassel.persistence
import androidx.room.TypeConverter
import org.threeten.bp.Instant
object InstantConverter {
@TypeConverter
fun fromTimestamp(value: Long): Instant {
return Instant.ofEpochMilli(value)
}
@TypeConverter
fun toTimestamp(instant: Instant): Long {
return instant.toEpochMilli()
}
}
/*
* libquassel
* Copyright (c) 2025 Janne Mareike Koschinski
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*/
package de.justjanne.libquassel.persistence
import androidx.room.Entity
import androidx.room.PrimaryKey
import de.justjanne.bitflags.toBits
import de.justjanne.libquassel.protocol.models.Message
import de.justjanne.libquassel.protocol.models.ids.BufferId
import de.justjanne.libquassel.protocol.models.ids.MsgId
import org.threeten.bp.Instant
@Entity
data class MessageEntity(
@PrimaryKey
val messageId: MsgId,
val time: Instant,
val type: Int,
val flag: Int,
val buffer: BufferId,
val sender: String,
val senderPrefixes: String,
val realName: String,
val avatarUrl: String,
val content: String,
) {
companion object {
fun fromDto(message: Message) = MessageEntity(
message.messageId,
message.time,
message.type.toBits().toInt(),
message.flag.toBits().toInt(),
message.bufferInfo.bufferId,
message.sender,
message.senderPrefixes,
message.realName,
message.avatarUrl,
message.content,
)
}
}
/*
* libquassel
* Copyright (c) 2021 Janne Mareike Koschinski
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*/
package de.justjanne.libquassel.protocol.session
import de.justjanne.libquassel.protocol.exceptions.HandshakeException
import de.justjanne.libquassel.protocol.features.FeatureSet
import de.justjanne.libquassel.protocol.variant.QVariantMap
interface HandshakeHandler : ConnectionHandler {
/**
* Register client and start connection
*/
@Throws(HandshakeException.InitException::class)
suspend fun init(
/**
* Human readable (HTML formatted) version of the client
*/
clientVersion: String,
/**
* Build timestamp of the client
*/
buildDate: String,
/**
* Enabled client features for this connection
*/
featureSet: FeatureSet,
): CoreState
/**
* Login to core with authentication data
*/
@Throws(HandshakeException.LoginException::class)
suspend fun login(
/**
* Username of the core account
*/
username: String,
/**
* Password of the core account
*/
password: String,
)
/**
* Configure core for the first time
*/
@Throws(HandshakeException.SetupException::class)
suspend fun configureCore(
/**
* Username of a new core account to be created
*/
adminUsername: String,
/**
* Password of a new core account to be created
*/
adminPassword: String,
/**
* Chosen storage backend id
*/
backend: String,
/**
* Storage backend configuration data
*/
backendConfiguration: QVariantMap,
/**
* Chosen authenticator backend id
*/
authenticator: String,
/**
* Authenticator backend configuration data
*/
authenticatorConfiguration: QVariantMap,
)
}
/*
* libquassel
* Copyright (c) 2021 Janne Mareike Koschinski
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*/
package de.justjanne.libquassel.protocol.session
import de.justjanne.libquassel.protocol.features.FeatureSet
import de.justjanne.libquassel.protocol.io.ChainedByteBuffer
import de.justjanne.libquassel.protocol.io.CoroutineChannel
import de.justjanne.libquassel.protocol.models.HandshakeMessage
import de.justjanne.libquassel.protocol.models.SignalProxyMessage
import de.justjanne.libquassel.protocol.serializers.HandshakeMessageSerializer
import de.justjanne.libquassel.protocol.serializers.SignalProxyMessageSerializer
import de.justjanne.libquassel.protocol.util.log.trace
import kotlinx.coroutines.coroutineScope
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.nio.ByteBuffer
class MessageChannel(
val channel: CoroutineChannel,
) : Closeable {
var negotiatedFeatures = FeatureSet.none()
private var handlers = mutableListOf<ConnectionHandler>()
fun register(handler: ConnectionHandler) {
handlers.add(handler)
}
private val sendBuffer = ThreadLocal.withInitial(::ChainedByteBuffer)
private val sizeBuffer = ThreadLocal.withInitial { ByteBuffer.allocateDirect(4) }
suspend fun init() {
setupHandlers()
}
private suspend fun readAmount(): Int {
val sizeBuffer = sizeBuffer.get()
sizeBuffer.clear()
channel.read(sizeBuffer)
sizeBuffer.flip()
val size = sizeBuffer.int
sizeBuffer.clear()
return size
}
suspend fun read() {
val amount = readAmount()
val messageBuffer = ByteBuffer.allocateDirect(minOf(amount, 65 * 1024 * 1024))
channel.read(messageBuffer)
messageBuffer.flip()
dispatch(messageBuffer)
}
private suspend fun setupHandlers(): List<ConnectionHandler> {
val removed = mutableListOf<ConnectionHandler>()
while (true) {
val handler = handlers.firstOrNull()
logger.trace { "Setting up handler $handler" }
if (handler?.init(this) != true) {
break
}
logger.trace { "Handler $handler is done" }
removed.add(handlers.removeFirst())
}
if (handlers.isEmpty()) {
logger.trace { "All handlers done" }
channel.close()
}
return removed
}
private suspend fun dispatch(message: ByteBuffer) {
val handlerDone =
try {
handlers.first().read(message)
} catch (e: Exception) {
logger.warn("Error while handling message: ", e)
false
}
if (handlerDone) {
val removed = listOf(handlers.removeFirst()) + setupHandlers()
for (handler in removed) {
handler.done()
}
}
}
suspend fun emit(message: HandshakeMessage) =
emit {
logger.trace { "Writing handshake message $message" }
HandshakeMessageSerializer.serialize(it, message, negotiatedFeatures)
}
suspend fun emit(message: SignalProxyMessage) =
emit {
logger.trace { "Writing signal proxy message $message" }
SignalProxyMessageSerializer.serialize(it, message, negotiatedFeatures)
}
suspend fun emit(
sizePrefix: Boolean = true,
f: (ChainedByteBuffer) -> Unit,
) = coroutineScope {
val sendBuffer = sendBuffer.get()
val sizeBuffer = sizeBuffer.get()
f(sendBuffer)
if (sizePrefix) {
sizeBuffer.clear()
sizeBuffer.putInt(sendBuffer.size)
sizeBuffer.flip()
channel.write(sizeBuffer)
sizeBuffer.clear()
}
channel.write(sendBuffer)
channel.flush()
sendBuffer.clear()
}
override fun close() {
channel.close()
}
companion object {
private val logger = LoggerFactory.getLogger(MessageChannel::class.java)
}
}
/*
* libquassel
* Copyright (c) 2021 Janne Mareike Koschinski
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*/
package de.justjanne.libquassel.protocol.session
import de.justjanne.libquassel.protocol.util.log.info
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.nio.channels.ClosedChannelException
import java.util.concurrent.Executors
class MessageChannelReader(
private val channel: MessageChannel,
) : Closeable {
private val executor = Executors.newSingleThreadExecutor()
private val dispatcher = executor.asCoroutineDispatcher()
private val scope = CoroutineScope(dispatcher)
private var job: Job? = null
fun start() {
job =
scope.launch {
try {
channel.init()
while (isActive && channel.channel.state().connected) {
channel.read()
}
} catch (e: ClosedChannelException) {
logger.info { "Channel closed" }
close()
}
}
}
override fun close() {
channel.close()
runBlocking { job?.cancelAndJoin() }
scope.cancel()
dispatcher.cancel()
executor.shutdown()
}
companion object {
private val logger = LoggerFactory.getLogger(MessageChannelReader::class.java)
}
}
......@@ -15,6 +15,7 @@ include(
":libquassel-annotations",
":libquassel-api",
":libquassel-client",
":libquassel-persistence",
":libquassel-generator",
":libquassel-irc",
":libquassel-protocol"
......