Merge pull request #2019 from nextcloud/feature/1948/respectChatPermission

Feature/1948/respect chat permission
This commit is contained in:
Marcel Hibbe 2022-05-12 15:08:45 +02:00 committed by GitHub
commit 2cb2cce39e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 267 additions and 72 deletions

View File

@ -147,6 +147,7 @@ import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.AttendeePermissionsUtil
import com.nextcloud.talk.utils.ConductorRemapping
import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
import com.nextcloud.talk.utils.ContactUtils
@ -273,6 +274,8 @@ class ChatController(args: Bundle) :
lateinit var mediaPlayerHandler: Handler
var currentlyPlayedVoiceMessage: ChatMessage? = null
var hasChatPermission: Boolean = false
init {
Log.d(TAG, "init ChatController: " + System.identityHashCode(this).toString())
@ -306,6 +309,9 @@ class ChatController(args: Bundle) :
}
this.voiceOnly = args.getBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false)
hasChatPermission =
AttendeePermissionsUtil(currentConversation!!.permissions).hasChatPermission(conversationUser)
}
private fun getRoomInfo() {
@ -337,11 +343,17 @@ class ChatController(args: Bundle) :
" sessionId: " + currentConversation?.sessionId
)
loadAvatarForStatusBar()
setTitle()
hasChatPermission =
AttendeePermissionsUtil(currentConversation!!.permissions).hasChatPermission(
conversationUser
)
try {
setupMentionAutocomplete()
checkReadOnlyState()
checkShowCallButtons()
checkShowMessageInputView()
checkLobbyState()
if (!inConversation) {
@ -580,7 +592,7 @@ class ChatController(args: Bundle) :
}
}
if (context != null) {
if (context != null && hasChatPermission && !isReadOnlyConversation()) {
val messageSwipeController = MessageSwipeCallback(
activity!!,
object : MessageSwipeActions {
@ -1158,13 +1170,24 @@ class ChatController(args: Bundle) :
)
}
private fun checkReadOnlyState() {
private fun checkShowCallButtons() {
if (isAlive()) {
if (isReadOnlyConversation() || shouldShowLobby()) {
disableCallButtons()
binding.messageInputView.visibility = View.GONE
} else {
enableCallButtons()
}
}
}
private fun checkShowMessageInputView() {
if (isAlive()) {
if (isReadOnlyConversation() ||
shouldShowLobby() ||
!hasChatPermission
) {
binding.messageInputView.visibility = View.GONE
} else {
binding.messageInputView.visibility = View.VISIBLE
}
}
@ -1437,6 +1460,12 @@ class ChatController(args: Bundle) :
private fun uploadFiles(files: MutableList<String>, isVoiceMessage: Boolean) {
var metaData = ""
if (!hasChatPermission) {
Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions")
return
}
if (isVoiceMessage) {
metaData = VOICE_MESSAGE_META_DATA
}
@ -2349,7 +2378,7 @@ class ChatController(args: Bundle) :
super.onPrepareOptionsMenu(menu)
conversationUser?.let {
if (CapabilitiesUtil.hasSpreedFeatureCapability(it, "read-only-rooms")) {
checkReadOnlyState()
checkShowCallButtons()
}
}
}
@ -2474,6 +2503,7 @@ class ChatController(args: Bundle) :
currentConversation,
chatMessage,
conversationUser,
hasChatPermission,
ncApi!!
).show()
}
@ -2501,6 +2531,7 @@ class ChatController(args: Bundle) :
conversationUser,
currentConversation,
isShowMessageDeletionButton(message),
hasChatPermission,
ncApi!!
).show()
}
@ -2512,50 +2543,59 @@ class ChatController(args: Bundle) :
}
fun deleteMessage(message: IMessage?) {
var apiVersion = 1
// FIXME Fix API checking with guests?
if (conversationUser != null) {
apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
}
ncApi?.deleteChatMessage(
credentials,
ApiUtils.getUrlForChatMessage(
apiVersion,
conversationUser?.baseUrl,
roomToken,
message?.id
if (!hasChatPermission) {
Log.w(
TAG,
"Deletion of message is skipped because of restrictions by permissions. " +
"This method should not have been called!"
)
)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<ChatOverallSingleMessage> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
} else {
var apiVersion = 1
// FIXME Fix API checking with guests?
if (conversationUser != null) {
apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
}
override fun onNext(t: ChatOverallSingleMessage) {
if (t.ocs.meta.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
Toast.makeText(
context, R.string.nc_delete_message_leaked_to_matterbridge,
Toast.LENGTH_LONG
).show()
ncApi?.deleteChatMessage(
credentials,
ApiUtils.getUrlForChatMessage(
apiVersion,
conversationUser?.baseUrl,
roomToken,
message?.id
)
)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<ChatOverallSingleMessage> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
}
override fun onError(e: Throwable) {
Log.e(
TAG,
"Something went wrong when trying to delete message with id " +
message?.id,
e
)
Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
}
override fun onNext(t: ChatOverallSingleMessage) {
if (t.ocs.meta.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
Toast.makeText(
context, R.string.nc_delete_message_leaked_to_matterbridge,
Toast.LENGTH_LONG
).show()
}
}
override fun onComplete() {
// unused atm
}
})
override fun onError(e: Throwable) {
Log.e(
TAG,
"Something went wrong when trying to delete message with id " +
message?.id,
e
)
Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
}
override fun onComplete() {
// unused atm
}
})
}
}
fun replyPrivately(message: IMessage?) {
@ -2809,29 +2849,27 @@ class ChatController(args: Bundle) :
private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
if (conversationUser == null) return false
if (message.systemMessageType != ChatMessage.SystemMessageType.DUMMY) return false
if (message.isDeleted) return false
if (message.hasFileAttachment()) return false
if (OBJECT_MESSAGE.equals(message.message)) return false
val isOlderThanSixHours = message
.createdAt
?.before(Date(System.currentTimeMillis() - AGE_THREHOLD_FOR_DELETE_MESSAGE)) == true
if (isOlderThanSixHours) return false
val isUserAllowedByPrivileges = if (message.actorId == conversationUser.userId) {
true
} else {
currentConversation!!.canModerate(conversationUser)
}
if (!isUserAllowedByPrivileges) return false
if (!CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "delete-messages")) return false
val isOlderThanSixHours = message
.createdAt
?.before(Date(System.currentTimeMillis() - AGE_THREHOLD_FOR_DELETE_MESSAGE)) == true
return true
return when {
!isUserAllowedByPrivileges -> false
isOlderThanSixHours -> false
message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
message.isDeleted -> false
message.hasFileAttachment() -> false
OBJECT_MESSAGE == message.message -> false
!CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "delete-messages") -> false
!hasChatPermission -> false
else -> true
}
}
override fun hasContentFor(message: ChatMessage, type: Byte): Boolean {

View File

@ -86,6 +86,7 @@ import com.nextcloud.talk.models.json.statuses.StatusesOverall;
import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment;
import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.AttendeePermissionsUtil;
import com.nextcloud.talk.utils.ClosedInterfaceImpl;
import com.nextcloud.talk.utils.ConductorRemapping;
import com.nextcloud.talk.utils.DisplayUtils;
@ -359,7 +360,6 @@ public class ConversationsListController extends BaseController implements Searc
showShareToScreen = !showShareToScreen && hasActivityActionSendIntent();
if (showShareToScreen) {
hideSearchBar();
getActionBar().setTitle(R.string.send_to_three_dots);
@ -867,13 +867,25 @@ public class ConversationsListController extends BaseController implements Searc
public boolean onItemClick(View view, int position) {
try {
selectedConversation = ((ConversationItem) Objects.requireNonNull(adapter.getItem(position))).getModel();
if (selectedConversation != null && getActivity() != null) {
boolean hasChatPermission =
new AttendeePermissionsUtil(selectedConversation.permissions).hasChatPermission(currentUser);
if (showShareToScreen) {
handleSharedData();
showShareToScreen = false;
if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
handleSharedData();
showShareToScreen = false;
} else {
Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
}
} else if (forwardMessage) {
openConversation(bundle.getString(BundleKeys.INSTANCE.getKEY_FORWARD_MSG_TEXT()));
forwardMessage = false;
if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
openConversation(bundle.getString(BundleKeys.INSTANCE.getKEY_FORWARD_MSG_TEXT()));
forwardMessage = false;
} else {
Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
}
} else {
openConversation();
}
@ -885,6 +897,11 @@ public class ConversationsListController extends BaseController implements Searc
return true;
}
private Boolean isReadOnlyConversation(Conversation conversation) {
return conversation.conversationReadOnlyState ==
Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY;
}
private void handleSharedData() {
collectDataFromIntent();
if (!textToPaste.isEmpty()) {
@ -930,7 +947,8 @@ public class ConversationsListController extends BaseController implements Searc
.setNegativeButton(R.string.nc_no, new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "sharing files aborted");
Log.d(TAG, "sharing files aborted, going back to share-to screen");
showShareToScreen = true;
}
})
.show();

View File

@ -111,6 +111,9 @@ public class Conversation {
@JsonField(name = "notificationCalls")
public Integer notificationCalls;
@JsonField(name = "permissions")
public int permissions;
public boolean isPublic() {
return (ConversationType.ROOM_PUBLIC_CALL.equals(type));
}
@ -281,6 +284,10 @@ public class Conversation {
public Integer getNotificationCalls() { return notificationCalls; }
public int getPermissions() {
return permissions;
}
public void setRoomId(String roomId) {
this.roomId = roomId;
}
@ -398,6 +405,9 @@ public class Conversation {
this.unreadMentionDirect = unreadMentionDirect;
}
public void setPermissions(int permissions) {
this.permissions = permissions;
}
@Override
public boolean equals(Object o) {
@ -500,6 +510,9 @@ public class Conversation {
if (!Objects.equals(notificationCalls, that.notificationCalls)) {
return false;
}
if (permissions != that.permissions) {
return false;
}
return Objects.equals(canDeleteConversation, that.canDeleteConversation);
}
@ -540,6 +553,7 @@ public class Conversation {
result = 31 * result + (canLeaveConversation != null ? canLeaveConversation.hashCode() : 0);
result = 31 * result + (canDeleteConversation != null ? canDeleteConversation.hashCode() : 0);
result = 31 * result + (notificationCalls != null ? notificationCalls.hashCode() : 0);
result = 31 * result + permissions;
return result;
}
@ -577,6 +591,7 @@ public class Conversation {
", canLeaveConversation=" + canLeaveConversation +
", canDeleteConversation=" + canDeleteConversation +
", notificationCalls=" + notificationCalls +
", permissions=" + permissions +
'}';
}

View File

@ -58,6 +58,7 @@ class MessageActionsDialog(
private val user: UserEntity?,
private val currentConversation: Conversation?,
private val showMessageDeletionButton: Boolean,
private val hasChatPermission: Boolean,
private val ncApi: NcApi
) : BottomSheetDialog(chatController.activity!!, R.style.BottomSheetDialogThemeNoFloating) {
@ -71,9 +72,9 @@ class MessageActionsDialog(
setContentView(dialogMessageActionsBinding.root)
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
initEmojiBar()
initEmojiBar(hasChatPermission)
initMenuItemCopy(!message.isDeleted)
initMenuReplyToMessage(message.replyable)
initMenuReplyToMessage(message.replyable && hasChatPermission)
initMenuReplyPrivately(
message.replyable &&
hasUserId(user) &&
@ -160,8 +161,9 @@ class MessageActionsDialog(
}
}
private fun initEmojiBar() {
if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "reactions") &&
private fun initEmojiBar(hasChatPermission: Boolean) {
if (hasChatPermission &&
CapabilitiesUtil.hasSpreedFeatureCapability(user, "reactions") &&
Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY !=
currentConversation?.conversationReadOnlyState &&
isReactableMessageType(message)

View File

@ -63,6 +63,7 @@ class ShowReactionsDialog(
private val currentConversation: Conversation?,
private val chatMessage: ChatMessage,
private val userEntity: UserEntity?,
private val hasChatPermission: Boolean,
private val ncApi: NcApi
) : BottomSheetDialog(activity), ReactionItemClickListener {
@ -183,7 +184,7 @@ class ShowReactionsDialog(
}
override fun onClick(reactionItem: ReactionItem) {
if (reactionItem.reactionVoter.actorId?.equals(userEntity?.userId) == true) {
if (hasChatPermission && reactionItem.reactionVoter.actorId?.equals(userEntity?.userId) == true) {
deleteReaction(chatMessage, reactionItem.reaction!!)
dismiss()
}

View File

@ -0,0 +1,72 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.utils
import com.nextcloud.talk.models.database.CapabilitiesUtil
import com.nextcloud.talk.models.database.UserEntity
/**
* see https://nextcloud-talk.readthedocs.io/en/latest/constants/#attendee-permissions
*/
class AttendeePermissionsUtil(flag: Int) {
var isDefault: Boolean = false
var isCustom: Boolean = false
var canStartCall: Boolean = false
var canJoinCall: Boolean = false
var canIgnoreLobby: Boolean = false
var canPublishAudio: Boolean = false
var canPublishVideo: Boolean = false
var canPublishScreen: Boolean = false
private var hasChatPermission: Boolean = false
init {
isDefault = (flag and DEFAULT) == DEFAULT
isCustom = (flag and CUSTOM) == CUSTOM
canStartCall = (flag and START_CALL) == START_CALL
canJoinCall = (flag and JOIN_CALL) == JOIN_CALL
canIgnoreLobby = (flag and CAN_IGNORE_LOBBY) == CAN_IGNORE_LOBBY
canPublishAudio = (flag and PUBLISH_AUDIO) == PUBLISH_AUDIO
canPublishVideo = (flag and PUBLISH_VIDEO) == PUBLISH_VIDEO
canPublishScreen = (flag and PUBLISH_SCREEN) == PUBLISH_SCREEN
hasChatPermission = (flag and CHAT) == CHAT
}
fun hasChatPermission(user: UserEntity): Boolean {
if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "chat-permission")) {
return hasChatPermission
}
// if capability is not available then the spreed version doesn't support to restrict this
return true
}
companion object {
val TAG = AttendeePermissionsUtil::class.simpleName
const val DEFAULT = 0
const val CUSTOM = 1
const val START_CALL = 2
const val JOIN_CALL = 4
const val CAN_IGNORE_LOBBY = 8
const val PUBLISH_AUDIO = 16
const val PUBLISH_VIDEO = 32
const val PUBLISH_SCREEN = 64
const val CHAT = 128
}
}

View File

@ -392,6 +392,8 @@
<string name="send_to_three_dots">Send to …</string>
<string name="read_storage_no_permission">Sharing files from storage is not possible without permissions</string>
<string name="open_in_files_app">Open in Files app</string>
<string name="send_to_forbidden">You are not allowed to share content to this chat</string>
<!-- Upload -->
<string name="nc_add_file">Add to conversation</string>

View File

@ -0,0 +1,47 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.utils
import junit.framework.TestCase
import org.junit.Test
class AttendeePermissionsUtilTest : TestCase() {
@Test
fun test_areFlagsSet() {
val attendeePermissionsUtil =
AttendeePermissionsUtil(
AttendeePermissionsUtil.PUBLISH_SCREEN or
AttendeePermissionsUtil.JOIN_CALL or
AttendeePermissionsUtil.DEFAULT
)
assert(attendeePermissionsUtil.canPublishScreen)
assert(attendeePermissionsUtil.canJoinCall)
assert(attendeePermissionsUtil.isDefault)
assertFalse(attendeePermissionsUtil.isCustom)
assertFalse(attendeePermissionsUtil.canStartCall)
assertFalse(attendeePermissionsUtil.canIgnoreLobby)
assertFalse(attendeePermissionsUtil.canPublishAudio)
assertFalse(attendeePermissionsUtil.canPublishVideo)
}
}