Merge branch 'develop' into fix_2100_2
|
@ -1,4 +1,4 @@
|
|||
A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
|
||||
A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
|
||||
|
||||
# Core team:
|
||||
|
||||
|
@ -33,3 +33,8 @@ First of all, we thank all contributors who use Element and report problems on t
|
|||
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
|
||||
|
||||
Feel free to add your name below, when you contribute to the project!
|
||||
|
||||
Name | Matrix ID | GitHub
|
||||
--------|---------------------|--------------------------------------
|
||||
gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ Improvements 🙌:
|
|||
- Prepare changelog for F-Droid (#2296)
|
||||
- Add graphic resources for F-Droid (#812, #2220)
|
||||
- Highlight text in the body of the displayed result (#2200)
|
||||
|
||||
- Considerably faster QR-code bitmap generation (#2331)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fixed ringtone handling (#2100 & #2246)
|
||||
- Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)
|
||||
|
|
|
@ -65,9 +65,8 @@ allprojects {
|
|||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
// Warnings are potential errors, so stop ignoring them
|
||||
// You can override by passing `-PallWarningsAsErrors=false` in the command line
|
||||
kotlinOptions.allWarningsAsErrors = project.properties['allWarningsAsErrors']?.toBoolean() ?: true
|
||||
kotlinOptions.allWarningsAsErrors = project.getProperties().getOrDefault("allWarningsAsErrors", "true").toBoolean()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
|
1
fastlane/metadata/android/en-US/changelogs/40100100.txt
Normal file
|
@ -0,0 +1 @@
|
|||
// TODO
|
BIN
fastlane/metadata/android/en-US/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 316 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 310 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 543 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 341 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 334 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 551 KiB |
30
fastlane/metadata/android/eo/full_description.txt
Normal file
|
@ -0,0 +1,30 @@
|
|||
Element estas nova speco de mesaĝilo kaj kunlaborilo, kiu:
|
||||
|
||||
1. Lasas vin regi vian komunikadon por protekti vian privatecon
|
||||
2. Lasas vin komuniki kun ĉiu ajn en la reto de Matrix, kaj eĉ pliaj, per interkompreno kun aplikaĵoj kiel ekzemple Slack
|
||||
3. Protektas vin de reklamoj, kolektado de datumoj, kaj muritaj ĝardenoj
|
||||
4. Sekurigas vian komunikadon per tutvoja ĉifrado, kun la eblo kontroli aliajn per delegaj subskriboj
|
||||
|
||||
Element malsamas al aliaj mesaĝiloj kaj kunlaboriloj, ĉar ĝi estas sencentra kaj malfermitkoda.
|
||||
|
||||
Element lasas vin gastigi vian propran servilon, aŭ elekti servilon, kiu plaĉas al vi, por ke vi ne perdu privatecon, kaj por ke vi daŭre regu kaj posedu viajn datumojn kaj interparolojn. Ĝi donas al vi aliron al malfermita reto; por ke via interparolado ne estu limigita nur al aliaj uzantoj de Element. Kaj ĝi estas tre sekura.
|
||||
|
||||
Element povas fari ĉi ĉion, ĉar ĝi funkcias per Matrix – publika normo por malfermita, sencentra komunikado.
|
||||
|
||||
Element lasas vi elekti, kiu gastigos viajn interparolojn. Per la aplikaĵo Element, vi povas elekti diversajn specojn de gastigado:
|
||||
|
||||
1. Akiri senpagan konton ĉe la publika servilo matrix.org, gastigata de la programistoj de Matrix, aŭ elekti unu el miloj da publikaj serviloj, gastigataj de volontuloj
|
||||
2. Memgastiĝi per via propra servilo ĉe via propra aparato
|
||||
3. Registriĝi ĉe propra servilo per simpla pagaliĝo al la gastiga platformo «Element Matrix Services»
|
||||
|
||||
<b>Kial Element?</b>
|
||||
|
||||
<b>POSEDU VIAJN DATUMOJN</b>: Vi decidu, kie vi tenu viajn datumojn kaj mesaĝojn. Vi posedas kaj regas ilin, ne iu granda komerca firmao, kiu kolektas viajn datumojn aŭ donas aliron al aliuloj.
|
||||
|
||||
<b>MALFERMAJ MESAĜADO KAJ KUNLABORADO</b>: Vi povas babili kun ĉiu alia en la reto de Matrix, ĉu ĝi uzas Elementon aŭ alian aplikaĵon de Matrix, kaj eĉ se ĝi uzas tute alian mesaĝilon, kiel ekzemple Slack, IRC, aŭ XMPP.
|
||||
|
||||
<b>TRE SEKURA</b>: Vera tutvoja ĉifrado (nur la interparolantoj povas malĉifri siajn mesaĝojn), kaj delegaj subskriboj por kontroli la aparatojn de partoprenantoj.
|
||||
|
||||
<b>SENMANKA KOMUNIKADO</b>: Mesaĝoj, voĉvokoj kaj vidvokoj, havigado de dosieroj, ekrano, kaj multaj diversaj kunigoj, robotoj kaj fenestraĵoj. Kreu ĉambrojn, komunumojn, komuniku kaj kunlaboru.
|
||||
|
||||
<b>ĈIE KUN VI</b>: Tenu vin ĝisdata per historio de mesaĝoj plene spegulita trans ĉiuj viaj aparatoj, kaj sur la reto per https://app.element.io.
|
1
fastlane/metadata/android/eo/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Sekura kaj sencentrigita vokado kaj babilado. Tenu viajn datumojn sekuraj.
|
1
fastlane/metadata/android/eo/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Element (antaŭe Riot.im)
|
30
fastlane/metadata/android/et/full_description.txt
Normal file
|
@ -0,0 +1,30 @@
|
|||
Element on uut tüüpi suhtlus- ja koostöörakendus, mis:
|
||||
|
||||
1. Võimaldab täielikku kontrolli privaatsuse üle
|
||||
2. Võimaldab suhelda kõigiga Matrixi võrgus ja isegi väljaspool seda, olles integreeritud selliste rakendustega nagu Slack
|
||||
3. Kaitseb sind reklaamide ja andmekogumise eest
|
||||
4. Tagab turvalisuse läbiva krüptimise abil, kasutades risttunnustamist vestlejate tuvastamiseks
|
||||
|
||||
Element erineb täielikult teistest sõnumside- ja koostöörakendustest, kuna see on detsentraliseeritud ja avatud lähtekoodiga.
|
||||
|
||||
Element võimaldab ise hostida - või valida hosti -, et oleks tagatud privaatsus ja kontroll oma andmete ja vestluste üle. Element annab ka juurdepääsu avatud võrgule, nii et te ei pea vaid Elemendi kasutajatega rääkima. Ning kogu süsteem on väga turvaline.
|
||||
|
||||
Element töötab Matrixil - avatud, detsentraliseeritud suhtlus-standardil.
|
||||
|
||||
Võimaldades valida, kes vestlusi korraldab, annab Element annab kontrolli sinule. Rakendust Element saad kasutada mitmel viisil.
|
||||
|
||||
1. Tasuta konto Matrixi arendajate hostitud avalikus serveris matrix.org või vali tuhandete avalike serverite hulgast, mida haldavad vabatahtlikud
|
||||
2. Hosti oma kontot ise, paigaldades serveri oma riistvarale
|
||||
3. Registreeruge serveris olevale kontole, tellides Element Matrix Services teenuseplatvormi
|
||||
|
||||
<b> Miks valida element? </b>
|
||||
|
||||
<b> KONTROLL ANDMETE ÜLE</b>: otsustad ise, kus oma andmeid ja sõnumeid hoida. Need kuuluvad sulle ja sinu käes on kontroll, mitte mõne MEGAFIRMA käes, mis andmeid oma kasuks kaevandab või kolmandatele isikutele juurdepääsu annab.
|
||||
|
||||
<b> AVATUD SUHTLUS JA KOOSTÖÖ </b>: saad vestelda kõigi teistega Matrixi võrgus, olenemata sellest, kas nad kasutavad Elementi või mõnda muud Matrixi rakendust, ja isegi kui nad kasutavad teistsugust suhtlussüsteemi nagu Slack, IRC või XMPP.
|
||||
|
||||
<b> ÜLITURVALINE </b>: tõeline läbiv krüptimine (ainult vestluses osalejad saavad sõnumeid lugeda) ja risttunnustamine vestluses osalejate tuvastamiseks.
|
||||
|
||||
<b> KÕIK SUHTLUSVÕIMALUSED</b>: sõnumid, hääl- ja videokõned, failide jagamine, ekraani jagamine ja terve hulk lõiminguid, roboteid ja vidinaid. Loo tubasid, kogukondi, hoia ühendust ja saa asjad aetud.
|
||||
|
||||
<b> KÕIKJAL, KUS VIIBITE</b>: saad suhelda kõigis oma seadmetes ja ka veebis aadressil https://app.element.io ning sealjuures täielikult sünkroonitud sõnumite ajalooga.
|
30
fastlane/metadata/android/fa/full_description.txt
Normal file
|
@ -0,0 +1,30 @@
|
|||
المنت گونهای جدید از کارههای پیامرسانی و همکاری است که:
|
||||
|
||||
۱. کنترل محرمانگیتان را در دست خودتان میگذارد
|
||||
۲. میگذارد با هرکسی در شبکهٔ ماتریکس و حتا فراتر از آن، ارتباط برقرار کنید
|
||||
۳. از شما در برابر تبلیغات، دادهکاوری و دیوارهای پرداختی، محافظت میکند
|
||||
۴. با رمزنگاری سرتاسری با ورود چندگانه، امنتان میکند
|
||||
|
||||
المنت به خاطر نامتمرکز و نرمافزار آزاد بودن، کاملاً با دیگر کارههای پیامرسانی و همکاری، فرق دارد.
|
||||
|
||||
المنت میگذارد خودمیزبانی کرده یا میزبانی برگزینید که امنیت، مالکیت و واپایش دادهها و گفتوگوهایتان را در اختیار داشته باشید. این کاره شما را به شبکهای باز و شدیداً امن وصل کرده تا مجبور نباشید فقط با دیگر کاربران المنت صحبت کنید.
|
||||
|
||||
المنت میتواند همهٔ این کارها را بکند، چرا که روی ماتریکس، استانداردی برای گفتوگوی باز و نامتمرکز عمل میکند.
|
||||
|
||||
المنت با اجازه برای گزینش کسی که گفتوگوهایتان را میزبانی میکند، کنترل را به شما میدهد. با کارهٔ المنت، میتوانید برگزینید که به روشهای مختلفی میزبانی شوید:
|
||||
|
||||
۱. گرفتن حسابی رایگان روی کارساز عمومی matrix.org که به دست توسعهدهندگان ماتریکس میزبانی میشود، یا گرینش از میان هزاران کارساز عمومی میزبانیشده به دست داوطلبان
|
||||
۲. خودمیزبانی حسابتان با اجرای کراسازی روی سختافزار خودتان
|
||||
۳. ثبتنام برای حسابی روی یک کارساز سفارشی با اشتراک در بنیازهٔ میزبانی خدمات ماتریکس المنت
|
||||
|
||||
<b>چرا المنت را برگزینیم؟</b>
|
||||
|
||||
<b>مالک دادههایتان باشید</b>: خوتان تصمیم میگیرید که دادهها و پیامهایتان را کجا نگه دارید. شما صاحبشان هستید و واپایششان میکنید، نه شرکتهای بزرگی که دادههایتان را کاویده و به شرکتهای دیگر دسترسی میدهند.
|
||||
|
||||
<b>پیامرسانی و همکاری باز</b>: میتوانید با هرکسی در شبکهٔ ماتریکس گپ بزنید، چه از المنت استفاده کنند و چه از هر کارهٔ ماتریکس دیگری؛ و حتا اگر از سامانهٔ پیامرسانی متفاوتی مثل اسلک، آیآرسی یا جبر استفاده کنند.
|
||||
|
||||
<b>فوق امن</b>: رمزنگاری سرتاسری واقعی (فقط کسانی که در گفتوگو هستند،میتوانند پیامها را رمزگشایی کنند) و ورود چندگانه برای تأیید هویت افزارههای شرکتکنندگان در گفتوگو.
|
||||
|
||||
<b>ارتباط کامل</b>: پیامرسانی، تماسهای صوتی و تصویری،همرسانی پرونده، همرسانی صفحه و یه عالمه یکپارچگی، بات و ابزارک. اتاق و اجتماع ساخته، در دسترس بوده و کارها را انجام دهید.
|
||||
|
||||
<b>هرجا که هستید</b>: هر کجا که هستید، با همگام سازی کامل تاریخچهٔ پیامها بین همهٔ افزارههایتان و وب روی https://app.element.io در دسترس باشید.
|
1
fastlane/metadata/android/fa/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
گپ و تماس نامتمرکز امن. دادههایتان را از شرکتها امن نگه دارید.
|
1
fastlane/metadata/android/fa/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
المنت (ریوت سابق)
|
1
fastlane/metadata/android/fr/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Chat & VoIP sûr et décentralisé. Gardez vos données en sécurité.
|
|
@ -21,7 +21,7 @@ Element sätter dig i kontroll genom att låta dig välja att vara värd för di
|
|||
|
||||
<b>ÄG DIN DATA</b>: Du väljer var du vill ha din data och dina meddelanden. Du äger den och kontrollerar den, inte nåt stort företag som samlar in din data och ger den till tredje parter.
|
||||
|
||||
<b>ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE</b>: Du kan chatta med med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP.
|
||||
<b>ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE</b>: Du kan chatta med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP.
|
||||
|
||||
<b>SUPERSÄKER</b>: Riktig totalsträckskryptering (bara de in konversationen kan avkryptera meddelandena), och korssingering för att verifiera konversationsmedlemmars enheter.
|
||||
|
||||
|
|
|
@ -142,6 +142,10 @@ class RxRoom(private val room: Room) {
|
|||
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
|
||||
room.updateAvatar(avatarUri, fileName, it)
|
||||
}
|
||||
|
||||
fun deleteAvatar(): Completable = completableBuilder<Unit> {
|
||||
room.deleteAvatar(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun Room.rx(): RxRoom {
|
||||
|
|
|
@ -88,7 +88,10 @@ class CommonTestHelper(context: Context) {
|
|||
fun syncSession(session: Session) {
|
||||
val lock = CountDownLatch(1)
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) { session.open() }
|
||||
val job = GlobalScope.launch(Dispatchers.Main) {
|
||||
session.open()
|
||||
}
|
||||
runBlocking { job.join() }
|
||||
|
||||
session.startSync(true)
|
||||
|
||||
|
@ -341,7 +344,7 @@ class CommonTestHelper(context: Context) {
|
|||
}
|
||||
|
||||
// Transform a method with a MatrixCallback to a synchronous method
|
||||
inline fun <reified T> doSync(block: (MatrixCallback<T>) -> Unit): T {
|
||||
inline fun <reified T> doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
|
||||
val lock = CountDownLatch(1)
|
||||
var result: T? = null
|
||||
|
||||
|
@ -354,7 +357,7 @@ class CommonTestHelper(context: Context) {
|
|||
|
||||
block.invoke(callback)
|
||||
|
||||
await(lock)
|
||||
await(lock, timeout)
|
||||
|
||||
assertNotNull(result)
|
||||
return result!!
|
||||
|
@ -366,8 +369,9 @@ class CommonTestHelper(context: Context) {
|
|||
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
|
||||
|
||||
fun signOutAndClose(session: Session) {
|
||||
doSync<Unit> { session.signOut(true, it) }
|
||||
session.close()
|
||||
doSync<Unit>(60_000) { session.signOut(true, it) }
|
||||
// no need signout will close
|
||||
// session.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,9 +32,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||
|
@ -197,47 +194,16 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val lock = CountDownLatch(1)
|
||||
|
||||
val bobEventsListener = object : Timeline.Listener {
|
||||
override fun onTimelineFailure(throwable: Throwable) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
.groupBy { it.root.senderId!! }
|
||||
|
||||
// Alice has sent 2 messages and Bob has sent 3 messages
|
||||
if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) {
|
||||
lock.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20))
|
||||
bobTimeline.start()
|
||||
bobTimeline.addListener(bobEventsListener)
|
||||
|
||||
// Alice sends a message
|
||||
roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
|
||||
mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1)
|
||||
// roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
|
||||
|
||||
// Bob send 3 messages
|
||||
roomFromBobPOV.sendTextMessage(messagesFromBob[0])
|
||||
roomFromBobPOV.sendTextMessage(messagesFromBob[1])
|
||||
roomFromBobPOV.sendTextMessage(messagesFromBob[2])
|
||||
|
||||
mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1)
|
||||
mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1)
|
||||
mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1)
|
||||
// Alice sends a message
|
||||
roomFromAlicePOV.sendTextMessage(messagesFromAlice[1])
|
||||
|
||||
mTestHelper.await(lock)
|
||||
|
||||
bobTimeline.removeListener(bobEventsListener)
|
||||
bobTimeline.dispose()
|
||||
mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1)
|
||||
|
||||
return cryptoTestData
|
||||
}
|
||||
|
@ -285,14 +251,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||
|
||||
fun createDM(alice: Session, bob: Session): String {
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
alice.createRoom(
|
||||
CreateRoomParams().apply {
|
||||
invitedUserIds.add(bob.myUserId)
|
||||
setDirectMessage()
|
||||
enableEncryptionIfInvitedUsersSupportIt = true
|
||||
},
|
||||
it
|
||||
)
|
||||
alice.createDirectRoom(bob.myUserId, it)
|
||||
}
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto
|
|||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||
|
@ -40,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
|
|||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
interface CryptoService {
|
||||
|
||||
|
@ -142,10 +144,13 @@ interface CryptoService {
|
|||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
||||
fun getGossipingEvents(): List<Event>
|
||||
|
||||
// For testing shared session
|
||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||
|
|
|
@ -35,6 +35,22 @@ interface RoomService {
|
|||
fun createRoom(createRoomParams: CreateRoomParams,
|
||||
callback: MatrixCallback<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters
|
||||
*/
|
||||
fun createDirectRoom(otherUserId: String,
|
||||
callback: MatrixCallback<String>): Cancelable {
|
||||
return createRoom(
|
||||
CreateRoomParams()
|
||||
.apply {
|
||||
invitedUserIds.add(otherUserId)
|
||||
setDirectMessage()
|
||||
enableEncryptionIfInvitedUsersSupportIt = true
|
||||
},
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a room by id
|
||||
* @param roomIdOrAlias the roomId or the room alias of the room to join
|
||||
|
@ -113,5 +129,16 @@ interface RoomService {
|
|||
*/
|
||||
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
|
||||
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String): Room?
|
||||
/**
|
||||
* Return the roomId of an existing DM with the other user, or null if such room does not exist
|
||||
* A room is a DM if:
|
||||
* - it is listed in the `m.direct` account data
|
||||
* - the current user has joined the room
|
||||
* - the other user is invited or has joined the room
|
||||
* - it has exactly 2 members
|
||||
* Note:
|
||||
* - the returning room can be encrypted or not
|
||||
* - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room
|
||||
*/
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String): String?
|
||||
}
|
||||
|
|
|
@ -63,8 +63,13 @@ data class RoomSummary constructor(
|
|||
val hasNewMessages: Boolean
|
||||
get() = notificationCount != 0
|
||||
|
||||
val isLowPriority: Boolean
|
||||
get() = hasTag(RoomTag.ROOM_TAG_LOW_PRIORITY)
|
||||
|
||||
val isFavorite: Boolean
|
||||
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
|
||||
get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE)
|
||||
|
||||
fun hasTag(tag: String) = tags.any { it.name == tag }
|
||||
|
||||
val canStartCall: Boolean
|
||||
get() = joinedMembersCount == 2
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.room.model.create
|
||||
|
||||
import android.net.Uri
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||
|
@ -51,6 +52,11 @@ class CreateRoomParams {
|
|||
*/
|
||||
var topic: String? = null
|
||||
|
||||
/**
|
||||
* If this is not null, the image uri will be sent to the media server and will be set as a room avatar.
|
||||
*/
|
||||
var avatarUri: Uri? = null
|
||||
|
||||
/**
|
||||
* A list of user IDs to invite to the room.
|
||||
* This will tell the server to invite everyone in the list to the newly created room.
|
||||
|
|
|
@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
|
|||
data class ReactionInfo(
|
||||
@Json(name = "rel_type") override val type: String?,
|
||||
@Json(name = "event_id") override val eventId: String,
|
||||
val key: String,
|
||||
@Json(name = "key") val key: String,
|
||||
// always null for reaction
|
||||
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
|
||||
@Json(name = "option") override val option: Int? = null
|
||||
|
|
|
@ -123,11 +123,6 @@ interface SendService {
|
|||
*/
|
||||
fun deleteFailedEcho(localEcho: TimelineEvent)
|
||||
|
||||
/**
|
||||
* Delete all the events in one of the sending states
|
||||
*/
|
||||
fun clearSendingQueue()
|
||||
|
||||
/**
|
||||
* Cancel sending a specific event. It has to be in one of the sending states
|
||||
*/
|
||||
|
|
|
@ -58,6 +58,11 @@ interface StateService {
|
|||
*/
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Delete the avatar of the room
|
||||
*/
|
||||
fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.whereType
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import org.matrix.android.sdk.internal.util.fetchCopied
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* The crypto module needs some information regarding rooms that are stored
|
||||
* in the session DB, this class encapsulate this functionality
|
||||
*/
|
||||
internal class CryptoSessionInfoProvider @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy
|
||||
) {
|
||||
|
||||
fun isRoomEncrypted(roomId: String): Boolean {
|
||||
val encryptionEvent = monarchy.fetchCopied { realm ->
|
||||
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||
.isNotNull(EventEntityFields.STATE_KEY) // should be an empty key
|
||||
.findFirst()
|
||||
}
|
||||
return encryptionEvent != null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param allActive if true return joined as well as invited, if false, only joined
|
||||
*/
|
||||
fun getRoomUserIds(roomId: String, allActive: Boolean): List<String> {
|
||||
var userIds: List<String> = emptyList()
|
||||
monarchy.doWithRealm { realm ->
|
||||
userIds = if (allActive) {
|
||||
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
||||
} else {
|
||||
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
|
||||
}
|
||||
}
|
||||
return userIds
|
||||
}
|
||||
}
|
|
@ -17,12 +17,10 @@
|
|||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import com.squareup.moshi.Types
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -51,9 +49,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
||||
|
@ -68,7 +64,6 @@ import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
|
|||
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
|
||||
|
@ -82,21 +77,15 @@ import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
|
|||
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
||||
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.whereType
|
||||
import org.matrix.android.sdk.internal.di.DeviceId
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.TaskThread
|
||||
|
@ -104,11 +93,11 @@ import org.matrix.android.sdk.internal.task.configureWith
|
|||
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.fetchCopied
|
||||
import org.matrix.olm.OlmManager
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
import kotlin.jvm.Throws
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
|
@ -171,28 +160,16 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
private val setDeviceNameTask: SetDeviceNameTask,
|
||||
private val uploadKeysTask: UploadKeysTask,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val messageEncrypter: MessageEncrypter
|
||||
private val eventDecryptor: EventDecryptor
|
||||
) : CryptoService {
|
||||
|
||||
init {
|
||||
verificationService.cryptoService = this
|
||||
}
|
||||
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val isStarting = AtomicBoolean(false)
|
||||
private val isStarted = AtomicBoolean(false)
|
||||
|
||||
// The date of the last time we forced establishment
|
||||
// of a new session for each user:device.
|
||||
private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
|
||||
|
||||
fun onStateEvent(roomId: String, event: Event) {
|
||||
when (event.getClearType()) {
|
||||
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||
|
@ -209,6 +186,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val gossipingBuffer = mutableListOf<Event>()
|
||||
|
||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||
setDeviceNameTask
|
||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
||||
|
@ -410,7 +389,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
*/
|
||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||
|
||||
incomingGossipingRequestManager.close()
|
||||
olmDevice.release()
|
||||
cryptoStore.close()
|
||||
}
|
||||
|
@ -452,6 +431,13 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
}
|
||||
}
|
||||
|
||||
tryOrNull {
|
||||
gossipingBuffer.toList().let {
|
||||
cryptoStore.saveGossipingEvents(it)
|
||||
}
|
||||
gossipingBuffer.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -612,13 +598,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
|
||||
*/
|
||||
override fun isRoomEncrypted(roomId: String): Boolean {
|
||||
val encryptionEvent = monarchy.fetchCopied { realm ->
|
||||
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||
.isNotNull(EventEntityFields.STATE_KEY)
|
||||
.findFirst()
|
||||
}
|
||||
return encryptionEvent != null
|
||||
return cryptoSessionInfoProvider.isRoomEncrypted(roomId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -660,11 +640,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
eventType: String,
|
||||
roomId: String,
|
||||
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
||||
// moved to crypto scope to have uptodate values
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
// if (!isStarted()) {
|
||||
// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
|
||||
// internalStart(false)
|
||||
// }
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
var alg = roomEncryptorsStore.get(roomId)
|
||||
if (alg == null) {
|
||||
|
@ -720,14 +697,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @param callback the callback to return data or null
|
||||
*/
|
||||
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||
cryptoCoroutineScope.launch {
|
||||
val result = runCatching {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
internalDecryptEvent(event, timeline)
|
||||
}
|
||||
}
|
||||
result.foldToCallback(callback)
|
||||
}
|
||||
eventDecryptor.decryptEventAsync(event, timeline, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -739,42 +709,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
val eventContent = event.content
|
||||
if (eventContent == null) {
|
||||
Timber.e("## CRYPTO | decryptEvent : empty event content")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||
} else {
|
||||
val algorithm = eventContent["algorithm"]?.toString()
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
||||
if (alg == null) {
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
||||
Timber.e("## CRYPTO | decryptEvent() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
||||
} else {
|
||||
try {
|
||||
return alg.decryptEvent(event, timeline)
|
||||
} catch (mxCryptoError: MXCryptoError) {
|
||||
Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
||||
if (mxCryptoError is MXCryptoError.Base
|
||||
&& mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
||||
// need to find sending device
|
||||
val olmContent = event.content.toModel<OlmEventContent>()
|
||||
cryptoStore.getUserDevices(event.senderId ?: "")
|
||||
?.values
|
||||
?.firstOrNull { it.identityKey() == olmContent?.senderKey }
|
||||
?.let {
|
||||
markOlmSessionForUnwedging(event.senderId ?: "", it)
|
||||
}
|
||||
?: run {
|
||||
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
|
||||
}
|
||||
}
|
||||
}
|
||||
throw mxCryptoError
|
||||
}
|
||||
}
|
||||
}
|
||||
return eventDecryptor.decryptEvent(event, timeline)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -796,19 +731,19 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
when (event.getClearType()) {
|
||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
gossipingBuffer.add(event)
|
||||
// Keys are imported directly, not waiting for end of sync
|
||||
onRoomKeyEvent(event)
|
||||
}
|
||||
EventType.REQUEST_SECRET,
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
// save audit trail
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
gossipingBuffer.add(event)
|
||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
gossipingBuffer.add(event)
|
||||
onSecretSendReceived(event)
|
||||
}
|
||||
EventType.ROOM_KEY_WITHHELD -> {
|
||||
|
@ -828,7 +763,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
*/
|
||||
private fun onRoomKeyEvent(event: Event) {
|
||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||
Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
|
||||
Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
|
||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||
Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields")
|
||||
return
|
||||
|
@ -935,19 +870,9 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getRoomUserIds(roomId: String): List<String> {
|
||||
var userIds: List<String> = emptyList()
|
||||
monarchy.doWithRealm { realm ->
|
||||
// Check whether the event content must be encrypted for the invited members.
|
||||
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
||||
&& shouldEncryptForInvitedMembers(roomId)
|
||||
|
||||
userIds = if (encryptForInvitedMembers) {
|
||||
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
||||
} else {
|
||||
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
|
||||
}
|
||||
}
|
||||
return userIds
|
||||
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
||||
&& shouldEncryptForInvitedMembers(roomId)
|
||||
return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1257,38 +1182,38 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
|
||||
}
|
||||
|
||||
private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
||||
val deviceKey = deviceInfo.identityKey()
|
||||
|
||||
val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
||||
Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
||||
lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
|
||||
|
||||
// Now send a blank message on that session so the other side knows about it.
|
||||
// (The keyshare request is sent in the clear so that won't do)
|
||||
// We send this first such that, as long as the toDevice messages arrive in the
|
||||
// same order we sent them, the other end will get this first, set up the new session,
|
||||
// then get the keyshare request and send the key over this new session (because it
|
||||
// is the session it has most recently received a message on).
|
||||
val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
|
||||
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
}
|
||||
}
|
||||
// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
||||
// val deviceKey = deviceInfo.identityKey()
|
||||
//
|
||||
// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
||||
// val now = System.currentTimeMillis()
|
||||
// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
||||
// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
||||
// lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
||||
//
|
||||
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
|
||||
//
|
||||
// // Now send a blank message on that session so the other side knows about it.
|
||||
// // (The keyshare request is sent in the clear so that won't do)
|
||||
// // We send this first such that, as long as the toDevice messages arrive in the
|
||||
// // same order we sent them, the other end will get this first, set up the new session,
|
||||
// // then get the keyshare request and send the key over this new session (because it
|
||||
// // is the session it has most recently received a message on).
|
||||
// val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
|
||||
//
|
||||
// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
// val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
|
||||
// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
|
||||
// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
// sendToDeviceTask.execute(sendToDeviceParams)
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Provides the list of unknown devices
|
||||
|
@ -1339,14 +1264,26 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||
return cryptoStore.getIncomingRoomKeyRequestsPaged()
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||
return cryptoStore.getIncomingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): List<Event> {
|
||||
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
||||
return cryptoStore.getGossipingEventsTrail()
|
||||
}
|
||||
|
||||
override fun getGossipingEvents(): List<Event> {
|
||||
return cryptoStore.getGossipingEvents()
|
||||
}
|
||||
|
||||
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
|
||||
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
||||
}
|
||||
|
|
|
@ -377,7 +377,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
|||
}
|
||||
|
||||
// Update devices trust for these users
|
||||
dispatchDeviceChange(downloadUsers)
|
||||
// dispatchDeviceChange(downloadUsers)
|
||||
|
||||
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
@SessionScope
|
||||
internal class EventDecryptor @Inject constructor(
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore
|
||||
) {
|
||||
|
||||
// The date of the last time we forced establishment
|
||||
// of a new session for each user:device.
|
||||
private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
|
||||
|
||||
/**
|
||||
* Decrypt an event
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return internalDecryptEvent(event, timeline)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an event asynchronously
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @param callback the callback to return data or null
|
||||
*/
|
||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||
// is it needed to do that on the crypto scope??
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
runCatching {
|
||||
internalDecryptEvent(event, timeline)
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an event
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @return the MXEventDecryptionResult data, or null in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
val eventContent = event.content
|
||||
if (eventContent == null) {
|
||||
Timber.e("## CRYPTO | decryptEvent : empty event content")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||
} else {
|
||||
val algorithm = eventContent["algorithm"]?.toString()
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
||||
if (alg == null) {
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
||||
Timber.e("## CRYPTO | decryptEvent() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
||||
} else {
|
||||
try {
|
||||
return alg.decryptEvent(event, timeline)
|
||||
} catch (mxCryptoError: MXCryptoError) {
|
||||
Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
||||
if (mxCryptoError is MXCryptoError.Base
|
||||
&& mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
||||
// need to find sending device
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
val olmContent = event.content.toModel<OlmEventContent>()
|
||||
cryptoStore.getUserDevices(event.senderId ?: "")
|
||||
?.values
|
||||
?.firstOrNull { it.identityKey() == olmContent?.senderKey }
|
||||
?.let {
|
||||
markOlmSessionForUnwedging(event.senderId ?: "", it)
|
||||
}
|
||||
?: run {
|
||||
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw mxCryptoError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// coroutineDispatchers.crypto scope
|
||||
private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
||||
val deviceKey = deviceInfo.identityKey()
|
||||
|
||||
val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
||||
Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
||||
lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
||||
|
||||
// offload this from crypto thread (?)
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||
ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
|
||||
|
||||
// Now send a blank message on that session so the other side knows about it.
|
||||
// (The keyshare request is sent in the clear so that won't do)
|
||||
// We send this first such that, as long as the toDevice messages arrive in the
|
||||
// same order we sent them, the other end will get this first, set up the new session,
|
||||
// then get the keyshare request and send the key over this new session (because it
|
||||
// is the session it has most recently received a message on).
|
||||
val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
|
||||
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
|
||||
withContext(coroutineDispatchers.io) {
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.util.LruCache
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import timber.log.Timber
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Allows to cache and batch store operations on inbound group session store.
|
||||
* Because it is used in the decrypt flow, that can be called quite rapidly
|
||||
*/
|
||||
internal class InboundGroupSessionStore @Inject constructor(
|
||||
private val store: IMXCryptoStore,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
|
||||
|
||||
private data class CacheKey(
|
||||
val sessionId: String,
|
||||
val senderKey: String
|
||||
)
|
||||
|
||||
private val sessionCache = object : LruCache<CacheKey, OlmInboundGroupSessionWrapper2>(30) {
|
||||
override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
|
||||
if (evicted && oldValue != null) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
|
||||
store.storeInboundGroupSessions(listOf(oldValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val timer = Timer()
|
||||
private var timerTask: TimerTask? = null
|
||||
|
||||
private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
|
||||
|
||||
@Synchronized
|
||||
fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
|
||||
synchronized(sessionCache) {
|
||||
val known = sessionCache[CacheKey(sessionId, senderKey)]
|
||||
Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
|
||||
return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
|
||||
Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
|
||||
sessionCache.put(CacheKey(sessionId, senderKey), it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2) {
|
||||
Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
|
||||
// We want to batch this a bit for performances
|
||||
dirtySession.add(wrapper)
|
||||
|
||||
timerTask?.cancel()
|
||||
timerTask = object : TimerTask() {
|
||||
override fun run() {
|
||||
batchSave()
|
||||
}
|
||||
}
|
||||
timer.schedule(timerTask!!, 2_000)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun batchSave() {
|
||||
val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
|
||||
dirtySession.clear()
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
|
||||
tryOrNull {
|
||||
store.storeInboundGroupSessions(toSave)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.session.SessionScope
|
|||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
|
@ -52,6 +53,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
|||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope) {
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
||||
// we received in the current sync.
|
||||
private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
|
||||
|
@ -64,6 +66,10 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
|||
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
|
||||
}
|
||||
|
||||
fun close() {
|
||||
executor.shutdownNow()
|
||||
}
|
||||
|
||||
// Recently verified devices (map of deviceId and timestamp)
|
||||
private val recentlyVerifiedDevices = HashMap<String, Long>()
|
||||
|
||||
|
@ -99,7 +105,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
|||
fun onGossipingRequestEvent(event: Event) {
|
||||
Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
|
||||
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
|
||||
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||
// val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||
when (roomKeyShare?.action) {
|
||||
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
|
||||
if (event.getClearType() == EventType.REQUEST_SECRET) {
|
||||
|
@ -108,8 +114,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
|||
// ignore, it was sent by me as *
|
||||
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||
} else {
|
||||
// save in DB
|
||||
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||
// // save in DB
|
||||
// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||
receivedGossipingRequests.add(it)
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +125,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
|||
// ignore, it was sent by me as *
|
||||
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
|
||||
} else {
|
||||
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||
// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
|
||||
receivedGossipingRequests.add(it)
|
||||
}
|
||||
}
|
||||
|
@ -144,13 +150,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
|||
fun processReceivedGossipingRequests() {
|
||||
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
|
||||
receivedGossipingRequests.clear()
|
||||
for (request in roomKeyRequestsToProcess) {
|
||||
if (request is IncomingRoomKeyRequest) {
|
||||
processIncomingRoomKeyRequest(request)
|
||||
} else if (request is IncomingSecretShareRequest) {
|
||||
processIncomingSecretShareRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process")
|
||||
|
||||
var receivedRequestCancellations: List<IncomingRequestCancellation>? = null
|
||||
|
||||
|
@ -161,24 +162,35 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
receivedRequestCancellations?.forEach { request ->
|
||||
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
|
||||
// we should probably only notify the app of cancellations we told it
|
||||
// about, but we don't currently have a record of that, so we just pass
|
||||
// everything through.
|
||||
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
|
||||
// ignore remote echo
|
||||
return@forEach
|
||||
executor.execute {
|
||||
cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess)
|
||||
for (request in roomKeyRequestsToProcess) {
|
||||
if (request is IncomingRoomKeyRequest) {
|
||||
processIncomingRoomKeyRequest(request)
|
||||
} else if (request is IncomingSecretShareRequest) {
|
||||
processIncomingSecretShareRequest(request)
|
||||
}
|
||||
}
|
||||
val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
|
||||
if (matchingIncoming == null) {
|
||||
// ignore that?
|
||||
return@forEach
|
||||
} else {
|
||||
// If it was accepted from this device, keep the information, do not mark as cancelled
|
||||
if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
|
||||
onRoomKeyRequestCancellation(request)
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
|
||||
|
||||
receivedRequestCancellations?.forEach { request ->
|
||||
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
|
||||
// we should probably only notify the app of cancellations we told it
|
||||
// about, but we don't currently have a record of that, so we just pass
|
||||
// everything through.
|
||||
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
|
||||
// ignore remote echo
|
||||
return@forEach
|
||||
}
|
||||
val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
|
||||
if (matchingIncoming == null) {
|
||||
// ignore that?
|
||||
return@forEach
|
||||
} else {
|
||||
// If it was accepted from this device, keep the information, do not mark as cancelled
|
||||
if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
|
||||
onRoomKeyRequestCancellation(request)
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,9 @@ internal class MXOlmDevice @Inject constructor(
|
|||
/**
|
||||
* The store where crypto data is saved.
|
||||
*/
|
||||
private val store: IMXCryptoStore) {
|
||||
private val store: IMXCryptoStore,
|
||||
private val inboundGroupSessionStore: InboundGroupSessionStore
|
||||
) {
|
||||
|
||||
/**
|
||||
* @return the Curve25519 key for the account.
|
||||
|
@ -657,7 +659,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
timelineSet.add(messageIndexKey)
|
||||
}
|
||||
|
||||
store.storeInboundGroupSessions(listOf(session))
|
||||
inboundGroupSessionStore.storeInBoundGroupSession(session)
|
||||
val payload = try {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||
|
@ -745,7 +747,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
|
||||
}
|
||||
|
||||
val session = store.getInboundGroupSession(sessionId, senderKey)
|
||||
val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
|
||||
|
||||
if (session != null) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
|
|
|
@ -88,7 +88,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
|||
* @param requestBody requestBody
|
||||
*/
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||
cancelRoomKeyRequest(requestBody, false)
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
|||
* @param requestBody requestBody
|
||||
*/
|
||||
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||
cancelRoomKeyRequest(requestBody, true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,13 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
|
@ -39,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.convertToUTF8
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -54,7 +58,9 @@ internal class MXMegolmEncryption(
|
|||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||
private val taskExecutor: TaskExecutor
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
) : IMXEncrypting {
|
||||
|
||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
||||
|
@ -84,15 +90,18 @@ internal class MXMegolmEncryption(
|
|||
}
|
||||
|
||||
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
|
||||
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
|
||||
devices.forEach { userId, deviceId, withheldCode ->
|
||||
this.add(UserDevice(userId, deviceId) to withheldCode)
|
||||
// offload to computation thread
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
|
||||
devices.forEach { userId, deviceId, withheldCode ->
|
||||
this.add(UserDevice(userId, deviceId) to withheldCode)
|
||||
}
|
||||
}.groupBy(
|
||||
{ it.second },
|
||||
{ it.first }
|
||||
).forEach { (code, targets) ->
|
||||
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
|
||||
}
|
||||
}.groupBy(
|
||||
{ it.second },
|
||||
{ it.first }
|
||||
).forEach { (code, targets) ->
|
||||
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -247,6 +256,15 @@ internal class MXMegolmEncryption(
|
|||
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||
for ((deviceId) in devicesToShareWith) {
|
||||
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = EventType.ROOM_KEY,
|
||||
senderId = credentials.userId,
|
||||
content = submap.apply {
|
||||
this["session_key"] = ""
|
||||
// we add a fake key for trail
|
||||
this["_dest"] = "$userId|$deviceId"
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -420,7 +438,7 @@ internal class MXMegolmEncryption(
|
|||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
true
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
|
||||
Timber.e(failure, "## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
|
@ -26,6 +27,7 @@ import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepo
|
|||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||
|
@ -38,7 +40,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
|||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope) {
|
||||
|
||||
fun create(roomId: String): MXMegolmEncryption {
|
||||
return MXMegolmEncryption(
|
||||
|
@ -52,7 +56,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
|||
sendToDeviceTask,
|
||||
messageEncrypter,
|
||||
warnOnUnknownDevicesRepository,
|
||||
taskExecutor
|
||||
taskExecutor,
|
||||
coroutineDispatchers,
|
||||
cryptoCoroutineScope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||
|
@ -39,15 +41,20 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
|||
import org.matrix.android.sdk.internal.util.withoutPrefix
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.olm.OlmPkSigning
|
||||
import org.matrix.olm.OlmUtility
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultCrossSigningService @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
@SessionId private val sessionId: String,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val initializeCrossSigningTask: InitializeCrossSigningTask,
|
||||
|
@ -55,7 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
private val taskExecutor: TaskExecutor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
|
||||
private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
|
||||
|
||||
private var olmUtility: OlmUtility? = null
|
||||
|
||||
|
@ -360,6 +367,12 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
// First let's get my user key
|
||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
||||
|
||||
checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
|
||||
|
||||
return UserTrustResult.Success
|
||||
}
|
||||
|
||||
fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
|
||||
val myUserKey = myCrossSigningInfo?.userKey()
|
||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
||||
|
||||
|
@ -368,15 +381,15 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
}
|
||||
|
||||
// Let's get the other user master key
|
||||
val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey()
|
||||
?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId)
|
||||
val otherMasterKey = otherInfo?.masterKey()
|
||||
?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
|
||||
|
||||
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
|
||||
?.get(userId) // Signatures made by me
|
||||
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
|
||||
|
||||
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
|
||||
Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey")
|
||||
Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey")
|
||||
return UserTrustResult.KeyNotSigned(otherMasterKey)
|
||||
}
|
||||
|
||||
|
@ -396,6 +409,15 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
||||
|
||||
return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId))
|
||||
}
|
||||
|
||||
fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
|
||||
// Special case when it's me,
|
||||
// I have to check that MSK -> USK -> SSK
|
||||
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
||||
// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
||||
|
||||
val myMasterKey = myCrossSigningInfo?.masterKey()
|
||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
||||
|
||||
|
@ -423,7 +445,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
// Maybe it's signed by a locally trusted device?
|
||||
myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
|
||||
val potentialDeviceId = key.withoutPrefix("ed25519:")
|
||||
val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId)
|
||||
val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
|
||||
if (potentialDevice != null && potentialDevice.isVerified) {
|
||||
// Check signature validity?
|
||||
try {
|
||||
|
@ -561,6 +583,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
||||
checkSelfTrust()
|
||||
// re-verify all trusts
|
||||
onUsersDeviceUpdate(listOf(userId))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -666,6 +690,55 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
||||
}
|
||||
|
||||
fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult {
|
||||
val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
|
||||
myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
|
||||
|
||||
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
|
||||
|
||||
otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId))
|
||||
|
||||
// TODO should we force verification ?
|
||||
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
|
||||
|
||||
// Check if the trust chain is valid
|
||||
/*
|
||||
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
||||
* ┃ ALICE ┃ ┃ BOB ┃
|
||||
* ┗━━━━━━━━┛ ┗━━━━━━━━┛
|
||||
* MSK ┌────────────▶MSK
|
||||
* │
|
||||
* │ │ │
|
||||
* │ SSK │ └──▶ SSK ──────────────────┐
|
||||
* │ │ │
|
||||
* │ │ USK │
|
||||
* └──▶ USK ────────────┘ (not visible by │
|
||||
* Alice) │
|
||||
* ▼
|
||||
* ┌──────────────┐
|
||||
* │ BOB's Device │
|
||||
* └──────────────┘
|
||||
*/
|
||||
|
||||
val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
|
||||
?: return legacyFallbackTrust(
|
||||
locallyTrusted,
|
||||
DeviceTrustResult.MissingDeviceSignature(otherDevice.deviceId, otherKeys.selfSigningKey()
|
||||
?.unpaddedBase64PublicKey
|
||||
?: ""
|
||||
)
|
||||
)
|
||||
|
||||
// Check bob's device is signed by bob's SSK
|
||||
try {
|
||||
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
|
||||
} catch (e: Throwable) {
|
||||
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
|
||||
}
|
||||
|
||||
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
||||
}
|
||||
|
||||
private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
|
||||
return if (locallyTrusted == true) {
|
||||
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
|
||||
|
@ -675,36 +748,18 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onUsersDeviceUpdate(userIds: List<String>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
|
||||
userIds.forEach { otherUserId ->
|
||||
checkUserTrust(otherUserId).let {
|
||||
Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
|
||||
setUserKeysAsTrusted(otherUserId, it.isVerified())
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds")
|
||||
val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds)
|
||||
val workerData = WorkerParamsFactory.toData(workerParams)
|
||||
|
||||
// now check device trust
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
userIds.forEach { otherUserId ->
|
||||
// TODO if my keys have changes, i should recheck all devices of all users?
|
||||
val devices = cryptoStore.getUserDeviceList(otherUserId)
|
||||
devices?.forEach { device ->
|
||||
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
||||
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
||||
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
||||
}
|
||||
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
|
||||
.setInputData(workerData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
if (otherUserId == userId) {
|
||||
// It's me, i should check if a newly trusted device is signing my master key
|
||||
// In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
|
||||
setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
|
||||
}
|
||||
}
|
||||
|
||||
eventBus.post(CryptoToSessionUserTrustChange(userIds))
|
||||
}
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
|
||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.util.createBackgroundHandler
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.android.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ShieldTrustUpdater @Inject constructor(
|
||||
private val eventBus: EventBus,
|
||||
private val computeTrustTask: ComputeTrustTask,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
@SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater
|
||||
) : SessionLifecycleObserver {
|
||||
|
||||
companion object {
|
||||
private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
|
||||
private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher()
|
||||
}
|
||||
|
||||
private val backgroundSessionRealm = AtomicReference<Realm>()
|
||||
|
||||
private val isStarted = AtomicBoolean()
|
||||
|
||||
override fun onStart() {
|
||||
if (isStarted.compareAndSet(false, true)) {
|
||||
eventBus.register(this)
|
||||
BACKGROUND_HANDLER.post {
|
||||
backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
if (isStarted.compareAndSet(true, false)) {
|
||||
eventBus.unregister(this)
|
||||
BACKGROUND_HANDLER.post {
|
||||
backgroundSessionRealm.getAndSet(null).also {
|
||||
it?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) {
|
||||
if (!isStarted.get()) {
|
||||
return
|
||||
}
|
||||
taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
|
||||
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect))
|
||||
// We need to send that back to session base
|
||||
backgroundSessionRealm.get()?.executeTransaction { realm ->
|
||||
roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
fun onTrustUpdate(update: CryptoToSessionUserTrustChange) {
|
||||
if (!isStarted.get()) {
|
||||
return
|
||||
}
|
||||
onCryptoDevicesChange(update.userIds)
|
||||
}
|
||||
|
||||
private fun onCryptoDevicesChange(users: List<String>) {
|
||||
taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
|
||||
val realm = backgroundSessionRealm.get() ?: return@launch
|
||||
val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
|
||||
.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
|
||||
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
|
||||
.findAll()
|
||||
.map { it.roomId }
|
||||
|
||||
distinctRoomIds.forEach { roomId ->
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
if (roomSummary?.isEncrypted.orFalse()) {
|
||||
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
||||
try {
|
||||
val updatedTrust = computeTrustTask.execute(
|
||||
ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true)
|
||||
)
|
||||
realm.executeTransaction {
|
||||
roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,322 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class UpdateTrustWorker(context: Context,
|
||||
params: WorkerParameters)
|
||||
: SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
override val lastFailureMessage: String? = null,
|
||||
val updatedUserIds: List<String>
|
||||
) : SessionWorkerParams
|
||||
|
||||
@Inject lateinit var crossSigningService: DefaultCrossSigningService
|
||||
|
||||
// It breaks the crypto store contract, but we need to batch things :/
|
||||
@CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
|
||||
@UserId @Inject lateinit var myUserId: String
|
||||
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
|
||||
@SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
|
||||
|
||||
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
|
||||
@Inject lateinit var cryptoStore: IMXCryptoStore
|
||||
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
var userList = params.updatedUserIds
|
||||
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
|
||||
// or a new device?) So we check all again :/
|
||||
|
||||
Timber.d("## CrossSigning - Updating trust for $userList")
|
||||
|
||||
// First we check that the users MSK are trusted by mine
|
||||
// After that we check the trust chain for each devices of each users
|
||||
Realm.getInstance(realmConfiguration).use { realm ->
|
||||
realm.executeTransaction {
|
||||
// By mapping here to model, this object is not live
|
||||
// I should update it if needed
|
||||
var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
|
||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||
|
||||
var myTrustResult: UserTrustResult? = null
|
||||
|
||||
if (userList.contains(myUserId)) {
|
||||
Timber.d("## CrossSigning - Clear all trust as a change on my user was detected")
|
||||
// i am in the list.. but i don't know exactly the delta of change :/
|
||||
// If it's my cross signing keys we should refresh all trust
|
||||
// do it anyway ?
|
||||
userList = realm.where(CrossSigningInfoEntity::class.java)
|
||||
.findAll().mapNotNull { it.userId }
|
||||
Timber.d("## CrossSigning - Updating trust for all $userList")
|
||||
|
||||
// check right now my keys and mark it as trusted as other trust depends on it
|
||||
val myDevices = realm.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, myUserId)
|
||||
.findFirst()
|
||||
?.devices
|
||||
?.map { deviceInfo ->
|
||||
CryptoMapper.mapToModel(deviceInfo)
|
||||
}
|
||||
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also {
|
||||
updateCrossSigningKeysTrust(realm, myUserId, it.isVerified())
|
||||
// update model reference
|
||||
myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
|
||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val otherInfos = userList.map {
|
||||
it to realm.where(CrossSigningInfoEntity::class.java)
|
||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
|
||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||
}
|
||||
.toMap()
|
||||
|
||||
val trusts = otherInfos.map { infoEntry ->
|
||||
infoEntry.key to when (infoEntry.key) {
|
||||
myUserId -> myTrustResult
|
||||
else -> {
|
||||
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also {
|
||||
Timber.d("## CrossSigning - user:${infoEntry.key} result:$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
// TODO! if it's me and my keys has changed... I have to reset trust for everyone!
|
||||
// i have all the new trusts, update DB
|
||||
trusts.forEach {
|
||||
val verified = it.value?.isVerified() == true
|
||||
updateCrossSigningKeysTrust(realm, it.key, verified)
|
||||
}
|
||||
|
||||
// Ok so now we have to check device trust for all these users..
|
||||
Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}")
|
||||
trusts.keys.forEach {
|
||||
val devicesEntities = realm.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, it)
|
||||
.findFirst()
|
||||
?.devices
|
||||
|
||||
val trustMap = devicesEntities?.map { device ->
|
||||
// get up to date from DB has could have been updated
|
||||
val otherInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, it)
|
||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||
device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
|
||||
}?.toMap()
|
||||
|
||||
// Update trust if needed
|
||||
devicesEntities?.forEach { device ->
|
||||
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
|
||||
Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
|
||||
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
|
||||
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
|
||||
// need to save
|
||||
val trustEntity = device.trustLevelEntity
|
||||
if (trustEntity == null) {
|
||||
realm.createObject(TrustLevelEntity::class.java).let {
|
||||
it.locallyVerified = false
|
||||
it.crossSignedVerified = crossSignedVerified
|
||||
device.trustLevelEntity = it
|
||||
}
|
||||
} else {
|
||||
trustEntity.crossSignedVerified = crossSignedVerified
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// So Cross Signing keys trust is updated, device trust is updated
|
||||
// We can now update room shields? in the session DB?
|
||||
|
||||
Timber.d("## CrossSigning - Updating shields for impacted rooms...")
|
||||
Realm.getInstance(sessionRealmConfiguration).use { it ->
|
||||
it.executeTransaction { realm ->
|
||||
val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
|
||||
.`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
|
||||
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
|
||||
.findAll()
|
||||
.map { it.roomId }
|
||||
Timber.d("## CrossSigning - ... impacted rooms $distinctRoomIds")
|
||||
distinctRoomIds.forEach { roomId ->
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
if (roomSummary?.isEncrypted == true) {
|
||||
Timber.d("## CrossSigning - Check shield state for room $roomId")
|
||||
val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
||||
try {
|
||||
val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary)
|
||||
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
|
||||
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
|
||||
roomSummary.roomEncryptionTrustLevel = updatedTrust
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) {
|
||||
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
|
||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
||||
.findFirst()
|
||||
xInfoEntity?.crossSigningKeys?.forEach { info ->
|
||||
// optimization to avoid trigger updates when there is no change..
|
||||
if (info.trustLevelEntity?.isVerified() != verified) {
|
||||
Timber.d("## CrossSigning - Trust change for $userId : $verified")
|
||||
val level = info.trustLevelEntity
|
||||
if (level == null) {
|
||||
val newLevel = realm.createObject(TrustLevelEntity::class.java)
|
||||
newLevel.locallyVerified = verified
|
||||
newLevel.crossSignedVerified = verified
|
||||
info.trustLevelEntity = newLevel
|
||||
} else {
|
||||
level.locallyVerified = verified
|
||||
level.crossSignedVerified = verified
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
|
||||
Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds")
|
||||
// The set of “all users” depends on the type of room:
|
||||
// For regular / topic rooms, all users including yourself, are considered when decorating a room
|
||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
||||
val listToCheck = if (roomSummaryEntity.isDirect) {
|
||||
activeMemberUserIds.filter { it != myUserId }
|
||||
} else {
|
||||
activeMemberUserIds
|
||||
}
|
||||
|
||||
val allTrustedUserIds = listToCheck
|
||||
.filter { userId ->
|
||||
Realm.getInstance(realmConfiguration).use {
|
||||
it.where(CrossSigningInfoEntity::class.java)
|
||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }?.isTrusted() == true
|
||||
}
|
||||
}
|
||||
val myCrossKeys = Realm.getInstance(realmConfiguration).use {
|
||||
it.where(CrossSigningInfoEntity::class.java)
|
||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
|
||||
.findFirst()?.let { mapCrossSigningInfoEntity(it) }
|
||||
}
|
||||
|
||||
return if (allTrustedUserIds.isEmpty()) {
|
||||
RoomEncryptionTrustLevel.Default
|
||||
} else {
|
||||
// If one of the verified user as an untrusted device -> warning
|
||||
// If all devices of all verified users are trusted -> green
|
||||
// else -> black
|
||||
allTrustedUserIds
|
||||
.mapNotNull { uid ->
|
||||
Realm.getInstance(realmConfiguration).use {
|
||||
it.where<UserEntity>()
|
||||
.equalTo(UserEntityFields.USER_ID, uid)
|
||||
.findFirst()
|
||||
?.devices
|
||||
?.map {
|
||||
CryptoMapper.mapToModel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
.let { allDevices ->
|
||||
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }}")
|
||||
if (myCrossKeys != null) {
|
||||
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
|
||||
} else {
|
||||
// Legacy method
|
||||
allDevices.any { !it.isVerified }
|
||||
}
|
||||
}
|
||||
.let { hasWarning ->
|
||||
if (hasWarning) {
|
||||
RoomEncryptionTrustLevel.Warning
|
||||
} else {
|
||||
if (listToCheck.size == allTrustedUserIds.size) {
|
||||
// all users are trusted and all devices are verified
|
||||
RoomEncryptionTrustLevel.Trusted
|
||||
} else {
|
||||
RoomEncryptionTrustLevel.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
|
||||
val userId = xsignInfo.userId ?: ""
|
||||
return MXCrossSigningInfo(
|
||||
userId = userId,
|
||||
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
||||
crossSigningKeysMapper.map(userId, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
}
|
|
@ -234,7 +234,10 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
this.callback = object : MatrixCallback<KeysVersion> {
|
||||
override fun onSuccess(data: KeysVersion) {
|
||||
// Reset backup markers.
|
||||
cryptoStore.resetBackupMarkers()
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
// move tx out of UI thread
|
||||
cryptoStore.resetBackupMarkers()
|
||||
}
|
||||
|
||||
val keyBackupVersion = KeysVersionResult(
|
||||
algorithm = createKeysBackupVersionBody.algorithm,
|
||||
|
@ -596,7 +599,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
val importResult = awaitCallback<ImportRoomKeysResult> {
|
||||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
|
||||
}
|
||||
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
|
||||
}
|
||||
Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
|
||||
} else {
|
||||
Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
|
||||
|
@ -685,7 +690,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
// Get backed up keys from the homeserver
|
||||
val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
|
||||
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
withContext(coroutineDispatchers.computation) {
|
||||
val sessionsData = ArrayList<MegolmSessionData>()
|
||||
// Restore that data
|
||||
var sessionsFromHsCount = 0
|
||||
|
@ -1123,7 +1128,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
|
||||
if (retrievedMegolmBackupAuthData != null) {
|
||||
keysBackupVersion = keysVersionResult
|
||||
cryptoStore.setKeyBackupVersion(keysVersionResult.version)
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoStore.setKeyBackupVersion(keysVersionResult.version)
|
||||
}
|
||||
|
||||
onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.crypto.store
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
@ -126,7 +127,10 @@ internal interface IMXCryptoStore {
|
|||
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
|
||||
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
|
||||
|
||||
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
|
||||
|
||||
fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>)
|
||||
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
|
||||
|
||||
/**
|
||||
|
@ -364,6 +368,7 @@ internal interface IMXCryptoStore {
|
|||
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
|
||||
|
||||
fun saveGossipingEvent(event: Event)
|
||||
fun saveGossipingEvents(events: List<Event>)
|
||||
|
||||
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
|
||||
updateGossipingRequestState(
|
||||
|
@ -441,10 +446,13 @@ internal interface IMXCryptoStore {
|
|||
// Dev tools
|
||||
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
|
||||
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
||||
fun getGossipingEvents(): List<Event>
|
||||
|
||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||
fun getDeviceKeysUploaded(): Boolean
|
||||
|
|
|
@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
|
@ -62,6 +64,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFie
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
|
@ -998,7 +1001,50 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): List<Event> {
|
||||
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm.where<IncomingGossipingRequestEntity>()
|
||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||
?: IncomingRoomKeyRequest(
|
||||
requestBody = null,
|
||||
deviceId = "",
|
||||
userId = "",
|
||||
requestId = "",
|
||||
state = GossipingRequestState.NONE,
|
||||
localCreationTimestamp = 0
|
||||
)
|
||||
}
|
||||
return monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||
LivePagedListBuilder(dataSourceFactory,
|
||||
PagedList.Config.Builder()
|
||||
.setPageSize(20)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPrefetchDistance(1)
|
||||
.build())
|
||||
)
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm.where<GossipingEventEntity>().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map { it.toModel() }
|
||||
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||
LivePagedListBuilder(dataSourceFactory,
|
||||
PagedList.Config.Builder()
|
||||
.setPageSize(20)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPrefetchDistance(1)
|
||||
.build())
|
||||
)
|
||||
return trail
|
||||
}
|
||||
|
||||
override fun getGossipingEvents(): List<Event> {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<GossipingEventEntity>()
|
||||
}.map {
|
||||
|
@ -1066,24 +1112,43 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
return request
|
||||
}
|
||||
|
||||
override fun saveGossipingEvent(event: Event) {
|
||||
override fun saveGossipingEvents(events: List<Event>) {
|
||||
val now = System.currentTimeMillis()
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||
val entity = GossipingEventEntity(
|
||||
type = event.type,
|
||||
sender = event.senderId,
|
||||
ageLocalTs = ageLocalTs,
|
||||
content = ContentMapper.map(event.content)
|
||||
).apply {
|
||||
sendState = SendState.SYNCED
|
||||
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||
decryptionErrorCode = event.mCryptoError?.name
|
||||
}
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.insertOrUpdate(entity)
|
||||
monarchy.writeAsync { realm ->
|
||||
events.forEach { event ->
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||
val entity = GossipingEventEntity(
|
||||
type = event.type,
|
||||
sender = event.senderId,
|
||||
ageLocalTs = ageLocalTs,
|
||||
content = ContentMapper.map(event.content)
|
||||
).apply {
|
||||
sendState = SendState.SYNCED
|
||||
decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||
decryptionErrorCode = event.mCryptoError?.name
|
||||
}
|
||||
realm.insertOrUpdate(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveGossipingEvent(event: Event) {
|
||||
monarchy.writeAsync { realm ->
|
||||
val now = System.currentTimeMillis()
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
||||
val entity = GossipingEventEntity(
|
||||
type = event.type,
|
||||
sender = event.senderId,
|
||||
ageLocalTs = ageLocalTs,
|
||||
content = ContentMapper.map(event.content)
|
||||
).apply {
|
||||
sendState = SendState.SYNCED
|
||||
decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
||||
decryptionErrorCode = event.mCryptoError?.name
|
||||
}
|
||||
realm.insertOrUpdate(entity)
|
||||
}
|
||||
}
|
||||
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
|
||||
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
||||
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||
|
@ -1284,6 +1349,28 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>) {
|
||||
doRealmTransactionAsync(realmConfiguration) { realm ->
|
||||
requests.forEach { request ->
|
||||
// After a clear cache, we might have a
|
||||
realm.createObject(IncomingGossipingRequestEntity::class.java).let {
|
||||
it.otherDeviceId = request.deviceId
|
||||
it.otherUserId = request.userId
|
||||
it.requestId = request.requestId ?: ""
|
||||
it.requestState = GossipingRequestState.PENDING
|
||||
it.localCreationTimestamp = request.localCreationTimestamp ?: System.currentTimeMillis()
|
||||
if (request is IncomingSecretShareRequest) {
|
||||
it.type = GossipRequestType.SECRET
|
||||
it.requestedInfoStr = request.secretName
|
||||
} else if (request is IncomingRoomKeyRequest) {
|
||||
it.type = GossipRequestType.KEY
|
||||
it.requestedInfoStr = request.requestBody?.toJson()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> {
|
||||
// return doRealmQueryAndCopyList(realmConfiguration) {
|
||||
// it.where<GossipingEventEntity>()
|
||||
|
@ -1417,6 +1504,27 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
.filterNotNull()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm
|
||||
.where(OutgoingGossipingRequestEntity::class.java)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
|
||||
}
|
||||
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||
LivePagedListBuilder(dataSourceFactory,
|
||||
PagedList.Config.Builder()
|
||||
.setPageSize(20)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPrefetchDistance(1)
|
||||
.build())
|
||||
)
|
||||
return trail
|
||||
}
|
||||
|
||||
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
|
||||
|
|
|
@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
|
|||
import org.matrix.android.sdk.internal.di.SerializeNulls
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import io.realm.RealmObjectSchema
|
||||
import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -57,6 +58,27 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||
const val CRYPTO_STORE_SCHEMA_VERSION = 11L
|
||||
}
|
||||
|
||||
private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
|
||||
if (!hasField(fieldName)) {
|
||||
addField(fieldName, fieldType)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun RealmObjectSchema.removeFieldIfExists(fieldName: String): RealmObjectSchema {
|
||||
if (hasField(fieldName)) {
|
||||
removeField(fieldName)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun RealmObjectSchema.setRequiredIfNotAlready(fieldName: String, isRequired: Boolean): RealmObjectSchema {
|
||||
if (isRequired != isRequired(fieldName)) {
|
||||
setRequired(fieldName, isRequired)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||
|
||||
|
@ -89,13 +111,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
|
||||
|
||||
realm.schema.get("IncomingRoomKeyRequestEntity")
|
||||
?.addField("requestBodyAlgorithm", String::class.java)
|
||||
?.addField("requestBodyRoomId", String::class.java)
|
||||
?.addField("requestBodySenderKey", String::class.java)
|
||||
?.addField("requestBodySessionId", String::class.java)
|
||||
?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
|
||||
?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
|
||||
?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
|
||||
?.addFieldIfNotExists("requestBodySessionId", String::class.java)
|
||||
?.transform { dynamicObject ->
|
||||
val requestBodyString = dynamicObject.getString("requestBodyString")
|
||||
try {
|
||||
val requestBodyString = dynamicObject.getString("requestBodyString")
|
||||
// It was a map before
|
||||
val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
|
||||
|
||||
|
@ -109,18 +131,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||
Timber.e(e, "Error")
|
||||
}
|
||||
}
|
||||
?.removeField("requestBodyString")
|
||||
?.removeFieldIfExists("requestBodyString")
|
||||
|
||||
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
|
||||
|
||||
realm.schema.get("OutgoingRoomKeyRequestEntity")
|
||||
?.addField("requestBodyAlgorithm", String::class.java)
|
||||
?.addField("requestBodyRoomId", String::class.java)
|
||||
?.addField("requestBodySenderKey", String::class.java)
|
||||
?.addField("requestBodySessionId", String::class.java)
|
||||
?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
|
||||
?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
|
||||
?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
|
||||
?.addFieldIfNotExists("requestBodySessionId", String::class.java)
|
||||
?.transform { dynamicObject ->
|
||||
val requestBodyString = dynamicObject.getString("requestBodyString")
|
||||
try {
|
||||
val requestBodyString = dynamicObject.getString("requestBodyString")
|
||||
// It was a map before
|
||||
val map: Map<String, String>? = deserializeFromRealm(requestBodyString)
|
||||
|
||||
|
@ -134,16 +156,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||
Timber.e(e, "Error")
|
||||
}
|
||||
}
|
||||
?.removeField("requestBodyString")
|
||||
?.removeFieldIfExists("requestBodyString")
|
||||
|
||||
Timber.d("Create KeysBackupDataEntity")
|
||||
|
||||
realm.schema.create("KeysBackupDataEntity")
|
||||
.addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
|
||||
.addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
|
||||
.setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
|
||||
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
|
||||
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
|
||||
if (!realm.schema.contains("KeysBackupDataEntity")) {
|
||||
realm.schema.create("KeysBackupDataEntity")
|
||||
.addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
|
||||
.addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
|
||||
.setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
|
||||
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
|
||||
.addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateTo3RiotX(realm: DynamicRealm) {
|
||||
|
@ -151,8 +175,8 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||
Timber.d("Migrate to RiotX model")
|
||||
|
||||
realm.schema.get("CryptoRoomEntity")
|
||||
?.addField(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
|
||||
?.setRequired(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
|
||||
?.addFieldIfNotExists(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
|
||||
?.setRequiredIfNotAlready(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
|
||||
|
||||
// Convert format of MXDeviceInfo, package has to be the same.
|
||||
realm.schema.get("DeviceInfoEntity")
|
||||
|
@ -204,8 +228,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||
// Version 4L added Cross Signing info persistence
|
||||
private fun migrateTo4(realm: DynamicRealm) {
|
||||
Timber.d("Step 3 -> 4")
|
||||
Timber.d("Create KeyInfoEntity")
|
||||
|
||||
if (realm.schema.contains("TrustLevelEntity")) {
|
||||
Timber.d("Skipping Step 3 -> 4 because entities already exist")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d("Create KeyInfoEntity")
|
||||
val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity")
|
||||
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
|
||||
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
|
||||
|
|
|
@ -17,8 +17,13 @@ package org.matrix.android.sdk.internal.crypto.tasks
|
|||
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
|
@ -28,23 +33,23 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
|
|||
data class Params(val roomId: String,
|
||||
val event: Event,
|
||||
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
|
||||
val keepKeys: List<String>? = null,
|
||||
val crypto: CryptoService
|
||||
val keepKeys: List<String>? = null
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultEncryptEventTask @Inject constructor(
|
||||
// private val crypto: CryptoService
|
||||
private val localEchoRepository: LocalEchoRepository
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val cryptoService: CryptoService
|
||||
) : EncryptEventTask {
|
||||
override suspend fun execute(params: EncryptEventTask.Params): Event {
|
||||
if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
|
||||
// don't want to wait for any query
|
||||
// if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
|
||||
val localEvent = params.event
|
||||
if (localEvent.eventId == null) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
|
||||
localEchoRepository.updateSendState(localEvent.eventId, localEvent.roomId, SendState.ENCRYPTING)
|
||||
|
||||
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
|
||||
params.keepKeys?.forEach {
|
||||
|
@ -52,8 +57,9 @@ internal class DefaultEncryptEventTask @Inject constructor(
|
|||
}
|
||||
|
||||
// try {
|
||||
// let it throws
|
||||
awaitCallback<MXEncryptEventContentResult> {
|
||||
params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
||||
cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
||||
}.let { result ->
|
||||
val modifiedContent = HashMap(result.eventContent)
|
||||
params.keepKeys?.forEach { toKeep ->
|
||||
|
@ -63,18 +69,34 @@ internal class DefaultEncryptEventTask @Inject constructor(
|
|||
}
|
||||
}
|
||||
val safeResult = result.copy(eventContent = modifiedContent)
|
||||
// Better handling of local echo, to avoid decrypting transition on remote echo
|
||||
// Should I only do it for text messages?
|
||||
val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
MXEventDecryptionResult(
|
||||
clearEvent = Event(
|
||||
type = localEvent.type,
|
||||
content = localEvent.content,
|
||||
roomId = localEvent.roomId
|
||||
).toContent(),
|
||||
forwardingCurve25519KeyChain = emptyList(),
|
||||
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
||||
claimedEd25519Key = cryptoService.getMyDevice().fingerprint()
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho ->
|
||||
localEcho.type = EventType.ENCRYPTED
|
||||
localEcho.content = ContentMapper.map(modifiedContent)
|
||||
decryptionLocalEcho?.also {
|
||||
localEcho.setDecryptionResult(it)
|
||||
}
|
||||
}
|
||||
return localEvent.copy(
|
||||
type = safeResult.eventType,
|
||||
content = safeResult.eventContent
|
||||
)
|
||||
}
|
||||
// } catch (throwable: Throwable) {
|
||||
// val sendState = when (throwable) {
|
||||
// is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
|
||||
// else -> SendState.UNDELIVERED
|
||||
// }
|
||||
// localEchoUpdater.updateSendState(localEvent.eventId, sendState)
|
||||
// throw throwable
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.tasks
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.session.room.send.SendResponse
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface RedactEventTask : Task<RedactEventTask.Params, String> {
|
||||
data class Params(
|
||||
val txID: String,
|
||||
val roomId: String,
|
||||
val eventId: String,
|
||||
val reason: String?
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultRedactEventTask @Inject constructor(
|
||||
private val roomAPI: RoomAPI,
|
||||
private val eventBus: EventBus) : RedactEventTask {
|
||||
|
||||
override suspend fun execute(params: RedactEventTask.Params): String {
|
||||
val executeRequest = executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.redactEvent(
|
||||
txId = params.txID,
|
||||
roomId = params.roomId,
|
||||
eventId = params.eventId,
|
||||
reason = if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
|
||||
)
|
||||
}
|
||||
return executeRequest.eventId
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.tasks
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
|
@ -23,13 +23,12 @@ import org.matrix.android.sdk.internal.session.room.RoomAPI
|
|||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import org.matrix.android.sdk.internal.session.room.send.SendResponse
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SendEventTask : Task<SendEventTask.Params, String> {
|
||||
data class Params(
|
||||
val event: Event,
|
||||
val cryptoService: CryptoService?
|
||||
val encrypt: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -40,11 +39,11 @@ internal class DefaultSendEventTask @Inject constructor(
|
|||
private val eventBus: EventBus) : SendEventTask {
|
||||
|
||||
override suspend fun execute(params: SendEventTask.Params): String {
|
||||
val event = handleEncryption(params)
|
||||
val localId = event.eventId!!
|
||||
|
||||
try {
|
||||
localEchoRepository.updateSendState(localId, SendState.SENDING)
|
||||
val event = handleEncryption(params)
|
||||
val localId = event.eventId!!
|
||||
|
||||
localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING)
|
||||
val executeRequest = executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.send(
|
||||
localId,
|
||||
|
@ -53,26 +52,22 @@ internal class DefaultSendEventTask @Inject constructor(
|
|||
eventType = event.type
|
||||
)
|
||||
}
|
||||
localEchoRepository.updateSendState(localId, SendState.SENT)
|
||||
localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT)
|
||||
return executeRequest.eventId
|
||||
} catch (e: Throwable) {
|
||||
localEchoRepository.updateSendState(localId, SendState.UNDELIVERED)
|
||||
// localEchoRepository.updateSendState(params.event.eventId!!, SendState.UNDELIVERED)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Throws
|
||||
private suspend fun handleEncryption(params: SendEventTask.Params): Event {
|
||||
if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
|
||||
try {
|
||||
return encryptEventTask.execute(EncryptEventTask.Params(
|
||||
params.event.roomId ?: "",
|
||||
params.event,
|
||||
listOf("m.relates_to"),
|
||||
params.cryptoService
|
||||
))
|
||||
} catch (throwable: Throwable) {
|
||||
// We said it's ok to send verification request in clear
|
||||
}
|
||||
if (params.encrypt && !params.event.isEncrypted()) {
|
||||
return encryptEventTask.execute(EncryptEventTask.Params(
|
||||
params.event.roomId ?: "",
|
||||
params.event,
|
||||
listOf("m.relates_to")
|
||||
))
|
||||
}
|
||||
return params.event
|
||||
}
|
||||
|
|
|
@ -15,21 +15,20 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.tasks
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import org.matrix.android.sdk.internal.session.room.send.SendResponse
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
|
||||
data class Params(
|
||||
val event: Event,
|
||||
val cryptoService: CryptoService?
|
||||
val event: Event
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -37,6 +36,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
|||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val encryptEventTask: DefaultEncryptEventTask,
|
||||
private val roomAPI: RoomAPI,
|
||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||
private val eventBus: EventBus) : SendVerificationMessageTask {
|
||||
|
||||
override suspend fun execute(params: SendVerificationMessageTask.Params): String {
|
||||
|
@ -44,7 +44,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
|||
val localId = event.eventId!!
|
||||
|
||||
try {
|
||||
localEchoRepository.updateSendState(localId, SendState.SENDING)
|
||||
localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING)
|
||||
val executeRequest = executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.send(
|
||||
localId,
|
||||
|
@ -53,22 +53,21 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
|||
eventType = event.type
|
||||
)
|
||||
}
|
||||
localEchoRepository.updateSendState(localId, SendState.SENT)
|
||||
localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT)
|
||||
return executeRequest.eventId
|
||||
} catch (e: Throwable) {
|
||||
localEchoRepository.updateSendState(localId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event {
|
||||
if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
|
||||
if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) {
|
||||
try {
|
||||
return encryptEventTask.execute(EncryptEventTask.Params(
|
||||
params.event.roomId ?: "",
|
||||
params.event,
|
||||
listOf("m.relates_to"),
|
||||
params.cryptoService
|
||||
listOf("m.relates_to")
|
||||
))
|
||||
} catch (throwable: Throwable) {
|
||||
// We said it's ok to send verification request in clear
|
||||
|
|
|
@ -20,7 +20,6 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import dagger.Lazy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
|
@ -111,9 +110,6 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
// Cannot be injected in constructor as it creates a dependency cycle
|
||||
lateinit var cryptoService: CryptoService
|
||||
|
||||
// map [sender : [transaction]]
|
||||
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
||||
|
||||
|
@ -129,7 +125,8 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
|
||||
// Event received from the sync
|
||||
fun onToDeviceEvent(event: Event) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.d("## SAS onToDeviceEvent ${event.getClearType()}")
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START -> {
|
||||
onStartRequestReceived(event)
|
||||
|
@ -163,7 +160,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
fun onRoomEvent(event: Event) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START -> {
|
||||
onRoomStartRequestReceived(event)
|
||||
|
@ -240,6 +237,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
}
|
||||
|
||||
private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
|
||||
Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}")
|
||||
uiHandler.post {
|
||||
listeners.forEach {
|
||||
try {
|
||||
|
@ -303,11 +301,14 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
// We don't want to block here
|
||||
val otherDeviceId = validRequestInfo.fromDevice
|
||||
|
||||
Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}")
|
||||
|
||||
cryptoCoroutineScope.launch {
|
||||
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
|
||||
Timber.e("## Verification device $otherDeviceId is not known")
|
||||
}
|
||||
}
|
||||
Timber.v("## SAS onRequestReceived .. checkKeysAreDownloaded launched")
|
||||
|
||||
// Remember this request
|
||||
val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
|
||||
|
@ -1203,7 +1204,9 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
// TODO refactor this with the DM one
|
||||
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
|
||||
|
||||
val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId }
|
||||
val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId)
|
||||
?.values?.map { it.deviceId } ?: emptyList()
|
||||
|
||||
val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
|
||||
|
||||
val transport = verificationTransportToDeviceFactory.createTransport(null)
|
||||
|
|
|
@ -20,7 +20,6 @@ import androidx.work.Data
|
|||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||
|
@ -47,7 +46,6 @@ internal class SendVerificationMessageWorker(context: Context,
|
|||
|
||||
@Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
@Inject lateinit var cryptoService: CryptoService
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
|
@ -70,8 +68,7 @@ internal class SendVerificationMessageWorker(context: Context,
|
|||
return try {
|
||||
val resultEventId = sendVerificationMessageTask.execute(
|
||||
SendVerificationMessageTask.Params(
|
||||
event = localEvent,
|
||||
cryptoService = cryptoService
|
||||
event = localEvent
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.verification
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
@ -34,12 +33,13 @@ import org.matrix.android.sdk.internal.di.DeviceId
|
|||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||
import timber.log.Timber
|
||||
import java.util.ArrayList
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class VerificationMessageProcessor @Inject constructor(
|
||||
private val cryptoService: CryptoService,
|
||||
private val eventDecryptor: EventDecryptor,
|
||||
private val verificationService: DefaultVerificationService,
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?
|
||||
|
@ -82,7 +82,7 @@ internal class VerificationMessageProcessor @Inject constructor(
|
|||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
val result = eventDecryptor.decryptEvent(event, "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
|
|
|
@ -17,10 +17,6 @@
|
|||
package org.matrix.android.sdk.internal.database
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
|
||||
|
@ -31,12 +27,13 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
|||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
|
||||
private val cryptoService: CryptoService)
|
||||
private val eventDecryptor: EventDecryptor)
|
||||
: RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventInsertEntity> {
|
||||
|
@ -74,7 +71,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
|
|||
return@forEach
|
||||
}
|
||||
val domainEvent = event.asDomain()
|
||||
decryptIfNeeded(domainEvent)
|
||||
// decryptIfNeeded(domainEvent)
|
||||
processors.filter {
|
||||
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
|
||||
}.forEach {
|
||||
|
@ -89,22 +86,22 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
|
|||
}
|
||||
}
|
||||
|
||||
private fun decryptIfNeeded(event: Event) {
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v("Failed to decrypt event")
|
||||
// TODO -> we should keep track of this and retry, or some processing will never be handled
|
||||
}
|
||||
}
|
||||
}
|
||||
// private fun decryptIfNeeded(event: Event) {
|
||||
// if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
// try {
|
||||
// val result = eventDecryptor.decryptEvent(event, event.roomId ?: "")
|
||||
// event.mxDecryptionResult = OlmDecryptionResult(
|
||||
// payload = result.clearEvent,
|
||||
// senderKey = result.senderCurve25519Key,
|
||||
// keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
// forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
// )
|
||||
// } catch (e: MXCryptoError) {
|
||||
// Timber.v("Failed to decrypt event")
|
||||
// // TODO -> we should keep track of this and retry, or some processing will never be handled
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
|
||||
return processors.any {
|
||||
|
|
|
@ -64,7 +64,7 @@ import org.matrix.android.sdk.internal.di.SessionId
|
|||
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
||||
import org.matrix.android.sdk.internal.session.sync.job.SyncThread
|
||||
import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
|
||||
|
@ -114,14 +114,14 @@ internal class DefaultSession @Inject constructor(
|
|||
private val accountDataService: Lazy<AccountDataService>,
|
||||
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
||||
private val accountService: Lazy<AccountService>,
|
||||
private val timelineEventDecryptor: TimelineEventDecryptor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val defaultIdentityService: DefaultIdentityService,
|
||||
private val integrationManagerService: IntegrationManagerService,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val callSignalingService: Lazy<CallSignalingService>,
|
||||
@UnauthenticatedWithCertificate
|
||||
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
|
||||
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>,
|
||||
private val eventSenderProcessor: EventSenderProcessor
|
||||
) : Session,
|
||||
RoomService by roomService.get(),
|
||||
RoomDirectoryService by roomDirectoryService.get(),
|
||||
|
@ -160,7 +160,7 @@ internal class DefaultSession @Inject constructor(
|
|||
lifecycleObservers.forEach { it.onStart() }
|
||||
}
|
||||
eventBus.register(this)
|
||||
timelineEventDecryptor.start()
|
||||
eventSenderProcessor.start()
|
||||
}
|
||||
|
||||
override fun requireBackgroundSync() {
|
||||
|
@ -197,13 +197,14 @@ internal class DefaultSession @Inject constructor(
|
|||
override fun close() {
|
||||
assert(isOpen)
|
||||
stopSync()
|
||||
timelineEventDecryptor.destroy()
|
||||
// timelineEventDecryptor.destroy()
|
||||
uiHandler.post {
|
||||
lifecycleObservers.forEach { it.onStop() }
|
||||
}
|
||||
cryptoService.get().close()
|
||||
isOpen = false
|
||||
eventBus.unregister(this)
|
||||
eventSenderProcessor.interrupt()
|
||||
}
|
||||
|
||||
override fun getSyncStateLive() = getSyncThread().liveState()
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
|
|||
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
||||
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
|
||||
import org.matrix.android.sdk.internal.di.MatrixComponent
|
||||
import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule
|
||||
|
@ -45,7 +46,6 @@ import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
|
|||
import org.matrix.android.sdk.internal.session.pushers.PushersModule
|
||||
import org.matrix.android.sdk.internal.session.room.RoomModule
|
||||
import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker
|
||||
import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker
|
||||
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
||||
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
|
||||
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
|
||||
|
@ -109,8 +109,6 @@ internal interface SessionComponent {
|
|||
|
||||
fun inject(worker: SendRelationWorker)
|
||||
|
||||
fun inject(worker: EncryptEventWorker)
|
||||
|
||||
fun inject(worker: MultipleEventSendingDispatcherWorker)
|
||||
|
||||
fun inject(worker: RedactEventWorker)
|
||||
|
@ -131,6 +129,8 @@ internal interface SessionComponent {
|
|||
|
||||
fun inject(worker: SendGossipWorker)
|
||||
|
||||
fun inject(worker: UpdateTrustWorker)
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(
|
||||
|
|
|
@ -41,8 +41,9 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
|||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.ShieldTrustUpdater
|
||||
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
|
||||
import org.matrix.android.sdk.internal.database.DatabaseCleaner
|
||||
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
|
||||
|
@ -331,10 +332,6 @@ internal abstract class SessionModule {
|
|||
@IntoSet
|
||||
abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
|
||||
|
@ -367,4 +364,7 @@ internal abstract class SessionModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker
|
||||
|
||||
@Binds
|
||||
abstract fun bindRedactEventTask(task: DefaultRedactEventTask): RedactEventTask
|
||||
}
|
||||
|
|
|
@ -36,8 +36,8 @@ import org.matrix.android.sdk.api.util.NoOpCancellable
|
|||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.session.call.model.MxCallImpl
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.session.room.send.RoomEventSender
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import timber.log.Timber
|
||||
|
@ -50,7 +50,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
private val userId: String,
|
||||
private val activeCallHandler: ActiveCallHandler,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val roomEventSender: RoomEventSender,
|
||||
private val eventSenderProcessor: EventSenderProcessor,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val turnServerTask: GetTurnServerTask
|
||||
) : CallSignalingService {
|
||||
|
@ -103,7 +103,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
otherUserId = otherUserId,
|
||||
isVideoCall = isVideoCall,
|
||||
localEchoEventFactory = localEchoEventFactory,
|
||||
roomEventSender = roomEventSender
|
||||
eventSenderProcessor = eventSenderProcessor
|
||||
)
|
||||
activeCallHandler.addCall(call).also {
|
||||
return call
|
||||
|
@ -165,7 +165,7 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
otherUserId = event.senderId ?: return@let,
|
||||
isVideoCall = content.isVideo(),
|
||||
localEchoEventFactory = localEchoEventFactory,
|
||||
roomEventSender = roomEventSender
|
||||
eventSenderProcessor = eventSenderProcessor
|
||||
)
|
||||
activeCallHandler.addCall(incomingCall)
|
||||
onCallInvite(incomingCall, content)
|
||||
|
|
|
@ -29,8 +29,8 @@ import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
|||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.session.room.send.RoomEventSender
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.SessionDescription
|
||||
import timber.log.Timber
|
||||
|
@ -43,7 +43,7 @@ internal class MxCallImpl(
|
|||
override val otherUserId: String,
|
||||
override val isVideoCall: Boolean,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val roomEventSender: RoomEventSender
|
||||
private val eventSenderProcessor: EventSenderProcessor
|
||||
) : MxCall {
|
||||
|
||||
override var state: CallState = CallState.Idle
|
||||
|
@ -91,7 +91,7 @@ internal class MxCallImpl(
|
|||
offer = CallInviteContent.Offer(sdp = sdp.description)
|
||||
)
|
||||
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
|
||||
.also { roomEventSender.sendEvent(it) }
|
||||
.also { eventSenderProcessor.postEvent(it) }
|
||||
}
|
||||
|
||||
override fun sendLocalIceCandidates(candidates: List<IceCandidate>) {
|
||||
|
@ -106,7 +106,7 @@ internal class MxCallImpl(
|
|||
}
|
||||
)
|
||||
.let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) }
|
||||
.also { roomEventSender.sendEvent(it) }
|
||||
.also { eventSenderProcessor.postEvent(it) }
|
||||
}
|
||||
|
||||
override fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) {
|
||||
|
@ -119,7 +119,7 @@ internal class MxCallImpl(
|
|||
callId = callId
|
||||
)
|
||||
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
|
||||
.also { roomEventSender.sendEvent(it) }
|
||||
.also { eventSenderProcessor.postEvent(it) }
|
||||
state = CallState.Terminated
|
||||
}
|
||||
|
||||
|
@ -132,7 +132,7 @@ internal class MxCallImpl(
|
|||
answer = CallAnswerContent.Answer(sdp = sdp.description)
|
||||
)
|
||||
.let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) }
|
||||
.also { roomEventSender.sendEvent(it) }
|
||||
.also { eventSenderProcessor.postEvent(it) }
|
||||
}
|
||||
|
||||
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
|
||||
|
|
|
@ -61,7 +61,7 @@ internal class DefaultRoomService @Inject constructor(
|
|||
return roomGetter.getRoom(roomId)
|
||||
}
|
||||
|
||||
override fun getExistingDirectRoomWithUser(otherUserId: String): Room? {
|
||||
override fun getExistingDirectRoomWithUser(otherUserId: String): String? {
|
||||
return roomGetter.getDirectRoomWith(otherUserId)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.session.room
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
|
@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate
|
|||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -78,9 +77,8 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
|
|||
return newState
|
||||
}
|
||||
|
||||
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String,
|
||||
private val cryptoService: CryptoService
|
||||
) : EventInsertLiveProcessor {
|
||||
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String)
|
||||
: EventInsertLiveProcessor {
|
||||
|
||||
private val allowedTypes = listOf(
|
||||
EventType.MESSAGE,
|
||||
|
|
|
@ -25,13 +25,12 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
|||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface RoomGetter {
|
||||
fun getRoom(roomId: String): Room?
|
||||
|
||||
fun getDirectRoomWith(otherUserId: String): Room?
|
||||
fun getDirectRoomWith(otherUserId: String): String?
|
||||
}
|
||||
|
||||
@SessionScope
|
||||
|
@ -46,16 +45,14 @@ internal class DefaultRoomGetter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getDirectRoomWith(otherUserId: String): Room? {
|
||||
override fun getDirectRoomWith(otherUserId: String): String? {
|
||||
return realmSessionProvider.withRealm { realm ->
|
||||
RoomSummaryEntity.where(realm)
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
|
||||
.findAll()
|
||||
.filter { dm -> dm.otherMemberIds.contains(otherUserId) }
|
||||
.map { it.roomId }
|
||||
.firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() }
|
||||
?.let { roomId -> createRoom(realm, roomId) }
|
||||
.firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first() == otherUserId }
|
||||
?.roomId
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.create
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||
import org.matrix.android.sdk.api.session.identity.toMedium
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
|
@ -27,11 +27,13 @@ import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
|
||||
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
|
||||
import org.matrix.android.sdk.internal.session.content.FileUploader
|
||||
import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask
|
||||
import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
|
||||
import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
|
||||
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||
import java.security.InvalidParameterException
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CreateRoomBodyBuilder @Inject constructor(
|
||||
|
@ -39,6 +41,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||
private val crossSigningService: CrossSigningService,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val identityStore: IdentityStore,
|
||||
private val fileUploader: FileUploader,
|
||||
@AuthenticatedIdentity
|
||||
private val accessTokenProvider: AccessTokenProvider
|
||||
) {
|
||||
|
@ -66,7 +69,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||
|
||||
val initialStates = listOfNotNull(
|
||||
buildEncryptionWithAlgorithmEvent(params),
|
||||
buildHistoryVisibilityEvent(params)
|
||||
buildHistoryVisibilityEvent(params),
|
||||
buildAvatarEvent(params)
|
||||
)
|
||||
.takeIf { it.isNotEmpty() }
|
||||
|
||||
|
@ -85,15 +89,33 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
|
||||
return params.avatarUri?.let { avatarUri ->
|
||||
// First upload the image, ignoring any error
|
||||
tryOrNull {
|
||||
fileUploader.uploadFromUri(
|
||||
uri = avatarUri,
|
||||
filename = UUID.randomUUID().toString(),
|
||||
mimeType = "image/jpeg")
|
||||
}
|
||||
?.let { response ->
|
||||
Event(
|
||||
type = EventType.STATE_ROOM_AVATAR,
|
||||
stateKey = "",
|
||||
content = mapOf("url" to response.contentUri)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? {
|
||||
return params.historyVisibility
|
||||
?.let {
|
||||
val contentMap = mapOf("history_visibility" to it)
|
||||
|
||||
Event(
|
||||
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
content = mapOf("history_visibility" to it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,12 +133,10 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||
if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
throw InvalidParameterException("Unsupported algorithm: $it")
|
||||
}
|
||||
val contentMap = mapOf("algorithm" to it)
|
||||
|
||||
Event(
|
||||
type = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
content = mapOf("algorithm" to it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,10 @@ package org.matrix.android.sdk.internal.session.room.relation
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
|
@ -32,30 +30,25 @@ import org.matrix.android.sdk.api.util.Cancelable
|
|||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
|
||||
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import org.matrix.android.sdk.internal.util.fetchCopyMap
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultRelationService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
@SessionId private val sessionId: String,
|
||||
private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon,
|
||||
private val eventSenderProcessor: EventSenderProcessor,
|
||||
private val eventFactory: LocalEchoEventFactory,
|
||||
private val cryptoService: CryptoService,
|
||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
||||
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
|
@ -83,8 +76,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
.none { it.addedByMe && it.key == reaction }) {
|
||||
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
||||
.also { saveLocalEcho(it) }
|
||||
val sendRelationWork = createSendEventWork(event, true)
|
||||
timeLineSendEventWorkCommon.postWork(roomId, sendRelationWork)
|
||||
return eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/)
|
||||
} else {
|
||||
Timber.w("Reaction already added")
|
||||
NoOpCancellable
|
||||
|
@ -107,9 +99,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
data.redactEventId?.let { toRedact ->
|
||||
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null)
|
||||
.also { saveLocalEcho(it) }
|
||||
val redactWork = createRedactEventWork(redactEvent, toRedact, null)
|
||||
|
||||
timeLineSendEventWorkCommon.postWork(roomId, redactWork)
|
||||
eventSenderProcessor.postRedaction(redactEvent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,18 +111,6 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
// TODO duplicate with send service?
|
||||
private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = RedactEventWorker.Params(
|
||||
sessionId,
|
||||
localEvent.eventId!!,
|
||||
roomId,
|
||||
eventId,
|
||||
reason)
|
||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
return timeLineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
|
||||
}
|
||||
|
||||
override fun editTextMessage(targetEventId: String,
|
||||
msgType: String,
|
||||
newBodyText: CharSequence,
|
||||
|
@ -141,14 +119,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
val event = eventFactory
|
||||
.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
||||
.also { saveLocalEcho(it) }
|
||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
|
||||
val workRequest = createSendEventWork(event, false)
|
||||
timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest)
|
||||
} else {
|
||||
val workRequest = createSendEventWork(event, true)
|
||||
timeLineSendEventWorkCommon.postWork(roomId, workRequest)
|
||||
}
|
||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||
}
|
||||
|
||||
override fun editReply(replyToEdit: TimelineEvent,
|
||||
|
@ -165,18 +136,11 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
compatibilityBodyText
|
||||
)
|
||||
.also { saveLocalEcho(it) }
|
||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
|
||||
val workRequest = createSendEventWork(event, false)
|
||||
timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest)
|
||||
} else {
|
||||
val workRequest = createSendEventWork(event, true)
|
||||
timeLineSendEventWorkCommon.postWork(roomId, workRequest)
|
||||
}
|
||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||
}
|
||||
|
||||
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
|
||||
val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId)
|
||||
val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId)
|
||||
fetchEditHistoryTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
|
@ -189,27 +153,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
?.also { saveLocalEcho(it) }
|
||||
?: return null
|
||||
|
||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
|
||||
val workRequest = createSendEventWork(event, false)
|
||||
timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest)
|
||||
} else {
|
||||
val workRequest = createSendEventWork(event, true)
|
||||
timeLineSendEventWorkCommon.postWork(roomId, workRequest)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, event.eventId!!, keepKeys)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||
}
|
||||
|
||||
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
|
||||
|
|
|
@ -25,7 +25,6 @@ import com.squareup.inject.assisted.Assisted
|
|||
import com.squareup.inject.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
||||
|
@ -45,13 +44,13 @@ import org.matrix.android.sdk.api.util.Cancelable
|
|||
import org.matrix.android.sdk.api.util.CancelableBag
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.util.CancelableWork
|
||||
import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.startChain
|
||||
import timber.log.Timber
|
||||
|
@ -63,13 +62,12 @@ private const val UPLOAD_WORK = "UPLOAD_WORK"
|
|||
internal class DefaultSendService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon,
|
||||
@SessionId private val sessionId: String,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val cryptoService: CryptoService,
|
||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val roomEventSender: RoomEventSender,
|
||||
private val eventSenderProcessor: EventSenderProcessor,
|
||||
private val cancelSendTracker: CancelSendTracker
|
||||
) : SendService {
|
||||
|
||||
|
@ -92,19 +90,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
.let { sendEvent(it) }
|
||||
}
|
||||
|
||||
// For test only
|
||||
private fun sendTextMessages(text: CharSequence, msgType: String, autoMarkdown: Boolean, times: Int): Cancelable {
|
||||
return CancelableBag().apply {
|
||||
// Send the event several times
|
||||
repeat(times) { i ->
|
||||
localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown)
|
||||
.also { createLocalEcho(it) }
|
||||
.let { sendEvent(it) }
|
||||
.also { add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
|
||||
return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType)
|
||||
.also { createLocalEcho(it) }
|
||||
|
@ -133,13 +118,14 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
|
||||
override fun redactEvent(event: Event, reason: String?): Cancelable {
|
||||
// TODO manage media/attachements?
|
||||
return createRedactEventWork(event, reason)
|
||||
.let { timelineSendEventWorkCommon.postWork(roomId, it) }
|
||||
val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
|
||||
.also { createLocalEcho(it) }
|
||||
return eventSenderProcessor.postRedaction(redactionEcho, reason)
|
||||
}
|
||||
|
||||
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable {
|
||||
if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) {
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
|
||||
return sendEvent(localEcho.root)
|
||||
}
|
||||
return NoOpCancellable
|
||||
|
@ -153,7 +139,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
val url = messageContent.getFileUrl() ?: return NoOpCancellable
|
||||
if (url.startsWith("mxc://")) {
|
||||
// We need to resend only the message as the attachment is ok
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
|
||||
return sendEvent(localEcho.root)
|
||||
}
|
||||
|
||||
|
@ -170,7 +156,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.IMAGE
|
||||
)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
|
||||
internalSendMedia(listOf(localEcho.root), attachmentData, true)
|
||||
}
|
||||
is MessageVideoContent -> {
|
||||
|
@ -184,7 +170,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.VIDEO
|
||||
)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
|
||||
internalSendMedia(listOf(localEcho.root), attachmentData, true)
|
||||
}
|
||||
is MessageFileContent -> {
|
||||
|
@ -195,7 +181,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.FILE
|
||||
)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
|
||||
internalSendMedia(listOf(localEcho.root), attachmentData, true)
|
||||
}
|
||||
is MessageAudioContent -> {
|
||||
|
@ -207,7 +193,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
queryUri = Uri.parse(messageContent.url),
|
||||
type = ContentAttachmentData.Type.AUDIO
|
||||
)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
|
||||
localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
|
||||
internalSendMedia(listOf(localEcho.root), attachmentData, true)
|
||||
}
|
||||
else -> NoOpCancellable
|
||||
|
@ -222,25 +208,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun clearSendingQueue() {
|
||||
timelineSendEventWorkCommon.cancelAllWorks(roomId)
|
||||
workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK))
|
||||
|
||||
// Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied
|
||||
workManagerProvider.matrixOneTimeWorkRequestBuilder<AlwaysSuccessfulWorker>()
|
||||
.build().let {
|
||||
timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE)
|
||||
|
||||
// need to clear also image sending queue
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
|
||||
.enqueue()
|
||||
}
|
||||
taskExecutor.executorScope.launch {
|
||||
localEchoRepository.clearSendingQueue(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelSend(eventId: String) {
|
||||
cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
|
||||
taskExecutor.executorScope.launch {
|
||||
|
@ -262,13 +229,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// override fun failAllPendingMessages() {
|
||||
// taskExecutor.executorScope.launch {
|
||||
// val eventsToResend = localEchoRepository.getAllEventsWithStates(roomId, SendState.PENDING_STATES)
|
||||
// localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNDELIVERED)
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun sendMedia(attachment: ContentAttachmentData,
|
||||
compressBeforeSending: Boolean,
|
||||
roomIds: Set<String>): Cancelable {
|
||||
|
@ -291,7 +251,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
|
||||
val cancelableBag = CancelableBag()
|
||||
|
||||
allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) }
|
||||
allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) }
|
||||
.apply {
|
||||
keys.forEach { isRoomEncrypted ->
|
||||
// Should never be empty
|
||||
|
@ -301,7 +261,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
|
||||
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
|
||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND_OR_REPLACE, uploadWork)
|
||||
.then(dispatcherWork)
|
||||
.enqueue()
|
||||
.also { operation ->
|
||||
|
@ -322,7 +282,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun sendEvent(event: Event): Cancelable {
|
||||
return roomEventSender.sendEvent(event)
|
||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!))
|
||||
}
|
||||
|
||||
private fun createLocalEcho(event: Event) {
|
||||
|
@ -333,28 +293,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
return "${roomId}_$identifier"
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
return EncryptEventWorker.Params(sessionId, event.eventId ?: "")
|
||||
.let { WorkerParamsFactory.toData(it) }
|
||||
.let {
|
||||
workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.setInputData(it)
|
||||
.startChain(startChain)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
||||
return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
|
||||
.also { createLocalEcho(it) }
|
||||
.let { RedactEventWorker.Params(sessionId, it.eventId!!, roomId, event.eventId, reason) }
|
||||
.let { WorkerParamsFactory.toData(it) }
|
||||
.let { timelineSendEventWorkCommon.createWork<RedactEventWorker>(it, true) }
|
||||
}
|
||||
|
||||
private fun createUploadMediaWork(allLocalEchos: List<Event>,
|
||||
attachment: ContentAttachmentData,
|
||||
isRoomEncrypted: Boolean,
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Possible previous worker: None
|
||||
* Possible next worker : Always [SendEventWorker]
|
||||
*/
|
||||
internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||
: SessionSafeCoroutineWorker<EncryptEventWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val eventId: String,
|
||||
/** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */
|
||||
val keepKeys: List<String>? = null,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
@Inject lateinit var crypto: CryptoService
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
|
||||
override fun injectWith(injector: SessionComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override suspend fun doSafeWork(params: Params): Result {
|
||||
Timber.v("## SendEvent: Start Encrypt work for event ${params.eventId}")
|
||||
|
||||
val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
|
||||
if (localEvent?.eventId == null) {
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (cancelSendTracker.isCancelRequestedFor(localEvent.eventId, localEvent.roomId)) {
|
||||
return Result.success()
|
||||
.also { Timber.e("## SendEvent: Event sending has been cancelled ${localEvent.eventId}") }
|
||||
}
|
||||
|
||||
localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
|
||||
|
||||
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
|
||||
params.keepKeys?.forEach {
|
||||
localMutableContent.remove(it)
|
||||
}
|
||||
|
||||
var error: Throwable? = null
|
||||
var result: MXEncryptEventContentResult? = null
|
||||
try {
|
||||
result = awaitCallback {
|
||||
crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
error = throwable
|
||||
}
|
||||
if (result != null) {
|
||||
val modifiedContent = HashMap(result.eventContent)
|
||||
params.keepKeys?.forEach { toKeep ->
|
||||
localEvent.content?.get(toKeep)?.let {
|
||||
// put it back in the encrypted thing
|
||||
modifiedContent[toKeep] = it
|
||||
}
|
||||
}
|
||||
// Better handling of local echo, to avoid decrypting transition on remote echo
|
||||
// Should I only do it for text messages?
|
||||
val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
MXEventDecryptionResult(
|
||||
clearEvent = Event(
|
||||
type = localEvent.type,
|
||||
content = localEvent.content,
|
||||
roomId = localEvent.roomId
|
||||
).toContent(),
|
||||
forwardingCurve25519KeyChain = emptyList(),
|
||||
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
||||
claimedEd25519Key = crypto.getMyDevice().fingerprint()
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho ->
|
||||
localEcho.type = EventType.ENCRYPTED
|
||||
localEcho.content = ContentMapper.map(modifiedContent)
|
||||
decryptionLocalEcho?.also {
|
||||
localEcho.setDecryptionResult(it)
|
||||
}
|
||||
}
|
||||
|
||||
val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, eventId = params.eventId)
|
||||
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||
} else {
|
||||
val sendState = when (error) {
|
||||
is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
|
||||
else -> SendState.UNDELIVERED
|
||||
}
|
||||
localEchoRepository.updateSendState(localEvent.eventId, sendState)
|
||||
// always return success, or the chain will be stuck for ever!
|
||||
val nextWorkerParams = SendEventWorker.Params(
|
||||
sessionId = params.sessionId,
|
||||
eventId = localEvent.eventId,
|
||||
lastFailureMessage = error?.localizedMessage ?: "Error"
|
||||
)
|
||||
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
}
|
|
@ -88,8 +88,9 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSendState(eventId: String, sendState: SendState) {
|
||||
fun updateSendState(eventId: String, roomId: String?, sendState: SendState) {
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
|
||||
eventBus.post(DefaultTimeline.OnLocalEchoUpdated(roomId ?: "", eventId, sendState))
|
||||
updateEchoAsync(eventId) { realm, sendingEventEntity ->
|
||||
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
|
||||
// If already synced, do not put as sent
|
||||
|
@ -137,6 +138,14 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
|||
}
|
||||
}
|
||||
|
||||
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
|
||||
EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
|
||||
roomSummaryUpdater.updateSendingInformation(realm, roomId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearSendingQueue(roomId: String) {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
TimelineEventEntity
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
@ -31,7 +30,6 @@ import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
|||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.startChain
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -57,7 +55,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
|
|||
|
||||
override fun doOnError(params: Params): Result {
|
||||
params.localEchoIds.forEach { localEchoIds ->
|
||||
localEchoRepository.updateSendState(localEchoIds.eventId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(localEchoIds.eventId, localEchoIds.roomId, SendState.UNDELIVERED)
|
||||
}
|
||||
|
||||
return super.doOnError(params)
|
||||
|
@ -73,19 +71,10 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
|
|||
params.localEchoIds.forEach { localEchoIds ->
|
||||
val roomId = localEchoIds.roomId
|
||||
val eventId = localEchoIds.eventId
|
||||
if (params.isEncrypted) {
|
||||
localEchoRepository.updateSendState(eventId, SendState.ENCRYPTING)
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event $eventId")
|
||||
val encryptWork = createEncryptEventWork(params.sessionId, eventId, true)
|
||||
// Note that event will be replaced by the result of the previous work
|
||||
val sendWork = createSendEventWork(params.sessionId, eventId, false)
|
||||
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
|
||||
} else {
|
||||
localEchoRepository.updateSendState(eventId, SendState.SENDING)
|
||||
localEchoRepository.updateSendState(eventId, roomId, SendState.SENDING)
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId")
|
||||
val sendWork = createSendEventWork(params.sessionId, eventId, true)
|
||||
timelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
|
@ -95,18 +84,6 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
|
|||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
|
||||
val params = EncryptEventWorker.Params(sessionId, eventId)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.setInputData(sendWorkData)
|
||||
.startChain(startChain)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createSendEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = eventId)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send
|
||||
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import org.matrix.android.sdk.internal.worker.startChain
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomEventSender @Inject constructor(
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon,
|
||||
@SessionId private val sessionId: String,
|
||||
private val cryptoService: CryptoService
|
||||
) {
|
||||
fun sendEvent(event: Event): Cancelable {
|
||||
// Encrypted room handling
|
||||
return if (cryptoService.isRoomEncrypted(event.roomId ?: "")
|
||||
&& !event.isEncrypted() // In case of resend where it's already encrypted so skip to send
|
||||
) {
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}")
|
||||
val encryptWork = createEncryptEventWork(event, true)
|
||||
// Note that event will be replaced by the result of the previous work
|
||||
val sendWork = createSendEventWork(event, false)
|
||||
timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork)
|
||||
} else {
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}")
|
||||
val sendWork = createSendEventWork(event, true)
|
||||
timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, event.eventId!!)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.setInputData(sendWorkData)
|
||||
.startChain(startChain)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
}
|
||||
}
|
|
@ -22,12 +22,11 @@ import com.squareup.moshi.JsonClass
|
|||
import io.realm.RealmConfiguration
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import timber.log.Timber
|
||||
|
@ -47,11 +46,14 @@ internal class SendEventWorker(context: Context,
|
|||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
override val lastFailureMessage: String? = null,
|
||||
val eventId: String
|
||||
val eventId: String,
|
||||
// use this as an override if you want to send in clear in encrypted room
|
||||
val isEncrypted: Boolean? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
@Inject lateinit var roomAPI: RoomAPI
|
||||
@Inject lateinit var sendEventTask: SendEventTask
|
||||
@Inject lateinit var cryptoService: CryptoService
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
@SessionDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
|
||||
|
@ -63,7 +65,7 @@ internal class SendEventWorker(context: Context,
|
|||
override suspend fun doSafeWork(params: Params): Result {
|
||||
val event = localEchoRepository.getUpToDateEcho(params.eventId)
|
||||
if (event?.eventId == null || event.roomId == null) {
|
||||
localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(params.eventId, event?.roomId, SendState.UNDELIVERED)
|
||||
return Result.success()
|
||||
.also { Timber.e("Work cancelled due to bad input data") }
|
||||
}
|
||||
|
@ -77,7 +79,7 @@ internal class SendEventWorker(context: Context,
|
|||
}
|
||||
|
||||
if (params.lastFailureMessage != null) {
|
||||
localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
|
||||
// Transmit the error
|
||||
return Result.success(inputData)
|
||||
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||
|
@ -85,12 +87,12 @@ internal class SendEventWorker(context: Context,
|
|||
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
|
||||
return try {
|
||||
sendEvent(event.eventId, event.roomId, event.type, event.content)
|
||||
sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId)))
|
||||
Result.success()
|
||||
} catch (exception: Throwable) {
|
||||
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
|
||||
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}")
|
||||
localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
|
||||
return Result.success()
|
||||
} else {
|
||||
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}")
|
||||
|
@ -102,12 +104,4 @@ internal class SendEventWorker(context: Context,
|
|||
override fun buildErrorParams(params: Params, message: String): Params {
|
||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
||||
}
|
||||
|
||||
private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) {
|
||||
localEchoRepository.updateSendState(eventId, SendState.SENDING)
|
||||
executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.send(eventId, roomId, type, content)
|
||||
}
|
||||
localEchoRepository.updateSendState(eventId, SendState.SENT)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.auth.data.sessionId
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.failure.isTokenError
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import javax.inject.Inject
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
/**
|
||||
* A simple ever running thread unique for that session responsible of sending events in order.
|
||||
* Each send is retried 3 times, if there is no network (e.g if cannot ping home server) it will wait and
|
||||
* periodically test reachability before resume (does not count as a retry)
|
||||
*
|
||||
* If the app is killed before all event were sent, on next wakeup the scheduled events will be re posted
|
||||
*/
|
||||
@SessionScope
|
||||
internal class EventSenderProcessor @Inject constructor(
|
||||
private val cryptoService: CryptoService,
|
||||
private val sessionParams: SessionParams,
|
||||
private val queuedTaskFactory: QueuedTaskFactory,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val memento: QueueMemento
|
||||
) : Thread("SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}") {
|
||||
|
||||
private fun markAsManaged(task: QueuedTask) {
|
||||
memento.track(task)
|
||||
}
|
||||
|
||||
private fun markAsFinished(task: QueuedTask) {
|
||||
memento.unTrack(task)
|
||||
}
|
||||
|
||||
// API
|
||||
fun postEvent(event: Event): Cancelable {
|
||||
return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
// We should check for sending events not handled because app was killed
|
||||
// But we should be careful of only took those that was submitted to us, because if it's
|
||||
// for example it's a media event it is handled by some worker and he will handle it
|
||||
// This is a bit fragile :/
|
||||
// also some events cannot be retried manually by users, e.g reactions
|
||||
// they were previously relying on workers to do the work :/ and was expected to always finally succeed
|
||||
// Also some echos are not to be resent like redaction echos (fake event created for aggregation)
|
||||
|
||||
tryOrNull {
|
||||
taskExecutor.executorScope.launch {
|
||||
Timber.d("## Send relaunched pending events on restart")
|
||||
memento.restoreTasks(this@EventSenderProcessor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun postEvent(event: Event, encrypt: Boolean): Cancelable {
|
||||
val task = queuedTaskFactory.createSendTask(event, encrypt)
|
||||
return postTask(task)
|
||||
}
|
||||
|
||||
fun postRedaction(redactionLocalEcho: Event, reason: String?): Cancelable {
|
||||
return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason)
|
||||
}
|
||||
|
||||
fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?): Cancelable {
|
||||
val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason)
|
||||
return postTask(task)
|
||||
}
|
||||
|
||||
fun postTask(task: QueuedTask): Cancelable {
|
||||
// non blocking add to queue
|
||||
sendingQueue.add(task)
|
||||
markAsManaged(task)
|
||||
return object : Cancelable {
|
||||
override fun cancel() {
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RETRY_WAIT_TIME_MS = 10_000L
|
||||
}
|
||||
|
||||
private var sendingQueue = LinkedBlockingQueue<QueuedTask>()
|
||||
|
||||
private var networkAvailableLock = Object()
|
||||
private var canReachServer = true
|
||||
private var retryNoNetworkTask: TimerTask? = null
|
||||
|
||||
override fun run() {
|
||||
Timber.v("## SendThread started ts:${System.currentTimeMillis()}")
|
||||
try {
|
||||
while (!isInterrupted) {
|
||||
Timber.v("## SendThread wait for task to process")
|
||||
val task = sendingQueue.take()
|
||||
Timber.v("## SendThread Found task to process $task")
|
||||
|
||||
if (task.isCancelled()) {
|
||||
Timber.v("## SendThread send cancelled for $task")
|
||||
// we do not execute this one
|
||||
continue
|
||||
}
|
||||
// we check for network connectivity
|
||||
while (!canReachServer) {
|
||||
Timber.v("## SendThread cannot reach server, wait ts:${System.currentTimeMillis()}")
|
||||
// schedule to retry
|
||||
waitForNetwork()
|
||||
// if thread as been killed meanwhile
|
||||
// if (state == State.KILLING) break
|
||||
}
|
||||
Timber.v("## Server is Reachable")
|
||||
// so network is available
|
||||
|
||||
runBlocking {
|
||||
retryLoop@ while (task.retryCount < 3) {
|
||||
try {
|
||||
// SendPerformanceProfiler.startStage(task.event.eventId!!, SendPerformanceProfiler.Stages.SEND_WORKER)
|
||||
Timber.v("## SendThread retryLoop for $task retryCount ${task.retryCount}")
|
||||
task.execute()
|
||||
// sendEventTask.execute(SendEventTask.Params(task.event, task.encrypt, cryptoService))
|
||||
// SendPerformanceProfiler.stopStage(task.event.eventId, SendPerformanceProfiler.Stages.SEND_WORKER)
|
||||
break@retryLoop
|
||||
} catch (exception: Throwable) {
|
||||
when {
|
||||
exception is IOException || exception is Failure.NetworkConnection -> {
|
||||
canReachServer = false
|
||||
task.retryCount++
|
||||
if (task.retryCount >= 3) task.onTaskFailed()
|
||||
while (!canReachServer) {
|
||||
Timber.v("## SendThread retryLoop cannot reach server, wait ts:${System.currentTimeMillis()}")
|
||||
// schedule to retry
|
||||
waitForNetwork()
|
||||
}
|
||||
}
|
||||
(exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> {
|
||||
task.retryCount++
|
||||
if (task.retryCount >= 3) task.onTaskFailed()
|
||||
Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}")
|
||||
// wait a bit
|
||||
// Todo if its a quota exception can we get timout?
|
||||
sleep(3_000)
|
||||
continue@retryLoop
|
||||
}
|
||||
exception.isTokenError() -> {
|
||||
Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt")
|
||||
// we can exit the loop
|
||||
task.onTaskFailed()
|
||||
throw InterruptedException()
|
||||
}
|
||||
else -> {
|
||||
Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
|
||||
// this task is in error, check next one?
|
||||
break@retryLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
markAsFinished(task)
|
||||
}
|
||||
} catch (interruptionException: InterruptedException) {
|
||||
// will be thrown is thread is interrupted while seeping
|
||||
interrupt()
|
||||
Timber.v("## InterruptedException!! ${interruptionException.localizedMessage}")
|
||||
}
|
||||
// state = State.KILLED
|
||||
// is this needed?
|
||||
retryNoNetworkTask?.cancel()
|
||||
Timber.w("## SendThread finished ${System.currentTimeMillis()}")
|
||||
}
|
||||
|
||||
private fun waitForNetwork() {
|
||||
retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) {
|
||||
synchronized(networkAvailableLock) {
|
||||
canReachServer = checkHostAvailable().also {
|
||||
Timber.v("## SendThread checkHostAvailable $it")
|
||||
}
|
||||
networkAvailableLock.notify()
|
||||
}
|
||||
}
|
||||
synchronized(networkAvailableLock) { networkAvailableLock.wait() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if homeserver is reachable.
|
||||
*/
|
||||
private fun checkHostAvailable(): Boolean {
|
||||
val host = sessionParams.homeServerConnectionConfig.homeServerUri.host ?: return false
|
||||
val port = sessionParams.homeServerConnectionConfig.homeServerUri.port.takeIf { it != -1 } ?: 80
|
||||
val timeout = 30_000
|
||||
try {
|
||||
Socket().use { socket ->
|
||||
val inetAddress: InetAddress = InetAddress.getByName(host)
|
||||
val inetSocketAddress = InetSocketAddress(inetAddress, port)
|
||||
socket.connect(inetSocketAddress, timeout)
|
||||
return true
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.v("## EventSender isHostAvailable failure ${e.localizedMessage}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
import android.content.Context
|
||||
import org.matrix.android.sdk.api.auth.data.sessionId
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Simple lightweight persistence
|
||||
* Don't want to go in DB due to current issues
|
||||
* Will never manage lots of events, it simply uses sharedPreferences.
|
||||
* It is just used to remember what events/localEchos was managed by the event sender in order to
|
||||
* reschedule them (and only them) on next restart
|
||||
*/
|
||||
internal class QueueMemento @Inject constructor(context: Context,
|
||||
@SessionId sessionId: String,
|
||||
private val queuedTaskFactory: QueuedTaskFactory,
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val cryptoService: CryptoService) {
|
||||
|
||||
private val storage = context.getSharedPreferences("QueueMemento_$sessionId", Context.MODE_PRIVATE)
|
||||
private val managedTaskInfos = mutableListOf<QueuedTask>()
|
||||
|
||||
fun track(task: QueuedTask) {
|
||||
synchronized(managedTaskInfos) {
|
||||
managedTaskInfos.add(task)
|
||||
persist()
|
||||
}
|
||||
}
|
||||
|
||||
fun unTrack(task: QueuedTask) {
|
||||
managedTaskInfos.remove(task)
|
||||
persist()
|
||||
}
|
||||
|
||||
private fun persist() {
|
||||
managedTaskInfos.mapIndexedNotNull { index, queuedTask ->
|
||||
toTaskInfo(queuedTask, index)?.let { TaskInfo.map(it) }
|
||||
}.toSet().let { set ->
|
||||
storage.edit()
|
||||
.putStringSet("ManagedBySender", set)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toTaskInfo(task: QueuedTask, order: Int): TaskInfo? {
|
||||
synchronized(managedTaskInfos) {
|
||||
return when (task) {
|
||||
is SendEventQueuedTask -> SendEventTaskInfo(
|
||||
localEchoId = task.event.eventId ?: "",
|
||||
encrypt = task.encrypt,
|
||||
order = order
|
||||
)
|
||||
is RedactQueuedTask -> RedactEventTaskInfo(
|
||||
redactionLocalEcho = task.redactionLocalEchoId,
|
||||
order = order
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restoreTasks(eventProcessor: EventSenderProcessor) {
|
||||
// events should be restarted in correct order
|
||||
storage.getStringSet("ManagedBySender", null)?.let { pending ->
|
||||
Timber.d("## Send - Recovering unsent events $pending")
|
||||
pending.mapNotNull { tryOrNull { TaskInfo.map(it) } }
|
||||
}
|
||||
?.sortedBy { it.order }
|
||||
?.forEach { info ->
|
||||
try {
|
||||
when (info) {
|
||||
is SendEventTaskInfo -> {
|
||||
localEchoRepository.getUpToDateEcho(info.localEchoId)?.let {
|
||||
if (it.sendState.isSending() && it.eventId != null && it.roomId != null) {
|
||||
localEchoRepository.updateSendState(it.eventId, it.roomId, SendState.UNSENT)
|
||||
Timber.d("## Send -Reschedule send $info")
|
||||
eventProcessor.postTask(queuedTaskFactory.createSendTask(it, info.encrypt ?: cryptoService.isRoomEncrypted(it.roomId)))
|
||||
}
|
||||
}
|
||||
}
|
||||
is RedactEventTaskInfo -> {
|
||||
info.redactionLocalEcho?.let { localEchoRepository.getUpToDateEcho(it) }?.let {
|
||||
localEchoRepository.updateSendState(it.eventId!!, it.roomId, SendState.UNSENT)
|
||||
// try to get reason
|
||||
val reason = it.content?.get("reason") as? String
|
||||
if (it.redacts != null && it.roomId != null) {
|
||||
Timber.d("## Send -Reschedule redact $info")
|
||||
eventProcessor.postTask(queuedTaskFactory.createRedactTask(it.eventId, it.redacts, it.roomId, reason))
|
||||
}
|
||||
}
|
||||
// postTask(queuedTaskFactory.createRedactTask(info.eventToRedactId, info.)
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e("failed to restore task $info")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
@ -13,14 +13,17 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
||||
|
||||
data class SessionToCryptoRoomMembersUpdate(
|
||||
val roomId: String,
|
||||
val isDirect: Boolean,
|
||||
val userIds: List<String>
|
||||
)
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
data class CryptoToSessionUserTrustChange(
|
||||
val userIds: List<String>
|
||||
)
|
||||
abstract class QueuedTask {
|
||||
var retryCount = 0
|
||||
|
||||
abstract suspend fun execute()
|
||||
|
||||
abstract fun onTaskFailed()
|
||||
|
||||
abstract fun isCancelled() : Boolean
|
||||
|
||||
abstract fun cancel()
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
|
||||
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class QueuedTaskFactory @Inject constructor(
|
||||
private val sendEventTask: SendEventTask,
|
||||
private val cryptoService: CryptoService,
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val redactEventTask: RedactEventTask,
|
||||
private val cancelSendTracker: CancelSendTracker
|
||||
) {
|
||||
|
||||
fun createSendTask(event: Event, encrypt: Boolean): QueuedTask {
|
||||
return SendEventQueuedTask(
|
||||
event = event,
|
||||
encrypt = encrypt,
|
||||
cryptoService = cryptoService,
|
||||
localEchoRepository = localEchoRepository,
|
||||
sendEventTask = sendEventTask,
|
||||
cancelSendTracker = cancelSendTracker
|
||||
)
|
||||
}
|
||||
|
||||
fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?): QueuedTask {
|
||||
return RedactQueuedTask(
|
||||
redactionLocalEchoId = redactionLocalEcho,
|
||||
toRedactEventId = eventId,
|
||||
roomId = roomId,
|
||||
reason = reason,
|
||||
redactEventTask = redactEventTask,
|
||||
localEchoRepository = localEchoRepository,
|
||||
cancelSendTracker = cancelSendTracker
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
|
||||
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
|
||||
internal class RedactQueuedTask(
|
||||
val toRedactEventId: String,
|
||||
val redactionLocalEchoId: String,
|
||||
val roomId: String,
|
||||
val reason: String?,
|
||||
val redactEventTask: RedactEventTask,
|
||||
val localEchoRepository: LocalEchoRepository,
|
||||
val cancelSendTracker: CancelSendTracker
|
||||
) : QueuedTask() {
|
||||
|
||||
private var _isCancelled: Boolean = false
|
||||
|
||||
override fun toString() = "[RedactEventRunnableTask $redactionLocalEchoId]"
|
||||
|
||||
override suspend fun execute() {
|
||||
redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason))
|
||||
}
|
||||
|
||||
override fun onTaskFailed() {
|
||||
localEchoRepository.updateSendState(redactionLocalEchoId, roomId, SendState.UNDELIVERED)
|
||||
}
|
||||
|
||||
override fun isCancelled(): Boolean {
|
||||
return _isCancelled || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId)
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
_isCancelled = true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
|
||||
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
|
||||
internal class SendEventQueuedTask(
|
||||
val event: Event,
|
||||
val encrypt: Boolean,
|
||||
val sendEventTask: SendEventTask,
|
||||
val cryptoService: CryptoService,
|
||||
val localEchoRepository: LocalEchoRepository,
|
||||
val cancelSendTracker: CancelSendTracker
|
||||
) : QueuedTask() {
|
||||
|
||||
private var _isCancelled: Boolean = false
|
||||
|
||||
override fun toString() = "[SendEventRunnableTask ${event.eventId}]"
|
||||
|
||||
override suspend fun execute() {
|
||||
sendEventTask.execute(SendEventTask.Params(event, encrypt))
|
||||
}
|
||||
|
||||
override fun onTaskFailed() {
|
||||
when (event.getClearType()) {
|
||||
EventType.REDACTION,
|
||||
EventType.REACTION -> {
|
||||
// we just delete? it will not be present on timeline and no ux to retry
|
||||
localEchoRepository.deleteFailedEchoAsync(eventId = event.eventId, roomId = event.roomId ?: "")
|
||||
// TODO update aggregation :/ or it will stay locally
|
||||
}
|
||||
else -> {
|
||||
localEchoRepository.updateSendState(event.eventId!!, event.roomId, SendState.UNDELIVERED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isCancelled(): Boolean {
|
||||
return _isCancelled || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId)
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
_isCancelled = true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.session.room.send.queue
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.di.SerializeNulls
|
||||
import org.matrix.android.sdk.internal.network.parsing.RuntimeJsonAdapterFactory
|
||||
|
||||
/**
|
||||
* Info that need to be persisted by the sender thread
|
||||
* With polymorphic moshi parsing
|
||||
*/
|
||||
internal interface TaskInfo {
|
||||
val type: String
|
||||
val order: Int
|
||||
|
||||
companion object {
|
||||
const val TYPE_UNKNOWN = "TYPE_UNKNOWN"
|
||||
const val TYPE_SEND = "TYPE_SEND"
|
||||
const val TYPE_REDACT = "TYPE_REDACT"
|
||||
|
||||
val moshi = Moshi.Builder()
|
||||
.add(RuntimeJsonAdapterFactory.of(TaskInfo::class.java, "type", FallbackTaskInfo::class.java)
|
||||
.registerSubtype(SendEventTaskInfo::class.java, TYPE_SEND)
|
||||
.registerSubtype(RedactEventTaskInfo::class.java, TYPE_REDACT)
|
||||
)
|
||||
.add(SerializeNulls.JSON_ADAPTER_FACTORY)
|
||||
.build()
|
||||
|
||||
fun map(info: TaskInfo): String {
|
||||
return moshi.adapter(TaskInfo::class.java).toJson(info)
|
||||
}
|
||||
|
||||
fun map(string: String): TaskInfo? {
|
||||
return tryOrNull { moshi.adapter(TaskInfo::class.java).fromJson(string) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class SendEventTaskInfo(
|
||||
@Json(name = "type") override val type: String = TaskInfo.TYPE_SEND,
|
||||
@Json(name = "localEchoId") val localEchoId: String,
|
||||
@Json(name = "encrypt") val encrypt: Boolean?,
|
||||
@Json(name = "order") override val order: Int
|
||||
) : TaskInfo
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class RedactEventTaskInfo(
|
||||
@Json(name = "type") override val type: String = TaskInfo.TYPE_REDACT,
|
||||
@Json(name = "redactionLocalEcho") val redactionLocalEcho: String?,
|
||||
@Json(name = "order") override val order: Int
|
||||
) : TaskInfo
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class FallbackTaskInfo(
|
||||
@Json(name = "type") override val type: String = TaskInfo.TYPE_REDACT,
|
||||
@Json(name = "order") override val order: Int
|
||||
) : TaskInfo
|
|
@ -140,4 +140,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_AVATAR,
|
||||
body = emptyMap(),
|
||||
callback = callback,
|
||||
stateKey = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,8 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.summary
|
||||
|
||||
import dagger.Lazy
|
||||
import io.realm.Realm
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
|
@ -28,9 +26,11 @@ import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
|
@ -46,7 +46,6 @@ import org.matrix.android.sdk.internal.di.UserId
|
|||
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
|
||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
|
||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
|
||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
|
||||
import timber.log.Timber
|
||||
|
@ -56,8 +55,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
@UserId private val userId: String,
|
||||
private val roomDisplayNameResolver: RoomDisplayNameResolver,
|
||||
private val roomAvatarResolver: RoomAvatarResolver,
|
||||
private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>,
|
||||
private val eventBus: EventBus) {
|
||||
private val eventDecryptor: EventDecryptor,
|
||||
private val crossSigningService: DefaultCrossSigningService) {
|
||||
|
||||
fun update(realm: Realm,
|
||||
roomId: String,
|
||||
|
@ -126,9 +125,14 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
}
|
||||
roomSummaryEntity.updateHasFailedSending()
|
||||
|
||||
if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) {
|
||||
val root = latestPreviewableEvent?.root
|
||||
if (root?.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
|
||||
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
|
||||
timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, ""))
|
||||
// mmm i want to decrypt now or is it ok to do it async?
|
||||
tryOrNull {
|
||||
eventDecryptor.decryptEvent(root.asDomain(), "")
|
||||
// eventDecryptor.decryptEventAsync(root.asDomain(), "", NoOpMatrixCallback())
|
||||
}
|
||||
}
|
||||
|
||||
if (updateMembers) {
|
||||
|
@ -142,7 +146,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
roomSummaryEntity.otherMemberIds.clear()
|
||||
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
|
||||
if (roomSummaryEntity.isEncrypted) {
|
||||
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId))
|
||||
// mmm maybe we could only refresh shield instead of checking trust also?
|
||||
crossSigningService.onUsersDeviceUpdate(roomSummaryEntity.otherMemberIds.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,13 +161,4 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
roomSummaryEntity.updateHasFailedSending()
|
||||
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
|
||||
}
|
||||
|
||||
fun updateShieldTrust(realm: Realm,
|
||||
roomId: String,
|
||||
trust: RoomEncryptionTrustLevel?) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
||||
if (roomSummaryEntity.isEncrypted) {
|
||||
roomSummaryEntity.roomEncryptionTrustLevel = trust
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,11 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
|
|||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
@ -83,6 +86,7 @@ internal class DefaultTimeline(
|
|||
|
||||
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
|
||||
data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent)
|
||||
data class OnLocalEchoUpdated(val roomId: String, val eventId: String, val sendState: SendState)
|
||||
|
||||
companion object {
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
||||
|
@ -102,7 +106,9 @@ internal class DefaultTimeline(
|
|||
|
||||
private var prevDisplayIndex: Int? = null
|
||||
private var nextDisplayIndex: Int? = null
|
||||
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||
|
||||
private val uiEchoManager = UIEchoManager()
|
||||
|
||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
||||
private val backwardsState = AtomicReference(State())
|
||||
|
@ -161,14 +167,14 @@ internal class DefaultTimeline(
|
|||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||
?: throw IllegalStateException("Can't open a timeline without a room")
|
||||
|
||||
sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll()
|
||||
// We don't want to filter here because some sending events that are not displayed
|
||||
// are still used for ui echo (relation like reaction)
|
||||
sendingEvents = roomEntity.sendingTimelineEvents.where()/*.filterEventsWithSettings()*/.findAll()
|
||||
sendingEvents.addChangeListener { events ->
|
||||
// Remove in memory as soon as they are known by database
|
||||
events.forEach { te ->
|
||||
inMemorySendingEvents.removeAll { te.eventId == it.eventId }
|
||||
}
|
||||
uiEchoManager.sentEventsUpdated(events)
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
||||
filteredEvents = nonFilteredEvents.where()
|
||||
.filterEventsWithSettings()
|
||||
|
@ -318,16 +324,15 @@ internal class DefaultTimeline(
|
|||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) {
|
||||
if (isLive && onLocalEchoCreated.roomId == roomId) {
|
||||
// do not add events that would have been filtered
|
||||
if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) {
|
||||
listeners.forEach {
|
||||
it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
|
||||
}
|
||||
Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}")
|
||||
inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
|
||||
postSnapshot()
|
||||
}
|
||||
if (uiEchoManager.onLocalEchoCreated(onLocalEchoCreated)) {
|
||||
postSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onLocalEchoUpdated(onLocalEchoUpdated: OnLocalEchoUpdated) {
|
||||
if (uiEchoManager.onLocalEchoUpdated(onLocalEchoUpdated)) {
|
||||
postSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -407,12 +412,17 @@ internal class DefaultTimeline(
|
|||
private fun buildSendingEvents(): List<TimelineEvent> {
|
||||
val builtSendingEvents = ArrayList<TimelineEvent>()
|
||||
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
|
||||
builtSendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings())
|
||||
sendingEvents.forEach { timelineEventEntity ->
|
||||
if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
|
||||
builtSendingEvents.add(timelineEventMapper.map(timelineEventEntity))
|
||||
}
|
||||
}
|
||||
builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings())
|
||||
sendingEvents
|
||||
.map { timelineEventMapper.map(it) }
|
||||
// Filter out sending event that are not displayable!
|
||||
.filterEventsWithSettings()
|
||||
.forEach { timelineEvent ->
|
||||
if (builtSendingEvents.find { it.eventId == timelineEvent.eventId } == null) {
|
||||
uiEchoManager.updateSentStateWithUiEcho(timelineEvent)
|
||||
builtSendingEvents.add(timelineEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return builtSendingEvents
|
||||
}
|
||||
|
@ -622,14 +632,11 @@ internal class DefaultTimeline(
|
|||
|
||||
val timelineEvent = buildTimelineEvent(eventEntity)
|
||||
val transactionId = timelineEvent.root.unsignedData?.transactionId
|
||||
val sendingEvent = inMemorySendingEvents.find {
|
||||
it.eventId == transactionId
|
||||
}
|
||||
inMemorySendingEvents.remove(sendingEvent)
|
||||
uiEchoManager.onSyncedEvent(transactionId)
|
||||
|
||||
if (timelineEvent.isEncrypted()
|
||||
&& timelineEvent.root.mxDecryptionResult == null) {
|
||||
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) }
|
||||
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineID)) }
|
||||
}
|
||||
|
||||
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
||||
|
@ -649,7 +656,10 @@ internal class DefaultTimeline(
|
|||
timelineEventEntity = eventEntity,
|
||||
buildReadReceipts = settings.buildReadReceipts,
|
||||
correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId)
|
||||
)
|
||||
).let {
|
||||
// eventually enhance with ui echo?
|
||||
(uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it)
|
||||
}
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it accesses realm live results
|
||||
|
@ -778,8 +788,8 @@ internal class DefaultTimeline(
|
|||
val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type)
|
||||
if (!filterType) return@filter false
|
||||
|
||||
val filterEdits = if (settings.filters.filterEdits && it.root.type == EventType.MESSAGE) {
|
||||
val messageContent = it.root.content.toModel<MessageContent>()
|
||||
val filterEdits = if (settings.filters.filterEdits && it.root.getClearType() == EventType.MESSAGE) {
|
||||
val messageContent = it.root.getClearContent().toModel<MessageContent>()
|
||||
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
|
||||
} else {
|
||||
true
|
||||
|
@ -797,4 +807,161 @@ internal class DefaultTimeline(
|
|||
val isPaginating: Boolean = false,
|
||||
val requestedPaginationCount: Int = 0
|
||||
)
|
||||
|
||||
private data class ReactionUiEchoData(
|
||||
val localEchoId: String,
|
||||
val reactedOnEventId: String,
|
||||
val reaction: String
|
||||
)
|
||||
|
||||
inner class UIEchoManager {
|
||||
|
||||
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||
|
||||
fun getInMemorySendingEvents(): List<TimelineEvent> {
|
||||
return inMemorySendingEvents.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Due to lag of DB updates, we keep some UI echo of some properties to update timeline faster
|
||||
*/
|
||||
private val inMemorySendingStates = Collections.synchronizedMap<String, SendState>(HashMap())
|
||||
|
||||
private val inMemoryReactions = Collections.synchronizedMap<String, MutableList<ReactionUiEchoData>>(HashMap())
|
||||
|
||||
fun sentEventsUpdated(events: RealmResults<TimelineEventEntity>) {
|
||||
// Remove in memory as soon as they are known by database
|
||||
events.forEach { te ->
|
||||
inMemorySendingEvents.removeAll { te.eventId == it.eventId }
|
||||
}
|
||||
inMemorySendingStates.keys.removeAll { key ->
|
||||
events.find { it.eventId == key } == null
|
||||
}
|
||||
|
||||
inMemoryReactions.forEach { (_, uiEchoData) ->
|
||||
uiEchoData.removeAll { data ->
|
||||
// I remove the uiEcho, when the related event is not anymore in the sending list
|
||||
// (means that it is synced)!
|
||||
events.find { it.eventId == data.localEchoId } == null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onLocalEchoUpdated(onLocalEchoUpdated: OnLocalEchoUpdated): Boolean {
|
||||
if (isLive && onLocalEchoUpdated.roomId == roomId) {
|
||||
val existingState = inMemorySendingStates[onLocalEchoUpdated.eventId]
|
||||
inMemorySendingStates[onLocalEchoUpdated.eventId] = onLocalEchoUpdated.sendState
|
||||
if (existingState != onLocalEchoUpdated.sendState) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// return true if should update
|
||||
fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated): Boolean {
|
||||
var postSnapshot = false
|
||||
if (isLive && onLocalEchoCreated.roomId == roomId) {
|
||||
// Manage some ui echos (do it before filter because actual event could be filtered out)
|
||||
when (onLocalEchoCreated.timelineEvent.root.getClearType()) {
|
||||
EventType.REDACTION -> {
|
||||
}
|
||||
EventType.REACTION -> {
|
||||
val content = onLocalEchoCreated.timelineEvent.root.content?.toModel<ReactionContent>()
|
||||
if (RelationType.ANNOTATION == content?.relatesTo?.type) {
|
||||
val reaction = content.relatesTo.key
|
||||
val relatedEventID = content.relatesTo.eventId
|
||||
inMemoryReactions.getOrPut(relatedEventID) { mutableListOf() }
|
||||
.add(
|
||||
ReactionUiEchoData(
|
||||
localEchoId = onLocalEchoCreated.timelineEvent.eventId,
|
||||
reactedOnEventId = relatedEventID,
|
||||
reaction = reaction
|
||||
)
|
||||
)
|
||||
postSnapshot = rebuildEvent(relatedEventID) {
|
||||
decorateEventWithReactionUiEcho(it)
|
||||
} || postSnapshot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do not add events that would have been filtered
|
||||
if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) {
|
||||
listeners.forEach {
|
||||
it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
|
||||
}
|
||||
Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}")
|
||||
inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
|
||||
postSnapshot = true
|
||||
}
|
||||
}
|
||||
return postSnapshot
|
||||
}
|
||||
|
||||
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? {
|
||||
val relatedEventID = timelineEvent.eventId
|
||||
val contents = inMemoryReactions[relatedEventID] ?: return null
|
||||
|
||||
var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary(
|
||||
relatedEventID
|
||||
)
|
||||
val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
|
||||
|
||||
contents.forEach { uiEchoReaction ->
|
||||
val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
|
||||
if (existing == null) {
|
||||
// just add the new key
|
||||
ReactionAggregatedSummary(
|
||||
key = uiEchoReaction.reaction,
|
||||
count = 1,
|
||||
addedByMe = true,
|
||||
firstTimestamp = System.currentTimeMillis(),
|
||||
sourceEvents = emptyList(),
|
||||
localEchoEvents = listOf(uiEchoReaction.localEchoId)
|
||||
).let { updateReactions.add(it) }
|
||||
} else {
|
||||
// update Existing Key
|
||||
if (!existing.localEchoEvents.contains(uiEchoReaction.localEchoId)) {
|
||||
updateReactions.remove(existing)
|
||||
// only update if echo is not yet there
|
||||
ReactionAggregatedSummary(
|
||||
key = existing.key,
|
||||
count = existing.count + 1,
|
||||
addedByMe = true,
|
||||
firstTimestamp = existing.firstTimestamp,
|
||||
sourceEvents = existing.sourceEvents,
|
||||
localEchoEvents = existing.localEchoEvents + uiEchoReaction.localEchoId
|
||||
|
||||
).let { updateReactions.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existingAnnotationSummary = existingAnnotationSummary.copy(
|
||||
reactionsSummary = updateReactions
|
||||
)
|
||||
return timelineEvent.copy(
|
||||
annotations = existingAnnotationSummary
|
||||
)
|
||||
}
|
||||
|
||||
fun updateSentStateWithUiEcho(element: TimelineEvent) {
|
||||
inMemorySendingStates[element.eventId]?.let {
|
||||
// Timber.v("## ${System.currentTimeMillis()} Send event refresh echo with live state ${it} from state ${element.root.sendState}")
|
||||
element.root.sendState = element.root.sendState.takeIf { it == SendState.SENT } ?: it
|
||||
}
|
||||
}
|
||||
|
||||
fun onSyncedEvent(transactionId: String?) {
|
||||
val sendingEvent = inMemorySendingEvents.find {
|
||||
it.eventId == transactionId
|
||||
}
|
||||
inMemorySendingEvents.remove(sendingEvent)
|
||||
// Is it too early to clear it? will be done when removed from sending anyway?
|
||||
inMemoryReactions.forEach { (_, u) ->
|
||||
u.filterNot { it.localEchoId == transactionId }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,24 +15,22 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.session.room.timeline
|
||||
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.NewSessionListener
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class TimelineEventDecryptor @Inject constructor(
|
||||
@SessionDatabase
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
|
@ -83,14 +81,14 @@ internal class TimelineEventDecryptor @Inject constructor(
|
|||
synchronized(unknownSessionsFailure) {
|
||||
for (requests in unknownSessionsFailure.values) {
|
||||
if (request in requests) {
|
||||
Timber.d("Skip Decryption request for event ${request.eventId}, unknown session")
|
||||
Timber.d("Skip Decryption request for event ${request.event.eventId}, unknown session")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
synchronized(existingRequests) {
|
||||
if (!existingRequests.add(request)) {
|
||||
Timber.d("Skip Decryption request for event ${request.eventId}, already requested")
|
||||
Timber.d("Skip Decryption request for event ${request.event.eventId}, already requested")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -101,25 +99,29 @@ internal class TimelineEventDecryptor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction {
|
||||
val eventId = request.eventId
|
||||
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
|
||||
val event = request.event
|
||||
val timelineId = request.timelineId
|
||||
Timber.v("Decryption request for event $eventId")
|
||||
val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst()
|
||||
?: return@executeTransaction Unit.also {
|
||||
Timber.d("Decryption request for unknown message")
|
||||
}
|
||||
val event = eventEntity.asDomain()
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, timelineId)
|
||||
Timber.v("Successfully decrypted event $eventId")
|
||||
eventEntity.setDecryptionResult(result)
|
||||
val result = cryptoService.decryptEvent(request.event, timelineId)
|
||||
Timber.v("Successfully decrypted event ${event.eventId}")
|
||||
realm.executeTransaction {
|
||||
EventEntity.where(it, eventId = event.eventId ?: "")
|
||||
.findFirst()
|
||||
?.setDecryptionResult(result)
|
||||
}
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v(e, "Failed to decrypt event $eventId")
|
||||
Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}")
|
||||
if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) {
|
||||
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||
eventEntity.decryptionErrorCode = e.errorType.name
|
||||
eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||
realm.executeTransaction {
|
||||
EventEntity.where(it, eventId = event.eventId ?: "")
|
||||
.findFirst()
|
||||
?.let {
|
||||
it.decryptionErrorCode = e.errorType.name
|
||||
it.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||
}
|
||||
}
|
||||
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
||||
content.sessionId?.let { sessionId ->
|
||||
synchronized(unknownSessionsFailure) {
|
||||
|
@ -130,7 +132,7 @@ internal class TimelineEventDecryptor @Inject constructor(
|
|||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}")
|
||||
Timber.e("Failed to decrypt event ${event.eventId}, ${t.localizedMessage}")
|
||||
} finally {
|
||||
synchronized(existingRequests) {
|
||||
existingRequests.remove(request)
|
||||
|
@ -139,7 +141,7 @@ internal class TimelineEventDecryptor @Inject constructor(
|
|||
}
|
||||
|
||||
data class DecryptionRequest(
|
||||
val eventId: String,
|
||||
val event: Event,
|
||||
val timelineId: String
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.work.ExistingWorkPolicy
|
|||
import androidx.work.ListenableWorker
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.util.CancelableWork
|
||||
import org.matrix.android.sdk.internal.worker.startChain
|
||||
|
@ -38,24 +37,6 @@ internal class TimelineSendEventWorkCommon @Inject constructor(
|
|||
private val workManagerProvider: WorkManagerProvider
|
||||
) {
|
||||
|
||||
fun postSequentialWorks(roomId: String, vararg workRequests: OneTimeWorkRequest): Cancelable {
|
||||
return when {
|
||||
workRequests.isEmpty() -> NoOpCancellable
|
||||
workRequests.size == 1 -> postWork(roomId, workRequests.first())
|
||||
else -> {
|
||||
val firstWork = workRequests.first()
|
||||
var continuation = workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(roomId), ExistingWorkPolicy.APPEND, firstWork)
|
||||
for (i in 1 until workRequests.size) {
|
||||
val workRequest = workRequests[i]
|
||||
continuation = continuation.then(workRequest)
|
||||
}
|
||||
continuation.enqueue()
|
||||
CancelableWork(workManagerProvider.workManager, firstWork.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND_OR_REPLACE): Cancelable {
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(buildWorkName(roomId), policy, workRequest)
|
||||
|
@ -77,11 +58,6 @@ internal class TimelineSendEventWorkCommon @Inject constructor(
|
|||
return "${roomId}_$SEND_WORK"
|
||||
}
|
||||
|
||||
fun cancelAllWorks(roomId: String) {
|
||||
workManagerProvider.workManager
|
||||
.cancelUniqueWork(buildWorkName(roomId))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEND_WORK = "SEND_WORK"
|
||||
private const val BACKOFF_DELAY = 10_000L
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.sync
|
||||
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.matrix.android.sdk.R
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
@ -54,16 +57,12 @@ import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
|
|||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
|
||||
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
|
||||
import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync
|
||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSync
|
||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData
|
||||
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral
|
||||
import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -76,8 +75,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
private val roomTypingUsersHandler: RoomTypingUsersHandler,
|
||||
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
||||
@UserId private val userId: String,
|
||||
private val eventBus: EventBus,
|
||||
private val timelineEventDecryptor: TimelineEventDecryptor) {
|
||||
private val eventBus: EventBus) {
|
||||
|
||||
sealed class HandlingStrategy {
|
||||
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
|
||||
|
|
|
@ -54,7 +54,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
|||
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
||||
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
||||
private val activeCallHandler: ActiveCallHandler
|
||||
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||
) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
|
||||
|
||||
private var state: SyncState = SyncState.Idle
|
||||
private var liveState = MutableLiveData<SyncState>(state)
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="summary_message">%1$s: %2$s</string>
|
||||
<string name="summary_user_sent_image">%1$s изпрати снимка.</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee">Поканата на %s</string>
|
||||
<string name="notice_room_invite">%1$s покани %2$s</string>
|
||||
<string name="notice_room_invite_you">%1$s Ви покани</string>
|
||||
|
@ -22,7 +20,7 @@
|
|||
<string name="notice_room_name_changed">%1$s смени името на стаята на: %2$s</string>
|
||||
<string name="notice_placed_video_call">%s започна видео разговор.</string>
|
||||
<string name="notice_placed_voice_call">%s започна гласов разговор.</string>
|
||||
<string name="notice_answered_call">%s отговори на повикването.</string>
|
||||
<string name="notice_answered_call">%s отговори на обаждането.</string>
|
||||
<string name="notice_ended_call">%s прекрати разговора.</string>
|
||||
<string name="notice_made_future_room_visibility">%1$s направи бъдещата история на стаята видима за %2$s</string>
|
||||
<string name="notice_room_visibility_invited">всички членове, от момента на поканването им в нея.</string>
|
||||
|
@ -31,54 +29,39 @@
|
|||
<string name="notice_room_visibility_world_readable">всеки.</string>
|
||||
<string name="notice_room_visibility_unknown">непозната (%s).</string>
|
||||
<string name="notice_end_to_end">%1$s включи шифроване от край до край (%2$s)</string>
|
||||
|
||||
<string name="notice_requested_voip_conference">%1$s заяви VoIP групов разговор</string>
|
||||
<string name="notice_voip_started">Започна VoIP групов разговор</string>
|
||||
<string name="notice_voip_finished">Груповият разговор приключи</string>
|
||||
|
||||
<string name="notice_avatar_changed_too">(профилната снимка също беше сменена)</string>
|
||||
<string name="notice_room_name_removed">%1$s премахна името на стаята</string>
|
||||
<string name="notice_room_topic_removed">%1$s премахна темата на стаята</string>
|
||||
<string name="notice_profile_change_redacted">%1$s обнови своя профил %2$s</string>
|
||||
<string name="notice_room_third_party_invite">%1$s изпрати покана на %2$s да се присъедини към стаята</string>
|
||||
<string name="notice_room_third_party_registered_invite">%1$s прие поканата за %2$s</string>
|
||||
|
||||
<string name="notice_crypto_unable_to_decrypt">** Неуспешно разшифроване: %s **</string>
|
||||
<string name="could_not_redact">Неуспешно премахване</string>
|
||||
<string name="unable_to_send_message">Неуспешно изпращане на съобщението</string>
|
||||
|
||||
<string name="message_failed_to_upload">Неуспешно качване на снимката</string>
|
||||
|
||||
<string name="network_error">Грешка в мрежата</string>
|
||||
<string name="matrix_error">Matrix грешка</string>
|
||||
|
||||
<string name="room_error_join_failed_empty_room">В момента не е възможно да се присъедините отново към празна стая.</string>
|
||||
|
||||
<string name="encrypted_message">Шифровано съобщение</string>
|
||||
|
||||
<string name="medium_email">Имейл адрес</string>
|
||||
<string name="medium_phone_number">Телефонен номер</string>
|
||||
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Устройството на подателя не изпрати ключовете за това съобщение.</string>
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s изпрати стикер.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Покана от %s</string>
|
||||
<string name="room_displayname_room_invite">Покана за стая</string>
|
||||
<string name="room_displayname_two_members">%1$s и %2$s</string>
|
||||
|
||||
<plurals name="room_displayname_three_and_more_members">
|
||||
<item quantity="one">%1$s и 1 друг</item>
|
||||
<item quantity="other">%1$s и %2$d други</item>
|
||||
</plurals>
|
||||
|
||||
<string name="room_displayname_empty_room">Празна стая</string>
|
||||
|
||||
<string name="notice_event_redacted">Премахнато съобщение</string>
|
||||
<string name="notice_event_redacted_by">Съобщение премахнато от %1$s</string>
|
||||
<string name="notice_event_redacted_with_reason">Премахнато съобщение [причина: %1$s]</string>
|
||||
<string name="notice_event_redacted_by_with_reason">Съобщение премахнато от %1$s [причина: %2$s]</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">Начална синхронизация:
|
||||
\nИмпортиране на профил…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Начална синхронизация:
|
||||
|
@ -95,12 +78,9 @@
|
|||
\nИмпортиране на общности</string>
|
||||
<string name="initial_sync_start_importing_account_data">Начална синхронизация:
|
||||
\nИмпортиране на данни за профила</string>
|
||||
|
||||
<string name="notice_room_update">%s обнови тази стая.</string>
|
||||
|
||||
<string name="event_status_sending_message">Изпращане на съобщение…</string>
|
||||
<string name="clear_timeline_send_queue">Изчисти опашката за изпращане</string>
|
||||
|
||||
<string name="notice_room_third_party_revoked_invite">%1$s оттегли поканата за присъединяване на %2$s към стаята</string>
|
||||
<string name="notice_room_invite_no_invitee_with_reason">поканата на %1$s. Причина: %2$s</string>
|
||||
<string name="notice_room_invite_with_reason">%1$s покани %2$s. Причина: %3$s</string>
|
||||
|
@ -115,34 +95,25 @@
|
|||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s премахна поканата за присъединяване на %2$s в стаята. Причина: %3$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_with_reason">%1$s прие поканата за %2$s. Причина: %3$s</string>
|
||||
<string name="notice_room_withdraw_with_reason">%1$s оттегли поканата на %2$s. Причина: %3$s</string>
|
||||
|
||||
<plurals name="notice_room_aliases_added">
|
||||
<item quantity="one">%1$s добави %2$s като адрес за тази стая.</item>
|
||||
<item quantity="other">%1$s добави %2$s като адреси за тази стая.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="notice_room_aliases_removed">
|
||||
<item quantity="one">%1$s премахна %2$s като адрес за тази стая.</item>
|
||||
<item quantity="other">%1$s премахна %2$s като адреси за тази стая.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="notice_room_aliases_added_and_removed">%1$s добави %2$s и премахна %3$s като адреси за тази стая.</string>
|
||||
|
||||
<string name="notice_room_canonical_alias_set">%1$s настрой %2$s като основен адрес за тази стая.</string>
|
||||
<string name="notice_room_canonical_alias_unset">%1$s премахна основния адрес за тази стая.</string>
|
||||
|
||||
<string name="notice_room_guest_access_can_join">%1$s разреши на гости да се присъединяват в стаята.</string>
|
||||
<string name="notice_room_guest_access_forbidden">%1$s предотврати присъединяването на гости в стаята.</string>
|
||||
|
||||
<string name="notice_end_to_end_ok">%1$s включи шифроване от-край-до-край.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm">%1$s включи шифроване от-край-до-край (неразпознат алгоритъм %2$s).</string>
|
||||
|
||||
<string name="key_verification_request_fallback_message">%s изпрати запитване за потвърждение на ключа ви, но клиентът ви не поддържа верифициране посредством чат. Ще трябва да използвате стария метод за верифициране на ключове.</string>
|
||||
|
||||
<string name="notice_room_created">%1$s създаде стаята</string>
|
||||
<string name="summary_you_sent_image">Изпратихте снимка.</string>
|
||||
<string name="summary_you_sent_sticker">Изпратихте стикер.</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee_by_you">Ваша покана</string>
|
||||
<string name="notice_room_created_by_you">Създадохте стаята</string>
|
||||
<string name="notice_room_invite_by_you">Поканихте %1$s</string>
|
||||
|
@ -152,4 +123,94 @@
|
|||
<string name="notice_room_kick_by_you">Изгонихте %1$s</string>
|
||||
<string name="notice_room_unban_by_you">Отблокирахте %1$s</string>
|
||||
<string name="notice_room_ban_by_you">Блокирахте %1$s</string>
|
||||
</resources>
|
||||
<string name="notice_end_to_end_unknown_algorithm_by_you">Включихте шифроване от-край-до-край (непознат алгоритъм: %1$s).</string>
|
||||
<string name="notice_end_to_end_ok_by_you">Включихте шифроване от-край-до-край.</string>
|
||||
<string name="notice_direct_room_guest_access_forbidden_by_you">Спряхте възможността гости да се присъединяват в стаята.</string>
|
||||
<string name="notice_direct_room_guest_access_forbidden">%1$s спря възможността гости да се присъединяват в стаята.</string>
|
||||
<string name="notice_room_guest_access_forbidden_by_you">Спряхте възможността гости да се присъединяват в стаята.</string>
|
||||
<string name="notice_direct_room_guest_access_can_join_by_you">Позволихте на гости да се присъединяват тук.</string>
|
||||
<string name="notice_direct_room_guest_access_can_join">%1$s позволи на гости да се присъединяват тук.</string>
|
||||
<string name="notice_room_guest_access_can_join_by_you">Позволихте на гости да се присъединяват към стаята.</string>
|
||||
<string name="notice_room_canonical_alias_unset_by_you">Премахнахте основния адрес на стаята.</string>
|
||||
<string name="notice_room_canonical_alias_set_by_you">Зададохте %1$s като основен адрес на стаята.</string>
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">Добавихте %1$s и премахнахте %2$s от адресите за стаята.</string>
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">Премахнахте %1$s от адресите на стаята.</item>
|
||||
<item quantity="other">Премахнахте %1$s от адресите на стаята.</item>
|
||||
</plurals>
|
||||
<plurals name="notice_room_aliases_added_by_you">
|
||||
<item quantity="one">Добавихте %1$s като адрес за тази стая.</item>
|
||||
<item quantity="other">Добавихте %1$s като адреси за тази стая.</item>
|
||||
</plurals>
|
||||
<string name="notice_room_withdraw_with_reason_by_you">Оттеглихте поканата на %1$s. Причина: %2$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Приехте поканата за %1$s. Причина: %2$s</string>
|
||||
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Оттеглихте поканата за присъединяване в стаята от %1$s. Причина: %2$s</string>
|
||||
<string name="notice_room_third_party_invite_with_reason_by_you">Изпратихте покана към %1$s за присъединяване в стаята. Причина: %2$s</string>
|
||||
<string name="notice_room_ban_with_reason_by_you">Блокирахте %1$s. Причина: %2$s</string>
|
||||
<string name="notice_room_unban_with_reason_by_you">Отблокирахте %1$s. Причина: %2$s</string>
|
||||
<string name="notice_room_kick_with_reason_by_you">Изгонихте %1$s. Причина: %2$s</string>
|
||||
<string name="notice_room_reject_with_reason_by_you">Отхвърлихте поканата. Причина: %1$s</string>
|
||||
<string name="notice_direct_room_leave_with_reason_by_you">Напуснахте. Причина: %1$s</string>
|
||||
<string name="notice_direct_room_leave_with_reason">%1$s напусна. Причина: %2$s</string>
|
||||
<string name="notice_room_leave_with_reason_by_you">Напуснахте стаята. Причина: %1$s</string>
|
||||
<string name="notice_direct_room_join_with_reason_by_you">Присъединихте се. Причина: %1$s</string>
|
||||
<string name="notice_direct_room_join_with_reason">%1$s се присъедини. Причина: %2$s</string>
|
||||
<string name="notice_room_join_with_reason_by_you">Присъединихте се в стаята. Причина: %1$s</string>
|
||||
<string name="notice_room_invite_with_reason_by_you">Поканихте %1$s. Причина: %2$s</string>
|
||||
<string name="notice_room_invite_no_invitee_with_reason_by_you">Ваша покана. Причина: %1$s</string>
|
||||
<string name="notice_power_level_diff">%1$s от %2$s на %3$s</string>
|
||||
<string name="notice_power_level_changed">%1$s промени нивото на достъп на %2$s.</string>
|
||||
<string name="notice_power_level_changed_by_you">Променихте нивото на достъп на %1$s.</string>
|
||||
<string name="power_level_custom_no_value">Собствено ниво</string>
|
||||
<string name="power_level_custom">Собствено ниво (%1$d)</string>
|
||||
<string name="power_level_default">По подразбиране</string>
|
||||
<string name="power_level_moderator">Модератор</string>
|
||||
<string name="power_level_admin">Администратор</string>
|
||||
<string name="notice_widget_modified_by_you">Променихте %1$s приспособлението</string>
|
||||
<string name="notice_widget_modified">%1$s промени %2$s приспособлението</string>
|
||||
<string name="notice_widget_removed_by_you">Премахнахте %1$s приспособлението</string>
|
||||
<string name="notice_widget_removed">%1$s премахна %2$s приспособлението</string>
|
||||
<string name="notice_widget_added_by_you">Добавихте %1$s приспособление</string>
|
||||
<string name="notice_widget_added">%1$s добави %2$s приспособление</string>
|
||||
<string name="notice_room_third_party_registered_invite_by_you">Приехте поканата за %1$s</string>
|
||||
<string name="notice_direct_room_third_party_revoked_invite_by_you">Оттеглихте поканата от %1$s</string>
|
||||
<string name="notice_direct_room_third_party_revoked_invite">%1$s оттегли поканата от %2$s</string>
|
||||
<string name="notice_room_third_party_revoked_invite_by_you">Оттеглихте поканата за присъединяване в стаята от %1$s</string>
|
||||
<string name="notice_direct_room_third_party_invite_by_you">Поканихте %1$s</string>
|
||||
<string name="notice_direct_room_third_party_invite">%1$s покани %2$s</string>
|
||||
<string name="notice_room_third_party_invite_by_you">Изпратихте покана към %1$s за присъединяване в стаята</string>
|
||||
<string name="notice_profile_change_redacted_by_you">Обновихте профила си %1$s</string>
|
||||
<string name="notice_room_avatar_removed_by_you">Премахнахте снимката на стаята</string>
|
||||
<string name="notice_room_avatar_removed">%1$s премахна снимката на стаята</string>
|
||||
<string name="notice_room_topic_removed_by_you">Премахнахте темата на стаята</string>
|
||||
<string name="notice_room_name_removed_by_you">Премахнахте името на стаята</string>
|
||||
<string name="notice_requested_voip_conference_by_you">Заявихте VoIP конференция</string>
|
||||
<string name="notice_direct_room_update_by_you">Обновихте чата.</string>
|
||||
<string name="notice_direct_room_update">%s обнови чата.</string>
|
||||
<string name="notice_room_update_by_you">Обновихте стаята.</string>
|
||||
<string name="notice_end_to_end_by_you">Включихте шифроване от-край-до-край (%1$s)</string>
|
||||
<string name="notice_made_future_direct_room_visibility_by_you">Направихте бъдещите съобщения видими за %1$s</string>
|
||||
<string name="notice_made_future_direct_room_visibility">%1$s направи бъдещите съобщения видими за %2$s</string>
|
||||
<string name="notice_made_future_room_visibility_by_you">Направихте бъдещата история на стаята видима за %1$s</string>
|
||||
<string name="notice_ended_call_by_you">Прекратихте разговора.</string>
|
||||
<string name="notice_placed_video_call_by_you">Започнахте видео разговор.</string>
|
||||
<string name="notice_answered_call_by_you">Отговорихте на обаждането.</string>
|
||||
<string name="notice_call_candidates_by_you">Изпратихте данни за настройка на разговора.</string>
|
||||
<string name="notice_call_candidates">%s изпрати данни за настройка на разговора.</string>
|
||||
<string name="notice_placed_voice_call_by_you">Започнахте гласов разговор.</string>
|
||||
<string name="notice_room_name_changed_by_you">Променихте името на стаята на: %1$s</string>
|
||||
<string name="notice_room_avatar_changed_by_you">Променихте снимката на стаята</string>
|
||||
<string name="notice_room_avatar_changed">%1$s промени снимката на стаята</string>
|
||||
<string name="notice_room_topic_changed_by_you">Променихте темата на: %1$s</string>
|
||||
<string name="notice_display_name_removed_by_you">Премахнахте името си (%1$s)</string>
|
||||
<string name="notice_display_name_changed_from_by_you">Променихте името си от %1$s на %2$s</string>
|
||||
<string name="notice_display_name_set_by_you">Променихте името си на %1$s</string>
|
||||
<string name="notice_avatar_url_changed_by_you">Променихте снимката си</string>
|
||||
<string name="notice_room_withdraw_by_you">Оттеглихте поканата от %1$s</string>
|
||||
<string name="notice_direct_room_leave_by_you">Напуснахте стаята</string>
|
||||
<string name="notice_direct_room_leave">%1$s напусна стаята</string>
|
||||
<string name="notice_direct_room_join_by_you">Присъединихте се</string>
|
||||
<string name="notice_direct_room_join">%1$s се присъедини</string>
|
||||
<string name="notice_direct_room_created_by_you">Създадохте дискусията</string>
|
||||
<string name="notice_direct_room_created">%1$s създаде дискусията</string>
|
||||
</resources>
|
|
@ -174,7 +174,7 @@
|
|||
</plurals>
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">আপনি এই ঘরের ঠিকানা হিসাবে %1$s সরিয়েছেন।</item>
|
||||
<item quantity="other">আপনি এই ঘরের ঠিকানা হিসাবে %2$s গুলি সরিয়েছেন।</item>
|
||||
<item quantity="other">আপনি এই ঘরের ঠিকানা হিসাবে %1$s গুলি সরিয়েছেন।</item>
|
||||
</plurals>
|
||||
<string name="notice_room_aliases_added_and_removed">%1$s %2$s যোগ করেছে এবং %3$s গুলি এই ঘরের ঠিকানা হিসাবে সরানো হয়েছে।</string>
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">আপনি %1$s যোগ করেছেন এবং %2$s কে এই ঘরের ঠিকানা হিসাবে সরিয়ে দিয়েছেন।</string>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="summary_message">%1$s: %2$s</string>
|
||||
<string name="summary_user_sent_image">Uživatel %1$s poslal obrázek.</string>
|
||||
<string name="summary_user_sent_sticker">Uživatel %1$s poslal nálepku.</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee">Pozvání od uživatele %s</string>
|
||||
<string name="notice_room_invite">Uživatel %1$s pozval uživatele %2$s</string>
|
||||
<string name="notice_room_invite_you">Uživatel %1$s vás pozval</string>
|
||||
|
@ -31,51 +30,36 @@
|
|||
<string name="notice_room_visibility_world_readable">kohokoliv.</string>
|
||||
<string name="notice_room_visibility_unknown">neznámým (%s).</string>
|
||||
<string name="notice_end_to_end">%1$s zapnuli end-to-end šifrování (%2$s)</string>
|
||||
|
||||
<string name="notice_requested_voip_conference">%1$s požádali o VoIP konferenci</string>
|
||||
<string name="notice_voip_started">Začala VoIP konference</string>
|
||||
<string name="notice_voip_finished">VoIP konference skončila</string>
|
||||
|
||||
<string name="notice_avatar_changed_too">(profilový obrázek byl také změněn)</string>
|
||||
<string name="notice_room_name_removed">%1$s odstranili název místnosti</string>
|
||||
<string name="notice_room_topic_removed">%1$s odstranili téma místnosti</string>
|
||||
<string name="notice_profile_change_redacted">%1$s aktualizovali svůj profil %2$s</string>
|
||||
<string name="notice_room_third_party_invite">%1$s do této místnosti pozvali %2$s</string>
|
||||
<string name="notice_room_third_party_registered_invite">%1$s přijali pozvání pro %2$s</string>
|
||||
|
||||
<string name="notice_crypto_unable_to_decrypt">** Nelze dešifrovat: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">Odesílatelovo zařízení nám neposlalo klíče pro tuto zprávu.</string>
|
||||
|
||||
<string name="could_not_redact">Nelze vymazat</string>
|
||||
<string name="unable_to_send_message">Zprávu nelze odeslat</string>
|
||||
|
||||
<string name="message_failed_to_upload">Obrázek nelze nahrát</string>
|
||||
|
||||
<string name="network_error">Chyba sítě</string>
|
||||
<string name="matrix_error">Chyba v Matrixu</string>
|
||||
|
||||
<string name="room_error_join_failed_empty_room">V současnosti není možné znovu vstoupit do prázdné místnosti.</string>
|
||||
|
||||
<string name="encrypted_message">Šifrovaná zpráva</string>
|
||||
|
||||
<string name="medium_email">E-mailová adresa</string>
|
||||
<string name="medium_phone_number">Telefonní číslo</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Pozvání od %s</string>
|
||||
<string name="room_displayname_room_invite">Pozvání do místnosti</string>
|
||||
|
||||
<string name="room_displayname_two_members">%1$s a %2$s</string>
|
||||
|
||||
<plurals name="room_displayname_three_and_more_members">
|
||||
<item quantity="one">%1$s a jeden další</item>
|
||||
<item quantity="few">%1$s a %2$d další</item>
|
||||
<item quantity="other">%1$s a %2$d dalších</item>
|
||||
</plurals>
|
||||
|
||||
<string name="room_displayname_empty_room">Prázdná místnost</string>
|
||||
|
||||
<string name="notice_room_update">%s povýšili tuto místnost.</string>
|
||||
|
||||
<string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string>
|
||||
<string name="notice_event_redacted_by_with_reason">Zpráva smazána uživatelem %1$s [důvod: %2$s]</string>
|
||||
<string name="notice_room_third_party_revoked_invite">%1$s zrušili pozvánku do místnosti pro %2$s</string>
|
||||
|
@ -93,13 +77,10 @@
|
|||
\nImportuji komunity</string>
|
||||
<string name="initial_sync_start_importing_account_data">Úvodní synchronizace:
|
||||
\nImportuji data účtu</string>
|
||||
|
||||
<string name="event_status_sending_message">Odesílám zprávu…</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Úvodní synchronizace:
|
||||
\nImportuji pozvání</string>
|
||||
<string name="clear_timeline_send_queue">Vymazat frontu neodeslaných zpráv</string>
|
||||
|
||||
<string name="notice_room_invite_with_reason">%1$s pozvali %2$s. Důvod: %3$s</string>
|
||||
<string name="notice_room_invite_you_with_reason">%1$s vás pozvali. Důvod: %2$s</string>
|
||||
<string name="notice_room_leave_with_reason">%1$s opustil místnost. Důvod: %2$s</string>
|
||||
|
@ -107,7 +88,6 @@
|
|||
<string name="notice_event_redacted_by">Zprávu odstranil/a %1$s</string>
|
||||
<string name="summary_you_sent_image">Poslali jste obrázek.</string>
|
||||
<string name="summary_you_sent_sticker">Poslali jste nálepku.</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee_by_you">Vaše pozvání</string>
|
||||
<string name="notice_room_created">%1$s založil místnost</string>
|
||||
<string name="notice_room_created_by_you">Vy jste založili místnost</string>
|
||||
|
@ -136,7 +116,6 @@
|
|||
<string name="notice_made_future_room_visibility_by_you">Učinili jste budoucí historii místnosti viditelnou pro %1$s</string>
|
||||
<string name="notice_end_to_end_by_you">Zapnuli jste end-to-end šifrování (%1$s)</string>
|
||||
<string name="notice_room_update_by_you">Povýšili jste tuto místnost.</string>
|
||||
|
||||
<string name="notice_requested_voip_conference_by_you">Požádali jste o VoIP konferenci</string>
|
||||
<string name="notice_room_name_removed_by_you">Odstranili jste jméno místnosti</string>
|
||||
<string name="notice_room_topic_removed_by_you">Odstranili jste téma místnosti</string>
|
||||
|
@ -146,24 +125,20 @@
|
|||
<string name="notice_room_third_party_invite_by_you">Poslali jste %1$s pozvání ke vstupu do místnosti</string>
|
||||
<string name="notice_room_third_party_revoked_invite_by_you">Zrušili jste pozvánku ke vstupu do místnosti pro %1$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_by_you">Přijali jste pozvání pro %1$s</string>
|
||||
|
||||
<string name="notice_widget_added">%1$s přidali widget %2$s</string>
|
||||
<string name="notice_widget_added_by_you">Přidali jste widget %1$s</string>
|
||||
<string name="notice_widget_removed">%1$s odstranili widget %2$s</string>
|
||||
<string name="notice_widget_removed_by_you">Odstranili jste widget %1$s</string>
|
||||
<string name="notice_widget_modified">%1$s změnil widget %2$s</string>
|
||||
<string name="notice_widget_modified_by_you">Změnili jste widget %1$s</string>
|
||||
|
||||
<string name="power_level_admin">Správce</string>
|
||||
<string name="power_level_moderator">Moderátor</string>
|
||||
<string name="power_level_default">Výchozí</string>
|
||||
<string name="power_level_custom">Vlastní (%1$d)</string>
|
||||
<string name="power_level_custom_no_value">Vlastní</string>
|
||||
|
||||
<string name="notice_power_level_changed_by_you">Změnili jste %1$s stupeň oprávnění.</string>
|
||||
<string name="notice_power_level_changed">%1$s změnili %2$s stupeň oprávnění.</string>
|
||||
<string name="notice_power_level_diff">%1$s z %2$s na %3$s</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee_with_reason">Pozvání od %1$s. Důvod: %2$s</string>
|
||||
<string name="notice_room_invite_no_invitee_with_reason_by_you">Vaše pozvání. Důvod: %1$s</string>
|
||||
<string name="notice_room_invite_with_reason_by_you">Pozvali jste %1$s. Důvod: %2$s</string>
|
||||
|
@ -186,49 +161,39 @@
|
|||
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Přijali jste pozvání pro %1$s. Důvod: %2$s</string>
|
||||
<string name="notice_room_withdraw_with_reason">%1$s zrušili pozvání pro %2$s. Důvod: %3$s</string>
|
||||
<string name="notice_room_withdraw_with_reason_by_you">Zrušili jste pozvání od %1$s. Důvod: %2$s</string>
|
||||
|
||||
<plurals name="notice_room_aliases_added">
|
||||
<item quantity="one">%1$s přidali %2$s jako adresu pro tuto místnost.</item>
|
||||
<item quantity="few">%1$s přidali %2$s jako adresy pro tuto místnost.</item>
|
||||
<item quantity="other">%1$s přidali %2$s jako adresy pro tuto místnost.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="notice_room_aliases_added_by_you">
|
||||
<item quantity="one">Přidali jste %1$s jako adresu pro tuto místnost.</item>
|
||||
<item quantity="few">Přidali jste %1$s jako adresy pro tuto místnost.</item>
|
||||
<item quantity="other">Přidali jste %1$s jako adresy pro tuto místnost.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="notice_room_aliases_removed">
|
||||
<item quantity="one">%1$s odstranili %2$s jako adresu pro tuto místnost.</item>
|
||||
<item quantity="few">%1$s odstranili %2$s jako adresy pro tuto místnost.</item>
|
||||
<item quantity="other">%1$s odstranili %2$s jako adresy pro tuto místnost.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">Odstranili jste %2$s jako adresu pro tuto místnost.</item>
|
||||
<item quantity="few">Odstranili jste %2$s jako adresuy pro tuto místnost.</item>
|
||||
<item quantity="other">Odstranili jste %2$s jako adresy pro tuto místnost.</item>
|
||||
<item quantity="one">Odstranili jste %1$s jako adresu pro tuto místnost.</item>
|
||||
<item quantity="few">Odstranili jste %1$s jako adresuy pro tuto místnost.</item>
|
||||
<item quantity="other">Odstranili jste %1$s jako adresy pro tuto místnost.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="notice_room_aliases_added_and_removed">%1$s přidali %2$ a odstranili %3$s jako adresy pro tuto místnost.</string>
|
||||
<string name="notice_room_aliases_added_and_removed">%1$s přidali %2$s a odstranili %3$s jako adresy pro tuto místnost.</string>
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">Přidali jste %1$s a odstranili %2$s jako adresy pro tuto místnost.</string>
|
||||
|
||||
<string name="notice_room_canonical_alias_set">%1$s nastavili hlavní adresu této místnosti na %2$s.</string>
|
||||
<string name="notice_room_canonical_alias_set_by_you">Nastavili jste %1$s na hlavní adresu této místnosti.</string>
|
||||
<string name="notice_room_canonical_alias_unset">%1$s odstranili hlavní adresu této místnosti.</string>
|
||||
<string name="notice_room_canonical_alias_unset_by_you">Odstranili jste hlavní adresu této místnosti.</string>
|
||||
|
||||
<string name="notice_room_guest_access_can_join">"%1$s povolili hostům vstoupit do místnosti."</string>
|
||||
<string name="notice_room_guest_access_can_join_by_you">Povolili jste hostům vstoupit do místnosti.</string>
|
||||
<string name="notice_room_guest_access_forbidden">%1$s zamezili hostům vstoupit do místnosti.</string>
|
||||
<string name="notice_room_guest_access_forbidden_by_you">Zamezili jste hostům vstoupit do místnosti.</string>
|
||||
|
||||
<string name="notice_end_to_end_ok">%1$s zapnuli end-to-end šifrování.</string>
|
||||
<string name="notice_end_to_end_ok_by_you">Zapnuli jste end-to-end šifrování.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm">%1$s zapnuli end-to-end šifrování (neznámý algoritmus %2$s).</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm_by_you">Zapnuli jste end-to-end šifrování (neznámý algoritmus %2$s).</string>
|
||||
|
||||
<string name="notice_end_to_end_unknown_algorithm_by_you">Zapnuli jste end-to-end šifrování (neznámý algoritmus %1$s).</string>
|
||||
<string name="key_verification_request_fallback_message">%s žádá ověření Vašeho klíče, ale Váš klient nepodporuje ověření klíče v chatu. Budete muset k ověření klíčů použít zastaralý způsob ověření.</string>
|
||||
|
||||
</resources>
|
||||
</resources>
|
|
@ -73,14 +73,22 @@
|
|||
<string name="notice_room_update">%s hat diesen Raum aufgewertet.</string>
|
||||
<string name="event_status_sending_message">Sende eine Nachricht…</string>
|
||||
<string name="clear_timeline_send_queue">Sendewarteschlange leeren</string>
|
||||
<string name="initial_sync_start_importing_account">Erste Synchronisation: Importiere Benutzerkonto…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Erste Synchronisation: Importiere Cryptoschlüssel</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">Erste Synchronisation: Importiere Räume</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Erste Synchronisation: Importiere betretene Räume</string>
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Erste Synchronisation: Importiere eingeladene Räume</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Erste Synchronisation: Importiere verlassene Räume</string>
|
||||
<string name="initial_sync_start_importing_account_groups">Erste Synchronisation: Importiere Gemeinschaften</string>
|
||||
<string name="initial_sync_start_importing_account_data">Erste Synchronisation: Importiere Benutzerdaten</string>
|
||||
<string name="initial_sync_start_importing_account">Erste Synchronisation:
|
||||
\nImportiere Benutzerkonto…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Erste Synchronisation:
|
||||
\nImportiere Cryptoschlüssel</string>
|
||||
<string name="initial_sync_start_importing_account_rooms">Erste Synchronisation:
|
||||
\nImportiere Räume</string>
|
||||
<string name="initial_sync_start_importing_account_joined_rooms">Erste Synchronisation:
|
||||
\nImportiere betretene Räume</string>
|
||||
<string name="initial_sync_start_importing_account_invited_rooms">Erste Synchronisation:
|
||||
\nImportiere eingeladene Räume</string>
|
||||
<string name="initial_sync_start_importing_account_left_rooms">Erste Synchronisation:
|
||||
\nImportiere verlassene Räume</string>
|
||||
<string name="initial_sync_start_importing_account_groups">Erste Synchronisation:
|
||||
\nImportiere Communities</string>
|
||||
<string name="initial_sync_start_importing_account_data">Erste Synchronisation:
|
||||
\nImportiere Benutzerdaten</string>
|
||||
<string name="notice_room_third_party_revoked_invite">%1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen</string>
|
||||
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s Einladung. Grund: %2$s</string>
|
||||
<string name="notice_room_invite_with_reason">%1$s hat %2$s eingeladen. Grund: %3$s</string>
|
||||
|
@ -107,7 +115,7 @@
|
|||
<string name="notice_room_canonical_alias_set">%1$s legt die Hauptadresse fest für diesen Raum als %2$s fest.</string>
|
||||
<string name="notice_room_canonical_alias_unset">%1$s entfernt die Hauptadresse für diesen Raum.</string>
|
||||
<string name="notice_room_guest_access_can_join">%1$s hat Gästen erlaubt den Raum zu betreten.</string>
|
||||
<string name="notice_room_guest_access_forbidden">%1$s hat Gäste unterbunden den Raum zu betreten.</string>
|
||||
<string name="notice_room_guest_access_forbidden">%1$s hat Gästen untersagt den Raum zu betreten.</string>
|
||||
<string name="notice_end_to_end_ok">%1$s aktivierte Ende-zu-Ende-Verschlüsselung.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm">%1$s aktivierte Ende-zu-Ende-Verschlüsselung (unbekannter Algorithmus %2$s).</string>
|
||||
<string name="key_verification_request_fallback_message">%s fordert zur Überprüfung deines Schlüssels auf, jedoch unterstützt dein Client nicht die Schlüsselüberprüfung im Chat. Du musst die herkömmliche Schlüsselüberprüfung verwenden, um die Schlüssel zu überprüfen.</string>
|
||||
|
@ -199,7 +207,7 @@
|
|||
<string name="notice_direct_room_third_party_revoked_invite">%1$s hat die Einladung für %2$s zurückgezogen</string>
|
||||
<string name="notice_direct_room_third_party_invite_by_you">Du hast %1$s eingeladen</string>
|
||||
<string name="notice_direct_room_third_party_invite">%1$s hat %2$s eingeladen</string>
|
||||
<string name="notice_made_future_direct_room_visibility_by_you">Du hast zukünftige Nachrichten für %2$s sichtbar gemacht</string>
|
||||
<string name="notice_made_future_direct_room_visibility_by_you">Du hast zukünftige Nachrichten für %1$s sichtbar gemacht</string>
|
||||
<string name="notice_made_future_direct_room_visibility">%1$s hat zukünftige Nachrichten für %2$s sichtbar gemacht</string>
|
||||
<string name="notice_direct_room_leave_by_you">Du hast den Raum verlassen</string>
|
||||
<string name="notice_direct_room_leave">%1$s hat den Raum verlassen</string>
|
||||
|
@ -207,4 +215,10 @@
|
|||
<string name="notice_direct_room_join">%1$s ist beigetreten</string>
|
||||
<string name="notice_direct_room_created_by_you">Du hast eine Diskussion erstellt</string>
|
||||
<string name="notice_direct_room_created">%1$s hat eine Diskussion erstellt</string>
|
||||
<string name="notice_direct_room_update">%s hat hier ein Upgrade durchgeführt.</string>
|
||||
<string name="notice_direct_room_update_by_you">Du hast hier ein Upgrade durchgeführt.</string>
|
||||
<string name="notice_direct_room_guest_access_forbidden_by_you">Du hast Gästen untersagt den Raum zu betreten.</string>
|
||||
<string name="notice_direct_room_guest_access_forbidden">%1$s hat Gästen untersagt den Raum zu betreten.</string>
|
||||
<string name="notice_direct_room_leave_with_reason_by_you">Du bist gegangen. Grund: %1$s</string>
|
||||
<string name="notice_direct_room_leave_with_reason">%1$s ist gegangen. Grund: %2$s</string>
|
||||
</resources>
|
|
@ -1,26 +1,24 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="summary_user_sent_image">%1$s sendis bildon.</string>
|
||||
<string name="summary_user_sent_sticker">%1$s sendis glumarkon.</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee">Invito de %s</string>
|
||||
<string name="notice_room_invite">%1$s invitis uzanton %2$s</string>
|
||||
<string name="notice_room_invite_you">%1$s invitis vin</string>
|
||||
<string name="notice_room_join">%1$s alvenis</string>
|
||||
<string name="notice_room_leave">%1$s foriris</string>
|
||||
<string name="notice_room_reject">%1$s malakceptis la inviton</string>
|
||||
<string name="notice_room_join">%1$s envenis</string>
|
||||
<string name="notice_room_leave">%1$s foriris de la ĉambro</string>
|
||||
<string name="notice_room_reject">%1$s rifuzis la inviton</string>
|
||||
<string name="notice_room_kick">%1$s forpelis uzanton %2$s</string>
|
||||
<string name="notice_room_unban">%1$s malforbaris uzanton %2$s</string>
|
||||
<string name="notice_room_ban">%1$s forbaris uzanton %2$s</string>
|
||||
<string name="notice_room_withdraw">%1$s nuligis inviton por %2$s</string>
|
||||
<string name="notice_avatar_url_changed">%1$s ŝanĝis sian profilbildon</string>
|
||||
<string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string>
|
||||
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendinto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string>
|
||||
<string name="summary_message">%1$s: %2$s</string>
|
||||
<string name="notice_display_name_set">%1$s ŝanĝis sian vidigan nomon al %2$s</string>
|
||||
<string name="notice_display_name_changed_from">%1$s ŝanĝis sian vidigan nomon de %2$s al %3$s</string>
|
||||
<string name="notice_display_name_removed">%1$s forigis sian vidigan nomon (%2$s)</string>
|
||||
<string name="notice_display_name_set">%1$s ŝanĝis sian prezentan nomon al %2$s</string>
|
||||
<string name="notice_display_name_changed_from">%1$s ŝanĝis sian prezentan nomon de %2$s al %3$s</string>
|
||||
<string name="notice_display_name_removed">%1$s forigis sian prezentan nomon (%2$s)</string>
|
||||
<string name="notice_room_topic_changed">%1$s ŝanĝis la temon al: %2$s</string>
|
||||
<string name="notice_room_name_changed">%1$s ŝanĝis nomon de la ĉambro al: %2$s</string>
|
||||
<string name="notice_placed_video_call">%s vidvokis.</string>
|
||||
|
@ -28,50 +26,38 @@
|
|||
<string name="notice_answered_call">%s respondis la vokon.</string>
|
||||
<string name="notice_ended_call">%s finis la vokon.</string>
|
||||
<string name="notice_made_future_room_visibility">%1$s videbligis estontan historion de ĉambro al %2$s</string>
|
||||
<string name="notice_room_visibility_invited">ĉiuj ĉambranoj, ekde iliaj invitoj.</string>
|
||||
<string name="notice_room_visibility_joined">ĉiuj ĉambranoj, ekde iliaj aliĝoj.</string>
|
||||
<string name="notice_room_visibility_invited">ĉiuj ĉambranoj, ekde siaj invitoj.</string>
|
||||
<string name="notice_room_visibility_joined">ĉiuj ĉambranoj, ekde siaj aliĝoj.</string>
|
||||
<string name="notice_room_visibility_shared">ĉiuj ĉambranoj.</string>
|
||||
<string name="notice_room_visibility_world_readable">ĉiu ajn.</string>
|
||||
<string name="notice_room_visibility_unknown">nekonata (%s).</string>
|
||||
<string name="notice_end_to_end">%1$s ŝaltis tutvojan ĉifradon (%2$s)</string>
|
||||
<string name="notice_room_update">%s gradaltigis la ĉambron.</string>
|
||||
|
||||
<string name="notice_event_redacted">Mesaĝo foriĝis</string>
|
||||
<string name="notice_event_redacted_by">Mesaĝo foriĝis de %1$s</string>
|
||||
<string name="notice_event_redacted_by">Mesaĝon forigis %1$s</string>
|
||||
<string name="notice_event_redacted_with_reason">Mesaĝo foriĝis [kialo: %1$s]</string>
|
||||
<string name="notice_event_redacted_by_with_reason">Mesaĝo foriĝis de %1$s [kialo: %2$s]</string>
|
||||
<string name="notice_event_redacted_by_with_reason">Mesaĝon forigis %1$s [kialo: %2$s]</string>
|
||||
<string name="notice_profile_change_redacted">%1$s ĝisdatigis sian profilon %2$s</string>
|
||||
<string name="notice_room_third_party_invite">%1$s sendis aliĝan inviton al %2$s</string>
|
||||
<string name="notice_room_third_party_revoked_invite">%1$s nuligis la aliĝan inviton por %2$s</string>
|
||||
<string name="notice_room_third_party_registered_invite">%1$s akceptis la inviton por %2$s</string>
|
||||
|
||||
<string name="could_not_redact">Ne povis redakti</string>
|
||||
<string name="unable_to_send_message">Ne povas sendi mesaĝon</string>
|
||||
|
||||
<string name="message_failed_to_upload">Malsukcesis alŝuti bildon</string>
|
||||
|
||||
<string name="network_error">Reta eraro</string>
|
||||
<string name="matrix_error">Matrix-eraro</string>
|
||||
|
||||
<string name="room_error_join_failed_empty_room">Nun ne eblas re-aliĝi al malplena ĉambro</string>
|
||||
|
||||
<string name="room_error_join_failed_empty_room">Nun ne eblas re-aliĝi al malplena ĉambro.</string>
|
||||
<string name="encrypted_message">Ĉifrita mesaĝo</string>
|
||||
|
||||
<string name="medium_email">Retpoŝtadreso</string>
|
||||
<string name="medium_phone_number">Telefonnumero</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Invito de %s</string>
|
||||
<string name="room_displayname_room_invite">Ĉambra invito</string>
|
||||
|
||||
<string name="room_displayname_room_invite">Invito al ĉambro</string>
|
||||
<string name="room_displayname_two_members">%1$s kaj %2$s</string>
|
||||
|
||||
<plurals name="room_displayname_three_and_more_members">
|
||||
<item quantity="one">%1$s kaj 1 alia</item>
|
||||
<item quantity="other">%1$s kaj %2$d aliaj</item>
|
||||
</plurals>
|
||||
|
||||
<string name="room_displayname_empty_room">Malplena ĉambro</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">Komenca spegulado:
|
||||
\nEnportante konton…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Komenca spegulado:
|
||||
|
@ -88,52 +74,143 @@
|
|||
\nEnportante komunumojn</string>
|
||||
<string name="initial_sync_start_importing_account_data">Komenca spegulado:
|
||||
\nEnportante datumojn de konto</string>
|
||||
|
||||
<string name="event_status_sending_message">Sendante mesaĝon…</string>
|
||||
<string name="clear_timeline_send_queue">Vakigi sendan atendovicon</string>
|
||||
|
||||
<string name="notice_requested_voip_conference">%1$s petis grupan vokon</string>
|
||||
<string name="notice_voip_started">Grupa voko komenciĝis</string>
|
||||
<string name="notice_voip_finished">Grupa voko finiĝis</string>
|
||||
|
||||
<string name="notice_avatar_changed_too">(ankaŭ profilbildo ŝanĝiĝis)</string>
|
||||
<string name="notice_room_name_removed">%1$s forigis nomon de la ĉambro</string>
|
||||
<string name="notice_room_topic_removed">%1$s forigis temon de la ĉambro</string>
|
||||
<string name="notice_room_invite_no_invitee_with_reason">Invito de %1$s. Kialo: %2$s</string>
|
||||
<string name="notice_room_invite_with_reason">%1$s invitis uzanton %2$s. Kialo: %3$s</string>
|
||||
<string name="notice_room_invite_you_with_reason">%1$s invitis vin. Kialo: %2$s</string>
|
||||
<string name="notice_room_join_with_reason">%1$s aliĝis al la ĉambro. Kialo: %2$s</string>
|
||||
<string name="notice_room_join_with_reason">%1$s envenis. Kialo: %2$s</string>
|
||||
<string name="notice_room_leave_with_reason">%1$s foriris de la ĉambro. Kialo: %2$s</string>
|
||||
<string name="notice_room_reject_with_reason">%1$s rifuzis la inviton. Kialo: %2$s</string>
|
||||
<string name="notice_room_kick_with_reason">%1$s forpelis uzanton %2$s. Kialo: %3$s</string>
|
||||
<string name="notice_room_unban_with_reason">%1$s malforbaris uzanton %2$s. Kialo: %3$s</string>
|
||||
<string name="notice_room_ban_with_reason">%1$s forbaris uzanton %2$s. Kialo: %3$s</string>
|
||||
<string name="notice_room_third_party_invite_with_reason">%1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s</string>
|
||||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s</string>
|
||||
<string name="notice_room_third_party_invite_with_reason">%1$s sendis al %2$s inviton al la ĉambro. Kialo: %3$s</string>
|
||||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s nuligis la inviton al la ĉambro por %2$s. Kialo: %3$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_with_reason">%1$s akceptis la inviton por %2$s. Kialo: %3$s</string>
|
||||
<string name="notice_room_withdraw_with_reason">%1$s nuligis la inviton al %2$s. Kialo: %3$s</string>
|
||||
|
||||
<string name="notice_room_withdraw_with_reason">%1$s nuligis la inviton por %2$s. Kialo: %3$s</string>
|
||||
<plurals name="notice_room_aliases_added">
|
||||
<item quantity="one">%1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro.</item>
|
||||
<item quantity="other">%1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="notice_room_aliases_removed">
|
||||
<item quantity="one">%1$s forigis %2$s kiel adreson por ĉi tiu ĉambro.</item>
|
||||
<item quantity="other">%1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="notice_room_aliases_added_and_removed">%1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro.</string>
|
||||
|
||||
<string name="notice_room_canonical_alias_set">%1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s.</string>
|
||||
<string name="notice_room_canonical_alias_set">%1$s agordis la ĉefadreson de ĉi tiu ĉambro al %2$s.</string>
|
||||
<string name="notice_room_canonical_alias_unset">%1$s forigis la ĉefadreson de ĉi tiu ĉambro.</string>
|
||||
|
||||
<string name="notice_room_guest_access_can_join">%1$s permesis al gastoj aliĝi al la ĉambro.</string>
|
||||
<string name="notice_room_guest_access_forbidden">%1$s malpermesis al gastoj aliĝi al la ĉambro.</string>
|
||||
|
||||
<string name="notice_room_guest_access_can_join">%1$s permesis al gastoj enveni.</string>
|
||||
<string name="notice_room_guest_access_forbidden">%1$s malpermesis al gastoj enveni.</string>
|
||||
<string name="notice_end_to_end_ok">%1$s ŝaltis tutvojan ĉifradon.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm">%1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s).</string>
|
||||
|
||||
<string name="key_verification_request_fallback_message">%s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de ŝlosiloj.</string>
|
||||
|
||||
</resources>
|
||||
<string name="key_verification_request_fallback_message">%s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovan kontrolmanieron de ŝlosiloj.</string>
|
||||
<string name="notice_power_level_changed_by_you">Vi ŝanĝis la povnivelon de %1$s.</string>
|
||||
<string name="notice_power_level_changed">%1$s sanĝis la povnivelon de %2$s.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm_by_you">Vi ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %1$s).</string>
|
||||
<string name="notice_end_to_end_ok_by_you">Vi ŝaltis tutvojan ĉifradon.</string>
|
||||
<string name="notice_direct_room_guest_access_forbidden_by_you">Vi malpermesis al gastoj aliĝi.</string>
|
||||
<string name="notice_direct_room_guest_access_forbidden">%1$s malpermesis al gastoj aliĝi.</string>
|
||||
<string name="notice_room_guest_access_forbidden_by_you">Vi malpermesis al gastoj enveni.</string>
|
||||
<string name="notice_direct_room_guest_access_can_join_by_you">Vi permesis al gastoj aliĝi.</string>
|
||||
<string name="notice_direct_room_guest_access_can_join">%1$s permesis al gastoj aliĝi.</string>
|
||||
<string name="notice_room_guest_access_can_join_by_you">Vi permesis al gastoj enveni.</string>
|
||||
<string name="notice_room_canonical_alias_unset_by_you">Vi forigis la ĉefadreson de ĉi tiu ĉambro.</string>
|
||||
<string name="notice_room_canonical_alias_set_by_you">Vi agordis al ĉefadreson de ĉi tiu ĉambro al %1$s.</string>
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">Vi aldonis %1$s kaj forigis %2$s kiel adresojn por ĉi tiu ĉambro.</string>
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">Vi forigis %1$s kiel adreson por ĉi tiu ĉambro.</item>
|
||||
<item quantity="other">Vi forigis %1$s kiel adresojn por ĉi tiu ĉambro.</item>
|
||||
</plurals>
|
||||
<plurals name="notice_room_aliases_added_by_you">
|
||||
<item quantity="one">Vi aldonis %1$s kiel adreson por ĉi tiu ĉambro.</item>
|
||||
<item quantity="other">Vi aldonis %1$s kiel adresojn por ĉi tiu ĉambro.</item>
|
||||
</plurals>
|
||||
<string name="notice_room_withdraw_with_reason_by_you">Vi nuligis la inviton por %1$s. Kialo: %2$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Vi akceptis la inviton por %1$s. Kialo: %2$s</string>
|
||||
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Vi nuligis inviton al la ĉambro por %1$s. Kialo: %2$s</string>
|
||||
<string name="notice_room_third_party_invite_with_reason_by_you">Vi sendis al %1$s inviton al la ĉambro. Kialo: %2$s</string>
|
||||
<string name="notice_room_ban_with_reason_by_you">Vi forbaris uzanton %1$s. Kialo: %2$s</string>
|
||||
<string name="notice_room_unban_with_reason_by_you">Vi malforbaris uzanton %1$s. Kialo: %2$s</string>
|
||||
<string name="notice_room_kick_with_reason_by_you">Vi forpelis uzanton %1$s. Kialo: %2$s</string>
|
||||
<string name="notice_room_reject_with_reason_by_you">Vi rifuzis la inviton. Kialo: %1$s</string>
|
||||
<string name="notice_direct_room_leave_with_reason_by_you">Vi foriris. Kialo: %1$s</string>
|
||||
<string name="notice_direct_room_leave_with_reason">%1$s foriris. Kialo: %2$s</string>
|
||||
<string name="notice_room_leave_with_reason_by_you">Vi foriris de la ĉambro. Kialo: %1$s</string>
|
||||
<string name="notice_room_join_with_reason_by_you">Vi envenis. Kialo: %1$s</string>
|
||||
<string name="notice_direct_room_join_with_reason_by_you">Vi aliĝis. Kialo: %1$s</string>
|
||||
<string name="notice_direct_room_join_with_reason">%1$s aliĝis. Kialo: %2$s</string>
|
||||
<string name="notice_room_invite_with_reason_by_you">Vi invitis uzanton %1$s. Kialo: %2$s</string>
|
||||
<string name="notice_room_invite_no_invitee_with_reason_by_you">Via invito. Kialo: %1$s</string>
|
||||
<string name="notice_power_level_diff">%1$s de %2$s al %3$s</string>
|
||||
<string name="power_level_custom_no_value">Propra</string>
|
||||
<string name="power_level_default">Ordinara</string>
|
||||
<string name="power_level_custom">Propra (%1$d)</string>
|
||||
<string name="power_level_moderator">Reguligisto</string>
|
||||
<string name="power_level_admin">Administranto</string>
|
||||
<string name="notice_widget_modified_by_you">Vi ŝanĝis la fenestraĵon %1$s</string>
|
||||
<string name="notice_widget_modified">%1$s ŝanĝis la fenestraĵon %2$s</string>
|
||||
<string name="notice_widget_removed_by_you">Vi forigis la fenestraĵon %1$s</string>
|
||||
<string name="notice_widget_removed">%1$s forigis la fenestraĵon %2$s</string>
|
||||
<string name="notice_widget_added_by_you">Vi aldonis la fenestraĵon %1$s</string>
|
||||
<string name="notice_widget_added">%1$s aldonis la fenestraĵon %2$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_by_you">Vi akceptis la inviton por %1$s</string>
|
||||
<string name="notice_direct_room_third_party_revoked_invite_by_you">Vi nuligis la inviton por %1$s</string>
|
||||
<string name="notice_direct_room_third_party_revoked_invite">%1$s nuligis la inviton por %2$s</string>
|
||||
<string name="notice_room_third_party_revoked_invite_by_you">Vi nuligis la aliĝan inviton por %1$s</string>
|
||||
<string name="notice_direct_room_third_party_invite_by_you">Vi invitis uzanton %1$s</string>
|
||||
<string name="notice_direct_room_third_party_invite">%1$s invitis uzanton %2$s</string>
|
||||
<string name="notice_room_third_party_invite_by_you">Vi sendis aliĝan inviton al %1$s</string>
|
||||
<string name="notice_profile_change_redacted_by_you">Vi ĝisdatigis vian profilon %1$s</string>
|
||||
<string name="notice_room_avatar_removed_by_you">Vi forigis bildon de la ĉambro</string>
|
||||
<string name="notice_room_avatar_removed">%1$s forigis bildon de la ĉambro</string>
|
||||
<string name="notice_room_topic_removed_by_you">Vi forigis temon de la ĉambro</string>
|
||||
<string name="notice_room_name_removed_by_you">Vi forigis nomon de la ĉambro</string>
|
||||
<string name="notice_requested_voip_conference_by_you">Vi petis grupan vokon</string>
|
||||
<string name="notice_direct_room_update_by_you">Vi gradaltigis la interparolon.</string>
|
||||
<string name="notice_direct_room_update">%s gradaltigis la interparolon.</string>
|
||||
<string name="notice_room_update_by_you">Vi gradaltigis la ĉambron.</string>
|
||||
<string name="notice_end_to_end_by_you">Vi ŝaltis tutvojan ĉifradon (%1$s)</string>
|
||||
<string name="notice_made_future_direct_room_visibility">%1$s videbligis al %2$s estontajn mesaĝojn</string>
|
||||
<string name="notice_made_future_direct_room_visibility_by_you">Vi videbligis al %1$s estontajn mesaĝojn</string>
|
||||
<string name="notice_made_future_room_visibility_by_you">Vi videbligis estontan historion de ĉambro al %1$s</string>
|
||||
<string name="notice_ended_call_by_you">Vi finis la vokon.</string>
|
||||
<string name="notice_answered_call_by_you">Vi respondis la vokon.</string>
|
||||
<string name="notice_call_candidates_by_you">Vi sendis datumojn por prepari la vokon.</string>
|
||||
<string name="notice_call_candidates">%s sendis datumojn por prepari la vokon.</string>
|
||||
<string name="notice_placed_voice_call_by_you">Vi voĉvokis.</string>
|
||||
<string name="notice_placed_video_call_by_you">Vi vidvokis.</string>
|
||||
<string name="notice_room_name_changed_by_you">Vi ŝanĝis la nomon de la ĉambro al: %1$s</string>
|
||||
<string name="notice_room_avatar_changed_by_you">Vi ŝanĝis la bildon de la ĉambro</string>
|
||||
<string name="notice_room_avatar_changed">%1$s ŝanĝis la bildon de la ĉambro</string>
|
||||
<string name="notice_room_topic_changed_by_you">Vi ŝanĝis la temon al: %1$s</string>
|
||||
<string name="notice_display_name_removed_by_you">Vi forigis vian prezentan nomon (%1$s)</string>
|
||||
<string name="notice_display_name_changed_from_by_you">Vi ŝanĝis vian prezentan nomon de %1$s al %2$s</string>
|
||||
<string name="notice_display_name_set_by_you">Vi ŝanĝis vian prezentan nomon al %1$s</string>
|
||||
<string name="notice_avatar_url_changed_by_you">Vi ŝanĝis vian profilbildon</string>
|
||||
<string name="notice_room_withdraw_by_you">Vi nuligis inviton por %1$s</string>
|
||||
<string name="notice_room_ban_by_you">Vi forbaris uzanton %1$s</string>
|
||||
<string name="notice_room_unban_by_you">Vi malforbaris uzanton %1$s</string>
|
||||
<string name="notice_room_kick_by_you">Vi forpelis uzanton %1$s</string>
|
||||
<string name="notice_room_reject_by_you">Vi rifuzis la inviton</string>
|
||||
<string name="notice_direct_room_leave_by_you">Vi foriris de la ĉambro</string>
|
||||
<string name="notice_direct_room_leave">%1$s foriris de la ĉambro</string>
|
||||
<string name="notice_room_leave_by_you">Vi foriris de la ĉambro</string>
|
||||
<string name="notice_direct_room_join_by_you">Vi envenis</string>
|
||||
<string name="notice_direct_room_join">%1$s envenis</string>
|
||||
<string name="notice_room_join_by_you">Vi envenis</string>
|
||||
<string name="notice_room_invite_by_you">Vi invitis uzanton %1$s</string>
|
||||
<string name="notice_direct_room_created_by_you">Vi kreis la diskuton</string>
|
||||
<string name="notice_direct_room_created">%1$s kreis la diskuton</string>
|
||||
<string name="notice_room_created_by_you">Vi kreis la ĉambron</string>
|
||||
<string name="notice_room_created">%1$s kreis la ĉambron</string>
|
||||
<string name="notice_room_invite_no_invitee_by_you">Via invito</string>
|
||||
<string name="summary_you_sent_sticker">Vi sendis glumarkon.</string>
|
||||
<string name="summary_you_sent_image">Vi sendis bildon.</string>
|
||||
</resources>
|
|
@ -226,7 +226,7 @@
|
|||
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">Quitaste %1$s como dirección para esta sala.</item>
|
||||
<item quantity="other">Quitaste %2$s como direcciones para esta sala.</item>
|
||||
<item quantity="other">Quitaste %1$s como direcciones para esta sala.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="notice_room_aliases_added_and_removed">"%1$s agregó %2$s y eliminó %3$s como direcciones para esta sala."</string>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="summary_message">%1$s: %2$s</string>
|
||||
<string name="summary_user_sent_image">%1$s تصویری فرستاد.</string>
|
||||
<string name="summary_user_sent_sticker">%1$s برچسبی فرستاد.</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee">دعوت %s</string>
|
||||
<string name="notice_room_invite">%1$s، %2$s را دعوت کرد</string>
|
||||
<string name="notice_room_invite_you">%1$s دعوتتان کرد</string>
|
||||
|
@ -32,11 +31,9 @@
|
|||
<string name="notice_room_visibility_unknown">ناشناخته (%s).</string>
|
||||
<string name="notice_end_to_end">%1$s رمزنگاری سرتاسری را روشن کرد (%2$s)</string>
|
||||
<string name="notice_room_update">%s این اتاق را ارتقا داد.</string>
|
||||
|
||||
<string name="notice_requested_voip_conference">%1$s درخواست یک گردهمایی صوتی داد</string>
|
||||
<string name="notice_voip_started">گردهمایی صوتی آغاز شد</string>
|
||||
<string name="notice_voip_finished">گردهمایی صوتی پایان یافت</string>
|
||||
|
||||
<string name="notice_avatar_changed_too">(تصویر هم عوض شد)</string>
|
||||
<string name="notice_room_name_removed">%1$s نام اتاق را پاک کرد</string>
|
||||
<string name="notice_room_topic_removed">%1$s موضوع اتاق را پاک کرد</string>
|
||||
|
@ -47,36 +44,24 @@
|
|||
<string name="notice_room_third_party_invite">%1$s دعوتی برای پیوستن %2$s به اتاق فرستاد</string>
|
||||
<string name="notice_room_third_party_revoked_invite">%1$s دعوت پیوستن به اتاق %2$s را باطل کرد</string>
|
||||
<string name="notice_room_third_party_registered_invite">%1$s دعوت برای %2$s را پذیرفت</string>
|
||||
|
||||
<string name="notice_crypto_unable_to_decrypt">** ناتوان در رمزگشایی: %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است.</string>
|
||||
|
||||
<string name="unable_to_send_message">ناتوان در فرستادن پیام</string>
|
||||
|
||||
<string name="message_failed_to_upload">شکست در بارگذاری تصویر</string>
|
||||
|
||||
<string name="network_error">خطای شبکه</string>
|
||||
<string name="matrix_error">خطای ماتریکس</string>
|
||||
|
||||
<string name="room_error_join_failed_empty_room">در حال حاضر امکان بازپیوست به اتاقی خالی وجود ندارد.</string>
|
||||
|
||||
<string name="encrypted_message">پیام رمزنگاری شده</string>
|
||||
|
||||
<string name="medium_email">نشانی رایانامه</string>
|
||||
<string name="medium_phone_number">شماره تلفن</string>
|
||||
|
||||
<string name="room_displayname_invite_from">دعوت از %s</string>
|
||||
<string name="room_displayname_room_invite">دعوت اتاق</string>
|
||||
|
||||
<string name="room_displayname_two_members">%1$s و %2$s</string>
|
||||
|
||||
<plurals name="room_displayname_three_and_more_members">
|
||||
<item quantity="one">%1$s و ۱ نفر دیگر</item>
|
||||
<item quantity="other">%1$s و %2$d نفر دیگر</item>
|
||||
</plurals>
|
||||
|
||||
<string name="room_displayname_empty_room">اتاق خالی</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">همگامسازی نخستین:
|
||||
\nدر حال درونریزی حساب…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">همگامسازی نخستین:
|
||||
|
@ -93,10 +78,8 @@
|
|||
\nدر حال درونریزی انجمنها</string>
|
||||
<string name="initial_sync_start_importing_account_data">همگامسازی نخستین:
|
||||
\nدر حال درونریزی دادههای حساب</string>
|
||||
|
||||
<string name="event_status_sending_message">در حال فرستادن پیام…</string>
|
||||
<string name="clear_timeline_send_queue">پاکسازی صفِ در حال ارسال</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee_with_reason">دعوت %1$s. دلیل: %2$s</string>
|
||||
<string name="notice_room_invite_with_reason">%1$s، %2$s را دعوت کرد. دلیل: %3$s</string>
|
||||
<string name="notice_room_invite_you_with_reason">%1$s دعوتتان کرد. دلیل: %2$s</string>
|
||||
|
@ -110,36 +93,27 @@
|
|||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s دعوت %2$s برای پیوستن به اتاق را باطل کرد. دلیل: %3$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_with_reason">%1$s دعوت برای %2$s را پذیرفت. دلیل: %3$s</string>
|
||||
<string name="notice_room_withdraw_with_reason">%1$s دعوت %2$s را نپذیرفت. دلیل: %3$s</string>
|
||||
|
||||
<plurals name="notice_room_aliases_added">
|
||||
<item quantity="one">%1$s، %2$s را به عنوان نشانیای برای این اتاق افزود.</item>
|
||||
<item quantity="other">%1$s، %2$s را به عنوان نشانیهایی برای این اتاق افزود.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="notice_room_aliases_removed">
|
||||
<item quantity="one">%1$s، %2$s را به عنوان نشانیای برای این اتاق پاک کرد.</item>
|
||||
<item quantity="other">%1$s، %3$s را به عنوان نشانیهایی برای این اتاق پاک کرد.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="notice_room_aliases_added_and_removed">%1$s برای نشانی این اتاق، %2$s را افزود و %3$s را پاک کرد.</string>
|
||||
|
||||
<string name="notice_room_canonical_alias_set">%1$s نشانی اصلی این اتاق را به %2$s تنظیم کرد.</string>
|
||||
<string name="notice_room_canonical_alias_unset">%1$s نشانی اصلی را برای این اتاق پاک کرد.</string>
|
||||
|
||||
<string name="notice_room_guest_access_can_join">%1$s اجازه داد میمهانان به گروه بپیوندند.</string>
|
||||
<string name="notice_room_guest_access_forbidden">%1$s جلوی پیوستن میمهانان به گروه را گرفت.</string>
|
||||
|
||||
<string name="notice_end_to_end_ok">%1$s رمزنگاری سرتاسری را روشن کرد.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm">%1$s رمزنگاری سرتاسری را روشن کرد (الگوریتم تشخیصدادهنشده %2$s ).</string>
|
||||
|
||||
<string name="key_verification_request_fallback_message">%s درخواست تأیید کلیدتان را دارد، ولی کارخواهتان تأیید کلید درون گپ را پشتیبانی نمیکند. برای تأیید کلیدها لازم است از تأییدیهٔ کلید قدیمی استفاده کنید.</string>
|
||||
|
||||
<string name="notice_room_created">%1$s اتاق را ایجاد کرد</string>
|
||||
<string name="notice_profile_change_redacted">%1$s نمایهاش را بهروز کرد %2$s</string>
|
||||
<string name="could_not_redact">نمیتوان ویرایش کرد</string>
|
||||
<string name="summary_you_sent_image">تصویری فرستادید.</string>
|
||||
<string name="summary_you_sent_sticker">برچسبی فرستادید.</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee_by_you">دعوتتان</string>
|
||||
<string name="notice_room_created_by_you">اتاق را ایجاد کردید</string>
|
||||
<string name="notice_room_invite_by_you">از %1$s دعوت کردید</string>
|
||||
|
@ -167,7 +141,6 @@
|
|||
<string name="notice_made_future_room_visibility_by_you">تاریخچهٔ آتی اتاق را برای %1$s نمایان کردید</string>
|
||||
<string name="notice_end_to_end_by_you">رمزنگاری سرتاسری را روشن کردید (%1$s)</string>
|
||||
<string name="notice_room_update_by_you">این اتاق را ارتقا دادید.</string>
|
||||
|
||||
<string name="notice_requested_voip_conference_by_you">دارخواست کنفرانس ویپ دادید</string>
|
||||
<string name="notice_room_name_removed_by_you">نام اتاق را برداشتید</string>
|
||||
<string name="notice_room_topic_removed_by_you">موضوع اتاق را برداشتید</string>
|
||||
|
@ -177,24 +150,20 @@
|
|||
<string name="notice_room_third_party_invite_by_you">برای %1$s دعوت پیوستن به اتاق فرستادید</string>
|
||||
<string name="notice_room_third_party_revoked_invite_by_you">دعوت پیوستن %1$s به اتاق را پس گرفتید</string>
|
||||
<string name="notice_room_third_party_registered_invite_by_you">دعوت برای %1$s را پذیرفتید</string>
|
||||
|
||||
<string name="notice_widget_added">%1$s ابزارک %2$s را افزود</string>
|
||||
<string name="notice_widget_added_by_you">ابزارک %1$s را افزودید</string>
|
||||
<string name="notice_widget_removed">%1$s ابزارک %2$s را برداشت</string>
|
||||
<string name="notice_widget_removed_by_you">ابزارک %1$s را برداشتید</string>
|
||||
<string name="notice_widget_modified">%1$s ابزارک %2$s را دستکاری کرد</string>
|
||||
<string name="notice_widget_modified_by_you">ابزارک %1$s را دستکاری کردید</string>
|
||||
|
||||
<string name="power_level_admin">مدیر</string>
|
||||
<string name="power_level_moderator">ناظم</string>
|
||||
<string name="power_level_default">پیشگزیده</string>
|
||||
<string name="power_level_custom">سفارشی (%1$d)</string>
|
||||
<string name="power_level_custom_no_value">سفارشی</string>
|
||||
|
||||
<string name="notice_power_level_changed_by_you">سطح قدرت %1$s را تغییر دادید.</string>
|
||||
<string name="notice_power_level_changed">%1$s سطح قدرت %2$s را تغییر داد.</string>
|
||||
<string name="notice_power_level_diff">%1$s از %2$s به %3$s</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee_with_reason_by_you">دعوتتان. دلیل: %1$s</string>
|
||||
<string name="notice_room_invite_with_reason_by_you">%1$s را دعوت کردید. دلیل: %2$s</string>
|
||||
<string name="notice_room_join_with_reason_by_you">به اتاق پیوستید. دلیل: %1$s</string>
|
||||
|
@ -207,26 +176,41 @@
|
|||
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">دعوت %1$s برای پیوستن به اتاق را پس گرفتید. دلیل: %2$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_with_reason_by_you">دعوت برای %1$s را پذیرفتید. دلیل: %2$s</string>
|
||||
<string name="notice_room_withdraw_with_reason_by_you">دعوت %1$s را رد کردید. دلیل: %2$s</string>
|
||||
|
||||
<plurals name="notice_room_aliases_added_by_you">
|
||||
<item quantity="one">نشانی %1$s را به این اتاق افزودید.</item>
|
||||
<item quantity="other">نشانیهای %1$s را به این اتاق افزودید.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">نشانی %1$s ار از این اتاق برداشتید.</item>
|
||||
<item quantity="other">نشانیهای %1$s ار از این اتاق برداشتید.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">نشانی %1$s ار افزوده و %2$s را از این اتاق برداشتید.</string>
|
||||
|
||||
<string name="notice_room_canonical_alias_set_by_you">نشانی اصلی این اتاق را به %1$s تنظیم کردید.</string>
|
||||
<string name="notice_room_canonical_alias_unset_by_you">نشانی اصلی این اتاق را برداشتید.</string>
|
||||
|
||||
<string name="notice_room_guest_access_can_join_by_you">به میهمانان اجازهٔ پیوستن به گروه دادید.</string>
|
||||
<string name="notice_room_guest_access_forbidden_by_you">میمهانان را از پیوستن به گروه بازداشتید.</string>
|
||||
|
||||
<string name="notice_end_to_end_ok_by_you">رمزنگاری سرتاسری را روشن کردید.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm_by_you">رمزنگاری سرتاسری را روشن کردید (الگوریتم ناشناخته %1$s).</string>
|
||||
|
||||
</resources>
|
||||
<string name="notice_direct_room_guest_access_forbidden_by_you">مهمانها را از پیوستن به اتاق بازداشتید.</string>
|
||||
<string name="notice_direct_room_guest_access_forbidden">%1$s مهمانها را از پیوستن به اتاق بازداشت.</string>
|
||||
<string name="notice_direct_room_guest_access_can_join_by_you">به مهمانها اجازه دادید به اینجا بپیوندند.</string>
|
||||
<string name="notice_direct_room_guest_access_can_join">%1$s به مهمانها اجازه داد به اینجا بپیوندند.</string>
|
||||
<string name="notice_direct_room_leave_with_reason_by_you">رفتید. دلیل: %1$s</string>
|
||||
<string name="notice_direct_room_leave_with_reason">%1$s رفت. دلیل: %2$s</string>
|
||||
<string name="notice_direct_room_join_with_reason_by_you">پیوستید. دلیل: %1$s</string>
|
||||
<string name="notice_direct_room_join_with_reason">%1$sپیوست. دلیل: %2$s</string>
|
||||
<string name="notice_direct_room_third_party_revoked_invite_by_you">دعوت %1$s را پس گرفتید</string>
|
||||
<string name="notice_direct_room_third_party_revoked_invite">%1$s دعوت %2$s را پس گرفت</string>
|
||||
<string name="notice_direct_room_third_party_invite_by_you">%1$s را دعوت کردید</string>
|
||||
<string name="notice_direct_room_third_party_invite">%1$s، %2$s را دعوت کرد</string>
|
||||
<string name="notice_direct_room_update_by_you">اینجا را ارتقا دادید.</string>
|
||||
<string name="notice_direct_room_update">%s اینجا را ارتقا داد.</string>
|
||||
<string name="notice_made_future_direct_room_visibility_by_you">پیامهای آینده را برای %1$s نمایان کردید</string>
|
||||
<string name="notice_made_future_direct_room_visibility">%1$s پیامهای آینده را برای %2$s نمایان کرد</string>
|
||||
<string name="notice_direct_room_leave_by_you">اتاق را ترک کردید</string>
|
||||
<string name="notice_direct_room_leave">%1$s اتاق را ترک کرد</string>
|
||||
<string name="notice_direct_room_join_by_you">پیوستید</string>
|
||||
<string name="notice_direct_room_join">%1$s پیوست</string>
|
||||
<string name="notice_direct_room_created_by_you">گفتوگو را ایجاد کردید</string>
|
||||
<string name="notice_direct_room_created">%1$s گفتوگو را ایجاد کرد</string>
|
||||
</resources>
|
|
@ -1,9 +1,7 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="summary_message">%1$s : %2$s</string>
|
||||
<string name="summary_user_sent_image">%1$s a envoyé une image.</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee">invitation de %s</string>
|
||||
<string name="notice_room_invite">%1$s a invité %2$s</string>
|
||||
<string name="notice_room_invite_you">%1$s vous a invité</string>
|
||||
|
@ -11,13 +9,13 @@
|
|||
<string name="notice_room_leave">%1$s est parti du salon</string>
|
||||
<string name="notice_room_reject">%1$s a rejeté l’invitation</string>
|
||||
<string name="notice_room_kick">%1$s a expulsé %2$s</string>
|
||||
<string name="notice_room_unban">%1$s a révoqué le bannissement de %2$s</string>
|
||||
<string name="notice_room_ban">%1$s a banni %2$s</string>
|
||||
<string name="notice_room_unban">%1$s a révoqué l\'exclusion de %2$s</string>
|
||||
<string name="notice_room_ban">%1$s a exclus %2$s</string>
|
||||
<string name="notice_room_withdraw">%1$s a annulé l’invitation de %2$s</string>
|
||||
<string name="notice_avatar_url_changed">%1$s a changé d’avatar</string>
|
||||
<string name="notice_display_name_set">%1$s a modifié son nom affiché en %2$s</string>
|
||||
<string name="notice_display_name_changed_from">%1$s a modifié son nom affiché %2$s en %3$s</string>
|
||||
<string name="notice_display_name_removed">%1$s a supprimé son nom affiché (%2$s)</string>
|
||||
<string name="notice_display_name_changed_from">%1$s a modifié son nom affiché de %2$s en %3$s</string>
|
||||
<string name="notice_display_name_removed">%1$s a supprimé son nom affiché (précédemment %2$s)</string>
|
||||
<string name="notice_room_topic_changed">%1$s a changé le sujet en : %2$s</string>
|
||||
<string name="notice_room_name_changed">%1$s a changé le nom du salon en : %2$s</string>
|
||||
<string name="notice_placed_video_call">%s a passé un appel vidéo.</string>
|
||||
|
@ -31,54 +29,39 @@
|
|||
<string name="notice_room_visibility_world_readable">n’importe qui.</string>
|
||||
<string name="notice_room_visibility_unknown">inconnu (%s).</string>
|
||||
<string name="notice_end_to_end">%1$s a activé le chiffrement de bout en bout (%2$s)</string>
|
||||
|
||||
<string name="notice_requested_voip_conference">%1$s a demandé une téléconférence VoIP</string>
|
||||
<string name="notice_voip_started">Téléconférence VoIP démarrée</string>
|
||||
<string name="notice_voip_finished">Téléconférence VoIP terminée</string>
|
||||
|
||||
<string name="notice_avatar_changed_too">(l’avatar a aussi changé)</string>
|
||||
<string name="notice_room_name_removed">%1$s a supprimé le nom du salon</string>
|
||||
<string name="notice_room_topic_removed">%1$s a supprimé le sujet du salon</string>
|
||||
<string name="notice_profile_change_redacted">%1$s a mis à jour son profil %2$s</string>
|
||||
<string name="notice_room_third_party_invite">%1$s a envoyé une invitation à %2$s pour rejoindre le salon</string>
|
||||
<string name="notice_room_third_party_registered_invite">%1$s a accepté l’invitation pour %2$s</string>
|
||||
|
||||
<string name="notice_crypto_unable_to_decrypt">** Déchiffrement impossible : %s **</string>
|
||||
<string name="notice_crypto_error_unkwown_inbound_session_id">L’appareil de l’expéditeur ne nous a pas envoyé les clés pour ce message.</string>
|
||||
|
||||
<string name="could_not_redact">Effacement impossible</string>
|
||||
<string name="unable_to_send_message">Envoi du message impossible</string>
|
||||
|
||||
<string name="message_failed_to_upload">L’envoi de l’image a échoué</string>
|
||||
|
||||
<string name="network_error">Erreur de réseau</string>
|
||||
<string name="matrix_error">Erreur de Matrix</string>
|
||||
|
||||
<string name="room_error_join_failed_empty_room">Il est impossible pour le moment de revenir dans un salon vide.</string>
|
||||
|
||||
<string name="encrypted_message">Message chiffré</string>
|
||||
|
||||
<string name="medium_email">Adresse e-mail</string>
|
||||
<string name="medium_phone_number">Numéro de téléphone</string>
|
||||
|
||||
<string name="summary_user_sent_sticker">%1$s a envoyé un sticker.</string>
|
||||
|
||||
<string name="room_displayname_invite_from">Invitation de %s</string>
|
||||
<string name="room_displayname_room_invite">Invitation au salon</string>
|
||||
<string name="room_displayname_empty_room">Salon vide</string>
|
||||
<string name="room_displayname_two_members">%1$s et %2$s</string>
|
||||
|
||||
<plurals name="room_displayname_three_and_more_members">
|
||||
<item quantity="one">%1$s et 1 autre</item>
|
||||
<item quantity="other">%1$s et %2$d autres</item>
|
||||
</plurals>
|
||||
|
||||
|
||||
<string name="notice_event_redacted">Message supprimé</string>
|
||||
<string name="notice_event_redacted_by">Message supprimé par %1$s</string>
|
||||
<string name="notice_event_redacted_with_reason">Message supprimé [motif : %1$s]</string>
|
||||
<string name="notice_event_redacted_by_with_reason">Message supprimé par %1$s [motif : %2$s]</string>
|
||||
|
||||
<string name="initial_sync_start_importing_account">Synchronisation initiale :
|
||||
\nImportation du compte…</string>
|
||||
<string name="initial_sync_start_importing_account_crypto">Synchronisation initiale :
|
||||
|
@ -95,12 +78,9 @@
|
|||
\nImportation des communautés</string>
|
||||
<string name="initial_sync_start_importing_account_data">Synchronisation initiale :
|
||||
\nImportation des données du compte</string>
|
||||
|
||||
<string name="notice_room_update">%s a mis à niveau ce salon.</string>
|
||||
|
||||
<string name="event_status_sending_message">Envoi du message…</string>
|
||||
<string name="clear_timeline_send_queue">Vider la file d’envoi</string>
|
||||
|
||||
<string name="notice_room_third_party_revoked_invite">%1$s a révoqué l’invitation pour %2$s à rejoindre le salon</string>
|
||||
<string name="notice_room_invite_no_invitee_with_reason">Invitation de %1$s. Raison : %2$s</string>
|
||||
<string name="notice_room_invite_with_reason">%1$s a invité %2$s. Raison : %3$s</string>
|
||||
|
@ -109,35 +89,128 @@
|
|||
<string name="notice_room_leave_with_reason">%1$s est parti du salon. Raison : %2$s</string>
|
||||
<string name="notice_room_reject_with_reason">%1$s a refusé l’invitation. Raison : %2$s</string>
|
||||
<string name="notice_room_kick_with_reason">%1$s a expulsé %2$s. Raison : %3$s</string>
|
||||
<string name="notice_room_unban_with_reason">%1$s a révoqué le bannissement de %2$s. Raison : %3$s</string>
|
||||
<string name="notice_room_ban_with_reason">%1$s a banni %2$s. Raison : %3$s</string>
|
||||
<string name="notice_room_unban_with_reason">%1$s a révoqué l\'exclusion de %2$s. Raison : %3$s</string>
|
||||
<string name="notice_room_ban_with_reason">%1$s a exclus %2$s. Raison : %3$s</string>
|
||||
<string name="notice_room_third_party_invite_with_reason">%1$s a envoyé une invitation à %2$s pour rejoindre le salon. Raison : %3$s</string>
|
||||
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s a révoqué l’invitation de %2$s à rejoindre le salon. Raison : %3$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_with_reason">%1$s a accepté l’invitation pour %2$s. Raison : %3$s</string>
|
||||
<string name="notice_room_withdraw_with_reason">%1$s a annulé l’invitation de %2$s. Raison : %3$s</string>
|
||||
|
||||
<plurals name="notice_room_aliases_added">
|
||||
<item quantity="one">%1$s a ajouté %2$s comme adresse pour ce salon.</item>
|
||||
<item quantity="other">%1$s a ajouté %2$s comme adresses pour ce salon.</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="notice_room_aliases_removed">
|
||||
<item quantity="one">%1$s a supprimé %2$s comme adresse pour ce salon.</item>
|
||||
<item quantity="other">%1$s a supprimé %3$s comme adresses pour ce salon.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="notice_room_aliases_added_and_removed">%1$s a ajouté %2$s et supprimé %3$s comme adresses pour ce salon.</string>
|
||||
|
||||
<string name="notice_room_canonical_alias_set">%1$s a défini %2$s comme adresse principale pour ce salon.</string>
|
||||
<string name="notice_room_canonical_alias_unset">%1$s a supprimé l’adresse principale de ce salon.</string>
|
||||
|
||||
<string name="notice_room_guest_access_can_join">%1$s a autorisé les visiteurs à rejoindre le salon.</string>
|
||||
<string name="notice_room_guest_access_forbidden">%1$s a empêché les visiteurs de rejoindre le salon.</string>
|
||||
|
||||
<string name="notice_end_to_end_ok">%1$s a activé le chiffrement de bout en bout.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm">%1$s a activé le chiffrement de bout en bout (algorithme %2$s inconnu).</string>
|
||||
|
||||
<string name="key_verification_request_fallback_message">%s demande à vérifier votre clé, mais votre client ne supporte pas la vérification de clés dans les discussions. Vous devrez utiliser l’ancienne vérification de clés pour vérifier les clés.</string>
|
||||
|
||||
<string name="notice_room_created">%1$s a créé le salon</string>
|
||||
</resources>
|
||||
<string name="notice_direct_room_update_by_you">Vous avez mis cet endroit à niveau.</string>
|
||||
<string name="notice_direct_room_update">%s a mis cet endroit à niveau.</string>
|
||||
<string name="notice_room_update_by_you">Vous avez mis à niveau ce salon.</string>
|
||||
<string name="notice_room_kick_by_you">Vous avez expulsé %1$s</string>
|
||||
<string name="notice_room_reject_by_you">Vous avez rejeté l\'invitation</string>
|
||||
<string name="notice_direct_room_leave_by_you">Vous avez quitté le salon</string>
|
||||
<string name="notice_direct_room_leave">%1$s a quitté le salon</string>
|
||||
<string name="notice_room_leave_by_you">Vous avez quitté le salon</string>
|
||||
<string name="notice_direct_room_join_by_you">Vous avez rejoint le salon</string>
|
||||
<string name="notice_direct_room_join">%1$s a rejoint le salon</string>
|
||||
<string name="notice_room_join_by_you">Vous avez rejoint le salon</string>
|
||||
<string name="notice_room_invite_by_you">Vous avez invité %1$s</string>
|
||||
<string name="notice_direct_room_created_by_you">Vous avez créé la discussion</string>
|
||||
<string name="notice_direct_room_created">%1$s a créé la discussion</string>
|
||||
<string name="notice_room_created_by_you">Vous avez créé le salon</string>
|
||||
<string name="notice_room_invite_no_invitee_by_you">Votre invitation</string>
|
||||
<string name="summary_you_sent_sticker">Vous avez envoyé un autocollant.</string>
|
||||
<string name="summary_you_sent_image">Vous avez envoyé une image.</string>
|
||||
<string name="notice_end_to_end_unknown_algorithm_by_you">Vous avez activé le chiffrement de bout en bout (algorithme %1$s inconnu).</string>
|
||||
<string name="notice_end_to_end_ok_by_you">Vous avez activé le chiffrement de bout en bout.</string>
|
||||
<string name="notice_direct_room_guest_access_forbidden_by_you">Vous avez empêché les visiteurs de rejoindre le salon.</string>
|
||||
<string name="notice_direct_room_guest_access_forbidden">%1$s a empêché les visiteurs de rejoindre le salon.</string>
|
||||
<string name="notice_room_guest_access_forbidden_by_you">Vous avez empêché les visiteurs de rejoindre le salon.</string>
|
||||
<string name="notice_direct_room_guest_access_can_join_by_you">Vous avez autorisé les visiteurs à venir ici.</string>
|
||||
<string name="notice_direct_room_guest_access_can_join">%1$s a autorisé les visiteurs à venir ici.</string>
|
||||
<string name="notice_room_guest_access_can_join_by_you">Vous avez autorisé les visiteurs à rejoindre le salon.</string>
|
||||
<string name="notice_room_canonical_alias_unset_by_you">Vous avez supprimé l’adresse principale de ce salon.</string>
|
||||
<string name="notice_room_canonical_alias_set_by_you">Vous avez défini %1$s comme adresse principale pour ce salon.</string>
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">Vous avez ajouté %1$s et supprimé %2$s comme adresses pour ce salon.</string>
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">Vous avez supprimé %1$s comme adresse pour ce salon.</item>
|
||||
<item quantity="other">Vous avez supprimé %1$s comme adresses pour ce salon.</item>
|
||||
</plurals>
|
||||
<plurals name="notice_room_aliases_added_by_you">
|
||||
<item quantity="one">Vous avez ajouté %1$s comme adresse pour ce salon.</item>
|
||||
<item quantity="other">Vous avez ajouté %1$s comme adresses pour ce salon.</item>
|
||||
</plurals>
|
||||
<string name="notice_room_withdraw_with_reason_by_you">Vous avez annulé l’invitation de %1$s. Raison : %2$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_with_reason_by_you">Vous avez accepté l’invitation pour %1$s. Raison : %2$s</string>
|
||||
<string name="notice_room_third_party_revoked_invite_with_reason_by_you">Vous avez révoqué l’invitation de %1$s à rejoindre le salon. Raison : %2$s</string>
|
||||
<string name="notice_room_third_party_invite_with_reason_by_you">Vous avez envoyé une invitation à %1$s pour rejoindre le salon. Raison : %2$s</string>
|
||||
<string name="notice_room_reject_with_reason_by_you">Vous avez refusé l’invitation. Raison : %1$s</string>
|
||||
<string name="notice_direct_room_leave_with_reason_by_you">Vous êtes parti. Raison : %1$s</string>
|
||||
<string name="notice_direct_room_leave_with_reason">%1$s est parti. Raison : %2$s</string>
|
||||
<string name="notice_room_leave_with_reason_by_you">Vous êtes parti du salon. Raison : %1$s</string>
|
||||
<string name="notice_direct_room_join_with_reason">%1$s rejoint. Raison : %2$s</string>
|
||||
<string name="notice_direct_room_join_with_reason_by_you">Vous avez rejoint. Raison : %1$s</string>
|
||||
<string name="notice_room_join_with_reason_by_you">Vous avez rejoint le salon. Raison : %1$s</string>
|
||||
<string name="notice_room_invite_with_reason_by_you">Vous avez invité %1$s. Raison : %2$s</string>
|
||||
<string name="notice_room_invite_no_invitee_with_reason_by_you">Votre invitation. Raison %1$s</string>
|
||||
<string name="notice_power_level_diff">%1$s de %2$s à %3$s</string>
|
||||
<string name="notice_power_level_changed">%1$s a modifié le niveau de pouvoir de %2$s.</string>
|
||||
<string name="notice_power_level_changed_by_you">Vous avez modifié le niveau de pouvoir de %1$s.</string>
|
||||
<string name="power_level_custom_no_value">Personnalisé</string>
|
||||
<string name="power_level_custom">Personnalisé (%1$d)</string>
|
||||
<string name="power_level_default">Défaut</string>
|
||||
<string name="power_level_moderator">Modérateur</string>
|
||||
<string name="power_level_admin">Admin</string>
|
||||
<string name="notice_widget_modified_by_you">Vous avez modifié le widget %1$s</string>
|
||||
<string name="notice_widget_modified">%1$s a modifié le widget %2$s</string>
|
||||
<string name="notice_widget_removed_by_you">Vous avez supprimé le widget %1$s</string>
|
||||
<string name="notice_widget_removed">%1$s a supprimé le widget %2$s</string>
|
||||
<string name="notice_widget_added_by_you">Vous avez ajouté le widget %1$s</string>
|
||||
<string name="notice_widget_added">%1$s a ajouté le widget %2$s</string>
|
||||
<string name="notice_room_third_party_registered_invite_by_you">Vous avez accepté l’invitation pour %1$s</string>
|
||||
<string name="notice_direct_room_third_party_revoked_invite_by_you">Vous avez révoqué l\'invitation pour %1$s</string>
|
||||
<string name="notice_direct_room_third_party_revoked_invite">%1$s a révoqué l\'invitation pour %2$s</string>
|
||||
<string name="notice_made_future_direct_room_visibility_by_you">Vous avez rendu les futurs messages visible pour %1$s</string>
|
||||
<string name="notice_made_future_direct_room_visibility">%1$s a rendu les futurs messages visible pour %2$s</string>
|
||||
<string name="notice_room_avatar_removed_by_you">Vous avez supprimé l\'avatar du salon</string>
|
||||
<string name="notice_room_avatar_removed">%1$s a supprimé l\'avatar du salon</string>
|
||||
<string name="notice_room_name_removed_by_you">Vous avez supprimé le nom du salon</string>
|
||||
<string name="notice_requested_voip_conference_by_you">Vous avez demandé une téléconférence VoIP</string>
|
||||
<string name="notice_end_to_end_by_you">Vous avez activé le chiffrement de bout en bout (%1$s)</string>
|
||||
<string name="notice_made_future_room_visibility_by_you">Vous avez rendu l’historique futur du salon visible pour %1$s</string>
|
||||
<string name="notice_ended_call_by_you">Vous avez raccroché.</string>
|
||||
<string name="notice_answered_call_by_you">Vous avez répondu à l’appel.</string>
|
||||
<string name="notice_call_candidates_by_you">Vous avez envoyé les données pour configurer l\'appel.</string>
|
||||
<string name="notice_call_candidates">%s a envoyé les données pour configurer l\'appel.</string>
|
||||
<string name="notice_placed_voice_call_by_you">Vous avez passé un appel vocal.</string>
|
||||
<string name="notice_placed_video_call_by_you">Vous avez passé un appel vidéo.</string>
|
||||
<string name="notice_room_name_changed_by_you">Vous avez changé le nom du salon en : %1$s</string>
|
||||
<string name="notice_room_avatar_changed_by_you">Vous avez modifié l\'avatar du salon</string>
|
||||
<string name="notice_room_avatar_changed">%1$s a modifié l\'avatar du salon</string>
|
||||
<string name="notice_display_name_changed_from_by_you">Vous avez modifié votre nom affiché de %1$s en %2$s</string>
|
||||
<string name="notice_display_name_set_by_you">Vous avez modifié votre nom affiché en %1$s</string>
|
||||
<string name="notice_avatar_url_changed_by_you">Vous avez changé votre avatar</string>
|
||||
<string name="notice_room_withdraw_by_you">Vous avez annulé l’invitation de %1$s</string>
|
||||
<string name="notice_room_topic_changed_by_you">Vous avez changé le sujet en : %1$s</string>
|
||||
<string name="notice_display_name_removed_by_you">Vous avez supprimé votre nom affiché (précédemment %1$s)</string>
|
||||
<string name="notice_room_third_party_revoked_invite_by_you">Vous avez révoqué l’invitation pour %1$s à rejoindre le salon</string>
|
||||
<string name="notice_direct_room_third_party_invite_by_you">Vous avez invité %1$s</string>
|
||||
<string name="notice_direct_room_third_party_invite">%1$s a invité %2$s</string>
|
||||
<string name="notice_room_third_party_invite_by_you">Vous avez envoyé une invitation à %1$s pour rejoindre le salon</string>
|
||||
<string name="notice_profile_change_redacted_by_you">Vous avez mis à jour votre profile %1$s</string>
|
||||
<string name="notice_room_topic_removed_by_you">Vous avez supprimé le sujet du salon</string>
|
||||
<string name="notice_room_ban_with_reason_by_you">Vous avez exclus %1$s. Raison : %2$s</string>
|
||||
<string name="notice_room_unban_with_reason_by_you">Vous avez révoqué l\'exclusion de %1$s. Raison : %2$s</string>
|
||||
<string name="notice_room_ban_by_you">Vous avez exclus %1$s</string>
|
||||
<string name="notice_room_unban_by_you">Vous avez révoqué l\'exclusion de %1$s</string>
|
||||
<string name="notice_room_kick_with_reason_by_you">Vous avez expulsé %1$s. Raison : %2$s</string>
|
||||
</resources>
|
|
@ -183,7 +183,7 @@
|
|||
</plurals>
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">Hai rimosso %1$s come indirizzo per questa stanza.</item>
|
||||
<item quantity="other">Hai rimosso %2$s come indirizzi per questa stanza.</item>
|
||||
<item quantity="other">Hai rimosso %1$s come indirizzi per questa stanza.</item>
|
||||
</plurals>
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">Hai aggiunto %1$s e rimosso %2$s come indirizzi per questa stanza.</string>
|
||||
<string name="notice_room_canonical_alias_set_by_you">Hai impostato l\'indirizzo principale per questa stanza a %1$s.</string>
|
||||
|
|
|
@ -172,7 +172,7 @@
|
|||
</plurals>
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">Tekkseḍ %1$s am tansa i texxamt-a.</item>
|
||||
<item quantity="other">Tekkseḍ %2$s am tansiwin i texxamt-a.</item>
|
||||
<item quantity="other">Tekkseḍ %1$s am tansiwin i texxamt-a.</item>
|
||||
</plurals>
|
||||
<string name="notice_room_aliases_added_and_removed">%1$s yerna %2$s terniḍ tekkseḍ %3$s am tansiwin i texxamt-a.</string>
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">Terniḍ %1$s terniḍ tekkseḍ %2$s am tansiwin i texxamt-a.</string>
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="summary_message">%1$s: %2$s</string>
|
||||
<string name="summary_user_sent_image">%1$s išsiuntė atvaizdą.</string>
|
||||
<string name="summary_user_sent_image">%1$s išsiuntė vaizdą.</string>
|
||||
<string name="summary_user_sent_sticker">%1$s išsiuntė lipduką.</string>
|
||||
|
||||
<string name="notice_room_invite_no_invitee">%s pakvietimas</string>
|
||||
</resources>
|
||||
<string name="notice_room_join_by_you">Jūs prisijungėte prie kambario</string>
|
||||
<string name="notice_room_join">%1$s prisijungė prie kambario</string>
|
||||
<string name="notice_room_invite_you">%1$s pakvietė jus</string>
|
||||
<string name="notice_room_invite_by_you">Jūs pakvietėte %1$s</string>
|
||||
<string name="notice_room_invite">%1$s pakvietė %2$s</string>
|
||||
<string name="notice_direct_room_created_by_you">Jūs sukūrėte diskusiją</string>
|
||||
<string name="notice_direct_room_created">%1$s sukūrė diskusiją</string>
|
||||
<string name="notice_room_created_by_you">Jūs sukūrėte kambarį</string>
|
||||
<string name="notice_room_created">%1$s sukūrė kambarį</string>
|
||||
<string name="notice_room_invite_no_invitee_by_you">Jūsų pakvietimas</string>
|
||||
<string name="summary_you_sent_sticker">Jūs išsiuntėte lipduką.</string>
|
||||
<string name="summary_you_sent_image">Jūs išsiuntėte vaizdą.</string>
|
||||
</resources>
|
|
@ -182,7 +182,7 @@
|
|||
</plurals>
|
||||
<plurals name="notice_room_aliases_removed_by_you">
|
||||
<item quantity="one">Você removeu %1$s como um endereço desta sala.</item>
|
||||
<item quantity="other">Você removeu %2$s como endereços desta sala.</item>
|
||||
<item quantity="other">Você removeu %1$s como endereços desta sala.</item>
|
||||
</plurals>
|
||||
<string name="notice_room_aliases_added_and_removed">%1$s adicionou %2$s e removeu %3$s como endereços desta sala.</string>
|
||||
<string name="notice_room_aliases_added_and_removed_by_you">Você adicionou %1$s e removeu %2$s como endereços desta sala.</string>
|
||||
|
|
|
@ -225,4 +225,6 @@
|
|||
<string name="notice_direct_room_join">%1$s вошел(ла)</string>
|
||||
<string name="notice_direct_room_created_by_you">Вы создали обсуждение</string>
|
||||
<string name="notice_direct_room_created">%1$s создал(а) обсуждение</string>
|
||||
<string name="notice_direct_room_update_by_you">Вы обновили.</string>
|
||||
<string name="notice_direct_room_update">%s обновлена.</string>
|
||||
</resources>
|