Invite by email (msisdn not working), command line (#548)

This commit is contained in:
Benoit Marty 2020-07-07 19:52:04 +02:00
parent 70e90d8542
commit ab1d652f17
14 changed files with 253 additions and 22 deletions

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.members
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
@ -63,6 +64,12 @@ interface MembershipService {
reason: String? = null, reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable callback: MatrixCallback<Unit>): Cancelable
/**
* Invite a user with email or phone number in the room
*/
fun invite3pid(threePid: ThreePid,
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Ban a user from the room * Ban a user from the room
*/ */

View File

@ -62,6 +62,7 @@ import javax.net.ssl.HttpsURLConnection
@SessionScope @SessionScope
internal class DefaultIdentityService @Inject constructor( internal class DefaultIdentityService @Inject constructor(
private val identityStore: IdentityStore, private val identityStore: IdentityStore,
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
private val getOpenIdTokenTask: GetOpenIdTokenTask, private val getOpenIdTokenTask: GetOpenIdTokenTask,
private val identityBulkLookupTask: IdentityBulkLookupTask, private val identityBulkLookupTask: IdentityBulkLookupTask,
private val identityRegisterTask: IdentityRegisterTask, private val identityRegisterTask: IdentityRegisterTask,
@ -278,7 +279,7 @@ internal class DefaultIdentityService @Inject constructor(
} }
private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> { private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
ensureToken() ensureIdentityTokenTask.execute(Unit)
return try { return try {
identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids)) identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids))
@ -295,17 +296,6 @@ internal class DefaultIdentityService @Inject constructor(
} }
} }
private suspend fun ensureToken() {
val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured
val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured
if (identityData.token == null) {
// Try to get a token
val token = getNewIdentityServerToken(url)
identityStore.setToken(token)
}
}
private suspend fun getNewIdentityServerToken(url: String): String { private suspend fun getNewIdentityServerToken(url: String): String {
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)

View File

@ -0,0 +1,59 @@
/*
* 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.identity
import dagger.Lazy
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.session.identity.data.IdentityStore
import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
import im.vector.matrix.android.internal.task.Task
import okhttp3.OkHttpClient
import javax.inject.Inject
internal interface EnsureIdentityTokenTask : Task<Unit, Unit>
internal class DefaultEnsureIdentityTokenTask @Inject constructor(
private val identityStore: IdentityStore,
private val retrofitFactory: RetrofitFactory,
@UnauthenticatedWithCertificate
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
private val getOpenIdTokenTask: GetOpenIdTokenTask,
private val identityRegisterTask: IdentityRegisterTask
) : EnsureIdentityTokenTask {
override suspend fun execute(params: Unit) {
val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured
val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured
if (identityData.token == null) {
// Try to get a token
val token = getNewIdentityServerToken(url)
identityStore.setToken(token)
}
}
private suspend fun getNewIdentityServerToken(url: String): String {
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
val openIdToken = getOpenIdTokenTask.execute(Unit)
val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken))
return token.token
}
}

View File

@ -78,6 +78,9 @@ internal abstract class IdentityModule {
@Binds @Binds
abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore
@Binds
abstract fun bindEnsureIdentityTokenTask(task: DefaultEnsureIdentityTokenTask): EnsureIdentityTokenTask
@Binds @Binds
abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask

View File

@ -31,6 +31,7 @@ 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.RoomMembersResponse
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason 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.membership.joining.InviteBody
import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse 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.reporting.ReportContentBody
import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.send.SendResponse
@ -170,6 +171,14 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
fun invite(@Path("roomId") roomId: String, @Body body: InviteBody): Call<Unit> fun invite(@Path("roomId") roomId: String, @Body body: InviteBody): Call<Unit>
/**
* Invite a user to a room, using a ThreePid
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#id101
* @param roomId Required. The room identifier (not alias) to which to invite the user.
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
fun invite3pid(@Path("roomId") roomId: String, @Body body: ThreePidInviteBody): Call<Unit>
/** /**
* Send a generic state events * Send a generic state events
* *

View File

@ -44,6 +44,8 @@ import im.vector.matrix.android.internal.session.room.membership.joining.InviteT
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
import im.vector.matrix.android.internal.session.room.membership.threepid.DefaultInviteThreePidTask
import im.vector.matrix.android.internal.session.room.membership.threepid.InviteThreePidTask
import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
@ -139,6 +141,9 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
@Binds
abstract fun bindInviteThreePidTask(task: DefaultInviteThreePidTask): InviteThreePidTask
@Binds @Binds
abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask

View File

@ -21,6 +21,7 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
@ -36,6 +37,7 @@ import im.vector.matrix.android.internal.session.room.membership.admin.Membershi
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask 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.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
import im.vector.matrix.android.internal.session.room.membership.threepid.InviteThreePidTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchCopied import im.vector.matrix.android.internal.util.fetchCopied
@ -48,6 +50,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val inviteTask: InviteTask, private val inviteTask: InviteTask,
private val inviteThreePidTask: InviteThreePidTask,
private val joinTask: JoinRoomTask, private val joinTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask, private val leaveRoomTask: LeaveRoomTask,
private val membershipAdminTask: MembershipAdminTask, private val membershipAdminTask: MembershipAdminTask,
@ -152,6 +155,15 @@ internal class DefaultMembershipService @AssistedInject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun invite3pid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable {
val params = InviteThreePidTask.Params(roomId, threePid)
return inviteThreePidTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable { override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
val params = JoinRoomTask.Params(roomId, reason, viaServers) val params = JoinRoomTask.Params(roomId, reason, viaServers)
return joinTask return joinTask

View File

@ -0,0 +1,65 @@
/*
* Copyright 2019 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.threepid
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.identity.toMedium
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
import im.vector.matrix.android.internal.session.identity.EnsureIdentityTokenTask
import im.vector.matrix.android.internal.session.identity.data.IdentityStore
import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface InviteThreePidTask : Task<InviteThreePidTask.Params, Unit> {
data class Params(
val roomId: String,
val threePid: ThreePid
)
}
internal class DefaultInviteThreePidTask @Inject constructor(
private val roomAPI: RoomAPI,
private val eventBus: EventBus,
private val identityStore: IdentityStore,
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
@AuthenticatedIdentity
private val accessTokenProvider: AccessTokenProvider
) : InviteThreePidTask {
override suspend fun execute(params: InviteThreePidTask.Params) {
ensureIdentityTokenTask.execute(Unit)
val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured
val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured
return executeRequest(eventBus) {
val body = ThreePidInviteBody(
id_server = identityServerUrlWithoutProtocol,
id_access_token = identityServerAccessToken,
medium = params.threePid.toMedium(),
address = params.threePid.value
)
apiCall = roomAPI.invite3pid(params.roomId, body)
}
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2019 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.threepid
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class ThreePidInviteBody(
/**
* Required. The hostname+port of the identity server which should be used for third party identifier lookups.
*/
@Json(name = "id_server") val id_server: String,
/**
* Required. An access token previously registered with the identity server. Servers can treat this as optional
* to distinguish between r0.5-compatible clients and this specification version.
*/
@Json(name = "id_access_token") val id_access_token: String,
/**
* Required. The kind of address being passed in the address field, for example email.
*/
@Json(name = "medium") val medium: String,
/**
* Required. The invitee's third party identifier.
*/
@Json(name = "address") val address: String
)

View File

@ -19,6 +19,8 @@ package im.vector.riotx.core.extensions
import android.os.Bundle import android.os.Bundle
import android.util.Patterns import android.util.Patterns
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
fun Boolean.toOnOff() = if (this) "ON" else "OFF" fun Boolean.toOnOff() = if (this) "ON" else "OFF"
@ -33,3 +35,16 @@ fun <T : Fragment> T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bu
* Check if a CharSequence is an email * Check if a CharSequence is an email
*/ */
fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches() fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
/**
* Check if a CharSequence is a phone number
* FIXME It does not work?
*/
fun CharSequence.isMsisdn(): Boolean {
return try {
PhoneNumberUtil.getInstance().parse(this, null)
true
} catch (e: NumberParseException) {
false
}
}

View File

@ -17,6 +17,9 @@
package im.vector.riotx.features.command package im.vector.riotx.features.command
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.riotx.core.extensions.isEmail
import im.vector.riotx.core.extensions.isMsisdn
import timber.log.Timber import timber.log.Timber
object CommandParser { object CommandParser {
@ -139,15 +142,24 @@ object CommandParser {
if (messageParts.size >= 2) { if (messageParts.size >= 2) {
val userId = messageParts[1] val userId = messageParts[1]
if (MatrixPatterns.isUserId(userId)) { when {
ParsedCommand.Invite( MatrixPatterns.isUserId(userId) -> {
userId, ParsedCommand.Invite(
textMessage.substring(Command.INVITE.length + userId.length) userId,
.trim() textMessage.substring(Command.INVITE.length + userId.length)
.takeIf { it.isNotBlank() } .trim()
) .takeIf { it.isNotBlank() }
} else { )
ParsedCommand.ErrorSyntax(Command.INVITE) }
userId.isEmail() -> {
ParsedCommand.Invite3Pid(ThreePid.Email(userId))
}
userId.isMsisdn() -> {
ParsedCommand.Invite3Pid(ThreePid.Msisdn(userId))
}
else -> {
ParsedCommand.ErrorSyntax(Command.INVITE)
}
} }
} else { } else {
ParsedCommand.ErrorSyntax(Command.INVITE) ParsedCommand.ErrorSyntax(Command.INVITE)

View File

@ -16,6 +16,8 @@
package im.vector.riotx.features.command package im.vector.riotx.features.command
import im.vector.matrix.android.api.session.identity.ThreePid
/** /**
* Represent a parsed command * Represent a parsed command
*/ */
@ -41,6 +43,7 @@ sealed class ParsedCommand {
class UnbanUser(val userId: String, val reason: String?) : ParsedCommand() class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand() class SetUserPowerLevel(val userId: String, val powerLevel: Int?) : ParsedCommand()
class Invite(val userId: String, val reason: String?) : ParsedCommand() class Invite(val userId: String, val reason: String?) : ParsedCommand()
class Invite3Pid(val threePid: ThreePid) : ParsedCommand()
class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand() class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand() class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
class ChangeTopic(val topic: String) : ParsedCommand() class ChangeTopic(val topic: String) : ParsedCommand()

View File

@ -960,7 +960,7 @@ class RoomDetailFragment @Inject constructor(
updateComposerText("") updateComposerText("")
} }
is RoomDetailViewEvents.SlashCommandResultError -> { is RoomDetailViewEvents.SlashCommandResultError -> {
displayCommandError(sendMessageResult.throwable.localizedMessage ?: getString(R.string.unexpected_error)) displayCommandError(errorFormatter.toHumanReadable(sendMessageResult.throwable))
} }
is RoomDetailViewEvents.SlashCommandNotImplemented -> { is RoomDetailViewEvents.SlashCommandNotImplemented -> {
displayCommandError(getString(R.string.not_implemented)) displayCommandError(getString(R.string.not_implemented))

View File

@ -457,6 +457,10 @@ class RoomDetailViewModel @AssistedInject constructor(
handleInviteSlashCommand(slashCommandResult) handleInviteSlashCommand(slashCommandResult)
popDraft() popDraft()
} }
is ParsedCommand.Invite3Pid -> {
handleInvite3pidSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.SetUserPowerLevel -> { is ParsedCommand.SetUserPowerLevel -> {
handleSetUserPowerLevel(slashCommandResult) handleSetUserPowerLevel(slashCommandResult)
popDraft() popDraft()
@ -678,6 +682,12 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) {
launchSlashCommandFlow {
room.invite3pid(invite.threePid, it)
}
}
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
val currentPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) val currentPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
?.content ?.content