Răsfoiți Sursa

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

Feature/1948/respect chat permission
Marcel Hibbe 3 ani în urmă
părinte
comite
2cb2cce39e

+ 99 - 61
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -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))
+            }
+
+            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 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 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 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 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
-                }
-            })
+                    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 {

+ 24 - 6
app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java

@@ -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();

+ 15 - 0
app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java

@@ -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 +
                 '}';
     }
 

+ 6 - 4
app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt

@@ -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)

+ 2 - 1
app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt

@@ -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()
         }

+ 72 - 0
app/src/main/java/com/nextcloud/talk/utils/AttendeePermissionsUtil.kt

@@ -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
+    }
+}

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -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>

+ 47 - 0
app/src/test/java/com/nextcloud/talk/utils/AttendeePermissionsUtilTest.kt

@@ -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)
+    }
+}