From b84879d71789240474036d49bdaacd91317d694d Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Thu, 8 Dec 2022 09:43:15 +0100 Subject: [PATCH] Import v1.5.11 from Element Android --- dependencies.gradle | 6 +- .../src/androidTest/assets/session_42.realm | Bin 0 -> 270336 bytes .../android/sdk/common/CommonTestHelper.kt | 5 + .../RealmSessionStoreMigration43Test.kt | 130 ++++++ .../database/SessionSanityMigrationTest.kt | 64 +++ .../database/TestRealmConfigurationFactory.kt | 196 +++++++++ .../room/timeline/PollAggregationTest.kt | 2 +- .../events/model/AggregatedAnnotation.kt | 1 - .../events/model/AggregatedRelations.kt | 6 +- .../session/events/model/AggregatedReplace.kt | 33 ++ .../sdk/api/session/events/model/Event.kt | 30 +- .../sdk/api/session/events/model/EventExt.kt | 46 +++ .../sdk/api/session/events/model/EventType.kt | 19 +- .../events/model/ValidDecryptedEvent.kt | 37 ++ .../room/location/LocationSharingService.kt | 3 +- .../room/model/EditAggregatedSummary.kt | 4 +- .../sdk/api/session/room/model/ReadReceipt.kt | 3 +- .../model/message/MessageStickerContent.kt | 2 +- .../model/relation/RelationDefaultContent.kt | 2 + .../sdk/api/session/room/read/ReadService.kt | 14 +- .../room/summary/RoomSummaryConstants.kt | 4 +- .../session/room/timeline/TimelineEvent.kt | 17 +- .../sdk/api/session/sync/FilterService.kt | 13 +- .../session/sync/filter/SyncFilterBuilder.kt | 129 ++++++ .../crypto/crosssigning/CrossSigningOlm.kt | 4 +- .../internal/crypto/tasks/EncryptEventTask.kt | 5 +- .../DefaultVerificationService.kt | 78 ++-- .../sdk/internal/database/AsyncTransaction.kt | 6 +- .../database/RealmSessionStoreMigration.kt | 8 +- .../database/helper/ChunkEntityHelper.kt | 3 +- .../database/helper/ThreadEventsHelper.kt | 60 ++- .../database/helper/ThreadSummaryHelper.kt | 27 -- .../EditAggregatedSummaryEntityMapper.kt | 45 +++ .../mapper/EventAnnotationsSummaryMapper.kt | 14 +- .../database/mapper/FilterParamsMapper.kt | 61 +++ .../mapper/ReadReceiptsSummaryMapper.kt | 2 +- .../database/migration/MigrateSessionTo008.kt | 6 +- .../database/migration/MigrateSessionTo043.kt | 42 ++ .../database/migration/MigrateSessionTo044.kt | 29 ++ .../database/migration/MigrateSessionTo045.kt | 38 ++ .../model/EditAggregatedSummaryEntity.kt | 5 +- .../model/EventAnnotationsSummaryEntity.kt | 16 - .../database/model/ReadReceiptEntity.kt | 1 + .../database/model/SessionRealmModule.kt | 3 +- .../database/model/SyncFilterParamsEntity.kt | 36 ++ .../database/model/TimelineEventEntity.kt | 5 + .../internal/database/query/ReadQueries.kt | 26 +- .../query/ReadReceiptEntityQueries.kt | 38 +- .../session/EventInsertLiveProcessor.kt | 2 +- .../session/call/CallEventProcessor.kt | 7 +- .../session/call/CallSignalingHandler.kt | 3 +- .../session/filter/DefaultFilterRepository.kt | 83 ++-- .../session/filter/DefaultFilterService.kt | 22 +- .../internal/session/filter/FilterFactory.kt | 39 -- .../internal/session/filter/FilterModule.kt | 3 + .../session/filter/FilterRepository.kt | 31 +- .../session/filter/GetCurrentFilterTask.kt | 55 +++ .../internal/session/filter/SaveFilterTask.kt | 45 +-- .../pushrules/ProcessEventForPushTask.kt | 4 +- .../session/room/EventEditValidator.kt | 126 ++++++ .../EventRelationsAggregationProcessor.kt | 206 +++++----- .../sdk/internal/session/room/RoomAPI.kt | 3 +- .../room/create/CreateLocalRoomTask.kt | 10 +- .../room/create/RoomCreateEventProcessor.kt | 2 +- .../location/DefaultLocationSharingService.kt | 3 +- .../GetActiveBeaconInfoForUserTask.kt | 2 +- ...iveLocationShareRedactionEventProcessor.kt | 4 +- .../location/StartLiveLocationShareTask.kt | 5 +- .../location/StopLiveLocationShareTask.kt | 2 +- .../room/prune/RedactionEventProcessor.kt | 77 ++-- .../session/room/read/DefaultReadService.kt | 34 +- .../internal/session/room/read/ReadBody.kt | 25 ++ .../session/room/read/SetReadMarkersTask.kt | 22 +- .../room/send/LocalEchoEventFactory.kt | 10 +- .../room/summary/RoomSummaryUpdater.kt | 8 +- .../session/room/timeline/DefaultTimeline.kt | 2 +- .../timeline/FetchTokenAndPaginateTask.kt | 2 +- .../room/timeline/GetContextOfEventTask.kt | 2 +- .../session/room/timeline/PaginationTask.kt | 2 +- .../tombstone/RoomTombstoneEventProcessor.kt | 2 +- .../sdk/internal/session/sync/SyncTask.kt | 15 +- .../sync/handler/room/ReadReceiptHandler.kt | 26 +- .../sync/handler/room/RoomSyncHandler.kt | 31 +- .../internal/sync/filter/SyncFilterParams.kt | 25 ++ .../android/sdk/internal/util/Monarchy.kt | 2 +- .../EditAggregatedSummaryEntityMapperTest.kt | 114 ++++++ .../session/event/ValidDecryptedEventTest.kt | 134 +++++++ .../session/room/EventEditValidatorTest.kt | 372 ++++++++++++++++++ .../aggregation/poll/PollEventsTestData.kt | 6 +- ...faultGetActiveBeaconInfoForUserTaskTest.kt | 2 +- .../DefaultLocationSharingServiceTest.kt | 9 +- .../DefaultStartLiveLocationShareTaskTest.kt | 8 +- .../DefaultStopLiveLocationShareTaskTest.kt | 2 +- ...ocationShareRedactionEventProcessorTest.kt | 2 +- .../room/send/LocalEchoEventFactoryTests.kt | 12 +- .../sync/DefaultGetCurrentFilterTaskTest.kt | 100 +++++ .../sdk/test/fakes/FakeFilterRepository.kt | 34 ++ .../FakeHomeServerCapabilitiesDataSource.kt | 30 ++ .../android/sdk/test/fakes/FakeMonarchy.kt | 6 +- .../sdk/test/fakes/FakeRealmConfiguration.kt | 6 +- .../sdk/test/fakes/FakeSaveFilterTask.kt | 40 ++ 101 files changed, 2545 insertions(+), 598 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/assets/session_42.realm create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt diff --git a/dependencies.gradle b/dependencies.gradle index dc66de43..31c32bb2 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -10,7 +10,7 @@ def gradle = "7.3.1" // Ref: https://kotlinlang.org/releases.html def kotlin = "1.7.21" def kotlinCoroutines = "1.6.4" -def dagger = "2.44" +def dagger = "2.44.2" def appDistribution = "16.0.0-beta05" def retrofit = "2.9.0" def markwon = "4.6.2" @@ -83,7 +83,7 @@ ext.libs = [ 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.0" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.1" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -98,7 +98,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.4.0" + 'wysiwyg' : "io.element.android:wysiwyg:0.7.0.1" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/matrix-sdk-android/src/androidTest/assets/session_42.realm b/matrix-sdk-android/src/androidTest/assets/session_42.realm new file mode 100644 index 0000000000000000000000000000000000000000..b92d13dab2fa6d44a38ee3443ae8cad85340db23 GIT binary patch literal 270336 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%0DTX_=9PRTAUr6@kN4E#1dFf1 zX^ftAD^Xy2us5=9)IBwU+bj$jj;dH`^p6Y&#QcU@JwP}N<VC!c<M(KhqX^kfkjpP6 zdkhe?<`N+!5UBjJOmmU=aFEVboQ+0evFLsW-QNkac0#Vl1P5}^cwX~%6%qb3Tdn9u zEJ&X%Gj9mS2t!-qYok_Iaqp%#k+?q`9upg}I||J{Jwy@!mnEr6<RD0z&3B{1IKSoK zhGC)T(w;gVzwNl4M0qwJjaloin!+r%qqdE(IfNlVJcO5o4D3?XZw*e>@{?SPT?f3E zVc{H{%X>sH>%^L@d#1ix1Z_r}%43@f%BHb3aTV142Q{0ZF#_M=uyA0l{!E{w(6k_Q zN9Le*1G~Sw%g)UTrgA(`kuAat)HQ=d!>TZ6e|ALg(l9j*<>}>@PJU%0n1uiHuM;8h zu1Aac9cPgzV(3rrpj*-{ca8l+%|x?OSSTU%uB>lWuPA}SLNtbshZ>VCsBdVA5Yej} zO~{Ea%w7()Ck)eHzdlE|sf9zI@?<<+K9}}zZXwTHJfw;uw!rI@vmMo8+e%nOQ9h}y zTcTCqN*>h&>cY&_?Ix~wGiv0SkY!-q(oTa?4bBqep?l!0(9*NG$Qu1u&W@Gj4&rPP zFz50Z43A}*$^)=OWX}B}cwr=uqldl1tl8)epI$*R^C9*qOXGzF-2C{4RfXw)dT5d< z4P{BZ-j7eL^<B{hKIcK8ix(Kd48y%H0G{l|izAA!(%D8~J8p=@(n5@f3?kpZ%JmGz zbhecZ#GIQ0p+L{JrWo>+@5UUUpp-&Gf<Xwynx~jBkv4J4U2_xEyh?31h}5>|?P2@> zfmgzhN)@$$j89i;E4eZyEYLB2FhqU~%*x#Gaal&ID|s$;efK;Bn%|5J2&^;Wtp5T~ zGoC$I9Gug+%*6`@M`0?6Qv|04A8}|~v|_4nK5cCm!lV;2)41M@`0<9k!HK=B0(bxH z_Y68rmG6n^o83Km5nDIp8sqvpUFMo%>ET_5yDtMfh5Ir}>3;Li*(F`P@hFQ}!@@?D z4uUwG{WA`lSt1_-dMKG5(~jxH_+1A-xJHOO*lZsjV-dK0I>iaxXcAWO%}=`BzW?K& zWC<JQl@6>cm5`k+sVGN>au#dsx9x!^7s`-wXhN6m&HpJIUi)*BFcB0fs);9M@4d0( zY#eBj`cC~3IuizV?9s*Hwm~QG#uEKE&D}ws^C|6Ysl-nZAhyd<_YM8cox}&kBvTh- zxJJVJzYU7J?|y3M?sgQP=pOwj?**fM-uloAm3`f1QT>GX>aqxu7AvOPqhsximQ;bf z$jJ=-^eWH@+C@MS%QiW*Wf)#og^<yj+t>$Urhiw=E52c~u35tZgxlG8HnZr94yti~ zDws`&3CC5_ywm{GFO4GFDm?F%RLsC8>UBs<KHZ5JT>d?GFTnpdk%3C}>6*n)uR%6A zxY9F{EyBM|@hsa!t(l1xQLlEWvR~T%>{1#=4g`FXbJ?$I`HssmUEsuQ&LlDsd0ZT$ zIe24B>IkvKBSF+3g>fFAMCFE;>&Ek}g%~aiSO(^b6H_ioyKt)Evr1oZG-2kB5`VwN zdIV1E-Q4+($M5}yvmmPT6s<w!xp#dD@5^VHM6=5RFxOqJ?e&Csbc}4guk&Jn&biGu zsqkSB{ViCkKJ#<Rrg?aqk)h0}k~T=sGGU4fU@0uYe}HWm#dG1t^?=c8W+zj(R<R`n zw6vwQcx=?&K=89p7-PkSV)Q1mjx$OWUfFEw;RxM?Dm-OWz69c+FpEB4f7NZ&<Wzvh zL=tKahPSZa`WileGdAVMUe;+nHZ>7`IR_Z>zBq1Y2|s$;2Vy6POx;zWLk2;u|6(7b zv&L~;{37B91+WD?>62{bB?70vj>D7wCrV5bleK}7c`HSoTd&&pRR9?O;mE1^CV(2^ zCdiO3WhBQIcobUddyUbXLfPMlR(j*{2{92&h5hW26QXV7zdngJPEi0NMwPP7@s_#F zJ>BxIY8=5IF-)q0llCNvO+#<jD;2e6V%_pTHi$v3aD4@G!qd`pY1Fg!=gky|ja;1O z{GJX<j)W``O|ljAgwDdz{^MLdfP+(<Lh(n4Nwk+eyZUItdLNjunnB80dEfwR`AstI z`48USf9rF26r>>Em!!#g3EC9g$=7kn)#pc18A)r=e)K)}mC#tGYF<$e7|XBg*R_{x z_Qwff9Wz=vsD>I}k(eW!77&;6Y8Tl(iWhcLg9{qz;V`u*Sgg9$nuTsV=NkY3=sW`T zK)j>Oq#<C~q}|k<78PlKKBb><V;iA9cG-wKlz=mdmW(Q&W*BmLh7ieb7hY`K1M12V z2&(`UWa`Equ$Y4bPi**UaszpanX7~yL$`7waZ3}vpZ%P{m;)r_=6y=m?a3;-oWx06 zL9S=D67q8S+H{gJWT?F^iwnd89BI<vq&bL(o~k7np;1C0)tCiGLoLrJCM+OG*2nrN z3|#V^Xx>ItKw!GrfLRXkyG@kyKtoqYJ|UKqwkbiAhF6_V>Uorx()b#)5&%0u#J?yw zN=|(+Ig)5t`=dsM?F%<F5Ksql2O_34=2gVi_(gL*$22grXL9xVsy>l^vxWD9J3!g{ z_?fwrrWOh$t{HM0`qBQ>E+M@Z+B%13X^XZMFfGkt8V$z@f?0=U>sEh63^|t9qlP*$ z4^HLx;8v<<&<3b_5ma%Zw~4*fe2VMQGqA(+YHI>8Ru|gtkXZ+95G{?_aJ=Dn(Af!C z60_S`)09XsX?jcXgzL2dgBY8q&IKDaHJ!h2BCwB=Ur;2|P3ZPs%aupgwhfYUZzq6a zI=eEry>@8S)<<0~1Kb!~%0zkjJHNp}WGy`03Vk~$npLqe$}XLP#{uktc{sjf=pn3= zekb*pa0&I}uoJ&FSp_q(HAH_oQ)dWD?#7YSrPcxDTL|O1e0}P<NvB%g5{g3f^rU!7 zKij|w2HWO=G`J1>Lmz;FDlnZ)NJ<sGJb$s1SVnJhF*9&-T8)F^_yrYd{kF6dCIaQ$ zvka;!1G}7z>O<h~UxdJ%)P3cZCRN;^*Ntgt59CHu%-(niB+*cZr|G)fwI?0HS=Xz! zuy(2Ol00<Q*NP>gSN^|NS2F>5xrrKH7hAcfp;&w_f2O)Mk5tNRKvoUx3p?CC6dP3G zqAiwS5~jVb?MFQe<}Ds~xBA5QBUPg+JY1@v#!ygb-jlh@j9wlJh4T)*K9BLg5YOKG zScXdz<-Oa|-h#{Rm1<XO0jCymw-*$kRZc;mx`z&eVQx3Cr&=7y&|sZ5zWUhxC~wUn z@VH=fV2|pI9|M><Z-;ldN;7+5j&!b33d9h9cQ6AOdU~^r*v5aoGLZ<ViX32>5f{3m zI!=8r!)FVpgSG)l^O#fqqQHe*Uz&hV95?d*8l2haDA_VVF2by@+aNF=(i50v$8b_& z-Bu3hk}DiQ!SqMCYTh|r<+$f)K$@@(VAfEOTH3zqS=xrAt!S{U;OHSQoqv)Mdn3)c z2eZ+m?+(MYTa(Lm%9hr6vG0sir*RUnO>2+r*FZvo*nCEVp26rkWBsi+KE0p500f=! zmXb2SC%AEbn`g8AOk5GO>gC>NN;KqgjFm^guvHS+B4}rBD@Jd9)G#ldjN=7R>54@i zG*YjN@v7Er&bpOu%lhRVUIH{zoWXKd?u%`VkV$aG=Hsl*I(v~cRw#y#YRP)vM8)Bi z1DOFuB*a%A6xRW<z~38_sIlP4br!xDY{;pR9jAbScx;SP)l-e9iXrDG*;@esKh=%w zQ@ps~eM|c+$2jRKrRUi?#|Ew{6fGtwjv2UI>_8$~L=WN_#xd*(H#(iHwp-pgnls|L z#h+=#ptS^Y=yow8OD0LWaHs?J#&r92m<{&-h|ecKs1!<1!Zp2{yGsj-Z!@FGAvKg* zw~UKgX2o1#s@+fXVyq<FGsT4g#Cu(WxdGNb>#)JX!e4AZ0>*RPwN2rfY3g7?;4hFc zXCaoTzz*KbVy%e0o%~(Y9Q>eE1U2qI1Nl3&-e$!6JkYsp4_HUn>a3DIYvLFRu;{{K zHYMDHyq5LvP%R@tZL2Px(Dp}QSp?<?Si0Tuz0AlFcQd!Ut!{g;vqt(=w6V>T{86pI z`X>-CEtpy;o44uT5t9W7i~yzmLUoKdKTis`>>&L0(gy^$_i5v{R<K0qNdm888%wZR z1s<+S)<TP*H$;TDt!s6B>Lzx9-e0DgIWJhsB|j{%kpI%JR19j@15@gaIz&>%$ovd6 z{0i!fDITVDfZYncWoQqiQ*#G=!>R~RtQ^2aulXT$S+v^s0`CKeGjK>K?jM`|j*22l ze{<<I+uD*RJ%tP^sM_HUSa&Fh7#I*OAkFY;Rf@ZMTeT0AZuw*B0^p=-=N0yDmzyC0 zGp>F+Y!JR`LV8l?;)`s0o)-@t&5H39{lF5~SQhu0Ltnn`o3<au<=4Gj%;71_I?%;4 z;Rw~>0crh#I3+GHO8147FSiVe+{f0!OoyV(MeOtwNF!|@^{bCj|5bz2+1$}?HH%%j zA$^8^H<r+@@RGY|yfLRiaR7Yt_o8!{W9Km}a6Rugh0ps+_F&DJDSa^wjBH1K%~md` z@V=t(v&9tqCg5>;AOtfgt>l@YKu|!dne?oc{iWZNkYonFKgy&ww{UPSxbIwjI9qG+ zWW4=QHbdv2t|m=X`%x+%de40H9(qE}uaOY{{yaE$x-A=KDa4_Z!pTY^6-DA^qev7# z=`Fx_&WNF}sK4XuPEJSn5Ab>PPWnWDPj^)LLOl1x#xg1(CTBtTupwy-2-BpykP!6p zA$<Ea8qko3uF_x1<|UV?Q$_uAOhsjcXLO=C5n9`mqg!Bdm3et?WJy<?#;%f|Y+fD) z0V1$$M=w<r<g|{#ac#BCPPH@m&=wAWZKyIbSz*eSCf`$Oj}&fu8#LQboQwtTHQm3+ znG7`Az-hV|W-ei;f<t>k8yBy7f<4#wjW?rm9es*k*=ambv3QY3^c#QP%ho@wgwbkc zkha#YJ9E7LYX6pY=OJk0?&MJ{{OVBO5pN0l5cyM~r*-0(O#N<+j6;v#TNk1KK;K^) z!&v79UtjQyir-Vln853(u<E}v@1_#nD53mE4pi=ySt!#hpap-!A%n1wg<AgMFcU|~ z8@M?E#mW=$Pc9%~lwh`6-6LQMi28mt^Igkhx~!^cr0~`Nv`q|!k$c4X!WSKoQ7A6+ zr}%)XD_SI3X9;+IOrD>Upezt}FYv8eiP5UOz#kJ%o!i{2?YnVvFaFdqh808Q#s}f2 zar_RCory1lN01to#l2Biabpo1{2i?N;1eN^t?{$WSr%pb@FyAJ&jWR2c#KNb|3yN_ z4H<jxQ9qt>+mGqz=Z|GpYBgY76QCem&<6}VynfGw8qY4}R5;lEH3kGPxk-*1|0hxy z*bB_qY6|m6?}GxhA~`a!wn`Tf@=A9Eb2WAPxMrxGSbjA}{9iY9ax&758l~(&;oGQf zV7JqvQg2L9p-8QoG#fg>+N-(LmPvAhH}JC`bYh#K)lC1{T5n^!L<ryRGH)Ci1-9D4 zhqxPBG%{t2xTO-bl;`EXVLC@FPH6UN*Ah}Te&^d3V{Wf9&_E0>7FGFJ+tTyy>~vGW zm?o_FkXgO2Yx8?aeyVg8q9tBNhn2J?$Q4kcyW~+q=K+qOkw%BaWY0WTq@N($XVBU$ z;y1`@k8rO}O#_z{8`SqRBqpx09x{EsB^$dpjk$}B${@m%`M02SFx>4KMq=aL@MIV! zsru_b4{FQxVcIdovQL4|F)wKwMjn5tGO1K(SH``*sf*vCqc)_YLn(HDU8xbUN|Xgw zt7_EaWZcJ3Dsj0Vh;`|?xD@N&HKNHldQ9is8{<C+a0j$qweQU%S@N2aZ*?}7UPZdY z9Lj6zmBt!|{Ss~4R2PJG#hsdY@QRrs%)%N^XVSlRj9}_`sHX1OJKPRFLcR~L6=+ZF zjd9Tf#VLzyA?PC$DRT+tW}+fs3xin*n43?IXBO+OHvDA9v88pA9MEXzzDnw32(FMR zq7CmL{en&0poDNml-XI6eSf>kVsW|IzVHc}J5Uo(!kyl4_F48uS}O#9w*>R(267BN zUhRJa3enBM2a$5DKvi{+)Hz33CAQv^br2|)Ouo^qthT;|Lq(?~!c$#Q!@GZE4K{GI zG64$(-@_d{Hjm%;Sa|>v)nNOhXKho-`n>MuRFP>Chk~m&ZMdQ)<!1_vh5Fq(u5y4b z`89J;U&kDRxbjId_5<Qh)~4p1%}1dvFDTL`dN#&{cAGRnoT>R3P8}eDKhy-7^)5wn zs`zAxE_VLDZ)$`Air{w+zQndJd!z^eiGQsZ`7pGJk+?c80#q>ycKJ|PCj_6dx;Etp zS@%<iS)HuSZfGLquMX<pC9^+eZd&tfW;r$XFr>(@c8p^AW&ouk42+%H-<wOWDeMVC z@Ks!kxq)=#bjCUi^q{JScRdslL8QqG21a<olCFqp?!?Jdt69Y+k+kqRPO^?wNn%Ir z-Zqb{8vi<69jDvTY<PsE6~hVSpDoScFn>LPzCl%}`zl$4HgR@L>g$Y|I02#eCgqM~ z_ddok`EC?P@izav92MUALLw!Twi}M7R?(E=ZxS5w&stD&BKeWr1-|~NV&ndq!<;)b z1FfoOO*>M9goVO)gp&m!ueenQn2szIcdJF1j$D%Zh7L2Hvr7yA<<lH^XVk)#!#;-n zR_@kPVO#{)H)K<Ja-ru?Q&XKg5&8!Nar9XEAbQ$!2QFUU+4^4!kI8UFCpjaD7g|1a zhfYibY=_6&0(G{MFXE^tIxx=}Sn+5#mK-X{A%gcb%WA40?Nn4vz;TEBg<^CK%>_tW zEHXyO(_IlFw#_a$E2$HU){3DVZ182s@+&eGBtWU><o<9>Yjbv{uSWz=)tIVvpzEet zEjBe@=N^ebu3hO{0haP@M3Mq@cKfmjNE+CHm&LJ{%5Ilo;}?!EzWKkgpQ(XlQ8<dq zCv#_(0<5T-tM9x<J~b91lU>4(8a+AYzDaG?>odLw0$pnLNlPp%c?3Q(`Z$oL<hm@J zHR?d4Z@d!Yfj^iqL1Ui9$+Tv`-FRf~XCEcRED!XOl37VUBIde&=}O#kGn@9JIa(o_ z`uX28=&t!gr_D~(p#`CaB3Egt3Z2M^M$KZ)5~Q~%y*;RWBn7t*lJWJ8cP#gE97}vQ zW5*I3+!UtV$ibk6pF(vB_0Sn&eX59OYL;46a;TM~hfsM3;prN_F|t)l#mcRfYDWuW zIp!=>pM<Zx2cNV5*|m>u%S)#Po1iJSE3ZzZP2qr^h@Glz=t~A&pBPF*pL$SRzu23= z-%ux)P?&EFuVZh|6g`g9Z4!)hVClG)gzo_k@*(k}Gz<rzA&ZZ~YHqOK><OW_+eA`v zPjxOT3CcnhNm`iBnBk|@KT2(~gdZ{vdcnv?j$ihB)W6(eU9|*mr9RnJp2%k4J!y4# z*laywyK%!Ay6Nyx6{MMm;%E&}Fxa2M;(kfY#2_Z}X5)%9%phtE#AX#bE?bKA_wHC> zvD6jdwY3P0YNI&A<N^NwNpB%5|NSLjQjmr-(*#S`tck7V-8Kj4R-rFHcYmswcPJyY z9`0cRM_yz_s2+b_8#QK*`{GMBqWGI&9;|~qpsszAj^#JkkOVp6jO|`8<Vjk7CFh5a zW&D4dR`NnJ;dhFq8idmO>3zCBn8VAe7`p9J8$@j)CZW@)5{0wc!?ydbu?1rX{IJ2% zSJu|q$3#%vXdf1IAU&+};upLZY+-P!c@qqBKk{{TB!vo*o6ybl7l#9mxOLUNdD0F1 z2;IWgktrjvcr3bYgDFpuq<mi!TU1hTowc44&H(*o%MskgzvW;>wFujYyLp>vk`Z~U z`BN7iAnmFPd9>PWAQ|)VY@e48aVgCatZzi(a9t(i&ES*tSY#1KZti76Y@PMTn(R`^ zqy`m5`Si#vf<^__hiJ7wG_M!J7~fi^N|q5cdz{0P;rq*4pW_kgIL7pm3)}><$^roW zY+q^Cq|+$uh#hypw@?)`|8K#}hZwVe#|~hFXfhm?HBLuU5}3r6>Id>%*@Xp>K7PW| z=MVIlAq4yvxc;YIvvC<QJEJ+MC4O{IB<sIc;nK+W<G|*JLtlP(f%(~o&3BQ+N#8o1 zSAIOqknNH^b!p_3?B*TS2CHPHM3=h3Mrx^>!~yv>0Z!DEutYX`w!+3z`U)o02{Ly4 zO84sUxYEL`w^B%gRHwXx`${0WiSEra61fQXa^I9$%;;f`%*q_0TxVUI<L7s<2Ems( zq0a%drdQF&B0;NXcn(B*ZZU)UMuFnAkS<UJ15t@zb_5H=cp%VOm7^nUVT%eed4*u0 zZpreecv~DVdaWmW&m<+eqwY`z?|T2qdE}qrK7f8!{o`cCmrW_<BC-+rb(}`->&*@; zF@2CE{e`B;!+$A*SF1dyUR#oq@&llU`vn68$;DIKYs$eVT?_+av!%;Gvun{Sx<M-_ z4LH04`Dz$KAPYoezTxi=UP(r@-F+P2fd6e35DM$8s@0m$ij#_e%^*Yy>cj-H7OBW5 zSLj)nODLR1LQiCc&<6D<K7nQvgBB(!`0SSuP06Qx%I9DKf%kYrky!v*<)pWE%CmpW zkf_e8EvS6c1Z+P^$9cG?yEj|JqRbAkY*gqip^4M>`sfiDpVvdQU{I2d$gzXzlrbf> zNIS+0V*ntTFdn-1ZbThl*Bzow4SxWNa62nlsyD-?ewOOVNP=&sn#QAqc8atP1`Fy= zKpZ+Kqf0OS8|v1KcUM3-=TBEbqm?740RNF>>vB}y%=xTjvGnieoXXnw9OrH=2?257 z`8#1TINIq>D(=4^3pfsL=ms+Zt9g}_;aK#6b}L6+77IFRHe(qRdh8;gAb=nW01Is} z)r$czW_D{;_2rKaf~VM7;*43>m$!d>)NdJ*Vu>Y2^q3-KOxDvVjyw+<(%ETJ;egIu z;@VhgB#z*g)5$$^z3E*%VG%;ZJF6w}imjkCfL#4BVfX%O{Rq%jkkR$x(!!s;Cx8Rs zGOdY5`~xBKjWJ$21xvUDch94m6wS_G-0sDW@R_9ma-;Q|<d{Cdz){r{__Z*;5av$k zres2p=^OsD(ND4OKpvWdiL_v)0~|J@yijsEYPtu!k!-3aGm*%}s$r*6>Bn{Q2z5{4 zYQD9n37CU42Dd?~o%9jHvo%W8P-%SrIN&AUOjwq<jK?wlIiS@wX)mW>P%jfmaaE!} z2D8p+yis(JzzGEJ%o4~EUu+d5424g3w(?Anahx{pw?7aMqbO*Lmn~pi_BIxZ1_-ci zTdrOFX*=7NPSd*?6&UF;-+dcZp8kFE2gVLNeabkyNhe`j0EYHbFNx|Iwv!d>QoR=; zX`_kMGeA>VNAKgR3B2a*hxr_RXFvTV772>@il3Lws3|s^Wdn{yKP3p<1ZyQpBRjYL z1jgx9^F`&K7F&pU_4De$?vOKh!oCI<+nZCYIv;HxIcnm?kgN~-#8H*!+Dx|?bRf(N zFVy-y`jen8Bt)oR(E%TYNEJkW822ON2-vWa@2WG^ane@;V0C$`n55I7fYS$57JcV; z!<Q97G3YP=Ce{mwRH9Tw8fq>W?d`Qs;bn;aqu$lE#$h4t^4%xk50_92QKLBChN~)q zzA~h=^c*LdpqKw^=ye6PD|B*Et-XSPG))B#Eg3bn3p=DDYD=i~NC()qk`hi2(9wWK zV33oO{Q!v3CjZ-_c*VNBFOcIp_lz4}8i=T(#Kx{nU2_{N-46_%6rV-x)*DLS3^=T* zQ~>S5R|>GU#*sO7Gjv|gFZg`$NW|lAL$R`t9z_PI3h~9`;c2p>S=YnOo+s7^42!>? zox=+NR0gNlzr;#2eK{+$tcT?j@lnJ7MYF;C)s<+W8!-J31Y56LsYEjCk+T&F-50DZ zE?H~b<9Dnp<Wz@WS~KL8zR3=$$}5LNxpn|J=0S9bIC2>1&P}!wlYDn8SlX(V?G#)F z_iK!Wi#X>xo2wR~IZ>rI)JUUk0J;j%4#=+U;~Sp8%_12XlJ$D`q4jUHwjWZ~R_{5{ zvkk>W=CE^95(MNArY)llv+F$U`Np62rV*-dp{NdZ^5rkw?VJnxXmJvGi*T}FC8R#D z923pgaJky*T$tow1dRPl(b*6z+P0G=ZgyZ}ALbVtR$n5>6>*9^?!)?7j#<AlC!i8H z50tHnw;Jd+T^Exh6hw|%9K3<T0<p$tmULBR%6m~lKV@EDHevWxUmV>i$kffLzvda% zT>!bl2)iXv?2KIm>w}dc!A)Hp=9xK4fNH2K>oyVp?Tf(tBSviMbIsSu4;7sy4w^%D zb60^Jr9jNP1&?XK7}^Rlkwdl8fY2;_6~$n3Zb3Or%mY$O)2*z_{Ct$@3R7(>nkuI> z+Ooaa@tIEUuBx+dgdPDNJ(*G0j6DZj`Ch3-0Mp5kdjMVzv{{tU`UW8DFg4qw*k=No zuPnKQ^8vEncOx#XQOcu}I8<9yt6GB&(r=u5zle4Iq5lbqi9_ntqzg^f42@4zZN7eT zAM+U-&~pnX22-7H{!!2)>BxlysWl=0I7IMOhsqf}Yvw_+1QI#fk$uGGl5X%>UbIGh z<-He!fnBrj%#NwUR4Y-Zt0gBT(y}7J3E<36kzZE4jX&<<_TevS?1Tf%N>i^G58!#Y z4@joR2dLV{N(@XX#9F(R*WA|s-=*Xhtq50m{%=@-y^X1gZQP<OYcWs1m~y_hZ5OVA z_Pb<YY6@-P=IrsV{GxOvy>+!^ERE`~(;IbOs?KgmwYletK8Oy~%kyW!H(W%$bjafp zDY7QuUz<?TUAnNo*~?Qg7zO2$kz}Xmfw0g{u)m&n<IxtHbyALZGf@L0-58^bQj0FK z$C7HA(6ik3!}O{T(;u2Jd+gR+g3K<mA%KEWVNh&J9O@<9d4n=vR{|3IRV>?jG~z%B zKB53R{M3BxHv-?S1-ry|fSk2|JtT_WW?c76kkLqcF#yVTmt2!@H-23Bg72Q+wa_+4 zu1F1Qcx+CL_7`p^+Wh)|eY3Q}L!McZ{0P$GqiirE%Wmoj|1D0$Kv{;#TiDa<Zmbnt zjfRC_v*={TmnN>W-YJJt&0~?!x6{5iXoE9b)xUA>QFj(4&m>l`q>03h9LDimDVDht z);vxlU`_q+B`KO6-zr9qY$@*O7zid(T<pBE9h6r3iX%LrmC|##z`XH9_XcMm<wP0w z2DFARh@~MUt%AfKAiH=O;wJyazN<%Z)_mjRa?_k`4^i~A?XDn<s)UgrrU-Qnx8K=q z6-^)G*}IO4zA8O<(3%!;CuZ)`UKl!)k?it)rF-U0z*Z`%r?ql}C0fUe3PP<8`VX6J zpiuvKGy-kl<rmKeK{s|ni%FD-O0@|kx@G`-DQ|OZ?c$asFsh;bsQ(9rjLdFgf%0L~ z{N4c~12XirA$|3#c0|$cbn1f187^+6YEJg%?iuQ37+LWfL!*to>0zvk%haoAE)t~b zl6kyt_Kd;+-g64Osm-Ga>!IC%dz>(Z$npzj<qgen$onDSs#Ar=vDBTcCxO7Ug59z) z9aoc;3;czB%3Qkel``-8t!$Z)!4!9$Ez7C~_|rwvl8RsGZAhKP)5-k4Q5{CKJabez z>{>*@5bWbS0hfg|7Pj9@Q^QZ(xtAx8{+G^Sb5mFU)@7Ly`FNty8L#(nFR4wzns96W z_$FgpgHN}Jz>mW$5#B=ZmI!OV8DdMmXfPlrFn3`e8$N0fY-6ncZnwP~fuqUD?Q9BK z^c@;pPV>_uI&j}cs>tz@#E)O{Un%GnjfV54QWyH)xhpHAKhHmVuAYtY#oqzESy9*^ zzLPlt5LtFyn}eFXaP2~4^qB;(22fw0Q5POMs_J@9l@ylF5RB51wcN+ecs|S)tfed4 zwKmI+Sl2uQJ%`eCr+t7XyhY*mnHFgn#ooBs#YJ8f97h%#{(DIrHXhT$#$rXjGQepd z9JZxpKDZfxuxGtl4zS7eA}!wm!#%lYfRf`}XY1)-GOuPzolkQ!$TltuR{GWFzw%8O zcb%JSZw3(->Y;;_+S;Y~B=HX!b4WSnzg#9bEB;+gEtk&umuUkXaiu13OFboeP&W}q zDT(v{|4Kocrrh%o24S@lhw_^c2vT=2T0yHkkX$Sn61A1lMkQeySE471h|T(Kbz?eH zKbihGF#i5&;#G@p86H_~Q(VxBlsR?pqshGaj!QwSj;2BWPs)ywT<ZEKVh7@w&k201 z)p;%XNCMVKf1XBG_0lpBkRs>2U|7QapQ4a-CW7`{^6|UxNZWS0qm34L;7@3v)SRHe zg}jD>LowY(w4T^aEGl}f7Sj;kv*z|1i{D_$r5s4c2eqU>OM}tTXNK}klNX?>y6F2_ zTHdyIr=YuBQduGDhlZEjqSO%R+d}e}y>1E=b6uVYu_k^Ebg^O#mO`h=*&HM-jpW%p z?T;ciA`9J9;_=E95B1%7f(C1BsyQT2YNX);%K|zB)>v7h@9zmd$y>5@5G!tL6>lLQ z3=zOSPGfyhb8C~(IL<V^?%$~ZlK4Qfg*AqsYNHHB^qh6^>1+wsCD0$M*fnUR7Vm1) zILQ^840HY7gGKw=jv-e`>rCZQu>uD9a4>-n+z9tL?q&LEX8|TcsOu;!E!sY)Y^h~a zuznuvF*(z|Bm?zcL@-Xe+^~~TAg95v)Luy4tDoUMr)FCYHFP1TsH>MO6U?#ab@Hfu z*rstT-ihmpjG1sE0ghvnCSGK`k$K#`==gX>u*SMuAMy3+8&5GhjThzFM3j~#<msNM zwFwwLD0KkBNA$SP734>xj90#rXE)+uOq+;GiL6pX4Vyt{XNP;O{4(Y5I$G>poC#RR z_^>V#A|%rWG2(4#+3<Mg2-nTs<_-{-y?wuW$<><9prd~5KKC*y!mVIO_}$jXQ$Vi7 zcle^h%W|A7$HHFE8lCMY4DsP;V~dB=%Wz#@&e@u-1Y?$~ASw<{_`cYEOMXOfS(GYt zz0|Amfva2~(m1q8XHr-yC1iH)mW(W467dBJKjc4FaNvDSL(pc?puX){HHr%tzUI`o zvD}@!5B2ctqli$2emuXP{JN;<Ixz=<quTPG5=j<O+Yw1oj8xu-fjYaVW$F2!u!c+a zOa~{(^N*9nMIE3nF$rIgP|WZ7p0FeLt|sLbEry7~jRJ>f_%^`xhG2omd?PT*Kv6_B ze+vX{jk@Iac!{Pa12Q<prS@lNxsu(MHlL4~VN-Pw8bYhM$4_rAY4GwuByCeLH&g+; z=X<3YTF>v5EE>ZqaYKiQ_lqhhmkPAU*Y@VOg2-TO4uDo<8eqj@!VQA9xr64I2=|<2 z-vnVsMPm=pWoJd(@a<J%<GoZP5dhj+dq_c-Cg{cv@xJ@=WsN6nCrv2+%`gbMWqeVS zUQsP9X8ZME|3&6&M)!DruLfEnxNIqr(g2vA6ajwCvC%J3VQgOv>t^`t^TGK`CpwN< z*D>nvN>;}!voI~#AgEd42tY?^J$^Z>zz`)kLQ*?(#zf*6CEEeD&py`Ii?;IA1DGl} zT_X>=@!sRe)zhk>esRhlX=yy=BeWFPeHgI2zx&q-*}afah~1Jw)?}Jc;rbc)HGIid z%rYB#H&%26eI+OS^fj8%Lj`OxaO@<lmxmU2u$JMWxAu^+wQ8#rF><AwM;<-;x;nh1 z?inS(s)u`o#3vGXg9eP=r?NZ%5)87{JcM$rqaJt~j-Nu0iCOR?>z1e($x}%~y7d@v zY6R(@^^-Or5)LV(4^z0~uZaI^iTyn~kZ2M(`#ro?XMl$OhXIP7mbu5yakiN1c+xY# z-ezu~Fx|L{Ca2p!Dw5Zv+5yEG@rBi4kAL*7Mp_|oT*UVpq4fLm*CPq$S^L1VX0HCJ zlDYGH(GB}YEqw7e=h!6khQ^EcYoa2z++|y9YDXzA7>|gx5vjm|!r&N(GKJ_p1E;4~ zr`S`2PiU5FJhb*SDd*XOYd(=~EYqv@yb0cY2|oszZ1Ff?*gpZI>0p-g^21?PfAb9q zH)s|F5OB(|7yh%ck>bsJZZ!dU0<$y359~~^;nTDL2Goyc&{0nA;3X3cqGE2Vi&#yh zv&{_dv6yFGAMvlU7sy*VY|vap?2&bJ@O}2_U<J}v>liY*T52QG!f<|BQ|P;01ml(E z0NkJ1iyaRCmBuw@ycGXigDRzxYkCb?c+g4K@`d7}BO5tlyieBqOSd}ACzk*URDJe9 zthL8YvE(?><Wje}@@HtO(<W;It0GJUTm_sF@?%Mp>JXO;-kH~t^$6F3jTs=K*IhOS zE<x@Z8{zgCq#X=by7>XTbP8<~$PbM8b*Cnex8?mS;gVM#-ZLS&CevjM{`LiTQid?c z$}33=Yh9Q*2#WvuE|2ll62w-ht`#x{8yrwqZ?F&j4zM_mt%GL>5NQK8nq$;7HCE%$ zfAe7R)*>8*Z-xh=!3#03*KTylUiT{-)8rWtioZGc0}GH#ned-MqUB;TJ+Gll$;hfG zhmh4gR0E;aK9hHQ1Jb^f26rl{^i2me)@+*}roWWq?u!;>|G|PRIaR_V{BJ$;_jN^7 z(R&5<032&JYmcua7;VOjwEtK<jz&9Ty&8<FGs1I%<9;xIeSQUoLc|=gsNunOCo=8v z|NpbeJlhvx!v$Cefg1P+P4_KE8imdM=|A5rb1WDA0o>dNZAmJc;PJ*A3E15aVr>yp zeT!EMLjk_J<j@$UI@$aB62a?+z;G^-(;hR+v$zHViXR6<HyI)?)%;NgpbJx}?mjC? zc2jrC(NIZPAm_u~5d}2y0!1yIuu;+qrmpQG(bfD5hmVJ7DHtB`05|JTsfD`SF%R2< zzt(-g)WY~6_+JTLSoL@=1C|x@l<~F8$KQb=H<1`6FfwSUssfuciaPt!1OrBHo(S== zHjoo4^>@E=Xx#Q(hZHw8=VgU4KbtuEC?mg<8uXxjgdFwCpey$InZKx4P6_f%k-h~| zZf@~Y<56x{X0jQ)1`Ca?@g_~azi#~@aZk73u5#0<L306_A7VWP!9};DD-K`2M3BBV z<|KSm4k2nFCpbqOGe>VQk&g!XQskE;fMXtLegtFW#6&uQ3C!UVkKr#?*b`b%Q2+h{ z@Eg(%AF~GcCg<BuM%m1XH&t<~IU&p|uSTe_mhjYyn2(+hZ(hC&cw>gJOrc+zoB03c z!A)t(oMWdw=MqQ+U~ovrL_t}+KGpp9F&J$FVz*4q$5I%7r!V_ldj!0>4e467bGYu7 zzJO7(+n9UuwX_txA%y$N_}8oWuZmM4R`_suGAYKH?yNuGahey9PA-{ukXxHQulsnq z7sz=UOkr031s63|>Y|Oq;|>h~=%!olLmS(lbk8Ro(jBMy=*a>r{YqvgATc&4wj^E3 zLquY6K_Y7m`Yx;qB!3+lIViNStGFq3OpqfN(26H?mn{TDUy_A(AM5~9Y&f(IvbktI zFJYsq%+dl~s)y_me3f`*zdJa&)OdnnbHHnae>{l77F0z<wCcWcCX2tGi30=^b75Mk zTjf=Ew3p&ugjMZ7W<HD#fhe9yvwEJ>$9M|NC<ZASho)&nK2(c<3Nq^eX6L;g)@ENz zvytRs0?$otYL0(0M}@8_rcJIwuYOAQh*r5uN=%*fRX1l+U$bOp5XOA!e`OZ*WS#Ij z(cjHE(A&IyFVuYQ1hHZsxl8bT)|zywq+8xu-#%P<GFpm2P0~70Bqt$++<E=xHzl50 z5biboV8PR_`UU}i&;as8--Vz`ae4H%TQ5J}kgpixukXjb+>B4$&kOwpk$4y#*=JpO zkkOGEalN?#=Wvsp^_KQNtEIBhTG(ZvupHvjR)H<~*Y(gs*lEvkHD5wNfK=%@G7`U^ z>%%y`K2&EF0g|N~_p1Juw?3RX7A=3!H5Et>B4K1ybLjLo0AGgW%osgVhR_Oduu~9! z0kGJwqsh7Kmh^GSYphoScf%LKg3P_Z(H#b<IH*1m@R)k==0zJ;<RuR^0upR#!=01~ zIDO3)4gtsOO2Io)>Zv@8tZ)J4eREJOB3cRfdL$@8WsLZ~puVhI@m}Hk4XKX}{1LHr zXieqaTj+WOLb`+W%s!-bLW#;pkYO9L&2{FM>}`q!C2qne&wW|^5#KVSWHFbfMmDi) z!qTwQS7F8U=2mhHIx2)2!~9|MZ#_+ciI<OrW6-)Ssfj>FZd@M>oVJjT8r|E*O~@C4 z7;_W_i%BM!p<(nwYKLKWeRmwiXDXjAn@1W34KOg(?Gj5l9QyFQuo3+<(x&221nYLM zv)g?f8(FG#f;J4LbY}cqGsjI^*&l<;3IQ-Z>O@_~-UCD_BnJGkDpno+c0yzkA=W3N z9ie^^3~aH7&?qwaN$n{OfF?|-te>EXRP8(!Fb=ntq9{O3^-hh}@RuKQS@Z24<pXU| zyZ=VsSA0+!?stImhdr(1@ux!9{oSMa3UGa1Op7r#@lH>%rhw%(c{<RcPI4Z7j<TgW z>VkDS*}oZ^3hz<r#(g_9$Hc!@tXH_;5hVfK>j5QRkE}ZyK3eCk8k>8e;oa0l6xgVH zy88oWx7##DUTKYAhkh>9|Dj~68(mwB`9=5@Q(suiU#j3E_03y>{?`lv=Giqm-t+Wl z4w4A$i<s+RQoR{sEeKLzC^@1!5<KDip>blwJfYOUa==|DbboG6a3E}6`Pqt@E}em} zd7LH#$dSciadzHO2e#s}G2S}>sEMb1nZ6+{Sr$;6Oqi;?GZrWU%D2U1wAR8Cn$F3C z^!+c;o#Ug;Zuw(LtU`a3iL~!uwl+qKOwCAA3#1aL#*gJq5}J0J{5iCfSA~gX3=HaA zRk$4EaCzRypZ&SYfg-8HQYeEM2NjWR`S;*f-Ao|sk}ty|;J#|V*wS|-L+9bSzUSWy z$&uNMLWl@d2#`cZG!MAMt%=H{-()}@<wuo+>0YY*6k6S7*KNF*dfd*MDJHV(iaZ^b zF}XAcHe1xv&1m&XzP!y_;Fr}x^1P!H&kiyZApQAVxBBs&R-S9j#N^jpw0;5;R#eVE z1mvF@fcV?j=h8yvnqjE$wD@5V;%{|AF)=~X2BB;2KX}X8|IIeVG0eCP7L3x_WqGo= zJBzmbhT})H-1GBxCuYCj71oejmhk+QKn`4Lwc7?KD*cO8<oQq50lvpsv~FIZ$I!8~ z$X*czNzPMhR`&jqa-?jXOyyyGe0m&>Ajh?%%W@_B1Th-@b}L$Qi?yKse4{v@HJfNI zz3j(D+TZNq!S=Ex2Gz(3S`u8VMQ<0Vl+hbqhJ_^_8mlp8k}NgI9i^iT^bmyzGvrpq zV;M~4R3mQL2zvSKQPFNa_}+|l(b=*BPJoZ@z>`_P+Z3MywqtC4N}1WELJ;I};-y=h zLVJuKH2ffbRK^$j#1fvcv5>CkPtNhud*2;v`ragq$fQa%1{Q5Qd9{B%zg*WjjS<#@ zf8$~~KWi>lAy_#kM4BWgRs7pHB?Qwo^u*r9qiX_8!nw5dZb5JF;n$e?W?P$yN^oiG zTl7{>wsxtT11gQgVG1E6=|EGd2pT?U^6_Oy*|iOMU`#hK|MnWkd@_$j5v=FOlnqk> zW+%>s_bbqH-`&TLAfr5lLwM3BnS}SjiFzr><QlP@GOwBejD)7krJj-07qb8l_eN0p zY&_a`^@Mh|Qi=g@>Y;;KVCvEqoOm7$ccEYPMX2gJ_un!$_m<`qy`vwQ$@{ThAs=1E ziq5DZ0hvp|cC$1P(ZyqxNkU|<SO8ZDJKpEsnOB{~caz2h=V^(U3Y>J4(2(pW@*foJ zcJrYABs!YMQb3gdXchK)3jv~nr~_`h$n_?i01)@S>{6Ewaq1e(Tox`&d~c%Ji;mcy z@#Fa%`DclaZ75Jfo*!+;ULb;BkMQhs0+01*r;|Grqgsov5A8RnQA6;<i>D^1f&D72 zW#R<@1CC!ZS*B6Vi)tQI#|Hem-`5Mx&P5?kW+ro{IsZGCasew)Eyup*abFGKp?ugr zQNIDxoDbm5Vp6$`qt}mVytzO<pTk*FS4~5I7Zf2^yE;Y2k=3g=2S(!Fn8Gu;B&)_c zm!4V?WRTKeFKo_f*;B_$w+<C%MlQ~Lr056?GEL;_Folyls>XMMMUSbZeYV`nn|0zQ zTaV_ZfZ0D6mJDIB2G~i0&8CB7)r=6mFoNlR&=rGQ{oLx`&h%e_MzA7&R9hcZqn1g@ z7@+_HpjD^>76cs>r+TbwO40@6`0z#ShbTpw&z9NGU6Qta+1_+vwH-q**p9rUunZ-h za+D-oA`lEH`t&4*_s4q9V9?L@t}H`~@Yexur^Deflty)^zX`?eJvTeV=lx_i=%;V& z@&IkpIf}|?H(w-2EZyRCbFvMaRUf5M1FAuPangVNZDvX=q%{*6T`zH3-vp=j3jx6p zht3GUDQ>&S>CYc^`;}*WrJLl^_Glagkm+0tK}Y_CC1>3N$%8fS)i}=|-g@IG5(&>0 zKMEpbLSI8_a5-$o=DqtOw>Utfh)bGt#qCG!2yE;B?4W1d`*ezC2)<)wxr+k9D<%XO z3O=liuz<_D1|sd2G8!Sh>(5Lh0Z7<ADt+~p{Wd;^?=0_|Y$v6sSB}mZ=x~sHwd;SI zgSvep&DSv@WCB;Qyy1^giVHh9NIkLchvkW}Q4}6==6~b&)c%~5Kd;+PLD@|aprAaS z5HupG5^-W_LtSRfLmH>w>B@49n6kq%_R)c=4Ng3Z1St&ZLWth8xCaZdUT0B+niI~< zd~J`Gx<qg)RI+u?RUu=cH#4t}Ycv;uI_UcT##3dn&d))5)!XYQchLOypp;PIR4@Af z5HzjhC3ne>hvIFTe3GLJV<)rN-|@s&k5#1f<miyLzqjPwD1Q)H*)ewr@10EI*B1yb z*S)c~ozLp*iQpvpYX6)W?7U1e5@S|uXJ0glpAi-_X%fCXJUp2@Rvphw$&bD4a_^kD z-&cP=?wNxYkNvr{qxRlN5oT*sIWN33-r{nrF-04)d*{Ge1Ek#+W6iwT7I+M%a18|4 z7$YQd>#$|exj-ZI)v+Om)xlY+`7!g(ObYPMnww^`_593q!fa*SI6GQAyr(4h+SV;? zCl0MX=ui=sHV`<wb`5FY^6@JNTOQc!y><!_f7r%pCdTbe4dK+S5R~;r;?0jH0Zm-X zvE{?7@7P}wZbVsH+K_RYwJzvZb#p?F@XB<EcJ<2_{U>?w4@ZX<Sedg#r*IID%b*i7 zfr`(a8`cb;K6lpcHb*|&`VT@|Og?(eKFf!A?^L>1W)%w3H?E}*Lf-iq3u$>a`r5Q{ zg40oPjSwi87aeYOnz4k}ag*MiC}WFlK!!UIFhn0Wq&1gCLRCl?J2MWyjfzr>KU}R= z>*zQbyNQP{?DAOuJgx=oE%w)vX<unfHC1-;2HTXC9;9jtWTu`}G<P&ba&U+hv>mhE zp<c#|Tm8n&2n;C20rnrCVq4>ZlXJ)?VNfY59~j?L63kDZP$w8@YmY3;eRUYg@Fl(U z7P)4krfsZNgBj|akTTFE^~zsPwQ~(uYvyoGAjmxh&M&PPu03y18&B|HEBzCp$119K zEg-|@fv)Fhf&X@Qm<<3t#_W)3G63?4FcRLG<{OvQy+K?Q*18(jjkINxjy>38{N*g? z_{w*)?ocVr9qSvqb9|XLtDx;ue)~ECF}m}pM+Q%vLt-w=xLiFyI_dz>TkY)Uh_uO9 z!jCHmAy-+Ea^@=JsgBw~uV8`Ik{o^Oc{E!J$TtIJq}D#GU&FEs7E{BWuZP9tCe5pC z(qF2i4RN&JlJ&PbT5N#gzwMLYx04Ww^_@u&tw2Mt?bSROUYFQvaMz?G3)fnJ$I)zt zwyisPKE|3|n@cE#E*3@`=rF8J1*|=U^oORricvBF;yoHQa(q-u;(D&?#LW?GOo75~ zbkAhyF`GEmO8$<CUh@fC&mYnl&EesTVLUe#UUYsn;wN78p^ifsvM@^mLI<t{eLAcf zk!Loqpsn;-lT5Xt&a)P+<8jI@$X?i8K&RLkkzckw&Rnx2hAqOY7<h>cIj)HIVfde> z^cumS-t1yE@@IFTpmrxag4wYAP7qKj^KBNmDdj>92iot|`BTzDM$nqzHzOp#+cZ&! z+NngQEb-H!6h^W0y=k!Ul=A-7B5%rSUv?paqHLs~qG@;#`L~+28ctu*>U7s9ot97J z4<{5trVC1RLOBGv+jXw+EbSWvSYQjjWdbh!EeXDulLMkoo}Y_aP?5%DqP{%+@B5dw zOx-n{HH)2+BFB{o*^23L{*?bb@%}vo;(p6<xfmlAH@Vb0;75VVeb>#%7epq-wRial z#On-dAnFI%>)+d4t?}(^K0R(3Szut&9NwHdtweAj28oocw7S^uP4UxfBKCXx4ywDN z{RPp8QY9^Bu4_gX=-<<c?O?pn!+0Dy<Q%L;@7)_2yza{Luf(>042SO@#5;H2Lpdp) zpdad5^Bs!?FySI~KdCsAArY;km)Ni()e57P(TCMMg1wHOF9A=KpcN^EZQEgHb0KHh zSe0!bbk`TQ(j24HvLxIo77*<ABXz4gIom)F`IM54V>kTJQv`ul{+PYkI+P&lR2s>9 zRkq-A&(|IB?@V|`u{&|zbg!Ux_Q$@r{p;`!_YcR;Kx?B7tj>V+Y-2KiQ@oeW?PyAA zt$;gL8?}BBXBppJT*pKqgsIHS;FDYYCOYlCQWf>G4LvK-VWPtw(Ft8^P4VVSdrT?@ zWzs>q#JTG>p*WG{QVV?V&FnJaNC<qzv<)$5)-EMB2a>5rvYR7`E<0`5uYj4CxU!ry zVZ!MEI89G@)h<(A;K4|q#%$LlGGJVxBU9dZMRe_P9A}ZQRwuGEM)xJj7N6L&OLs@` zLNxy4b`5a`y*bhKDO3|c%%4A@UD9T&B-JB<cVhn1K(bL<4G|6R?!x;<nTv`-V*7V% z=0IBj)^c^nH1i{t_p$qz+=bYa>JIv1M$&jQRkwu@pG05gO>Qu7P1gKqH`e4YuEiAt zVJ7wm%~qB7YvBnm(NePzs5CQkuu{_1I-)yr1d9Xs=6Mq2A0R}&BFm1pcL4N1T2DAD z=UR>|T1^)5OqLwjfr?WkEXtU6i^_j7eS$WriWf9AUyGP#9w=0jCgEVsb47|++H)yi z8B~VyhEV#@g`$h(v3x*&&aP?e=79meh$(_{o75M|3NXsYsu{f?Z=g_L8ZbmYARdAZ zh{_v~_LM4fRx66oY<MxL`qzX&R6gp%CqXTB{7UC(?nE$;)N_p*T4j#Oe<W+P74F6J z40^%G?T=?@LYkd34)fnEs8uv`BB^o2lv6)|*N6AFUBiV$QRd%Swe)a@(l->v-Xt5@ zy>qhlD+j6_#>SH?m}Vt+#KIt2QF=4s>Mi)^^%p^xa-rH7j2pv#u$It!a|IhrgjU%G zGB%?4wHH{~*&+S9xETj$JYGoshM$u+Hv)kgywPp4tecKpWY?uG4hL^DA0D)ho;>nf z%T4B8P?jn^qQ(``Acs)_0^Z4mT09R>1P1rgxKk!nF<My@)4)gbiFKCZapvUTMf$}Z z^Cxa^HyC*zzAvbAqZLBK$dj6gpAo1=n;kxF!c+WdU5BP|W1f^A=KB#Eb{&}~gOW@o zEp{4qc^J~RL{b=>RbS7pHBz_7Nd@f3bR$J(iv0&CkiF!y%mcO?M$L@0&~<CZ(S2Ln z)OU8~V-U%zMwT9uo-iy&aiz(Q!2XIHYA+OGp`8-48dnZbTMoGks{hyYBHT!U9lW^O zUt7rI>e~2<X7BKnu7WJWm4#OJk<(QUt{X3SyqYvU0eL;Wd7hRFYo*%M{m1I!5u(H) zYl06e&KX`5P0zJ8bPMY0SBbYd5QFDhs6-PJccpu|?@WXs?>nOT6HEF@9C`&OCnkI2 zE4s<N3)zorSz<bs$C!tef}h*}a07Z{GPG4FC#ZcfD1MM_4Ov>OcT)E`v!J#6TgC-I z3_%M5{ql!YacRD48ABdk-O@Hgw-<~WPnF|$v@?GDfMzD#Erth{q}t<;qwqk;vtO3- zmu~TLzSH}NjbkZ!WJob#3dM}pJHcdh_a030Cq5<a`clEorguYzgNi-p{Vj(OqorRB z+chRBysNJMfH{JpZe=mrPN(%j3fB^Vb5S~L5&W;)0CPcfI?<_To)R4o@eKVnB4>NU z4CDM2hezyU3KHsTOy5u`X8?Ovo?$oi7twFrJ`xSPoT@J9wpsG>r0AyEl?$PMjQoAU zTv=OO&oa>I0~^AT^RirlG6TmubD9Wwlz7I3y(ut`*^@v$=WZGbg?&P<bBz`K+prf1 zci{$_QUh(w_tkQB;y+7LN704PhebBNRC%hLP-tBMlU+o=z6~eZ@lx=dBMF|ec%iJJ z&Vf+bJHN*%CP+4b9QZLv_o5c4k8eC=K7JaTAPQ<eWBqN>=E64%+#%u}Gdep64#dQa z){)1H#n668_v||-sV}9!c~HTaj-GyA(yNc$W8qm}bCJ53%TWRFa`Z*<=1x~V%Lx1l zlSNv=K-^BRWj;*L&mQuh3`R~iOLU9#H?i_<<pD1QR6tN*IS|M2F2F^F9WDhWHL{qJ z=1H$&0zdgt{hlmF&nWMR%#&@zhp5V@4<X<MXQ9s`IiqW!JN=&C96S`^;>lzjBCNFk z?uBwZfK`IyhlR_Jf%q*70P^6n__t%Uu{+3=DErB1Aev&*mm=<4NElC3?9pt;3C6J1 z3`EEW0+VbAK^Ge{6q@9W1NkrHGHd<rJq2Wg>*9ZhLATQ#t6`D?DuR+4aUrIo32s#i zYn#<Xd1t~u&JDqsT~S6v6e5B%g_|N`6$#@{{CKfOeNra`m$3Tha8_*%?3tQx0HxDj zxv<i%uCbCW4;3P|6T{Q0H+Iv>G(C_w{1`Gv_0Bg!Sp*(vjb+fE-c0-6FkM;9LOLh{ zTz?3)tn+C>p^#YPMgBB4!UvQ122yz2izMH49m17>=scYucXDnLAm>wNNWiloA)i8m zpIg^ddtw<@h8DSN(d?fq4S)ltiu*WW^%7)clP;(F^#z$DgWJvR+>HK~N6xrNKXv1L z9e6?KC{^2~-w;$4wA%bf%qXEWpQOE6m7nFqk^X!*egt3^vPt%`;<~Wr!@fe5nSshe zHw5F^Q!rXyjTF^0yk~6V()I3{=V-bE$Z*>2cRkl`mU8+b%p^}t@5-m4ejXPAzGjoU zh~Y+SRtxQm`Tn`=kJXQd-hx6+!LYv2E5)~P@Myxfs0Sg*#6%X&>&%7#(^yDRC_C!5 zwH2hfgymVZC!YJEZaIcwB#%dbJ`ttQZ1nF|*s$#M8skQbj8zTxExoY0+-xCn+GvG5 zw8emX9z<O@B?IwBIMPw!Hp@P?Di7TTl}ZM{6xeX>5Pg<RV#zbo;MCyKH9yv?A-{}z zJsqB=X&g)=stlVtKX*Bg6`vW^ZcS$XJ5z%)Kc#jD4%~p1^Wxx`wQ*wVX?*4XKerKp ze!uu+e(db2K`vPS;>q`FmJ;K>>#?sSwyK^+@<I-~CfU2I65hHsO)dbggW{W7jc?6& z)X=|5{X40LyRgY<&de7@%6S>#T1z*hsB~LB7j6POm}z2ofo8P6$+mLueD|4+c}-cP zg*Z=I`~^&v`N!cyFYOZ^V~4L;nEQ((E<7fG;-QSeX1MMO3<rzE#6vXcpR<+?a`1K9 z@Wh^u<-c{cO%N-ATW}}y8Pc1C7@EM;L+;uy{>uaUoJ%LYTqBr{0dBmkqzd{oud@`d zA5c6VSXnGq0PDRvvHnn|r2k%AI@mL0+7tAvWDufNNU!~0rZYHwz|PmuHRo^wlT68{ zmqwVFs>r<CWPaY-TF&B8eR}iz5q1Gcf+V!LRxQOL>4DSReDNJmm(F4{09l9)!xJZ9 z4xkQ;9ai*yqcdhp@~#43In~iQG6uf+bH+yVxCw%HOL+Den;+imm_PqG*d#_?!?xt^ z0IH~|;PY8f_{@0<wZF8j{G(w4sh8nmK%|j=e2h7{z`v{?3<Ws)*3D_=v4aE5L%Q?> z7Lm;+Sx`k-y=pW+ZO4BmybYnw&)lbT&(LC@_Z9}b>J>WYIN8xOIAK<l%xWG}2qq5F zZ+wdy%QLT?Lm^Z!r-b|+(KQojq1pu?24ah<SysGZCU@FW*1}~m|5$GfnMU4iy9^fg z*6rRAZ6{TOm+jqSLzr(H2JpP>mc3;!tz~Q3HkMnqZEIP}TE;)yE!+0;>e+{QUS9V{ z_}y>r`@GKUIASn}{b5*Fl|>XSN0JZw33mAUUHON#Y^?2v-4tgtF7D#4xU#n5-#xJo zRlTabB~7HE4Wxvn9p_fc2#Y4MMCY+S1is#6amOuSaRwKS)9LTpE3^r8hc@bKSvEx( z|Hs-7`9&l0@6T%|{Y5wU@H~~IpB7q|tZx`Skhv+97Fl2Dd`Qeop-`&@jwaM#@9fJf znFy8%G}Z-PpNGG<8bJoyyV7@e3Q`Ja^CsE1jKxgPn~&oB+0N5ivFK3$j!inher}t~ zaK5LgSxu2;Rl`!pJrl5bS%b(mm-?aaCfaZ3j(2P(YuP{h@U1EYkwI7Ra97G)ihg2A z*>Nz5CBR7B#vq#nZ|ZH7m^o*xUl#KBbyt8bB(LM9_P8+Pz{DwB=HNVj+_q=?9|MBt zB<D`Y*tsE0jPo7*&uY|a$K|oK&)S&GwJ+ZA4)9Q~lV4L$HK3J$U84-mj{Phfr43VU ze~RO+hnUQv&@YWnp!CP7l9ev1oA8ep7)QV{gN(@Kn};-K?p)aWir}?;M4@6=!w2gC ze@;1^?6xEQrLz;hGVL3q<sYUZE0_WG;Dj}J_t`+--V$6!%FYm98;a8#$@*`aEQPPU zQKe&k^CT@X&3CKxX!A;bxHsuGMe-$&BY)3UMV{kqppEk>`~MV{9f~f&1;CuH=?SFM zi52%2{dSyE>4njEll2ey>BZFgwPMU9JRv|`?CRUM#=EMG(K6o-?~m^_D0WbtR}%>7 zC0as9ku7Me#!^Ow=Xpj+=WKM<iTgM?Tw(}RH;n3Xh18FI+zt}V;$P;CiE~_$2VPa+ zsW-XM_|d-f2^ZYCl+CQ(h4|}N*{^&^$@^%U_s9I#)&6F|I7&jmbye)FMBC+m!NbBv z)eB=x;rLDiyLx#K%{TrEN!bJ?%hy>N5Kl>8@$SBF-a;hE#3dN$lm4Fko^{tK&#Zhh zTyCL8iHt;kYS;4hvvKR&Ca(=gE5u;CvTX+CL_>69l@-}$Y(#jR;XC5)@9iyWjn3F& z`c*c+JpXoOPfGdD>TPixrDpggda<q>9edoC!5NcAIUWt_?Uf)l#$2Z~&&R*`(yKz@ zUgE_}BUlK=%rJa-ADWY#L&sT^iqtZ*B+#m}>>qn=uN@G8s@fmFQ&RHZn36&py1$ns z&DQ*_df)GR)jmyL2G+QITgu0ruIW21h}QO29`j)=g(%#s<Yz03SAA^jg;C+}^xsPm z64$X#(-6zTU5<Wfoo->6(u(J3VZ+(#q83?ja!%b>3XF$Us`IN||KQ5!?#IY_$Cyeb z`b*+auTOH>=dasBZ~w`!<=GhuY#r}=flUQ0uHEK9$nbP53b))p7pxrj%8F<iW&dp? zF$L*rx=>sclcOVCzSu43j0hmL#FoeKHhHJT$!hXHB-U<dkF~l8ry_654Zirr29xYs zXAUF;v3-|#V~vcNo!Is0aSo$9r$R;NLE3z3^CdAhII7$HR2@AF+jPOZHOc?b?H3`6 z?NxxoToS8O`4j16eLneqP$E$C^v4-IQrQvV=3G515)b_#JZ*&5Xb7`YL#`mc^#~%t zggD0*M^SQ64lUvm)oJf^d4<l6A@58K1A~O`IW_KU&#+M2Iq_RCRm)VN)h4|mF&t@f zLyP`;qPc+ubl;k#t`Lt}li<4u&2{&STrBC$LANS#;ny)m;;$UBF~iT#*=93z-%(T* z_};+;RKe4$dVEOQA8MC8X2eUde{JN{$E9=T-BEJu7GSo}waJ_@e2qV)5a1bx-C%X6 zGh8Yvv%dLaD+>*u_R6M_>?}0m)vtkU)fydaG$c8-K^+s^ahxgmL-_Rgg2gZ~c*}hb z-YqksN}_oD-_6WX;yFF6Ud(asZkGDiv~ii{S=t=ZIRb{HzT8ULyWBYzA}%641o~U5 zWR4%qlUk@$mA-StR*pQBO8J>b%&mpgyQAlLDg7bE!(+U3;jQRv;zeO~JO6GnBvxqI zc8`sW__&5s|BZyDjWnScu^3wr*85~V#Zb^$zU_znMDL=Bd(Y57dgd2EFF-6?YHrjg zjl%_bS~+a(I8*vz7m0SFZFsANjyS6se#zet(>M-R2eTSg<F6&fga~<A@RIx2BqLMf z;rp1%Y4;q8-}gDyVe9-yBh^M2ufhqGf)_Z$+{&^H2hC_~k-iN5aX5y3>el%^M)_BJ z!Xrbv)9#Di>_US+C8BJKCud)WFrog?rgn817=6E~3u?8b`Vt@-LnSl<*AdR6;wx6_ z#f4k^A5Wk)qd_^o(FW6&v!2X1f&AeRT*!P|17`><ge@P1Vr<m--c)n@=6wdXByN%6 zDxxtdE9WGSg_bt5MjTD3N<ZpWoZ-Itp;|ROSE&S^!~-8kBiYnKED{a-&5_?19fh4M zX+OQ$WiU}Y)bkkP9_p>mF+Z>|dt}LOE8nFLc^6^wKlPKWve6rF(KUDFd}4dEzM(Rz z#wl$YRaJ7|j3{K*e4f=?JkLoTjZF`*3i$nAtuvc-n>;O__)IARlaX-daeL&#PY)Nb z$&Uj!(xbpLdykX*nQcgApBO@}f)TG_r*G%Zjwp*FeK5%AP>+e{bAjrby8U{}M-7q1 z)TJ@H<tI}qIhDyWDAPAuNCHRs?A7#JbR3`Oy(w;a*249Eoim<<;0-4lbD4dsvFTYM z_A(x(WQte<J8ihhW7A%2n}pqNsi23PN4>ugHoe7z<*rlQ7TiJVT8?Rw?B@AVxyXh> za?5Wx12bB13h_ttBsIlCr}@IBD6r}Ha2;hr+~JI*hSaFo;jrvuER^!6W!N;=yz_7> zi)I@2dGn$L+BUkzrIahK79pEQgBMdS3%82o=cV0f_dF;+&o2@jK2m<;h9+(P3oUj; zfgX68ea^$HFE7@?HNt%y&Dq9rcX(PGz?P>pe{#p9oFAl0_WCu1V)TKI;1DAL?g=|% zz_CPNg0)V%F|?VDNv?GEX#&>tPgG!E^!x%W9VX;|hmMP>#)?HIs5Fg=@6?bf-+h77 zH~n>jmMTQdPHb8v$}Js*P=ib%-iAaSpLJY!akEzOpMm>ekkC>4N}W^N2b?et+4$2K zWv7Uj`tGod1UK2pTiB1#F0A=UD*Bi%IgV5&&b{JMK|Ri{82+ixkKc9*qsej8RU!G^ z1?bZ;nlHW!Md?_$p!@qfCzX71oa)6WZc8;ilH7~z{D3e`rcnD+X<($ntbi3)5P9%E z3{P~ACd@vK{-sM$+FJD17VWj~KfBksvd1K@=^hCR^`d*XE&o^lncgn98@nOt5|Os# z5m@`~T9_w0l9rbF;k~<o9u_IxbkD0wTf3NO^C(m=Y>Mz<dVMRF(;pUMY3L!t*gv8Z zPN%x2y%+yFb(uH_Vh$zj&wXEZJl%n;C-Q6Hy)vLAzQWKz{V7SrasxyByh*38I4^r6 zl+IwPx<mJ?#G~e(klw)P4bL6p>$RJKKq)Og6G;#kU-!xk<AIZu<1iQ1kXi(-UL+yL zx@4rGwrO0%ty9yI;D3wNL6?|UA7ZZFr=4i8i@Ro`UQ?BRH_=1Zclfvj-xO*fxTJuX zMZ%PfznNo>2~A}TOIE`0$Y;Z^{#^I3zxTG&OMAA92Y1^6lYG`?@7{UIMbBDw)N`$8 zeKG^NKtO`>gYR2W^bhOFxmZzeUecLbkBbsNad!x80c67k4l%ytEdt1^-}?$;pMIxm z7UXYt^B5)w!F-nSWdEx6gM0zK<IEhskY6M%kl5K|=HYDlPSAS*JqxL|9COT$Ff0c; zF<VRIJ6zLYc>g$>FtK*!`VGR?ulw_6W9LO}r1ZZnm14TS5=)hTiGNQ~T++QiN%^_B zNY3nSYECfgev}f>j~B_sjV@1fP8ZQ~{)ptrub*lb;-$$V8sf}D`6b19&xSWd%{Vm^ zxuOWA^Av{hFBZuQUN+JvO_h&4|B4v=gMOqzD@4Sk>8A~I?3aJ^eB(te4G+!#MPxuZ zCHjq(BNIdEZr5A6H8g>(xztyJd!nxQOY0#?k|;dBmU1Cc)m~pAM(2AmpRad;?gj}( z_7vxHl0K7~mH1J*i<rbw(Q!vlM|}PbfoZ)=YlE*UJtN8T??s%}CHuNK!_GHllF|-c zblqFY4{s3#KF^^N8P(oTaeNO-R+3WMZhlfR)!C-;3ZEw3s~u8yXt%lPE2cQ*#`4T; z;G24q97YRBktxTb?#z_Ss4*#B7jwUc9I+36pN8;v&0#6gkF^*oY(wiYok7-QUH$u| z8z)9a9$_vnN+hapGQI=pqN5em&$B74(b4v-LMD7jQl7LCPQOJVzh`HP$x8a@+$=47 z$?Ikprib0~W6HC<{>bsS)FX1wP~>V3BSUHD&FAy4OACrZ3!W#_8GQNGu*cND$rV1w zKNw@`Ke5c2xL6sgH^y~XAfgrXD$4E(d?1I{w@94cCFGB!!NPUzQLzqggSUs%_!kh2 z&6#R4xBgq%@>F3QS?WK@D0lv$iXS4*7(T1%M`V#<caEcSnt5OHFe4AewxBS+48; zNi&~7jYlnpNK9OY&pVeBY)dkpmSQ^4caGljNgR?y)j-~ZguCDgz&p{wFH=^)5S><c zok*cI9C`VLjl2)}VC;Lfb`GaUEWM(MY@V!W_Y-vlBL&xk1qY45rM=IW@MEg&f+CE3 z$-NM<ww})0y4nnEPES@AAM!I^Gkrkba&IRwa}sl4!8hkG!P8{v5Y`aC97a|fZ5|#a zZTrGtM|0F5&kFUNl!UK@8QwhOghUcY^p4F16+f}Km6!8H+chT;R+E;8F=qHzikbB( z1xn}74a?I!2f?q;J7kC6Ih3e5PQ|qw|Mw8^{OWhUP-@utiF1BeIBPFV%!yATcL_R@ zwravT3O;Q!Y5(EXCmXvF_X~%T1}eEi6P#&mye-|@M&Q~VKTdb)Cad3iq^qJ9hJ+8z zUwm@7Tng)?M!r1EfCBf|<T-cruG`78kWu8$H;A^1+>pSh_>!@_P3>ftQ!CE5cL($| z(~Iy->-XUXf|)WEg7fb_)jEBBQG8QBF%~jvIL&qprU;d)xY?4KV}*kh$xV}nUh0&X zh4o<yXPhzDY*e{8*L(S7Qn_q}bs}zipHzR_4=Y7U|M|Pzg~7E+OjkT-@ri{1754*K zf2UL#ajnzK`g6$rV{?hnC+mGF15f;?wKn|(MQFF3!>d~Y&vHgqOc`l{<$*y_vGnfB zmPs}V42m%0L05uD-YV4Haw6C&ld%!c0`xWJe(SNCn*%sk34CPO3*^igXsF+QW&WnC z8dIrr20eewS$pPN5wwYYE(*^+re2Y&n&JEX!k3zij-gEz$x8oO=yKc|;fLvraB0=% zG0*YyKk^Zb2x}~D!sNUIf8iA>dnmj@QNfx7Z~cmGp8r~|tUOwrAXRw32ai4|vfZ7M z95N^E?7p#=Fl5bbsSmA`CgZSu`J1JJnS>`WCbaD$63D|pjPqyc{ga^4ja|JNTlh(p z!gcsU?5~J<uBKB;2F9&w+ST&oJTIgXmhJAxOkw1f%m`~}k<1HfFOsu0>rEcLNC`ri zJT;PfThe2WyAu|%*wQcps?Q8-kpv~B5A?dB1Ag)iq8s?XJtnc*^Mhr5(#ps_2jyKz ziu8TrJ9t{B#dJKz4#)W*M1s$Q))hv`H|vY<#$-~wttD@yFEs+)ZSu=6ZL{AWG4aw3 z-8g>3(NctzMA#UYU62yRePpNe{)a~DZj<U6w%2SU`8Sa{pG27pcf<OwE6dNfpXghg zE=V;`CMch=i03PB5mz#P$m5X}3N<-b*ncMza_*9e>667!TNMk-4s<$DSeyCtOprn} zwT1PE;Zg3X1OJ85&ASy#!sJV<n6D)zi%06+XU)@C3K%DmhC~9KkLJCSgPC7;zQ~<7 zV`=yOAk)F?g1;k{ck%gUK9((o&g;B^B818J>+}on5;dOEkuT*V%)6SMxNcn<0!A0% zl|Scqumo%;e2P<~OLwGY-#ieaA4-d`h8&EPdW-~Sm<YcO--V81C?J~q;93dpta)$H zt6`|6zVIHJ(U|&h1!lV`%8?mikuhn=J(F*JJv66ZlnAzYImnj#fDd1VX)q_%M$*X5 zOL3Slj+*qB_`K+_iik)uaJF#d(ZmnV9-9%XN^Q+cHsDBU-Oy)h)41tnHL%U$KO3Lt zzoBPE;ZZp*m=NzB$0Z#%qrN>}xksLC29R~hoSduvb#qp*yhih7KQYT9>u>vFd(||N zT{F$XD-^?;ePEnzK?-H~4)Was>H6x8EVIgLf!!lzd=ULN|0v``eT7;WT{jWI<iJgl z^UVp!|Hy{oC4T8?%~gr7%@o7>S;(1)FD+fx2RR_{rzErMF5`Q~4TQn4ywq&n_UNMB z)VF36VHF1tqM#fML%jV;;np*N`Fhl6f8s1bCjl`Nbm6U5a1Hxa_8|LXYci1xHT0^O zG@%yq5$95nBYxc1&TXQ?m`wOznemdr7~VI}L@C}%HT?KcGf-kF8r<h|7uJeR>nu4! zUCAMJJ?DxeVPu4?%pV{SY)7-yByqFzF!AZQ&x{M8E+(X>{wUt8%}3UFbIT>hvL;+x z+>Tt%2A`<F;;P~#>xdJ<!yXg`#uH1gZ)bK8`QsfjP(f{t!7tF_A(vmOeiQtLVga=T ziKhzrhMoGk(!IM+?ZK(&egAEUGVU$c1J{fhH>7UX0fbnU*C`ZbHW_d3JAaL@5SYqz z-y(3ndXfvynXn8c!-mM}^x7V#mi{<Xv1z*5jiLPHR;$C#GG&+|QLV;wJ&7<>RF`Di z)51Oc);xgjPaOQ0<0&6fI%ZXUTSiU8&}#Kru#)Z>{>MJ%mK7U>6Zax|guKE|J#1Yn zVp?ydA?7gTZ@<t!55`lnubVgF>3%r)ZlURYP?aPmzg~GUSQj@1)Hu=g3;&SE?3AZ= zX&XE;@WDy<FFdAS)Lb<<ze=7v`VHA^vm-vug`92q1!a{bG1^Tt=I>c?DxmgIe_JZZ z9@{O(3nh4FS4W+&UQtU~$P!<m>5gE||Ec<60MDZqnqx79V$cU-_^VW(zaKdlB<-KJ zwa@778(GSuQ3j%vK`NwS$6}C>(&43nyy2Y&Cl2i%M=#EqsDzH?BamVI=%VvqjQBgL z{zy&AY<ER0LaWfa{rjywx?Arkt8JArj`R9@?XtmE8Ja?1;lV9&EkGPRk*LLdGg66K zPC1bqMeg0NI;;v4aUsrNYR~X0wbLYPwzMSo3Vl&{$d7<M@pifKfRXAEhQApU9P64* zG6`Ayw!vriGfC=%x)MdhWDwJ1@J-LY)nU^md2+ua5mMkH-%&UE<3YHA)y&VMu@L{k zI4sPC_Tr$QPN?ROF8|&n*LwaGK3yx!eklHZYF|^nu{ZwvD`CyX(FBBPX#7FVl};D$ zn2Lv=(W<#_!HoL-#dSg`TMT;A-_|AA?ke($l*>8;B<%ln#kLMt$uni49r>DUU>t~F z*LZT;_!8AS*D2d{j8SG<O-g8gkVY+Oq~$nD_gH-+X=n};uxMR}!{26Sj#m?e9(Wl} zK-K*W$%A83Gih1<%PF91A!LaPgIDm^Zq*B=h48f)3-Jp<i?++JDjab^;~FBk^}Vq5 z;m+!CEiMlXn->fPDDx6=UE^(|AL>`^HqPv6(^rr6d@G6bSnNcVuuo^mAD8$8HFR9$ zI!NPvRV1_CPB6`!p72H}t3NzcQ=zs-dTgO8H<yMFwPXMIPN7PbqfqtH&zy!!4pR2A zt&f%Cn-}FR1%-usEfsHyZ5F2R?{1RE_SO1x=}TgVylw~&N5`FC`)6orJI=IyImY=} z8$5zihhra#R!f!Oa+O?>3UtQ);_K12uOvm_6AG&{sHs*#<eMAmp}6phM7~|dCR34r zo@7!YU%r1<QEKNVEf<`FJ1BKAz_5Qvf`I4NAEeLySKn|;-i!&Y!8EM7N-vpv@!*3O znL_egI()f>XnWy?xKcjFPtYCxq>;^{mB$Bmy*)!fFSZ)3i8<@yqNottVKivXKdA2L z(m>r^>s`t`gqLWEob)J5!`(z0<=pQO3-UDms0LdI>ykzN2}c}lyTHd*qp=;tBdatw zGdW80by<_mo>fQctg+sQRIgLyUq^ba8j{{(W$*h*Ltl7uy=qd-&M;zc18->-dl2Lz zPQ^bt&_oJsKKxFh=+4fr@?cFE<^9Gc>5#~A9m!;4c*-0d#I;G95v`BKIhuV_`fwgh zt4fhJe^d_1c7QLvRZg_lA+yQA^wB5eY{1INA^}FX9FssL3Z4Ng<IDZwQVN2Ts%W@% zF#0NVfw1eNoC(G$N!F5gCGz}jsLE{MPLk{g5gP&ocRyX{w(3d7PJ^P4ZDlb-q<@-} zew4NB^$7LpjiG-lj6gd07p2)PKl_{bGXBS;GZd<o#Sa*j>}EY2pCdF8!VJ6%I%)z; zqhgzs4q0S5k_5+f><VpSo)G<eZco>d&@(YI#F4ali~M)yQPj%!7*4voJ_c#~atT4t zrH>4YvgqzZ@0jjf*qLvl<Ez&sp1)^H4rw1}EDMPCtc94ZVACsj#1|taeG+W0xODY@ zqVm!yU~O!_QxJufcQvD)|Fa=!yMXNe3;$~Azw@M><g-}YZ0m4xxq)tEY<k8;o2J=# zx_kVz#MKVyH9KkoyPXi%s&D^tjU**!!)$b77h0KAvaJP7jsK0`^cUcaR<#{!AxHiF zkC~HR_jS(*VAq$CF9SDPD1T{IUM%n{w-D5wYN_i%yqh0xh2=7#zm%zFuDHym;&#Ti zPPMZvGK~BFk&>sYPIcPIRxr<3<fxyy1WCT5woO+`nYno!GO$~V#YpIyD+xZLN<udW z<<BicO`7t5DEvK$gl`<v(QmYAeTzc;S5whqn~o>b!|v942rH!*KgM>&^(G?K?c*g! z$gSO*#CE*u${VR2AA5cf7vak$t0pelxL__h4C{MVTJxW%;5j8t3oQ4{`(jt#xIdSm zyygYURB>6cNwOaELfg*pps5N@xA%BRu)EMveR5g!vk@OJn^R^;p5;`2d%L5K-hM$l z)T|X&8$KLI!!^`sRv7s7uMSok_lP(pdbP_&OIww3dc4S%hp(-Jq|GOd?OSV5SVykI zNeyoW^3a#vG<^vj#p0{mv-HDx_@jRQe{D}gcO)<)vir8W_l7im3Gyu5p2TLzRsDIW zsRQ_(x}E&h!~5)BWQzmi5aCKZwMU41bucKJN*tNK#p()}`h;_K;wu?0x(FHJW~WNz z6xR;#u7*!5(|Bu6^iZ089bh>NuIJzzwp~4W2uYF?m6<M0eNWfUx4---u`?#WIiB&< z)DEtYu?obYrC~cmBvqE7Ts&*=rFA<rl9c|`s9zpsngnA|DWTNuBS>%3g~eMKrXb?G z&^tVlLSy<b$B1FE7M3Rl<*`CU6z2n0Lj;<3Vr6J6k%ZzJ@!Uc_IUHhwAfpQG{YxFZ z&Vl}u-NIO^Q{0ncqvhgY|9|bfxs9<)uHl6t2g5qk@0lJbA@K9pY@?>o*mTe)rmrqW zX2_W<r=cR*_f`nM{aJu@3;qEefcU;@#oNRyEo0s@wNi?vq5ECCz`YZ?+RWo*<PRj; zDHDy5X=3f?5|!(QOo{5XE`4hnPPYa%(sr`SKW(u3?v|AfFsyMWBg>1yE<G&f_s-Ed z6!<s8u6`H8Or~M+AF_@96MLZ%?b7CJ!!gwP73I0_#ils-CsG&x*jD@8+x*>%<tM1f zdJaTSYUI94?J4<Qsqy{uZrs=gF~j~F@(s!v;q<ea#&(&$$299_vBB}OwYK)#$~T<h zhWv|FW#^AvFe}vD90fDs$r81Y+Q*;bv-N)q=d!R-F{eNwy{nkABgOs1*EI3zxW?=i zZm(jYnT}}7Dn@*o)>FZlg8XmC%q^MuU~16H5vOvGJzDO+W5GPM5bHVxuL@hy!yB?z zyZgZ;r4Bu*XI#VGF@GerWC4$@+*Vd-%$ueZ?_|m)bILsm2Js(`4y|zu#0#B2T=sUB zJ3<$dr>=Wi4syt=#dup4XtJv0FRkB>tp_a87@pe$ZAC)ftrGQ{xn9cgzYjcTsOv0f z2}P~L9O!Xp=CYd-$5zF^<>PGNFSq7xtz9Rwt=~|Dn@9BB!8ni2Hse{VFxPMSy6z<P zBZ32&s0MmW#i<)JW8R^=lY2CGClTjAPMsoECaKwuke#G-sZGSK3PJ<gwm5~1moL!J zCdN<H>s`vjdgzkgzq_4hujNu!+0^R#i{K!wM>8A!p9YYqe-XX)Ze)EJABv-_BGYyG zyM5MGE3%>ojp)?hDiGiokE`%A|D^phju`nS0i1*2QE#EOa0K0$q~rXy7iH0eP)U;o zlmHZVab%2b+T4MhER;HRaE2^|bqo6bzhdwp$B|lIl!>(Nm&(yARp<&i3!5PeQR_IA zm!8~@#)bjPK}t;5q7Y#ctVLJBLY55BkLxZZ(6s4tJ0&W7ft!a!yrt2rc5gSr`nc28 zL88A_@{Q3QA0}?5`TCY<q;7<|g-PCm{y^tKzh@2BWIXtQYU@fUlV%*CWkE-(@L%fA zUkoON;)INRK_nXZ2DHhs<`b$mOFwCIdmMtgCoU3Z=piQ>iLtUID%9|~ZP-mQvei(Q zcYj%6eX@yhbrf#zo7XzlhbMSqFKm9#vgFvwSvLu|HjCph8{h@WDn1Gg8x5S@`bd50 zl;3QG_klS;-R}~RVKzluv*#wG#-gLH4Y-vtJt$Hq>drD7erY1>e$t$g&sssx+gv!e zlWu^VOD_I7$z9cE@heqr0DHb#ysBS;T)*stb3%ahXYcoZk>;PWl$*jRdzr80DcQa+ zx2}Ep4>fXQ(>beCv{z&f^MoGWzU)!D{pV22YSHnDPr&2CGo-02r1ud4S$#~Np*O>x zllePKF7v~3r@%cwwQBbCnGbU83nLmU)}@%!96hSPgIduD2iiq6`wf0`RI9S&3BKP@ zP<Q)oY&jsx_uTyW6E7_XmxAzW!rMmmv*KJZONRvST8f1B1WqR(FESkBt5c@WcNN;7 zEGvSq=}R!4SW~1h?TijV(wd><g2XmhxqBgU7{}p(GGAzZK3BC79IQ?pnRXtp39|~F zv9PXxct>5HM|FFy--KbaJZ4X0W8VI@vxb#g1tX`FSQ^bEE~Gnw?;Rh7@fAfs$23t{ zM<uJh2)mE5=HID<Sk(|^@X2E#|BA=`tr1jjNJu7;a$qM)cJ(;|{YF7Q&1<yqim$Wp z5~Fj-Q|vY6R|_tM!wuakSEi2l-@HgV+@XkL@d-EvZTEw0&1e@rsGj%BYPIEeHGVG! zTnj0!u@~5t%Y3YeGcUj~H4DYs%sl$3?dNgVoA6S=hx|p@%OOqV+_Ow%5H|^Wu~>4Z z_|1tMy7FnETeV%rH~SP3C!dLqpT2I@iAtcN2=ixlp-j@OqR-O{k3t0d)<~{&P<3~N z)n9j8%gj2gZr&GbFM0kOtqR(w42xj*_O0NmiwGo0B9E9k7==vK<7#1zi_>tV>Rlsu z+OgTntk%kkfRbLZuN{e_>{vaHetJKy{!16#drgbkf&FCug>}*l&OCUBk(~4I)S*z# zN2A+3Pv_aiu>~yN8=Cq}q-3wRjFQ1pq!jfL2^@r0yV-p%n8{ZRTBYNxyQ-Vxc=Q%j z)<yB!3g-nDL$o_B{tOz8zXJqgTnTbs?yM!6g8RY(^cO4zxu&X?3I(+p@(Ne+g5owh zFdIEIbjo%#pV7{Q-cga2#XSE#;K_EIOdE&mKVfQC*SC^Z)p!<_aHIJB;SvEJDVAZ+ zAV9q`0Oze~$~-aGg!eewfb@H=$cC1Di(m6_E{i@)t*f>AlyKx^#9|(!Lg}{;<;<g^ zoPQx75$@B}1|qy|#2XlO9=S;y!p1)HS!KSj{@C!E`)_yqv!dTYF)vC6?C;QSVF&jO zA2cCLCH$Xu>Z-}^xBf9V=B`uUR^4Zs6`5l#zX^QW5hA@prGvM=>T|g_JN3MBt?~9j z_-z0^UgFBNyO@_x$=&2MlcZ#I6EXQw+|oy{=C6PaVFlu?jNIjnX$})aq%LiS0wwF# z`!qp8(zS_#jb2|Ba#*`W#h&ELjp=(#$0j!W7x7f`xrEt7c1+<0yFlc9!B}~3cH-v@ z+X@$q1XmW4&byT58)-}zl73ECdGE*Fgxb9tBgc@mdaYpRNdB_21^kcV@=|&8Xi(SH z64RuKBK1ZXdWMdJZkpVVQX8kP&_yK=cTrI#m*wy40>fJ_r9%bwD;Arnu@LiF-#fGT z>ZKCw|GbPD`eaCfTA@b;|HR&TYmEp~$Z*L)p7G9*6rC9PzwhHaT2=JD6@~CVv_wmv zbs1U+S-g=@y5nza>DqDY(G(M;)%Pf0jfm#7e?L5$(jpvdEk9z9RXhdn#@+0+{w}Rx zM@_^m-f1tV%|$`xhtuyJ-HH-x(a@1)h$$cX5FqF*c~Y;0?JFVDH<A~?!V=54#4IgR zgh_d}Qx@&dM`^6X5F(ZZ-8dI-A@$?ml)PIF#VU4cap;p8W_wMpl_7-XPiMj0P7SQ( z@9%9PitR?&*SyI2M?~XYUQ}pWW<Ci8W{d~&*Y8~!V`qwT3Z+TaP%&pmO-Na`B_^Ml zsfLZ3wq1P<`$lQ!<eBQgD>BD(3HRzg1=X=$da-3MV3t*MBE{~zL4M7K>H0^ms|C@E zD1H<X?`qMAmGgELyQy5sFW}NxznQhN*;u3dnr)=7RiEC;(i|`3j;fy~vzgH>q5|>P z;RlO}b?w+ayR1_y+Jy{C3Xxzg>*#thyM{FVH}m{3xiQ_rK?`HkmDej<Ze5Kz;-0m6 zyyR0StWKYjRe52%6ZV6rZ`rJ;i#lZ9+B`S)<Ej2x=d0f?gN7$;p<uZ0Ir$iK;82o} zz2mL|JeA+p;Z~t1N|rELqk}E{NA$^Sb=%BtlD4ePG5)mZcTJ4T3*MUP6ggJVMt_&9 zV*Fladr<S#07cPZZt9I7IH4m$k<7k)6RSn4kan)ZGsUrct%B2LGV%8z(z**)zut{G z@aldjAb(qK#@W)pMYeucEUTTk`(m=L+ZzLt8#Sm9U8C47gXtlc(|0?bY}wt_DR?db zN+PIDZ?qB8Q(&W-*0peE``uW759E>^x>3~<=?zrK^f;^Fkhqa)R&mZ2RH<7B+6>gy zyv-S|!xC&0bdWacNGA0h{ja_KyB@6^sLKQs4rz0mcZ56lj0><iK~^<BYyYb0_wWiY z|JBf*Xzc2oFXBJ`YxB$dBHxoQ0q@<dp4l1dFARO%@}F)iIaUJKgBa5L%icKqt;-`5 z7U($m?E$rc8XjtDC4P5_2K3w&9w>IUp)XW14AE>oECVsMU+ugaQpCHOQ$w?6DU{DX zEZ(nR=lwe5V=De*zvj<C32lhN&63!F@ZMBOEei9{G9v(HbYL+-0c9hZ--dd$n{uDe zNBx4o&vk4!NiGYiC3Ca1Z!fL@f0C_wN^n_Vegz9h8rkpJ%Qth*1XYIdWLWx?K7@!H z^RsM%bkF=?HTf-31<W^fg-&}Ln|8}|{cF|=Bnm<u(o*Aq@SY=aH?~Z>d?Wa2^vL$Q zIO)onL9UcxDLUMDf-_NZkFt%)uzL0pKW?uR*wkD~%oX57Sa#I@lT7L_C;w|T%_pMZ z#&_pyuc5v{=*z6~@Tc9W__H>AK^X+u+?>;S35|N-6@4(;kD`1=-+wJGkP8mbjMgP5 zd>Pv+3g70VRp4Yw4l<IT=;}WyVI~CQGLysER2M~!_s_z9+;<JEPB7FkujBToq?uX~ zWJLBm?dE_lA%^4`s<<DVpIK{$Lb19>3yKvgi0|^sDSG+001q*tEO6J(-n;%_Y?gks z!67CKjUyM9#~b~}3?ZzkV1i*BHO(nE$*y*YKjTQi8QxYq+w<hR6Cz%YLWh;D>DtFe z@)%zR>yp!>Y5O`w86N5R)L1d(LS4F`{Cvg@Cf-H7e+vzr)9?jn<6QqD$!4>fTtZV| z-W$kwe+;pMyo;3(cfa=S4pqJF!f_#}igD!|-@_5lk`|+RXSj;wLbbTk;jsQ{INqt* z<+=r{X7Jq2dOuZ6i%}GfJ5GuBMLVa5EgBYOojCnw025J=HMd&jru(L_yXN=kOlP=2 zGq2X2I+;T<zuXigjUOyh*Ha>51X|aJ{WzL&VTmJ2?)i!q0?YY3jbazAFSD1~t7iiG z4rhYdmo_i5<D<w-9m=FFXrtqYzmOnpc5VNhRwm0@@meb5?brFLjJ&UP&-r<E|K5KL zHX$n9)i~38;%%(VnbehcSbAMKx8!<)z((?mv7UM-1h;rG&qI6?0@B)bqS^s(gQRFU z8NYy&u{@rV2eH|$Ma(fCcHWa0pO`5g^Xf{$0N%Q>Zs(L%W>r@R%r_yYWwX7t6<zbV zFs=7Vdo+}cAHV<c&3NqB<5Y>tbd+t}V2HE!4#y%qGjSnLOWP<gXeC)6I$nE}bsH2P z9I_*+H}e+$D&?=mF=d>4@oO~!lUfAnEMCAz%P2*Gp`FYcl3ol>Q9R#I^DXQnl=;_4 zJ7}srx+zz;7gO%=0x<>!?A+-MK^d+rExnkUrn<@a3nTH~2bHZ#nTV=R-HI-uwUVTB z=n48Q?*Fp8oH@_W?mi#CpvBPQBJ#i!&8O?%L#QOY-S%3U!tO=!#Wi{@LZs9zlIWpq z*U*g%@G1%z3jBorGOhkBq?O{KT(Z`!o8q!4O6V@g_CaVMJm?FIbmk~V3L##uAX{ya zc9V(1k~UPZQ=W;%N_UQBV*B5PjQ*o`sr1QnA9JIOVqDUYz&`f#*ZUO>vi{=Q07_E? zdvy~VZTIF+7@V-x-GY%5j~sVBD4`~+3U8Z#p!G@{dpiAW!|ph9`0BK2Xtpdex8>g* zvqn4{dWgDt{)vYY%FLiS*zV!r@-kkaTSZE_4iCLZ6(_((B1F+_@#|tWWW=kZZkuaz z8+7hAy^-d)|24GB$)3D=M53`U_OuW4gP1&EgsWmvlWlB+dK8`uDy+EHNAm9gyXnn) zU-6ayB1g@LeMt>$bf=xv0(ZViUhh3F;EUO~g(cQrg}CrR`o%g>PyT^2k>M_5t+UmS z`JNO9iD@4k&_%}LMs8dNM|<%{81+y4&(zaF^@^$%i>D=LP3>`3rD&>$H4YhrXp(#9 zhQ}=QPfe$Vu{&9;_+%z(Xw*Bg3k4g#+RT%I#@srz!HdMHqu-SYvvHwzpm3CnW{w}* z#>bwOH)-S)F1#j@-IP+7rs4;jSq91=(L?iKum&Tm7?s0f8V#lF5F)I(nsOgp<6d@A zWTVsupitASx3f+8`~xrgi&KC173`0AO5JSIBF0zCdH$=h|GeBrqH%Dm4zIbYY2kF2 zC7#NS+#jgNYD%p>^j#9N5RF#mbI&i`OiJ$i=ITVpPK+au9KuEej>ckGV+GDQbhv-t zs|Wr>vmeGPLk2!GABi-6>a9tu;?O|P%5_Xmel?v4(mYC$`IjlzIbRq^Px>+tE2mGt z5oI-@a6k8Nx8nAjKq!N}$>66Qbz}F3lJ~p5d6bl;3kpGqrcs<7H{-b6FLjL}@%Ead zI)rD54v(3y97SFDdvcWc)Z0<8_{QI*O5-CtirEZUa!jY>p*~S&bX`Ek`Su>oiP_bS zX7-FF<Yq+ECL>=7k}7c(yL7GiGIv*N+#t}7*UhgOhSH0m^f9(5G(3>SBH;#WzEa)P zm+~r8H&OWpuQ2^rt$gA<R|=caNbZ4Aq2x=C<A7lyYoCuDO<=$Lc@u%F^zzd&^xp%a zWx5knM=gYG9JgcN6+(C!Q-3<Ck55J&Wf>aRtdYM38=0MxF+Nm8-w8mo(LJF`8p?;Z z{Sj#B54qx$X#6)p-e_XSnkznw!X`6{@o%Iy+Hb(3-MvQr=2Ge@fG5Pn)zCdl8Hl3) zMVq9}1FtmI%WZLN^nj5QMa}Rb7ZbZ|HiziwAeh^m8__{DQdckQHd=I^J_S*X+ji<> zkVTa}*P>Y@v1$|bJFhdf?eSyGXOjw{0gq`HH!l?Gdr}Pu9aMhE_u@%}kMp6lGhdb6 zjwRhf*)#TVMs?QxTdOUp4;CXmwl!VoIr3c-b)X{19#mE^;cen;V{(ZV%uG6!FQ2bn zn!AJv(=j`mIZB4ho&WNu+Q#kUYi5@R!V75plo=Q<D(4kwfEY;L4!+$aau9BcsP(X7 zaFjnTi-4b{;1Bexmw4G8oaFQ|<SB@ga+0#bHMCq%_M5!s{dc77Wz97%cjn|~hozL6 zJ=A+E{TMR+oGE@!Cu4kv0B8OanMl3rw|z7}{?~}FGcx~4onuG0zSZrwu3^JK4~JHZ z#M>G=p_|DJ@udbC)9ZRVSFu4zpXnl?y<ciFy6%uSM2m40Jw(k&$oj+;bB+BW$~a%M zkXVK1%VP85Q>@IZv=W~v;W_FeR;}$lUd-CYSi$x!vd)w0O=^>H*as}f89xXk%!_Z< z55G3T8u(ipW(Ji8f*f5x>wj{`_uq~-XR@ZXZZjLcuKEz`nb2$|EE`W$V<~dm*GFc8 zC)!%c>-Q7cLm|LwN~e3o`Z&vud*g^qV%8LLh#seyWW8_N0WC|Rk0c10#N_UC#^+L2 zWVzI4R6=5hGUFeoUa*T77`Y>q`I8iy8s0%8>n?9kQA3l<bE5~XH>#*x124+|8qzjM zeIhVzi5k*2X!gIO|1QX}_zgXy?^0dG`XFe1H~b$?@iv{Hrww`Ma&v)yn?85yo9F!4 z;tn~v-xsJx@y2MTHs~*r1r+)@&G@^{spGrWrpXHK{-HN3M`COh2%8hJCxN^yf=@0I zk5fGX{|TNvy;EA^FK$VS*fBUJ+%T3FHIrxHUm6_VdZM>?YekP->C|4Q3vW>qDo;{I zUU))w)1g9YBvnIR;m9y&**C!#<^7!?X$Vh{#zmk{cwz1mZrXJo@3<l){1>KVAM+dK z(7!0u+fD99kJyR&jQ$?XOZiWq(m7{x{r)h&ueNPYhQ>FKEHl(YVn%+@Ig5_!i|CWa zqhPC_zbdHBWsNN_&0KC?sGRhFOs_{9GUl$a%HWIqb%C9<n;Z@CoMKTJ<^GY@vP>qg zPOTb8Yk7mHDuSc*OL_vPvI5G!>8`_R4g&w-KzL~N$YdYZSw*ijRpyB?Gg6`bwUQ?5 zU-+xRh+`5D!5yeT3uq@gfAb!!#P)E+*?e75G493N+tp52#Tis)genVCLE-Zps%C)z z)mwCQW(5c}eFy>c{i~lp^u5_hI&v|5_&P>?mK;Y0&QQ@DMG{owFoYlqR6jqmq<iek zl{-botU(*Z@fADK$#ZC_GvwT}Lw#4SmbVdLfy4~`p!X~mDgA8L`zlUp)_KgJ^`Ze0 z?7ktuxGYnO+{`T@fB&DFva-6wC?s5+;LffcJ^fzUIIV(kT(H8=@OA>02+IV%Iy$== z6u0#^xB~fasMO>uYJpd1=d7!-pS>PS_SMW4mEyfe*LBnmJ;~mEdgt%b5kU;4>JgAz z`x2Z_X{Fh+vL_Mq!9m8r=_jfRfud%+Ql(Omg%e_{mJFW{)~RG;j=?KN8}}4+Tpdgm zN5l5V>n6+^!BaI;5m&W4H#GJ-^5<o*7p4<zy31N*hTZ|pg3mw8d%_ak>-hLr4k9~# z<1Il18u^QlhA4B{XJQ|9xVp!&_g=>;ZBGk?HVUi`GvG|48vC1A5lXGFaz&oWE7#_Q z$%Ndn%I%6dM~JQdLC_ce+@|{|N%bMndRWCj&+t1Iqqc&}9uoeB0)2axOTxbp<Z&hU z`**rFTMZRWvAzig;Xl?jsALB{ylv7=5Nyn#CQmBw939SBSt9JDzEoPKe9<-HtBF<T zyH^d^nicio<EZxKRC1#T*S)BDCAoC^5xXdKkW*m62eqljY=)}Gf*5GNjJWy(hN<hG z0D1cmZKvrA5<lxYLdjx}x`mvEg1=4O=eVAWZqdB##4Zm=>(3EUw3E;?Dc8!JIb}r6 zN|U$};%O1#40REv&O>gW)AwY$)9VhCB)fy^7hU@2PlAly_syQDE27Wa^<CQ8Lxasn z$MmrNJ|(@4)nd1-P_DABL5)#calofa-E}lHK9dudd2ahVedrFumKdfG+eOEMqfS`U z7_DC<y|Pqj3_NDIT2|5si`x#9Nw<oM%WflXAG1kOI~}ifQ$Y%h`oqIU?X5VJ+~>Y< zzkmmg*k~%ITxpb+wJc?Z7b|LapR<0?_YM|jWskmYrpRmDJMXTzio&mxgHyLM=31rT zvTn0yD5pxS4?h1d7v%{ee`s|*(f28AQDtSEdfV#|i!-F(U(uT(hsV<bR4A?c{fv9D z8NISE!|!5W4F8jz<s(j6a*!XH{7Vm)?S)MyFI|R*wCWS{bBi^ZS-YC&<kl5k)4J|6 z@g)*mVP01?mY{|JFMEWdB_IFmkZRcRBV(n-phe^ptA_m#l^ka%_;H79{@mO8T5@Ce z{7H_=CemH`^a}%7(jk$byM8%HCJd(c<N5NRQ?(Mb)Zsqr+lpZP7`$XeN$s92<+?T? zagvD~PKBxCB8LDE5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# z00KY&2mk>f00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e# L00KbZ|0M7~<4-Vn literal 0 HcmV?d00001 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index eeb2def5..8edecb27 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -50,6 +50,7 @@ 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 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder import timber.log.Timber import java.util.UUID import java.util.concurrent.CountDownLatch @@ -346,6 +347,10 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: assertTrue(registrationResult is RegistrationResult.Success) val session = (registrationResult as RegistrationResult.Success).session session.open() + session.filterService().setSyncFilter( + SyncFilterBuilder() + .lazyLoadMembersForStateEvents(true) + ) if (sessionTestParams.withInitialSync) { syncSession(session, 120_000) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt new file mode 100644 index 00000000..e74aa524 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2022 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.database + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.Realm +import org.amshove.kluent.fail +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldNotBe +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.SessionRealmModule +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.util.Normalizer + +@RunWith(AndroidJUnit4::class) +class RealmSessionStoreMigration43Test { + + @get:Rule val configurationFactory = TestRealmConfigurationFactory() + + lateinit var context: Context + var realm: Realm? = null + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + } + + @After + fun tearDown() { + realm?.close() + } + + @Test + fun migrationShouldBeNeeed() { + val realmName = "session_42.realm" + val realmConfiguration = configurationFactory.createConfiguration( + realmName, + "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0", + SessionRealmModule(), + 43, + null + ) + configurationFactory.copyRealmFromAssets(context, realmName, realmName) + + try { + realm = Realm.getInstance(realmConfiguration) + fail("Should need a migration") + } catch (failure: Throwable) { + // nop + } + } + + // Database key for alias `session_db_e00482619b2597069b1f192b86de7da9`: efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0 + // $WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI + // $11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo + @Test + fun testMigration43() { + val realmName = "session_42.realm" + val migration = RealmSessionStoreMigration(Normalizer()) + val realmConfiguration = configurationFactory.createConfiguration( + realmName, + "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0", + SessionRealmModule(), + 43, + migration + ) + configurationFactory.copyRealmFromAssets(context, realmName, realmName) + + realm = Realm.getInstance(realmConfiguration) + + // assert that the edit from 42 are migrated + val editions = EventAnnotationsSummaryEntity + .where(realm!!, "\$WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI") + .findFirst() + ?.editSummary + ?.editions + + editions shouldNotBe null + editions!!.size shouldBe 1 + val firstEdition = editions.first() + firstEdition?.eventId shouldBeEqualTo "\$DvOyA8vJxwGfTaJG3OEJVcL4isShyaVDnprihy38W28" + firstEdition?.isLocalEcho shouldBeEqualTo false + + val editEvent = EventMapper.map(firstEdition!!.event!!) + val body = editEvent.content.toModel<MessageContent>()?.body + body shouldBeEqualTo "* Message 2 with edit" + + // assert that the edit from 42 are migrated + val editionsOfE2E = EventAnnotationsSummaryEntity + .where(realm!!, "\$11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo") + .findFirst() + ?.editSummary + ?.editions + + editionsOfE2E shouldNotBe null + editionsOfE2E!!.size shouldBe 1 + val firstEditionE2E = editionsOfE2E.first() + firstEditionE2E?.eventId shouldBeEqualTo "\$HUwJOQRCJwfPv7XSKvBPcvncjM0oR3q2tGIIIdv9Zts" + firstEditionE2E?.isLocalEcho shouldBeEqualTo false + + val editEventE2E = EventMapper.map(firstEditionE2E!!.event!!) + val body2 = editEventE2E.getClearContent().toModel<MessageContent>()?.body + body2 shouldBeEqualTo "* Message 2, e2e edit" + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt new file mode 100644 index 00000000..fc1a7883 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt @@ -0,0 +1,64 @@ +/* + * 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.database + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.Realm +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.internal.database.model.SessionRealmModule +import org.matrix.android.sdk.internal.util.Normalizer + +@RunWith(AndroidJUnit4::class) +class SessionSanityMigrationTest { + + @get:Rule val configurationFactory = TestRealmConfigurationFactory() + + lateinit var context: Context + var realm: Realm? = null + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + } + + @After + fun tearDown() { + realm?.close() + } + + @Test + fun sessionDatabaseShouldMigrateGracefully() { + val realmName = "session_42.realm" + val migration = RealmSessionStoreMigration(Normalizer()) + val realmConfiguration = configurationFactory.createConfiguration( + realmName, + "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0", + SessionRealmModule(), + migration.schemaVersion, + migration + ) + configurationFactory.copyRealmFromAssets(context, realmName, realmName) + + realm = Realm.getInstance(realmConfiguration) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt new file mode 100644 index 00000000..fc5a0172 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2022 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.database + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmMigration +import org.junit.rules.TemporaryFolder +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.lang.IllegalStateException +import java.util.Collections +import java.util.Locale +import java.util.concurrent.ConcurrentHashMap +import kotlin.Throws + +/** + * Based on https://github.com/realm/realm-java/blob/master/realm/realm-library/src/testUtils/java/io/realm/TestRealmConfigurationFactory.java + */ +class TestRealmConfigurationFactory : TemporaryFolder() { + private val map: Map<RealmConfiguration, Boolean> = ConcurrentHashMap() + private val configurations = Collections.newSetFromMap(map) + @get:Synchronized private var isUnitTestFailed = false + private var testName = "" + private var tempFolder: File? = null + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + setTestName(description) + before() + try { + base.evaluate() + } catch (throwable: Throwable) { + setUnitTestFailed() + throw throwable + } finally { + after() + } + } + } + } + + @Throws(Throwable::class) + override fun before() { + Realm.init(InstrumentationRegistry.getInstrumentation().targetContext) + super.before() + } + + override fun after() { + try { + for (configuration in configurations) { + Realm.deleteRealm(configuration) + } + } catch (e: IllegalStateException) { + // Only throws the exception caused by deleting the opened Realm if the test case itself doesn't throw. + if (!isUnitTestFailed) { + throw e + } + } finally { + // This will delete the temp directory. + super.after() + } + } + + @Throws(IOException::class) + override fun create() { + super.create() + tempFolder = File(super.getRoot(), testName) + check(!(tempFolder!!.exists() && !tempFolder!!.delete())) { "Could not delete folder: " + tempFolder!!.absolutePath } + check(tempFolder!!.mkdir()) { "Could not create folder: " + tempFolder!!.absolutePath } + } + + override fun getRoot(): File { + checkNotNull(tempFolder) { "the temporary folder has not yet been created" } + return tempFolder!! + } + + /** + * To be called in the [.apply]. + */ + protected fun setTestName(description: Description) { + testName = description.displayName + } + + @Synchronized + fun setUnitTestFailed() { + isUnitTestFailed = true + } + + // This builder creates a configuration that is *NOT* managed. + // You have to delete it yourself. + private fun createConfigurationBuilder(): RealmConfiguration.Builder { + return RealmConfiguration.Builder().directory(root) + } + + fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + fun createConfiguration( + name: String, + key: String?, + module: Any, + schemaVersion: Long, + migration: RealmMigration? + ): RealmConfiguration { + val builder = createConfigurationBuilder() + builder + .directory(root) + .name(name) + .apply { + if (key != null) { + encryptionKey(key.decodeHex()) + } + } + .modules(module) + // Allow writes on UI + .allowWritesOnUiThread(true) + .schemaVersion(schemaVersion) + .apply { + migration?.let { migration(it) } + } + val configuration = builder.build() + configurations.add(configuration) + return configuration + } + + // Copies a Realm file from assets to temp dir + @Throws(IOException::class) + fun copyRealmFromAssets(context: Context, realmPath: String, newName: String) { + val config = RealmConfiguration.Builder() + .directory(root) + .name(newName) + .build() + copyRealmFromAssets(context, realmPath, config) + } + + @Throws(IOException::class) + fun copyRealmFromAssets(context: Context, realmPath: String, config: RealmConfiguration) { + check(!File(config.path).exists()) { String.format(Locale.ENGLISH, "%s exists!", config.path) } + val outFile = File(config.realmDirectory, config.realmFileName) + copyFileFromAssets(context, realmPath, outFile) + } + + @Throws(IOException::class) + fun copyFileFromAssets(context: Context, assetPath: String?, outFile: File?) { + var stream: InputStream? = null + var os: FileOutputStream? = null + try { + stream = context.assets.open(assetPath!!) + os = FileOutputStream(outFile) + val buf = ByteArray(1024) + var bytesRead: Int + while (stream.read(buf).also { bytesRead = it } > -1) { + os.write(buf, 0, bytesRead) + } + } finally { + if (stream != null) { + try { + stream.close() + } catch (ignore: IOException) { + } + } + if (os != null) { + try { + os.close() + } catch (ignore: IOException) { + } + } + } + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt index a37d2ce0..a52e3cd7 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt @@ -66,7 +66,7 @@ class PollAggregationTest : InstrumentedTest { val aliceEventsListener = object : Timeline.Listener { override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { - snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent -> + snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START.values }?.let { pollEvent -> val pollEventId = pollEvent.eventId val pollContent = pollEvent.root.content?.toModel<MessagePollContent>() val pollSummary = pollEvent.annotations?.pollResponseSummary diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt index 239f7499..5b2ab774 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt @@ -38,5 +38,4 @@ data class AggregatedAnnotation( override val limited: Boolean? = false, override val count: Int? = 0, val chunk: List<RelationChunkInfo>? = null - ) : UnsignedRelationInfo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt index ae8ed394..6577a9b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt @@ -19,7 +19,8 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass /** - * <code> + * Server side relation aggregation. + * ``` * { * "m.annotation": { * "chunk": [ @@ -43,12 +44,13 @@ import com.squareup.moshi.JsonClass * "count": 1 * } * } - * </code> + * ``` */ @JsonClass(generateAdapter = true) data class AggregatedRelations( @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null, + @Json(name = "m.replace") val replaces: AggregatedReplace? = null, @Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt new file mode 100644 index 00000000..2ae091a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Note that there can be multiple events with an m.replace relationship to a given event (for example, if an event is edited multiple times). + * These should be aggregated by the homeserver. + * https://spec.matrix.org/v1.4/client-server-api/#server-side-aggregation-of-mreplace-relationships + * + */ +@JsonClass(generateAdapter = true) +data class AggregatedReplace( + @Json(name = "event_id") val eventId: String? = null, + @Json(name = "origin_server_ts") val originServerTs: Long? = null, + @Json(name = "sender") val senderId: String? = null, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 6ae585a2..40ce6ecb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -26,13 +26,12 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent -import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent -import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.session.room.model.relation.isReply import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.threads.ThreadDetails @@ -228,11 +227,14 @@ data class Event( return when { isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text) isFileMessage() -> "sent a file." + isVoiceMessage() -> "sent a voice message." isAudioMessage() -> "sent an audio file." isImageMessage() -> "sent an image." isVideoMessage() -> "sent a video." - isSticker() -> "sent a sticker" + isSticker() -> "sent a sticker." isPoll() -> getPollQuestion() ?: "created a poll." + isLiveLocation() -> "Live location." + isLocationMessage() -> "has shared their location." else -> text } } @@ -386,24 +388,18 @@ fun Event.isLocationMessage(): Boolean { } } -fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END +fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START.values || getClearType() in EventType.POLL_END.values fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER -fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO +fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO.values fun Event.getRelationContent(): RelationDefaultContent? { return if (isEncrypted()) { content.toModel<EncryptedEventContent>()?.relatesTo } else { - content.toModel<MessageContent>()?.relatesTo ?: run { - // Special cases when there is only a local msgtype for some event types - when (getClearType()) { - EventType.STICKER -> getClearContent().toModel<MessageStickerContent>()?.relatesTo - in EventType.BEACON_LOCATION_DATA -> getClearContent().toModel<MessageBeaconLocationDataContent>()?.relatesTo - else -> getClearContent()?.get("m.relates_to")?.toContent().toModel() - } - } + content.toModel<MessageContent>()?.relatesTo + ?: getClearContent()?.get("m.relates_to")?.toContent().toModel() // Special cases when there is only a local msgtype for some event types } } @@ -420,7 +416,7 @@ fun Event.getRelationContentForType(type: String): RelationDefaultContent? = getRelationContent()?.takeIf { it.type == type } fun Event.isReply(): Boolean { - return getRelationContent()?.inReplyTo?.eventId != null + return getRelationContent().isReply() } fun Event.isReplyRenderedInThread(): Boolean { @@ -443,11 +439,11 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER && content?.toModel<RoomMemberContent>()?.membership == Membership.INVITE fun Event.getPollContent(): MessagePollContent? { - return content.toModel<MessagePollContent>() + return getClearContent().toModel<MessagePollContent>() } fun Event.supportsNotification() = - this.getClearType() in EventType.MESSAGE + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + this.getClearType() in EventType.MESSAGE + EventType.POLL_START.values + EventType.STATE_ROOM_BEACON_INFO.values fun Event.isContentReportable() = - this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO + this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt new file mode 100644 index 00000000..32d5ebed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2022 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.api.session.events.model + +fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? { + if (!this.isEncrypted()) return null + val decryptedContent = this.getDecryptedContent() ?: return null + val eventId = this.eventId ?: return null + val roomId = this.roomId ?: return null + val type = this.getDecryptedType() ?: return null + val senderKey = this.getSenderKey() ?: return null + val algorithm = this.content?.get("algorithm") as? String ?: return null + + // copy the relation as it's in clear in the encrypted content + val updatedContent = this.content.get("m.relates_to")?.let { + decryptedContent.toMutableMap().apply { + put("m.relates_to", it) + } + } ?: decryptedContent + return ValidDecryptedEvent( + type = type, + eventId = eventId, + clearContent = updatedContent, + prevContent = this.prevContent, + originServerTs = this.originServerTs ?: 0, + cryptoSenderKey = senderKey, + roomId = roomId, + unsignedData = this.unsignedData, + redacts = this.redacts, + algorithm = algorithm + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 3ad4f3a8..e5c14afa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -49,11 +49,10 @@ object EventType { const val STATE_ROOM_JOIN_RULES = "m.room.join_rules" const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access" const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" - val STATE_ROOM_BEACON_INFO = listOf("org.matrix.msc3672.beacon_info", "m.beacon_info") - val BEACON_LOCATION_DATA = listOf("org.matrix.msc3672.beacon", "m.beacon") + val STATE_ROOM_BEACON_INFO = StableUnstableId(stable = "m.beacon_info", unstable = "org.matrix.msc3672.beacon_info") + val BEACON_LOCATION_DATA = StableUnstableId(stable = "m.beacon", unstable = "org.matrix.msc3672.beacon") const val STATE_SPACE_CHILD = "m.space.child" - const val STATE_SPACE_PARENT = "m.space.parent" /** @@ -81,8 +80,7 @@ object EventType { const val CALL_NEGOTIATE = "m.call.negotiate" const val CALL_REJECT = "m.call.reject" const val CALL_HANGUP = "m.call.hangup" - const val CALL_ASSERTED_IDENTITY = "m.call.asserted_identity" - const val CALL_ASSERTED_IDENTITY_PREFIX = "org.matrix.call.asserted_identity" + val CALL_ASSERTED_IDENTITY = StableUnstableId(stable = "m.call.asserted_identity", unstable = "org.matrix.call.asserted_identity") // This type is not processed by the client, just sent to the server const val CALL_REPLACES = "m.call.replaces" @@ -90,10 +88,7 @@ object EventType { // Key share events const val ROOM_KEY_REQUEST = "m.room_key_request" const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" - val ROOM_KEY_WITHHELD = StableUnstableId( - stable = "m.room_key.withheld", - unstable = "org.matrix.room_key.withheld" - ) + val ROOM_KEY_WITHHELD = StableUnstableId(stable = "m.room_key.withheld", unstable = "org.matrix.room_key.withheld") const val REQUEST_SECRET = "m.secret.request" const val SEND_SECRET = "m.secret.send" @@ -111,9 +106,9 @@ object EventType { const val REACTION = "m.reaction" // Poll - val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start") - val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response") - val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end") + val POLL_START = StableUnstableId(stable = "m.poll.start", unstable = "org.matrix.msc3381.poll.start") + val POLL_RESPONSE = StableUnstableId(stable = "m.poll.response", unstable = "org.matrix.msc3381.poll.response") + val POLL_END = StableUnstableId(stable = "m.poll.end", unstable = "org.matrix.msc3381.poll.end") // Unwedging internal const val DUMMY = "m.dummy" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt new file mode 100644 index 00000000..b305bf19 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 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.api.session.events.model + +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent + +data class ValidDecryptedEvent( + val type: String, + val eventId: String, + val clearContent: Content, + val prevContent: Content? = null, + val originServerTs: Long, + val cryptoSenderKey: String, + val roomId: String, + val unsignedData: UnsignedData? = null, + val redacts: String? = null, + val algorithm: String, +) + +fun ValidDecryptedEvent.getRelationContent(): RelationDefaultContent? { + return clearContent.toModel<MessageRelationContent?>()?.relatesTo +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index cd8acbcc..93208be2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -47,10 +47,9 @@ interface LocationSharingService { /** * Starts sharing live location in the room. * @param timeoutMillis timeout of the live in milliseconds - * @param description description of the live for text fallback * @return the result of the update of the live */ - suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult + suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult /** * Stops sharing live location in the room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt index 67bab626..7d445a5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt @@ -15,10 +15,10 @@ */ package org.matrix.android.sdk.api.session.room.model -import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event data class EditAggregatedSummary( - val latestContent: Content? = null, + val latestEdit: Event? = null, // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) val sourceEvents: List<String>, val localEchos: List<String>, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt index 56397302..da7e4ea9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt @@ -18,5 +18,6 @@ package org.matrix.android.sdk.api.session.room.model data class ReadReceipt( val roomMember: RoomMemberSummary, - val originServerTs: Long + val originServerTs: Long, + val threadId: String? ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt index f8c1c0d7..627ce53d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt @@ -34,7 +34,7 @@ data class MessageStickerContent( * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image, * or some kind of content description for accessibility e.g. 'image attachment'. */ - @Json(name = "body") override val body: String, + @Json(name = "body") override val body: String = "", /** * Metadata about the image referred to in url. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt index 5dcb1b43..b9f9335d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt @@ -28,3 +28,5 @@ data class RelationDefaultContent( ) : RelationContent fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false + +fun RelationDefaultContent?.isReply(): Boolean = this?.inReplyTo?.eventId != null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt index dac1a1a7..83680ec2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt @@ -34,12 +34,14 @@ interface ReadService { /** * Force the read marker to be set on the latest event. */ - suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH) + suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, mainTimeLineOnly: Boolean = true) /** * Set the read receipt on the event with provided eventId. + * @param eventId the id of the event where read receipt will be set + * @param threadId the id of the thread in which read receipt will be set. For main thread use [ReadService.THREAD_ID_MAIN] constant */ - suspend fun setReadReceipt(eventId: String) + suspend fun setReadReceipt(eventId: String, threadId: String) /** * Set the read marker on the event with provided eventId. @@ -59,10 +61,10 @@ interface ReadService { /** * Returns a live read receipt id for the room. */ - fun getMyReadReceiptLive(): LiveData<Optional<String>> + fun getMyReadReceiptLive(threadId: String?): LiveData<Optional<String>> /** - * Get the eventId where the read receipt for the provided user is. + * Get the eventId from the main timeline where the read receipt for the provided user is. * @param userId the id of the user to look for * * @return the eventId where the read receipt for the provided user is attached, or null if not found @@ -74,4 +76,8 @@ interface ReadService { * @param eventId the event */ fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> + + companion object { + const val THREAD_ID_MAIN = "main" + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt index 8f214e0f..634e71c4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt @@ -33,5 +33,7 @@ object RoomSummaryConstants { EventType.ENCRYPTED, EventType.STICKER, EventType.REACTION - ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + ) + + EventType.POLL_START.values + + EventType.STATE_ROOM_BEACON_INFO.values } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 6f4049de..9053425a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.timeline import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.extensions.orFalse +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.api.session.events.model.RelationType @@ -142,13 +143,21 @@ fun TimelineEvent.getEditedEventId(): String? { fun TimelineEvent.getLastMessageContent(): MessageContent? { return when (root.getClearType()) { EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>() - in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>() - in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconInfoContent>() - in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>() - else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() + // XXX + // Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing + // so toModel<MessageContent> won't parse them correctly + // It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion? + in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>() + in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconInfoContent>() + in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>() + else -> (getLastEditNewContent() ?: root.getClearContent()).toModel() } } +fun TimelineEvent.getLastEditNewContent(): Content? { + return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessageContent>()?.newContent +} + /** * Returns true if it's a reply. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt index bc592df4..7347bee1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt @@ -16,19 +16,12 @@ package org.matrix.android.sdk.api.session.sync -interface FilterService { - - enum class FilterPreset { - NoFilter, +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder - /** - * Filter for Element, will include only known event type. - */ - ElementFilter - } +interface FilterService { /** * Configure the filter for the sync. */ - fun setFilter(filterPreset: FilterPreset) + suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt new file mode 100644 index 00000000..ad55b26d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2022 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.api.session.sync.filter + +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.internal.session.filter.Filter +import org.matrix.android.sdk.internal.session.filter.RoomEventFilter +import org.matrix.android.sdk.internal.session.filter.RoomFilter +import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams + +class SyncFilterBuilder { + private var lazyLoadMembersForStateEvents: Boolean? = null + private var lazyLoadMembersForMessageEvents: Boolean? = null + private var useThreadNotifications: Boolean? = null + private var listOfSupportedEventTypes: List<String>? = null + private var listOfSupportedStateEventTypes: List<String>? = null + + fun lazyLoadMembersForStateEvents(lazyLoadMembersForStateEvents: Boolean) = apply { this.lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents } + + fun lazyLoadMembersForMessageEvents(lazyLoadMembersForMessageEvents: Boolean) = + apply { this.lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents } + + fun useThreadNotifications(useThreadNotifications: Boolean) = + apply { this.useThreadNotifications = useThreadNotifications } + + fun listOfSupportedStateEventTypes(listOfSupportedStateEventTypes: List<String>) = + apply { this.listOfSupportedStateEventTypes = listOfSupportedStateEventTypes } + + fun listOfSupportedTimelineEventTypes(listOfSupportedEventTypes: List<String>) = + apply { this.listOfSupportedEventTypes = listOfSupportedEventTypes } + + internal fun with(currentFilterParams: SyncFilterParams?) = + apply { + currentFilterParams?.let { + useThreadNotifications = currentFilterParams.useThreadNotifications + lazyLoadMembersForMessageEvents = currentFilterParams.lazyLoadMembersForMessageEvents + lazyLoadMembersForStateEvents = currentFilterParams.lazyLoadMembersForStateEvents + listOfSupportedEventTypes = currentFilterParams.listOfSupportedEventTypes?.toList() + listOfSupportedStateEventTypes = currentFilterParams.listOfSupportedStateEventTypes?.toList() + } + } + + internal fun extractParams(): SyncFilterParams { + return SyncFilterParams( + useThreadNotifications = useThreadNotifications, + lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents, + lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents, + listOfSupportedEventTypes = listOfSupportedEventTypes, + listOfSupportedStateEventTypes = listOfSupportedStateEventTypes, + ) + } + + internal fun build(homeServerCapabilities: HomeServerCapabilities): Filter { + return Filter( + room = buildRoomFilter(homeServerCapabilities) + ) + } + + private fun buildRoomFilter(homeServerCapabilities: HomeServerCapabilities): RoomFilter { + return RoomFilter( + timeline = buildTimelineFilter(homeServerCapabilities), + state = buildStateFilter() + ) + } + + private fun buildTimelineFilter(homeServerCapabilities: HomeServerCapabilities): RoomEventFilter? { + val resolvedUseThreadNotifications = if (homeServerCapabilities.canUseThreadReadReceiptsAndNotifications) { + useThreadNotifications + } else { + null + } + return RoomEventFilter( + enableUnreadThreadNotifications = resolvedUseThreadNotifications, + lazyLoadMembers = lazyLoadMembersForMessageEvents + ).orNullIfEmpty() + } + + private fun buildStateFilter(): RoomEventFilter? = + RoomEventFilter( + lazyLoadMembers = lazyLoadMembersForStateEvents, + types = listOfSupportedStateEventTypes + ).orNullIfEmpty() + + private fun RoomEventFilter.orNullIfEmpty(): RoomEventFilter? { + return if (hasData()) { + this + } else { + null + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SyncFilterBuilder + + if (lazyLoadMembersForStateEvents != other.lazyLoadMembersForStateEvents) return false + if (lazyLoadMembersForMessageEvents != other.lazyLoadMembersForMessageEvents) return false + if (useThreadNotifications != other.useThreadNotifications) return false + if (listOfSupportedEventTypes != other.listOfSupportedEventTypes) return false + if (listOfSupportedStateEventTypes != other.listOfSupportedStateEventTypes) return false + + return true + } + + override fun hashCode(): Int { + var result = lazyLoadMembersForStateEvents?.hashCode() ?: 0 + result = 31 * result + (lazyLoadMembersForMessageEvents?.hashCode() ?: 0) + result = 31 * result + (useThreadNotifications?.hashCode() ?: 0) + result = 31 * result + (listOfSupportedEventTypes?.hashCode() ?: 0) + result = 31 * result + (listOfSupportedStateEventTypes?.hashCode() ?: 0) + return result + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt index 3218b999..0f29404d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt @@ -83,9 +83,7 @@ internal class CrossSigningOlm @Inject constructor( val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me ?.get("ed25519:$pubKey") - if (signaturesMadeByMyKey.isNullOrBlank()) { - throw IllegalArgumentException("Not signed with my key $type") - } + require(signaturesMadeByMyKey.orEmpty().isNotBlank()) { "Not signed with my key $type" } // Check that Alice USK signature of Bob MSK is valid olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable)) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index f93da745..5d2797a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -47,9 +47,8 @@ internal class DefaultEncryptEventTask @Inject constructor( // 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 || localEvent.type == null) { - throw IllegalArgumentException() - } + require(localEvent.eventId != null) + require(localEvent.type != null) localEchoRepository.updateSendState(localEvent.eventId, localEvent.roomId, SendState.ENCRYPTING) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index 1a04ee03..5b400aa6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -1140,28 +1140,25 @@ internal class DefaultVerificationService @Inject constructor( override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? { val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId) // should check if already one (and cancel it) - if (method == VerificationMethod.SAS) { - val tx = DefaultOutgoingSASDefaultVerificationTransaction( - setDeviceVerificationAction, - userId, - deviceId, - cryptoStore, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - txID, - otherUserId, - otherDeviceId - ) - tx.transport = verificationTransportToDeviceFactory.createTransport(tx) - addTransaction(tx) + require(method == VerificationMethod.SAS) { "Unknown verification method" } + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingKeyRequestManager, + secretShareManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + txID, + otherUserId, + otherDeviceId + ) + tx.transport = verificationTransportToDeviceFactory.createTransport(tx) + addTransaction(tx) - tx.start() - return txID - } else { - throw IllegalArgumentException("Unknown verification method") - } + tx.start() + return txID } override fun requestKeyVerificationInDMs( @@ -1343,28 +1340,25 @@ internal class DefaultVerificationService @Inject constructor( otherUserId: String, otherDeviceId: String ): String { - if (method == VerificationMethod.SAS) { - val tx = DefaultOutgoingSASDefaultVerificationTransaction( - setDeviceVerificationAction, - userId, - deviceId, - cryptoStore, - crossSigningService, - outgoingKeyRequestManager, - secretShareManager, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - transactionId, - otherUserId, - otherDeviceId - ) - tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) - addTransaction(tx) + require(method == VerificationMethod.SAS) { "Unknown verification method" } + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + outgoingKeyRequestManager, + secretShareManager, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + transactionId, + otherUserId, + otherDeviceId + ) + tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) + addTransaction(tx) - tx.start() - return transactionId - } else { - throw IllegalArgumentException("Unknown verification method") - } + tx.start() + return transactionId } override fun readyPendingVerificationInDMs( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt index 7d263f19..a1ea88a7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt @@ -26,17 +26,17 @@ import kotlinx.coroutines.withContext import timber.log.Timber import kotlin.system.measureTimeMillis -internal fun <T> CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) { +internal fun <T> CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: (realm: Realm) -> T) { asyncTransaction(monarchy.realmConfiguration, transaction) } -internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) { +internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: (realm: Realm) -> T) { launch { awaitTransaction(realmConfiguration, transaction) } } -internal suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T { +internal suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: (realm: Realm) -> T): T { return withContext(Realm.WRITE_EXECUTOR.asCoroutineDispatcher()) { Realm.getInstance(config).use { bgRealm -> bgRealm.beginTransaction() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 30836c02..2fb87ca8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -59,6 +59,9 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -67,7 +70,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 42L, + schemaVersion = 45L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -119,5 +122,8 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 40) MigrateSessionTo040(realm).perform() if (oldVersion < 41) MigrateSessionTo041(realm).perform() if (oldVersion < 42) MigrateSessionTo042(realm).perform() + if (oldVersion < 43) MigrateSessionTo043(realm).perform() + if (oldVersion < 44) MigrateSessionTo044(realm).perform() + if (oldVersion < 45) MigrateSessionTo045(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 221abe0d..43f84e77 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -83,7 +83,6 @@ internal fun ChunkEntity.addTimelineEvent( this.eventId = eventId this.roomId = roomId this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() - ?.also { it.cleanUp(eventEntity.sender) } this.readReceipts = readReceiptsSummaryEntity this.displayIndex = displayIndex this.ownedByThreadChunk = ownedByThreadChunk @@ -133,7 +132,7 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE val originServerTs = eventEntity.originServerTs if (originServerTs != null) { val timestampOfEvent = originServerTs.toDouble() - val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId) + val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = eventEntity.rootThreadEventId) // If the synced RR is older, update if (timestampOfEvent > readReceiptOfSender.originServerTs) { val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index dfac7f67..7999a2ea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -65,11 +65,11 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded( inThreadMessages = inThreadMessages, latestMessageTimelineEventEntity = latestEventInThread ) - } - } - if (shouldUpdateNotifications) { - updateNotificationsNew(roomId, realm, currentUserId) + if (shouldUpdateNotifications) { + updateThreadNotifications(roomId, realm, currentUserId, rootThreadEventId) + } + } } } @@ -273,8 +273,8 @@ internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm, /** * Find the read receipt for the current user. */ -internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? = - ReadReceiptEntity.where(realm, roomId = roomId, userId = userId) +internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): String? = + ReadReceiptEntity.where(realm, roomId = roomId, userId = userId, threadId = threadId) .findFirst() ?.eventId @@ -293,28 +293,29 @@ internal fun isUserMentioned(currentUserId: String, timelineEventEntity: Timelin * Important: It will work only with the latest chunk, while read marker will be changed * immediately so we should not display wrong notifications */ -internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) { - val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return +internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUserId: String, rootThreadEventId: String) { + val readReceipt = findMyReadReceipt(realm, roomId, currentUserId, threadId = rootThreadEventId) ?: return val readReceiptChunk = ChunkEntity .findIncludingEvent(realm, readReceipt) ?: return - val readReceiptChunkTimelineEvents = readReceiptChunk + val readReceiptChunkThreadEvents = readReceiptChunk .timelineEvents .where() .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) .findAll() ?: return - val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt } + val readReceiptChunkPosition = readReceiptChunkThreadEvents.indexOfFirst { it.eventId == readReceipt } if (readReceiptChunkPosition == -1) return - if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) { + if (readReceiptChunkPosition < readReceiptChunkThreadEvents.lastIndex) { // If the read receipt is found inside the chunk - val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents - .slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex) + val threadEventsAfterReadReceipt = readReceiptChunkThreadEvents + .slice(readReceiptChunkPosition..readReceiptChunkThreadEvents.lastIndex) .filter { it.root?.isThread() == true } // In order for the below code to work for old events, we should save the previous read receipt @@ -343,26 +344,21 @@ internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: it.root?.rootThreadEventId } - // Find the root events in the new thread events - val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId } - - // Update root thread events only if the user have participated in - rootThreads.forEach { eventId -> - val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread( - realm = realm, - roomId = roomId, - rootThreadEventId = eventId, - senderId = currentUserId - ) - val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst() - - if (isUserParticipating) { - rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE - } + // Update root thread event only if the user have participated in + val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread( + realm = realm, + roomId = roomId, + rootThreadEventId = rootThreadEventId, + senderId = currentUserId + ) + val rootThreadEventEntity = EventEntity.where(realm, rootThreadEventId).findFirst() + + if (isUserParticipating) { + rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE + } - if (userMentionsList.contains(eventId)) { - rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE - } + if (userMentionsList.contains(rootThreadEventId)) { + rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 193710f9..0ac8dc79 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.database.helper import io.realm.Realm import io.realm.RealmQuery import io.realm.Sort -import io.realm.kotlin.createObject import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -103,32 +102,6 @@ internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent( } } -private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap<String, RoomMemberContent?>): TimelineEventEntity { - val roomId = roomId - val eventId = eventId - val localId = TimelineEventEntity.nextId(realm) - val senderId = sender ?: "" - - val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply { - this.localId = localId - this.root = this@toTimelineEventEntity - this.eventId = eventId - this.roomId = roomId - this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() - ?.also { it.cleanUp(sender) } - this.ownedByThreadChunk = true // To skip it from the original event flow - val roomMemberContent = roomMemberContentsByUser[senderId] - this.senderAvatar = roomMemberContent?.avatarUrl - this.senderName = roomMemberContent?.displayName - isUniqueDisplayName = if (roomMemberContent?.displayName != null) { - computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser) - } else { - true - } - } - return timelineEventEntity -} - internal fun ThreadSummaryEntity.Companion.createOrUpdate( threadSummaryType: ThreadSummaryUpdateType, realm: Realm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt new file mode 100644 index 00000000..8c209f2f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt @@ -0,0 +1,45 @@ +/* + * 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.database.mapper + +import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EditionOfEvent + +internal object EditAggregatedSummaryEntityMapper { + + fun map(summary: EditAggregatedSummaryEntity?): EditAggregatedSummary? { + summary ?: return null + /** + * The most recent event is determined by comparing origin_server_ts; + * if two or more replacement events have identical origin_server_ts, + * the event with the lexicographically largest event_id is treated as more recent. + */ + val latestEdition = summary.editions.sortedWith(compareBy<EditionOfEvent> { it.timestamp }.thenBy { it.eventId }) + .lastOrNull() ?: return null + val editEvent = latestEdition.event + + return EditAggregatedSummary( + latestEdit = editEvent?.asDomain(), + sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } + .map { editionOfEvent -> editionOfEvent.eventId }, + localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } + .map { editionOfEvent -> editionOfEvent.eventId }, + lastEditTs = latestEdition.timestamp + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt index 6bbeb17f..d4bb5791 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.database.mapper -import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary 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.ReferencesAggregatedSummary @@ -35,18 +34,7 @@ internal object EventAnnotationsSummaryMapper { it.sourceLocalEcho.toList() ) }, - editSummary = annotationsSummary.editSummary - ?.let { - val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null - EditAggregatedSummary( - latestContent = ContentMapper.map(latestEdition.content), - sourceEvents = it.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho } - .map { editionOfEvent -> editionOfEvent.eventId }, - localEchos = it.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho } - .map { editionOfEvent -> editionOfEvent.eventId }, - lastEditTs = latestEdition.timestamp - ) - }, + editSummary = EditAggregatedSummaryEntityMapper.map(annotationsSummary.editSummary), referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let { ReferencesAggregatedSummary( ContentMapper.map(it.content), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt new file mode 100644 index 00000000..645cb41a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2022 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.database.mapper + +import io.realm.RealmList +import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity +import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams +import javax.inject.Inject + +internal class FilterParamsMapper @Inject constructor() { + + fun map(entity: SyncFilterParamsEntity): SyncFilterParams { + val eventTypes = if (entity.listOfSupportedEventTypesHasBeenSet) { + entity.listOfSupportedEventTypes?.toList() + } else { + null + } + val stateEventTypes = if (entity.listOfSupportedStateEventTypesHasBeenSet) { + entity.listOfSupportedStateEventTypes?.toList() + } else { + null + } + return SyncFilterParams( + useThreadNotifications = entity.useThreadNotifications, + lazyLoadMembersForMessageEvents = entity.lazyLoadMembersForMessageEvents, + lazyLoadMembersForStateEvents = entity.lazyLoadMembersForStateEvents, + listOfSupportedEventTypes = eventTypes, + listOfSupportedStateEventTypes = stateEventTypes, + ) + } + + fun map(params: SyncFilterParams): SyncFilterParamsEntity { + return SyncFilterParamsEntity( + useThreadNotifications = params.useThreadNotifications, + lazyLoadMembersForMessageEvents = params.lazyLoadMembersForMessageEvents, + lazyLoadMembersForStateEvents = params.lazyLoadMembersForStateEvents, + listOfSupportedEventTypes = params.listOfSupportedEventTypes.toRealmList(), + listOfSupportedEventTypesHasBeenSet = params.listOfSupportedEventTypes != null, + listOfSupportedStateEventTypes = params.listOfSupportedStateEventTypes.toRealmList(), + listOfSupportedStateEventTypesHasBeenSet = params.listOfSupportedStateEventTypes != null, + ) + } + + private fun List<String>?.toRealmList(): RealmList<String>? { + return this?.toTypedArray()?.let { RealmList(*it) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt index 2be4510b..3b71ae3d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt @@ -50,7 +50,7 @@ internal class ReadReceiptsSummaryMapper @Inject constructor( .mapNotNull { val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst() ?: return@mapNotNull null - ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong()) + ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong(), it.threadId) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt index b61bf7e6..f85a0661 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt @@ -25,11 +25,11 @@ internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8 override fun doMigrate(realm: DynamicRealm) { val editionOfEventSchema = realm.schema.create("EditionOfEvent") - .addField(EditionOfEventFields.CONTENT, String::class.java) + .addField("content", String::class.java) .addField(EditionOfEventFields.EVENT_ID, String::class.java) .setRequired(EditionOfEventFields.EVENT_ID, true) - .addField(EditionOfEventFields.SENDER_ID, String::class.java) - .setRequired(EditionOfEventFields.SENDER_ID, true) + .addField("senderId", String::class.java) + .setRequired("senderId", true) .addField(EditionOfEventFields.TIMESTAMP, Long::class.java) .addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt new file mode 100644 index 00000000..49e9bac1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 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.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.EditionOfEventFields +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 43) { + + override fun doMigrate(realm: DynamicRealm) { + // content(string) & senderId(string) have been removed and replaced by a link to the actual event + realm.schema.get("EditionOfEvent") + ?.addRealmObjectField(EditionOfEventFields.EVENT.`$`, realm.schema.get("EventEntity")!!) + ?.transform { dynamicObject -> + realm.where("EventEntity") + .equalTo(EventEntityFields.EVENT_ID, dynamicObject.getString(EditionOfEventFields.EVENT_ID)) + .equalTo(EventEntityFields.SENDER, dynamicObject.getString("senderId")) + .findFirst() + .let { + dynamicObject.setObject(EditionOfEventFields.EVENT.`$`, it) + } + } + ?.removeField("senderId") + ?.removeField("content") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt new file mode 100644 index 00000000..2d3efc83 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 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.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo044(realm: DynamicRealm) : RealmMigrator(realm, 44) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("ReadReceiptEntity") + ?.addField(ReadReceiptEntityFields.THREAD_ID, String::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt new file mode 100644 index 00000000..d2b43ded --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 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.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo045(realm: DynamicRealm) : RealmMigrator(realm, 45) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.create("SyncFilterParamsEntity") + .addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, Boolean::class.java) + .setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, true) + .addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, Boolean::class.java) + .setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, true) + .addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java) + .addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java) + .addField(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, Boolean::class.java) + .setNullable(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, true) + .addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES.`$`, String::class.java) + .addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES.`$`, String::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt index 61acd51d..7b7b90f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt @@ -32,9 +32,8 @@ internal open class EditAggregatedSummaryEntity( @RealmClass(embedded = true) internal open class EditionOfEvent( - var senderId: String = "", var eventId: String = "", - var content: String? = null, var timestamp: Long = 0, - var isLocalEcho: Boolean = false + var isLocalEcho: Boolean = false, + var event: EventEntity? = null, ) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt index 645998d0..9a201ab4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -19,7 +19,6 @@ import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity -import timber.log.Timber internal open class EventAnnotationsSummaryEntity( @PrimaryKey @@ -32,21 +31,6 @@ internal open class EventAnnotationsSummaryEntity( var liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummaryEntity? = null, ) : RealmObject() { - /** - * Cleanup undesired editions, done by users different from the originalEventSender. - */ - fun cleanUp(originalEventSenderId: String?) { - originalEventSenderId ?: return - - editSummary?.editions?.filter { - it.senderId != originalEventSenderId - } - ?.forEach { - Timber.w("Deleting an edition from ${it.senderId} of event sent by $originalEventSenderId") - it.deleteFromRealm() - } - } - companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt index 9623c953..cedd5e74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt @@ -26,6 +26,7 @@ internal open class ReadReceiptEntity( var eventId: String = "", var roomId: String = "", var userId: String = "", + var threadId: String? = null, var originServerTs: Double = 0.0 ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index b222bcb7..93ff67a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -70,7 +70,8 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit SpaceChildSummaryEntity::class, SpaceParentSummaryEntity::class, UserPresenceEntity::class, - ThreadSummaryEntity::class + ThreadSummaryEntity::class, + SyncFilterParamsEntity::class, ] ) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt new file mode 100644 index 00000000..e4b62f28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2022 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.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * This entity stores Sync Filter configuration data, provided by the client. + */ +internal open class SyncFilterParamsEntity( + var lazyLoadMembersForStateEvents: Boolean? = null, + var lazyLoadMembersForMessageEvents: Boolean? = null, + var useThreadNotifications: Boolean? = null, + var listOfSupportedEventTypes: RealmList<String>? = null, + var listOfSupportedEventTypesHasBeenSet: Boolean = false, + var listOfSupportedStateEventTypes: RealmList<String>? = null, + var listOfSupportedStateEventTypesHasBeenSet: Boolean = false, +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt index c8f22dc2..1deca47b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt @@ -20,6 +20,7 @@ import io.realm.RealmObject import io.realm.RealmResults import io.realm.annotations.Index import io.realm.annotations.LinkingObjects +import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.internal.extensions.assertIsManaged internal open class TimelineEventEntity( @@ -52,3 +53,7 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) { } deleteFromRealm() } + +internal fun TimelineEventEntity.getThreadId(): String { + return root?.rootThreadEventId ?: ReadService.THREAD_ID_MAIN +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index 0b0f01a6..ebfe2310 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -18,17 +18,20 @@ package org.matrix.android.sdk.internal.database.query import io.realm.Realm import io.realm.RealmConfiguration import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.getThreadId internal fun isEventRead( realmConfiguration: RealmConfiguration, userId: String?, roomId: String?, - eventId: String? + eventId: String?, + shouldCheckIfReadInEventsThread: Boolean ): Boolean { if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) { return false @@ -45,7 +48,8 @@ internal fun isEventRead( eventToCheck.root?.sender == userId -> true // If new event exists and the latest event is from ourselves we can infer the event is read latestEventIsFromSelf(realm, roomId, userId) -> true - eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true + eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, null) -> true + (shouldCheckIfReadInEventsThread && eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, eventToCheck.getThreadId())) -> true else -> false } } @@ -54,27 +58,33 @@ internal fun isEventRead( private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true) ?.root?.sender == userId -private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean { - return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt -> +private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): Boolean { + val isMoreRecent = ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()?.let { readReceipt -> val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() readReceiptEvent?.isMoreRecentThan(this) } ?: false + return isMoreRecent } /** * Missing events can be caused by the latest timeline chunk no longer contain an older event or * by fast lane eagerly displaying events before the database has finished updating. */ -private fun hasReadMissingEvent(realm: Realm, latestChunkEntity: ChunkEntity, roomId: String, userId: String, eventId: String): Boolean { - return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId) +private fun hasReadMissingEvent(realm: Realm, + latestChunkEntity: ChunkEntity, + roomId: String, + userId: String, + eventId: String, + threadId: String? = ReadService.THREAD_ID_MAIN): Boolean { + return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId) } private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean { return ChunkEntity.findIncludingEvent(this, eventId) != null } -private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String): Boolean { - return ReadReceiptEntity.where(this, roomId = roomId, userId = userId).findFirst()?.let { +private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String, threadId: String?): Boolean { + return ReadReceiptEntity.where(this, roomId = roomId, userId = userId, threadId = threadId).findFirst()?.let { latestChunkEntity.timelineEvents.find(it.eventId) } != null } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt index 170814d3..0f9f56b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt @@ -20,12 +20,20 @@ import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.createObject import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields -internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> { +internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String, threadId: String?): RealmQuery<ReadReceiptEntity> { return realm.where<ReadReceiptEntity>() - .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId)) + .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, threadId)) +} + +internal fun ReadReceiptEntity.Companion.forMainTimelineWhere(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> { + return realm.where<ReadReceiptEntity>() + .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, ReadService.THREAD_ID_MAIN)) + .or() + .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, null)) } internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery<ReadReceiptEntity> { @@ -38,23 +46,37 @@ internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: Strin .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId) } -internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { +internal fun ReadReceiptEntity.Companion.createUnmanaged( + roomId: String, + eventId: String, + userId: String, + threadId: String?, + originServerTs: Double +): ReadReceiptEntity { return ReadReceiptEntity().apply { - this.primaryKey = "${roomId}_$userId" + this.primaryKey = buildPrimaryKey(roomId, userId, threadId) this.eventId = eventId this.roomId = roomId this.userId = userId + this.threadId = threadId this.originServerTs = originServerTs } } -internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { - return ReadReceiptEntity.where(realm, roomId, userId).findFirst() - ?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId)) +internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String, threadId: String?): ReadReceiptEntity { + return ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst() + ?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId, threadId)) .apply { this.roomId = roomId this.userId = userId + this.threadId = threadId } } -private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId" +private fun buildPrimaryKey(roomId: String, userId: String, threadId: String?): String { + return if (threadId == null) { + "${roomId}_${userId}" + } else { + "${roomId}_${userId}_${threadId}" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt index a650fa2d..9741a7bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt @@ -24,7 +24,7 @@ internal interface EventInsertLiveProcessor { fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean - suspend fun process(realm: Realm, event: Event) + fun process(realm: Realm, event: Event) /** * Called after transaction. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt index b15a6474..b6ad7581 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -41,9 +41,8 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.ENCRYPTED, - EventType.CALL_ASSERTED_IDENTITY, - EventType.CALL_ASSERTED_IDENTITY_PREFIX - ) + ) + + EventType.CALL_ASSERTED_IDENTITY.values private val eventsToPostProcess = mutableListOf<Event>() @@ -54,7 +53,7 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH return allowedTypes.contains(eventType) } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { eventsToPostProcess.add(event) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 48a9dfd3..d824aaa5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -84,8 +84,7 @@ internal class CallSignalingHandler @Inject constructor( EventType.CALL_NEGOTIATE -> { handleCallNegotiateEvent(event) } - EventType.CALL_ASSERTED_IDENTITY, - EventType.CALL_ASSERTED_IDENTITY_PREFIX -> { + in EventType.CALL_ASSERTED_IDENTITY.values -> { handleCallAssertedIdentityEvent(event) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt index 1d1bb0e7..4e5b0055 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt @@ -17,74 +17,71 @@ package org.matrix.android.sdk.internal.session.filter import com.zhuinden.monarchy.Monarchy -import io.realm.Realm import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.mapper.FilterParamsMapper import org.matrix.android.sdk.internal.database.model.FilterEntity -import org.matrix.android.sdk.internal.database.model.FilterEntityFields +import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity import org.matrix.android.sdk.internal.database.query.get import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams import org.matrix.android.sdk.internal.util.awaitTransaction import javax.inject.Inject -internal class DefaultFilterRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : FilterRepository { +internal class DefaultFilterRepository @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val filterParamsMapper: FilterParamsMapper +) : FilterRepository { - override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val filterEntity = FilterEntity.get(realm) - // Filter has changed, or no filter Id yet - filterEntity == null || - filterEntity.filterBodyJson != filter.toJSONString() || - filterEntity.filterId.isBlank() - }.also { hasChanged -> - if (hasChanged) { - // Filter is new or has changed, store it and reset the filter Id. - // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread - monarchy.awaitTransaction { realm -> - // We manage only one filter for now - val filterJson = filter.toJSONString() - val roomEventFilterJson = roomEventFilter.toJSONString() + override suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) { + monarchy.awaitTransaction { realm -> + // We manage only one filter for now + val filterJson = filter.toJSONString() + val roomEventFilterJson = roomEventFilter.toJSONString() - val filterEntity = FilterEntity.getOrCreate(realm) + val filterEntity = FilterEntity.getOrCreate(realm) - filterEntity.filterBodyJson = filterJson - filterEntity.roomEventFilterJson = roomEventFilterJson - // Reset filterId - filterEntity.filterId = "" - } - } + filterEntity.filterBodyJson = filterJson + filterEntity.roomEventFilterJson = roomEventFilterJson + filterEntity.filterId = filterId } } - override suspend fun storeFilterId(filter: Filter, filterId: String) { - monarchy.awaitTransaction { - // We manage only one filter for now - val filterJson = filter.toJSONString() - - // Update the filter id, only if the filter body matches - it.where<FilterEntity>() - .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson) - ?.findFirst() - ?.filterId = filterId + override suspend fun getStoredSyncFilterBody(): String { + return monarchy.awaitTransaction { + FilterEntity.getOrCreate(it).filterBodyJson } } - override suspend fun getFilter(): String { + override suspend fun getStoredSyncFilterId(): String? { return monarchy.awaitTransaction { - val filter = FilterEntity.getOrCreate(it) - if (filter.filterId.isBlank()) { - // Use the Json format - filter.filterBodyJson + val id = FilterEntity.get(it)?.filterId + if (id.isNullOrBlank()) { + null } else { - // Use FilterId - filter.filterId + id } } } - override suspend fun getRoomFilter(): String { + override suspend fun getRoomFilterBody(): String { return monarchy.awaitTransaction { FilterEntity.getOrCreate(it).roomEventFilterJson } } + + override suspend fun getStoredFilterParams(): SyncFilterParams? { + return monarchy.awaitTransaction { realm -> + realm.where<SyncFilterParamsEntity>().findFirst()?.let { + filterParamsMapper.map(it) + } + } + } + + override suspend fun storeFilterParams(params: SyncFilterParams) { + return monarchy.awaitTransaction { realm -> + val entity = filterParamsMapper.map(params) + realm.insertOrUpdate(entity) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt index 2e68d02d..c54e7de0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt @@ -17,19 +17,27 @@ package org.matrix.android.sdk.internal.session.filter import org.matrix.android.sdk.api.session.sync.FilterService -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource import javax.inject.Inject internal class DefaultFilterService @Inject constructor( private val saveFilterTask: SaveFilterTask, - private val taskExecutor: TaskExecutor + private val filterRepository: FilterRepository, + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, ) : FilterService { // TODO Pass a list of support events instead - override fun setFilter(filterPreset: FilterService.FilterPreset) { - saveFilterTask - .configureWith(SaveFilterTask.Params(filterPreset)) - .executeBy(taskExecutor) + override suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder) { + filterRepository.storeFilterParams(filterBuilder.extractParams()) + + // don't upload/store filter until homeserver capabilities are fetched + homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.let { homeServerCapabilities -> + saveFilterTask.execute( + SaveFilterTask.Params( + filter = filterBuilder.build(homeServerCapabilities) + ) + ) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt index e0919c52..1bd2e59e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt @@ -45,46 +45,7 @@ internal object FilterFactory { return FilterUtil.enableLazyLoading(Filter(), true) } - fun createElementFilter(): Filter { - return Filter( - room = RoomFilter( - timeline = createElementTimelineFilter(), - state = createElementStateFilter() - ) - ) - } - fun createDefaultRoomFilter(): RoomEventFilter { return RoomEventFilter(lazyLoadMembers = true) } - - fun createElementRoomFilter(): RoomEventFilter { - return RoomEventFilter( - lazyLoadMembers = true, - // TODO Enable this for optimization - // types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList() - ) - } - - private fun createElementTimelineFilter(): RoomEventFilter? { -// we need to check if homeserver supports thread notifications before setting this param -// return RoomEventFilter(enableUnreadThreadNotifications = true) - return null - } - - private fun createElementStateFilter(): RoomEventFilter { - return RoomEventFilter(lazyLoadMembers = true) - } - - // Get only managed types by Element - private val listOfSupportedEventTypes = listOf( - // TODO Complete the list - EventType.MESSAGE - ) - - // Get only managed types by Element - private val listOfSupportedStateEventTypes = listOf( - // TODO Complete the list - EventType.STATE_ROOM_MEMBER - ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt index 8531bed1..ca9f798f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt @@ -44,4 +44,7 @@ internal abstract class FilterModule { @Binds abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask + + @Binds + abstract fun bindGetCurrentFilterTask(task: DefaultGetCurrentFilterTask): GetCurrentFilterTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt index f40231c8..71d7391e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt @@ -16,25 +16,42 @@ package org.matrix.android.sdk.internal.session.filter +import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams + +/** + * Repository for request filters. + */ internal interface FilterRepository { /** - * Return true if the filterBody has changed, or need to be sent to the server. + * Stores sync filter and room filter. + * Note: It looks like we could use [Filter.room.timeline] instead of a separate [RoomEventFilter], but it's not clear if it's safe, so research is needed + * @return true if the filterBody has changed, or need to be sent to the server. */ - suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean + suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) /** - * Set the filterId of this filter. + * Returns stored sync filter's JSON body if it exists. */ - suspend fun storeFilterId(filter: Filter, filterId: String) + suspend fun getStoredSyncFilterBody(): String? /** - * Return filter json or filter id. + * Returns stored sync filter's ID if it exists. */ - suspend fun getFilter(): String + suspend fun getStoredSyncFilterId(): String? /** * Return the room filter. */ - suspend fun getRoomFilter(): String + suspend fun getRoomFilterBody(): String + + /** + * Returns filter params stored in local storage if it exists. + */ + suspend fun getStoredFilterParams(): SyncFilterParams? + + /** + * Stores filter params to local storage. + */ + suspend fun storeFilterParams(params: SyncFilterParams) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt new file mode 100644 index 00000000..76805c5c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2022 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.filter + +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetCurrentFilterTask : Task<Unit, String> + +internal class DefaultGetCurrentFilterTask @Inject constructor( + private val filterRepository: FilterRepository, + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + private val saveFilterTask: SaveFilterTask +) : GetCurrentFilterTask { + + override suspend fun execute(params: Unit): String { + val storedFilterId = filterRepository.getStoredSyncFilterId() + val storedFilterBody = filterRepository.getStoredSyncFilterBody() + val homeServerCapabilities = homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities() + val currentFilter = SyncFilterBuilder() + .with(filterRepository.getStoredFilterParams()) + .build(homeServerCapabilities) + + val currentFilterBody = currentFilter.toJSONString() + + return when (storedFilterBody) { + currentFilterBody -> storedFilterId ?: storedFilterBody + else -> saveFilter(currentFilter) ?: currentFilterBody + } + } + + private suspend fun saveFilter(filter: Filter) = saveFilterTask + .execute( + SaveFilterTask.Params( + filter = filter + ) + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt index 63afa1bb..0223cd3e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.internal.session.filter -import org.matrix.android.sdk.api.session.sync.FilterService +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest @@ -25,11 +25,12 @@ import javax.inject.Inject /** * Save a filter, in db and if any changes, upload to the server. + * Return the filterId if uploading to the server is successful, else return null. */ -internal interface SaveFilterTask : Task<SaveFilterTask.Params, Unit> { +internal interface SaveFilterTask : Task<SaveFilterTask.Params, String?> { data class Params( - val filterPreset: FilterService.FilterPreset + val filter: Filter ) } @@ -37,33 +38,23 @@ internal class DefaultSaveFilterTask @Inject constructor( @UserId private val userId: String, private val filterAPI: FilterApi, private val filterRepository: FilterRepository, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, ) : SaveFilterTask { - override suspend fun execute(params: SaveFilterTask.Params) { - val filterBody = when (params.filterPreset) { - FilterService.FilterPreset.ElementFilter -> { - FilterFactory.createElementFilter() - } - FilterService.FilterPreset.NoFilter -> { - FilterFactory.createDefaultFilter() - } - } - val roomFilter = when (params.filterPreset) { - FilterService.FilterPreset.ElementFilter -> { - FilterFactory.createElementRoomFilter() - } - FilterService.FilterPreset.NoFilter -> { - FilterFactory.createDefaultRoomFilter() + override suspend fun execute(params: SaveFilterTask.Params): String? { + val filter = params.filter + val filterResponse = tryOrNull { + executeRequest(globalErrorReceiver) { + filterAPI.uploadFilter(userId, filter) } } - val updated = filterRepository.storeFilter(filterBody, roomFilter) - if (updated) { - val filterResponse = executeRequest(globalErrorReceiver) { - // TODO auto retry - filterAPI.uploadFilter(userId, filterBody) - } - filterRepository.storeFilterId(filterBody, filterResponse.filterId) - } + + val filterId = filterResponse?.filterId + filterRepository.storeSyncFilter( + filter = filter, + filterId = filterId.orEmpty(), + roomEventFilter = FilterFactory.createDefaultRoomFilter() + ) + return filterId } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt index 09d7d50e..9fe93d82 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt @@ -56,8 +56,8 @@ internal class DefaultProcessEventForPushTask @Inject constructor( val allEvents = (newJoinEvents + inviteEvents).filter { event -> when (event.type) { - in EventType.POLL_START, - in EventType.STATE_ROOM_BEACON_INFO, + in EventType.POLL_START.values, + in EventType.STATE_ROOM_BEACON_INFO.values, EventType.MESSAGE, EventType.REDACTION, EventType.ENCRYPTED, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt new file mode 100644 index 00000000..41d0c3f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2022 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 + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import timber.log.Timber +import javax.inject.Inject + +internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) { + + sealed class EditValidity { + object Valid : EditValidity() + data class Invalid(val reason: String) : EditValidity() + object Unknown : EditValidity() + } + + /** + * There are a number of requirements on replacement events, which must be satisfied for the replacement + * to be considered valid: + * As with all event relationships, the original event and replacement event must have the same room_id + * (i.e. you cannot send an event in one room and then an edited version in a different room). + * The original event and replacement event must have the same sender (i.e. you cannot edit someone else’s messages). + * The replacement and original events must have the same type (i.e. you cannot change the original event’s type). + * The replacement and original events must not have a state_key property (i.e. you cannot edit state events at all). + * The original event must not, itself, have a rel_type of m.replace + * (i.e. you cannot edit an edit — though you can send multiple edits for a single original event). + * The replacement event (once decrypted, if appropriate) must have an m.new_content property. + * + * If the original event was encrypted, the replacement should be too. + */ + fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity { + Timber.v("###REPLACE valide event $originalEvent replaced $replaceEvent") + // we might not know the original event at that time. In this case we can't perform the validation + // Edits should be revalidated when the original event is received + if (originalEvent == null) { + return EditValidity.Unknown + } + + if (LocalEcho.isLocalEchoId(replaceEvent.eventId.orEmpty())) { + // Don't validate local echo + return EditValidity.Unknown + } + + if (originalEvent.roomId != replaceEvent.roomId) { + return EditValidity.Invalid("original event and replacement event must have the same room_id") + } + if (originalEvent.isStateEvent() || replaceEvent.isStateEvent()) { + return EditValidity.Invalid("replacement and original events must not have a state_key property") + } + // check it's from same sender + + if (originalEvent.isEncrypted()) { + if (!replaceEvent.isEncrypted()) return EditValidity.Invalid("If the original event was encrypted, the replacement should be too") + val originalDecrypted = originalEvent.toValidDecryptedEvent() + ?: return EditValidity.Unknown // UTD can't decide + val replaceDecrypted = replaceEvent.toValidDecryptedEvent() + ?: return EditValidity.Unknown // UTD can't decide + + val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId + val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId + + if (originalDecrypted.getRelationContent()?.type == RelationType.REPLACE) { + return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") + } + + if (originalCryptoSenderId == null || editCryptoSenderId == null) { + // mm what can we do? we don't know if it's cryptographically from same user? + // let valid and UI should display send by deleted device warning? + val bestEffortOriginal = originalCryptoSenderId ?: originalEvent.senderId + val bestEffortEdit = editCryptoSenderId ?: replaceEvent.senderId + if (bestEffortOriginal != bestEffortEdit) { + return EditValidity.Invalid("original event and replacement event must have the same sender") + } + } else { + if (originalCryptoSenderId != editCryptoSenderId) { + return EditValidity.Invalid("Crypto: original event and replacement event must have the same sender") + } + } + + if (originalDecrypted.type != replaceDecrypted.type) { + return EditValidity.Invalid("replacement and original events must have the same type") + } + if (replaceDecrypted.clearContent.toModel<MessageContent>()?.newContent == null) { + return EditValidity.Invalid("replacement event must have an m.new_content property") + } + } else { + if (originalEvent.getRelationContent()?.type == RelationType.REPLACE) { + return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") + } + + // check the sender + if (originalEvent.senderId != replaceEvent.senderId) { + return EditValidity.Invalid("original event and replacement event must have the same sender") + } + if (originalEvent.type != replaceEvent.type) { + return EditValidity.Invalid("replacement and original events must have the same type") + } + if (replaceEvent.content.toModel<MessageContent>()?.newContent == null) { + return EditValidity.Invalid("replacement event must have an m.new_content property") + } + } + + return EditValidity.Valid + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 24d4975e..be733098 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.verification.toState import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.EditionOfEvent import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity @@ -72,6 +73,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private val sessionManager: SessionManager, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, private val pollAggregationProcessor: PollAggregationProcessor, + private val editValidator: EventEditValidator, private val clock: Clock, ) : EventInsertLiveProcessor { @@ -79,22 +81,28 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.MESSAGE, EventType.REDACTION, EventType.REACTION, + // The aggregator handles verification events but just to render tiles in the timeline + // It's not participating in verification itself, just timeline display EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, - // TODO Add ? - // EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY, EventType.ENCRYPTED - ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA + ) + + EventType.POLL_START.values + + EventType.POLL_RESPONSE.values + + EventType.POLL_END.values + + EventType.STATE_ROOM_BEACON_INFO.values + + EventType.BEACON_LOCATION_DATA.values override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { return allowedTypes.contains(eventType) } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { try { // Temporary catch, should be removed val roomId = event.roomId if (roomId == null) { @@ -102,7 +110,31 @@ internal class EventRelationsAggregationProcessor @Inject constructor( return } val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") - when (event.type) { + + // It might be a late decryption of the original event or a event received when back paginating? + // let's check if there is already a summary for it and do some cleaning + if (!isLocalEcho) { + EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId.orEmpty()) + .findFirst() + ?.editSummary + ?.editions + ?.forEach { editionOfEvent -> + EventEntity.where(realm, editionOfEvent.eventId).findFirst()?.asDomain()?.let { editEvent -> + when (editValidator.validateEdit(event, editEvent)) { + is EventEditValidator.EditValidity.Invalid -> { + // delete it, it was invalid + Timber.v("## Replace: Removing a previously accepted edit for event ${event.eventId}") + editionOfEvent.deleteFromRealm() + } + else -> { + // nop + } + } + } + } + } + + when (event.getClearType()) { EventType.REACTION -> { // we got a reaction!! Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") @@ -113,21 +145,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}") handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) - EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() - ?.let { - TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll() - ?.forEach { tet -> tet.annotations = it } - } + // XXX do something for aggregated edits? + // it's a bit strange as it would require to do a server query to get the edition? } - val content: MessageContent? = event.content.toModel() - if (content?.relatesTo?.type == RelationType.REPLACE) { + val relationContent = event.getRelationContent() + if (relationContent?.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! - handleReplace(realm, event, content, roomId, isLocalEcho) + handleReplace(realm, event, roomId, isLocalEcho, relationContent.eventId) } } - EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_ACCEPT, @@ -142,74 +170,31 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } } - + // As for now Live event processors are not receiving UTD events. + // They will get an update if the event is decrypted later EventType.ENCRYPTED -> { - // Relation type is in clear + // Relation type is in clear, it might be possible to do some things? + // Notice that if the event is decrypted later, process be called again val encryptedEventContent = event.content.toModel<EncryptedEventContent>() - if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE || - encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE - ) { - event.getClearContent().toModel<MessageContent>()?.let { - if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - // A replace! - handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) - } else if (event.getClearType() in EventType.POLL_RESPONSE) { - sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> - pollAggregationProcessor.handlePollResponseEvent(session, realm, event) - } - } + when (encryptedEventContent?.relatesTo?.type) { + RelationType.REPLACE -> { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } - } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { - when (event.getClearType()) { - EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_KEY -> { - Timber.v("## SAS REF in room $roomId for event ${event.eventId}") - encryptedEventContent.relatesTo.eventId?.let { - handleVerification(realm, event, roomId, isLocalEcho, it) - } - } - in EventType.POLL_RESPONSE -> { - event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { - sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> - pollAggregationProcessor.handlePollResponseEvent(session, realm, event) - } - } - } - in EventType.POLL_END -> { - sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> - getPowerLevelsHelper(event.roomId)?.let { - pollAggregationProcessor.handlePollEndEvent(session, it, realm, event) - } - } - } - in EventType.BEACON_LOCATION_DATA -> { - handleBeaconLocationData(event, realm, roomId, isLocalEcho) - } + RelationType.RESPONSE -> { + // can we / should we do we something for UTD response?? + Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") } - } else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) { - // Reaction - if (event.getClearType() == EventType.REACTION) { - // we got a reaction!! - Timber.v("###REACTION e2e in room $roomId , reaction eventID ${event.eventId}") - handleReaction(realm, event, roomId, isLocalEcho) + RelationType.REFERENCE -> { + // can we / should we do we something for UTD reference?? + Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + } + RelationType.ANNOTATION -> { + // can we / should we do we something for UTD annotation?? + Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") } } - // HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations -// else if (event.unsignedData?.relations?.annotations != null) { -// Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}") -// handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) -// EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() -// ?.let { -// TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll() -// ?.forEach { tet -> tet.annotations = it } -// } -// } } EventType.REDACTION -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } @@ -217,9 +202,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( when (eventToPrune.type) { EventType.MESSAGE -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") -// val unsignedData = EventMapper.map(eventToPrune).unsignedData -// ?: UnsignedData(null, null) - // was this event a m.replace val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>() if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { @@ -231,34 +213,34 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } } - in EventType.POLL_START -> { + in EventType.POLL_START.values -> { val content: MessagePollContent? = event.content.toModel() if (content?.relatesTo?.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! - handleReplace(realm, event, content, roomId, isLocalEcho) + handleReplace(realm, event, roomId, isLocalEcho, content.relatesTo.eventId) } } - in EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE.values -> { event.content.toModel<MessagePollResponseContent>(catchError = true)?.let { sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> pollAggregationProcessor.handlePollResponseEvent(session, realm, event) } } } - in EventType.POLL_END -> { + in EventType.POLL_END.values -> { sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> getPowerLevelsHelper(event.roomId)?.let { pollAggregationProcessor.handlePollEndEvent(session, it, realm, event) } } } - in EventType.STATE_ROOM_BEACON_INFO -> { + in EventType.STATE_ROOM_BEACON_INFO.values -> { event.content.toModel<MessageBeaconInfoContent>(catchError = true)?.let { liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho) } } - in EventType.BEACON_LOCATION_DATA -> { + in EventType.BEACON_LOCATION_DATA.values -> { handleBeaconLocationData(event, realm, roomId, isLocalEcho) } else -> Timber.v("UnHandled event ${event.eventId}") @@ -274,23 +256,22 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private fun handleReplace( realm: Realm, event: Event, - content: MessageContent, roomId: String, isLocalEcho: Boolean, - relatedEventId: String? = null + relatedEventId: String? ) { val eventId = event.eventId ?: return - val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return - val newContent = content.newContent ?: return - - // Check that the sender is the same + val targetEventId = relatedEventId ?: return val editedEvent = EventEntity.where(realm, targetEventId).findFirst() - if (editedEvent == null) { - // We do not know yet about the edited event - } else if (editedEvent.sender != event.senderId) { - // Edited by someone else, ignore - Timber.w("Ignore edition by someone else") - return + + when (val validity = editValidator.validateEdit(editedEvent?.asDomain(), event)) { + is EventEditValidator.EditValidity.Invalid -> return Unit.also { + Timber.w("Dropping invalid edit ${event.eventId}, reason:${validity.reason}") + } + EventEditValidator.EditValidity.Unknown, // we can't drop the source event might be unknown, will be validated later + EventEditValidator.EditValidity.Valid -> { + // continue + } } // ok, this is a replace @@ -305,11 +286,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor( .also { editSummary -> editSummary.editions.add( EditionOfEvent( - senderId = event.senderId ?: "", eventId = event.eventId, - content = ContentMapper.map(newContent), - timestamp = if (isLocalEcho) 0 else event.originServerTs ?: 0, - isLocalEcho = isLocalEcho + event = EventEntity.where(realm, eventId).findFirst(), + timestamp = if (isLocalEcho) clock.epochMillis() else event.originServerTs ?: clock.epochMillis(), + isLocalEcho = isLocalEcho, ) ) } @@ -326,17 +306,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // ok it has already been managed Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") existingSummary.editions.firstOrNull { it.eventId == txId }?.let { - it.eventId = event.eventId + it.eventId = eventId it.timestamp = event.originServerTs ?: clock.epochMillis() it.isLocalEcho = false + it.event = EventEntity.where(realm, eventId).findFirst() } } else { Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") existingSummary.editions.add( EditionOfEvent( - senderId = event.senderId ?: "", - eventId = event.eventId, - content = ContentMapper.map(newContent), + eventId = eventId, + event = EventEntity.where(realm, eventId).findFirst(), timestamp = if (isLocalEcho) { clock.epochMillis() } else { @@ -349,7 +329,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } - if (event.getClearType() in EventType.POLL_START) { + if (event.getClearType() in EventType.POLL_START.values) { pollAggregationProcessor.handlePollStartEvent(realm, event) } @@ -501,7 +481,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val sourceToDiscard = eventSummary.editSummary?.editions?.firstOrNull { it.eventId == redacted.eventId } if (sourceToDiscard == null) { - Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") + Timber.w("Redaction of a replace that was not known in aggregation") return } // Need to remove this event from the edition list @@ -599,12 +579,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) { event.getClearContent().toModel<MessageBeaconLocationDataContent>(catchError = true)?.let { liveLocationAggregationProcessor.handleBeaconLocationData( - realm, - event, - it, - roomId, - event.getRelationContent()?.eventId, - isLocalEcho + realm = realm, + event = event, + content = it, + roomId = roomId, + relatedEventId = event.getRelationContent()?.eventId, + isLocalEcho = isLocalEcho ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 9bcb7b8e..31bed90b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMembersRespon import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody +import org.matrix.android.sdk.internal.session.room.read.ReadBody import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody import org.matrix.android.sdk.internal.session.room.send.SendResponse @@ -173,7 +174,7 @@ internal interface RoomAPI { @Path("roomId") roomId: String, @Path("receiptType") receiptType: String, @Path("eventId") eventId: String, - @Body body: JsonDict = emptyMap() + @Body body: ReadBody ) /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt index 03c2b2a4..0cda6eca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt @@ -21,6 +21,7 @@ import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.Membership @@ -95,7 +96,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor( * Create a local room entity from the given room creation params. * This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room. */ - private suspend fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) { + private fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) { RoomEntity.getOrCreate(realm, roomId).apply { membership = Membership.JOIN chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody)) @@ -148,13 +149,16 @@ internal class DefaultCreateLocalRoomTask @Inject constructor( * * @return a chunk entity */ - private suspend fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity { + private fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity { val chunkEntity = realm.createObject<ChunkEntity>().apply { isLastBackward = true isLastForward = true } - val eventList = createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody)) + // Can't suspend when using realm as it could jump thread + val eventList = runBlocking { + createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody)) + } val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() for (event in eventList) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt index eb966b68..8b5fde6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt @@ -30,7 +30,7 @@ import javax.inject.Inject internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor { - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { val createRoomContent = event.getClearContent().toModel<RoomCreateContent>() val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 60312071..c36efa06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -73,7 +73,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return sendLiveLocationTask.execute(params) } - override suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult { + override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult { // Ensure to stop any active live before starting a new one if (checkIfExistingActiveLive()) { val result = stopLiveLocationShare() @@ -84,7 +84,6 @@ internal class DefaultLocationSharingService @AssistedInject constructor( val params = StartLiveLocationShareTask.Params( roomId = roomId, timeoutMillis = timeoutMillis, - description = description ) return startLiveLocationShareTask.execute(params) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt index a8d955af..ae7022a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt @@ -39,7 +39,7 @@ internal class DefaultGetActiveBeaconInfoForUserTask @Inject constructor( ) : GetActiveBeaconInfoForUserTask { override suspend fun execute(params: GetActiveBeaconInfoForUserTask.Params): Event? { - return EventType.STATE_ROOM_BEACON_INFO + return EventType.STATE_ROOM_BEACON_INFO.values .mapNotNull { stateEventDataSource.getStateEvent( roomId = params.roomId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt index fa3479ed..dbdc5dc2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt @@ -40,7 +40,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) { return } @@ -48,7 +48,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst() ?: return - if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) { + if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO.values) { val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId) if (liveSummary != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt index 79019e47..13753115 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt @@ -30,7 +30,6 @@ internal interface StartLiveLocationShareTask : Task<StartLiveLocationShareTask. data class Params( val roomId: String, val timeoutMillis: Long, - val description: String, ) } @@ -42,12 +41,12 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor( override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult { val beaconContent = MessageBeaconInfoContent( - body = params.description, + body = "Live location", timeout = params.timeoutMillis, isLive = true, unstableTimestampMillis = clock.epochMillis() ).toContent() - val eventType = EventType.STATE_ROOM_BEACON_INFO.first() + val eventType = EventType.STATE_ROOM_BEACON_INFO.stable val sendStateTaskParams = SendStateTask.Params( roomId = params.roomId, stateKey = userId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt index da5fd769..40f7aa2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt @@ -45,7 +45,7 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor( val sendStateTaskParams = SendStateTask.Params( roomId = params.roomId, stateKey = stateKey, - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + eventType = EventType.STATE_ROOM_BEACON_INFO.stable, body = updatedContent ) return try { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index cc86679c..0cff2c5a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -46,7 +46,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr return eventType == EventType.REDACTION } - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { pruneEvent(realm, event) } @@ -61,45 +61,36 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr val isLocalEcho = LocalEcho.isLocalEchoId(redactionEvent.eventId ?: "") Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho") - val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() - ?: return + val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() ?: return val typeToPrune = eventToPrune.type val stateKey = eventToPrune.stateKey val allowedKeys = computeAllowedKeys(typeToPrune) - if (allowedKeys.isNotEmpty()) { - val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } - eventToPrune.content = ContentMapper.map(prunedContent) - } else { - when (typeToPrune) { - EventType.ENCRYPTED, - EventType.MESSAGE, - in EventType.STATE_ROOM_BEACON_INFO, - in EventType.BEACON_LOCATION_DATA, - in EventType.POLL_START -> { - Timber.d("REDACTION for message ${eventToPrune.eventId}") - val unsignedData = EventMapper.map(eventToPrune).unsignedData - ?: UnsignedData(null, null) - - // was this event a m.replace + when { + allowedKeys.isNotEmpty() -> { + val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } + eventToPrune.content = ContentMapper.map(prunedContent) + } + canPruneEventType(typeToPrune) -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") + val unsignedData = EventMapper.map(eventToPrune).unsignedData ?: UnsignedData(null, null) + + // was this event a m.replace // val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>() // if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { // eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) // } - val modified = unsignedData.copy(redactedEvent = redactionEvent) - // Deleting the content of a thread message will result to delete the thread relation, however threads are now dynamic - // so there is not much of a problem - eventToPrune.content = ContentMapper.map(emptyMap()) - eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) - eventToPrune.decryptionResultJson = null - eventToPrune.decryptionErrorCode = null - - handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho) - } -// EventType.REACTION -> { -// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) -// } + val modified = unsignedData.copy(redactedEvent = redactionEvent) + eventToPrune.content = ContentMapper.map(emptyMap()) + eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) + eventToPrune.decryptionResultJson = null + eventToPrune.decryptionErrorCode = null + + handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho) + } + else -> { + Timber.w("Not pruning event (type $typeToPrune)") } } if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) { @@ -167,4 +158,28 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr else -> emptyList() } } + + private fun canPruneEventType(eventType: String): Boolean { + return when { + EventType.isCallEvent(eventType) -> false + EventType.isVerificationEvent(eventType) -> false + eventType == EventType.STATE_ROOM_WIDGET_LEGACY || + eventType == EventType.STATE_ROOM_WIDGET || + eventType == EventType.STATE_ROOM_NAME || + eventType == EventType.STATE_ROOM_TOPIC || + eventType == EventType.STATE_ROOM_AVATAR || + eventType == EventType.STATE_ROOM_THIRD_PARTY_INVITE || + eventType == EventType.STATE_ROOM_GUEST_ACCESS || + eventType == EventType.STATE_SPACE_CHILD || + eventType == EventType.STATE_SPACE_PARENT || + eventType == EventType.STATE_ROOM_TOMBSTONE || + eventType == EventType.STATE_ROOM_HISTORY_VISIBILITY || + eventType == EventType.STATE_ROOM_RELATED_GROUPS || + eventType == EventType.STATE_ROOM_PINNED_EVENT || + eventType == EventType.STATE_ROOM_ENCRYPTION || + eventType == EventType.STATE_ROOM_SERVER_ACL || + eventType == EventType.REACTION -> false + else -> true + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index b30c66c8..36ec5e8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -30,17 +30,20 @@ import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity +import org.matrix.android.sdk.internal.database.query.forMainTimelineWhere import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource internal class DefaultReadService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, private val setReadMarkersTask: SetReadMarkersTask, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - @UserId private val userId: String + @UserId private val userId: String, + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource ) : ReadService { @AssistedFactory @@ -48,17 +51,28 @@ internal class DefaultReadService @AssistedInject constructor( fun create(roomId: String): DefaultReadService } - override suspend fun markAsRead(params: ReadService.MarkAsReadParams) { + override suspend fun markAsRead(params: ReadService.MarkAsReadParams, mainTimeLineOnly: Boolean) { + val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) { + if (mainTimeLineOnly) ReadService.THREAD_ID_MAIN else null + } else { + null + } val taskParams = SetReadMarkersTask.Params( roomId = roomId, forceReadMarker = params.forceReadMarker(), - forceReadReceipt = params.forceReadReceipt() + forceReadReceipt = params.forceReadReceipt(), + readReceiptThreadId = readReceiptThreadId ) setReadMarkersTask.execute(taskParams) } - override suspend fun setReadReceipt(eventId: String) { - val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId) + override suspend fun setReadReceipt(eventId: String, threadId: String) { + val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) { + threadId + } else { + null + } + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId, readReceiptThreadId = readReceiptThreadId) setReadMarkersTask.execute(params) } @@ -68,7 +82,8 @@ internal class DefaultReadService @AssistedInject constructor( } override fun isEventRead(eventId: String): Boolean { - return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId) + val shouldCheckIfReadInEventsThread = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true + return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId, shouldCheckIfReadInEventsThread) } override fun getReadMarkerLive(): LiveData<Optional<String>> { @@ -81,9 +96,9 @@ internal class DefaultReadService @AssistedInject constructor( } } - override fun getMyReadReceiptLive(): LiveData<Optional<String>> { + override fun getMyReadReceiptLive(threadId: String?): LiveData<Optional<String>> { val liveRealmData = monarchy.findAllMappedWithChanges( - { ReadReceiptEntity.where(it, roomId = roomId, userId = userId) }, + { ReadReceiptEntity.where(it, roomId = roomId, userId = userId, threadId = threadId) }, { it.eventId } ) return Transformations.map(liveRealmData) { @@ -94,10 +109,11 @@ internal class DefaultReadService @AssistedInject constructor( override fun getUserReadReceipt(userId: String): String? { var eventId: String? = null monarchy.doWithRealm { - eventId = ReadReceiptEntity.where(it, roomId = roomId, userId = userId) + eventId = ReadReceiptEntity.forMainTimelineWhere(it, roomId = roomId, userId = userId) .findFirst() ?.eventId } + return eventId } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt new file mode 100644 index 00000000..9374de5d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt @@ -0,0 +1,25 @@ +/* + * 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.read + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ReadBody( + @Json(name = "thread_id") val threadId: String?, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt index a124a8a4..8e7592a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.read import com.zhuinden.monarchy.Monarchy import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.isEventRead @@ -45,8 +46,9 @@ internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> { val roomId: String, val fullyReadEventId: String? = null, val readReceiptEventId: String? = null, + val readReceiptThreadId: String? = null, val forceReadReceipt: Boolean = false, - val forceReadMarker: Boolean = false + val forceReadMarker: Boolean = false, ) } @@ -61,12 +63,14 @@ internal class DefaultSetReadMarkersTask @Inject constructor( @UserId private val userId: String, private val globalErrorReceiver: GlobalErrorReceiver, private val clock: Clock, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { val markers = mutableMapOf<String, String>() Timber.v("Execute set read marker with params: $params") val latestSyncedEventId = latestSyncedEventId(params.roomId) + val readReceiptThreadId = params.readReceiptThreadId val fullyReadEventId = if (params.forceReadMarker) { latestSyncedEventId } else { @@ -77,6 +81,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( } else { params.readReceiptEventId } + if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy.realmConfiguration, params.roomId, fullyReadEventId)) { if (LocalEcho.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event $fullyReadEventId") @@ -84,8 +89,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor( markers[READ_MARKER] = fullyReadEventId } } + + val shouldCheckIfReadInEventsThread = readReceiptThreadId != null && + homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications + if (readReceiptEventId != null && - !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId)) { + !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId, shouldCheckIfReadInEventsThread)) { if (LocalEcho.isLocalEchoId(readReceiptEventId)) { Timber.w("Can't set read receipt for local event $readReceiptEventId") } else { @@ -95,7 +104,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId if (markers.isNotEmpty() || shouldUpdateRoomSummary) { - updateDatabase(params.roomId, markers, shouldUpdateRoomSummary) + updateDatabase(params.roomId, readReceiptThreadId, markers, shouldUpdateRoomSummary) } if (markers.isNotEmpty()) { executeRequest( @@ -104,7 +113,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor( ) { if (markers[READ_MARKER] == null) { if (readReceiptEventId != null) { - roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId) + val readBody = ReadBody(threadId = params.readReceiptThreadId) + roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId, readBody) } } else { // "m.fully_read" value is mandatory to make this call @@ -119,7 +129,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId } - private suspend fun updateDatabase(roomId: String, markers: Map<String, String>, shouldUpdateRoomSummary: Boolean) { + private suspend fun updateDatabase(roomId: String, readReceiptThreadId: String?, markers: Map<String, String>, shouldUpdateRoomSummary: Boolean) { monarchy.awaitTransaction { realm -> val readMarkerId = markers[READ_MARKER] val readReceiptId = markers[READ_RECEIPT] @@ -127,7 +137,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor( roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId)) } if (readReceiptId != null) { - val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, clock.epochMillis()) + val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, readReceiptThreadId, clock.epochMillis()) readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null) } if (shouldUpdateRoomSummary) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 55ba78c2..2f8be694 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -181,7 +181,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_START.first(), + type = EventType.POLL_START.stable, content = newContent.toContent().plus(additionalContent.orEmpty()) ) } @@ -206,7 +206,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_RESPONSE.first(), + type = EventType.POLL_RESPONSE.stable, content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) @@ -226,7 +226,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_START.first(), + type = EventType.POLL_START.stable, content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) @@ -249,7 +249,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_END.first(), + type = EventType.POLL_END.stable, content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) @@ -300,7 +300,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.BEACON_LOCATION_DATA.first(), + type = EventType.BEACON_LOCATION_DATA.stable, content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 7c83a4af..21a0862c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -24,6 +24,7 @@ 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.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent @@ -75,7 +76,8 @@ internal class RoomSummaryUpdater @Inject constructor( private val roomAvatarResolver: RoomAvatarResolver, private val eventDecryptor: EventDecryptor, private val crossSigningService: DefaultCrossSigningService, - private val roomAccountDataDataSource: RoomAccountDataDataSource + private val roomAccountDataDataSource: RoomAccountDataDataSource, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, ) { fun refreshLatestPreviewContent(realm: Realm, roomId: String) { @@ -151,9 +153,11 @@ internal class RoomSummaryUpdater @Inject constructor( latestPreviewableEvent.attemptToDecrypt() } + val shouldCheckIfReadInEventsThread = homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications + roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 || // avoid this call if we are sure there are unread events - latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false + latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId, shouldCheckIfReadInEventsThread) } ?: false roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId)) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index c380ccf1..0854cc5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -411,7 +411,7 @@ internal class DefaultTimeline( private fun ensureReadReceiptAreLoaded(realm: Realm) { readReceiptHandler.getContentFromInitSync(roomId) ?.also { - Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId") + Timber.d("INIT_SYNC Insert when opening timeline RR for room $roomId") } ?.let { readReceiptContent -> realm.executeTransactionAsync { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt index 96646b42..9d8d8ecb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt @@ -47,7 +47,7 @@ internal class DefaultFetchTokenAndPaginateTask @Inject constructor( ) : FetchTokenAndPaginateTask { override suspend fun execute(params: FetchTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result { - val filter = filterRepository.getRoomFilter() + val filter = filterRepository.getRoomFilterBody() val response = executeRequest(globalErrorReceiver) { roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt index 015e55f0..c3911dfa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt @@ -39,7 +39,7 @@ internal class DefaultGetContextOfEventTask @Inject constructor( ) : GetContextOfEventTask { override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { - val filter = filterRepository.getRoomFilter() + val filter = filterRepository.getRoomFilterBody() val response = executeRequest(globalErrorReceiver) { // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process. roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt index 8aeccb66..1a7b1cda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt @@ -41,7 +41,7 @@ internal class DefaultPaginationTask @Inject constructor( ) : PaginationTask { override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { - val filter = filterRepository.getRoomFilter() + val filter = filterRepository.getRoomFilterBody() val chunk = executeRequest( globalErrorReceiver, canRetry = true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt index 2b404775..3684bec1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt @@ -30,7 +30,7 @@ import javax.inject.Inject internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor { - override suspend fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event) { if (event.roomId == null) return val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>() if (createRoomContent?.replacementRoomId == null) return diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt index bc1a6976..8a287fb0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt @@ -36,7 +36,7 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.toFailure import org.matrix.android.sdk.internal.session.SessionListeners import org.matrix.android.sdk.internal.session.dispatchTo -import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.session.filter.GetCurrentFilterTask import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseParser import org.matrix.android.sdk.internal.session.user.UserStore @@ -64,11 +64,9 @@ internal interface SyncTask : Task<SyncTask.Params, SyncResponse> { internal class DefaultSyncTask @Inject constructor( private val syncAPI: SyncAPI, @UserId private val userId: String, - private val filterRepository: FilterRepository, private val syncResponseHandler: SyncResponseHandler, private val syncRequestStateTracker: SyncRequestStateTracker, private val syncTokenStore: SyncTokenStore, - private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, private val userStore: UserStore, private val session: Session, private val sessionListeners: SessionListeners, @@ -79,6 +77,8 @@ internal class DefaultSyncTask @Inject constructor( private val syncResponseParser: InitialSyncResponseParser, private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore, private val clock: Clock, + private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask, + private val getCurrentFilterTask: GetCurrentFilterTask ) : SyncTask { private val workingDir = File(fileDirectory, "is") @@ -100,8 +100,13 @@ internal class DefaultSyncTask @Inject constructor( requestParams["since"] = token timeout = params.timeout } + + // Maybe refresh the homeserver capabilities data we know + getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false)) + val filter = getCurrentFilterTask.execute(Unit) + requestParams["timeout"] = timeout.toString() - requestParams["filter"] = filterRepository.getFilter() + requestParams["filter"] = filter params.presence?.let { requestParams["set_presence"] = it.value } val isInitialSync = token == null @@ -115,8 +120,6 @@ internal class DefaultSyncTask @Inject constructor( ) syncRequestStateTracker.startRoot(InitialSyncStep.ImportingAccount, 100) } - // Maybe refresh the homeserver capabilities data we know - getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false)) val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt index 7329611a..7f12ce65 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt @@ -33,10 +33,11 @@ import javax.inject.Inject // value : dict key $UserId // value dict key ts // dict value ts value -internal typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Double>>>> +internal typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Any>>>> private const val READ_KEY = "m.read" private const val TIMESTAMP_KEY = "ts" +private const val THREAD_ID_KEY = "thread_id" internal class ReadReceiptHandler @Inject constructor( private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore @@ -47,14 +48,19 @@ internal class ReadReceiptHandler @Inject constructor( fun createContent( userId: String, eventId: String, + threadId: String?, currentTimeMillis: Long ): ReadReceiptContent { + val userReadReceipt = mutableMapOf<String, Any>( + TIMESTAMP_KEY to currentTimeMillis.toDouble(), + ) + threadId?.let { + userReadReceipt.put(THREAD_ID_KEY, threadId) + } return mapOf( eventId to mapOf( READ_KEY to mapOf( - userId to mapOf( - TIMESTAMP_KEY to currentTimeMillis.toDouble() - ) + userId to userReadReceipt ) ) ) @@ -98,8 +104,9 @@ internal class ReadReceiptHandler @Inject constructor( val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId) for ((userId, paramsDict) in userIdsDict) { - val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 - val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts) + val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0 + val threadId = paramsDict[THREAD_ID_KEY] as String? + val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, threadId, ts) readReceiptsSummary.readReceipts.add(receiptEntity) } readReceiptSummaries.add(readReceiptsSummary) @@ -115,7 +122,7 @@ internal class ReadReceiptHandler @Inject constructor( ) { // First check if we have data from init sync to handle getContentFromInitSync(roomId)?.let { - Timber.w("INIT_SYNC Insert during incremental sync RR for room $roomId") + Timber.d("INIT_SYNC Insert during incremental sync RR for room $roomId") doIncrementalSyncStrategy(realm, roomId, it) aggregator?.ephemeralFilesToDelete?.add(roomId) } @@ -132,8 +139,9 @@ internal class ReadReceiptHandler @Inject constructor( } for ((userId, paramsDict) in userIdsDict) { - val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 - val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId) + val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0 + val threadId = paramsDict[THREAD_ID_KEY] as String? + val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId, threadId) // ensure new ts is superior to the previous one if (ts > receiptEntity.originServerTs) { ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 2825be82..4001ae2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult 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.RelationType +import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.room.model.Membership @@ -49,6 +51,7 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity @@ -486,23 +489,41 @@ internal class RoomSyncHandler @Inject constructor( cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) // Try to remove local echo - event.unsignedData?.transactionId?.also { - val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) + event.unsignedData?.transactionId?.also { txId -> + val sendingEventEntity = roomEntity.sendingTimelineEvents.find(txId) if (sendingEventEntity != null) { - Timber.v("Remove local echo for tx:$it") + Timber.v("Remove local echo for tx:$txId") roomEntity.sendingTimelineEvents.remove(sendingEventEntity) if (event.isEncrypted() && event.content?.get("algorithm") as? String == MXCRYPTO_ALGORITHM_MEGOLM) { - // updated with echo decryption, to avoid seeing it decrypt again + // updated with echo decryption, to avoid seeing txId decrypt again val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java) sendingEventEntity.root?.decryptionResultJson?.let { json -> eventEntity.decryptionResultJson = json event.mxDecryptionResult = adapter.fromJson(json) } } + // also update potential edit that could refer to that event? + // If not display will flicker :/ + val relationContent = event.getRelationContent() + if (relationContent?.type == RelationType.REPLACE) { + relationContent.eventId?.let { targetId -> + EventAnnotationsSummaryEntity.where(realm, roomId, targetId) + .findFirst() + ?.editSummary + ?.editions + ?.forEach { + if (it.eventId == txId) { + // just do that, the aggregation processor will to the rest + it.event = eventEntity + } + } + } + } + // Finally delete the local echo sendingEventEntity.deleteOnCascade(true) } else { - Timber.v("Can't find corresponding local echo for tx:$it") + Timber.v("Can't find corresponding local echo for tx:$txId") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt new file mode 100644 index 00000000..a7de7f55 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 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.sync.filter + +internal data class SyncFilterParams( + val lazyLoadMembersForStateEvents: Boolean? = null, + val lazyLoadMembersForMessageEvents: Boolean? = null, + val useThreadNotifications: Boolean? = null, + val listOfSupportedEventTypes: List<String>? = null, + val listOfSupportedStateEventTypes: List<String>? = null, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt index 6152eaca..af3ba80f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt @@ -22,7 +22,7 @@ import io.realm.RealmModel import org.matrix.android.sdk.internal.database.awaitTransaction import java.util.concurrent.atomic.AtomicReference -internal suspend fun <T> Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T { +internal suspend fun <T> Monarchy.awaitTransaction(transaction: (realm: Realm) -> T): T { return awaitTransaction(realmConfiguration, transaction) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt new file mode 100644 index 00000000..7ad5bb40 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2022 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.database.mapper + +import io.mockk.every +import io.mockk.mockk +import io.realm.RealmList +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldNotBe +import org.junit.Test +import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.EditionOfEvent +import org.matrix.android.sdk.internal.database.model.EventEntity + +class EditAggregatedSummaryEntityMapperTest { + + @Test + fun `test mapping summary entity to model`() { + val edits = RealmList<EditionOfEvent>( + EditionOfEvent( + timestamp = 0L, + eventId = "e0", + isLocalEcho = false, + event = mockEvent("e0") + ), + EditionOfEvent( + timestamp = 1L, + eventId = "e1", + isLocalEcho = false, + event = mockEvent("e1") + ), + EditionOfEvent( + timestamp = 30L, + eventId = "e2", + isLocalEcho = true, + event = mockEvent("e2") + ) + ) + val fakeSummaryEntity = mockk<EditAggregatedSummaryEntity> { + every { editions } returns edits + } + + val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity) + mapped shouldNotBe null + mapped!!.sourceEvents.size shouldBeEqualTo 2 + mapped.localEchos.size shouldBeEqualTo 1 + mapped.localEchos.first() shouldBeEqualTo "e2" + + mapped.lastEditTs shouldBeEqualTo 30L + mapped.latestEdit?.eventId shouldBeEqualTo "e2" + } + + @Test + fun `event with lexicographically largest event_id is treated as more recent`() { + val lowerId = "\$Albatross" + val higherId = "\$Zebra" + + (higherId > lowerId) shouldBeEqualTo true + val timestamp = 1669288766745L + val edits = RealmList<EditionOfEvent>( + EditionOfEvent( + timestamp = timestamp, + eventId = lowerId, + isLocalEcho = false, + event = mockEvent(lowerId) + ), + EditionOfEvent( + timestamp = timestamp, + eventId = higherId, + isLocalEcho = false, + event = mockEvent(higherId) + ), + EditionOfEvent( + timestamp = 1L, + eventId = "e2", + isLocalEcho = true, + event = mockEvent("e2") + ) + ) + + val fakeSummaryEntity = mockk<EditAggregatedSummaryEntity> { + every { editions } returns edits + } + val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity) + mapped!!.lastEditTs shouldBeEqualTo timestamp + mapped.latestEdit?.eventId shouldBeEqualTo higherId + } + + private fun mockEvent(eventId: String): EventEntity { + return EventEntity().apply { + this.eventId = eventId + this.content = """ + { + "body" : "Hello", + "msgtype": "text" + } + """.trimIndent() + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt new file mode 100644 index 00000000..5fda242b --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2022 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.event + +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldNotBe +import org.junit.Test +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +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.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent + +class ValidDecryptedEventTest { + + private val fakeEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$eventId", + roomId = "!fakeRoom", + content = EncryptedEventContent( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + ciphertext = "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...", + sessionId = "TO2G4u2HlnhtbIJk", + senderKey = "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0", + deviceId = "FAKEE" + ).toContent() + ) + + @Test + fun `A failed to decrypt message should give a null validated decrypted event`() { + fakeEvent.toValidDecryptedEvent() shouldBe null + } + + @Test + fun `Mismatch sender key detection`() { + val decryptedEvent = fakeEvent + .apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "the_real_sender_key", + ) + } + + val validDecryptedEvent = decryptedEvent.toValidDecryptedEvent() + validDecryptedEvent shouldNotBe null + + fakeEvent.content!!["senderKey"] shouldNotBe "the_real_sender_key" + validDecryptedEvent!!.cryptoSenderKey shouldBe "the_real_sender_key" + } + + @Test + fun `Mixed content event should be detected`() { + val mixedEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$eventd ", + roomId = "!fakeRoo", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "ciphertext" to "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...", + "sessionId" to "TO2G4u2HlnhtbIJk", + "senderKey" to "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0", + "deviceId" to "FAKEE", + "body" to "some message", + "msgtype" to "m.text" + ).toContent() + ) + + val unValidatedContent = mixedEvent.getClearContent().toModel<MessageTextContent>() + unValidatedContent?.body shouldBe "some message" + + mixedEvent.toValidDecryptedEvent()?.clearContent?.toModel<MessageTextContent>() shouldBe null + } + + @Test + fun `Basic field validation`() { + val decryptedEvent = fakeEvent + .apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "the_real_sender_key", + ) + } + + decryptedEvent.toValidDecryptedEvent() shouldNotBe null + decryptedEvent.copy(roomId = null).toValidDecryptedEvent() shouldBe null + decryptedEvent.copy(eventId = null).toValidDecryptedEvent() shouldBe null + } + + @Test + fun `A clear event is not a valid decrypted event`() { + val mockTextEvent = Event( + type = EventType.MESSAGE, + eventId = "eventId", + roomId = "!fooe:example.com", + content = mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + originServerTs = 1000, + senderId = "@anne:example.com", + ) + mockTextEvent.toValidDecryptedEvent() shouldBe null + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt new file mode 100644 index 00000000..0ae712bf --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2022 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 + +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.Test +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +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.store.IMXCryptoStore + +class EventEditValidatorTest { + + private val mockTextEvent = Event( + type = EventType.MESSAGE, + eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + originServerTs = 1000, + senderId = "@alice:example.com", + ) + + private val mockEdit = Event( + type = EventType.MESSAGE, + eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "body" to "* some message edited", + "msgtype" to "m.text", + "m.new_content" to mapOf( + "body" to "some message edited", + "msgtype" to "m.text" + ), + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ), + originServerTs = 2000, + senderId = "@alice:example.com", + ) + + @Test + fun `edit should be valid`() { + val mockCryptoStore = mockk<IMXCryptoStore>() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit(mockTextEvent, mockEdit) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + } + + @Test + fun `original event and replacement event must have the same sender`() { + val mockCryptoStore = mockk<IMXCryptoStore>() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent, + mockEdit.copy(senderId = "@bob:example.com") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `original event and replacement event must have the same room_id`() { + val mockCryptoStore = mockk<IMXCryptoStore>() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent, + mockEdit.copy(roomId = "!someotherroom") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy(roomId = "!someotherroom") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `replacement and original events must not have a state_key property`() { + val mockCryptoStore = mockk<IMXCryptoStore>() + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent, + mockEdit.copy(stateKey = "") + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + mockTextEvent.copy(stateKey = ""), + mockEdit + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `replacement event must have an new_content property`() { + val mockCryptoStore = mockk<IMXCryptoStore> { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk<CryptoDeviceInfo> { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit(mockTextEvent, mockEdit.copy( + content = mockEdit.content!!.toMutableMap().apply { + this.remove("m.new_content") + } + )) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "* some message edited", + "msgtype" to "m.text", + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + ) + ) + } + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `The original event must not itself have a rel_type of m_replace`() { + val mockCryptoStore = mockk<IMXCryptoStore> { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk<CryptoDeviceInfo> { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + mockTextEvent.copy( + content = mockTextEvent.content!!.toMutableMap().apply { + this["m.relates_to"] = mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + } + ), + mockEdit + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + validator + .validateEdit( + encryptedEvent.copy( + content = encryptedEvent.content!!.toMutableMap().apply { + put( + "m.relates_to", + mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + } + ).apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text", + ), + ) + ) + }, + encryptedEditEvent + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `valid e2ee edit`() { + val mockCryptoStore = mockk<IMXCryptoStore> { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk<CryptoDeviceInfo> { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent + ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + } + + @Test + fun `If the original event was encrypted, the replacement should be too`() { + val mockCryptoStore = mockk<IMXCryptoStore> { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk<CryptoDeviceInfo> { + every { userId } returns "@alice:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + mockEdit + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + @Test + fun `encrypted, original event and replacement event must have the same sender`() { + val mockCryptoStore = mockk<IMXCryptoStore> { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + mockk { + every { userId } returns "@bob:example.com" + } + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" + ) + } + + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + + // if sent fom a deleted device it should use the event claimed sender id + } + + @Test + fun `encrypted, sent fom a deleted device, original event and replacement event must have the same sender`() { + val mockCryptoStore = mockk<IMXCryptoStore> { + every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + mockk { + every { userId } returns "@alice:example.com" + } + every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + null + } + val validator = EventEditValidator(mockCryptoStore) + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy().apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" + ) + } + + ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + + validator + .validateEdit( + encryptedEvent, + encryptedEditEvent.copy( + senderId = "bob@example.com" + ).apply { + mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( + senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" + ) + } + + ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class + } + + private val encryptedEditEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + "session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ", + "device_id" to "QDHBLWOTSN", + "ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG...deLfCQOSPunSSNDFdWuDkB8Cg", + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ), + originServerTs = 2000, + senderId = "@alice:example.com", + ).apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "* some message edited", + "msgtype" to "m.text", + "m.new_content" to mapOf( + "body" to "some message edited", + "msgtype" to "m.text" + ), + "m.relates_to" to mapOf( + "rel_type" to "m.replace", + "event_id" to mockTextEvent.eventId + ) + ) + ), + senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + isSafe = true + ) + } + + private val encryptedEvent = Event( + type = EventType.ENCRYPTED, + eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ", + roomId = "!GXKhWsRwiWWvbQDBpe:example.com", + content = mapOf( + "algorithm" to "m.megolm.v1.aes-sha2", + "sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + "session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ", + "device_id" to "QDHBLWOTSN", + "ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG+4Vr...Yf0gYyhVWZY4SedF3fTMwkjmTuel4fwrmq", + ), + originServerTs = 2000, + senderId = "@alice:example.com", + ).apply { + mxDecryptionResult = OlmDecryptionResult( + payload = mapOf( + "type" to EventType.MESSAGE, + "content" to mapOf( + "body" to "some message", + "msgtype" to "m.text" + ), + ), + senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo", + isSafe = true + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt index 129d4963..bdd1fd9b 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt @@ -87,7 +87,7 @@ object PollEventsTestData { ) internal val A_POLL_START_EVENT = Event( - type = EventType.POLL_START.first(), + type = EventType.POLL_START.stable, eventId = AN_EVENT_ID, originServerTs = 1652435922563, senderId = A_USER_ID_1, @@ -96,7 +96,7 @@ object PollEventsTestData { ) internal val A_POLL_RESPONSE_EVENT = Event( - type = EventType.POLL_RESPONSE.first(), + type = EventType.POLL_RESPONSE.stable, eventId = AN_EVENT_ID, originServerTs = 1652435922563, senderId = A_USER_ID_1, @@ -105,7 +105,7 @@ object PollEventsTestData { ) internal val A_POLL_END_EVENT = Event( - type = EventType.POLL_END.first(), + type = EventType.POLL_END.stable, eventId = AN_EVENT_ID, originServerTs = 1652435922563, senderId = A_USER_ID_1, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt index d51ed773..4a107956 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt @@ -69,7 +69,7 @@ class DefaultGetActiveBeaconInfoForUserTaskTest { result shouldBeEqualTo currentStateEvent fakeStateEventDataSource.verifyGetStateEvent( roomId = params.roomId, - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + eventType = EventType.STATE_ROOM_BEACON_INFO.stable, stateKey = QueryStringValue.Equals(A_USER_ID) ) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index a01f5160..1f15a9be 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -53,7 +53,6 @@ private const val A_LATITUDE = 1.4 private const val A_LONGITUDE = 40.0 private const val AN_UNCERTAINTY = 5.0 private const val A_TIMEOUT = 15_000L -private const val A_DESCRIPTION = "description" private const val A_REASON = "reason" @ExperimentalCoroutinesApi @@ -143,7 +142,7 @@ internal class DefaultLocationSharingServiceTest { coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id") coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( @@ -157,7 +156,6 @@ internal class DefaultLocationSharingServiceTest { val expectedStartParams = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) coVerify { startLiveLocationShareTask.execute(expectedStartParams) } } @@ -168,7 +166,7 @@ internal class DefaultLocationSharingServiceTest { val error = Throwable() coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error) - val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error) val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( @@ -186,7 +184,7 @@ internal class DefaultLocationSharingServiceTest { coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION) + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( @@ -196,7 +194,6 @@ internal class DefaultLocationSharingServiceTest { val expectedStartParams = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) coVerify { startLiveLocationShareTask.execute(expectedStartParams) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt index aa882624..a5c126cf 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt @@ -34,7 +34,6 @@ import org.matrix.android.sdk.test.fakes.FakeSendStateTask private const val A_USER_ID = "user-id" private const val A_ROOM_ID = "room-id" private const val AN_EVENT_ID = "event-id" -private const val A_DESCRIPTION = "description" private const val A_TIMEOUT = 15_000L private const val AN_EPOCH = 1655210176L @@ -60,7 +59,6 @@ internal class DefaultStartLiveLocationShareTaskTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) fakeClock.givenEpoch(AN_EPOCH) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) @@ -69,7 +67,7 @@ internal class DefaultStartLiveLocationShareTaskTest { result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val expectedBeaconContent = MessageBeaconInfoContent( - body = A_DESCRIPTION, + body = "Live location", timeout = params.timeoutMillis, isLive = true, unstableTimestampMillis = AN_EPOCH @@ -77,7 +75,7 @@ internal class DefaultStartLiveLocationShareTaskTest { val expectedParams = SendStateTask.Params( roomId = params.roomId, stateKey = A_USER_ID, - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + eventType = EventType.STATE_ROOM_BEACON_INFO.stable, body = expectedBeaconContent ) fakeSendStateTask.verifyExecuteRetry( @@ -91,7 +89,6 @@ internal class DefaultStartLiveLocationShareTaskTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) fakeClock.givenEpoch(AN_EPOCH) fakeSendStateTask.givenExecuteRetryReturns("") @@ -106,7 +103,6 @@ internal class DefaultStartLiveLocationShareTaskTest { val params = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT, - description = A_DESCRIPTION ) fakeClock.givenEpoch(AN_EPOCH) val error = Throwable() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt index 1abf179c..a7adadfc 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt @@ -79,7 +79,7 @@ class DefaultStopLiveLocationShareTaskTest { val expectedSendParams = SendStateTask.Params( roomId = params.roomId, stateKey = A_USER_ID, - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + eventType = EventType.STATE_ROOM_BEACON_INFO.stable, body = expectedBeaconContent ) fakeSendStateTask.verifyExecuteRetry( diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt index 24d9c300..d6edb69d 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt @@ -79,7 +79,7 @@ class LiveLocationShareRedactionEventProcessorTest { @Test fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest { val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID) - val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first()) + val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.stable) fakeRealm.givenWhere<EventEntity>() .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) .givenFindFirst(redactedEventEntity) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt index b30428e5..19f58d69 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt @@ -23,8 +23,6 @@ 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.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary -import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody import org.matrix.android.sdk.api.session.room.sender.SenderInfo @@ -226,16 +224,14 @@ class LocalEchoEventFactoryTests { ).toMessageTextContent().toContent() } return TimelineEvent( - root = A_START_EVENT, + root = A_START_EVENT.copy( + type = EventType.MESSAGE, + content = textContent + ), localId = 1234, eventId = AN_EVENT_ID, displayIndex = 0, senderInfo = SenderInfo(A_USER_ID_1, A_USER_ID_1, true, null), - annotations = if (textContent != null) { - EventAnnotationsSummary( - editSummary = EditAggregatedSummary(latestContent = textContent, emptyList(), emptyList()) - ) - } else null ) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt new file mode 100644 index 00000000..20142368 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2022 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.sync + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder +import org.matrix.android.sdk.internal.session.filter.DefaultGetCurrentFilterTask +import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams +import org.matrix.android.sdk.test.fakes.FakeFilterRepository +import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource +import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask + +private const val A_FILTER_ID = "filter-id" +private val A_HOMESERVER_CAPABILITIES = HomeServerCapabilities() +private val A_SYNC_FILTER_PARAMS = SyncFilterParams( + lazyLoadMembersForMessageEvents = true, + lazyLoadMembersForStateEvents = true, + useThreadNotifications = true +) + +@ExperimentalCoroutinesApi +class DefaultGetCurrentFilterTaskTest { + + private val filterRepository = FakeFilterRepository() + private val homeServerCapabilitiesDataSource = FakeHomeServerCapabilitiesDataSource() + private val saveFilterTask = FakeSaveFilterTask() + + private val getCurrentFilterTask = DefaultGetCurrentFilterTask( + filterRepository = filterRepository, + homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource.instance, + saveFilterTask = saveFilterTask + ) + + @Test + fun `given no filter is stored, when execute, then executes task to save new filter`() = runTest { + filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS) + + homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES) + + filterRepository.givenFilterStored(null, null) + + getCurrentFilterTask.execute(Unit) + + val filter = SyncFilterBuilder() + .with(A_SYNC_FILTER_PARAMS) + .build(A_HOMESERVER_CAPABILITIES) + + saveFilterTask.verifyExecution(filter) + } + + @Test + fun `given filter is stored and didn't change, when execute, then returns stored filter id`() = runTest { + filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS) + + homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES) + + val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES) + filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString()) + + val result = getCurrentFilterTask.execute(Unit) + + result shouldBeEqualTo A_FILTER_ID + } + + @Test + fun `given filter is set and home server capabilities has changed, when execute, then executes task to save new filter`() = runTest { + filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS) + + homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES) + + val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES) + filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString()) + + val newHomeServerCapabilities = HomeServerCapabilities(canUseThreadReadReceiptsAndNotifications = true) + homeServerCapabilitiesDataSource.givenHomeServerCapabilities(newHomeServerCapabilities) + val newFilter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(newHomeServerCapabilities) + + getCurrentFilterTask.execute(Unit) + + saveFilterTask.verifyExecution(newFilter) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt new file mode 100644 index 00000000..b8225f21 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2022 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.test.fakes + +import io.mockk.coEvery +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.filter.FilterRepository +import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams + +internal class FakeFilterRepository : FilterRepository by mockk() { + + fun givenFilterStored(filterId: String?, filterBody: String?) { + coEvery { getStoredSyncFilterId() } returns filterId + coEvery { getStoredSyncFilterBody() } returns filterBody + } + + fun givenFilterParamsAreStored(syncFilterParams: SyncFilterParams?) { + coEvery { getStoredFilterParams() } returns syncFilterParams + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt new file mode 100644 index 00000000..9a56a599 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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.test.fakes + +import io.mockk.every +import io.mockk.mockk +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource + +internal class FakeHomeServerCapabilitiesDataSource { + val instance = mockk<HomeServerCapabilitiesDataSource>() + + fun givenHomeServerCapabilities(homeServerCapabilities: HomeServerCapabilities) { + every { instance.getHomeServerCapabilities() } returns homeServerCapabilities + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 93999458..76ede759 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -38,9 +38,9 @@ internal class FakeMonarchy { init { mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt") coEvery { - instance.awaitTransaction(any<suspend (Realm) -> Any>()) - } coAnswers { - secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance) + instance.awaitTransaction(any<(Realm) -> Any>()) + } answers { + secondArg<(Realm) -> Any>().invoke(fakeRealm.instance) } coEvery { instance.doWithRealm(any()) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt index 15a9823c..9ad70322 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.test.fakes import io.mockk.coEvery import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.slot import io.realm.Realm import io.realm.RealmConfiguration import org.matrix.android.sdk.internal.database.awaitTransaction @@ -33,9 +32,8 @@ internal class FakeRealmConfiguration { val instance = mockk<RealmConfiguration>() fun <T> givenAwaitTransaction(realm: Realm) { - val transaction = slot<suspend (Realm) -> T>() - coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { - secondArg<suspend (Realm) -> T>().invoke(realm) + coEvery { awaitTransaction(instance, any<(Realm) -> T>()) } answers { + secondArg<(Realm) -> T>().invoke(realm) } } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt new file mode 100644 index 00000000..40bee227 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2022 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.test.fakes + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.slot +import org.amshove.kluent.shouldBeEqualTo +import org.matrix.android.sdk.internal.session.filter.Filter +import org.matrix.android.sdk.internal.session.filter.SaveFilterTask +import java.util.UUID + +internal class FakeSaveFilterTask : SaveFilterTask by mockk() { + + init { + coEvery { execute(any()) } returns UUID.randomUUID().toString() + } + + fun verifyExecution(filter: Filter) { + val slot = slot<SaveFilterTask.Params>() + coVerify { execute(capture(slot)) } + val params = slot.captured + params.filter shouldBeEqualTo filter + } +} -- GitLab