├── examples ├── ping-bot │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ ├── resources │ │ │ ├── .gitignore │ │ │ └── application.yml.example │ │ └── kotlin │ │ │ └── net │ │ │ └── folivo │ │ │ └── spring │ │ │ └── matrix │ │ │ └── bot │ │ │ └── examples │ │ │ └── pingappservice │ │ │ ├── PingApplication.kt │ │ │ └── PingHandler.kt │ │ └── test │ │ └── kotlin │ │ └── net │ │ └── folivo │ │ └── spring │ │ └── matrix │ │ └── bot │ │ └── examples │ │ └── pingappservice │ │ └── PingHandlerTest.kt ├── ping-appservice-bot │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ ├── .gitignore │ │ ├── resources │ │ │ ├── .gitignore │ │ │ └── application.yml.example │ │ └── kotlin │ │ │ └── net │ │ │ └── folivo │ │ │ └── spring │ │ │ └── matrix │ │ │ └── bot │ │ │ └── examples │ │ │ └── pingappservice │ │ │ ├── PingApplication.kt │ │ │ └── PingHandler.kt │ │ └── test │ │ └── kotlin │ │ └── net │ │ └── folivo │ │ └── spring │ │ └── matrix │ │ └── bot │ │ └── examples │ │ └── pingappservice │ │ └── PingHandlerTest.kt ├── demo-infra │ ├── synapse │ │ ├── matrix-kotlin-sdk-it-synapse.signing.key │ │ ├── .gitignore │ │ ├── ping-appservice.yaml │ │ └── matrix-kotlin-sdk-it-synapse.log.config │ ├── docker-compose.yml │ └── config │ │ └── application.yml └── build.gradle.kts ├── matrix-spring-boot-bot ├── src │ ├── test │ │ ├── resources │ │ │ ├── mockito-extensions │ │ │ │ └── org.mockito.plugins.MockMaker │ │ │ └── application.yml │ │ └── kotlin │ │ │ └── net │ │ │ └── folivo │ │ │ └── spring │ │ │ └── matrix │ │ │ └── bot │ │ │ ├── TestApplication.kt │ │ │ ├── KotestConfig.kt │ │ │ ├── client │ │ │ ├── MatrixClientSyncRunnerTest.kt │ │ │ └── ClientMemberEventHandlerTest.kt │ │ │ ├── appservice │ │ │ ├── BotUserInitializerTest.kt │ │ │ ├── sync │ │ │ │ └── InitialSyncServiceTest.kt │ │ │ ├── event │ │ │ │ └── DefaultAppserviceEventServiceTest.kt │ │ │ ├── DefaultAppserviceRoomServiceTest.kt │ │ │ ├── DefaultAppserviceUserServiceTest.kt │ │ │ └── AppserviceMemberEventHandlerTest.kt │ │ │ ├── util │ │ │ └── BotServiceHelperTest.kt │ │ │ ├── event │ │ │ ├── MatrixSyncBatchTokenRepositoryTest.kt │ │ │ ├── EventHandlerRunnerTest.kt │ │ │ ├── MessageEventHandlerTest.kt │ │ │ └── PersistentSyncBatchTokenServiceTest.kt │ │ │ ├── user │ │ │ ├── MatrixUserServiceTest.kt │ │ │ └── MatrixUserRepositoryTest.kt │ │ │ ├── room │ │ │ ├── MatrixRoomRepositoryTest.kt │ │ │ └── MatrixRoomServiceTest.kt │ │ │ └── membership │ │ │ ├── MatrixMembershipRepositoryTest.kt │ │ │ ├── MatrixMembershipSyncServiceTest.kt │ │ │ ├── MembershipChangeHandlerTest.kt │ │ │ └── DefaultMembershipChangeServiceTest.kt │ └── main │ │ ├── resources │ │ ├── db │ │ │ └── changelog │ │ │ │ ├── net.folivo.matrix.bot.changelog-master.yml │ │ │ │ ├── net.folivo.matrix.bot.changelog-1.0.0.yaml │ │ │ │ └── net.folivo.matrix.bot.changelog-0.4.0.RELEASE.yaml │ │ └── META-INF │ │ │ └── spring.factories │ │ └── kotlin │ │ └── net │ │ └── folivo │ │ └── spring │ │ └── matrix │ │ └── bot │ │ ├── event │ │ ├── MatrixMessageHandler.kt │ │ ├── MatrixEventHandler.kt │ │ ├── MatrixSyncBatchTokenRepository.kt │ │ ├── MatrixSyncBatchToken.kt │ │ ├── PersistentSyncBatchTokenService.kt │ │ ├── MessageEventHandler.kt │ │ ├── EventHandlerRunner.kt │ │ └── MessageContext.kt │ │ ├── appservice │ │ ├── event │ │ │ ├── MatrixEventTransactionRepository.kt │ │ │ ├── MatrixEventTransactionService.kt │ │ │ ├── MatrixEventTransaction.kt │ │ │ └── DefaultAppserviceEventTnxService.kt │ │ ├── BotUserInitializer.kt │ │ ├── sync │ │ │ └── InitialSyncService.kt │ │ ├── DefaultAppserviceRoomService.kt │ │ ├── DefaultAppserviceUserService.kt │ │ └── AppserviceMemberEventHandler.kt │ │ ├── membership │ │ ├── MembershipChangeService.kt │ │ ├── MatrixMembership.kt │ │ ├── MatrixMembershipRepository.kt │ │ ├── MatrixMembershipService.kt │ │ ├── DefaultMembershipChangeService.kt │ │ ├── MembershipChangeHandler.kt │ │ └── MatrixMembershipSyncService.kt │ │ ├── config │ │ ├── MatrixIdWritingConverter.kt │ │ ├── MatrixIdReadingConverter.kt │ │ ├── MatrixBotProperties.kt │ │ ├── MatrixClientBotAutoconfiguration.kt │ │ ├── MatrixBotDatabaseAutoconfiguration.kt │ │ ├── MatrixBotAutoconfiguration.kt │ │ └── MatrixAppserviceBotAutoconfiguration.kt │ │ ├── room │ │ ├── MatrixRoomAliasRepository.kt │ │ ├── MatrixRoom.kt │ │ ├── MatrixRoomAlias.kt │ │ ├── MatrixRoomRepository.kt │ │ └── MatrixRoomService.kt │ │ ├── user │ │ ├── MatrixUser.kt │ │ ├── MatrixUserRepository.kt │ │ └── MatrixUserService.kt │ │ ├── util │ │ └── BotServiceHelper.kt │ │ └── client │ │ ├── MatrixClientSyncRunner.kt │ │ └── ClientMemberEventHandler.kt └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── matrix-spring-boot-rest-client ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── spring.factories │ │ └── kotlin │ │ │ └── net │ │ │ └── folivo │ │ │ └── spring │ │ │ └── matrix │ │ │ └── restclient │ │ │ ├── MatrixClientConfigurationProperties.kt │ │ │ └── MatrixClientAutoconfiguration.kt │ └── test │ │ └── kotlin │ │ └── net │ │ └── folivo │ │ └── spring │ │ └── matrix │ │ └── restclient │ │ └── MatrixClientAutoconfigurationContextTest.kt └── build.gradle.kts ├── settings.gradle.kts ├── matrix-spring-boot-rest-appservice ├── src │ ├── test │ │ ├── resources │ │ │ └── application.yml │ │ └── kotlin │ │ │ └── net │ │ │ └── folivo │ │ │ └── spring │ │ │ └── matrix │ │ │ └── appservice │ │ │ ├── TestApplication.kt │ │ │ ├── MatrixAppserviceAutoconfigurationContextTest.kt │ │ │ └── TestConfiguration.kt │ └── main │ │ └── kotlin │ │ └── net │ │ └── folivo │ │ └── spring │ │ └── matrix │ │ └── appservice │ │ ├── MatrixAppserviceConfigurationProperties.kt │ │ ├── AppserviceApplicationEngine.kt │ │ └── MatrixAppserviceAutoconfiguration.kt └── build.gradle.kts ├── .gitignore ├── .gitlab-ci.yml ├── gradlew.bat ├── gradlew └── README.md /examples/ping-bot/.gitignore: -------------------------------------------------------------------------------- 1 | testdb/ -------------------------------------------------------------------------------- /examples/ping-bot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/ping-appservice-bot/.gitignore: -------------------------------------------------------------------------------- 1 | testdb/ -------------------------------------------------------------------------------- /examples/ping-appservice-bot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/ping-appservice-bot/src/main/.gitignore: -------------------------------------------------------------------------------- 1 | testdb/ -------------------------------------------------------------------------------- /examples/ping-bot/src/main/resources/.gitignore: -------------------------------------------------------------------------------- 1 | application.yml -------------------------------------------------------------------------------- /examples/ping-appservice-bot/src/main/resources/.gitignore: -------------------------------------------------------------------------------- 1 | application.yml -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /examples/demo-infra/synapse/matrix-kotlin-sdk-it-synapse.signing.key: -------------------------------------------------------------------------------- 1 | ed25519 a_EgjW U0b5hmg9zXoLxAZFVDLTvtggKw+vkZQepCgjL8ZYRfI 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benkuly/matrix-spring-boot-sdk/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/demo-infra/synapse/.gitignore: -------------------------------------------------------------------------------- 1 | homeserver.db 2 | 3 | !.gitignore 4 | !homeserver.yaml 5 | !matrix-kotlin-sdk-it-synapse.log.config 6 | !matrix-kotlin-sdk-it-synapse.signing.key -------------------------------------------------------------------------------- /matrix-spring-boot-rest-client/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | net.folivo.spring.matrix.restclient.MatrixClientAutoconfiguration -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | # https://gradle.org/releases/ 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "matrix-spring-boot-sdk" 2 | include("matrix-spring-boot-rest-client") 3 | include("matrix-spring-boot-rest-appservice") 4 | include("matrix-spring-boot-bot") 5 | include("examples") 6 | include("examples:ping-bot") 7 | include("examples:ping-appservice-bot") -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/resources/db/changelog/net.folivo.matrix.bot.changelog-master.yml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - include: 3 | file: db/changelog/net.folivo.matrix.bot.changelog-0.4.0.RELEASE.yaml 4 | - include: 5 | file: db/changelog/net.folivo.matrix.bot.changelog-1.0.0.yaml -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/resources/db/changelog/net.folivo.matrix.bot.changelog-1.0.0.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - changeSet: 3 | id: net.folivo.matrix.bot.changelog-1.0.0 4 | author: benkuly 5 | changes: 6 | - dropColumn: 7 | tableName: matrix_event_transaction 8 | columnName: tnx_id -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/event/MatrixMessageHandler.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import net.folivo.trixnity.core.model.events.m.room.MessageEventContent 4 | 5 | interface MatrixMessageHandler { 6 | 7 | suspend fun handleMessage(content: MessageEventContent, context: MessageContext) 8 | 9 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/TestApplication.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot 2 | 3 | import org.springframework.boot.SpringBootConfiguration 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration 5 | 6 | 7 | @SpringBootConfiguration 8 | @EnableAutoConfiguration 9 | class TestApplication { 10 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-appservice/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | appservice: 3 | hsToken: validToken 4 | asUsername: superAppservice 5 | namespaces: 6 | users: [] 7 | aliases: [] 8 | rooms: [] 9 | client: 10 | homeServer: 11 | hostname: localhost 12 | secure: false 13 | token: superSecretToken -------------------------------------------------------------------------------- /examples/demo-infra/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | matrix-synapse: 4 | image: matrixdotorg/synapse:v1.32.2 5 | volumes: 6 | - type: bind 7 | source: ./synapse 8 | target: /data 9 | environment: 10 | - SYNAPSE_REPORT_STATS=false 11 | - UID=1000 12 | - GID=1000 13 | ports: 14 | - 8008:8008 -------------------------------------------------------------------------------- /examples/ping-appservice-bot/src/test/kotlin/net/folivo/spring/matrix/bot/examples/pingappservice/PingHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.examples.pingappservice 2 | 3 | import io.mockk.junit5.MockKExtension 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | 6 | @ExtendWith(MockKExtension::class) 7 | class PingHandlerTest { 8 | 9 | // TODO implement 10 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | net.folivo.spring.matrix.bot.config.MatrixBotAutoconfiguration,\ 3 | net.folivo.spring.matrix.bot.config.MatrixClientBotAutoconfiguration,\ 4 | net.folivo.spring.matrix.bot.config.MatrixAppserviceBotAutoconfiguration,\ 5 | net.folivo.spring.matrix.bot.config.MatrixBotDatabaseAutoconfiguration -------------------------------------------------------------------------------- /examples/demo-infra/synapse/ping-appservice.yaml: -------------------------------------------------------------------------------- 1 | id: "Ping Appservice" 2 | url: "http://172.17.0.1:8080" 3 | as_token: "30c05ae90a248a4188e620216fa72e349803310ec83e2a77b34fe90be6081f46" 4 | hs_token: "312df522183efd404ec1cd22d2ffa4bbc76a8c1ccf541dd692eef281356bb74e" 5 | sender_localpart: "ping" 6 | namespaces: 7 | users: 8 | - regex: "^@ping_.+:matrix-local$" 9 | exclusive: true 10 | aliases: [ ] 11 | rooms: [ ] -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/appservice/event/MatrixEventTransactionRepository.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice.event 2 | 3 | import org.springframework.data.repository.kotlin.CoroutineCrudRepository 4 | import org.springframework.stereotype.Repository 5 | 6 | @Repository 7 | interface MatrixEventTransactionRepository : CoroutineCrudRepository { 8 | } -------------------------------------------------------------------------------- /examples/ping-bot/src/main/kotlin/net/folivo/spring/matrix/bot/examples/pingappservice/PingApplication.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.examples.pingappservice 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class PingApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api("org.springframework.boot:spring-boot-starter") 3 | 4 | api("net.folivo:trixnity-rest-client:${Versions.trixnity}") 5 | implementation("io.ktor:ktor-client-java:${Versions.ktor}") 6 | 7 | annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor") 8 | annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") 9 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/KotestConfig.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot 2 | 3 | import io.kotest.core.config.AbstractProjectConfig 4 | import io.kotest.core.extensions.Extension 5 | import io.kotest.spring.SpringAutowireConstructorExtension 6 | 7 | class KotestConfig : AbstractProjectConfig() { 8 | override fun extensions(): List = listOf(SpringAutowireConstructorExtension) 9 | 10 | } -------------------------------------------------------------------------------- /examples/ping-appservice-bot/src/main/kotlin/net/folivo/spring/matrix/bot/examples/pingappservice/PingApplication.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.examples.pingappservice 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class PingApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/event/MatrixEventHandler.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import liquibase.pro.packaged.T 4 | import net.folivo.trixnity.core.model.events.Event 5 | import net.folivo.trixnity.core.model.events.EventContent 6 | import kotlin.reflect.KClass 7 | 8 | interface MatrixEventHandler { 9 | 10 | fun supports(): KClass 11 | 12 | suspend fun handleEvent(event: Event) 13 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/membership/MembershipChangeService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | 5 | interface MembershipChangeService { 6 | suspend fun onRoomJoin(userId: MatrixId.UserId, roomId: MatrixId.RoomId) 7 | suspend fun onRoomLeave(userId: MatrixId.UserId, roomId: MatrixId.RoomId) 8 | suspend fun shouldJoinRoom(userId: MatrixId.UserId, roomId: MatrixId.RoomId): Boolean 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | db/ 5 | !gradle/wrapper/gradle-wrapper.jar 6 | !**/src/main/** 7 | !**/src/test/** 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | *.iws 21 | *.iml 22 | *.ipr 23 | out/ 24 | 25 | ### NetBeans ### 26 | /nbproject/private/ 27 | /nbbuild/ 28 | /dist/ 29 | /nbdist/ 30 | /.nb-gradle/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/config/MatrixIdWritingConverter.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.config 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.core.convert.converter.Converter 5 | import org.springframework.data.convert.WritingConverter 6 | 7 | @WritingConverter 8 | class MatrixIdWritingConverter : Converter { 9 | override fun convert(source: MatrixId): String { 10 | return source.full 11 | } 12 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-appservice/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation("org.springframework.boot:spring-boot-starter") 3 | 4 | api(project(":matrix-spring-boot-rest-client")) 5 | api("net.folivo:trixnity-rest-appservice:${Versions.trixnity}") 6 | implementation("io.ktor:ktor-server-cio:${Versions.ktor}") 7 | 8 | annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor") 9 | annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") 10 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/config/MatrixIdReadingConverter.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.config 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.core.convert.converter.Converter 5 | import org.springframework.data.convert.ReadingConverter 6 | 7 | @ReadingConverter 8 | class MatrixIdReadingConverter : Converter { 9 | override fun convert(source: String): MatrixId { 10 | return MatrixId.of(source) 11 | } 12 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-appservice/src/test/kotlin/net/folivo/spring/matrix/appservice/TestApplication.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.appservice 2 | 3 | import org.springframework.boot.SpringBootConfiguration 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration 5 | import org.springframework.context.annotation.Import 6 | 7 | 8 | @SpringBootConfiguration 9 | @EnableAutoConfiguration 10 | @Import(MatrixAppserviceAutoconfiguration::class, TestConfiguration::class) 11 | class TestApplication { 12 | 13 | } -------------------------------------------------------------------------------- /examples/ping-bot/src/main/resources/application.yml.example: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | web-application-type: NONE 4 | 5 | logging: 6 | level: 7 | net.folivo.matrix: DEBUG 8 | 9 | matrix: 10 | bot: 11 | serverName: example.org 12 | username: ping 13 | migration: 14 | url: jdbc:h2:file:./testdb/testdb 15 | database: 16 | url: r2dbc:h2:file:///./testdb/testdb 17 | username: sa 18 | client: 19 | homeServer: 20 | hostname: matrix.example.org 21 | secure: true 22 | token: superSecretToken -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/room/MatrixRoomAliasRepository.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.room 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.data.repository.kotlin.CoroutineCrudRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | @Repository 8 | interface MatrixRoomAliasRepository : CoroutineCrudRepository { 9 | 10 | suspend fun findByRoomId(roomId: MatrixId.RoomId): MatrixRoomAlias? 11 | 12 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/event/MatrixSyncBatchTokenRepository.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.data.repository.kotlin.CoroutineCrudRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | @Repository 8 | interface MatrixSyncBatchTokenRepository : CoroutineCrudRepository { 9 | 10 | suspend fun findByUserId(userId: MatrixId.UserId): MatrixSyncBatchToken? 11 | } -------------------------------------------------------------------------------- /examples/build.gradle.kts: -------------------------------------------------------------------------------- 1 | subprojects { 2 | dependencies { 3 | implementation(project(":matrix-spring-boot-bot")) 4 | developmentOnly("org.springframework.boot:spring-boot-devtools") 5 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") 6 | 7 | implementation("io.r2dbc:r2dbc-h2") 8 | implementation("com.h2database:h2") 9 | } 10 | 11 | tasks.getByName("jar") { 12 | enabled = false 13 | } 14 | 15 | tasks.withType { 16 | enabled = true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/appservice/event/MatrixEventTransactionService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice.event 2 | 3 | class MatrixEventTransactionService(private val eventTransactionRepository: MatrixEventTransactionRepository) { 4 | 5 | suspend fun hasTransaction(tnxId: String): Boolean { 6 | return eventTransactionRepository.existsById(tnxId) 7 | } 8 | 9 | suspend fun saveTransaction(eventTransaction: MatrixEventTransaction): MatrixEventTransaction { 10 | return eventTransactionRepository.save(eventTransaction) 11 | } 12 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/appservice/event/MatrixEventTransaction.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice.event 2 | 3 | import org.springframework.data.annotation.Id 4 | import org.springframework.data.annotation.Version 5 | import org.springframework.data.relational.core.mapping.Column 6 | import org.springframework.data.relational.core.mapping.Table 7 | 8 | @Table("matrix_event_transaction") 9 | data class MatrixEventTransaction( 10 | @Id 11 | @Column("id") 12 | val id: String, 13 | @Version 14 | @Column("version") 15 | val version: Int = 0 16 | ) -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - publish 3 | 4 | image: gradle:7-jdk11 5 | 6 | # Disable the Gradle daemon for Continuous Integration servers as correctness 7 | # is usually a priority over speed in CI environments. Using a fresh 8 | # runtime for each build is more reliable since the runtime is completely 9 | # isolated from any previous builds. 10 | variables: 11 | GRADLE_OPTS: "-Dorg.gradle.daemon=false" 12 | 13 | before_script: 14 | - export GRADLE_USER_HOME=`pwd`/.gradle 15 | 16 | publish: 17 | stage: publish 18 | rules: 19 | - if: '$CI_COMMIT_TAG =~ /^v\d+.\d+.\d+/' 20 | script: 21 | - gradle check 22 | - gradle publish 23 | -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/room/MatrixRoom.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.room 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.data.annotation.Id 5 | import org.springframework.data.annotation.Version 6 | import org.springframework.data.relational.core.mapping.Column 7 | import org.springframework.data.relational.core.mapping.Table 8 | 9 | @Table("matrix_room") 10 | data class MatrixRoom( 11 | @Id 12 | @Column("id") 13 | val id: MatrixId.RoomId, 14 | 15 | @Column("is_managed") 16 | val isManaged: Boolean = false, 17 | 18 | @Version 19 | @Column("version") 20 | val version: Int = 0 21 | ) -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/user/MatrixUser.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.user 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.data.annotation.Id 5 | import org.springframework.data.annotation.Version 6 | import org.springframework.data.relational.core.mapping.Column 7 | import org.springframework.data.relational.core.mapping.Table 8 | 9 | 10 | @Table("matrix_user") 11 | data class MatrixUser( 12 | @Id 13 | @Column("id") 14 | val id: MatrixId.UserId, 15 | 16 | @Column("is_managed") 17 | val isManaged: Boolean = false, 18 | 19 | @Version 20 | @Column("version") 21 | val version: Int = 0 22 | ) -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/room/MatrixRoomAlias.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.room 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.data.annotation.Id 5 | import org.springframework.data.annotation.Version 6 | import org.springframework.data.relational.core.mapping.Column 7 | import org.springframework.data.relational.core.mapping.Table 8 | 9 | @Table("matrix_room_alias") 10 | data class MatrixRoomAlias( 11 | @Id 12 | @Column("alias") 13 | val alias: MatrixId.RoomAliasId, 14 | 15 | @Column("room_id") 16 | val roomId: MatrixId.RoomId, 17 | 18 | @Version 19 | @Column("version") 20 | val version: Int = 0 21 | ) -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/event/MatrixSyncBatchToken.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.data.annotation.Id 5 | import org.springframework.data.annotation.Version 6 | import org.springframework.data.relational.core.mapping.Column 7 | import org.springframework.data.relational.core.mapping.Table 8 | 9 | 10 | @Table("matrix_sync_batch_token") 11 | data class MatrixSyncBatchToken( 12 | @Id 13 | @Column("user_id") 14 | val userId: MatrixId.UserId, 15 | 16 | @Column("token") 17 | val token: String?, 18 | 19 | @Version 20 | @Column("version") 21 | val version: Int = 0 22 | ) -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | bot: 3 | serverName: matrix-local 4 | displayName: BOT 5 | username: ping 6 | mode: APPSERVICE 7 | migration: 8 | url: jdbc:h2:mem:testdb 9 | username: sa 10 | database: 11 | url: r2dbc:h2:mem:///testdb 12 | username: sa 13 | client: 14 | homeServer: 15 | hostname: localhost 16 | port: 8008 17 | secure: false 18 | token: 30c05ae90a248a4188e620216fa72e349803310ec83e2a77b34fe90be6081f46 19 | appservice: 20 | hsToken: 312df522183efd404ec1cd22d2ffa4bbc76a8c1ccf541dd692eef281356bb74e 21 | namespaces: 22 | users: 23 | - localpartRegex: "ping_.*" 24 | aliases: 25 | - localpartRegex: "ping_.*" 26 | rooms: [ ] -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/user/MatrixUserRepository.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.user 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import net.folivo.trixnity.core.model.MatrixId 5 | import org.springframework.data.r2dbc.repository.Query 6 | import org.springframework.data.repository.kotlin.CoroutineCrudRepository 7 | import org.springframework.stereotype.Repository 8 | 9 | @Repository 10 | interface MatrixUserRepository : CoroutineCrudRepository { 11 | 12 | @Query( 13 | """ 14 | SELECT * FROM matrix_user u 15 | JOIN matrix_membership m ON m.user_id = u.id 16 | WHERE m.room_id = :roomId 17 | """ 18 | ) 19 | fun findByRoomId(roomId: MatrixId.RoomId): Flow 20 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/membership/MatrixMembership.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.data.annotation.Id 5 | import org.springframework.data.annotation.Version 6 | import org.springframework.data.relational.core.mapping.Column 7 | import org.springframework.data.relational.core.mapping.Table 8 | 9 | 10 | @Table("matrix_membership") 11 | data class MatrixMembership( 12 | @Column("user_id") 13 | val userId: MatrixId.UserId, 14 | @Column("room_id") 15 | val roomId: MatrixId.RoomId, 16 | @Id 17 | @Column("id") 18 | val id: String = "${userId.full}-${roomId.full}", 19 | @Version 20 | @Column("version") 21 | val version: Int = 0 22 | ) -------------------------------------------------------------------------------- /examples/demo-infra/config/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | neo4j: 3 | uri: bolt://neo4j:7687 4 | authentication: 5 | username: neo4j 6 | password: secret 7 | 8 | logging: 9 | level: 10 | net.folivo.matrix.bridge.sms: DEBUG 11 | 12 | matrix: 13 | bridge: 14 | sms: 15 | provider: 16 | gammu: 17 | enabled: true 18 | defaultRoomId: "!tRraLABExGeUDmOWvF:matrix-local" 19 | defaultRegion: DE 20 | defaultTimeZone: Europe/Berlin 21 | bot: 22 | serverName: matrix-local 23 | client: 24 | homeServer: 25 | hostname: matrix-synapse 26 | port: 8008 27 | secure: false 28 | token: 30c05ae90a248a4188e620216fa72e349803310ec83e2a77b34fe90be6081f46 29 | appservice: 30 | hsToken: 312df522183efd404ec1cd22d2ffa4bbc76a8c1ccf541dd692eef281356bb74e -------------------------------------------------------------------------------- /examples/demo-infra/synapse/matrix-kotlin-sdk-it-synapse.log.config: -------------------------------------------------------------------------------- 1 | 2 | version: 1 3 | 4 | formatters: 5 | precise: 6 | format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' 7 | 8 | filters: 9 | context: 10 | (): synapse.util.logcontext.LoggingContextFilter 11 | request: "" 12 | 13 | handlers: 14 | console: 15 | class: logging.StreamHandler 16 | formatter: precise 17 | filters: [context] 18 | 19 | loggers: 20 | synapse: 21 | level: WARNING 22 | 23 | synapse.storage.SQL: 24 | # beware: increasing this to DEBUG will make synapse log sensitive 25 | # information such as access tokens. 26 | level: WARNING 27 | 28 | rest_auth_provider: 29 | level: INFO 30 | 31 | root: 32 | level: WARNING 33 | handlers: [console] 34 | -------------------------------------------------------------------------------- /examples/ping-appservice-bot/src/main/resources/application.yml.example: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | net.folivo.matrix: DEBUG 4 | 5 | matrix: 6 | bot: 7 | serverName: matrix-local 8 | username: ping 9 | displayname: PING BOT 2000 10 | mode: APPSERVICE 11 | migration: 12 | url: jdbc:h2:file:./testdb/testdb 13 | database: 14 | url: r2dbc:h2:file:///./testdb/testdb 15 | username: sa 16 | client: 17 | homeServer: 18 | hostname: localhost 19 | port: 8008 20 | secure: false 21 | token: 30c05ae90a248a4188e620216fa72e349803310ec83e2a77b34fe90be6081f46 22 | appservice: 23 | hsToken: 312df522183efd404ec1cd22d2ffa4bbc76a8c1ccf541dd692eef281356bb74e 24 | namespaces: 25 | users: 26 | - localpartRegex: "ping_.*" 27 | aliases: 28 | - localpartRegex: "ping_.*" 29 | rooms: [] -------------------------------------------------------------------------------- /matrix-spring-boot-bot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | annotationProcessor("org.springframework.boot:spring-boot-autoconfigure-processor") 3 | annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") 4 | 5 | api(project(":matrix-spring-boot-rest-client")) 6 | api(project(":matrix-spring-boot-rest-appservice")) 7 | 8 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") 9 | implementation("com.michael-bull.kotlin-retry:kotlin-retry:${Versions.kotlinRetry}") 10 | 11 | 12 | api("org.springframework.boot:spring-boot-starter-data-r2dbc") 13 | 14 | api("org.liquibase:liquibase-core") 15 | implementation("org.springframework.data:spring-data-jdbc") 16 | implementation("com.zaxxer:HikariCP") 17 | 18 | testImplementation("io.r2dbc:r2dbc-h2") 19 | testImplementation("com.h2database:h2") 20 | 21 | testImplementation("io.projectreactor:reactor-test") 22 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/appservice/event/DefaultAppserviceEventTnxService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice.event 2 | 3 | import net.folivo.trixnity.appservice.rest.event.AppserviceEventTnxService 4 | import net.folivo.trixnity.appservice.rest.event.AppserviceEventTnxService.EventTnxProcessingState 5 | 6 | open class DefaultAppserviceEventTnxService( 7 | private val eventTransactionService: MatrixEventTransactionService, 8 | ) : AppserviceEventTnxService { 9 | 10 | override suspend fun eventTnxProcessingState(tnxId: String): EventTnxProcessingState { 11 | return if (eventTransactionService.hasTransaction(tnxId)) 12 | EventTnxProcessingState.PROCESSED 13 | else 14 | EventTnxProcessingState.NOT_PROCESSED 15 | } 16 | 17 | override suspend fun onEventTnxProcessed(tnxId: String) { 18 | eventTransactionService.saveTransaction(MatrixEventTransaction(tnxId)) 19 | } 20 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/room/MatrixRoomRepository.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.room 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import net.folivo.trixnity.core.model.MatrixId 5 | import org.springframework.data.r2dbc.repository.Query 6 | import org.springframework.data.repository.kotlin.CoroutineCrudRepository 7 | import org.springframework.stereotype.Repository 8 | 9 | @Repository 10 | interface MatrixRoomRepository : CoroutineCrudRepository { 11 | 12 | @Query( 13 | """ 14 | WITH counted_rooms AS 15 | (SELECT room_id FROM matrix_membership m 16 | WHERE m.user_id IN (:members) 17 | GROUP BY m.room_id 18 | HAVING COUNT(m.user_id) = :#{#members.size()}) 19 | SELECT * FROM matrix_room r 20 | JOIN counted_rooms ON counted_rooms.room_id = r.id 21 | """ 22 | ) 23 | fun findByMembers(members: Set): Flow 24 | } -------------------------------------------------------------------------------- /examples/ping-bot/src/main/kotlin/net/folivo/spring/matrix/bot/examples/pingappservice/PingHandler.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.examples.pingappservice 2 | 3 | import net.folivo.spring.matrix.bot.event.MatrixMessageHandler 4 | import net.folivo.spring.matrix.bot.event.MessageContext 5 | import net.folivo.trixnity.core.model.events.m.room.MessageEventContent 6 | import org.slf4j.LoggerFactory 7 | import org.springframework.stereotype.Component 8 | 9 | @Component 10 | class PingHandler : MatrixMessageHandler { 11 | companion object { 12 | private val LOG = LoggerFactory.getLogger(this::class.java) 13 | } 14 | 15 | override suspend fun handleMessage(content: MessageEventContent, context: MessageContext) { 16 | if (content is MessageEventContent.TextMessageEventContent) { 17 | if (content.body.contains("ping")) { 18 | val messageId = context.answer("pong") 19 | LOG.info("pong (messageId: $messageId)") 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/util/BotServiceHelper.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.util 2 | 3 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 4 | import net.folivo.trixnity.core.model.MatrixId 5 | 6 | class BotServiceHelper( 7 | private val botProperties: MatrixBotProperties, 8 | private val userNamespaceRegex: Set, 9 | private val roomNamespaceRegex: Set 10 | ) { 11 | fun isManagedUser(userId: MatrixId.UserId): Boolean { 12 | return if (userId.domain == botProperties.serverName) 13 | userId.localpart == botProperties.username || userNamespaceRegex.map { userId.localpart.matches(it) } 14 | .contains(true) 15 | else false 16 | } 17 | 18 | fun isManagedRoom(roomAlias: MatrixId.RoomAliasId): Boolean { 19 | return if (roomAlias.domain == botProperties.serverName) 20 | roomNamespaceRegex.map { roomAlias.localpart.matches(it) }.contains(true) 21 | else false 22 | } 23 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-appservice/src/main/kotlin/net/folivo/spring/matrix/appservice/MatrixAppserviceConfigurationProperties.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.appservice 2 | 3 | import net.folivo.trixnity.appservice.rest.MatrixAppserviceProperties 4 | import org.springframework.boot.context.properties.ConfigurationProperties 5 | import org.springframework.boot.context.properties.ConstructorBinding 6 | 7 | @ConfigurationProperties("matrix.appservice") 8 | @ConstructorBinding 9 | data class MatrixAppserviceConfigurationProperties( 10 | val hsToken: String, 11 | val port: Int = 8080, 12 | val namespaces: Namespaces = Namespaces() 13 | ) { 14 | data class Namespaces( 15 | val users: List = emptyList(), 16 | val aliases: List = emptyList(), 17 | val rooms: List = emptyList() 18 | ) 19 | 20 | data class Namespace( 21 | val localpartRegex: String 22 | ) 23 | 24 | fun toMatrixAppserviceProperties(): MatrixAppserviceProperties { 25 | return MatrixAppserviceProperties(hsToken) 26 | } 27 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/client/MatrixClientSyncRunnerTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.client 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.mockk.clearMocks 5 | import io.mockk.coVerify 6 | import io.mockk.mockk 7 | import net.folivo.trixnity.client.rest.MatrixClient 8 | 9 | class MatrixClientSyncRunnerTest : DescribeSpec({ 10 | val matrixClientMock: MatrixClient = mockk(relaxed = true) 11 | 12 | val cut = MatrixClientSyncRunner(matrixClientMock) 13 | 14 | describe(MatrixClientSyncRunner::startClientJob.name) { 15 | it("should start sync") { 16 | cut.startClientJob() 17 | coVerify { matrixClientMock.sync.start(wait = true) } 18 | cut.destroy() 19 | } 20 | } 21 | 22 | describe(MatrixClientSyncRunner::destroy.name) { 23 | it("should stop sync") { 24 | cut.destroy() 25 | coVerify { 26 | matrixClientMock.sync.stop() 27 | } 28 | } 29 | } 30 | 31 | afterTest { clearMocks(matrixClientMock) } 32 | }) -------------------------------------------------------------------------------- /matrix-spring-boot-rest-client/src/main/kotlin/net/folivo/spring/matrix/restclient/MatrixClientConfigurationProperties.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.restclient 2 | 3 | import net.folivo.trixnity.client.rest.MatrixClientProperties 4 | import org.springframework.boot.context.properties.ConfigurationProperties 5 | import org.springframework.boot.context.properties.ConstructorBinding 6 | 7 | @ConfigurationProperties("matrix.client") 8 | @ConstructorBinding 9 | data class MatrixClientConfigurationProperties( 10 | val homeServer: MatrixHomeServerConfigurationProperties, 11 | val token: String? 12 | ) { 13 | data class MatrixHomeServerConfigurationProperties( 14 | val hostname: String, 15 | val port: Int = 443, 16 | val secure: Boolean = true 17 | ) 18 | 19 | fun toMatrixClientProperties(): MatrixClientProperties { 20 | return MatrixClientProperties( 21 | MatrixClientProperties.MatrixHomeServerProperties( 22 | homeServer.hostname, 23 | homeServer.port, 24 | homeServer.secure 25 | ), token 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/user/MatrixUserService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.user 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 5 | import net.folivo.trixnity.core.model.MatrixId 6 | 7 | class MatrixUserService( 8 | private val userRepository: MatrixUserRepository, 9 | private val helper: BotServiceHelper 10 | ) { 11 | 12 | suspend fun getOrCreateUser(userId: MatrixId.UserId): MatrixUser { 13 | return userRepository.findById(userId) 14 | ?: userRepository.save(MatrixUser(userId, helper.isManagedUser(userId))) 15 | } 16 | 17 | suspend fun deleteUser(userId: MatrixId.UserId) { 18 | userRepository.deleteById(userId) 19 | } 20 | 21 | suspend fun deleteAllUsers() { 22 | userRepository.deleteAll() 23 | } 24 | 25 | suspend fun existsUser(userId: MatrixId.UserId): Boolean { 26 | return userRepository.existsById(userId) 27 | } 28 | 29 | fun getUsersByRoom(roomId: MatrixId.RoomId): Flow { 30 | return userRepository.findByRoomId(roomId) 31 | } 32 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/client/MatrixClientSyncRunner.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.client 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import net.folivo.trixnity.client.rest.MatrixClient 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory 7 | import org.springframework.beans.factory.DisposableBean 8 | import org.springframework.boot.context.event.ApplicationReadyEvent 9 | import org.springframework.context.event.EventListener 10 | import kotlin.concurrent.thread 11 | 12 | class MatrixClientSyncRunner( 13 | private val matrixClient: MatrixClient, 14 | ) : DisposableBean { 15 | 16 | companion object { 17 | private val LOG: Logger = LoggerFactory.getLogger(this::class.java) 18 | } 19 | 20 | @EventListener(ApplicationReadyEvent::class) 21 | fun startClientJob() { 22 | thread { 23 | runBlocking { 24 | LOG.debug("starting sync") 25 | matrixClient.sync.start(wait = true) 26 | } 27 | } 28 | } 29 | 30 | override fun destroy() { 31 | runBlocking { 32 | LOG.debug("stopping sync") 33 | matrixClient.sync.stop() 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/appservice/BotUserInitializer.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 5 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService 6 | import org.slf4j.LoggerFactory 7 | import org.springframework.boot.context.event.ApplicationReadyEvent 8 | import org.springframework.context.event.EventListener 9 | 10 | class BotUserInitializer( 11 | private val appserviceUserService: AppserviceUserService, 12 | private val botProperties: MatrixBotProperties 13 | ) { 14 | companion object { 15 | private val LOG = LoggerFactory.getLogger(this::class.java) 16 | } 17 | 18 | @EventListener(ApplicationReadyEvent::class) 19 | fun initializeBotUser() { 20 | runBlocking { 21 | LOG.info("Initializing appservice bot") 22 | 23 | val userId = botProperties.botUserId 24 | 25 | try { 26 | appserviceUserService.registerManagedUser(userId) 27 | } catch (error: Throwable) { 28 | LOG.warn("failed to initialize appservice bot: ${error.message}") 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/config/MatrixBotProperties.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.config 2 | 3 | import net.folivo.trixnity.core.model.MatrixId 4 | import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties 5 | import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties 6 | import org.springframework.boot.context.properties.ConfigurationProperties 7 | import org.springframework.boot.context.properties.ConstructorBinding 8 | 9 | @ConfigurationProperties("matrix.bot") 10 | @ConstructorBinding 11 | data class MatrixBotProperties( 12 | val autoJoin: AutoJoinMode = AutoJoinMode.RESTRICTED, 13 | val trackMembership: TrackMembershipMode = TrackMembershipMode.ALL, 14 | val serverName: String, 15 | val username: String, 16 | val botUserId: MatrixId.UserId = MatrixId.UserId(username, serverName), 17 | val displayName: String? = null, 18 | val mode: BotMode = BotMode.CLIENT, 19 | val database: R2dbcProperties, 20 | val migration: DataSourceProperties 21 | ) { 22 | enum class BotMode { 23 | APPSERVICE, CLIENT 24 | } 25 | 26 | enum class AutoJoinMode { 27 | ENABLED, DISABLED, RESTRICTED 28 | } 29 | 30 | enum class TrackMembershipMode { 31 | ALL, MANAGED, NONE 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/ping-bot/src/test/kotlin/net/folivo/spring/matrix/bot/examples/pingappservice/PingHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.examples.pingappservice 2 | 3 | import io.mockk.coEvery 4 | import io.mockk.coVerify 5 | import io.mockk.impl.annotations.MockK 6 | import io.mockk.junit5.MockKExtension 7 | import kotlinx.coroutines.runBlocking 8 | import net.folivo.spring.matrix.bot.event.MessageContext 9 | import net.folivo.trixnity.core.model.MatrixId 10 | import net.folivo.trixnity.core.model.events.m.room.MessageEventContent 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.api.extension.ExtendWith 13 | 14 | @ExtendWith(MockKExtension::class) 15 | class PingHandlerTest { 16 | 17 | @MockK(relaxed = true) 18 | lateinit var context: MessageContext 19 | 20 | @Test 21 | fun `should pong after ping message`() { 22 | val cut = PingHandler() 23 | 24 | coEvery { context.answer(any()) } returns MatrixId.EventId("event", "server") 25 | 26 | runBlocking { 27 | cut.handleMessage(MessageEventContent.TextMessageEventContent("ping"), context) 28 | cut.handleMessage(MessageEventContent.TextMessageEventContent("some ping message"), context) 29 | } 30 | 31 | coVerify(exactly = 2) { context.answer("pong") } 32 | } 33 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/appservice/BotUserInitializerTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.mockk.coEvery 5 | import io.mockk.coVerify 6 | import io.mockk.mockk 7 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 8 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService 9 | import net.folivo.trixnity.core.model.MatrixId 10 | 11 | class BotUserInitializerTest : DescribeSpec(testBody()) 12 | 13 | private fun testBody(): DescribeSpec.() -> Unit { 14 | return { 15 | val appserviceUserServiceMock: AppserviceUserService = mockk(relaxed = true) 16 | val botPropertiesMock: MatrixBotProperties = mockk() 17 | 18 | val cut = BotUserInitializer(appserviceUserServiceMock, botPropertiesMock) 19 | 20 | describe(BotUserInitializer::initializeBotUser.name) { 21 | it("should initialize bot user") { 22 | coEvery { botPropertiesMock.botUserId }.returns(MatrixId.UserId("@bot:server")) 23 | cut.initializeBotUser() 24 | 25 | coVerify { 26 | appserviceUserServiceMock.registerManagedUser(MatrixId.UserId("@bot:server")) 27 | } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-client/src/test/kotlin/net/folivo/spring/matrix/restclient/MatrixClientAutoconfigurationContextTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.restclient 2 | 3 | import net.folivo.trixnity.client.rest.MatrixClient 4 | import net.folivo.trixnity.client.rest.api.sync.SyncBatchTokenService 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.Test 7 | import org.springframework.boot.autoconfigure.AutoConfigurations 8 | import org.springframework.boot.test.context.assertj.AssertableApplicationContext 9 | import org.springframework.boot.test.context.runner.ApplicationContextRunner 10 | 11 | 12 | class MatrixClientAutoconfigurationContextTest { 13 | 14 | private val contextRunner = ApplicationContextRunner() 15 | .withConfiguration( 16 | AutoConfigurations.of( 17 | MatrixClientAutoconfiguration::class.java, 18 | ) 19 | ) 20 | 21 | @Test 22 | fun `default services`() { 23 | this.contextRunner 24 | .withPropertyValues("matrix.client.homeServer.hostname=localhost", "matrix.client.token=test") 25 | .run { context: AssertableApplicationContext -> 26 | assertThat(context).hasSingleBean(MatrixClient::class.java) 27 | assertThat(context).hasSingleBean(SyncBatchTokenService::class.java) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/event/PersistentSyncBatchTokenService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 4 | import net.folivo.spring.matrix.bot.user.MatrixUserService 5 | import net.folivo.trixnity.client.rest.api.sync.SyncBatchTokenService 6 | import net.folivo.trixnity.core.model.MatrixId 7 | 8 | class PersistentSyncBatchTokenService( 9 | private val syncBatchTokenRepository: MatrixSyncBatchTokenRepository, 10 | private val userService: MatrixUserService, 11 | private val botProperties: MatrixBotProperties 12 | ) : SyncBatchTokenService { 13 | 14 | override suspend fun getBatchToken(userId: MatrixId.UserId?): String? { 15 | return syncBatchTokenRepository.findByUserId(userId ?: botProperties.botUserId)?.token 16 | } 17 | 18 | override suspend fun setBatchToken(value: String?, userId: MatrixId.UserId?) { 19 | val realUserId = userId ?: botProperties.botUserId 20 | val token = syncBatchTokenRepository.findByUserId(realUserId) 21 | if (token != null) { 22 | syncBatchTokenRepository.save(token.copy(token = value)) 23 | } else { 24 | userService.getOrCreateUser(realUserId) 25 | syncBatchTokenRepository.save(MatrixSyncBatchToken(realUserId, value)) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/client/ClientMemberEventHandler.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.client 2 | 3 | import net.folivo.spring.matrix.bot.event.MatrixEventHandler 4 | import net.folivo.spring.matrix.bot.membership.MembershipChangeHandler 5 | import net.folivo.trixnity.core.model.MatrixId 6 | import net.folivo.trixnity.core.model.events.Event 7 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent 8 | import kotlin.reflect.KClass 9 | 10 | class ClientMemberEventHandler( 11 | private val membershipChangeHandler: MembershipChangeHandler 12 | ) : MatrixEventHandler { 13 | 14 | override fun supports(): KClass { 15 | return MemberEventContent::class 16 | } 17 | 18 | override suspend fun handleEvent(event: Event) { 19 | if (event is Event.StateEvent) { 20 | membershipChangeHandler.handleMembership( 21 | MatrixId.UserId(event.stateKey), 22 | event.roomId, 23 | event.content.membership 24 | ) 25 | } else if (event is Event.StrippedStateEvent) { 26 | membershipChangeHandler.handleMembership( 27 | MatrixId.UserId(event.stateKey), 28 | event.roomId, 29 | event.content.membership 30 | ) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/appservice/sync/InitialSyncService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice.sync 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import net.folivo.spring.matrix.bot.membership.MatrixMembershipSyncService 5 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 6 | import net.folivo.spring.matrix.bot.user.MatrixUserService 7 | import org.slf4j.LoggerFactory 8 | import org.springframework.boot.context.event.ApplicationReadyEvent 9 | import org.springframework.context.event.EventListener 10 | 11 | class InitialSyncService( 12 | private val userService: MatrixUserService, 13 | private val roomService: MatrixRoomService, 14 | private val membershipSyncService: MatrixMembershipSyncService 15 | ) { 16 | 17 | companion object { 18 | private val LOG = LoggerFactory.getLogger(this::class.java) 19 | } 20 | 21 | @EventListener(ApplicationReadyEvent::class) 22 | fun initialSync() { 23 | LOG.info("started initial sync") 24 | 25 | runBlocking { 26 | LOG.info("delete all users and rooms") 27 | roomService.deleteAllRooms() 28 | userService.deleteAllUsers() 29 | 30 | LOG.info("collect all joined rooms (of bot user) - this can take some time!") 31 | membershipSyncService.syncBotRoomsAndMemberships() 32 | 33 | LOG.info("finished initial sync") 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/event/MessageEventHandler.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import net.folivo.trixnity.client.rest.MatrixClient 4 | import net.folivo.trixnity.core.model.events.Event 5 | import net.folivo.trixnity.core.model.events.m.room.MessageEventContent 6 | import org.slf4j.LoggerFactory 7 | import kotlin.reflect.KClass 8 | 9 | class MessageEventHandler( 10 | private val messageHandler: List, 11 | private val matrixClient: MatrixClient 12 | ) : MatrixEventHandler { 13 | 14 | companion object { 15 | private val LOG = LoggerFactory.getLogger(this::class.java) 16 | } 17 | 18 | override fun supports(): KClass { 19 | return MessageEventContent::class 20 | } 21 | 22 | override suspend fun handleEvent(event: Event) { 23 | require(event is Event.RoomEvent) 24 | 25 | val messageContext = MessageContext( 26 | matrixClient, 27 | event, 28 | event.roomId 29 | ) 30 | LOG.debug("handle message event") 31 | messageHandler 32 | .forEach { 33 | try { 34 | it.handleMessage(event.content, messageContext) 35 | } catch (error: Throwable) { 36 | LOG.warn("could not handle message", error) 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-appservice/src/main/kotlin/net/folivo/spring/matrix/appservice/AppserviceApplicationEngine.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.appservice 2 | 3 | import io.ktor.server.cio.* 4 | import io.ktor.server.engine.* 5 | import net.folivo.trixnity.appservice.rest.AppserviceService 6 | import net.folivo.trixnity.appservice.rest.matrixAppserviceModule 7 | import org.slf4j.Logger 8 | import org.slf4j.LoggerFactory 9 | import org.springframework.beans.factory.DisposableBean 10 | import org.springframework.boot.context.event.ApplicationReadyEvent 11 | import org.springframework.context.event.EventListener 12 | import kotlin.concurrent.thread 13 | 14 | class AppserviceApplicationEngine( 15 | properties: MatrixAppserviceConfigurationProperties, 16 | appserviceService: AppserviceService 17 | ) : DisposableBean { 18 | 19 | companion object { 20 | private val LOG: Logger = LoggerFactory.getLogger(this::class.java) 21 | } 22 | 23 | private val engine: ApplicationEngine = embeddedServer(CIO, port = properties.port) { 24 | matrixAppserviceModule(properties.toMatrixAppserviceProperties(), appserviceService) 25 | } 26 | 27 | @EventListener(ApplicationReadyEvent::class) 28 | fun startApplicationEngine() { 29 | thread { 30 | LOG.debug("starting appservice webserver") 31 | engine.start(wait = true) 32 | } 33 | } 34 | 35 | override fun destroy() { 36 | engine.stop(5000, 500) 37 | } 38 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-appservice/src/test/kotlin/net/folivo/spring/matrix/appservice/MatrixAppserviceAutoconfigurationContextTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.appservice 2 | 3 | import net.folivo.spring.matrix.restclient.MatrixClientAutoconfiguration 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.boot.autoconfigure.AutoConfigurations 7 | import org.springframework.boot.test.context.assertj.AssertableApplicationContext 8 | import org.springframework.boot.test.context.runner.ApplicationContextRunner 9 | 10 | class MatrixAppserviceAutoconfigurationContextTest { 11 | 12 | private val contextRunner = ApplicationContextRunner() 13 | .withConfiguration( 14 | AutoConfigurations.of( 15 | MatrixAppserviceAutoconfiguration::class.java, 16 | MatrixClientAutoconfiguration::class.java, 17 | TestConfiguration::class.java 18 | ) 19 | ) 20 | 21 | @Test 22 | fun `default services`() { 23 | this.contextRunner 24 | .withPropertyValues( 25 | "matrix.client.homeServer.hostname=localhost", 26 | "matrix.client.token=test", 27 | "matrix.appservice.hsToken=token" 28 | ) 29 | .run { context: AssertableApplicationContext -> 30 | assertThat(context).hasSingleBean(AppserviceApplicationEngine::class.java) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/appservice/sync/InitialSyncServiceTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice.sync 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.mockk.coVerify 5 | import io.mockk.mockk 6 | import net.folivo.spring.matrix.bot.membership.MatrixMembershipSyncService 7 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 8 | import net.folivo.spring.matrix.bot.user.MatrixUserService 9 | 10 | class InitialSyncServiceTest : DescribeSpec(testBody()) 11 | 12 | private fun testBody(): DescribeSpec.() -> Unit { 13 | return { 14 | val userServiceMock: MatrixUserService = mockk(relaxed = true) 15 | val roomServiceMock: MatrixRoomService = mockk(relaxed = true) 16 | val membershipSyncServiceMock: MatrixMembershipSyncService = mockk(relaxed = true) 17 | 18 | val cut = InitialSyncService(userServiceMock, roomServiceMock, membershipSyncServiceMock) 19 | 20 | describe(InitialSyncService::initialSync.name) { 21 | cut.initialSync() 22 | it("should delete all rooms") { 23 | coVerify { roomServiceMock.deleteAllRooms() } 24 | } 25 | it("should delete all users") { 26 | coVerify { userServiceMock.deleteAllUsers() } 27 | } 28 | it("should sync rooms of bot user") { 29 | coVerify { membershipSyncServiceMock.syncBotRoomsAndMemberships() } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/appservice/DefaultAppserviceRoomService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice 2 | 3 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 4 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 5 | import net.folivo.trixnity.appservice.rest.room.AppserviceRoomService 6 | import net.folivo.trixnity.appservice.rest.room.AppserviceRoomService.RoomExistingState.* 7 | import net.folivo.trixnity.appservice.rest.room.CreateRoomParameter 8 | import net.folivo.trixnity.client.rest.MatrixClient 9 | import net.folivo.trixnity.core.model.MatrixId 10 | 11 | open class DefaultAppserviceRoomService( 12 | private val roomService: MatrixRoomService, 13 | private val helper: BotServiceHelper, 14 | override val matrixClient: MatrixClient 15 | ) : AppserviceRoomService { 16 | 17 | override suspend fun roomExistingState(roomAlias: MatrixId.RoomAliasId): AppserviceRoomService.RoomExistingState { 18 | val roomExists = roomService.existsByRoomAlias(roomAlias) 19 | return if (roomExists) EXISTS 20 | else if (helper.isManagedRoom(roomAlias)) CAN_BE_CREATED else DOES_NOT_EXISTS 21 | } 22 | 23 | override suspend fun getCreateRoomParameter(roomAlias: MatrixId.RoomAliasId): CreateRoomParameter { 24 | return CreateRoomParameter() 25 | } 26 | 27 | override suspend fun onCreatedRoom(roomAlias: MatrixId.RoomAliasId, roomId: MatrixId.RoomId) { 28 | roomService.getOrCreateRoomAlias(roomAlias, roomId) 29 | } 30 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/event/EventHandlerRunner.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import kotlinx.coroutines.* 4 | import kotlinx.coroutines.flow.collect 5 | import net.folivo.trixnity.core.EventEmitter 6 | import net.folivo.trixnity.core.model.events.Event 7 | import org.slf4j.Logger 8 | import org.slf4j.LoggerFactory 9 | import org.springframework.beans.factory.DisposableBean 10 | import org.springframework.boot.context.event.ApplicationReadyEvent 11 | import org.springframework.context.event.EventListener 12 | 13 | class EventHandlerRunner( 14 | private val eventEmitter: EventEmitter, 15 | private val eventHandler: List>, 16 | ) : DisposableBean { 17 | companion object { 18 | private val LOG: Logger = LoggerFactory.getLogger(this::class.java) 19 | } 20 | 21 | private val scope = CoroutineScope(Dispatchers.Default) 22 | private val jobs = mutableListOf() 23 | 24 | @EventListener(ApplicationReadyEvent::class) 25 | fun startEventListening() { 26 | runBlocking { 27 | LOG.debug("started event listening") 28 | eventHandler.forEach { handler -> 29 | val eventFlow = eventEmitter.events(handler.supports()) 30 | jobs.add(scope.launch { 31 | @Suppress("UNCHECKED_CAST") 32 | eventFlow.collect { handler.handleEvent(it as Event) } 33 | }) 34 | } 35 | } 36 | } 37 | 38 | override fun destroy() { 39 | jobs.forEach { it.cancel() } 40 | jobs.clear() 41 | } 42 | } -------------------------------------------------------------------------------- /examples/ping-appservice-bot/src/main/kotlin/net/folivo/spring/matrix/bot/examples/pingappservice/PingHandler.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.examples.pingappservice 2 | 3 | import kotlinx.coroutines.flow.collect 4 | import kotlinx.coroutines.flow.filter 5 | import net.folivo.spring.matrix.bot.event.MatrixMessageHandler 6 | import net.folivo.spring.matrix.bot.event.MessageContext 7 | import net.folivo.spring.matrix.bot.user.MatrixUserService 8 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 9 | import net.folivo.trixnity.client.rest.MatrixClient 10 | import net.folivo.trixnity.core.model.events.m.room.MessageEventContent 11 | import org.slf4j.LoggerFactory 12 | import org.springframework.stereotype.Component 13 | 14 | @Component 15 | class PingHandler( 16 | private val matrixClient: MatrixClient, 17 | private val helper: BotServiceHelper, 18 | private val userService: MatrixUserService 19 | ) : MatrixMessageHandler { 20 | companion object { 21 | private val LOG = LoggerFactory.getLogger(this::class.java) 22 | } 23 | 24 | override suspend fun handleMessage(content: MessageEventContent, context: MessageContext) { 25 | if (content is MessageEventContent.TextMessageEventContent) { 26 | if (content.body.contains("ping")) { 27 | userService.getUsersByRoom(context.roomId) 28 | .filter { it.isManaged } 29 | .collect { member -> 30 | val messageId = context.answer("pong", asUserId = member.id) 31 | LOG.info("pong (messageid: $messageId)") 32 | } 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-client/src/main/kotlin/net/folivo/spring/matrix/restclient/MatrixClientAutoconfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.restclient 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.java.* 5 | import net.folivo.trixnity.client.rest.MatrixClient 6 | import net.folivo.trixnity.client.rest.api.sync.InMemorySyncBatchTokenService 7 | import net.folivo.trixnity.client.rest.api.sync.SyncBatchTokenService 8 | import org.slf4j.LoggerFactory 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.Configuration 13 | 14 | @Configuration 15 | @EnableConfigurationProperties(MatrixClientConfigurationProperties::class) 16 | class MatrixClientAutoconfiguration { 17 | 18 | companion object { 19 | private val LOG = LoggerFactory.getLogger(this::class.java) 20 | } 21 | 22 | @Bean 23 | @ConditionalOnMissingBean 24 | fun matrixClient( 25 | properties: MatrixClientConfigurationProperties, 26 | syncBatchTokenService: SyncBatchTokenService 27 | ): MatrixClient { 28 | return MatrixClient(HttpClient(Java), properties.toMatrixClientProperties(), syncBatchTokenService) 29 | } 30 | 31 | @Bean 32 | @ConditionalOnMissingBean 33 | fun inMemorySyncBatchTokenService(): SyncBatchTokenService { 34 | LOG.info("you should implement a persistent SyncBatchTokenService if you use the sync api. Currently used: ${InMemorySyncBatchTokenService::class.simpleName}") 35 | return InMemorySyncBatchTokenService 36 | } 37 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/appservice/DefaultAppserviceUserService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice 2 | 3 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 4 | import net.folivo.spring.matrix.bot.user.MatrixUserService 5 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 6 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService 7 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService.UserExistingState.* 8 | import net.folivo.trixnity.appservice.rest.user.RegisterUserParameter 9 | import net.folivo.trixnity.client.rest.MatrixClient 10 | import net.folivo.trixnity.core.model.MatrixId 11 | 12 | open class DefaultAppserviceUserService( 13 | private val userService: MatrixUserService, 14 | private val helper: BotServiceHelper, 15 | private val botProperties: MatrixBotProperties, 16 | override val matrixClient: MatrixClient 17 | ) : AppserviceUserService { 18 | 19 | override suspend fun userExistingState(userId: MatrixId.UserId): AppserviceUserService.UserExistingState { 20 | val userExists = userService.existsUser(userId) 21 | return if (userExists) EXISTS 22 | else if (helper.isManagedUser(userId)) CAN_BE_CREATED else DOES_NOT_EXISTS 23 | 24 | } 25 | 26 | override suspend fun getRegisterUserParameter(userId: MatrixId.UserId): RegisterUserParameter { 27 | return if (userId == botProperties.botUserId) { 28 | RegisterUserParameter(botProperties.displayName) 29 | } else { 30 | RegisterUserParameter() 31 | } 32 | } 33 | 34 | override suspend fun onRegisteredUser(userId: MatrixId.UserId) { 35 | userService.getOrCreateUser(userId) 36 | } 37 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/membership/MatrixMembershipRepository.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import net.folivo.trixnity.core.model.MatrixId 5 | import org.springframework.data.r2dbc.repository.Query 6 | import org.springframework.data.repository.kotlin.CoroutineCrudRepository 7 | import org.springframework.stereotype.Repository 8 | 9 | @Repository 10 | interface MatrixMembershipRepository : CoroutineCrudRepository { 11 | 12 | fun findByRoomId(roomId: MatrixId.RoomId): Flow 13 | 14 | fun findByUserId(userId: MatrixId.UserId): Flow 15 | 16 | suspend fun countByRoomId(roomId: MatrixId.RoomId): Long 17 | 18 | suspend fun countByUserId(userId: MatrixId.UserId): Long 19 | 20 | suspend fun findByUserIdAndRoomId(userId: MatrixId.UserId, roomId: MatrixId.RoomId): MatrixMembership? 21 | 22 | suspend fun deleteByUserIdAndRoomId(userId: MatrixId.UserId, roomId: MatrixId.RoomId) 23 | 24 | @Query( 25 | """ 26 | SELECT CASE WHEN COUNT(*) = :#{#members.size()} THEN true ELSE false END 27 | FROM matrix_membership m 28 | WHERE m.room_id = :roomId AND m.user_id IN (:members) 29 | """ 30 | ) 31 | suspend fun containsMembersByRoomId(roomId: MatrixId.RoomId, members: Set): Boolean 32 | 33 | @Query( 34 | """ 35 | SELECT CASE WHEN COUNT(*) = 0 THEN true ELSE false END 36 | FROM matrix_membership m 37 | JOIN matrix_user u ON m.user_id = u.id 38 | WHERE m.room_id = :roomId AND u.is_managed = false 39 | """ 40 | ) 41 | suspend fun containsOnlyManagedMembersByRoomId(roomId: MatrixId.RoomId): Boolean 42 | 43 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/event/MessageContext.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import com.github.michaelbull.retry.policy.binaryExponentialBackoff 4 | import com.github.michaelbull.retry.policy.limitAttempts 5 | import com.github.michaelbull.retry.policy.plus 6 | import com.github.michaelbull.retry.retry 7 | import net.folivo.trixnity.client.rest.MatrixClient 8 | import net.folivo.trixnity.core.model.MatrixId 9 | import net.folivo.trixnity.core.model.events.Event 10 | import net.folivo.trixnity.core.model.events.m.room.MessageEventContent 11 | import org.slf4j.LoggerFactory 12 | 13 | class MessageContext( 14 | val matrixClient: MatrixClient, 15 | val originalEvent: Event.RoomEvent, 16 | val roomId: MatrixId.RoomId 17 | ) { 18 | 19 | companion object { 20 | private val LOG = LoggerFactory.getLogger(this::class.java) 21 | } 22 | 23 | suspend fun answer( 24 | content: MessageEventContent, 25 | asUserId: MatrixId.UserId? = null 26 | ): MatrixId.EventId { 27 | return try { 28 | retry(limitAttempts(5) + binaryExponentialBackoff(LongRange(500, 10000))) { 29 | matrixClient.room.sendRoomEvent( 30 | roomId = roomId, 31 | eventContent = content, 32 | asUserId = asUserId 33 | ) 34 | } 35 | } catch (error: Throwable) { 36 | LOG.warn("could not answer to $roomId", error) 37 | throw error 38 | } 39 | } 40 | 41 | suspend fun answer( 42 | content: String, 43 | asUserId: MatrixId.UserId? = null 44 | ): MatrixId.EventId { 45 | return answer(MessageEventContent.NoticeMessageEventContent(content), asUserId) 46 | } 47 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-appservice/src/main/kotlin/net/folivo/spring/matrix/appservice/MatrixAppserviceAutoconfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.appservice 2 | 3 | import net.folivo.trixnity.appservice.rest.AppserviceService 4 | import net.folivo.trixnity.appservice.rest.DefaultAppserviceService 5 | import net.folivo.trixnity.appservice.rest.event.AppserviceEventTnxService 6 | import net.folivo.trixnity.appservice.rest.room.AppserviceRoomService 7 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService 8 | import net.folivo.trixnity.client.rest.MatrixClient 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.Configuration 13 | 14 | 15 | @Configuration 16 | @EnableConfigurationProperties(MatrixAppserviceConfigurationProperties::class) 17 | class MatrixAppserviceAutoconfiguration { 18 | 19 | 20 | @Bean 21 | @ConditionalOnMissingBean 22 | fun defaultAppserviceService( 23 | matrixClient: MatrixClient, 24 | appserviceEventService: AppserviceEventTnxService, 25 | appserviceUserService: AppserviceUserService, 26 | appserviceRoomService: AppserviceRoomService, 27 | ): AppserviceService { 28 | return DefaultAppserviceService( 29 | appserviceEventService, 30 | appserviceUserService, 31 | appserviceRoomService, 32 | ) 33 | } 34 | 35 | 36 | @Bean 37 | @ConditionalOnMissingBean 38 | fun appserviceApplication( 39 | properties: MatrixAppserviceConfigurationProperties, 40 | appserviceService: AppserviceService 41 | ): AppserviceApplicationEngine { 42 | return AppserviceApplicationEngine(properties, appserviceService) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/config/MatrixClientBotAutoconfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.config 2 | 3 | import net.folivo.spring.matrix.bot.client.ClientMemberEventHandler 4 | import net.folivo.spring.matrix.bot.client.MatrixClientSyncRunner 5 | import net.folivo.spring.matrix.bot.event.EventHandlerRunner 6 | import net.folivo.spring.matrix.bot.event.MatrixEventHandler 7 | import net.folivo.spring.matrix.bot.membership.MembershipChangeHandler 8 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 9 | import net.folivo.trixnity.client.rest.MatrixClient 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.Configuration 13 | 14 | 15 | @Configuration 16 | @ConditionalOnProperty(prefix = "matrix.bot", name = ["mode"], havingValue = "CLIENT", matchIfMissing = true) 17 | class MatrixClientBotAutoconfiguration { 18 | 19 | @Bean 20 | fun clientMemberEventHandler(membershipChangeHandler: MembershipChangeHandler): ClientMemberEventHandler { 21 | return ClientMemberEventHandler(membershipChangeHandler) 22 | } 23 | 24 | @Bean 25 | fun matrixClientSyncRunner( 26 | matrixClient: MatrixClient, 27 | ): MatrixClientSyncRunner { 28 | return MatrixClientSyncRunner( 29 | matrixClient, 30 | ) 31 | } 32 | 33 | @Bean 34 | fun eventHandlerRunner( 35 | matrixClient: MatrixClient, 36 | eventHandler: List>, 37 | ): EventHandlerRunner { 38 | return EventHandlerRunner( 39 | matrixClient.sync, 40 | eventHandler 41 | ) 42 | } 43 | 44 | @Bean 45 | fun clientBotServiceHelper( 46 | botProperties: MatrixBotProperties 47 | ): BotServiceHelper { 48 | return BotServiceHelper(botProperties, setOf(), setOf()) 49 | } 50 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/util/BotServiceHelperTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.util 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.booleans.shouldBeFalse 5 | import io.kotest.matchers.booleans.shouldBeTrue 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 9 | import net.folivo.trixnity.core.model.MatrixId 10 | 11 | class BotServiceHelperTest : DescribeSpec(testBody()) 12 | 13 | private fun testBody(): DescribeSpec.() -> Unit { 14 | return { 15 | val botPropertiesMock: MatrixBotProperties = mockk { 16 | every { serverName } returns "server" 17 | every { username } returns "bot" 18 | } 19 | 20 | val cut = BotServiceHelper(botPropertiesMock, setOf(Regex("unicorn_.+")), setOf(Regex("dino_.+"))) 21 | 22 | describe(BotServiceHelper::isManagedUser.name) { 23 | it("it should return true when userId is bot") { 24 | cut.isManagedUser(MatrixId.UserId("@bot:server")).shouldBeTrue() 25 | } 26 | it("it should return true when userId is in namespace") { 27 | cut.isManagedUser(MatrixId.UserId("@unicorn_fluffy:server")).shouldBeTrue() 28 | } 29 | it("it should return false when userId is not in namespace") { 30 | cut.isManagedUser(MatrixId.UserId("@cat_fluffy:server")).shouldBeFalse() 31 | } 32 | } 33 | describe(BotServiceHelper::isManagedRoom.name) { 34 | it("it should return true when room alias is in namespace") { 35 | cut.isManagedRoom(MatrixId.RoomAliasId("#dino_large:server")).shouldBeTrue() 36 | } 37 | it("it should return false when room alias is not in namespace") { 38 | cut.isManagedRoom(MatrixId.RoomAliasId("#cat_buhu:server")).shouldBeFalse() 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/room/MatrixRoomService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.room 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import net.folivo.trixnity.core.model.MatrixId 5 | 6 | class MatrixRoomService( 7 | private val roomRepository: MatrixRoomRepository, 8 | private val roomAliasRepository: MatrixRoomAliasRepository 9 | ) { 10 | suspend fun getOrCreateRoom(roomId: MatrixId.RoomId): MatrixRoom { 11 | return roomRepository.findById(roomId) 12 | ?: roomRepository.save(MatrixRoom(roomId)) 13 | } 14 | 15 | suspend fun getOrCreateRoomAlias(roomAlias: MatrixId.RoomAliasId, roomId: MatrixId.RoomId): MatrixRoomAlias { 16 | roomRepository.findById(roomId) ?: roomRepository.save(MatrixRoom(roomId, true)) 17 | val existingRoomAlias = roomAliasRepository.findById(roomAlias) 18 | return if (existingRoomAlias != null) { 19 | if (existingRoomAlias.roomId == roomId) existingRoomAlias 20 | else roomAliasRepository.save(existingRoomAlias.copy(roomId = roomId)) 21 | } else { 22 | roomAliasRepository.save(MatrixRoomAlias(roomAlias, roomId)) 23 | } 24 | } 25 | 26 | suspend fun getRoomAliasByRoomId(roomId: MatrixId.RoomId): MatrixRoomAlias? { 27 | return roomAliasRepository.findByRoomId(roomId) 28 | } 29 | 30 | suspend fun getRoomAlias(roomAlias: MatrixId.RoomAliasId): MatrixRoomAlias? { 31 | return roomAliasRepository.findById(roomAlias) 32 | } 33 | 34 | suspend fun existsByRoomAlias(roomAlias: MatrixId.RoomAliasId): Boolean { 35 | return roomAliasRepository.existsById(roomAlias) 36 | } 37 | 38 | fun getRoomsByMembers(members: Set): Flow { 39 | return roomRepository.findByMembers(members) 40 | } 41 | 42 | suspend fun deleteRoom(roomId: MatrixId.RoomId) { 43 | roomRepository.deleteById(roomId) 44 | } 45 | 46 | suspend fun deleteAllRooms() { 47 | roomRepository.deleteAll() 48 | } 49 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/event/MatrixSyncBatchTokenRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.nulls.shouldBeNull 5 | import io.kotest.matchers.shouldBe 6 | import kotlinx.coroutines.reactive.awaitFirstOrNull 7 | import net.folivo.spring.matrix.bot.config.MatrixBotDatabaseAutoconfiguration 8 | import net.folivo.spring.matrix.bot.user.MatrixUser 9 | import net.folivo.trixnity.core.model.MatrixId 10 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration 11 | import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest 12 | import org.springframework.data.r2dbc.core.R2dbcEntityTemplate 13 | import org.springframework.data.r2dbc.core.delete 14 | 15 | @DataR2dbcTest 16 | @ImportAutoConfiguration(MatrixBotDatabaseAutoconfiguration::class) 17 | class MatrixSyncBatchTokenRepositoryTest( 18 | cut: MatrixSyncBatchTokenRepository, 19 | db: R2dbcEntityTemplate 20 | ) : DescribeSpec(testBody(cut, db)) 21 | 22 | private fun testBody(cut: MatrixSyncBatchTokenRepository, db: R2dbcEntityTemplate): DescribeSpec.() -> Unit { 23 | return { 24 | val userId = MatrixId.UserId("user", "server") 25 | beforeSpec { 26 | db.insert(MatrixUser(userId, true)).awaitFirstOrNull() 27 | db.insert(MatrixSyncBatchToken(userId, "someToken")).awaitFirstOrNull() 28 | } 29 | 30 | describe(MatrixSyncBatchTokenRepository::findByUserId.name) { 31 | it("should find matching token") { 32 | cut.findByUserId(userId).shouldBe(MatrixSyncBatchToken(userId, "someToken", 1)) 33 | } 34 | it("should not find matching token") { 35 | cut.findByUserId(MatrixId.UserId("unknown", "server")).shouldBeNull() 36 | } 37 | } 38 | 39 | afterSpec { 40 | db.delete().all().awaitFirstOrNull() 41 | db.delete().all().awaitFirstOrNull() 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/appservice/AppserviceMemberEventHandler.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice 2 | 3 | import io.ktor.http.* 4 | import net.folivo.spring.matrix.bot.event.MatrixEventHandler 5 | import net.folivo.spring.matrix.bot.membership.MembershipChangeHandler 6 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService 7 | import net.folivo.trixnity.client.rest.api.MatrixServerException 8 | import net.folivo.trixnity.core.model.MatrixId 9 | import net.folivo.trixnity.core.model.events.Event 10 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent 11 | import org.slf4j.LoggerFactory 12 | import kotlin.reflect.KClass 13 | 14 | class AppserviceMemberEventHandler( 15 | private val membershipChangeHandler: MembershipChangeHandler, 16 | private val appserviceUserService: AppserviceUserService, 17 | ) : MatrixEventHandler { 18 | 19 | companion object { 20 | private val LOG = LoggerFactory.getLogger(this::class.java) 21 | } 22 | 23 | override fun supports(): KClass { 24 | return MemberEventContent::class 25 | } 26 | 27 | override suspend fun handleEvent(event: Event) { 28 | require(event is Event.StateEvent) 29 | val userId = MatrixId.UserId(event.stateKey) 30 | try { 31 | membershipChangeHandler.handleMembership(userId, event.roomId, event.content.membership) 32 | } catch (error: MatrixServerException) { 33 | if (error.statusCode == HttpStatusCode.Forbidden) { 34 | LOG.warn("try to register user because of ${error.errorResponse}") 35 | try { 36 | appserviceUserService.registerManagedUser(userId) 37 | membershipChangeHandler.handleMembership(userId, event.roomId, event.content.membership) 38 | } catch (registerError: MatrixServerException) { 39 | if (registerError.statusCode == HttpStatusCode.Forbidden) { 40 | LOG.warn("could not register user due to: ${error.errorResponse}") 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/config/MatrixBotDatabaseAutoconfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.config 2 | 3 | import io.r2dbc.spi.ConnectionFactory 4 | import liquibase.integration.spring.SpringLiquibase 5 | import org.springframework.beans.factory.annotation.Qualifier 6 | import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBuilder 7 | import org.springframework.boot.autoconfigure.r2dbc.EmbeddedDatabaseConnection 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties 9 | import org.springframework.context.annotation.Bean 10 | import org.springframework.context.annotation.Configuration 11 | import org.springframework.core.io.ResourceLoader 12 | import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration 13 | import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories 14 | import javax.sql.DataSource 15 | 16 | @Configuration 17 | @EnableR2dbcRepositories(basePackages = ["net.folivo.spring.matrix.bot"]) 18 | //@EnableTransactionManagement TODO wait until supported 19 | @EnableConfigurationProperties(MatrixBotProperties::class) 20 | class MatrixBotDatabaseAutoconfiguration( 21 | private val botProperties: MatrixBotProperties, 22 | private val resourceLoader: ResourceLoader 23 | ) : AbstractR2dbcConfiguration() { 24 | 25 | 26 | @Bean("liquibaseDatasource") 27 | fun liquibaseDatasource(): DataSource { 28 | return botProperties.migration.initializeDataSourceBuilder().build(); 29 | } 30 | 31 | @Bean 32 | fun liquibase(@Qualifier("liquibaseDatasource") liquibaseDatasource: DataSource): SpringLiquibase { 33 | return SpringLiquibase().apply { 34 | changeLog = "classpath:db/changelog/net.folivo.matrix.bot.changelog-master.yml" 35 | dataSource = liquibaseDatasource 36 | } 37 | } 38 | 39 | @Bean 40 | override fun connectionFactory(): ConnectionFactory { 41 | return ConnectionFactoryBuilder.of(botProperties.database) { EmbeddedDatabaseConnection.get(resourceLoader.classLoader) } 42 | .build() 43 | } 44 | 45 | override fun getCustomConverters(): MutableList { 46 | return mutableListOf(MatrixIdReadingConverter(), MatrixIdWritingConverter()) 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/client/ClientMemberEventHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.client 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.mockk.clearMocks 6 | import io.mockk.coVerify 7 | import io.mockk.mockk 8 | import net.folivo.spring.matrix.bot.membership.MembershipChangeHandler 9 | import net.folivo.trixnity.core.model.MatrixId 10 | import net.folivo.trixnity.core.model.events.Event 11 | import net.folivo.trixnity.core.model.events.UnsignedData 12 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent 13 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent.Membership.INVITE 14 | 15 | class ClientMemberEventHandlerTest : DescribeSpec(testBody()) 16 | 17 | private fun testBody(): DescribeSpec.() -> Unit { 18 | return { 19 | val membershipChangeHandlerMock: MembershipChangeHandler = mockk(relaxed = true) 20 | 21 | val cut = ClientMemberEventHandler(membershipChangeHandlerMock) 22 | 23 | describe(ClientMemberEventHandler::supports.name) { 24 | it("should only support ${MemberEventContent::class.simpleName}") { 25 | cut.supports().shouldBe(MemberEventContent::class) 26 | } 27 | } 28 | 29 | describe(ClientMemberEventHandler::handleEvent.name) { 30 | val event = Event.StateEvent( 31 | MemberEventContent(membership = INVITE), 32 | MatrixId.EventId("event", "server"), 33 | MatrixId.UserId("sender", "server"), 34 | MatrixId.RoomId("room", "server"), 35 | 123, 36 | UnsignedData(), 37 | MatrixId.UserId("invited", "server").toString() 38 | ) 39 | it("should delegate to ${MembershipChangeHandler::class.simpleName}") { 40 | cut.handleEvent(event) 41 | coVerify { 42 | membershipChangeHandlerMock.handleMembership( 43 | MatrixId.UserId("invited", "server"), MatrixId.RoomId("room", "server"), 44 | INVITE 45 | ) 46 | } 47 | } 48 | } 49 | 50 | afterTest { clearMocks(membershipChangeHandlerMock) } 51 | } 52 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/appservice/event/DefaultAppserviceEventServiceTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice.event 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.mockk.clearMocks 6 | import io.mockk.coEvery 7 | import io.mockk.coVerify 8 | import io.mockk.mockk 9 | import net.folivo.trixnity.appservice.rest.event.AppserviceEventTnxService.EventTnxProcessingState.NOT_PROCESSED 10 | import net.folivo.trixnity.appservice.rest.event.AppserviceEventTnxService.EventTnxProcessingState.PROCESSED 11 | 12 | class DefaultAppserviceEventServiceTest : DescribeSpec(testBody()) 13 | 14 | private fun testBody(): DescribeSpec.() -> Unit { 15 | return { 16 | 17 | val eventTransactionServiceMock = mockk(relaxed = true) 18 | 19 | val cut = DefaultAppserviceEventTnxService( 20 | eventTransactionServiceMock, 21 | ) 22 | 23 | describe(DefaultAppserviceEventTnxService::eventTnxProcessingState.name) { 24 | describe("event already processed") { 25 | it("should return $PROCESSED") { 26 | coEvery { eventTransactionServiceMock.hasTransaction("someTnxId") } 27 | .returns(true) 28 | cut.eventTnxProcessingState("someTnxId") 29 | .shouldBe(PROCESSED) 30 | } 31 | } 32 | describe("event not processed") { 33 | it("should return $NOT_PROCESSED") { 34 | coEvery { eventTransactionServiceMock.hasTransaction("someTnxId") } 35 | .returns(false) 36 | cut.eventTnxProcessingState("someTnxId") 37 | .shouldBe(NOT_PROCESSED) 38 | } 39 | } 40 | } 41 | describe(DefaultAppserviceEventTnxService::onEventTnxProcessed.name) { 42 | it("should save event as processed") { 43 | cut.onEventTnxProcessed("someTnxId") 44 | 45 | coVerify { 46 | eventTransactionServiceMock.saveTransaction(MatrixEventTransaction("someTnxId")) 47 | } 48 | } 49 | } 50 | 51 | afterTest { clearMocks(eventTransactionServiceMock) } 52 | } 53 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/event/EventHandlerRunnerTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.mockk.coEvery 5 | import io.mockk.coVerify 6 | import io.mockk.mockk 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.launch 10 | import net.folivo.trixnity.core.EventEmitter 11 | import net.folivo.trixnity.core.model.MatrixId.* 12 | import net.folivo.trixnity.core.model.events.Event 13 | import net.folivo.trixnity.core.model.events.Event.RoomEvent 14 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent 15 | import net.folivo.trixnity.core.model.events.m.room.MessageEventContent.TextMessageEventContent 16 | 17 | class TestEventEmitter : EventEmitter() { 18 | suspend fun emitTestEvent(event: Event<*>) { 19 | emitEvent(event) 20 | } 21 | } 22 | 23 | class EventHandlerRunnerTest : DescribeSpec({ 24 | val eventEmitter: TestEventEmitter = TestEventEmitter() 25 | val eventHandler1: MatrixEventHandler = mockk(relaxed = true) 26 | val eventHandler2: MatrixEventHandler = mockk(relaxed = true) 27 | 28 | val cut = EventHandlerRunner(eventEmitter, listOf(eventHandler1, eventHandler2)) 29 | 30 | describe(EventHandlerRunner::startEventListening.name) { 31 | beforeTest { 32 | coEvery { eventHandler1.supports() } returns MemberEventContent::class 33 | coEvery { eventHandler2.supports() } returns TextMessageEventContent::class 34 | } 35 | 36 | it("should register event handler and process events") { 37 | val testEvent = RoomEvent( 38 | TextMessageEventContent("hoho"), 39 | EventId("event", "server"), 40 | UserId("user", "server"), 41 | RoomId("room", "server"), 42 | 1234L 43 | ) 44 | val job = GlobalScope.launch { 45 | cut.startEventListening() 46 | } 47 | delay(50) 48 | eventEmitter.emitTestEvent(testEvent) 49 | coVerify(exactly = 0) { eventHandler1.handleEvent(any()) } 50 | coVerify { eventHandler2.handleEvent(testEvent) } 51 | job.cancel() 52 | } 53 | } 54 | }) -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/user/MatrixUserServiceTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.user 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.mockk.clearMocks 6 | import io.mockk.coEvery 7 | import io.mockk.coVerify 8 | import io.mockk.mockk 9 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 10 | import net.folivo.trixnity.core.model.MatrixId 11 | 12 | class MatrixUserServiceTest : DescribeSpec(testBody()) 13 | 14 | private fun testBody(): DescribeSpec.() -> Unit { 15 | return { 16 | val userRepositoryMock: MatrixUserRepository = mockk(relaxed = true) 17 | val helperMock: BotServiceHelper = mockk() 18 | 19 | val cut = MatrixUserService(userRepositoryMock, helperMock) 20 | 21 | val userId1 = MatrixId.UserId("user1", "server") 22 | val userId2 = MatrixId.UserId("user2", "server") 23 | 24 | describe(MatrixUserService::getOrCreateUser.name) { 25 | 26 | it("should save new user when user does not exist") { 27 | val user1 = MatrixUser(userId1, true) 28 | val user2 = MatrixUser(userId2, false) 29 | 30 | coEvery { userRepositoryMock.findById(any()) }.returns(null) 31 | coEvery { userRepositoryMock.save(user1) }.returns(user1) 32 | coEvery { userRepositoryMock.save(user2) }.returns(user2) 33 | coEvery { helperMock.isManagedUser(userId1) }.returns(true) 34 | coEvery { helperMock.isManagedUser(userId2) }.returns(false) 35 | 36 | cut.getOrCreateUser(userId1).shouldBe(user1) 37 | cut.getOrCreateUser(userId2).shouldBe(user2) 38 | 39 | coVerify { 40 | userRepositoryMock.save(user1) 41 | userRepositoryMock.save(user2) 42 | } 43 | } 44 | it("should use existing room") { 45 | val user = MatrixUser(userId1) 46 | 47 | coEvery { userRepositoryMock.findById(any()) }.returns(user) 48 | cut.getOrCreateUser(userId1).shouldBe(user) 49 | coVerify(exactly = 0) { userRepositoryMock.save(any()) } 50 | } 51 | } 52 | 53 | afterTest { clearMocks(userRepositoryMock, helperMock) } 54 | } 55 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/membership/MatrixMembershipService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 5 | import net.folivo.spring.matrix.bot.user.MatrixUserService 6 | import net.folivo.trixnity.core.model.MatrixId 7 | import org.slf4j.LoggerFactory 8 | import org.springframework.stereotype.Service 9 | 10 | @Service 11 | class MatrixMembershipService( 12 | private val membershipRepository: MatrixMembershipRepository, 13 | private val userService: MatrixUserService, 14 | private val roomService: MatrixRoomService 15 | ) { 16 | 17 | companion object { 18 | private val LOG = LoggerFactory.getLogger(this::class.java) 19 | } 20 | 21 | suspend fun getMembership(id: String): MatrixMembership? { 22 | return membershipRepository.findById(id) 23 | } 24 | 25 | suspend fun getOrCreateMembership(userId: MatrixId.UserId, roomId: MatrixId.RoomId): MatrixMembership { 26 | roomService.getOrCreateRoom(roomId) 27 | userService.getOrCreateUser(userId) 28 | return membershipRepository.findByUserIdAndRoomId(userId, roomId) 29 | ?: membershipRepository.save(MatrixMembership(userId, roomId)) 30 | } 31 | 32 | suspend fun getMembershipsByRoomId(roomId: MatrixId.RoomId): Flow { 33 | return membershipRepository.findByRoomId(roomId) 34 | } 35 | 36 | suspend fun getMembershipsByUserId(userId: MatrixId.UserId): Flow { 37 | return membershipRepository.findByUserId(userId) 38 | } 39 | 40 | suspend fun getMembershipsSizeByUserId(userId: MatrixId.UserId): Long { 41 | return membershipRepository.countByUserId(userId) 42 | } 43 | 44 | suspend fun getMembershipsSizeByRoomId(roomId: MatrixId.RoomId): Long { 45 | return membershipRepository.countByRoomId(roomId) 46 | } 47 | 48 | suspend fun hasRoomOnlyManagedUsersLeft(roomId: MatrixId.RoomId): Boolean { 49 | return membershipRepository.containsOnlyManagedMembersByRoomId(roomId) 50 | } 51 | 52 | suspend fun deleteMembership(userId: MatrixId.UserId, roomId: MatrixId.RoomId) { 53 | membershipRepository.deleteByUserIdAndRoomId(userId, roomId) 54 | } 55 | 56 | suspend fun doesRoomContainsMembers(roomId: MatrixId.RoomId, members: Set): Boolean { 57 | return membershipRepository.containsMembersByRoomId(roomId, members) 58 | } 59 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/event/MessageEventHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.mockk.clearMocks 6 | import io.mockk.coVerifyAll 7 | import io.mockk.mockk 8 | import kotlinx.coroutines.runBlocking 9 | import net.folivo.trixnity.client.rest.MatrixClient 10 | import net.folivo.trixnity.core.model.MatrixId 11 | import net.folivo.trixnity.core.model.events.Event 12 | import net.folivo.trixnity.core.model.events.UnsignedData 13 | import net.folivo.trixnity.core.model.events.m.room.MessageEventContent 14 | 15 | class MessageEventHandlerTest : DescribeSpec(testBody()) 16 | 17 | private fun testBody(): DescribeSpec.() -> Unit { 18 | return { 19 | val messageHandlerMock1: MatrixMessageHandler = mockk(relaxed = true) 20 | val messageHandlerMock2: MatrixMessageHandler = mockk(relaxed = true) 21 | val matrixClientMock: MatrixClient = mockk(relaxed = true) 22 | 23 | val cut = MessageEventHandler(listOf(messageHandlerMock1, messageHandlerMock2), matrixClientMock) 24 | 25 | describe(MessageEventHandler::supports.name) { 26 | it("should only support message events") { 27 | cut.supports().shouldBe(MessageEventContent::class) 28 | } 29 | } 30 | 31 | describe(MessageEventHandler::handleEvent.name) { 32 | it("should delegate message events to each handler") { 33 | val content = MessageEventContent.TextMessageEventContent("test") 34 | runBlocking { 35 | cut.handleEvent( 36 | Event.RoomEvent( 37 | content = content, 38 | roomId = MatrixId.RoomId("room", "server"), 39 | id = MatrixId.EventId("event", "server"), 40 | sender = MatrixId.UserId("sender", "server"), 41 | originTimestamp = 1234, 42 | unsigned = UnsignedData() 43 | ) 44 | ) 45 | } 46 | coVerifyAll { 47 | messageHandlerMock1.handleMessage(content, any()) 48 | messageHandlerMock2.handleMessage(content, any()) 49 | } 50 | } 51 | } 52 | 53 | afterTest { clearMocks(messageHandlerMock1, messageHandlerMock2, matrixClientMock) } 54 | } 55 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/appservice/DefaultAppserviceRoomServiceTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.mockk.coEvery 6 | import io.mockk.coVerify 7 | import io.mockk.mockk 8 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 9 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 10 | import net.folivo.trixnity.appservice.rest.room.AppserviceRoomService.RoomExistingState.* 11 | import net.folivo.trixnity.appservice.rest.room.CreateRoomParameter 12 | import net.folivo.trixnity.client.rest.MatrixClient 13 | import net.folivo.trixnity.core.model.MatrixId 14 | 15 | class DefaultAppserviceRoomServiceTest : DescribeSpec(testBody()) 16 | 17 | private fun testBody(): DescribeSpec.() -> Unit { 18 | return { 19 | val roomServiceMock: MatrixRoomService = mockk(relaxed = true) 20 | val helperMock: BotServiceHelper = mockk(relaxed = true) 21 | val matrixClientMock: MatrixClient = mockk(relaxed = true) 22 | 23 | val cut = DefaultAppserviceRoomService(roomServiceMock, helperMock, matrixClientMock) 24 | 25 | val roomAlias = MatrixId.RoomAliasId("alias", "server") 26 | 27 | describe(DefaultAppserviceRoomService::roomExistingState.name) { 28 | it("should return $EXISTS when alias does exists") { 29 | coEvery { roomServiceMock.existsByRoomAlias(roomAlias) }.returns(true) 30 | cut.roomExistingState(roomAlias).shouldBe(EXISTS) 31 | } 32 | it("should return $CAN_BE_CREATED when alias does not exists but is managed") { 33 | coEvery { roomServiceMock.existsByRoomAlias(roomAlias) }.returns(false) 34 | coEvery { helperMock.isManagedRoom(roomAlias) }.returns(true) 35 | cut.roomExistingState(roomAlias).shouldBe(CAN_BE_CREATED) 36 | } 37 | it("should return $DOES_NOT_EXISTS when alias does not exists and is not managed") { 38 | coEvery { roomServiceMock.existsByRoomAlias(roomAlias) }.returns(false) 39 | coEvery { helperMock.isManagedRoom(roomAlias) }.returns(false) 40 | cut.roomExistingState(roomAlias).shouldBe(DOES_NOT_EXISTS) 41 | } 42 | } 43 | 44 | describe(DefaultAppserviceRoomService::getCreateRoomParameter.name) { 45 | it("should return empty ${CreateRoomParameter::class.simpleName}") { 46 | cut.getCreateRoomParameter(roomAlias).shouldBe(CreateRoomParameter()) 47 | } 48 | } 49 | 50 | describe(DefaultAppserviceRoomService::onCreatedRoom.name) { 51 | val room = MatrixId.RoomId("room", "server") 52 | it("should save room alias") { 53 | cut.onCreatedRoom(roomAlias, room) 54 | coVerify { roomServiceMock.getOrCreateRoomAlias(roomAlias, room) } 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /matrix-spring-boot-rest-appservice/src/test/kotlin/net/folivo/spring/matrix/appservice/TestConfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.appservice 2 | 3 | import net.folivo.trixnity.appservice.rest.event.AppserviceEventTnxService 4 | import net.folivo.trixnity.appservice.rest.event.AppserviceEventTnxService.EventTnxProcessingState.PROCESSED 5 | import net.folivo.trixnity.appservice.rest.room.AppserviceRoomService 6 | import net.folivo.trixnity.appservice.rest.room.CreateRoomParameter 7 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService 8 | import net.folivo.trixnity.appservice.rest.user.RegisterUserParameter 9 | import net.folivo.trixnity.client.rest.MatrixClient 10 | import net.folivo.trixnity.core.model.MatrixId 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.Configuration 13 | 14 | 15 | @Configuration 16 | class TestConfiguration { 17 | 18 | @Bean 19 | fun noOpAppserviceEventService(): AppserviceEventTnxService { 20 | return object : AppserviceEventTnxService { 21 | override suspend fun eventTnxProcessingState(tnxId: String): AppserviceEventTnxService.EventTnxProcessingState { 22 | return PROCESSED 23 | } 24 | 25 | override suspend fun onEventTnxProcessed(tnxId: String) { 26 | } 27 | 28 | } 29 | } 30 | 31 | @Bean 32 | fun noOpAppserviceRoomService(matrixClient: MatrixClient): AppserviceRoomService { 33 | return object : AppserviceRoomService { 34 | override val matrixClient: MatrixClient 35 | get() = matrixClient 36 | 37 | override suspend fun roomExistingState(roomAlias: MatrixId.RoomAliasId): AppserviceRoomService.RoomExistingState { 38 | return AppserviceRoomService.RoomExistingState.DOES_NOT_EXISTS 39 | } 40 | 41 | override suspend fun getCreateRoomParameter(roomAlias: MatrixId.RoomAliasId): CreateRoomParameter { 42 | return CreateRoomParameter() 43 | } 44 | 45 | override suspend fun onCreatedRoom(roomAlias: MatrixId.RoomAliasId, roomId: MatrixId.RoomId) { 46 | 47 | } 48 | } 49 | } 50 | 51 | @Bean 52 | fun noOpAppserviceUserService(matrixClient: MatrixClient): AppserviceUserService { 53 | return object : AppserviceUserService { 54 | override val matrixClient: MatrixClient 55 | get() = matrixClient 56 | 57 | override suspend fun userExistingState(userId: MatrixId.UserId): AppserviceUserService.UserExistingState { 58 | return AppserviceUserService.UserExistingState.DOES_NOT_EXISTS 59 | } 60 | 61 | override suspend fun getRegisterUserParameter(userId: MatrixId.UserId): RegisterUserParameter { 62 | return RegisterUserParameter() 63 | } 64 | 65 | override suspend fun onRegisteredUser(userId: MatrixId.UserId) { 66 | 67 | } 68 | } 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/room/MatrixRoomRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.room 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.collections.shouldBeEmpty 5 | import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder 6 | import kotlinx.coroutines.flow.toList 7 | import kotlinx.coroutines.reactive.awaitFirstOrNull 8 | import net.folivo.spring.matrix.bot.config.MatrixBotDatabaseAutoconfiguration 9 | import net.folivo.spring.matrix.bot.membership.MatrixMembership 10 | import net.folivo.spring.matrix.bot.user.MatrixUser 11 | import net.folivo.trixnity.core.model.MatrixId 12 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration 13 | import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest 14 | import org.springframework.data.r2dbc.core.R2dbcEntityTemplate 15 | import org.springframework.data.r2dbc.core.delete 16 | 17 | @DataR2dbcTest 18 | @ImportAutoConfiguration(MatrixBotDatabaseAutoconfiguration::class) 19 | class MatrixRoomRepositoryTest( 20 | cut: MatrixRoomRepository, 21 | db: R2dbcEntityTemplate 22 | ) : DescribeSpec(testBody(cut, db)) 23 | 24 | private fun testBody(cut: MatrixRoomRepository, db: R2dbcEntityTemplate): DescribeSpec.() -> Unit { 25 | return { 26 | val user1 = MatrixId.UserId("user1", "server") 27 | val user2 = MatrixId.UserId("user2", "server") 28 | val room1 = MatrixId.RoomId("room1", "server") 29 | val room2 = MatrixId.RoomId("room2", "server") 30 | val room3 = MatrixId.RoomId("room3", "server") 31 | 32 | 33 | beforeSpec { 34 | db.insert(MatrixUser(user1)).awaitFirstOrNull() 35 | db.insert(MatrixUser(user2)).awaitFirstOrNull() 36 | db.insert(MatrixRoom(room1)).awaitFirstOrNull() 37 | db.insert(MatrixRoom(room2)).awaitFirstOrNull() 38 | db.insert(MatrixRoom(room3)).awaitFirstOrNull() 39 | db.insert(MatrixMembership(user1, room1)).awaitFirstOrNull() 40 | db.insert(MatrixMembership(user2, room1)).awaitFirstOrNull() 41 | db.insert(MatrixMembership(user2, room2)).awaitFirstOrNull() 42 | db.insert(MatrixMembership(user1, room3)).awaitFirstOrNull() 43 | db.insert(MatrixMembership(user2, room3)).awaitFirstOrNull() 44 | } 45 | 46 | describe(MatrixRoomRepository::findByMembers.name) { 47 | it("should find matching room") { 48 | cut.findByMembers(setOf(user1, user2)).toList().map { it.id } 49 | .shouldContainExactlyInAnyOrder(room1, room3) 50 | cut.findByMembers(setOf(user2)).toList().map { it.id } 51 | .shouldContainExactlyInAnyOrder(room1, room2, room3) 52 | 53 | } 54 | it("should not find matching room") { 55 | cut.findByMembers(setOf(MatrixId.UserId("unknown", "server"))).toList().shouldBeEmpty() 56 | } 57 | } 58 | 59 | afterSpec { 60 | db.delete().all().awaitFirstOrNull() 61 | db.delete().all().awaitFirstOrNull() 62 | db.delete().all().awaitFirstOrNull() 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/user/MatrixUserRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.user 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.collections.shouldBeEmpty 5 | import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder 6 | import kotlinx.coroutines.flow.toList 7 | import kotlinx.coroutines.reactive.awaitFirstOrNull 8 | import net.folivo.spring.matrix.bot.config.MatrixBotDatabaseAutoconfiguration 9 | import net.folivo.spring.matrix.bot.membership.MatrixMembership 10 | import net.folivo.spring.matrix.bot.room.MatrixRoom 11 | import net.folivo.trixnity.core.model.MatrixId 12 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration 13 | import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest 14 | import org.springframework.data.r2dbc.core.R2dbcEntityTemplate 15 | import org.springframework.data.r2dbc.core.delete 16 | 17 | @DataR2dbcTest 18 | @ImportAutoConfiguration(MatrixBotDatabaseAutoconfiguration::class) 19 | class MatrixUserRepositoryTest( 20 | cut: MatrixUserRepository, 21 | db: R2dbcEntityTemplate 22 | ) : DescribeSpec(testBody(cut, db)) 23 | 24 | private fun testBody(cut: MatrixUserRepository, db: R2dbcEntityTemplate): DescribeSpec.() -> Unit { 25 | return { 26 | val userId1 = MatrixId.UserId("user1", "server") 27 | val userId2 = MatrixId.UserId("user2", "server") 28 | val userId3 = MatrixId.UserId("user3", "server") 29 | val roomId1 = MatrixId.RoomId("room1", "server") 30 | val roomId2 = MatrixId.RoomId("room2", "server") 31 | val roomId3 = MatrixId.RoomId("room3", "server") 32 | 33 | beforeSpec { 34 | db.insert(MatrixUser(userId1)).awaitFirstOrNull() 35 | db.insert(MatrixUser(userId2)).awaitFirstOrNull() 36 | db.insert(MatrixUser(userId3)).awaitFirstOrNull() 37 | db.insert(MatrixRoom(roomId1)).awaitFirstOrNull() 38 | db.insert(MatrixRoom(roomId2)).awaitFirstOrNull() 39 | db.insert(MatrixRoom(roomId3)).awaitFirstOrNull() 40 | db.insert(MatrixMembership(userId1, roomId1)).awaitFirstOrNull() 41 | db.insert(MatrixMembership(userId2, roomId1)).awaitFirstOrNull() 42 | db.insert(MatrixMembership(userId3, roomId1)).awaitFirstOrNull() 43 | db.insert(MatrixMembership(userId1, roomId2)).awaitFirstOrNull() 44 | db.insert(MatrixMembership(userId2, roomId2)).awaitFirstOrNull() 45 | } 46 | 47 | describe(MatrixUserRepository::findByRoomId.name) { 48 | it("should find users") { 49 | cut.findByRoomId(roomId1).toList().map { it.id } 50 | .shouldContainExactlyInAnyOrder(userId1, userId2, userId3) 51 | cut.findByRoomId(roomId2).toList().map { it.id } 52 | .shouldContainExactlyInAnyOrder(userId1, userId2) 53 | } 54 | it("should not find users") { 55 | cut.findByRoomId(roomId3).toList().map { it.id } 56 | .shouldBeEmpty() 57 | } 58 | } 59 | 60 | afterSpec { 61 | db.delete().all().awaitFirstOrNull() 62 | db.delete().all().awaitFirstOrNull() 63 | db.delete().all().awaitFirstOrNull() 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/room/MatrixRoomServiceTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.room 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.mockk.clearMocks 6 | import io.mockk.coEvery 7 | import io.mockk.coVerify 8 | import io.mockk.mockk 9 | import net.folivo.trixnity.core.model.MatrixId 10 | 11 | class MatrixRoomServiceTest : DescribeSpec(testBody()) 12 | 13 | private fun testBody(): DescribeSpec.() -> Unit { 14 | return { 15 | val roomRepositoryMock: MatrixRoomRepository = mockk(relaxed = true) 16 | val roomAliasRepositoryMock: MatrixRoomAliasRepository = mockk(relaxed = true) 17 | 18 | val roomId = MatrixId.RoomId("room", "server") 19 | val alias = MatrixId.RoomAliasId("room", "server") 20 | 21 | val cut = MatrixRoomService( 22 | roomRepositoryMock, 23 | roomAliasRepositoryMock 24 | ) 25 | 26 | describe(MatrixRoomService::getOrCreateRoom.name) { 27 | val room = MatrixRoom(roomId) 28 | 29 | it("should save new room when room does not exist") { 30 | coEvery { roomRepositoryMock.findById(any()) }.returns(null) 31 | coEvery { roomRepositoryMock.save(any()) }.returns(room) 32 | cut.getOrCreateRoom(roomId).shouldBe(room) 33 | coVerify { roomRepositoryMock.save(room) } 34 | } 35 | it("should use existing room") { 36 | coEvery { roomRepositoryMock.findById(any()) }.returns(room) 37 | cut.getOrCreateRoom(roomId).shouldBe(room) 38 | coVerify(exactly = 0) { roomRepositoryMock.save(any()) } 39 | } 40 | } 41 | 42 | describe(MatrixRoomService::getOrCreateRoomAlias.name) { 43 | val roomAlias = MatrixRoomAlias(alias, roomId) 44 | 45 | beforeTest { 46 | coEvery { roomRepositoryMock.findById(any()) }.returns(mockk()) 47 | } 48 | it("should save new room alias when it does not exist") { 49 | coEvery { roomAliasRepositoryMock.findById(any()) }.returns(null) 50 | coEvery { roomAliasRepositoryMock.save(any()) }.returns(roomAlias) 51 | cut.getOrCreateRoomAlias(alias, roomId).shouldBe(roomAlias) 52 | coVerify { roomAliasRepositoryMock.save(roomAlias) } 53 | } 54 | it("should use existing room alias") { 55 | coEvery { roomAliasRepositoryMock.findById(any()) }.returns(roomAlias) 56 | cut.getOrCreateRoomAlias(alias, roomId).shouldBe(roomAlias) 57 | coVerify(exactly = 0) { roomAliasRepositoryMock.save(roomAlias) } 58 | } 59 | it("should change existing room alias") { 60 | val oldRoomAlias = MatrixRoomAlias(alias, MatrixId.RoomId("old", "server")) 61 | coEvery { roomAliasRepositoryMock.findById(any()) }.returns(oldRoomAlias) 62 | coEvery { roomAliasRepositoryMock.save(any()) }.returns(roomAlias) 63 | cut.getOrCreateRoomAlias(alias, roomId).shouldBe(roomAlias) 64 | coVerify { roomAliasRepositoryMock.save(roomAlias) } 65 | } 66 | } 67 | 68 | afterTest { 69 | clearMocks(roomRepositoryMock, roomAliasRepositoryMock) 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /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 init 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 init 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 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/membership/DefaultMembershipChangeService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import kotlinx.coroutines.flow.collect 4 | import kotlinx.coroutines.flow.map 5 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 6 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 7 | import net.folivo.spring.matrix.bot.user.MatrixUserService 8 | import net.folivo.trixnity.client.rest.MatrixClient 9 | import net.folivo.trixnity.core.model.MatrixId 10 | import org.slf4j.LoggerFactory 11 | 12 | open class DefaultMembershipChangeService( 13 | private val roomService: MatrixRoomService, 14 | private val membershipService: MatrixMembershipService, 15 | private val userService: MatrixUserService, 16 | private val membershipSyncService: MatrixMembershipSyncService, 17 | private val matrixClient: MatrixClient, 18 | private val botProperties: MatrixBotProperties 19 | ) : MembershipChangeService { 20 | 21 | companion object { 22 | private val LOG = LoggerFactory.getLogger(this::class.java) 23 | } 24 | 25 | override suspend fun onRoomJoin(userId: MatrixId.UserId, roomId: MatrixId.RoomId) { 26 | LOG.debug("save join in $roomId of user $userId") 27 | membershipService.getOrCreateMembership(userId = userId, roomId = roomId) 28 | membershipSyncService.syncRoomMemberships(roomId) 29 | } 30 | 31 | override suspend fun onRoomLeave(userId: MatrixId.UserId, roomId: MatrixId.RoomId) { 32 | if (membershipService.doesRoomContainsMembers(roomId, setOf(userId))) { 33 | 34 | LOG.debug("save room leave in room $roomId of user $userId") 35 | membershipService.deleteMembership(userId, roomId) 36 | 37 | deleteUserWhenNotManaged(userId) 38 | 39 | val noMembersLeft = membershipService.getMembershipsSizeByRoomId(roomId) == 0L 40 | val onlyManagedUsersLeft = membershipService.hasRoomOnlyManagedUsersLeft(roomId) 41 | val isManaged = roomService.getOrCreateRoom(roomId).isManaged 42 | 43 | if (!isManaged) { 44 | if (onlyManagedUsersLeft) { 45 | LOG.debug("leave room $roomId with all managed users because there are only managed users left") 46 | val memberships = membershipService.getMembershipsByRoomId(roomId) 47 | memberships 48 | .map { it.userId } 49 | .collect { joinedUserId -> 50 | if (joinedUserId == botProperties.botUserId) 51 | matrixClient.room.leaveRoom(roomId) 52 | else matrixClient.room.leaveRoom(roomId, joinedUserId) 53 | membershipService.deleteMembership(joinedUserId, roomId) 54 | } 55 | } 56 | if (onlyManagedUsersLeft || noMembersLeft) { 57 | roomService.deleteRoom(roomId) 58 | } 59 | } 60 | } 61 | } 62 | 63 | private suspend fun deleteUserWhenNotManaged(userId: MatrixId.UserId) { 64 | if (!userService.getOrCreateUser(userId).isManaged && membershipService.getMembershipsSizeByUserId(userId) == 0L) { 65 | LOG.debug("delete user $userId because there are no memberships left") 66 | userService.deleteUser(userId) 67 | } 68 | } 69 | 70 | override suspend fun shouldJoinRoom(userId: MatrixId.UserId, roomId: MatrixId.RoomId): Boolean { 71 | return true 72 | } 73 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/appservice/DefaultAppserviceUserServiceTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.mockk.coEvery 6 | import io.mockk.coVerify 7 | import io.mockk.mockk 8 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 9 | import net.folivo.spring.matrix.bot.user.MatrixUserService 10 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 11 | import net.folivo.trixnity.appservice.rest.room.CreateRoomParameter 12 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService.UserExistingState.* 13 | import net.folivo.trixnity.appservice.rest.user.RegisterUserParameter 14 | import net.folivo.trixnity.client.rest.MatrixClient 15 | import net.folivo.trixnity.core.model.MatrixId 16 | 17 | class DefaultAppserviceUserServiceTest : DescribeSpec(testBody()) 18 | 19 | private fun testBody(): DescribeSpec.() -> Unit { 20 | return { 21 | val userServiceMock: MatrixUserService = mockk(relaxed = true) 22 | val helperMock: BotServiceHelper = mockk(relaxed = true) 23 | val botPropertiesMock: MatrixBotProperties = mockk(relaxed = true) 24 | val matrixClientMock: MatrixClient = mockk(relaxed = true) 25 | 26 | val cut = DefaultAppserviceUserService(userServiceMock, helperMock, botPropertiesMock, matrixClientMock) 27 | 28 | val userId = MatrixId.UserId("user", "server") 29 | val botUserId = MatrixId.UserId("bot", "server") 30 | 31 | describe(DefaultAppserviceUserService::userExistingState.name) { 32 | it("should return $EXISTS when user id does exists") { 33 | coEvery { userServiceMock.existsUser(userId) }.returns(true) 34 | cut.userExistingState(userId).shouldBe(EXISTS) 35 | } 36 | it("should return $CAN_BE_CREATED when user id does not exists but is managed") { 37 | coEvery { userServiceMock.existsUser(userId) }.returns(false) 38 | coEvery { helperMock.isManagedUser(userId) }.returns(true) 39 | cut.userExistingState(userId).shouldBe(CAN_BE_CREATED) 40 | } 41 | it("should return $DOES_NOT_EXISTS when user id does not exists and is not managed") { 42 | coEvery { userServiceMock.existsUser(userId) }.returns(false) 43 | coEvery { helperMock.isManagedUser(userId) }.returns(false) 44 | cut.userExistingState(userId).shouldBe(DOES_NOT_EXISTS) 45 | } 46 | } 47 | 48 | describe(DefaultAppserviceUserService::getRegisterUserParameter.name) { 49 | it("should return displaname when bot") { 50 | coEvery { botPropertiesMock.botUserId }.returns(botUserId) 51 | coEvery { botPropertiesMock.displayName }.returns("BOT") 52 | cut.getRegisterUserParameter(botUserId).shouldBe(RegisterUserParameter(displayName = "BOT")) 53 | } 54 | it("should return empty ${CreateRoomParameter::class.simpleName}") { 55 | coEvery { botPropertiesMock.botUserId }.returns(botUserId) 56 | cut.getRegisterUserParameter(userId).shouldBe(RegisterUserParameter()) 57 | } 58 | } 59 | 60 | describe(DefaultAppserviceUserService::onRegisteredUser.name) { 61 | it("should save room alias") { 62 | cut.onRegisteredUser(userId) 63 | coVerify { userServiceMock.getOrCreateUser(userId) } 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/appservice/AppserviceMemberEventHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.appservice 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.shouldBe 5 | import io.ktor.http.* 6 | import io.mockk.coEvery 7 | import io.mockk.coVerify 8 | import io.mockk.mockk 9 | import net.folivo.spring.matrix.bot.membership.MembershipChangeHandler 10 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService 11 | import net.folivo.trixnity.client.rest.api.ErrorResponse 12 | import net.folivo.trixnity.client.rest.api.MatrixServerException 13 | import net.folivo.trixnity.core.model.MatrixId 14 | import net.folivo.trixnity.core.model.events.Event 15 | import net.folivo.trixnity.core.model.events.UnsignedData 16 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent 17 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent.Membership.INVITE 18 | 19 | class AppserviceMemberEventHandlerTest : DescribeSpec(testBody()) 20 | 21 | private fun testBody(): DescribeSpec.() -> Unit { 22 | return { 23 | val membershipChangeHandlerMock: MembershipChangeHandler = mockk(relaxed = true) 24 | val appserviceUserServiceMock: AppserviceUserService = mockk(relaxed = true) 25 | val cut = AppserviceMemberEventHandler(membershipChangeHandlerMock, appserviceUserServiceMock) 26 | 27 | describe(AppserviceMemberEventHandler::supports.name) { 28 | it("should support ${MemberEventContent::class.simpleName}") { 29 | cut.supports().shouldBe(MemberEventContent::class) 30 | } 31 | } 32 | 33 | describe(AppserviceMemberEventHandler::handleEvent.name) { 34 | val event = Event.StateEvent( 35 | MemberEventContent(membership = INVITE), 36 | MatrixId.EventId("event", "server"), 37 | MatrixId.UserId("sender", "server"), 38 | MatrixId.RoomId("room", "server"), 39 | 123, 40 | UnsignedData(), 41 | MatrixId.UserId("invitedUser", "server").toString() 42 | ) 43 | it("should delegate to ${MembershipChangeHandler::class.simpleName}") { 44 | cut.handleEvent(event) 45 | coVerify { 46 | membershipChangeHandlerMock.handleMembership( 47 | MatrixId.UserId("invitedUser", "server"), 48 | MatrixId.RoomId("room", "server"), 49 | INVITE 50 | ) 51 | } 52 | } 53 | describe("delegate to ${MembershipChangeHandler::class.simpleName} fails with ${MatrixServerException::class.simpleName} and ${HttpStatusCode.Forbidden}") { 54 | coEvery { membershipChangeHandlerMock.handleMembership(any(), any(), any()) } 55 | .throws(MatrixServerException(HttpStatusCode.Forbidden, ErrorResponse("FORBIDDEN"))) 56 | it("should try to register user") { 57 | cut.handleEvent(event) 58 | coVerify { 59 | appserviceUserServiceMock.registerManagedUser(MatrixId.UserId("invitedUser", "server")) 60 | membershipChangeHandlerMock.handleMembership( 61 | MatrixId.UserId("invitedUser", "server"), 62 | MatrixId.RoomId("room", "server"), 63 | INVITE 64 | ) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/membership/MembershipChangeHandler.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 4 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.AutoJoinMode.DISABLED 5 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.TrackMembershipMode.ALL 6 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.TrackMembershipMode.MANAGED 7 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 8 | import net.folivo.trixnity.client.rest.MatrixClient 9 | import net.folivo.trixnity.core.model.MatrixId 10 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent 11 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent.Membership.* 12 | import org.slf4j.LoggerFactory 13 | 14 | class MembershipChangeHandler( 15 | private val matrixClient: MatrixClient, 16 | private val membershipChangeService: MembershipChangeService, 17 | private val botHelper: BotServiceHelper, 18 | private val botProperties: MatrixBotProperties 19 | ) { 20 | 21 | companion object { 22 | private val LOG = LoggerFactory.getLogger(this::class.java) 23 | } 24 | 25 | suspend fun handleMembership( 26 | userId: MatrixId.UserId, 27 | roomId: MatrixId.RoomId, 28 | membership: MemberEventContent.Membership 29 | ) { 30 | val isManagedUser = botHelper.isManagedUser(userId) 31 | val serverName = botProperties.serverName 32 | 33 | when (membership) { 34 | INVITE -> { 35 | val autoJoin = botProperties.autoJoin 36 | if (isManagedUser && autoJoin != DISABLED) { 37 | val asUserId = if (userId == botProperties.botUserId) null else userId 38 | if (autoJoin == MatrixBotProperties.AutoJoinMode.RESTRICTED && roomId.domain != serverName) { 39 | LOG.warn("reject room invite of $userId to $roomId because autoJoin is restricted to $serverName") 40 | matrixClient.room.leaveRoom(roomId = roomId, asUserId = asUserId) 41 | } else { 42 | if (membershipChangeService.shouldJoinRoom(userId, roomId)) { 43 | LOG.debug("join room $roomId with $userId") 44 | matrixClient.room.joinRoom(roomId = roomId, asUserId = asUserId) 45 | } else { 46 | LOG.debug("reject room invite of $userId to $roomId because autoJoin denied by service") 47 | matrixClient.room.leaveRoom(roomId = roomId, asUserId = asUserId) 48 | } 49 | } 50 | } else { 51 | LOG.debug("invited user $userId not managed or autoJoin disabled.") 52 | } 53 | } 54 | JOIN -> { 55 | val trackMembershipMode = botProperties.trackMembership 56 | if (trackMembershipMode == MANAGED && isManagedUser || trackMembershipMode == ALL) { 57 | LOG.debug("save room join of user $userId and room $roomId") 58 | membershipChangeService.onRoomJoin(userId, roomId) 59 | } 60 | } 61 | LEAVE, BAN -> { 62 | val trackMembershipMode = botProperties.trackMembership 63 | if (trackMembershipMode == MANAGED && isManagedUser || trackMembershipMode == ALL) { 64 | LOG.debug("save room leave of user $userId and room $roomId") 65 | membershipChangeService.onRoomLeave(userId, roomId) 66 | } 67 | } 68 | else -> { 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/membership/MatrixMembershipSyncService.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import kotlinx.coroutines.flow.collect 4 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 5 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.TrackMembershipMode.ALL 6 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.TrackMembershipMode.NONE 7 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 8 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 9 | import net.folivo.trixnity.client.rest.MatrixClient 10 | import net.folivo.trixnity.client.rest.api.MatrixServerException 11 | import net.folivo.trixnity.core.model.MatrixId 12 | import net.folivo.trixnity.core.model.events.m.room.CanonicalAliasEventContent 13 | import org.slf4j.LoggerFactory 14 | 15 | class MatrixMembershipSyncService( 16 | private val matrixClient: MatrixClient, 17 | private val roomService: MatrixRoomService, 18 | private val membershipService: MatrixMembershipService, 19 | private val helper: BotServiceHelper, 20 | private val botProperties: MatrixBotProperties 21 | ) { 22 | 23 | companion object { 24 | private val LOG = LoggerFactory.getLogger(this::class.java) 25 | } 26 | 27 | suspend fun syncBotRoomsAndMemberships() { 28 | try { 29 | matrixClient.room.getJoinedRooms() 30 | .collect { roomId -> 31 | LOG.info("sync room $roomId") 32 | try { 33 | val roomAlias = matrixClient.room.getStateEvent(roomId).alias 34 | if (roomAlias != null && helper.isManagedRoom(roomAlias)) { 35 | LOG.debug("set room alias of room $roomId to $roomAlias") 36 | roomService.getOrCreateRoomAlias(roomAlias, roomId) 37 | } 38 | } catch (error: MatrixServerException) { 39 | LOG.debug("room $roomId seems to have no alias and therefore is not managed") 40 | } 41 | matrixClient.room.getJoinedMembers(roomId).joined.keys.forEach { userId -> 42 | LOG.debug("save membership of $userId in $roomId") 43 | membershipService.getOrCreateMembership(userId, roomId) 44 | } 45 | } 46 | LOG.debug("synced bot user members") 47 | } catch (error: Throwable) { 48 | LOG.error("tried to sync bot user rooms, but that was not possible: ${error.message}") 49 | } 50 | } 51 | 52 | suspend fun syncRoomMemberships(roomId: MatrixId.RoomId) { 53 | // this is needed to get all members, e.g. when managed user joins a new room 54 | val trackMembershipMode = botProperties.trackMembership 55 | if (trackMembershipMode != NONE && membershipService.getMembershipsSizeByRoomId(roomId) == 1L) { // FIXME test 56 | LOG.debug("collect all members in room $roomId because we didn't saved it yet") 57 | try { 58 | matrixClient.room.getJoinedMembers(roomId).joined.keys 59 | .forEach { joinedUserId -> 60 | if (trackMembershipMode == ALL 61 | || trackMembershipMode == MatrixBotProperties.TrackMembershipMode.MANAGED && helper.isManagedUser( 62 | joinedUserId 63 | ) 64 | ) 65 | membershipService.getOrCreateMembership(joinedUserId, roomId) 66 | } 67 | } catch (error: Throwable) { 68 | LOG.error("tried to sync room $roomId, but that was not possible: ${error.message}") 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/event/PersistentSyncBatchTokenServiceTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.event 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.nulls.shouldBeNull 5 | import io.kotest.matchers.shouldBe 6 | import io.mockk.* 7 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 8 | import net.folivo.spring.matrix.bot.user.MatrixUserService 9 | import net.folivo.trixnity.core.model.MatrixId 10 | 11 | class PersistentSyncBatchTokenServiceTest : DescribeSpec(testBody()) 12 | 13 | private fun testBody(): DescribeSpec.() -> Unit { 14 | return { 15 | val syncBatchTokenRepositoryMock: MatrixSyncBatchTokenRepository = mockk(relaxed = true) 16 | 17 | val botUserId = MatrixId.UserId("bot", "server") 18 | val userId = MatrixId.UserId("user", "server") 19 | 20 | val botPropertiesMock: MatrixBotProperties = mockk() 21 | val userServiceMock: MatrixUserService = mockk(relaxed = true) 22 | 23 | val cut = PersistentSyncBatchTokenService(syncBatchTokenRepositoryMock, userServiceMock, botPropertiesMock) 24 | 25 | beforeTest { 26 | every { botPropertiesMock.botUserId }.returns(botUserId) 27 | } 28 | 29 | describe(PersistentSyncBatchTokenService::getBatchToken.name) { 30 | it("should get token from repository") { 31 | coEvery { syncBatchTokenRepositoryMock.findByUserId(userId) } 32 | .returns(MatrixSyncBatchToken(userId, "someToken")) 33 | cut.getBatchToken(userId).shouldBe("someToken") 34 | } 35 | it("should use bot user when no user given") { 36 | coEvery { syncBatchTokenRepositoryMock.findByUserId(botUserId) } 37 | .returns(MatrixSyncBatchToken(userId, "someToken")) 38 | cut.getBatchToken().shouldBe("someToken") 39 | } 40 | it("should return null when no token found") { 41 | coEvery { syncBatchTokenRepositoryMock.findByUserId(userId) } 42 | .returns(MatrixSyncBatchToken(userId, null)) 43 | cut.getBatchToken(userId).shouldBeNull() 44 | 45 | coEvery { syncBatchTokenRepositoryMock.findByUserId(userId) } 46 | .returns(null) 47 | cut.getBatchToken(userId).shouldBeNull() 48 | } 49 | } 50 | 51 | describe(PersistentSyncBatchTokenService::setBatchToken.name) { 52 | describe("token does not exists in database") { 53 | beforeTest { coEvery { syncBatchTokenRepositoryMock.findByUserId(any()) }.returns(null) } 54 | it("should save new token") { 55 | cut.setBatchToken("someToken", userId) 56 | coVerify { userServiceMock.getOrCreateUser(userId) } 57 | coVerify { syncBatchTokenRepositoryMock.save(MatrixSyncBatchToken(userId, "someToken")) } 58 | } 59 | it("should use bot user when no user given") { 60 | cut.setBatchToken("someToken") 61 | coVerify { userServiceMock.getOrCreateUser(botUserId) } 62 | coVerify { syncBatchTokenRepositoryMock.save(MatrixSyncBatchToken(botUserId, "someToken")) } 63 | } 64 | } 65 | describe("token exists in database") { 66 | it("should override token") { 67 | coEvery { syncBatchTokenRepositoryMock.findByUserId(any()) } 68 | .returns(MatrixSyncBatchToken(userId, "someToken", version = 3)) 69 | cut.setBatchToken("someNewToken", userId) 70 | coVerify { 71 | syncBatchTokenRepositoryMock 72 | .save(MatrixSyncBatchToken(userId, "someNewToken", version = 3)) 73 | } 74 | } 75 | it("should use bot user when no user given") { 76 | coEvery { syncBatchTokenRepositoryMock.findByUserId(any()) } 77 | .returns(MatrixSyncBatchToken(botUserId, "someToken", version = 3)) 78 | cut.setBatchToken("someNewToken") 79 | coVerify { 80 | syncBatchTokenRepositoryMock 81 | .save(MatrixSyncBatchToken(botUserId, "someNewToken", version = 3)) 82 | } 83 | } 84 | } 85 | } 86 | 87 | afterTest { clearMocks(syncBatchTokenRepositoryMock, userServiceMock) } 88 | } 89 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/config/MatrixBotAutoconfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.config 2 | 3 | import net.folivo.spring.matrix.bot.event.MatrixMessageHandler 4 | import net.folivo.spring.matrix.bot.event.MatrixSyncBatchTokenRepository 5 | import net.folivo.spring.matrix.bot.event.MessageEventHandler 6 | import net.folivo.spring.matrix.bot.event.PersistentSyncBatchTokenService 7 | import net.folivo.spring.matrix.bot.membership.* 8 | import net.folivo.spring.matrix.bot.room.MatrixRoomAliasRepository 9 | import net.folivo.spring.matrix.bot.room.MatrixRoomRepository 10 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 11 | import net.folivo.spring.matrix.bot.user.MatrixUserRepository 12 | import net.folivo.spring.matrix.bot.user.MatrixUserService 13 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 14 | import net.folivo.trixnity.client.rest.MatrixClient 15 | import net.folivo.trixnity.client.rest.api.sync.SyncBatchTokenService 16 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 17 | import org.springframework.boot.context.properties.EnableConfigurationProperties 18 | import org.springframework.context.annotation.Bean 19 | import org.springframework.context.annotation.Configuration 20 | 21 | 22 | @Configuration 23 | @EnableConfigurationProperties(MatrixBotProperties::class) 24 | class MatrixBotAutoconfiguration { 25 | 26 | @Bean 27 | fun messageEventHandler( 28 | matrixMessageHandler: List, 29 | matrixClient: MatrixClient 30 | ): MessageEventHandler { 31 | return MessageEventHandler(matrixMessageHandler, matrixClient) 32 | } 33 | 34 | @Bean 35 | @ConditionalOnMissingBean 36 | fun persistentSyncBatchTokenService( 37 | syncBatchTokenRepository: MatrixSyncBatchTokenRepository, 38 | userService: MatrixUserService, 39 | botProperties: MatrixBotProperties 40 | ): SyncBatchTokenService { 41 | return PersistentSyncBatchTokenService(syncBatchTokenRepository, userService, botProperties) 42 | } 43 | 44 | @Bean 45 | fun matrixUserService( 46 | userRepository: MatrixUserRepository, 47 | helper: BotServiceHelper 48 | ): MatrixUserService { 49 | return MatrixUserService(userRepository, helper) 50 | } 51 | 52 | @Bean 53 | fun matrixRoomService( 54 | roomRepository: MatrixRoomRepository, 55 | roomAliasRepository: MatrixRoomAliasRepository 56 | ): MatrixRoomService { 57 | return MatrixRoomService(roomRepository, roomAliasRepository) 58 | } 59 | 60 | @Bean 61 | fun matrixMembershipService( 62 | membershipRepository: MatrixMembershipRepository, 63 | userService: MatrixUserService, 64 | roomService: MatrixRoomService 65 | ): MatrixMembershipService { 66 | return MatrixMembershipService(membershipRepository, userService, roomService) 67 | } 68 | 69 | @Bean 70 | @ConditionalOnMissingBean 71 | fun defaultMembershipChangeService( 72 | roomService: MatrixRoomService, 73 | membershipService: MatrixMembershipService, 74 | userService: MatrixUserService, 75 | membershipSyncService: MatrixMembershipSyncService, 76 | matrixClient: MatrixClient, 77 | botProperties: MatrixBotProperties 78 | ): DefaultMembershipChangeService { 79 | return DefaultMembershipChangeService( 80 | roomService, 81 | membershipService, 82 | userService, 83 | membershipSyncService, 84 | matrixClient, 85 | botProperties 86 | ) 87 | } 88 | 89 | @Bean 90 | fun membershipChangeHandler( 91 | matrixClient: MatrixClient, 92 | membershipChangeService: MembershipChangeService, 93 | botHelper: BotServiceHelper, 94 | botProperties: MatrixBotProperties 95 | ): MembershipChangeHandler { 96 | return MembershipChangeHandler(matrixClient, membershipChangeService, botHelper, botProperties) 97 | } 98 | 99 | @Bean 100 | fun matrixMembershipSyncService( 101 | matrixClient: MatrixClient, 102 | roomService: MatrixRoomService, 103 | membershipService: MatrixMembershipService, 104 | helper: BotServiceHelper, 105 | botProperties: MatrixBotProperties 106 | ): MatrixMembershipSyncService { 107 | return MatrixMembershipSyncService(matrixClient, roomService, membershipService, helper, botProperties) 108 | } 109 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/resources/db/changelog/net.folivo.matrix.bot.changelog-0.4.0.RELEASE.yaml: -------------------------------------------------------------------------------- 1 | databaseChangeLog: 2 | - changeSet: 3 | id: net.folivo.matrix.bot.changelog-0.4.0.RELEASE 4 | author: benkuly 5 | changes: 6 | - createTable: 7 | tableName: matrix_event_transaction 8 | columns: 9 | - column: 10 | name: id 11 | type: VARCHAR(250) 12 | constraints: 13 | nullable: false 14 | primaryKey: true 15 | - column: 16 | name: tnx_id 17 | type: VARCHAR(250) 18 | constraints: 19 | nullable: false 20 | - column: 21 | name: event_id 22 | type: VARCHAR(250) 23 | constraints: 24 | nullable: false 25 | - column: 26 | name: version 27 | type: INTEGER 28 | constraints: 29 | nullable: false 30 | - createTable: 31 | tableName: matrix_room 32 | columns: 33 | - column: 34 | name: id 35 | type: VARCHAR(250) 36 | constraints: 37 | nullable: false 38 | primaryKey: true 39 | - column: 40 | name: is_managed 41 | type: BOOLEAN 42 | constraints: 43 | nullable: false 44 | - column: 45 | name: version 46 | type: INTEGER 47 | constraints: 48 | nullable: false 49 | - createTable: 50 | tableName: matrix_room_alias 51 | columns: 52 | - column: 53 | name: alias 54 | type: VARCHAR(250) 55 | constraints: 56 | nullable: false 57 | primaryKey: true 58 | - column: 59 | name: room_id 60 | type: VARCHAR(250) 61 | constraints: 62 | nullable: false 63 | - column: 64 | name: version 65 | type: INTEGER 66 | constraints: 67 | nullable: false 68 | - addForeignKeyConstraint: 69 | baseTableName: matrix_room_alias 70 | baseColumnNames: room_id 71 | constraintName: fk_matrix_room_alias_matrix_room 72 | onDelete: CASCADE 73 | referencedTableName: matrix_room 74 | referencedColumnNames: id 75 | - createTable: 76 | tableName: matrix_user 77 | columns: 78 | - column: 79 | name: id 80 | type: VARCHAR(250) 81 | constraints: 82 | nullable: false 83 | primaryKey: true 84 | - column: 85 | name: is_managed 86 | type: BOOLEAN 87 | constraints: 88 | nullable: false 89 | - column: 90 | name: version 91 | type: INTEGER 92 | constraints: 93 | nullable: false 94 | - createTable: 95 | tableName: matrix_membership 96 | columns: 97 | - column: 98 | name: id 99 | type: VARCHAR(250) 100 | constraints: 101 | nullable: false 102 | primaryKey: true 103 | - column: 104 | name: user_id 105 | type: VARCHAR(250) 106 | constraints: 107 | nullable: false 108 | - column: 109 | name: room_id 110 | type: VARCHAR(250) 111 | constraints: 112 | nullable: false 113 | - column: 114 | name: version 115 | type: INTEGER 116 | constraints: 117 | nullable: false 118 | - addForeignKeyConstraint: 119 | baseTableName: matrix_membership 120 | baseColumnNames: user_id 121 | constraintName: fk_matrix_membership_matrix_user 122 | onDelete: CASCADE 123 | referencedTableName: matrix_user 124 | referencedColumnNames: id 125 | - addForeignKeyConstraint: 126 | baseTableName: matrix_membership 127 | baseColumnNames: room_id 128 | constraintName: fk_matrix_membership_matrix_room 129 | onDelete: CASCADE 130 | referencedTableName: matrix_room 131 | referencedColumnNames: id 132 | - createTable: 133 | tableName: matrix_sync_batch_token 134 | columns: 135 | - column: 136 | name: user_id 137 | type: VARCHAR(250) 138 | constraints: 139 | nullable: false 140 | primaryKey: true 141 | - column: 142 | name: token 143 | type: VARCHAR(250) 144 | constraints: 145 | nullable: true 146 | - column: 147 | name: version 148 | type: INTEGER 149 | constraints: 150 | nullable: false 151 | - addForeignKeyConstraint: 152 | baseTableName: matrix_sync_batch_token 153 | baseColumnNames: user_id 154 | constraintName: fk_matrix_sync_batch_token_matrix_user 155 | onDelete: CASCADE 156 | referencedTableName: matrix_user 157 | referencedColumnNames: id -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/main/kotlin/net/folivo/spring/matrix/bot/config/MatrixAppserviceBotAutoconfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.config 2 | 3 | import net.folivo.spring.matrix.appservice.MatrixAppserviceAutoconfiguration 4 | import net.folivo.spring.matrix.appservice.MatrixAppserviceConfigurationProperties 5 | import net.folivo.spring.matrix.bot.appservice.AppserviceMemberEventHandler 6 | import net.folivo.spring.matrix.bot.appservice.BotUserInitializer 7 | import net.folivo.spring.matrix.bot.appservice.DefaultAppserviceRoomService 8 | import net.folivo.spring.matrix.bot.appservice.DefaultAppserviceUserService 9 | import net.folivo.spring.matrix.bot.appservice.event.DefaultAppserviceEventTnxService 10 | import net.folivo.spring.matrix.bot.appservice.event.MatrixEventTransactionRepository 11 | import net.folivo.spring.matrix.bot.appservice.event.MatrixEventTransactionService 12 | import net.folivo.spring.matrix.bot.appservice.sync.InitialSyncService 13 | import net.folivo.spring.matrix.bot.event.EventHandlerRunner 14 | import net.folivo.spring.matrix.bot.event.MatrixEventHandler 15 | import net.folivo.spring.matrix.bot.membership.MatrixMembershipSyncService 16 | import net.folivo.spring.matrix.bot.membership.MembershipChangeHandler 17 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 18 | import net.folivo.spring.matrix.bot.user.MatrixUserService 19 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 20 | import net.folivo.trixnity.appservice.rest.DefaultAppserviceService 21 | import net.folivo.trixnity.appservice.rest.event.AppserviceEventTnxService 22 | import net.folivo.trixnity.appservice.rest.room.AppserviceRoomService 23 | import net.folivo.trixnity.appservice.rest.user.AppserviceUserService 24 | import net.folivo.trixnity.client.rest.MatrixClient 25 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean 26 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 27 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 28 | import org.springframework.context.annotation.Bean 29 | import org.springframework.context.annotation.Configuration 30 | import org.springframework.context.annotation.Import 31 | import org.springframework.context.annotation.Profile 32 | 33 | 34 | @Configuration 35 | @ConditionalOnProperty(prefix = "matrix.bot", name = ["mode"], havingValue = "APPSERVICE") 36 | class MatrixAppserviceBotAutoconfiguration { 37 | 38 | @Configuration 39 | @Profile("initialsync") 40 | inner class InitialSyncConfiguration { 41 | @Bean 42 | fun initialSyncService( 43 | userService: MatrixUserService, 44 | roomService: MatrixRoomService, 45 | membershipSyncService: MatrixMembershipSyncService 46 | ): InitialSyncService { 47 | return InitialSyncService(userService, roomService, membershipSyncService) 48 | } 49 | } 50 | 51 | @Configuration 52 | @Profile("!initialsync") 53 | @Import(MatrixAppserviceAutoconfiguration::class) 54 | inner class NotInitialSyncConfiguration { 55 | 56 | @ConditionalOnBean(DefaultAppserviceService::class) 57 | @Bean 58 | fun eventHandlerRunner( 59 | appserviceService: DefaultAppserviceService, 60 | eventHandler: List>, 61 | ): EventHandlerRunner { 62 | return EventHandlerRunner( 63 | appserviceService, 64 | eventHandler 65 | ) 66 | } 67 | 68 | @Bean 69 | fun botUserInitializer( 70 | appserviceUserService: AppserviceUserService, 71 | botProperties: MatrixBotProperties 72 | ): BotUserInitializer { 73 | return BotUserInitializer(appserviceUserService, botProperties) 74 | } 75 | 76 | @Bean 77 | fun matrixEventTransactionService(eventTransactionRepository: MatrixEventTransactionRepository): MatrixEventTransactionService { 78 | return MatrixEventTransactionService(eventTransactionRepository) 79 | } 80 | 81 | @Bean 82 | @ConditionalOnMissingBean 83 | fun defaultAppserviceEventService( 84 | eventHandler: List>, 85 | eventTransactionService: MatrixEventTransactionService 86 | ): AppserviceEventTnxService { 87 | return DefaultAppserviceEventTnxService(eventTransactionService) 88 | } 89 | 90 | @Bean 91 | @ConditionalOnMissingBean 92 | fun defaultAppserviceRoomService( 93 | roomService: MatrixRoomService, 94 | helper: BotServiceHelper, 95 | matrixClient: MatrixClient 96 | ): AppserviceRoomService { 97 | return DefaultAppserviceRoomService(roomService, helper, matrixClient) 98 | } 99 | 100 | @Bean 101 | @ConditionalOnMissingBean 102 | fun defaultAppserviceUserService( 103 | matrixUserService: MatrixUserService, 104 | helper: BotServiceHelper, 105 | botProperties: MatrixBotProperties, 106 | matrixClient: MatrixClient 107 | ): AppserviceUserService { 108 | return DefaultAppserviceUserService(matrixUserService, helper, botProperties, matrixClient) 109 | } 110 | 111 | @Bean 112 | fun appserviceMemberEventHandler( 113 | membershipChangeHandler: MembershipChangeHandler, 114 | appserviceUserService: AppserviceUserService 115 | ): AppserviceMemberEventHandler { 116 | return AppserviceMemberEventHandler(membershipChangeHandler, appserviceUserService) 117 | } 118 | } 119 | 120 | 121 | @Bean 122 | fun appserviceBotServiceHelper( 123 | botProperties: MatrixBotProperties, 124 | appserviceProperties: MatrixAppserviceConfigurationProperties 125 | ): BotServiceHelper { 126 | return BotServiceHelper( 127 | botProperties, 128 | appserviceProperties.namespaces.users.map { Regex(it.localpartRegex) }.toSet(), 129 | appserviceProperties.namespaces.rooms.map { Regex(it.localpartRegex) }.toSet() 130 | ) 131 | } 132 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Version](https://maven-badges.herokuapp.com/maven-central/net.folivo/matrix-spring-boot-bot/badge.svg) 2 | 3 | # matrix-spring-boot-sdk 4 | 5 | This project contains tools to use [Matrix](https://matrix.org/) with Spring Boot and is based 6 | on [Trixnity](https://gitlab.com/benkuly/trixnity). 7 | 8 | * [matrix-spring-boot-rest-client](./matrix-spring-boot-rest-client) creates a Trixnity MatrixClient for you. 9 | * [matrix-spring-boot-rest-appservice](./matrix-spring-boot-rest-appservice) creates a Trixnity Appservice for you. 10 | * [matrix-spring-boot-bot](./matrix-spring-boot-bot) to create bots and appservices easily. 11 | * [examples](./examples) contains examples how to create bots. 12 | 13 | The most developers only need to use [matrix-spring-boot-bot](./matrix-spring-boot-bot), which contains the other 14 | projects. Therefore, this documentation focuses on this sub-project. 15 | 16 | You need help? Ask your questions 17 | in [#matrix-spring-boot-sdk:imbitbu.de](https://matrix.to/#/#matrix-spring-boot-sdk:imbitbu.de). 18 | 19 | ## How to use 20 | 21 | Just add the maven/gradle dependency `net.folivo:matrix-spring-boot-bot` to you project. 22 | 23 | Then decide which database you want to use. E. g. for embeddable [H2](h2database.com) include `io.r2dbc:r2dbc-h2` 24 | and `com.h2database:h2`. 25 | 26 | ### Properties 27 | 28 | Add the following properties to your `application.yml` or `application.properties` file: 29 | 30 | ```yaml 31 | matrix: 32 | bot: 33 | # The domain-part of matrix-ids. E. g. example.org when your userIds look like @unicorn:example.org 34 | serverName: example.org 35 | # The localpart (username) of the user associated with the application service 36 | # or just the username of your bot. 37 | username: superAppservice 38 | # (optional) Display name for the bot user. 39 | displayname: SUPER BOT 40 | # (optional) The mode you want to use to create a bot. Default is CLIENT. The other is APPSERVICE. 41 | mode: CLIENT 42 | # (optional) Configure how users managed by your bot do automatically join rooms. 43 | # ENABLED allows automatic joins to every invited room. 44 | # DISABLED disables this feature. 45 | # Default is RESTRICTED, which means, that only automatic joins to serverName are allowed. 46 | autoJoin: RESTRICTED 47 | # (optional) Configure if ALL membership changes should be tracked/saved with help of MatrixAppserviceRoomService 48 | # or only membership changes of users, which are MANAGED by the bridge. Default is ALL (no tracking/saving). 49 | trackMembership: MANAGED 50 | # Connection settings to the database (only r2dbc drivers are supported) 51 | database: 52 | url: r2dbc:h2:file:///./testdb/testdb 53 | username: sa 54 | password: 55 | # Connection setting to the database for migration purpose only (only jdbc drivers ar supported) 56 | migration: 57 | url: jdbc:h2:file:./testdb/testdb 58 | username: sa 59 | password: 60 | client: 61 | homeServer: 62 | # The hostname of your Homeserver. 63 | hostname: matrix.example.org 64 | # (optional) The port of your Homeserver. Default is 443. 65 | port: 443 66 | # (optional) Use http or https. Default is true (so uses https). 67 | secure: true 68 | # The token to authenticate against the Homeserver. 69 | token: superSecretMatrixToken 70 | # Properties under appservice are only relevant if you use bot mode APPSERVICE. 71 | appservice: 72 | # A unique token for Homeservers to use to authenticate requests to application services. 73 | hsToken: superSecretHomeserverToken 74 | # A list of users, aliases and rooms namespaces that the application service controls. 75 | namespaces: 76 | users: 77 | # A regular expression defining which values this namespace includes. 78 | # Note that this is not similar to the matrix homeserver appservice config, 79 | # because this regex only regards the localpart and not the complete matrix id. 80 | - localpartRegex: "_superAppservice_.*" 81 | aliases: 82 | - localpartRegex: "_superAppservice_.*" 83 | rooms: [ ] 84 | ``` 85 | 86 | See also the [matrix application service spec](https://matrix.org/docs/spec/application_service/r0.1.2#registration) 87 | for more information about the properties defined under `appservice`. 88 | 89 | ### Persistence 90 | 91 | The bot uses JDBC for migrations within the database (via liquibase, which doesn't support R2DBC yet) and R2DBC for 92 | unblocking database operations. Therefore, you need to integrate both JDBC and R2DBC into you project. 93 | 94 | ### Bot modes 95 | 96 | You can run your bot in two modes. 97 | 98 | #### Client mode 99 | 100 | The `CLIENT` mode simply acts as a matrix client. This allows you to create bots without any additional configuration on 101 | the Homerserver. It does does sync endless to the Homeserver as soon as you start your application. 102 | 103 | #### Appservice mode 104 | 105 | The `APPSERVICE` mode acts as a matrix appservice, which allows more customized bots. To customize the default behaviour 106 | of the `APPSERVICE` mode you may override `DefaultAppserviceEventService`, `DefaultAppserviceRoomService` 107 | and/or `DefaultAppserviceUserService` and make them available as bean (annotate it with `@Component`). This allows you 108 | to control which and how users and rooms should be created and events are handled. 109 | 110 | ### Handle events 111 | 112 | #### MessageEvents 113 | 114 | Just implement `MatrixMessageHandler` and make it available as bean (annotate it with `@Component`). This allows you to 115 | react and answer to all Message Events from any room, that you joined. 116 | 117 | ```kotlin 118 | @Component 119 | class PingHandler : MatrixMessageHandler { 120 | override suspend fun handleMessage(content: MessageEventContent, context: MessageContext) { 121 | if (content is TextMessageEventContent && content.body.contains("ping")) { 122 | context.answer("pong") 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | #### Handle all incoming events 129 | 130 | Implement `MatrixEventHandler` and make it available as bean (annotate it with `@Component`). This allows you to react 131 | to every Event from any room, that you joined. 132 | 133 | ```kotlin 134 | class MemberEventHandler : MatrixEventHandler { 135 | override suspend fun supports(): KClass { 136 | return MemberEventContent::class 137 | } 138 | override suspend fun handleEvent(event: Event) { 139 | if (event is Event.StateEvent) { 140 | println("${event.stateKey} did ${event.content.membership} in room ${event.roomId}") 141 | } 142 | } 143 | } 144 | ``` 145 | 146 | ### Interact with Homeserver 147 | 148 | A Bean of type `MatrixClient` from Trixnity is created, which can be autowired and used to interact with the Matrix 149 | Client-Server-API. 150 | 151 | ## Examples 152 | 153 | The module [examples](./examples) contains some examples how to use this framework in practice. 154 | 155 | Copy the `application.yml.example` file and save it at the same place as `application.yml`. You eventually need to 156 | modify the properties in that file. 157 | -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/membership/MatrixMembershipRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.booleans.shouldBeFalse 5 | import io.kotest.matchers.booleans.shouldBeTrue 6 | import io.kotest.matchers.collections.shouldBeEmpty 7 | import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder 8 | import io.kotest.matchers.nulls.shouldBeNull 9 | import io.kotest.matchers.shouldBe 10 | import kotlinx.coroutines.flow.toList 11 | import kotlinx.coroutines.reactive.awaitFirstOrNull 12 | import net.folivo.spring.matrix.bot.config.MatrixBotDatabaseAutoconfiguration 13 | import net.folivo.spring.matrix.bot.room.MatrixRoom 14 | import net.folivo.spring.matrix.bot.user.MatrixUser 15 | import net.folivo.trixnity.core.model.MatrixId 16 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration 17 | import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest 18 | import org.springframework.data.r2dbc.core.R2dbcEntityTemplate 19 | import org.springframework.data.r2dbc.core.delete 20 | 21 | @DataR2dbcTest 22 | @ImportAutoConfiguration(MatrixBotDatabaseAutoconfiguration::class) 23 | class MatrixMembershipRepositoryTest( 24 | cut: MatrixMembershipRepository, 25 | db: R2dbcEntityTemplate 26 | ) : DescribeSpec(testBody(cut, db)) 27 | 28 | private fun testBody(cut: MatrixMembershipRepository, db: R2dbcEntityTemplate): DescribeSpec.() -> Unit { 29 | return { 30 | val userId1 = MatrixId.UserId("user1", "server") 31 | val userId2 = MatrixId.UserId("user2", "server") 32 | val roomId1 = MatrixId.RoomId("room1", "server") 33 | val roomId2 = MatrixId.RoomId("room2", "server") 34 | 35 | beforeSpec { 36 | db.insert(MatrixUser(userId1)).awaitFirstOrNull() 37 | db.insert(MatrixUser(userId2)).awaitFirstOrNull() 38 | db.insert(MatrixRoom(roomId1)).awaitFirstOrNull() 39 | db.insert(MatrixRoom(roomId2)).awaitFirstOrNull() 40 | db.insert(MatrixMembership(userId1, roomId1)).awaitFirstOrNull() 41 | db.insert(MatrixMembership(userId2, roomId1)).awaitFirstOrNull() 42 | db.insert(MatrixMembership(userId2, roomId2)).awaitFirstOrNull() 43 | } 44 | 45 | describe(MatrixMembershipRepository::findByRoomId.name) { 46 | it("should return multiple memberships") { 47 | cut.findByRoomId(roomId1).toList().map { it.userId to it.roomId } 48 | .shouldContainExactlyInAnyOrder( 49 | userId1 to roomId1, 50 | userId2 to roomId1 51 | ) 52 | } 53 | it("should return no memberships") { 54 | cut.findByRoomId(MatrixId.RoomId("unknown", "blub")).toList().shouldBeEmpty() 55 | } 56 | } 57 | describe(MatrixMembershipRepository::findByUserId.name) { 58 | it("should return multiple memberships") { 59 | cut.findByUserId(userId2).toList().map { it.userId to it.roomId } 60 | .shouldContainExactlyInAnyOrder( 61 | userId2 to roomId1, 62 | userId2 to roomId2 63 | ) 64 | } 65 | it("should return no memberships") { 66 | cut.findByUserId(MatrixId.UserId("unknown", "blub")).toList().shouldBeEmpty() 67 | } 68 | } 69 | describe(MatrixMembershipRepository::countByRoomId.name) { 70 | it("should count multiple memberships") { 71 | cut.countByRoomId(roomId1).shouldBe(2L) 72 | } 73 | it("should count no memberships") { 74 | cut.countByRoomId(MatrixId.RoomId("unknown", "blub")).shouldBe(0L) 75 | } 76 | } 77 | describe(MatrixMembershipRepository::countByUserId.name) { 78 | it("should count multiple memberships") { 79 | cut.countByUserId(userId2).shouldBe(2L) 80 | } 81 | it("should count no memberships") { 82 | cut.countByUserId(MatrixId.UserId("unknown", "blub")).shouldBe(0L) 83 | } 84 | } 85 | describe(MatrixMembershipRepository::findByUserIdAndRoomId.name) { 86 | it("should return one membership") { 87 | cut.findByUserIdAndRoomId(userId1, roomId1) 88 | .let { it?.userId to it?.roomId } 89 | .shouldBe(userId1 to roomId1) 90 | } 91 | it("should return no membership") { 92 | cut.findByUserIdAndRoomId(MatrixId.UserId("unknown", "blub"), roomId1).shouldBeNull() 93 | } 94 | } 95 | describe(MatrixMembershipRepository::deleteByUserIdAndRoomId.name) { 96 | it("should delete membership") { 97 | db.insert(MatrixMembership(userId1, roomId2)).awaitFirstOrNull() 98 | cut.deleteByUserIdAndRoomId(userId1, roomId2) 99 | cut.findByUserIdAndRoomId(userId1, roomId2).shouldBeNull() 100 | } 101 | it("should do nothing") { 102 | cut.deleteByUserIdAndRoomId(MatrixId.UserId("unknown", "blub"), roomId1) 103 | } 104 | } 105 | describe(MatrixMembershipRepository::containsMembersByRoomId.name) { 106 | it("should contain members in room") { 107 | cut.containsMembersByRoomId(roomId1, setOf(userId1, userId2)).shouldBeTrue() 108 | cut.containsMembersByRoomId(roomId1, setOf(userId2)).shouldBeTrue() 109 | } 110 | it("should not contain members in room") { 111 | cut.containsMembersByRoomId(roomId1, setOf(MatrixId.UserId("unknown", "blub"))).shouldBeFalse() 112 | cut.containsMembersByRoomId(MatrixId.RoomId("unknown", "blub"), setOf(userId1)).shouldBeFalse() 113 | cut.containsMembersByRoomId(roomId1, setOf(userId1, MatrixId.UserId("unknown", "blub"))).shouldBeFalse() 114 | } 115 | } 116 | describe(MatrixMembershipRepository::containsOnlyManagedMembersByRoomId.name) { 117 | it("should contain only managed members") { 118 | val managedUser1 = MatrixId.UserId("managed1", "server") 119 | val managedUser2 = MatrixId.UserId("managed2", "server") 120 | val managedRoom = MatrixId.RoomId("managed", "server") 121 | db.insert(MatrixUser(managedUser1, true)).awaitFirstOrNull() 122 | db.insert(MatrixUser(managedUser2, true)).awaitFirstOrNull() 123 | db.insert(MatrixRoom(managedRoom)).awaitFirstOrNull() 124 | db.insert(MatrixMembership(managedUser1, managedRoom)).awaitFirstOrNull() 125 | db.insert(MatrixMembership(managedUser2, managedRoom)).awaitFirstOrNull() 126 | 127 | cut.containsOnlyManagedMembersByRoomId(managedRoom).shouldBeTrue() 128 | } 129 | it("should not contain only managed members") { 130 | cut.containsOnlyManagedMembersByRoomId(roomId1).shouldBeFalse() 131 | } 132 | } 133 | 134 | afterSpec { 135 | db.delete().all().awaitFirstOrNull() 136 | db.delete().all().awaitFirstOrNull() 137 | db.delete().all().awaitFirstOrNull() 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/membership/MatrixMembershipSyncServiceTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.mockk.clearMocks 5 | import io.mockk.coEvery 6 | import io.mockk.coVerify 7 | import io.mockk.mockk 8 | import kotlinx.coroutines.flow.flowOf 9 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 10 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.TrackMembershipMode.ALL 11 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.TrackMembershipMode.NONE 12 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 13 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 14 | import net.folivo.trixnity.client.rest.MatrixClient 15 | import net.folivo.trixnity.client.rest.api.room.GetJoinedMembersResponse 16 | import net.folivo.trixnity.core.model.MatrixId 17 | import net.folivo.trixnity.core.model.events.m.room.CanonicalAliasEventContent 18 | 19 | class MatrixSyncServiceTest : DescribeSpec(testBody()) 20 | 21 | private fun testBody(): DescribeSpec.() -> Unit { 22 | return { 23 | val matrixClientMock: MatrixClient = mockk() 24 | val roomServiceMock: MatrixRoomService = mockk() 25 | val membershipServiceMock: MatrixMembershipService = mockk() 26 | val helperMock: BotServiceHelper = mockk() 27 | val botPropertiesMock: MatrixBotProperties = mockk() 28 | 29 | val roomId1 = MatrixId.RoomId("room1", "server") 30 | val roomId2 = MatrixId.RoomId("room2", "server") 31 | val roomAlias2 = MatrixId.RoomAliasId("alias2", "server") 32 | val userId1 = MatrixId.UserId("user1", "server") 33 | val userId2 = MatrixId.UserId("user2", "server") 34 | val cut = MatrixMembershipSyncService( 35 | matrixClientMock, 36 | roomServiceMock, 37 | membershipServiceMock, 38 | helperMock, 39 | botPropertiesMock 40 | ) 41 | 42 | describe(MatrixMembershipSyncService::syncBotRoomsAndMemberships.name) { 43 | it("should create membership for each room and member") { 44 | coEvery { matrixClientMock.room.getJoinedRooms() } 45 | .returns(flowOf(roomId1, roomId2)) 46 | coEvery { matrixClientMock.room.getJoinedMembers(roomId1) } 47 | .returns(GetJoinedMembersResponse(mapOf(userId1 to GetJoinedMembersResponse.RoomMember()))) 48 | coEvery { matrixClientMock.room.getJoinedMembers(roomId2) } 49 | .returns( 50 | GetJoinedMembersResponse( 51 | mapOf( 52 | userId1 to GetJoinedMembersResponse.RoomMember(), 53 | userId2 to GetJoinedMembersResponse.RoomMember() 54 | ) 55 | ) 56 | ) 57 | coEvery { matrixClientMock.room.getStateEvent(CanonicalAliasEventContent::class, roomId1) } 58 | .returns(CanonicalAliasEventContent()) 59 | coEvery { matrixClientMock.room.getStateEvent(CanonicalAliasEventContent::class, roomId2) } 60 | .returns(CanonicalAliasEventContent(roomAlias2)) 61 | coEvery { membershipServiceMock.getOrCreateMembership(any(), any()) }.returns(mockk()) 62 | coEvery { roomServiceMock.getOrCreateRoomAlias(any(), any()) }.returns(mockk()) 63 | coEvery { helperMock.isManagedRoom(roomAlias2) }.returns(true) 64 | 65 | cut.syncBotRoomsAndMemberships() 66 | 67 | coVerify { 68 | membershipServiceMock.getOrCreateMembership(userId1, roomId1) 69 | membershipServiceMock.getOrCreateMembership(userId1, roomId2) 70 | membershipServiceMock.getOrCreateMembership(userId2, roomId2) 71 | roomServiceMock.getOrCreateRoomAlias(roomAlias2, roomId2) 72 | } 73 | } 74 | } 75 | 76 | describe(MatrixMembershipSyncService::syncRoomMemberships.name) { 77 | it("should fetch all members when there are no memberships for this rooms") { 78 | coEvery { botPropertiesMock.trackMembership }.returns(ALL) 79 | coEvery { membershipServiceMock.getMembershipsSizeByRoomId(roomId1) }.returns(1L) 80 | coEvery { matrixClientMock.room.getJoinedMembers(roomId1) } 81 | .returns( 82 | GetJoinedMembersResponse( 83 | mapOf( 84 | userId1 to GetJoinedMembersResponse.RoomMember(), 85 | userId2 to GetJoinedMembersResponse.RoomMember() 86 | ) 87 | ) 88 | ) 89 | coEvery { membershipServiceMock.getOrCreateMembership(any(), any()) }.returns(mockk()) 90 | 91 | cut.syncRoomMemberships(roomId1) 92 | 93 | coVerify { 94 | membershipServiceMock.getOrCreateMembership(userId1, roomId1) 95 | membershipServiceMock.getOrCreateMembership(userId2, roomId1) 96 | } 97 | } 98 | it("should fetch only managed members when there are no memberships for this rooms") { 99 | coEvery { botPropertiesMock.trackMembership }.returns(MatrixBotProperties.TrackMembershipMode.MANAGED) 100 | coEvery { helperMock.isManagedUser(any()) }.returnsMany(true, false) 101 | coEvery { membershipServiceMock.getMembershipsSizeByRoomId(roomId1) }.returns(1L) 102 | coEvery { matrixClientMock.room.getJoinedMembers(roomId1) } 103 | .returns( 104 | GetJoinedMembersResponse( 105 | mapOf( 106 | userId1 to GetJoinedMembersResponse.RoomMember(), 107 | userId2 to GetJoinedMembersResponse.RoomMember() 108 | ) 109 | ) 110 | ) 111 | coEvery { membershipServiceMock.getOrCreateMembership(any(), any()) }.returns(mockk()) 112 | 113 | cut.syncRoomMemberships(roomId1) 114 | 115 | coVerify { 116 | membershipServiceMock.getOrCreateMembership(userId1, roomId1) 117 | } 118 | } 119 | it("should not fetch members when there are memberships for this rooms") { 120 | coEvery { botPropertiesMock.trackMembership }.returns(ALL) 121 | coEvery { membershipServiceMock.getMembershipsSizeByRoomId(roomId1) }.returns(2L) 122 | 123 | cut.syncRoomMemberships(roomId1) 124 | 125 | coVerify(exactly = 0) { 126 | membershipServiceMock.getOrCreateMembership(any(), any()) 127 | } 128 | } 129 | it("should not fetch members when not wanted") { 130 | coEvery { botPropertiesMock.trackMembership }.returns(NONE) 131 | coEvery { membershipServiceMock.getMembershipsSizeByRoomId(roomId1) }.returns(1L) 132 | 133 | cut.syncRoomMemberships(roomId1) 134 | 135 | coVerify(exactly = 0) { 136 | membershipServiceMock.getOrCreateMembership(any(), any()) 137 | } 138 | } 139 | } 140 | 141 | afterTest { clearMocks(matrixClientMock, roomServiceMock, membershipServiceMock, helperMock) } 142 | } 143 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/membership/MembershipChangeHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.mockk.* 5 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 6 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.AutoJoinMode.DISABLED 7 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.AutoJoinMode.ENABLED 8 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.TrackMembershipMode.ALL 9 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties.TrackMembershipMode.NONE 10 | import net.folivo.spring.matrix.bot.util.BotServiceHelper 11 | import net.folivo.trixnity.client.rest.MatrixClient 12 | import net.folivo.trixnity.core.model.MatrixId 13 | import net.folivo.trixnity.core.model.events.m.room.MemberEventContent.Membership.* 14 | 15 | class MembershipChangeHandlerTest : DescribeSpec(testBody()) 16 | 17 | private fun testBody(): DescribeSpec.() -> Unit { 18 | return { 19 | val matrixClientMock: MatrixClient = mockk(relaxed = true) 20 | val membershipChangeServiceMock: MembershipChangeService = mockk(relaxed = true) 21 | val botHelperMock: BotServiceHelper = mockk() 22 | val botPropertiesMock: MatrixBotProperties = mockk { 23 | every { serverName } returns "server" 24 | } 25 | 26 | val cut = MembershipChangeHandler( 27 | matrixClientMock, 28 | membershipChangeServiceMock, 29 | botHelperMock, 30 | botPropertiesMock 31 | ) 32 | 33 | val botUserId = MatrixId.UserId("bot", "server") 34 | val userId = MatrixId.UserId("user", "server") 35 | val roomId = MatrixId.RoomId("room", "server") 36 | val roomOnForeignServerId = MatrixId.RoomId("room", "foreignServer") 37 | 38 | beforeTest { 39 | every { botPropertiesMock.botUserId }.returns(botUserId) 40 | every { botHelperMock.isManagedUser(any()) }.returns(true) 41 | every { botPropertiesMock.serverName }.returns("server") 42 | coEvery { membershipChangeServiceMock.shouldJoinRoom(any(), any()) }.returns(true) 43 | } 44 | 45 | describe(MembershipChangeHandler::handleMembership.name) { 46 | describe("membership is $INVITE") { 47 | it("should do nothing when autoJoin is $DISABLED") { 48 | every { botPropertiesMock.autoJoin }.returns(DISABLED) 49 | cut.handleMembership(userId, roomId, INVITE) 50 | coVerify { 51 | matrixClientMock wasNot Called 52 | } 53 | } 54 | it("should do nothing when user is not managed") { 55 | every { botPropertiesMock.autoJoin }.returns(ENABLED) 56 | every { botHelperMock.isManagedUser(userId) }.returns(false) 57 | cut.handleMembership(userId, roomId, INVITE) 58 | coVerify { 59 | matrixClientMock wasNot Called 60 | } 61 | } 62 | it("should leave room when autoJoin to foreign server is ${MatrixBotProperties.AutoJoinMode.RESTRICTED}") { 63 | every { botPropertiesMock.autoJoin }.returns(MatrixBotProperties.AutoJoinMode.RESTRICTED) 64 | cut.handleMembership(userId, roomOnForeignServerId, INVITE) 65 | coVerify { 66 | matrixClientMock.room.leaveRoom(roomOnForeignServerId, asUserId = userId) 67 | } 68 | } 69 | it("should join room when autoJoin is ${MatrixBotProperties.AutoJoinMode.RESTRICTED}, but server is allowed") { 70 | every { botPropertiesMock.autoJoin }.returns(MatrixBotProperties.AutoJoinMode.RESTRICTED) 71 | cut.handleMembership(userId, roomId, INVITE) 72 | coVerify { 73 | matrixClientMock.room.joinRoom(roomId, asUserId = userId) 74 | } 75 | } 76 | it("should join room when autoJoin is $ENABLED") { 77 | every { botPropertiesMock.autoJoin }.returns(ENABLED) 78 | cut.handleMembership(userId, roomOnForeignServerId, INVITE) 79 | coVerify { 80 | matrixClientMock.room.joinRoom(roomOnForeignServerId, asUserId = userId) 81 | } 82 | } 83 | it("should not join and leave room when delegated server wants to") { 84 | every { botPropertiesMock.autoJoin }.returns(ENABLED) 85 | coEvery { membershipChangeServiceMock.shouldJoinRoom(userId, roomOnForeignServerId) } 86 | .returns(false) 87 | cut.handleMembership(userId, roomOnForeignServerId, INVITE) 88 | coVerify { 89 | matrixClientMock.room.leaveRoom(roomOnForeignServerId, asUserId = userId) 90 | } 91 | } 92 | } 93 | describe("membership is $JOIN") { 94 | it("should notify service when trackMembershipMode is $ALL") { 95 | every { botPropertiesMock.trackMembership }.returns(ALL) 96 | every { botHelperMock.isManagedUser(userId) }.returns(false) 97 | cut.handleMembership(userId, roomId, JOIN) 98 | coVerify { 99 | membershipChangeServiceMock.onRoomJoin(userId, roomId) 100 | } 101 | } 102 | it("should notify service when trackMembershipMode is ${MatrixBotProperties.TrackMembershipMode.MANAGED} and user is managed") { 103 | every { botPropertiesMock.trackMembership }.returns(MatrixBotProperties.TrackMembershipMode.MANAGED) 104 | cut.handleMembership(userId, roomId, JOIN) 105 | coVerify { 106 | membershipChangeServiceMock.onRoomJoin(userId, roomId) 107 | } 108 | } 109 | it("should not notify service when trackMembershipMode is ${MatrixBotProperties.TrackMembershipMode.MANAGED} and user is not managed") { 110 | every { botPropertiesMock.trackMembership }.returns(MatrixBotProperties.TrackMembershipMode.MANAGED) 111 | every { botHelperMock.isManagedUser(userId) }.returns(false) 112 | 113 | cut.handleMembership(userId, roomId, JOIN) 114 | coVerify { 115 | membershipChangeServiceMock wasNot Called 116 | } 117 | } 118 | it("should not notify service when trackMembershipMode is $NONE") { 119 | every { botPropertiesMock.trackMembership }.returns(NONE) 120 | cut.handleMembership(userId, roomId, JOIN) 121 | coVerify { 122 | membershipChangeServiceMock wasNot Called 123 | } 124 | } 125 | } 126 | describe("membership is $LEAVE or $BAN") { 127 | it("should notify service when trackMembershipMode is $ALL") { 128 | every { botPropertiesMock.trackMembership }.returns(ALL) 129 | every { botHelperMock.isManagedUser(userId) }.returns(false) 130 | cut.handleMembership(userId, roomId, LEAVE) 131 | cut.handleMembership(userId, roomId, BAN) 132 | coVerify(exactly = 2) { 133 | membershipChangeServiceMock.onRoomLeave(userId, roomId) 134 | } 135 | } 136 | it("should notify service when trackMembershipMode is ${MatrixBotProperties.TrackMembershipMode.MANAGED} and user is managed") { 137 | every { botPropertiesMock.trackMembership }.returns(MatrixBotProperties.TrackMembershipMode.MANAGED) 138 | cut.handleMembership(userId, roomId, LEAVE) 139 | cut.handleMembership(userId, roomId, BAN) 140 | coVerify(exactly = 2) { 141 | membershipChangeServiceMock.onRoomLeave(userId, roomId) 142 | } 143 | } 144 | it("should not notify service when trackMembershipMode is ${MatrixBotProperties.TrackMembershipMode.MANAGED} and user is not managed") { 145 | every { botPropertiesMock.trackMembership }.returns(MatrixBotProperties.TrackMembershipMode.MANAGED) 146 | every { botHelperMock.isManagedUser(userId) }.returns(false) 147 | 148 | cut.handleMembership(userId, roomId, LEAVE) 149 | cut.handleMembership(userId, roomId, BAN) 150 | coVerify { 151 | membershipChangeServiceMock wasNot Called 152 | } 153 | } 154 | it("should not notify service when trackMembershipMode is $NONE") { 155 | every { botPropertiesMock.trackMembership }.returns(NONE) 156 | cut.handleMembership(userId, roomId, LEAVE) 157 | cut.handleMembership(userId, roomId, BAN) 158 | 159 | coVerify { 160 | membershipChangeServiceMock wasNot Called 161 | } 162 | } 163 | } 164 | } 165 | 166 | afterTest { clearMocks(matrixClientMock, membershipChangeServiceMock, botHelperMock) } 167 | } 168 | } -------------------------------------------------------------------------------- /matrix-spring-boot-bot/src/test/kotlin/net/folivo/spring/matrix/bot/membership/DefaultMembershipChangeServiceTest.kt: -------------------------------------------------------------------------------- 1 | package net.folivo.spring.matrix.bot.membership 2 | 3 | import io.kotest.core.spec.style.DescribeSpec 4 | import io.kotest.matchers.booleans.shouldBeTrue 5 | import io.mockk.* 6 | import kotlinx.coroutines.flow.flowOf 7 | import net.folivo.spring.matrix.bot.config.MatrixBotProperties 8 | import net.folivo.spring.matrix.bot.room.MatrixRoom 9 | import net.folivo.spring.matrix.bot.room.MatrixRoomService 10 | import net.folivo.spring.matrix.bot.user.MatrixUser 11 | import net.folivo.spring.matrix.bot.user.MatrixUserService 12 | import net.folivo.trixnity.client.rest.MatrixClient 13 | import net.folivo.trixnity.core.model.MatrixId 14 | 15 | class AppserviceMembershipChangeServiceTest : DescribeSpec(testBody()) 16 | 17 | private fun testBody(): DescribeSpec.() -> Unit { 18 | return { 19 | 20 | val roomServiceMock: MatrixRoomService = mockk(relaxed = true) 21 | val membershipServiceMock: MatrixMembershipService = mockk(relaxed = true) 22 | val userServiceMock: MatrixUserService = mockk(relaxed = true) 23 | val membershipSyncServiceMock: MatrixMembershipSyncService = mockk(relaxed = true) 24 | val matrixClientMock: MatrixClient = mockk(relaxed = true) 25 | val botPropertiesMock: MatrixBotProperties = mockk() 26 | 27 | val cut = DefaultMembershipChangeService( 28 | roomServiceMock, 29 | membershipServiceMock, 30 | userServiceMock, 31 | membershipSyncServiceMock, 32 | matrixClientMock, 33 | botPropertiesMock 34 | ) 35 | 36 | val botUserId = MatrixId.UserId("bot", "server") 37 | val userId1 = MatrixId.UserId("user1", "server") 38 | val userId2 = MatrixId.UserId("user2", "server") 39 | val roomId = MatrixId.RoomId("room", "server") 40 | 41 | beforeTest { 42 | coEvery { botPropertiesMock.botUserId }.returns(botUserId) 43 | } 44 | 45 | describe(DefaultMembershipChangeService::onRoomJoin.name) { 46 | it("should get or create user, room and membership") { 47 | cut.onRoomJoin(userId1, roomId) 48 | 49 | coVerifyOrder { 50 | membershipServiceMock.getOrCreateMembership(userId1, roomId) 51 | membershipSyncServiceMock.syncRoomMemberships(roomId) 52 | } 53 | } 54 | } 55 | describe(DefaultMembershipChangeService::onRoomLeave.name) { 56 | beforeTest { 57 | coEvery { roomServiceMock.getOrCreateRoom(roomId) }.returns(MatrixRoom(roomId)) 58 | } 59 | it("should do nothing, when user is not in room") { 60 | coEvery { membershipServiceMock.doesRoomContainsMembers(any(), any()) } 61 | .returns(false) 62 | cut.onRoomLeave(userId1, roomId) 63 | coVerifyAll { 64 | membershipServiceMock.doesRoomContainsMembers( 65 | roomId, 66 | match { it.contains(userId1) && it.size == 1 }) 67 | } 68 | } 69 | describe("user is member") { 70 | beforeTest { 71 | coEvery { membershipServiceMock.doesRoomContainsMembers(any(), any()) } 72 | .returns(true) 73 | coEvery { membershipServiceMock.getMembershipsSizeByRoomId(any()) } 74 | .returns(12L) 75 | coEvery { membershipServiceMock.hasRoomOnlyManagedUsersLeft(any()) } 76 | .returns(false) 77 | } 78 | it("should delete membership") { 79 | coEvery { userServiceMock.getOrCreateUser(any()) }.returns(MatrixUser(userId1, true)) 80 | cut.onRoomLeave(userId1, roomId) 81 | coVerify { 82 | membershipServiceMock.deleteMembership(userId1, roomId) 83 | } 84 | } 85 | it("should delete user when not managed and does not have rooms") { 86 | coEvery { userServiceMock.getOrCreateUser(any()) }.returns(MatrixUser(userId1, false)) 87 | coEvery { membershipServiceMock.getMembershipsSizeByUserId(userId1) }.returns(0L) 88 | cut.onRoomLeave(userId1, roomId) 89 | coVerify { 90 | userServiceMock.deleteUser(userId1) 91 | } 92 | } 93 | it("should not delete user when managed") { 94 | coEvery { userServiceMock.getOrCreateUser(any()) }.returns(MatrixUser(userId1, true)) 95 | cut.onRoomLeave(userId1, roomId) 96 | coVerify(exactly = 0) { 97 | userServiceMock.deleteUser(userId1) 98 | } 99 | } 100 | it("should not delete user when member in other rooms") { 101 | coEvery { userServiceMock.getOrCreateUser(any()) }.returns(MatrixUser(userId1, false)) 102 | coEvery { membershipServiceMock.getMembershipsSizeByUserId(userId1) }.returns(1L) 103 | cut.onRoomLeave(userId1, roomId) 104 | coVerify(exactly = 0) { 105 | userServiceMock.deleteUser(userId1) 106 | } 107 | } 108 | } 109 | describe("user is last member") { 110 | beforeTest { 111 | coEvery { userServiceMock.getOrCreateUser(any()) }.returns(MatrixUser(userId1, true)) 112 | coEvery { membershipServiceMock.doesRoomContainsMembers(any(), any()) } 113 | .returns(true) 114 | coEvery { membershipServiceMock.getMembershipsSizeByRoomId(any()) } 115 | .returns(0L) 116 | coEvery { membershipServiceMock.hasRoomOnlyManagedUsersLeft(any()) } 117 | .returns(false) 118 | } 119 | it("should delete room when not managed") { 120 | coEvery { roomServiceMock.getOrCreateRoom(any()) }.returns(MatrixRoom(roomId, false)) 121 | cut.onRoomLeave(userId1, roomId) 122 | coVerify { roomServiceMock.deleteRoom(roomId) } 123 | } 124 | it("should not delete room when managed") { 125 | coEvery { roomServiceMock.getOrCreateRoom(any()) }.returns(MatrixRoom(roomId, true)) 126 | cut.onRoomLeave(userId1, roomId) 127 | coVerify(exactly = 0) { roomServiceMock.deleteRoom(any()) } 128 | } 129 | } 130 | describe("room has only managed users left") { 131 | beforeTest { 132 | coEvery { userServiceMock.getOrCreateUser(userId1) }.returns(MatrixUser(userId1, true)) 133 | coEvery { membershipServiceMock.doesRoomContainsMembers(any(), any()) } 134 | .returns(true) 135 | coEvery { membershipServiceMock.getMembershipsSizeByRoomId(any()) } 136 | .returns(2L) 137 | coEvery { membershipServiceMock.hasRoomOnlyManagedUsersLeft(any()) } 138 | .returns(true) 139 | coEvery { membershipServiceMock.getMembershipsByRoomId(roomId) } 140 | .returns( 141 | flowOf( 142 | MatrixMembership(userId2, roomId), 143 | MatrixMembership(botUserId, roomId) 144 | ) 145 | ) 146 | } 147 | describe("room is managed") { 148 | beforeTest { 149 | coEvery { roomServiceMock.getOrCreateRoom(any()) }.returns(MatrixRoom(roomId, true)) 150 | } 151 | it("no members should leave room") { 152 | cut.onRoomLeave(userId1, roomId) 153 | coVerify(exactly = 0) { 154 | matrixClientMock.room.leaveRoom(any(), any()) 155 | } 156 | } 157 | it("should not delete room") { 158 | cut.onRoomLeave(userId1, roomId) 159 | coVerify(exactly = 0) { roomServiceMock.deleteRoom(any()) } 160 | } 161 | } 162 | describe("room is not managed") { 163 | beforeTest { 164 | coEvery { roomServiceMock.getOrCreateRoom(any()) }.returns(MatrixRoom(roomId, false)) 165 | } 166 | it("all members should leave room and their membership deleted") { 167 | cut.onRoomLeave(userId1, roomId) 168 | coVerify { 169 | matrixClientMock.room.leaveRoom(roomId, userId2) 170 | matrixClientMock.room.leaveRoom(roomId) 171 | membershipServiceMock.deleteMembership(userId2, roomId) 172 | membershipServiceMock.deleteMembership(botUserId, roomId) 173 | } 174 | } 175 | it("should delete room") { 176 | cut.onRoomLeave(userId1, roomId) 177 | coVerify { roomServiceMock.deleteRoom(roomId) } 178 | } 179 | } 180 | } 181 | } 182 | 183 | describe(DefaultMembershipChangeService::shouldJoinRoom.name) { 184 | it("should always return true") { 185 | cut.shouldJoinRoom(MatrixId.UserId("blub", "bla"), MatrixId.RoomId("bla", "blub")).shouldBeTrue() 186 | } 187 | } 188 | 189 | afterTest { 190 | clearMocks( 191 | roomServiceMock, 192 | membershipServiceMock, 193 | userServiceMock, 194 | membershipSyncServiceMock, 195 | matrixClientMock, 196 | botPropertiesMock 197 | ) 198 | } 199 | } 200 | } --------------------------------------------------------------------------------