Merge branch 'release/0.22.0'

This commit is contained in:
Benoit Marty 2020-06-15 23:27:49 +02:00
commit 8c98125755
345 changed files with 11674 additions and 1757 deletions

View File

@ -1,3 +1,34 @@
Changes in RiotX 0.22.0 (2020-06-15)
===================================================
Features ✨:
- Integration Manager and Widget support (#48)
- Send stickers (#51)
Improvements 🙌:
- New wording for notice when current user is the sender
- Hide "X made no changes" event by default in timeline (#1430)
- Hide left rooms in breadcrumbs (#766)
- Handle PowerLevel properly (#627)
- Correctly handle SSO login redirection
- SSO login is now performed in the default browser, or in Chrome Custom tab if available (#1400)
- Improve checking of homeserver version support (#1442)
- Add capability to add and remove a room from the favorites (#1217)
Bugfix 🐛:
- Switch theme is not fully taken into account without restarting the app
- Temporary fix to show error when user is creating an account on matrix.org with userId containing only digits (#1410)
- Reply composer overlay stays on screen too long after send (#1169)
- Fix navigation bar icon contrast on API in [21,27[ (#1342)
- Fix status bar icon contrast on API in [21,23[
- Wrong /query request (#1444)
- Make Credentials.homeServer optional because it is deprecated (#1443)
- Fix issue on dark themes, after alert popup dismiss
Other changes:
- Send plain text in the body of events containing formatted body, as per https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
- Update link to Modular url from "https://modular.im/" to "https://modular.im/services/matrix-hosting-riot" and open it using ChromeCustomTab
Changes in RiotX 0.21.0 (2020-05-28)
===================================================

View File

@ -59,6 +59,12 @@ It's recommended to run tests using an Android Emulator and not a real device. F
You can run all the tests in the `androidTest` folders.
It can be done using this command:
```bash
./gradlew vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest
```
## Stop Synapse
To stop Synapse, you can run the following commands:

View File

@ -2,7 +2,7 @@
This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver.
## Sign up flows
## Sign in flows
### Get the flow
@ -58,7 +58,7 @@ We get credential (200)
```json
{
"user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lr",
"home_server": "matrix.org",
"device_id": "GTVREDALBF",
"well_known": {
@ -117,7 +117,7 @@ We get the credentials (200)
```json
{
"user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3Jnfrfdegfszsefddvf",
"home_server": "matrix.org",
"device_id": "WBSREDASND",
"well_known": {
@ -145,12 +145,59 @@ Not supported yet in RiotX
"flows": [
{
"type": "m.login.sso"
},
{
"type": "m.login.token"
}
]
}
```
In this case, the user can click on "Sign in with SSO" and the web screen will be displayed on the page `https://homeserver.with.sso/_matrix/static/client/login/` and the credentials will be passed back to the native code through the JS bridge
In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=riotx%3A%2F%2Friotx
The parameter `redirectUrl` is set to `riotx://riotx`.
ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs)
The browser will then take care of the SSO login, which may include creating a third party account, entering an email, settings a display name, or any other possibilities.
During the process, user may be asked to validate an email by clicking on a link it contains. The link has to be opened in the browser which initiates the authentication. This is why we cannot use WebView anymore.
Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken`
> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
This navigation is intercepted by RiotX by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
```json
{
"type": "m.login.token",
"token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"
}
```
We get the credentials (200)
```json
{
"user_id": "@alice:homeserver.with.sso",
"access_token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVyIGtleQowMDEwY2lkIGdlbiA9IDEKMDAyY2NpZCB1c2",
"home_server": "homeserver.with.sso",
"device_id": "DETBTVAHCH",
"well_known": {
"m.homeserver": {
"base_url": "https:\/\/homeserver.with.sso\/"
},
"m.identity_server": {
"base_url": "https:\/\/vector.im"
}
}
}
```
## Reset password

View File

@ -16,6 +16,7 @@
package im.vector.matrix.rx
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
@ -61,7 +62,7 @@ class RxRoom(private val room: Room) {
}
}
fun liveStateEvent(eventType: String, stateKey: String): Observable<Optional<Event>> {
fun liveStateEvent(eventType: String, stateKey: QueryStringValue): Observable<Optional<Event>> {
return room.getStateEventLive(eventType, stateKey).asObservable()
.startWithCallable {
room.getStateEvent(eventType, stateKey).toOptional()

View File

@ -17,6 +17,7 @@
package im.vector.matrix.rx
import androidx.paging.PagedList
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
@ -28,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
@ -54,10 +56,10 @@ class RxSession(private val session: Session) {
}
}
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
return session.getBreadcrumbsLive().asObservable()
fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
return session.getBreadcrumbsLive(queryParams).asObservable()
.startWithCallable {
session.getBreadcrumbs()
session.getBreadcrumbs(queryParams)
}
}
@ -151,6 +153,18 @@ class RxSession(private val session: Session) {
session.getAccountDataEvents(types)
}
}
fun liveRoomWidgets(
roomId: String,
widgetId: QueryStringValue,
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): Observable<List<Widget>> {
return session.widgetService().getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes).asObservable()
.startWithCallable {
session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
}
}
}
fun Session.rx(): RxSession {

View File

@ -0,0 +1,278 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.room.send
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.commonmark.renderer.text.TextContentRenderer
import org.junit.Assert.assertEquals
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
/**
* It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild,
* we can add more tests to cover the edge cases.
* Some tests are suffixed with `_not_passing`, maybe one day we will fix them...
* Riot-Web should be used as a reference for expected results, but not always. Especially Riot-Web add lots of `\n` in the
* formatted body, which is quite useless.
* Also Riot-Web does not provide plain text body when formatted text is provided. The body contains what the user has entered.
* See https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
*/
@Suppress("SpellCheckingInspection")
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class MarkdownParserTest : InstrumentedTest {
/**
* Create the same parser than in the RoomModule
*/
private val markdownParser = MarkdownParser(
Parser.builder().build(),
HtmlRenderer.builder().build(),
TextContentRenderer.builder().build()
)
@Test
fun parseNoMarkdown() {
testIdentity("")
testIdentity("a")
testIdentity("1")
testIdentity("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " +
"dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" +
"modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pari" +
"atur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
}
@Test
fun parseSpaces() {
testIdentity(" ")
testIdentity(" ")
testIdentity("\n")
}
@Test
fun parseNewLines() {
testIdentity("line1\nline2")
testIdentity("line1\nline2\nline3")
}
@Test
fun parseBold() {
testType(
name = "bold",
markdownPattern = "**",
htmlExpectedTag = "strong"
)
}
@Test
fun parseItalic() {
testType(
name = "italic",
markdownPattern = "*",
htmlExpectedTag = "em"
)
}
@Test
fun parseItalic2() {
// Riot-Web format
"_italic_".let { markdownParser.parse(it) }.expect("italic", "<em>italic</em>")
}
/**
* Note: the test is not passing, it does not work on Riot-Web neither
*/
@Test
fun parseStrike_not_passing() {
testType(
name = "strike",
markdownPattern = "~~",
htmlExpectedTag = "del"
)
}
@Test
fun parseCode() {
testType(
name = "code",
markdownPattern = "`",
htmlExpectedTag = "code",
plainTextPrefix = "\"",
plainTextSuffix = "\""
)
}
@Test
fun parseCode2() {
testType(
name = "code",
markdownPattern = "``",
htmlExpectedTag = "code",
plainTextPrefix = "\"",
plainTextSuffix = "\""
)
}
@Test
fun parseCode3() {
testType(
name = "code",
markdownPattern = "```",
htmlExpectedTag = "code",
plainTextPrefix = "\"",
plainTextSuffix = "\""
)
}
@Test
fun parseUnorderedList() {
"- item1".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li></ul>") }
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li><li>item2</li></ul>") }
}
@Test
fun parseOrderedList() {
"1. item1".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li></ol>") }
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li><li>item2</li></ol>") }
}
@Test
fun parseHorizontalLine() {
"---".let { markdownParser.parse(it) }.expect("***", "<hr />")
}
@Test
fun parseH2AndContent() {
"a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "<h2>a</h2><p>b</p>")
}
@Test
fun parseQuote() {
"> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "<blockquote><p>quoted</p></blockquote>")
}
@Test
fun parseQuote_not_passing() {
"> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "<blockquote><p>quoted<br/>line2</p></blockquote>")
}
@Test
fun parseBoldItalic() {
"*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "<em>italic</em> <strong>bold</strong>")
"**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "<strong>bold</strong> <em>italic</em>")
}
@Test
fun parseHead() {
"# head1".let { markdownParser.parse(it) }.expect("head1", "<h1>head1</h1>")
"## head2".let { markdownParser.parse(it) }.expect("head2", "<h2>head2</h2>")
"### head3".let { markdownParser.parse(it) }.expect("head3", "<h3>head3</h3>")
"#### head4".let { markdownParser.parse(it) }.expect("head4", "<h4>head4</h4>")
"##### head5".let { markdownParser.parse(it) }.expect("head5", "<h5>head5</h5>")
"###### head6".let { markdownParser.parse(it) }.expect("head6", "<h6>head6</h6>")
}
@Test
fun parseHeads() {
"# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "<h1>head1</h1><h1>head2</h1>")
}
@Test
fun parseBoldNewLines_not_passing() {
"**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "<strong>bold</strong><br />line2")
}
@Test
fun parseLinks() {
"[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """<a href="target">link</a>""")
}
@Test
fun parseParagraph() {
"# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "<h1>head</h1><p>content</p>")
}
private fun testIdentity(text: String) {
markdownParser.parse(text).expect(text, null)
}
private fun testType(name: String,
markdownPattern: String,
htmlExpectedTag: String,
plainTextPrefix: String = "",
plainTextSuffix: String = "") {
// Test simple case
"$markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>")
// Test twice the same tag
"$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix and $plainTextPrefix$name bis$plainTextSuffix",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
val textBefore = "a"
val textAfter = "b"
// With sticked text before
"$textBefore$markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>")
// With text before and space
"$textBefore $markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
// With sticked text after
"$markdownPattern$name$markdownPattern$textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
// With space and text after
"$markdownPattern$name$markdownPattern $textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
// With sticked text before and text after
"$textBefore$markdownPattern$name$markdownPattern$textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter",
expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
// With text before and after, with spaces
"$textBefore $markdownPattern$name$markdownPattern $textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter",
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
}
private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) {
assertEquals("TextContent are not identical", TextContent(expectedText, expectedFormattedText), this)
}
}

View File

@ -0,0 +1,54 @@
var android_widget_events = {};
var sendObjectMessageToRiotAndroid = function(parameters) {
Android.onWidgetEvent(JSON.stringify(parameters));
};
var onWidgetMessageToRiotAndroid = function(event) {
/* Use an internal "_id" field for matching onMessage events and requests
_id was originally used by the Modular API. Keep it */
if (!event.data._id) {
/* The Matrix Widget API v2 spec says:
"The requestId field should be unique and included in all requests" */
event.data._id = event.data.requestId;
}
/* Make sure to have one id */
if (!event.data._id) {
event.data._id = Date.now() + "-" + Math.random().toString(36);
}
console.log("onWidgetMessageToRiotAndroid " + event.data._id);
if (android_widget_events[event.data._id]) {
console.log("onWidgetMessageToRiotAndroid : already managed");
return;
}
if (!event.origin) {
event.origin = event.originalEvent.origin;
}
android_widget_events[event.data._id] = event;
console.log("onWidgetMessageToRiotAndroid : manage " + event.data);
sendObjectMessageToRiotAndroid({'event.data': event.data});
};
var sendResponseFromRiotAndroid = function(eventId, res) {
var event = android_widget_events[eventId];
console.log("sendResponseFromRiotAndroid to " + event.data.action + " for "+ eventId + ": " + JSON.stringify(res));
var data = JSON.parse(JSON.stringify(event.data));
data.response = res;
console.log("sendResponseFromRiotAndroid ---> " + data);
event.source.postMessage(data, event.origin);
android_widget_events[eventId] = true;
console.log("sendResponseFromRiotAndroid to done");
};
window.addEventListener('message', onWidgetMessageToRiotAndroid, false);

View File

@ -22,6 +22,15 @@ import java.net.Proxy
data class MatrixConfiguration(
val applicationFlavor: String = "Default-application-flavor",
val cryptoConfig: MXCryptoConfig = MXCryptoConfig(),
val integrationUIUrl: String = "https://scalar.vector.im/",
val integrationRestUrl: String = "https://scalar.vector.im/api",
val integrationWidgetUrls: List<String> = listOf(
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
),
/**
* Optional proxy to connect to the matrix servers
* You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port)

View File

@ -32,6 +32,6 @@ const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/"
* Path to use when the client want to connect using SSO
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
*/
const val SSO_FALLBACK_PATH = "/_matrix/client/r0/login/sso/redirect"
const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
const val SSO_REDIRECT_URL_PARAM = "redirectUrl"

View File

@ -45,7 +45,7 @@ data class Credentials(
* @Deprecated. Clients should extract the server_name from user_id (by splitting at the first colon)
* if they require it. Note also that homeserver is not spelt this way.
*/
@Json(name = "home_server") val homeServer: String,
@Json(name = "home_server") val homeServer: String?,
/**
* ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified.
*/

View File

@ -20,6 +20,7 @@ import android.net.Uri
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig.Builder
import im.vector.matrix.android.internal.network.ssl.Fingerprint
import im.vector.matrix.android.internal.util.ensureTrailingSlash
import okhttp3.CipherSuite
import okhttp3.TlsVersion
@ -71,15 +72,11 @@ data class HomeServerConnectionConfig(
throw RuntimeException("Invalid home server URI: " + hsUri)
}
// ensure trailing /
homeServerUri = if (!hsUri.toString().endsWith("/")) {
try {
val url = hsUri.toString()
Uri.parse("$url/")
} catch (e: Exception) {
throw RuntimeException("Invalid home server URI: $hsUri")
}
} else {
hsUri
val hsString = hsUri.toString().ensureTrailingSlash()
homeServerUri = try {
Uri.parse(hsString)
} catch (e: Exception) {
throw RuntimeException("Invalid home server URI: $hsUri")
}
return this
}
@ -97,15 +94,11 @@ data class HomeServerConnectionConfig(
throw RuntimeException("Invalid identity server URI: $identityServerUri")
}
// ensure trailing /
if (!identityServerUri.toString().endsWith("/")) {
try {
val url = identityServerUri.toString()
this.identityServerUri = Uri.parse("$url/")
} catch (e: Exception) {
throw RuntimeException("Invalid identity server URI: $identityServerUri")
}
} else {
this.identityServerUri = identityServerUri
val isString = identityServerUri.toString().ensureTrailingSlash()
this.identityServerUri = try {
Uri.parse(isString)
} catch (e: Exception) {
throw RuntimeException("Invalid identity server URI: $identityServerUri")
}
return this
}

View File

@ -16,12 +16,10 @@
package im.vector.matrix.android.api.auth.data
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
// Either a LoginFlowResponse, or an error if the homeserver is outdated
// Either a list of supported login types, or an error if the homeserver is outdated
sealed class LoginFlowResult {
data class Success(
val loginFlowResponse: LoginFlowResponse,
val supportedLoginTypes: List<String>,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String
) : LoginFlowResult()

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.data
package im.vector.matrix.android.api.auth.data
object LoginFlowTypes {
const val PASSWORD = "m.login.password"

View File

@ -54,30 +54,4 @@ data class WellKnown(
@Json(name = "m.integrations")
val integrations: JsonDict? = null
) {
/**
* Returns the list of integration managers proposed
*/
fun getIntegrationManagers(): List<WellKnownManagerConfig> {
val managers = ArrayList<WellKnownManagerConfig>()
integrations?.get("managers")?.let {
(it as? ArrayList<*>)?.let { configs ->
configs.forEach { config ->
(config as? Map<*, *>)?.let { map ->
val apiUrl = map["api_url"] as? String
val uiUrl = map["ui_url"] as? String ?: apiUrl
if (apiUrl != null
&& apiUrl.startsWith("https://")
&& uiUrl!!.startsWith("https://")) {
managers.add(WellKnownManagerConfig(
apiUrl = apiUrl,
uiUrl = uiUrl
))
}
}
}
}
}
return managers
}
}
)

View File

@ -34,6 +34,12 @@ interface LoginWizard {
deviceName: String,
callback: MatrixCallback<Session>): Cancelable
/**
* Exchange a login token to an access token
*/
fun loginWithToken(loginToken: String,
callback: MatrixCallback<Session>): Cancelable
/**
* Reset user password
*/

View File

@ -21,15 +21,12 @@ sealed class Stage(open val mandatory: Boolean) {
// m.login.recaptcha
data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
// m.login.oauth2
// m.login.email.identity
data class Email(override val mandatory: Boolean) : Stage(mandatory)
// m.login.msisdn
data class Msisdn(override val mandatory: Boolean) : Stage(mandatory)
// m.login.token
// m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username
// and a password, the dummy stage has to be done
data class Dummy(override val mandatory: Boolean) : Stage(mandatory)

View File

@ -39,6 +39,6 @@ class SenderNotificationPermissionCondition(
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
val powerLevelsHelper = PowerLevelsHelper(powerLevels)
return event.senderId != null && powerLevelsHelper.getUserPowerLevel(event.senderId) >= powerLevelsHelper.notificationLevel(key)
return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevelsHelper.notificationLevel(key)
}
}

View File

@ -25,8 +25,8 @@ sealed class QueryStringValue {
object IsNotNull : QueryStringValue()
object IsEmpty : QueryStringValue()
object IsNotEmpty : QueryStringValue()
data class Equals(val string: String, val case: Case) : QueryStringValue()
data class Contains(val string: String, val case: Case) : QueryStringValue()
data class Equals(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue()
data class Contains(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue()
enum class Case {
SENSITIVE,

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.identity.IdentityService
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
@ -42,6 +43,7 @@ import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService
/**
* This interface defines interactions with a session.
@ -153,6 +155,16 @@ interface Session :
*/
fun identityService(): IdentityService
/**
* Returns the widget service associated with the session
*/
fun widgetService(): WidgetService
/**
* Returns the integration manager service associated with the session
*/
fun integrationManagerService(): IntegrationManagerService
/**
* Add a listener to the session.
* @param listener the listener to add.

View File

@ -98,7 +98,7 @@ data class Event(
* @return true if event is state event.
*/
fun isStateEvent(): Boolean {
return EventType.isStateEvent(getClearType())
return stateKey != null
}
// ==============================================================================================================
@ -162,6 +162,8 @@ data class Event(
*/
fun isRedactedBySameUser() = senderId == unsignedData?.redactedEvent?.senderId
fun resolvedPrevContent(): Content? = prevContent ?: unsignedData?.prevContent
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

View File

@ -38,6 +38,8 @@ object EventType {
// State Events
const val STATE_ROOM_WIDGET_LEGACY = "im.vector.modular.widgets"
const val STATE_ROOM_WIDGET = "m.widget"
const val STATE_ROOM_NAME = "m.room.name"
const val STATE_ROOM_TOPIC = "m.room.topic"
const val STATE_ROOM_AVATAR = "m.room.avatar"
@ -84,29 +86,6 @@ object EventType {
// Unwedging
internal const val DUMMY = "m.dummy"
private val STATE_EVENTS = listOf(
STATE_ROOM_NAME,
STATE_ROOM_TOPIC,
STATE_ROOM_AVATAR,
STATE_ROOM_MEMBER,
STATE_ROOM_THIRD_PARTY_INVITE,
STATE_ROOM_CREATE,
STATE_ROOM_JOIN_RULES,
STATE_ROOM_GUEST_ACCESS,
STATE_ROOM_POWER_LEVELS,
STATE_ROOM_ALIASES,
STATE_ROOM_TOMBSTONE,
STATE_ROOM_CANONICAL_ALIAS,
STATE_ROOM_HISTORY_VISIBILITY,
STATE_ROOM_RELATED_GROUPS,
STATE_ROOM_PINNED_EVENT,
STATE_ROOM_ENCRYPTION
)
fun isStateEvent(type: String): Boolean {
return STATE_EVENTS.contains(type)
}
fun isCallEvent(type: String): Boolean {
return type == CALL_INVITE
|| type == CALL_CANDIDATES

View File

@ -16,7 +16,9 @@
package im.vector.matrix.android.api.session.identity
sealed class IdentityServiceError : Throwable() {
import im.vector.matrix.android.api.failure.Failure
sealed class IdentityServiceError : Failure.FeatureFailure() {
object OutdatedIdentityServer : IdentityServiceError()
object OutdatedHomeServer : IdentityServiceError()
object NoIdentityServerConfigured : IdentityServiceError()

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.integrationmanager
/**
* This class holds configuration of integration manager.
*/
data class IntegrationManagerConfig(
val uiUrl: String,
val restUrl: String,
val kind: Kind
) {
// Order matters, first is preferred
/**
* The kind of config, it will reflect where the data is coming from.
*/
enum class Kind {
/**
* Defined in UserAccountData
*/
ACCOUNT,
/**
* Defined in Wellknown
*/
HOMESERVER,
/**
* Fallback value, hardcoded by the SDK
*/
DEFAULT
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.integrationmanager
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
/**
* This is the entry point to manage integration. You can grab an instance of this service through an active session.
*/
interface IntegrationManagerService {
/**
* This listener allow you to observe change related to integrations.
*/
interface Listener {
/**
* Is called whenever integration is enabled or disabled, comes from user account data.
*/
fun onIsEnabledChanged(enabled: Boolean) {
// No-op
}
/**
* Is called whenever configs from user account data or wellknown are updated.
*/
fun onConfigurationChanged(configs: List<IntegrationManagerConfig>) {
// No-op
}
/**
* Is called whenever widget permissions from user account data are updated.
*/
fun onWidgetPermissionsChanged(widgets: Map<String, Boolean>) {
// No-op
}
}
/**
* Adds a listener to observe changes.
*/
fun addListener(listener: Listener)
/**
* Removes a previously added listener.
*/
fun removeListener(listener: Listener)
/**
* Return the list of current configurations, sorted by kind. First one is preferred.
* See [IntegrationManagerConfig.Kind]
*/
fun getOrderedConfigs(): List<IntegrationManagerConfig>
/**
* Return the preferred current configuration.
* See [IntegrationManagerConfig.Kind]
*/
fun getPreferredConfig(): IntegrationManagerConfig
/**
* Returns true if integration is enabled, false otherwise.
*/
fun isIntegrationEnabled(): Boolean
/**
* Offers to enable or disable the integration.
* @param enable the param to change
* @param callback the matrix callback to listen for result.
* @return Cancelable
*/
fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable
/**
* Offers to allow or disallow a widget.
* @param stateEventId the eventId of the state event defining the widget.
* @param allowed the param to change
* @param callback the matrix callback to listen for result.
* @return Cancelable
*/
fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable
/**
* Returns true if the widget is allowed, false otherwise.
* @param stateEventId the eventId of the state event defining the widget.
*/
fun isWidgetAllowed(stateEventId: String): Boolean
/**
* Offers to allow or disallow a native widget domain.
* @param widgetType the widget type to check for
* @param domain the domain to check for
*/
fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable
/**
* Returns true if the widget domain is allowed, false otherwise.
* @param widgetType the widget type to check for
* @param domain the domain to check for
*/
fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean
}

View File

@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.room.reporting.ReportingService
import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.tags.TagsService
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.typing.TypingService
import im.vector.matrix.android.api.session.room.uploads.UploadsService
@ -41,6 +42,7 @@ interface Room :
DraftService,
ReadService,
TypingService,
TagsService,
MembershipService,
StateService,
UploadsService,

View File

@ -73,15 +73,17 @@ interface RoomService {
/**
* Get a snapshot list of Breadcrumbs
* @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance.
* @return the immutable list of [RoomSummary]
*/
fun getBreadcrumbs(): List<RoomSummary>
fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary>
/**
* Get a live list of Breadcrumbs
* @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance.
* @return the [LiveData] of [RoomSummary]
*/
fun getBreadcrumbsLive(): LiveData<List<RoomSummary>>
fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>>
/**
* Inform the Matrix SDK that a room is displayed.

View File

@ -63,6 +63,27 @@ interface MembershipService {
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Ban a user from the room
*/
fun ban(userId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Unban a user from the room
*/
fun unban(userId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Kick a user from the room
*/
fun kick(userId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Join the room, or accept an invitation.
*/

View File

@ -44,6 +44,10 @@ enum class Membership(val value: String) {
return this == KNOCK || this == LEAVE || this == BAN
}
fun isActive(): Boolean {
return activeMemberships().contains(this)
}
companion object {
fun activeMemberships(): List<Membership> {
return listOf(INVITE, JOIN)

View File

@ -18,21 +18,21 @@ package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants
import im.vector.matrix.android.api.session.room.powerlevels.Role
/**
* Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content.
*/
@JsonClass(generateAdapter = true)
data class PowerLevelsContent(
@Json(name = "ban") val ban: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "kick") val kick: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "invite") val invite: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "redact") val redact: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "events_default") val eventsDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL,
@Json(name = "ban") val ban: Int = Role.Moderator.value,
@Json(name = "kick") val kick: Int = Role.Moderator.value,
@Json(name = "invite") val invite: Int = Role.Moderator.value,
@Json(name = "redact") val redact: Int = Role.Moderator.value,
@Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
@Json(name = "events") val events: MutableMap<String, Int> = HashMap(),
@Json(name = "users_default") val usersDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL,
@Json(name = "users_default") val usersDefault: Int = Role.Default.value,
@Json(name = "users") val users: MutableMap<String, Int> = HashMap(),
@Json(name = "state_default") val stateDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
@Json(name = "notifications") val notifications: Map<String, Any> = HashMap()
)

View File

@ -59,6 +59,9 @@ data class RoomSummary constructor(
val hasNewMessages: Boolean
get() = notificationCount != 0
val isFavorite: Boolean
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
companion object {
const val NOT_IN_BREADCRUMBS = -1
}

View File

@ -22,9 +22,8 @@ data class RoomTag(
) {
companion object {
val ROOM_TAG_FAVOURITE = "m.favourite"
val ROOM_TAG_LOW_PRIORITY = "m.lowpriority"
val ROOM_TAG_NO_TAG = "m.recent"
val ROOM_TAG_SERVER_NOTICE = "m.server_notice"
const val ROOM_TAG_FAVOURITE = "m.favourite"
const val ROOM_TAG_LOW_PRIORITY = "m.lowpriority"
const val ROOM_TAG_SERVER_NOTICE = "m.server_notice"
}
}

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.session.room.powerlevels
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
/**
@ -31,32 +30,84 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @param userId the user id
* @return the power level
*/
fun getUserPowerLevel(userId: String): Int {
fun getUserPowerLevelValue(userId: String): Int {
return powerLevelsContent.users.getOrElse(userId) {
powerLevelsContent.usersDefault
}
}
/**
* Returns the user power level of a dedicated user Id
*
* @param userId the user id
* @return the power level
*/
fun getUserRole(userId: String): Role {
val value = getUserPowerLevelValue(userId)
return Role.fromValue(value, powerLevelsContent.eventsDefault)
}
/**
* Tell if an user can send an event of a certain type
*
* @param userId the id of the user to check for.
* @param isState true if the event is a state event (ie. state key is not null)
* @param eventType the event type to check for
* @param userId the user id
* @return true if the user can send this type of event
*/
fun isAllowedToSend(eventType: String, userId: String): Boolean {
return if (eventType.isNotEmpty() && userId.isNotEmpty()) {
val powerLevel = getUserPowerLevel(userId)
fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean {
return if (userId.isNotEmpty()) {
val powerLevel = getUserPowerLevelValue(userId)
val minimumPowerLevel = powerLevelsContent.events[eventType]
?: if (EventType.isStateEvent(eventType)) {
powerLevelsContent.stateDefault
} else {
powerLevelsContent.eventsDefault
}
?: if (isState) {
powerLevelsContent.stateDefault
} else {
powerLevelsContent.eventsDefault
}
powerLevel >= minimumPowerLevel
} else false
}
/**
* Check if the user have the necessary power level to invite
* @param userId the id of the user to check for.
* @return true if able to invite
*/
fun isUserAbleToInvite(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.invite
}
/**
* Check if the user have the necessary power level to ban
* @param userId the id of the user to check for.
* @return true if able to ban
*/
fun isUserAbleToBan(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.ban
}
/**
* Check if the user have the necessary power level to kick
* @param userId the id of the user to check for.
* @return true if able to kick
*/
fun isUserAbleToKick(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.kick
}
/**
* Check if the user have the necessary power level to redact
* @param userId the id of the user to check for.
* @return true if able to redact
*/
fun isUserAbleToRedact(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.redact
}
/**
* Get the notification level for a dedicated key.
*
@ -68,7 +119,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
// the first implementation was a string value
is String -> value.toInt()
is Int -> value
else -> PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL
else -> Role.Moderator.value
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.room.powerlevels
import androidx.annotation.StringRes
import im.vector.matrix.android.R
sealed class Role(open val value: Int, @StringRes val res: Int) : Comparable<Role> {
object Admin : Role(100, R.string.power_level_admin)
object Moderator : Role(50, R.string.power_level_moderator)
object Default : Role(0, R.string.power_level_default)
data class Custom(override val value: Int) : Role(value, R.string.power_level_custom)
override fun compareTo(other: Role): Int {
return value.compareTo(other.value)
}
companion object {
// Order matters, default value should be checked after defined roles
fun fromValue(value: Int, default: Int): Role {
return when (value) {
Admin.value -> Admin
Moderator.value -> Moderator
Default.value,
default -> Default
else -> Custom(value)
}
}
}
}

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.OptionItem
@ -28,6 +29,14 @@ import im.vector.matrix.android.api.util.Cancelable
*/
interface SendService {
/**
* Method to send a generic event asynchronously. If you want to send a state event, please use [StateService] instead.
* @param eventType the type of the event
* @param content the optional body as a json dict.
* @return a [Cancelable]
*/
fun sendEvent(eventType: String, content: Content?): Cancelable
/**
* Method to send a text message asynchronously.
* The text to send can be a Spannable and contains special spans (MatrixItemSpan) that will be translated

View File

@ -18,7 +18,10 @@ package im.vector.matrix.android.api.session.room.state
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
interface StateService {
@ -26,9 +29,15 @@ interface StateService {
/**
* Update the topic of the room
*/
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable
fun getStateEvent(eventType: String, stateKey: String): Event?
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable
fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>>
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<Optional<Event>>
fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): List<Event>
fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>>
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.room.tags
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
/**
* This interface defines methods to handle tags of a room. It's implemented at the room level.
*/
interface TagsService {
/**
* Add a tag to a room
*/
fun addTag(tag: String, order: Double?, callback: MatrixCallback<Unit>): Cancelable
/**
* Remove tag from a room
*/
fun deleteTag(tag: String, callback: MatrixCallback<Unit>): Cancelable
}

View File

@ -32,6 +32,10 @@ data class TimelineSettings(
* A flag to filter redacted events
*/
val filterRedacted: Boolean = false,
/**
* A flag to filter useless events, such as membership events without any change
*/
val filterUseless: Boolean = false,
/**
* A flag to filter by types. It should be used with [allowedTypes] field
*/
@ -44,5 +48,4 @@ data class TimelineSettings(
* If true, will build read receipts for each event.
*/
val buildReadReceipts: Boolean = true
)

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.widgets
import im.vector.matrix.android.api.failure.Failure
sealed class WidgetManagementFailure : Failure.FeatureFailure() {
object NotEnoughPower : WidgetManagementFailure()
object CreationFailed : WidgetManagementFailure()
data class TermsNotSignedException(val baseUrl: String, val token: String) : WidgetManagementFailure()
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.widgets
import android.webkit.WebView
import im.vector.matrix.android.api.util.JsonDict
import java.lang.reflect.Type
interface WidgetPostAPIMediator {
/**
* This initialize the webview to handle.
* It will add a JavaScript Interface.
* Please call [clearWebView] method when finished to clean the provided webview
*/
fun setWebView(webView: WebView)
/**
* Set handler to communicate with the widgetPostAPIMediator.
* Please remove the reference by passing null when finished.
*/
fun setHandler(handler: Handler?)
/**
* This clear the mediator by removing the JavaScript Interface and cleaning references.
*/
fun clearWebView()
/**
* Inject the necessary javascript into the configured WebView.
* Should be called after a web page has been loaded.
*/
fun injectAPI()
/**
* Send a boolean response
*
* @param response the response
* @param eventData the modular data
*/
fun sendBoolResponse(response: Boolean, eventData: JsonDict)
/**
* Send an integer response
*
* @param response the response
* @param eventData the modular data
*/
fun sendIntegerResponse(response: Int, eventData: JsonDict)
/**
* Send an object response
*
* @param klass the class of the response
* @param response the response
* @param eventData the modular data
*/
fun <T> sendObjectResponse(type: Type, response: T?, eventData: JsonDict)
/**
* Send success
*
* @param eventData the modular data
*/
fun sendSuccess(eventData: JsonDict)
/**
* Send an error
*
* @param message the error message
* @param eventData the modular data
*/
fun sendError(message: String, eventData: JsonDict)
interface Handler {
/**
* Triggered when a widget is posting
*/
fun handleWidgetRequest(eventData: JsonDict): Boolean
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.widgets
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.session.widgets.model.Widget
/**
* This is the entry point to manage widgets. You can grab an instance of this service through an active session.
*/
interface WidgetService {
/**
* Returns an instance of [WidgetURLFormatter].
*/
fun getWidgetURLFormatter(): WidgetURLFormatter
/**
* Returns an instance of [WidgetPostAPIMediator].
* This is to be used for "admin" widgets so you can interact through JS.
*/
fun getWidgetPostAPIMediator(): WidgetPostAPIMediator
/**
* Returns the current room widgets defined through state events.
* Some widgets can be deactivated, so be sure to check for isActive if needed.
*
* @param roomId the room where you want to fetch widgets
* @param widgetId if you want to fetch for some particular widget
* @param widgetTypes if you want to filter some widget type.
* @param excludedTypes if you want to exclude some widget type.
*/
fun getRoomWidgets(
roomId: String,
widgetId: QueryStringValue = QueryStringValue.NoCondition,
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget>
/**
* Returns the live room widgets so you can listen to them.
* Some widgets can be deactivated, so be sure to check for isActive.
*
* @param roomId the room where you want to fetch widgets
* @param widgetId if you want to fetch for some particular widget
* @param widgetTypes if you want to filter some widget type.
* @param excludedTypes if you want to exclude some widget type.
*/
fun getRoomWidgetsLive(
roomId: String,
widgetId: QueryStringValue = QueryStringValue.NoCondition,
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>>
/**
* Returns the current user widgets.
* Some widgets can be deactivated, so be sure to check for isActive.
*
* @param widgetTypes if you want to filter some widget type.
* @param excludedTypes if you want to exclude some widget type.
*/
fun getUserWidgets(
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): List<Widget>
/**
* Returns the live user widgets so you can listen to them.
* Some widgets can be deactivated, so be sure to check for isActive.
*
* @param widgetTypes if you want to filter some widget type.
* @param excludedTypes if you want to exclude some widget type.
*/
fun getUserWidgetsLive(
widgetTypes: Set<String>? = null,
excludedTypes: Set<String>? = null
): LiveData<List<Widget>>
/**
* Creates a new widget in a room. It makes sure you have the rights to handle this.
*
* @param roomId: the room where you want to deactivate the widget.
* @param widgetId: the widget to deactivate.
* @param callback the matrix callback to listen for result.
* @return Cancelable
*/
fun createRoomWidget(roomId: String, widgetId: String, content: Content, callback: MatrixCallback<Widget>): Cancelable
/**
* Deactivate a widget in a room. It makes sure you have the rights to handle this.
*
* @param roomId: the room where you want to deactivate the widget.
* @param widgetId: the widget to deactivate.
* @param callback the matrix callback to listen for result.
* @return Cancelable
*/
fun destroyRoomWidget(roomId: String, widgetId: String, callback: MatrixCallback<Unit>): Cancelable
/**
* Returns true if you can add/remove widgets. It goes through
* @param roomId the room where you want to administrate widgets.
*/
fun hasPermissionsToHandleWidgets(roomId: String): Boolean
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.widgets
interface WidgetURLFormatter {
/**
* Takes care of fetching a scalar token if required and build the final url.
* This methods can throw, you should take care of handling failure.
*
* @param baseUrl the baseUrl which will be checked for scalar token
* @param params additional params you want to append to the base url.
* @param forceFetchScalarToken if true, you will force to fetch a new scalar token
* from the server (only if the base url is whitelisted)
* @param bypassWhitelist if true, the base url will be considered as whitelisted
*/
suspend fun format(
baseUrl: String,
params: Map<String, String> = emptyMap(),
forceFetchScalarToken: Boolean = false,
bypassWhitelist: Boolean
): String
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.widgets.model
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.sender.SenderInfo
data class Widget(
val widgetContent: WidgetContent,
val event: Event,
val widgetId: String,
val senderInfo: SenderInfo?,
val isAddedByMe: Boolean,
val computedUrl: String?,
val type: WidgetType
) {
val isActive = widgetContent.isActive()
val name = widgetContent.getHumanName()
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.widgets.model
import android.annotation.SuppressLint
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict
@JsonClass(generateAdapter = true)
data class WidgetContent(
@Json(name = "creatorUserId") val creatorUserId: String? = null,
@Json(name = "id") val id: String? = null,
@Json(name = "type") val type: String? = null,
@Json(name = "url") val url: String? = null,
@Json(name = "name") val name: String? = null,
@Json(name = "data") val data: JsonDict = emptyMap(),
@Json(name = "waitForIframeLoad") val waitForIframeLoad: Boolean = false
) {
fun isActive() = type != null && url != null
@SuppressLint("DefaultLocale")
fun getHumanName(): String {
return (name ?: type ?: "").capitalize()
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.widgets.model
sealed class WidgetType(open val preferred: String, open val legacy: String = preferred) {
object Jitsi : WidgetType("m.jitsi", "jitsi")
object TradingView : WidgetType("m.tradingview")
object Spotify : WidgetType("m.spotify")
object Video : WidgetType("m.video")
object GoogleDoc : WidgetType("m.googledoc")
object GoogleCalendar : WidgetType("m.googlecalendar")
object Etherpad : WidgetType("m.etherpad")
object StickerPicker : WidgetType("m.stickerpicker")
object Grafana : WidgetType("m.grafana")
object Custom : WidgetType("m.custom")
object IntegrationManager : WidgetType("m.integration_manager")
data class Fallback(override val preferred: String) : WidgetType(preferred)
fun matches(type: String?): Boolean {
return type == preferred || type == legacy
}
fun values(): Set<String> {
return setOf(preferred, legacy)
}
companion object {
private val DEFINED_TYPES = listOf(
Jitsi,
TradingView,
Spotify,
Video,
GoogleDoc,
GoogleCalendar,
Etherpad,
StickerPicker,
Grafana,
Custom,
IntegrationManager
)
fun fromString(type: String): WidgetType {
val matchingType = DEFINED_TYPES.firstOrNull {
it.matches(type)
}
return matchingType ?: Fallback(type)
}
}
}

View File

@ -47,6 +47,10 @@ data class Optional<T : Any> constructor(private val value: T?) {
fun <T : Any> from(value: T?): Optional<T> {
return Optional(value)
}
fun <T: Any> empty(): Optional<T> {
return Optional(null)
}
}
}

View File

@ -17,16 +17,17 @@
package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.RiotConfig
import im.vector.matrix.android.internal.auth.data.TokenLoginParams
import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
import im.vector.matrix.android.internal.auth.registration.RegistrationParams
import im.vector.matrix.android.internal.auth.registration.SuccessResult
import im.vector.matrix.android.internal.auth.registration.ValidationCodeBody
import im.vector.matrix.android.internal.auth.version.Versions
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
@ -54,7 +55,7 @@ internal interface AuthAPI {
fun versions(): Call<Versions>
/**
* Register to the homeserver
* Register to the homeserver, or get error 401 with a RegistrationFlowResponse object if registration is incomplete
* Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
@ -91,6 +92,11 @@ internal interface AuthAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>
// Unfortunately we cannot use interface for @Body parameter, so I duplicate the method for the type TokenLoginParams
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun login(@Body loginParams: TokenLoginParams): Call<Credentials>
/**
* Ask the homeserver to reset the password associated with the provided email.
*/

View File

@ -23,9 +23,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.api.auth.data.isSupportedBySdk
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
@ -40,6 +37,9 @@ import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
import im.vector.matrix.android.internal.auth.login.DirectLoginTask
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
import im.vector.matrix.android.internal.auth.version.Versions
import im.vector.matrix.android.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.internal.auth.version.isSupportedBySdk
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
@ -236,7 +236,7 @@ internal class DefaultAuthenticationService @Inject constructor(
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
apiCall = authAPI.getLoginFlows()
}
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
} else {
// Not supported
LoginFlowResult.OutdatedHomeserver

View File

@ -20,7 +20,19 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LoginFlowResponse(
internal data class LoginFlowResponse(
/**
* The homeserver's supported login types
*/
@Json(name = "flows")
val flows: List<InteractiveAuthenticationFlow>
val flows: List<LoginFlow>?
)
@JsonClass(generateAdapter = true)
internal data class LoginFlow(
/**
* The login type. This is supplied as the type when logging in.
*/
@Json(name = "type")
val type: String?
)

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/**
* Ref:

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true)
internal data class TokenLoginParams(
@Json(name = "type") override val type: String = LoginFlowTypes.TOKEN,
@Json(name = "token") val token: String
) : LoginParams

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import im.vector.matrix.android.internal.auth.data.TokenLoginParams
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
@ -65,6 +66,22 @@ internal class DefaultLoginWizard(
}
}
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#handling-the-authentication-endpoint
*/
override fun loginWithToken(loginToken: String, callback: MatrixCallback<Session>): Cancelable {
return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) {
val loginParams = TokenLoginParams(
token = loginToken
)
val credentials = executeRequest<Credentials>(null) {
apiCall = authAPI.login(loginParams)
}
sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
}
}
private suspend fun loginInternal(login: String,
password: String,
deviceName: String) = withContext(coroutineDispatchers.computation) {

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/**
* Open class, parent to all possible authentication parameters

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.auth.registration
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.api.auth.registration.RegistrationResult
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
@ -28,7 +29,6 @@ import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.task.launchToCallback

View File

@ -18,12 +18,12 @@ package im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.auth.registration.TermPolicies
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true)
data class RegistrationFlowResponse(

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.auth.version
/**
* Values will take the form "rX.Y.Z".
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions
*/
internal data class HomeServerVersion(
val major: Int,
val minor: Int,
val patch: Int
) : Comparable<HomeServerVersion> {
override fun compareTo(other: HomeServerVersion): Int {
return when {
major > other.major -> 1
major < other.major -> -1
minor > other.minor -> 1
minor < other.minor -> -1
patch > other.patch -> 1
patch < other.patch -> -1
else -> 0
}
}
companion object {
internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""")
internal fun parse(value: String): HomeServerVersion? {
val result = pattern.matchEntire(value) ?: return null
return HomeServerVersion(
major = result.groupValues[1].toInt(),
minor = result.groupValues[2].toInt(),
patch = result.groupValues[3].toInt()
)
}
val r0_0_0 = HomeServerVersion(major = 0, minor = 0, patch = 0)
val r0_1_0 = HomeServerVersion(major = 0, minor = 1, patch = 0)
val r0_2_0 = HomeServerVersion(major = 0, minor = 2, patch = 0)
val r0_3_0 = HomeServerVersion(major = 0, minor = 3, patch = 0)
val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018 New Vector Ltd
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.api.auth.data
package im.vector.matrix.android.internal.auth.version
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@ -38,7 +38,7 @@ import com.squareup.moshi.JsonClass
* </pre>
*/
@JsonClass(generateAdapter = true)
data class Versions(
internal data class Versions(
@Json(name = "versions")
val supportedVersions: List<String>? = null,
@ -46,15 +46,6 @@ data class Versions(
val unstableFeatures: Map<String, Boolean>? = null
)
// MatrixClientServerAPIVersion
private const val r0_0_1 = "r0.0.1"
private const val r0_1_0 = "r0.1.0"
private const val r0_2_0 = "r0.2.0"
private const val r0_3_0 = "r0.3.0"
private const val r0_4_0 = "r0.4.0"
private const val r0_5_0 = "r0.5.0"
private const val r0_6_0 = "r0.6.0"
// MatrixVersionsFeature
private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
@ -64,14 +55,14 @@ private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
/**
* Return true if the SDK supports this homeserver version
*/
fun Versions.isSupportedBySdk(): Boolean {
internal fun Versions.isSupportedBySdk(): Boolean {
return supportLazyLoadMembers()
}
/**
* Return true if the SDK supports this homeserver version for login and registration
*/
fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
return !doesServerRequireIdentityServerParam()
&& doesServerAcceptIdentityAccessToken()
&& doesServerSeparatesAddAndBind()
@ -83,7 +74,7 @@ fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
* @return true if the server support the lazy loading of room members
*/
private fun Versions.supportLazyLoadMembers(): Boolean {
return supportedVersions?.contains(r0_5_0) == true
return getMaxVersion() >= HomeServerVersion.r0_5_0
|| unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true
}
@ -92,7 +83,7 @@ private fun Versions.supportLazyLoadMembers(): Boolean {
* adding a 3pid or resetting password.
*/
private fun Versions.doesServerRequireIdentityServerParam(): Boolean {
if (supportedVersions?.contains(r0_6_0) == true) return false
if (getMaxVersion() >= HomeServerVersion.r0_6_0) return false
return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true
}
@ -101,11 +92,18 @@ private fun Versions.doesServerRequireIdentityServerParam(): Boolean {
* Some homeservers may trigger errors if they are not prepared for the new parameter.
*/
private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean {
return supportedVersions?.contains(r0_6_0) == true
return getMaxVersion() >= HomeServerVersion.r0_6_0
|| unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false
}
private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
return supportedVersions?.contains(r0_6_0) == true
return getMaxVersion() >= HomeServerVersion.r0_6_0
|| unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
}
private fun Versions.getMaxVersion(): HomeServerVersion {
return supportedVersions
?.mapNotNull { HomeServerVersion.parse(it) }
?.max()
?: HomeServerVersion.r0_0_0
}

View File

@ -36,7 +36,7 @@ internal data class KeysQueryBody(
* A map from user ID, to a list of device IDs, or to an empty list to indicate all devices for the corresponding user.
*/
@Json(name = "device_keys")
val deviceKeys: Map<String, Any>,
val deviceKeys: Map<String, List<String>>,
/**
* If the client is fetching keys as a result of a device update received in a sync request, this should be the 'since' token

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
/**
* This class provides the authentication data by using user and password

View File

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth

View File

@ -27,7 +27,7 @@ import javax.inject.Inject
internal interface DownloadKeysForUsersTask : Task<DownloadKeysForUsersTask.Params, KeysQueryResponse> {
data class Params(
// the list of users to get keys for.
val userIds: List<String>?,
val userIds: List<String>,
// the up-to token
val token: String?
)
@ -39,7 +39,7 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(
) : DownloadKeysForUsersTask {
override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse {
val downloadQuery = params.userIds?.associateWith { emptyMap<String, Any>() }.orEmpty()
val downloadQuery = params.userIds.associateWith { emptyList<String>() }
val body = KeysQueryBody(
deviceKeys = downloadQuery,

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.identity.todelete
package im.vector.matrix.android.internal.database.mapper
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
@ -22,7 +22,6 @@ import im.vector.matrix.android.internal.database.model.UserAccountDataEntity
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import javax.inject.Inject
// There will be a duplicated class when Integration manager will be merged, so delete this one
internal class AccountDataMapper @Inject constructor(moshi: Moshi) {
private val adapter = moshi.adapter<Map<String, Any>>(JSON_DICT_PARAMETERIZED_TYPE)

View File

@ -36,8 +36,8 @@ internal object EventMapper {
eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}"
eventEntity.roomId = event.roomId ?: roomId
eventEntity.content = ContentMapper.map(event.content)
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
eventEntity.prevContent = ContentMapper.map(resolvedPrevContent)
eventEntity.prevContent = ContentMapper.map(event.resolvedPrevContent())
eventEntity.isUseless = IsUselessResolver.isUseless(event)
eventEntity.stateKey = event.stateKey
eventEntity.type = event.type
eventEntity.sender = event.senderId

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toContent
internal object IsUselessResolver {
/**
* @return true if the event is useless
*/
fun isUseless(event: Event): Boolean {
return when (event.type) {
EventType.STATE_ROOM_MEMBER -> {
// Call toContent(), to filter out null value
event.content != null
&& event.content.toContent() == event.resolvedPrevContent()?.toContent()
}
else -> false
}
}
}

View File

@ -28,6 +28,7 @@ internal open class EventEntity(@Index var eventId: String = "",
@Index var type: String = "",
var content: String? = null,
var prevContent: String? = null,
var isUseless: Boolean = false,
@Index var stateKey: String? = null,
var originServerTs: Long? = null,
@Index var sender: String? = null,

View File

@ -14,11 +14,15 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.extensions
package im.vector.matrix.android.internal.database.model
/**
* Ex: "abcdef".subStringBetween("a", "f") -> "bcde"
* Ex: "abcdefff".subStringBetween("a", "f") -> "bcdeff"
* Ex: "aaabcdef".subStringBetween("a", "f") -> "aabcde"
*/
internal fun String.subStringBetween(prefix: String, suffix: String) = substringAfter(prefix).substringBeforeLast(suffix)
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class ScalarTokenEntity(
@PrimaryKey var serverUrl: String = "",
var token: String = ""
) : RealmObject() {
companion object
}

View File

@ -55,6 +55,8 @@ import io.realm.annotations.RealmModule
HomeServerCapabilitiesEntity::class,
RoomMemberSummaryEntity::class,
CurrentStateEventEntity::class,
UserAccountDataEntity::class
UserAccountDataEntity::class,
ScalarTokenEntity::class,
WellknownIntegrationManagerConfigEntity::class
])
internal class SessionRealmModule

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class WellknownIntegrationManagerConfigEntity(
@PrimaryKey var id: Long = 0,
var apiUrl: String = "",
var uiUrl: String = ""
) : RealmObject() {
companion object
}

View File

@ -23,7 +23,7 @@ import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.createObject
internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: String, type: String): RealmQuery<CurrentStateEventEntity> {
internal fun CurrentStateEventEntity.Companion.whereType(realm: Realm, roomId: String, type: String): RealmQuery<CurrentStateEventEntity> {
return realm.where(CurrentStateEventEntity::class.java)
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
.equalTo(CurrentStateEventEntityFields.TYPE, type)
@ -31,7 +31,7 @@ internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: Strin
internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String)
: RealmQuery<CurrentStateEventEntity> {
return where(realm = realm, roomId = roomId, type = type)
return whereType(realm = realm, roomId = roomId, type = type)
.equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey)
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.ScalarTokenEntity
import im.vector.matrix.android.internal.database.model.ScalarTokenEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun ScalarTokenEntity.Companion.where(realm: Realm, serverUrl: String): RealmQuery<ScalarTokenEntity> {
return realm
.where<ScalarTokenEntity>()
.equalTo(ScalarTokenEntityFields.SERVER_URL, serverUrl)
}

View File

@ -14,13 +14,12 @@
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.identity.todelete
package im.vector.matrix.android.internal.extensions
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
// There will be a duplicated class when Integration manager will be merged, so delete this one
inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
this.observe(owner, Observer { observer(it) })
}

View File

@ -31,6 +31,5 @@ internal object NetworkConstants {
const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2"
const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/"
// TODO Ganfra, use correct value
const val URI_INTEGRATION_MANAGER_PATH = "TODO/"
const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/"
}

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.network
import com.squareup.moshi.Moshi
import dagger.Lazy
import im.vector.matrix.android.internal.util.ensureTrailingSlash
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
@ -29,7 +30,7 @@ class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
fun create(okHttpClient: Lazy<OkHttpClient>, baseUrl: String): Retrofit {
return Retrofit.Builder()
.baseUrl(baseUrl)
.baseUrl(baseUrl.ensureTrailingSlash())
.callFactory(object : Call.Factory {
override fun newCall(request: Request): Call {
return okHttpClient.get().newCall(request)

View File

@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
@ -45,6 +46,7 @@ import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
@ -56,6 +58,7 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecr
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.matrix.android.internal.session.widgets.WidgetDependenciesHolder
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.Dispatchers
@ -90,6 +93,7 @@ internal class DefaultSession @Inject constructor(
private val fileService: Lazy<FileService>,
private val secureStorageService: Lazy<SecureStorageService>,
private val profileService: Lazy<ProfileService>,
private val widgetService: Lazy<WidgetService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val syncTokenStore: SyncTokenStore,
@ -101,11 +105,13 @@ internal class DefaultSession @Inject constructor(
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>,
private val timelineEventDecryptor: TimelineEventDecryptor,
private val shieldTrustUpdater: ShieldTrustUpdater,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val defaultIdentityService: DefaultIdentityService,
private val taskExecutor: TaskExecutor
) : Session,
private val integrationManagerService: IntegrationManagerService,
private val taskExecutor: TaskExecutor,
private val widgetDependenciesHolder: WidgetDependenciesHolder,
private val shieldTrustUpdater: ShieldTrustUpdater)
: Session,
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
GroupService by groupService.get(),
@ -142,6 +148,7 @@ internal class DefaultSession @Inject constructor(
timelineEventDecryptor.start()
shieldTrustUpdater.start()
defaultIdentityService.start()
widgetDependenciesHolder.start()
}
override fun requireBackgroundSync() {
@ -187,6 +194,7 @@ internal class DefaultSession @Inject constructor(
taskExecutor.executorScope.launch(coroutineDispatchers.main) {
// This has to be done on main thread
defaultIdentityService.stop()
widgetDependenciesHolder.stop()
}
}
@ -233,6 +241,10 @@ internal class DefaultSession @Inject constructor(
override fun identityService() = defaultIdentityService
override fun widgetService(): WidgetService = widgetService.get()
override fun integrationManagerService() = integrationManagerService
override fun addListener(listener: Session.Listener) {
sessionListeners.addListener(listener)
}

View File

@ -37,6 +37,7 @@ import im.vector.matrix.android.internal.session.group.GetGroupDataWorker
import im.vector.matrix.android.internal.session.group.GroupModule
import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule
import im.vector.matrix.android.internal.session.identity.IdentityModule
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManagerModule
import im.vector.matrix.android.internal.session.openid.OpenIdModule
import im.vector.matrix.android.internal.session.profile.ProfileModule
import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker
@ -55,6 +56,7 @@ import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import im.vector.matrix.android.internal.session.terms.TermsModule
import im.vector.matrix.android.internal.session.user.UserModule
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule
import im.vector.matrix.android.internal.session.widgets.WidgetModule
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -74,6 +76,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
CryptoModule::class,
PushersModule::class,
OpenIdModule::class,
WidgetModule::class,
IntegrationManagerModule::class,
IdentityModule::class,
TermsModule::class,
AccountDataModule::class,

View File

@ -19,16 +19,16 @@ package im.vector.matrix.android.internal.session.content
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.util.ensureTrailingSlash
import javax.inject.Inject
private const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {
private val baseUrl = homeServerConnectionConfig.homeServerUri.toString()
private val sep = if (baseUrl.endsWith("/")) "" else "/"
private val baseUrl = homeServerConnectionConfig.homeServerUri.toString().ensureTrailingSlash()
override val uploadUrl = baseUrl + sep + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"
override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"
override fun resolveFullSize(contentUrl: String?): String? {
return contentUrl
@ -66,7 +66,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
}
return baseUrl + sep + prefix + serverAndMediaId + params + fragment
return baseUrl + prefix + serverAndMediaId + params + fragment
}
private fun String.isValidMatrixContentUrl(): Boolean {

View File

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.session.homeserver
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.internal.auth.version.Versions
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.GET

View File

@ -17,18 +17,20 @@
package im.vector.matrix.android.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.internal.wellknown.GetWellknownTask
import im.vector.matrix.android.internal.auth.version.Versions
import im.vector.matrix.android.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManagerConfigExtractor
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import im.vector.matrix.android.internal.wellknown.GetWellknownTask
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import java.util.Date
import javax.inject.Inject
@ -39,6 +41,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val monarchy: Monarchy,
private val eventBus: EventBus,
private val getWellknownTask: GetWellknownTask,
private val configExtractor: IntegrationManagerConfigExtractor,
@UserId
private val userId: String
) : GetHomeServerCapabilitiesTask {
@ -102,8 +105,14 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl
}
// We are also checking for integration manager configurations
val config = configExtractor.extract(getWellknownResult.wellKnown)
if (config != null) {
Timber.v("Extracted integration config : $config")
realm.insertOrUpdate(config)
}
}
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
}
}

View File

@ -37,16 +37,16 @@ import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.extensions.observeNotNull
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.identity.data.IdentityStore
import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource
import im.vector.matrix.android.internal.session.identity.todelete.observeNotNull
import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
import im.vector.matrix.android.internal.session.profile.BindThreePidsTask
import im.vector.matrix.android.internal.session.profile.UnbindThreePidsTask
import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.launchToCallback

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.integrationmanager
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AllowedWidgetsContent(
/**
* Map of stateEventId to Allowed
*/
@Json(name = "widgets") val widgets: Map<String, Boolean> = emptyMap(),
/**
* Map of native widgetType to a map of domain to Allowed
* {
* "jitsi" : {
* "jitsi.domain.org" : true,
* "jitsi.other.org" : false
* }
* }
*/
@Json(name = "native_widgets") val native: Map<String, Map<String, Boolean>> = emptyMap()
)

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.integrationmanager
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.api.util.Cancelable
import javax.inject.Inject
internal class DefaultIntegrationManagerService @Inject constructor(private val integrationManager: IntegrationManager) : IntegrationManagerService {
override fun addListener(listener: IntegrationManagerService.Listener) {
integrationManager.addListener(listener)
}
override fun removeListener(listener: IntegrationManagerService.Listener) {
integrationManager.removeListener(listener)
}
override fun getOrderedConfigs(): List<IntegrationManagerConfig> {
return integrationManager.getOrderedConfigs()
}
override fun getPreferredConfig(): IntegrationManagerConfig {
return integrationManager.getPreferredConfig()
}
override fun isIntegrationEnabled(): Boolean {
return integrationManager.isIntegrationEnabled()
}
override fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable {
return integrationManager.setIntegrationEnabled(enable, callback)
}
override fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
return integrationManager.setWidgetAllowed(stateEventId, allowed, callback)
}
override fun isWidgetAllowed(stateEventId: String): Boolean {
return integrationManager.isWidgetAllowed(stateEventId)
}
override fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
return integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed, callback)
}
override fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean {
return integrationManager.isNativeWidgetDomainAllowed(widgetType, domain)
}
}

View File

@ -0,0 +1,288 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.integrationmanager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerConfig
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
import im.vector.matrix.android.api.session.widgets.model.WidgetContent
import im.vector.matrix.android.api.session.widgets.model.WidgetType
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.database.model.WellknownIntegrationManagerConfigEntity
import im.vector.matrix.android.internal.extensions.observeNotNull
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import im.vector.matrix.android.internal.session.user.accountdata.AccountDataDataSource
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.session.widgets.helper.WidgetFactory
import im.vector.matrix.android.internal.session.widgets.helper.extractWidgetSequence
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import timber.log.Timber
import javax.inject.Inject
/**
* The integration manager allows to
* - Get the Integration Manager that a user has explicitly set for its account (via account data)
* - Get the recommended/preferred Integration Manager list as defined by the HomeServer (via wellknown)
* - Check if the user has disabled the integration manager feature
* - Allow / Disallow Integration manager (propagated to other riot clients)
*
* The integration manager listen to account data, and can notify observer for changes.
*
* The wellknown is refreshed at each application fresh start
*
*/
@SessionScope
internal class IntegrationManager @Inject constructor(matrixConfiguration: MatrixConfiguration,
private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val accountDataDataSource: AccountDataDataSource,
private val widgetFactory: WidgetFactory) {
private val currentConfigs = ArrayList<IntegrationManagerConfig>()
private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry }
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner)
private val listeners = HashSet<IntegrationManagerService.Listener>()
fun addListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.add(listener) }
fun removeListener(listener: IntegrationManagerService.Listener) = synchronized(listeners) { listeners.remove(listener) }
init {
val defaultConfig = IntegrationManagerConfig(
uiUrl = matrixConfiguration.integrationUIUrl,
restUrl = matrixConfiguration.integrationRestUrl,
kind = IntegrationManagerConfig.Kind.DEFAULT
)
currentConfigs.add(defaultConfig)
}
fun start() {
lifecycleRegistry.currentState = Lifecycle.State.STARTED
observeWellknownConfig()
accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS)
.observeNotNull(lifecycleOwner) {
val allowedWidgetsContent = it.getOrNull()?.content?.toModel<AllowedWidgetsContent>()
if (allowedWidgetsContent != null) {
notifyWidgetPermissionsChanged(allowedWidgetsContent)
}
}
accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING)
.observeNotNull(lifecycleOwner) {
val integrationProvisioningContent = it.getOrNull()?.content?.toModel<IntegrationProvisioningContent>()
if (integrationProvisioningContent != null) {
notifyIsEnabledChanged(integrationProvisioningContent)
}
}
accountDataDataSource
.getLiveAccountDataEvent(UserAccountData.TYPE_WIDGETS)
.observeNotNull(lifecycleOwner) {
val integrationManagerContent = it.getOrNull()?.asIntegrationManagerWidgetContent()
val config = integrationManagerContent?.extractIntegrationManagerConfig()
updateCurrentConfigs(IntegrationManagerConfig.Kind.ACCOUNT, config)
}
}
fun stop() {
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
}
fun hasConfig() = currentConfigs.isNotEmpty()
fun getOrderedConfigs(): List<IntegrationManagerConfig> {
return currentConfigs.sortedBy {
it.kind
}
}
fun getPreferredConfig(): IntegrationManagerConfig {
// This can't be null as we should have at least the default one registered
return getOrderedConfigs().first()
}
/**
* Returns false if the user as disabled integration manager feature
*/
fun isIntegrationEnabled(): Boolean {
val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING)
val integrationProvisioningContent = integrationProvisioningData?.content?.toModel<IntegrationProvisioningContent>()
return integrationProvisioningContent?.enabled ?: false
}
fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback<Unit>): Cancelable {
val isIntegrationEnabled = isIntegrationEnabled()
if (enable == isIntegrationEnabled) {
callback.onSuccess(Unit)
return NoOpCancellable
}
val integrationProvisioningContent = IntegrationProvisioningContent(enabled = enable)
val params = UpdateUserAccountDataTask.IntegrationProvisioning(integrationProvisioningContent = integrationProvisioningContent)
return updateUserAccountDataTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
val newContent = if (currentContent == null) {
val allowedWidget = mapOf(stateEventId to allowed)
AllowedWidgetsContent(widgets = allowedWidget, native = emptyMap())
} else {
val allowedWidgets = currentContent.widgets.toMutableMap().apply {
put(stateEventId, allowed)
}
currentContent.copy(widgets = allowedWidgets)
}
val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent)
return updateUserAccountDataTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
fun isWidgetAllowed(stateEventId: String): Boolean {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
return currentContent?.widgets?.get(stateEventId) ?: false
}
fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
val newContent = if (currentContent == null) {
val nativeAllowedWidgets = mapOf(widgetType to mapOf(domain to allowed))
AllowedWidgetsContent(widgets = emptyMap(), native = nativeAllowedWidgets)
} else {
val nativeAllowedWidgets = currentContent.native.toMutableMap().apply {
(get(widgetType))?.let {
set(widgetType, it.toMutableMap().apply { set(domain, allowed) })
} ?: run {
set(widgetType, mapOf(domain to allowed))
}
}
currentContent.copy(native = nativeAllowedWidgets)
}
val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent)
return updateUserAccountDataTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
fun isNativeWidgetDomainAllowed(widgetType: String, domain: String?): Boolean {
val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS)
val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>()
return currentContent?.native?.get(widgetType)?.get(domain) ?: false
}
private fun notifyConfigurationChanged() {
synchronized(listeners) {
listeners.forEach {
try {
it.onConfigurationChanged(currentConfigs)
} catch (t: Throwable) {
Timber.e(t, "Failed to notify listener")
}
}
}
}
private fun notifyWidgetPermissionsChanged(allowedWidgets: AllowedWidgetsContent) {
Timber.v("On widget permissions changed: $allowedWidgets")
synchronized(listeners) {
listeners.forEach {
try {
it.onWidgetPermissionsChanged(allowedWidgets.widgets)
} catch (t: Throwable) {
Timber.e(t, "Failed to notify listener")
}
}
}
}
private fun notifyIsEnabledChanged(provisioningContent: IntegrationProvisioningContent) {
Timber.v("On provisioningContent changed : $provisioningContent")
synchronized(listeners) {
listeners.forEach {
try {
it.onIsEnabledChanged(provisioningContent.enabled)
} catch (t: Throwable) {
Timber.e(t, "Failed to notify listener")
}
}
}
}
private fun WidgetContent.extractIntegrationManagerConfig(): IntegrationManagerConfig? {
if (url.isNullOrBlank()) {
return null
}
val integrationManagerData = data.toModel<IntegrationManagerWidgetData>()
return IntegrationManagerConfig(
uiUrl = url,
restUrl = integrationManagerData?.apiUrl ?: url,
kind = IntegrationManagerConfig.Kind.ACCOUNT
)
}
private fun UserAccountDataEvent.asIntegrationManagerWidgetContent(): WidgetContent? {
return extractWidgetSequence(widgetFactory)
.filter {
WidgetType.IntegrationManager == it.type
}
.firstOrNull()?.widgetContent
}
private fun observeWellknownConfig() {
val liveData = monarchy.findAllMappedWithChanges(
{ it.where(WellknownIntegrationManagerConfigEntity::class.java) },
{ IntegrationManagerConfig(it.uiUrl, it.apiUrl, IntegrationManagerConfig.Kind.HOMESERVER) }
)
liveData.observeNotNull(lifecycleOwner) {
val config = it.firstOrNull()
updateCurrentConfigs(IntegrationManagerConfig.Kind.HOMESERVER, config)
}
}
private fun updateCurrentConfigs(kind: IntegrationManagerConfig.Kind, config: IntegrationManagerConfig?) {
val hasBeenRemoved = currentConfigs.removeAll { currentConfig ->
currentConfig.kind == kind
}
if (config != null) {
currentConfigs.add(config)
}
if (hasBeenRemoved || config != null) {
notifyConfigurationChanged()
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.integrationmanager
import im.vector.matrix.android.api.auth.data.WellKnown
import im.vector.matrix.android.internal.database.model.WellknownIntegrationManagerConfigEntity
import javax.inject.Inject
internal class IntegrationManagerConfigExtractor @Inject constructor() {
fun extract(wellKnown: WellKnown): WellknownIntegrationManagerConfigEntity? {
wellKnown.integrations?.get("managers")?.let {
(it as? List<*>)?.let { configs ->
configs.forEach { config ->
(config as? Map<*, *>)?.let { map ->
val apiUrl = map["api_url"] as? String
val uiUrl = map["ui_url"] as? String ?: apiUrl
if (apiUrl != null
&& apiUrl.startsWith("https://")
&& uiUrl!!.startsWith("https://")) {
return WellknownIntegrationManagerConfigEntity(
apiUrl = apiUrl,
uiUrl = uiUrl
)
}
}
}
}
}
return null
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.integrationmanager
import dagger.Binds
import dagger.Module
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
@Module
internal abstract class IntegrationManagerModule {
@Binds
abstract fun bindIntegrationManagerService(service: DefaultIntegrationManagerService): IntegrationManagerService
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.integrationmanager
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class IntegrationManagerWidgetData(
@Json(name = "api_url") val apiUrl: String? = null
)

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.integrationmanager
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class IntegrationProvisioningContent(
@Json(name = "enabled") val enabled: Boolean
)

View File

@ -48,7 +48,7 @@ internal class DefaultConditionResolver @Inject constructor(
val roomId = event.roomId ?: return false
val room = roomGetter.getRoom(roomId) ?: return false
val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
?.content
?.toModel<PowerLevelsContent>()
?: PowerLevelsContent()

View File

@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.reporting.ReportingService
import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.tags.TagsService
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.typing.TypingService
import im.vector.matrix.android.api.session.room.uploads.UploadsService
@ -59,6 +60,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
private val reportingService: ReportingService,
private val readService: ReadService,
private val typingService: TypingService,
private val tagsService: TagsService,
private val cryptoService: CryptoService,
private val relationService: RelationService,
private val roomMembersService: MembershipService,
@ -74,6 +76,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
ReportingService by reportingService,
ReadService by readService,
TypingService by typingService,
TagsService by tagsService,
RelationService by relationService,
MembershipService by roomMembersService,
RoomPushRuleService by roomPushRuleService {
@ -116,9 +119,11 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
callback.onFailure(InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported"))
}
else -> {
val params = SendStateTask.Params(roomId,
EventType.STATE_ROOM_ENCRYPTION,
mapOf(
val params = SendStateTask.Params(
roomId = roomId,
stateKey = null,
eventType = EventType.STATE_ROOM_ENCRYPTION,
body = mapOf(
"algorithm" to algorithm
))

View File

@ -111,24 +111,22 @@ internal class DefaultRoomService @Inject constructor(
return query
}
override fun getBreadcrumbs(): List<RoomSummary> {
override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
return monarchy.fetchAllMappedSync(
{ breadcrumbsQuery(it) },
{ breadcrumbsQuery(it, queryParams) },
{ roomSummaryMapper.map(it) }
)
}
override fun getBreadcrumbsLive(): LiveData<List<RoomSummary>> {
override fun getBreadcrumbsLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ breadcrumbsQuery(it) },
{ breadcrumbsQuery(it, queryParams) },
{ roomSummaryMapper.map(it) }
)
}
private fun breadcrumbsQuery(realm: Realm): RealmQuery<RoomSummaryEntity> {
return RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
private fun breadcrumbsQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
return roomSummariesQuery(realm, queryParams)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummary.NOT_IN_BREADCRUMBS)
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
}

View File

@ -24,18 +24,22 @@ import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.session.room.tags.TagBody
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
import im.vector.matrix.android.internal.session.room.typing.TypingBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
@ -175,7 +179,7 @@ internal interface RoomAPI {
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state/{state_event_type}")
fun sendStateEvent(@Path("roomId") roomId: String,
@Path("state_event_type") stateEventType: String,
@Body params: Map<String, String>): Call<Unit>
@Body params: JsonDict): Call<Unit>
/**
* Send a generic state events
@ -189,7 +193,7 @@ internal interface RoomAPI {
fun sendStateEvent(@Path("roomId") roomId: String,
@Path("state_event_type") stateEventType: String,
@Path("state_key") stateKey: String,
@Body params: Map<String, String>): Call<Unit>
@Body params: JsonDict): Call<Unit>
/**
* Send a relation event to a room.
@ -242,6 +246,33 @@ internal interface RoomAPI {
fun leave(@Path("roomId") roomId: String,
@Body params: Map<String, String?>): Call<Unit>
/**
* Ban a user from the given room.
*
* @param roomId the room id
* @param userIdAndReason the banned user object (userId and reason for ban)
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban")
fun ban(@Path("roomId") roomId: String, @Body userIdAndReason: UserIdAndReason): Call<Unit>
/**
* unban a user from the given room.
*
* @param roomId the room id
* @param userIdAndReason the unbanned user object (userId and reason for unban)
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban")
fun unban(@Path("roomId") roomId: String, @Body userIdAndReason: UserIdAndReason): Call<Unit>
/**
* Kick a user from the given room.
*
* @param roomId the room id
* @param userIdAndReason the kicked user object (userId and reason for kicking)
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick")
fun kick(@Path("roomId") roomId: String, @Body userIdAndReason: UserIdAndReason): Call<Unit>
/**
* Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room.
* This cannot be undone.
@ -287,4 +318,25 @@ internal interface RoomAPI {
fun sendTypingState(@Path("roomId") roomId: String,
@Path("userId") userId: String,
@Body body: TypingBody): Call<Unit>
/**
* Room tagging
*/
/**
* Add a tag to a room.
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}")
fun putTag(@Path("userId") userId: String,
@Path("roomId") roomId: String,
@Path("tag") tag: String,
@Body body: TagBody): Call<Unit>
/**
* Delete a tag from a room.
*/
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}")
fun deleteTag(@Path("userId") userId: String,
@Path("roomId") roomId: String,
@Path("tag") tag: String): Call<Unit>
}

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.session.room.reporting.DefaultReporting
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
import im.vector.matrix.android.internal.session.room.state.SendStateTask
import im.vector.matrix.android.internal.session.room.tags.DefaultTagsService
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.typing.DefaultTypingService
import im.vector.matrix.android.internal.session.room.uploads.DefaultUploadsService
@ -52,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
private val reportingServiceFactory: DefaultReportingService.Factory,
private val readServiceFactory: DefaultReadService.Factory,
private val typingServiceFactory: DefaultTypingService.Factory,
private val tagsServiceFactory: DefaultTagsService.Factory,
private val relationServiceFactory: DefaultRelationService.Factory,
private val membershipServiceFactory: DefaultMembershipService.Factory,
private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory,
@ -72,6 +74,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
reportingService = reportingServiceFactory.create(roomId),
readService = readServiceFactory.create(roomId),
typingService = typingServiceFactory.create(roomId),
tagsService = tagsServiceFactory.create(roomId),
cryptoService = cryptoService,
relationService = relationServiceFactory.create(roomId),
roomMembersService = membershipServiceFactory.create(roomId),

View File

@ -34,6 +34,8 @@ import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTas
import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask
import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.admin.DefaultMembershipAdminTask
import im.vector.matrix.android.internal.session.room.membership.admin.MembershipAdminTask
import im.vector.matrix.android.internal.session.room.membership.joining.DefaultInviteTask
import im.vector.matrix.android.internal.session.room.membership.joining.DefaultJoinRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
@ -56,6 +58,10 @@ import im.vector.matrix.android.internal.session.room.reporting.DefaultReportCon
import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
import im.vector.matrix.android.internal.session.room.state.SendStateTask
import im.vector.matrix.android.internal.session.room.tags.AddTagToRoomTask
import im.vector.matrix.android.internal.session.room.tags.DefaultAddTagToRoomTask
import im.vector.matrix.android.internal.session.room.tags.DefaultDeleteTagFromRoomTask
import im.vector.matrix.android.internal.session.room.tags.DeleteTagFromRoomTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultFetchNextTokenAndPaginateTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
@ -66,6 +72,9 @@ import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTa
import im.vector.matrix.android.internal.session.room.typing.SendTypingTask
import im.vector.matrix.android.internal.session.room.uploads.DefaultGetUploadsTask
import im.vector.matrix.android.internal.session.room.uploads.GetUploadsTask
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.commonmark.renderer.text.TextContentRenderer
import retrofit2.Retrofit
@Module
@ -79,6 +88,28 @@ internal abstract class RoomModule {
fun providesRoomAPI(retrofit: Retrofit): RoomAPI {
return retrofit.create(RoomAPI::class.java)
}
@Provides
@JvmStatic
fun providesParser(): Parser {
return Parser.builder().build()
}
@Provides
@JvmStatic
fun providesHtmlRenderer(): HtmlRenderer {
return HtmlRenderer
.builder()
.build()
}
@Provides
@JvmStatic
fun providesTextContentRenderer(): TextContentRenderer {
return TextContentRenderer
.builder()
.build()
}
}
@Binds
@ -117,6 +148,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindLeaveRoomTask(task: DefaultLeaveRoomTask): LeaveRoomTask
@Binds
abstract fun bindMembershipAdminTask(task: DefaultMembershipAdminTask): MembershipAdminTask
@Binds
abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask
@ -161,4 +195,10 @@ internal abstract class RoomModule {
@Binds
abstract fun bindGetUploadsTask(task: DefaultGetUploadsTask): GetUploadsTask
@Binds
abstract fun bindAddTagToRoomTask(task: DefaultAddTagToRoomTask): AddTagToRoomTask
@Binds
abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask
}

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.query.process
import im.vector.matrix.android.internal.session.room.membership.admin.MembershipAdminTask
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
@ -48,6 +49,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
private val inviteTask: InviteTask,
private val joinTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask,
private val membershipAdminTask: MembershipAdminTask,
@UserId
private val userId: String
) : MembershipService {
@ -113,6 +115,33 @@ internal class DefaultMembershipService @AssistedInject constructor(
}
}
override fun ban(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = MembershipAdminTask.Params(MembershipAdminTask.Type.BAN, roomId, userId, reason)
return membershipAdminTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun unban(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = MembershipAdminTask.Params(MembershipAdminTask.Type.UNBAN, roomId, userId, reason)
return membershipAdminTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun kick(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = MembershipAdminTask.Params(MembershipAdminTask.Type.KICK, roomId, userId, reason)
return membershipAdminTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = InviteTask.Params(roomId, userId, reason)
return inviteTask

View File

@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.room.membership
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import io.realm.Realm
@ -35,7 +34,7 @@ internal class RoomMemberEventHandler @Inject constructor() {
val userId = event.stateKey ?: return false
val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember)
realm.insertOrUpdate(roomMemberEntity)
if (roomMember.membership in Membership.activeMemberships()) {
if (roomMember.membership.isActive()) {
val userEntity = UserEntityFactory.create(userId, roomMember)
realm.insertOrUpdate(userEntity)
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.room.membership.admin
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface MembershipAdminTask : Task<MembershipAdminTask.Params, Unit> {
enum class Type {
BAN,
UNBAN,
KICK
}
data class Params(
val type: Type,
val roomId: String,
val userId: String,
val reason: String?
)
}
internal class DefaultMembershipAdminTask @Inject constructor(private val roomAPI: RoomAPI) : MembershipAdminTask {
override suspend fun execute(params: MembershipAdminTask.Params) {
val userIdAndReason = UserIdAndReason(params.userId, params.reason)
executeRequest<Unit>(null) {
apiCall = when (params.type) {
MembershipAdminTask.Type.BAN -> roomAPI.ban(params.roomId, userIdAndReason)
MembershipAdminTask.Type.UNBAN -> roomAPI.unban(params.roomId, userIdAndReason)
MembershipAdminTask.Type.KICK -> roomAPI.kick(params.roomId, userIdAndReason)
}
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.room.membership.admin
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserIdAndReason(
@Json(name = "user_id") val userId: String,
@Json(name = "reason") val reason: String? = null
)

View File

@ -33,6 +33,7 @@ import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.content.UploadContentWorker
@ -67,6 +68,12 @@ internal class DefaultSendService @AssistedInject constructor(
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
override fun sendEvent(eventType: String, content: JsonDict?): Cancelable {
return localEchoEventFactory.createEvent(roomId, eventType, content)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown)
.also { createLocalEcho(it) }

View File

@ -23,6 +23,7 @@ import androidx.exifinterface.media.ExifInterface
import im.vector.matrix.android.R
import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
@ -57,14 +58,11 @@ import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.subStringBetween
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.StringProvider
import kotlinx.coroutines.launch
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import javax.inject.Inject
/**
@ -80,41 +78,23 @@ internal class LocalEchoEventFactory @Inject constructor(
private val context: Context,
@UserId private val userId: String,
private val stringProvider: StringProvider,
private val markdownParser: MarkdownParser,
private val textPillsUtils: TextPillsUtils,
private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository
) {
// TODO Inject
private val parser = Parser.builder().build()
// TODO Inject
private val renderer = HtmlRenderer.builder().build()
fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event {
if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) {
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType)
}
val content = MessageTextContent(msgType = msgType, body = text.toString())
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
if (autoMarkdown) {
val source = textPillsUtils.processSpecialSpansToMarkdown(text)
?: text.toString()
val document = parser.parse(source)
val htmlText = renderer.render(document)
// Cleanup extra paragraph
val cleanHtmlText = if (htmlText.startsWith("<p>") && htmlText.endsWith("</p>\n")) {
htmlText.subStringBetween("<p>", "</p>\n")
} else {
htmlText
}
if (isFormattedTextPertinent(source, cleanHtmlText)) {
return TextContent(text.toString(), cleanHtmlText)
}
val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString()
return markdownParser.parse(source)
} else {
// Try to detect pills
textPillsUtils.processSpecialSpansToHtml(text)?.let {
@ -125,11 +105,8 @@ internal class LocalEchoEventFactory @Inject constructor(
return TextContent(text.toString())
}
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event {
return createEvent(roomId, textContent.toMessageTextContent(msgType))
return createMessageEvent(roomId, textContent.toMessageTextContent(msgType))
}
fun createReplaceTextEvent(roomId: String,
@ -138,7 +115,7 @@ internal class LocalEchoEventFactory @Inject constructor(
newBodyAutoMarkdown: Boolean,
msgType: String,
compatibilityText: String): Event {
return createEvent(roomId,
return createMessageEvent(roomId,
MessageTextContent(
msgType = msgType,
body = compatibilityText,
@ -153,7 +130,7 @@ internal class LocalEchoEventFactory @Inject constructor(
pollEventId: String,
optionIndex: Int,
optionLabel: String): Event {
return createEvent(roomId,
return createMessageEvent(roomId,
MessagePollResponseContent(
body = optionLabel,
relatesTo = RelationDefaultContent(
@ -175,7 +152,7 @@ internal class LocalEchoEventFactory @Inject constructor(
append(it.value)
}
}
return createEvent(
return createMessageEvent(
roomId,
MessageOptionsContent(
body = compatLabel,
@ -211,7 +188,7 @@ internal class LocalEchoEventFactory @Inject constructor(
//
val replyFallback = buildReplyFallback(body, originalEvent.root.senderId ?: "", newBodyText)
return createEvent(roomId,
return createMessageEvent(roomId,
MessageTextContent(
msgType = msgType,
body = compatibilityText,
@ -280,7 +257,7 @@ internal class LocalEchoEventFactory @Inject constructor(
),
url = attachment.queryUri.toString()
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
@ -316,7 +293,7 @@ internal class LocalEchoEventFactory @Inject constructor(
),
url = attachment.queryUri.toString()
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event {
@ -329,7 +306,7 @@ internal class LocalEchoEventFactory @Inject constructor(
),
url = attachment.queryUri.toString()
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event {
@ -342,18 +319,22 @@ internal class LocalEchoEventFactory @Inject constructor(
),
url = attachment.queryUri.toString()
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun createEvent(roomId: String, content: Any? = null): Event {
private fun createMessageEvent(roomId: String, content: MessageContent? = null): Event {
return createEvent(roomId, EventType.MESSAGE, content.toContent())
}
fun createEvent(roomId: String, type: String, content: Content?): Event {
val localId = LocalEcho.createLocalEchoId()
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localId,
type = EventType.MESSAGE,
content = content.toContent(),
type = type,
content = content,
unsignedData = UnsignedData(age = null, transactionId = localId)
)
}
@ -410,7 +391,7 @@ internal class LocalEchoEventFactory @Inject constructor(
formattedBody = replyFormatted,
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
)
return createEvent(roomId, content)
return createMessageEvent(roomId, content)
}
private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.room.send
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.commonmark.renderer.text.TextContentRenderer
import javax.inject.Inject
/**
* This class convert a text to an html text
* This class is tested by [MarkdownParserTest].
* If any change is required, please add a test covering the problem and make sure all the tests are still passing.
*/
internal class MarkdownParser @Inject constructor(
private val parser: Parser,
private val htmlRenderer: HtmlRenderer,
private val textContentRenderer: TextContentRenderer
) {
private val mdSpecialChars = "[`_\\-\\*>\\.\\[\\]#~]".toRegex()
fun parse(text: String): TextContent {
// If no special char are detected, just return plain text
if (text.contains(mdSpecialChars).not()) {
return TextContent(text.toString())
}
val document = parser.parse(text)
val htmlText = htmlRenderer.render(document)
// Cleanup extra paragraph
val cleanHtmlText = if (htmlText.lastIndexOf("<p>") == 0) {
htmlText.removeSurrounding("<p>", "</p>\n")
} else {
htmlText
}
return if (isFormattedTextPertinent(text, cleanHtmlText)) {
// According to https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes:
// The plain text version of the HTML should be provided in the body.
val plainText = textContentRenderer.render(document)
TextContent(plainText, cleanHtmlText.postTreatment())
} else {
TextContent(text.toString())
}
}
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
/**
* The parser makes some mistakes, so deal with it here
*/
private fun String.postTreatment(): String {
return this
// Remove extra space before and after the content
.trim()
// There is no need to include new line in an html-like source
.replace("\n", "")
}
}

View File

@ -17,26 +17,21 @@
package im.vector.matrix.android.internal.session.room.state
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.query.getOrNull
import im.vector.matrix.android.internal.database.query.whereStateKey
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.Realm
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy,
private val stateEventDataSource: StateEventDataSource,
private val taskExecutor: TaskExecutor,
private val sendStateTask: SendStateTask
) : StateService {
@ -46,33 +41,47 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
fun create(roomId: String): StateService
}
override fun getStateEvent(eventType: String, stateKey: String): Event? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
CurrentStateEventEntity.getOrNull(realm, roomId, type = eventType, stateKey = stateKey)?.root?.asDomain()
}
override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? {
return stateEventDataSource.getStateEvent(roomId, eventType, stateKey)
}
override fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm -> CurrentStateEventEntity.whereStateKey(realm, roomId, type = eventType, stateKey = "") },
{ it.root?.asDomain() }
override fun getStateEventLive(eventType: String, stateKey: QueryStringValue): LiveData<Optional<Event>> {
return stateEventDataSource.getStateEventLive(roomId, eventType, stateKey)
}
override fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> {
return stateEventDataSource.getStateEvents(roomId, eventTypes, stateKey)
}
override fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue): LiveData<List<Event>> {
return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey)
}
override fun sendStateEvent(
eventType: String,
stateKey: String?,
body: JsonDict,
callback: MatrixCallback<Unit>
): Cancelable {
val params = SendStateTask.Params(
roomId = roomId,
stateKey = stateKey,
eventType = eventType,
body = body
)
return Transformations.map(liveData) { results ->
results.firstOrNull().toOptional()
}
}
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>) {
val params = SendStateTask.Params(roomId,
EventType.STATE_ROOM_TOPIC,
mapOf(
"topic" to topic
))
sendStateTask
return sendStateTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable {
return sendStateEvent(
eventType = EventType.STATE_ROOM_TOPIC,
body = mapOf("topic" to topic),
callback = callback,
stateKey = null
)
}
}

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.session.room.state
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
@ -25,8 +26,9 @@ import javax.inject.Inject
internal interface SendStateTask : Task<SendStateTask.Params, Unit> {
data class Params(
val roomId: String,
val stateKey: String?,
val eventType: String,
val body: Map<String, String>
val body: JsonDict
)
}
@ -37,7 +39,20 @@ internal class DefaultSendStateTask @Inject constructor(
override suspend fun execute(params: SendStateTask.Params) {
return executeRequest(eventBus) {
apiCall = roomAPI.sendStateEvent(params.roomId, params.eventType, params.body)
apiCall = if (params.stateKey == null) {
roomAPI.sendStateEvent(
roomId = params.roomId,
stateEventType = params.eventType,
params = params.body
)
} else {
roomAPI.sendStateEvent(
roomId = params.roomId,
stateEventType = params.eventType,
stateKey = params.stateKey,
params = params.body
)
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More