diff --git a/NOTICE.txt b/NOTICE.txt index 21cdf72ba008..1fb5981b936c 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -9,6 +9,90 @@ This document includes a list of open source components used in Mattermost Serve -------- +## @atlaskit/pragmatic-drag-and-drop + +This product contains '@atlaskit/pragmatic-drag-and-drop' by Atlassian Pty Ltd. + +The core package for Pragmatic drag and drop - enabling fast drag and drop for any experience on any tech stack + +* HOMEPAGE: + * https://atlassian.design/components/pragmatic-drag-and-drop/ + +* LICENSE: Apache-2.0 + +Copyright 2024 Atlassian Pty 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. + + +--- + +## @atlaskit/pragmatic-drag-and-drop-hitbox + +This product contains '@atlaskit/pragmatic-drag-and-drop-hitbox' by Atlassian Pty Ltd. + +An optional package for Pragmatic drag and drop that enables the attaching of interaction information to a drop target + +* HOMEPAGE: + * https://atlassian.design/components/pragmatic-drag-and-drop/ + +* LICENSE: Apache-2.0 + +Copyright 2024 Atlassian Pty 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. + + +--- + +## @atlaskit/pragmatic-drag-and-drop-react-drop-indicator + +This product contains '@atlaskit/pragmatic-drag-and-drop-react-drop-indicator' by Atlassian Pty Ltd. + +An optional Pragmatic drag and drop package containing react components that provide a visual indication about what the user will achieve when the drop (eg lines) + +* HOMEPAGE: + * https://atlassian.design/components/pragmatic-drag-and-drop/ + +* LICENSE: Apache-2.0 + +Copyright 2024 Atlassian Pty 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. + + +--- + ## @floating-ui/react This product contains '@floating-ui/react' by atomiks. @@ -10578,6 +10662,222 @@ Data model artifacts for Prometheus. limitations under the License. +--- + +## prometheus/common + +This product contains 'prometheus/common' by Prometheus. + +Go libraries shared across Prometheus components and libraries. + +* HOMEPAGE: + * https://github.com/prometheus/common + +* LICENSE: Apache License 2.0 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + --- ## prop-types diff --git a/api/Makefile b/api/Makefile index c54262a3a23f..041d7f20cbe9 100644 --- a/api/Makefile +++ b/api/Makefile @@ -21,7 +21,6 @@ build-v4: node_modules playbooks @cat $(V4_SRC)/preferences.yaml >> $(V4_YAML) @cat $(V4_SRC)/files.yaml >> $(V4_YAML) @cat $(V4_SRC)/recaps.yaml >> $(V4_YAML) - @cat $(V4_SRC)/ai.yaml >> $(V4_YAML) @cat $(V4_SRC)/uploads.yaml >> $(V4_YAML) @cat $(V4_SRC)/jobs.yaml >> $(V4_YAML) @cat $(V4_SRC)/system.yaml >> $(V4_YAML) @@ -36,7 +35,6 @@ build-v4: node_modules playbooks @cat $(V4_SRC)/commands.yaml >> $(V4_YAML) @cat $(V4_SRC)/oauth.yaml >> $(V4_YAML) @cat $(V4_SRC)/elasticsearch.yaml >> $(V4_YAML) - @cat $(V4_SRC)/bleve.yaml >> $(V4_YAML) @cat $(V4_SRC)/dataretention.yaml >> $(V4_YAML) @cat $(V4_SRC)/plugins.yaml >> $(V4_YAML) @cat $(V4_SRC)/roles.yaml >> $(V4_YAML) diff --git a/api/v4/source/ai.yaml b/api/v4/source/ai.yaml deleted file mode 100644 index 183bcbbf1892..000000000000 --- a/api/v4/source/ai.yaml +++ /dev/null @@ -1,54 +0,0 @@ - /api/v4/ai/agents: - get: - tags: - - ai - summary: Get available AI agents - description: > - Retrieve all available AI agents from the AI plugin's bridge API. - If a user ID is provided, only agents accessible to that user are returned. - - ##### Permissions - - Must be authenticated. - - __Minimum server version__: 11.2 - operationId: GetAIAgents - responses: - "200": - description: AI agents retrieved successfully - content: - application/json: - schema: - $ref: "#/components/schemas/AgentsResponse" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalServerError" - /api/v4/ai/services: - get: - tags: - - ai - summary: Get available AI services - description: > - Retrieve all available AI services from the AI plugin's bridge API. - If a user ID is provided, only services accessible to that user - (via their permitted bots) are returned. - - ##### Permissions - - Must be authenticated. - - __Minimum server version__: 11.2 - operationId: GetAIServices - responses: - "200": - description: AI services retrieved successfully - content: - application/json: - schema: - $ref: "#/components/schemas/ServicesResponse" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalServerError" - diff --git a/api/v4/source/bleve.yaml b/api/v4/source/bleve.yaml deleted file mode 100644 index 35ce63b2c910..000000000000 --- a/api/v4/source/bleve.yaml +++ /dev/null @@ -1,28 +0,0 @@ - /api/v4/bleve/purge_indexes: - post: - tags: - - bleve - summary: Purge all Bleve indexes - description: > - Deletes all Bleve indexes and their contents. After calling this - endpoint, it is - - necessary to schedule a new Bleve indexing job to repopulate the indexes. - - __Minimum server version__: 5.24 - - ##### Permissions - - Must have `sysconsole_write_experimental` permission. - operationId: PurgeBleveIndexes - responses: - "200": - description: Indexes purged successfully. - content: - application/json: - schema: - $ref: "#/components/schemas/StatusOK" - "500": - $ref: "#/components/responses/InternalServerError" - "501": - $ref: "#/components/responses/NotImplemented" diff --git a/api/v4/source/bots.yaml b/api/v4/source/bots.yaml index c30ccf8372c0..b5334183351a 100644 --- a/api/v4/source/bots.yaml +++ b/api/v4/source/bots.yaml @@ -279,127 +279,6 @@ $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" - "/api/v4/bots/{bot_user_id}/icon": - get: - tags: - - bots - summary: Get bot's LHS icon - description: | - Get a bot's LHS icon image based on bot_user_id string parameter. - ##### Permissions - Must be logged in. - __Minimum server version__: 5.14 - operationId: GetBotIconImage - parameters: - - name: bot_user_id - in: path - description: Bot user ID - required: true - schema: - type: string - responses: - "200": - description: Bot's LHS icon image - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" - "501": - $ref: "#/components/responses/NotImplemented" - post: - tags: - - bots - summary: Set bot's LHS icon image - description: > - Set a bot's LHS icon image based on bot_user_id string parameter. Icon - image must be SVG format, all other formats are rejected. - - ##### Permissions - - Must have `manage_bots` permission. - - __Minimum server version__: 5.14 - operationId: SetBotIconImage - parameters: - - name: bot_user_id - in: path - description: Bot user ID - required: true - schema: - type: string - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - image: - description: SVG icon image to be uploaded - type: string - format: binary - required: - - image - responses: - "200": - description: SVG icon image set successful - content: - application/json: - schema: - $ref: "#/components/schemas/StatusOK" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "413": - $ref: "#/components/responses/TooLarge" - "500": - $ref: "#/components/responses/InternalServerError" - "501": - $ref: "#/components/responses/NotImplemented" - delete: - tags: - - bots - summary: Delete bot's LHS icon image - description: | - Delete bot's LHS icon image based on bot_user_id string parameter. - ##### Permissions - Must have `manage_bots` permission. - __Minimum server version__: 5.14 - operationId: DeleteBotIconImage - parameters: - - name: bot_user_id - in: path - description: Bot user ID - required: true - schema: - type: string - responses: - "200": - description: Icon image deletion successful - content: - application/json: - schema: - $ref: "#/components/schemas/StatusOK" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" - "501": - $ref: "#/components/responses/NotImplemented" "/api/v4/bots/{bot_user_id}/convert_to_user": post: tags: diff --git a/api/v4/source/channels.yaml b/api/v4/source/channels.yaml index 7a3794d85964..9566147659a6 100644 --- a/api/v4/source/channels.yaml +++ b/api/v4/source/channels.yaml @@ -1980,6 +1980,83 @@ $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" + "/api/v4/channels/members/{user_id}/mark_read": + post: + tags: + - channels + summary: Mark multiple channels as read + description: | + Mark multiple channels as viewed for the given user. + ##### Permissions + Must be logged in as the user or have `edit_other_users` permission. + operationId: MarkChannelsReadForUser + parameters: + - in: path + name: user_id + description: User ID to mark channels read for + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: array + items: + type: string + required: true + responses: + "200": + description: Channels marked as read + content: + application/json: + schema: + type: object + properties: + status: + type: string + last_viewed_at_times: + type: object + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "/api/v4/channels/stats/member_count": + post: + tags: + - channels + summary: Get member counts for multiple channels + description: | + Get channel member counts for a list of channel IDs. + ##### Permissions + Must have access to member count for all requested channels. + operationId: GetChannelsMemberCount + requestBody: + content: + application/json: + schema: + type: array + items: + type: string + required: true + responses: + "200": + description: Channel member counts retrieval successful + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int64 + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" "/api/v4/channels/members/{user_id}/view": post: tags: @@ -2916,3 +2993,50 @@ $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" + "/api/v4/channels/{channel_id}/convert_to_channel": + post: + tags: + - channels + - group message + summary: Convert group message to private channel + description: | + Converts a group message channel into a private channel in the specified team. + ##### Permissions + Must have `create_private_channel` permission in the destination team. + operationId: ConvertGroupMessageToChannel + parameters: + - name: channel_id + in: path + description: Group message channel ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - channel_id + - team_id + properties: + channel_id: + type: string + team_id: + type: string + responses: + "200": + description: Conversion successful + content: + application/json: + schema: + $ref: "#/components/schemas/Channel" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" diff --git a/api/v4/source/cloud.yaml b/api/v4/source/cloud.yaml index ef47b59c6f1d..490304896d62 100644 --- a/api/v4/source/cloud.yaml +++ b/api/v4/source/cloud.yaml @@ -58,71 +58,6 @@ $ref: "#/components/responses/Forbidden" "501": $ref: "#/components/responses/NotImplemented" - /api/v4/cloud/payment: - post: - tags: - - cloud - summary: Create a customer setup payment intent - description: | - Creates a customer setup payment intent for the given Mattermost cloud installation. - - ##### Permissions - - Must have `manage_system` permission and be licensed for Cloud. - - __Minimum server version__: 5.28 - __Note:__: This is intended for internal use and is subject to change. - - operationId: CreateCustomerPayment - responses: - "201": - description: Payment setup intented created - content: - application/json: - schema: - $ref: "#/components/schemas/PaymentSetupIntent" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "501": - $ref: "#/components/responses/NotImplemented" - /api/v4/cloud/payment/confirm: - post: - tags: - - cloud - summary: Completes the payment setup intent - description: > - Confirms the payment setup intent initiated when posting to `/cloud/payment`. - - ##### Permissions - - Must have `manage_system` permission and be licensed for Cloud. - - __Minimum server version__: 5.28 - __Note:__ This is intended for internal use and is subject to change. - operationId: ConfirmCustomerPayment - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - stripe_setup_intent_id: - type: string - responses: - "200": - description: Payment setup intent confirmed successfully - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "501": - $ref: "#/components/responses/NotImplemented" /api/v4/cloud/customer: get: tags: @@ -237,6 +172,73 @@ $ref: "#/components/responses/Forbidden" "501": $ref: "#/components/responses/NotImplemented" + /api/v4/cloud/validate-business-email: + post: + tags: + - cloud + summary: Validate business email + description: > + Validate whether an email address is considered a business email by the cloud service. + + ##### Permissions + Must be authenticated. + operationId: ValidateBusinessEmail + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + properties: + email: + type: string + responses: + "200": + description: Email validation successful + content: + application/json: + schema: + type: object + properties: + is_valid: + type: boolean + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "501": + $ref: "#/components/responses/NotImplemented" + /api/v4/cloud/validate-workspace-business-email: + post: + tags: + - cloud + summary: Validate workspace business email + description: > + Validate the current workspace customer/admin email as a business email. + + ##### Permissions + Must have `sysconsole_write_billing` permission and be licensed for Cloud. + operationId: ValidateWorkspaceBusinessEmail + responses: + "200": + description: Workspace email validation successful + content: + application/json: + schema: + type: object + properties: + is_valid: + type: boolean + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "501": + $ref: "#/components/responses/NotImplemented" /api/v4/cloud/subscription: get: tags: @@ -360,6 +362,22 @@ $ref: "#/components/responses/Forbidden" "501": $ref: "#/components/responses/NotImplemented" + /api/v4/hosted_customer/signup_available: + get: + tags: + - cloud + summary: Check hosted signup availability + description: > + Checks whether hosted signup is available for self-hosted workspaces. + + ##### Permissions + Must be authenticated. + operationId: HostedCustomerSignupAvailable + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "501": + $ref: "#/components/responses/NotImplemented" /api/v4/cloud/check-cws-connection: get: tags: diff --git a/api/v4/source/commands.yaml b/api/v4/source/commands.yaml index c2019d6c8eee..0eb42ea4da47 100644 --- a/api/v4/source/commands.yaml +++ b/api/v4/source/commands.yaml @@ -121,7 +121,7 @@ $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" - '/api/v4/teams/{team_id}/commands/autocomplete_suggestions': + "/api/v4/teams/{team_id}/commands/autocomplete_suggestions": get: tags: - commands diff --git a/api/v4/source/definitions.yaml b/api/v4/source/definitions.yaml index e2abece907c7..0968f660efef 100644 --- a/api/v4/source/definitions.yaml +++ b/api/v4/source/definitions.yaml @@ -473,6 +473,200 @@ components: type: string metadata: $ref: "#/components/schemas/PostMetadata" + PostPriority: + type: object + description: Priority metadata associated with a post or draft. + properties: + priority: + type: string + description: The priority label of a post, either empty, important, or urgent. + enum: + - "" + - important + - urgent + requested_ack: + type: boolean + description: Whether the post author has requested acknowledgements. + persistent_notifications: + type: boolean + description: Whether persistent notifications are enabled for the post. + PostInfo: + type: object + description: Additional team and channel context metadata for a post. + properties: + channel_id: + type: string + description: The ID of the channel containing the post. + channel_type: + type: string + description: The type of the channel containing the post. + channel_display_name: + type: string + description: The display name of the channel containing the post. + has_joined_channel: + type: boolean + description: Whether the requesting user is already a member of the channel. + team_id: + type: string + description: The ID of the team containing the channel, if applicable. + team_type: + type: string + description: The type of the team containing the channel, if applicable. + team_display_name: + type: string + description: The display name of the team containing the channel, if applicable. + has_joined_team: + type: boolean + description: Whether the requesting user is already a member of the team. + Draft: + type: object + properties: + create_at: + type: integer + format: int64 + update_at: + type: integer + format: int64 + delete_at: + type: integer + format: int64 + description: Deprecated. Drafts are hard-deleted. + user_id: + type: string + channel_id: + type: string + root_id: + type: string + message: + type: string + type: + type: string + props: + type: object + additionalProperties: true + file_ids: + type: array + items: + type: string + metadata: + $ref: "#/components/schemas/PostMetadata" + priority: + $ref: "#/components/schemas/PostPriority" + DraftUpsertRequest: + type: object + required: + - channel_id + - message + properties: + channel_id: + type: string + root_id: + type: string + message: + type: string + description: Draft message. Set to an empty string to delete the draft. + type: + type: string + props: + type: object + additionalProperties: true + file_ids: + type: array + items: + type: string + priority: + $ref: "#/components/schemas/PostPriority" + NotifyAdminToUpgradeRequest: + type: object + properties: + trial_notification: + type: boolean + required_plan: + type: string + required_feature: + type: string + PluginReattachAddress: + type: object + properties: + Name: + type: string + Net: + type: string + PluginReattachConfig: + type: object + properties: + Protocol: + type: string + ProtocolVersion: + type: integer + Addr: + $ref: "#/components/schemas/PluginReattachAddress" + Pid: + type: integer + Test: + type: boolean + PluginReattachRequest: + type: object + required: + - Manifest + - PluginReattachConfig + properties: + Manifest: + $ref: "#/components/schemas/PluginManifest" + PluginReattachConfig: + $ref: "#/components/schemas/PluginReattachConfig" + InstallMarketplacePluginRequest: + type: object + required: + - id + properties: + id: + type: string + description: The ID of the plugin to install. + version: + type: string + description: Optional plugin version. If omitted, the latest compatible version is installed. + RemoteClusterMsg: + type: object + properties: + id: + type: string + topic: + type: string + create_at: + type: integer + format: int64 + payload: + description: Raw message payload. + type: object + additionalProperties: true + RemoteClusterFrame: + type: object + properties: + remote_id: + type: string + msg: + $ref: "#/components/schemas/RemoteClusterMsg" + RemoteClusterPing: + type: object + properties: + sent_at: + type: integer + format: int64 + recv_at: + type: integer + format: int64 + RemoteClusterResponse: + type: object + properties: + status: + type: string + err: + type: string + payload: + description: Raw response payload. + type: object + additionalProperties: true PropertyField: type: object properties: @@ -716,17 +910,11 @@ components: items: $ref: "#/components/schemas/Reaction" priority: - type: object + allOf: + - $ref: "#/components/schemas/PostPriority" description: > Post priority set for this post. This field will be null if no priority metadata has been set. - properties: - priority: - type: string - description: The priority label of a post, could be either empty, important, or urgent. - requested_ack: - type: boolean - description: Whether the post author has requested for acknowledgements or not. acknowledgements: type: array description: > @@ -3340,13 +3528,6 @@ components: active: type: integer nullable: true - PaymentSetupIntent: - type: object - properties: - id: - type: string - client_secret: - type: string PaymentMethod: type: object properties: @@ -4093,12 +4274,6 @@ components: type: array items: $ref: "#/components/schemas/UserThread" - LicenseRenewalLink: - type: object - properties: - renewal_link: - description: License renewal link - type: string System: type: object properties: diff --git a/api/v4/source/exports.yaml b/api/v4/source/exports.yaml index badbe391705c..4dbf619892a1 100644 --- a/api/v4/source/exports.yaml +++ b/api/v4/source/exports.yaml @@ -84,3 +84,43 @@ $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalServerError" + "/api/v4/exports/{export_name}/presign-url": + post: + tags: + - exports + summary: Create a presigned URL for export download + description: | + Creates a presigned URL for downloading an export file. + + __Minimum server version__: 5.33 + + ##### Permissions + Must have `manage_system` permission. + operationId: PresignExport + parameters: + - name: export_name + in: path + description: The name of the export file + required: true + schema: + type: string + responses: + "200": + description: Presigned URL created successfully + content: + application/json: + schema: + type: object + properties: + url: + type: string + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "500": + $ref: "#/components/responses/InternalServerError" diff --git a/api/v4/source/files.yaml b/api/v4/source/files.yaml index e116ae0a1971..b8c37223166b 100644 --- a/api/v4/source/files.yaml +++ b/api/v4/source/files.yaml @@ -126,6 +126,31 @@ $ref: "#/components/responses/NotFound" "501": $ref: "#/components/responses/NotImplemented" + head: + tags: + - files + summary: Get file metadata headers + description: | + Performs the same permission and existence checks as getting a file, but returns headers only. + operationId: HeadFile + parameters: + - name: file_id + in: path + description: The ID of the file to get + required: true + schema: + type: string + responses: + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "501": + $ref: "#/components/responses/NotImplemented" "/api/v4/files/{file_id}/thumbnail": get: tags: @@ -163,6 +188,31 @@ $ref: "#/components/responses/NotFound" "501": $ref: "#/components/responses/NotImplemented" + head: + tags: + - files + summary: Get thumbnail metadata headers + description: | + Performs the same permission and existence checks as getting a thumbnail, but returns headers only. + operationId: HeadFileThumbnail + parameters: + - name: file_id + in: path + description: The ID of the file to get + required: true + schema: + type: string + responses: + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "501": + $ref: "#/components/responses/NotImplemented" "/api/v4/files/{file_id}/preview": get: tags: @@ -200,6 +250,31 @@ $ref: "#/components/responses/NotFound" "501": $ref: "#/components/responses/NotImplemented" + head: + tags: + - files + summary: Get preview metadata headers + description: | + Performs the same permission and existence checks as getting a preview, but returns headers only. + operationId: HeadFilePreview + parameters: + - name: file_id + in: path + description: The ID of the file to get + required: true + schema: + type: string + responses: + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "501": + $ref: "#/components/responses/NotImplemented" "/api/v4/files/{file_id}/link": get: tags: @@ -334,6 +409,37 @@ $ref: "#/components/responses/NotFound" "501": $ref: "#/components/responses/NotImplemented" + head: + tags: + - files + summary: Get public file metadata headers + description: | + Performs the same validation checks as getting a public file, but returns headers only. + operationId: HeadFilePublic + parameters: + - name: file_id + in: path + description: The ID of the file to get + required: true + schema: + type: string + - name: h + in: query + description: File hash + required: true + schema: + type: string + responses: + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "501": + $ref: "#/components/responses/NotImplemented" "/api/v4/teams/{team_id}/files/search": post: diff --git a/api/v4/source/groups.yaml b/api/v4/source/groups.yaml index aab9b25c6b3f..eba8e32b3a63 100644 --- a/api/v4/source/groups.yaml +++ b/api/v4/source/groups.yaml @@ -307,9 +307,11 @@ - groups summary: Link a team to a group description: | - Link a team to a group + Link a team to a group. + ##### Permissions - Must have `manage_team` permission. + Requires `invite_user` on the target team, or `sysconsole_write_user_management_groups`. + If the group has `allow_reference` disabled, also requires `sysconsole_read_user_management_groups`. __Minimum server version__: 5.11 operationId: LinkGroupSyncableForTeam @@ -322,13 +324,13 @@ type: string - name: team_id in: path - description: Team GUID + description: Team GUID. required: true schema: type: string responses: "201": - description: Team successfully linked to group + description: Team linked to group content: application/json: schema: @@ -344,11 +346,13 @@ delete: tags: - groups - summary: Delete a link from a team to a group + summary: Unlink a team from a group description: | - Delete a link from a team to a group + Delete a link between a team and a group. + ##### Permissions - Must have `manage_team` permission. + Requires `invite_user` on the target team, or `sysconsole_write_user_management_groups`. + If the group has `allow_reference` disabled, also requires `sysconsole_read_user_management_groups`. __Minimum server version__: 5.11 operationId: UnlinkGroupSyncableForTeam @@ -361,13 +365,13 @@ type: string - name: team_id in: path - description: Team GUID + description: Team GUID. required: true schema: type: string responses: "200": - description: Successfully deleted link between team and group + description: Team unlinked from group content: application/json: schema: @@ -385,15 +389,15 @@ tags: - groups summary: Link a channel to a group - description: > - Link a channel to a group + description: | + Link a channel to a group. ##### Permissions - - If the channel is private, you must have `manage_private_channel_members` permission. - - Otherwise, you must have the `manage_public_channel_members` permission. - + Requires `manage_private_channel_members` (private channel) or + `manage_public_channel_members` (public channel) on the target channel. + If this is the group's first linkage into the channel's team context, also + requires `invite_user` on the team, or `sysconsole_write_user_management_groups`. + If the group has `allow_reference` disabled, also requires `sysconsole_read_user_management_groups`. __Minimum server version__: 5.11 operationId: LinkGroupSyncableForChannel @@ -406,13 +410,13 @@ type: string - name: channel_id in: path - description: Channel GUID + description: Channel GUID. required: true schema: type: string responses: "201": - description: Channel successfully linked to group + description: Channel linked to group content: application/json: schema: @@ -428,16 +432,16 @@ delete: tags: - groups - summary: Delete a link from a channel to a group - description: > - Delete a link from a channel to a group + summary: Unlink a channel from a group + description: | + Delete a link between a channel and a group. ##### Permissions - - If the channel is private, you must have `manage_private_channel_members` permission. - - Otherwise, you must have the `manage_public_channel_members` permission. - + Requires `manage_private_channel_members` (private channel) or + `manage_public_channel_members` (public channel) on the target channel. + If unlinking would leave the group with no remaining linkage in that channel's team context (last/only linkage for that team), also + requires `invite_user` on the team, or `sysconsole_write_user_management_groups`. + If the group has `allow_reference` disabled, also requires `sysconsole_read_user_management_groups`. __Minimum server version__: 5.11 operationId: UnlinkGroupSyncableForChannel @@ -450,13 +454,13 @@ type: string - name: channel_id in: path - description: Channel GUID + description: Channel GUID. required: true schema: type: string responses: "200": - description: Successfully deleted link between channel and group + description: Channel unlinked from group content: application/json: schema: @@ -473,9 +477,10 @@ get: tags: - groups - summary: Get GroupSyncable from Team ID + summary: Get a team syncable for a group description: | - Get the GroupSyncable object with group_id and team_id from params + Get the GroupSyncableTeam object with the provided group and team identifiers. + ##### Permissions Must have `manage_system` permission. @@ -490,13 +495,13 @@ type: string - name: team_id in: path - description: Team GUID + description: Team GUID. required: true schema: type: string responses: "200": - description: GroupSyncable object retrieval successful + description: Team syncable retrieved content: application/json: schema: @@ -515,9 +520,10 @@ get: tags: - groups - summary: Get GroupSyncable from channel ID + summary: Get a channel syncable for a group description: | - Get the GroupSyncable object with group_id and channel_id from params + Get the GroupSyncableChannel object with the provided group and channel identifiers. + ##### Permissions Must have `manage_system` permission. @@ -532,13 +538,13 @@ type: string - name: channel_id in: path - description: Channel GUID + description: Channel GUID. required: true schema: type: string responses: "200": - description: GroupSyncable object retrieval successful + description: Channel syncable retrieved content: application/json: schema: @@ -557,9 +563,10 @@ get: tags: - groups - summary: Get group teams + summary: Get team syncables for a group description: | - Retrieve the list of teams associated to the group + Retrieve the list of team syncables associated with the group. + ##### Permissions Must have `manage_system` permission. @@ -574,7 +581,7 @@ type: string responses: "200": - description: Teams list retrieval successful + description: Team syncables retrieved content: application/json: schema: @@ -595,9 +602,10 @@ get: tags: - groups - summary: Get group channels + summary: Get channel syncables for a group description: | - Retrieve the list of channels associated to the group + Retrieve the list of channel syncables associated with the group. + ##### Permissions Must have `manage_system` permission. @@ -612,7 +620,7 @@ type: string responses: "200": - description: Channel list retrieval successful + description: Channel syncables retrieved content: application/json: schema: @@ -633,19 +641,14 @@ put: tags: - groups - summary: Patch a GroupSyncable associated to Team + summary: Patch a team syncable for a group description: > - Partially update a GroupSyncable by providing only the fields you want - to update. Omitted fields will not be updated. The fields that can be - updated are defined in the request body, all other provided fields will - be ignored. - + Partially update a GroupSyncableTeam by providing only the fields you want + to update. Omitted fields will not be updated. ##### Permissions - Must have `manage_system` permission. - __Minimum server version__: 5.11 operationId: PatchGroupSyncableForTeam parameters: @@ -657,24 +660,23 @@ type: string - name: team_id in: path - description: Team GUID + description: Team GUID. required: true schema: type: string requestBody: - description: GroupSyncable object that is to be updated - required: true - content: - application/json: - schema: - type: object - properties: - auto_add: - type: boolean - + description: GroupSyncableTeam object that is to be updated + required: true + content: + application/json: + schema: + type: object + properties: + auto_add: + type: boolean responses: "200": - description: GroupSyncable patch successful + description: Team syncable patched content: application/json: schema: @@ -691,19 +693,14 @@ put: tags: - groups - summary: Patch a GroupSyncable associated to Channel + summary: Patch a channel syncable for a group description: > - Partially update a GroupSyncable by providing only the fields you want - to update. Omitted fields will not be updated. The fields that can be - updated are defined in the request body, all other provided fields will - be ignored. - + Partially update a GroupSyncableChannel by providing only the fields you + want to update. Omitted fields will not be updated. ##### Permissions - Must have `manage_system` permission. - __Minimum server version__: 5.11 operationId: PatchGroupSyncableForChannel parameters: @@ -715,23 +712,23 @@ type: string - name: channel_id in: path - description: Channel GUID + description: Channel GUID. required: true schema: type: string requestBody: - description: GroupSyncable object that is to be updated - required: true - content: - application/json: - schema: - type: object - properties: - auto_add: - type: boolean + description: GroupSyncableChannel object that is to be updated + required: true + content: + application/json: + schema: + type: object + properties: + auto_add: + type: boolean responses: "200": - description: GroupSyncable patch successful + description: Channel syncable patched content: application/json: schema: @@ -1102,68 +1099,69 @@ "501": $ref: "#/components/responses/NotImplemented" "/api/v4/teams/{team_id}/groups_by_channels": - get: - tags: - - groups - summary: Get team groups by channels - description: | - Retrieve the set of groups associated with the channels in the given team grouped by channel. + get: + tags: + - groups + summary: Get team groups by channels + description: | + Retrieve the set of groups associated with the channels in the given team grouped by channel. - ##### Permissions - Must have the `list_team_channels` permission. + ##### Permissions + Must have the `list_team_channels` permission. - __Minimum server version__: 5.11 - operationId: GetGroupsAssociatedToChannelsByTeam - parameters: - - name: team_id - in: path - description: Team GUID - required: true - schema: - type: string - - name: page - in: query - description: The page to select. - schema: - type: integer - default: 0 - - name: per_page - in: query - description: The number of groups per page. - schema: - type: integer - default: 60 - - name: filter_allow_reference - in: query - description: Boolean which filters in the group entries with the `allow_reference` attribute set. - schema: - type: boolean - default: false - - name: paginate - in: query - description: Boolean to determine whether the pagination should be applied or not - schema: - type: boolean - default: false - responses: - "200": - description: Group list retrieval successful - content: - application/json: - schema: - type: object - items: + __Minimum server version__: 5.11 + operationId: GetGroupsAssociatedToChannelsByTeam + parameters: + - name: team_id + in: path + description: Team GUID + required: true + schema: + type: string + - name: page + in: query + description: The page to select. + schema: + type: integer + default: 0 + - name: per_page + in: query + description: The number of groups per page. + schema: + type: integer + default: 60 + - name: filter_allow_reference + in: query + description: Boolean which filters in the group entries with the `allow_reference` attribute set. + schema: + type: boolean + default: false + - name: paginate + in: query + description: Boolean to determine whether the pagination should be applied or not + schema: + type: boolean + default: false + responses: + "200": + description: Group list retrieval successful + content: + application/json: + schema: + type: object + properties: + groups: $ref: "#/components/schemas/GroupsAssociatedToChannels" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "500": - $ref: "#/components/responses/InternalServerError" - "501": - $ref: "#/components/responses/NotImplemented" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "500": + $ref: "#/components/responses/InternalServerError" + "501": + $ref: "#/components/responses/NotImplemented" "/api/v4/users/{user_id}/groups": get: tags: diff --git a/api/v4/source/introduction.yaml b/api/v4/source/introduction.yaml index 2efde426c6cb..313b775c802f 100644 --- a/api/v4/source/introduction.yaml +++ b/api/v4/source/introduction.yaml @@ -30,9 +30,9 @@ info: * [Authentication](#/#authentication-1) - * [Websocket Events](#/#websocket-events) + * [WebSocket Events](#/#websocket-events) - * [Websocket API](#/#websocket-api) + * [WebSocket API](#/#websocket-api) ### Drivers @@ -110,7 +110,7 @@ info: ```bash curl -i -H 'Authorization: Bearer ckh3t4knu3fzujt76o57f5jo4w' http://localhost:8065/api/v4/users/me ``` - Alternatively, include the `Token` as your `MMAUTHTOKEN` cookie value on you future API requests: + Alternatively, include the `Token` as your `MMAUTHTOKEN` cookie value on your future API requests: ```bash curl -i -H 'Cookie: MMAUTHTOKEN=ckh3t4knu3fzujt76o57f5jo4w' http://localhost:8065/api/v4/users/me @@ -213,7 +213,7 @@ info: Once successfully authenticated, the server will pass a `hello` WebSocket event containing server version over the connection. - ### Websocket Events + ### WebSocket Events WebSocket events are primarily used to alert the client to changes in Mattermost, such as delivering new posts or alerting the client that another user is typing in a channel. @@ -289,7 +289,7 @@ info: - property_field_deleted - property_values_updated - ### Websocket API + ### WebSocket API Mattermost has some basic support for WebSocket APIs. A connected WebSocket can make requests by sending the following over the connection: @@ -436,7 +436,7 @@ tags: - name: LDAP description: Endpoints for configuring and interacting with LDAP. - name: groups - description: Endpoints related to LDAP groups. + description: Endpoints related to groups, including LDAP-synced and custom groups. - name: compliance description: Endpoints for creating, getting and downloading compliance reports. - name: cluster diff --git a/api/v4/source/jobs.yaml b/api/v4/source/jobs.yaml index 208dd93e7ac5..e52020a39919 100644 --- a/api/v4/source/jobs.yaml +++ b/api/v4/source/jobs.yaml @@ -3,15 +3,27 @@ tags: - jobs summary: Get the jobs. - description: > + description: | Get a page of jobs. Use the query parameters to modify the behaviour of this endpoint. __Minimum server version: 4.1__ ##### Permissions + Must have permission to read at least one job type returned by this call. + When no `job_type` query parameter is set, the server only includes job types your session may read; required permission depends on the job type: + + - `read_data_retention_job` — `data_retention` + - `read_compliance_export_job` — `message_export` + - `read_elasticsearch_post_indexing_job` — `elasticsearch_post_indexing` + - `read_elasticsearch_post_aggregation_job` — `elasticsearch_post_aggregation` + - `read_ldap_sync_job` — `ldap_sync` + - `read_jobs` — `migrations`, `plugins`, `product_notices`, `expiry_notify`, `active_users`, `import_process`, `import_delete`, `export_process`, `export_delete`, `cloud`, `mobile_session_metadata`, `extract_content` + - `manage_system` — `access_control_sync` - Must have `manage_jobs` permission. + When `job_type` is set, you must have the permission that matches that type (same mapping as above). + + This endpoint does not accept `team_id`. To list `access_control_sync` jobs scoped to a team without `manage_system`, use `GET /api/v4/jobs/type/access_control_sync` with query parameter `team_id` set to the team GUID (requires `manage_team_access_rules` on that team). operationId: GetJobs parameters: - name: page @@ -59,7 +71,15 @@ Create a new job. __Minimum server version: 4.1__ ##### Permissions - Must have `manage_jobs` permission. + Must have permission to create the requested job type. Required permission depends on `type`: + + - `create_data_retention_job` — `data_retention` + - `create_compliance_export_job` — `message_export` + - `create_elasticsearch_post_indexing_job` — `elasticsearch_post_indexing` + - `create_elasticsearch_post_aggregation_job` — `elasticsearch_post_aggregation` + - `create_ldap_sync_job` — `ldap_sync` + - `manage_jobs` — `migrations`, `plugins`, `product_notices`, `expiry_notify`, `active_users`, `import_process`, `import_delete`, `export_process`, `export_delete`, `cloud`, `extract_content` + - `access_control_sync` — `manage_system`, or `manage_channel_access_rules` on the channel given in job `data`, or `manage_team_access_rules` on the team in job `data` (see server logic for scoped sync jobs) operationId: CreateJob requestBody: content: @@ -100,7 +120,15 @@ Gets a single job. __Minimum server version: 4.1__ ##### Permissions - Must have `manage_jobs` permission. + Must have permission to read the job's type: + + - `read_data_retention_job` — `data_retention` + - `read_compliance_export_job` — `message_export` + - `read_elasticsearch_post_indexing_job` — `elasticsearch_post_indexing` + - `read_elasticsearch_post_aggregation_job` — `elasticsearch_post_aggregation` + - `read_ldap_sync_job` — `ldap_sync` + - `read_jobs` — `migrations`, `plugins`, `product_notices`, `expiry_notify`, `active_users`, `import_process`, `import_delete`, `export_process`, `export_delete`, `cloud`, `mobile_session_metadata`, `extract_content` + - `manage_system` — `access_control_sync` operationId: GetJob parameters: - name: job_id @@ -133,7 +161,7 @@ Download the result of a single job. __Minimum server version: 5.28__ ##### Permissions - Must have `manage_jobs` permission. + Must have `download_compliance_export_result` permission for message export jobs. operationId: DownloadJob parameters: - name: job_id @@ -160,7 +188,15 @@ Cancel a job. __Minimum server version: 4.1__ ##### Permissions - Must have `manage_jobs` permission. + Same as creating that job type (cancel uses the create permission check): + + - `create_data_retention_job` — `data_retention` + - `create_compliance_export_job` — `message_export` + - `create_elasticsearch_post_indexing_job` — `elasticsearch_post_indexing` + - `create_elasticsearch_post_aggregation_job` — `elasticsearch_post_aggregation` + - `create_ldap_sync_job` — `ldap_sync` + - `manage_jobs` — `migrations`, `plugins`, `product_notices`, `expiry_notify`, `active_users`, `import_process`, `import_delete`, `export_process`, `export_delete`, `cloud`, `extract_content` + - `access_control_sync` — `manage_system`, or `manage_channel_access_rules` / `manage_team_access_rules` for scoped jobs as when creating operationId: CancelJob parameters: - name: job_id @@ -184,28 +220,43 @@ $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" - "/api/v4/jobs/type/{type}": + "/api/v4/jobs/type/{job_type}": get: tags: - jobs summary: Get the jobs of the given type. - description: > + description: | Get a page of jobs of the given type. Use the query parameters to modify the behaviour of this endpoint. __Minimum server version: 4.1__ ##### Permissions + Must have permission to read the path `job_type`, using the same mapping as `GET /api/v4/jobs`: + + - `read_data_retention_job` — `data_retention` + - `read_compliance_export_job` — `message_export` + - `read_elasticsearch_post_indexing_job` — `elasticsearch_post_indexing` + - `read_elasticsearch_post_aggregation_job` — `elasticsearch_post_aggregation` + - `read_ldap_sync_job` — `ldap_sync` + - `read_jobs` — `migrations`, `plugins`, `product_notices`, `expiry_notify`, `active_users`, `import_process`, `import_delete`, `export_process`, `export_delete`, `cloud`, `mobile_session_metadata`, `extract_content` + - `manage_system` — `access_control_sync` - Must have `manage_jobs` permission. + When `job_type` is `access_control_sync` and query parameter `team_id` is set to a valid team GUID, team admins with `manage_team_access_rules` on that team may list jobs scoped to that team without `manage_system`. When `team_id` is set, results include only jobs whose stored data matches that team for the requested type. operationId: GetJobsByType parameters: - - name: type + - name: job_type in: path description: Job type required: true schema: type: string + - name: team_id + in: query + description: | + Optional team GUID. When set, the server returns jobs of the given `job_type` whose job data includes this `team_id` (see server filtering). For `access_control_sync`, team admins with `manage_team_access_rules` on this team may use this parameter to read team-scoped jobs without `manage_system`. + schema: + type: string - name: page in: query description: The page to select. @@ -238,13 +289,24 @@ tags: - jobs summary: Update the status of a job - description: > + description: | Update the status of a job. Valid status updates: - 'in_progress' -> 'pending' - 'in_progress' | 'pending' -> 'cancel_requested' - 'cancel_requested' -> 'canceled' Add force to the body of the PATCH request to bypass the given rules, the only statuses you can go to are: pending, cancel_requested and canceled. This can have unexpected consequences and should be used with caution. + + ##### Permissions + Must have permission to manage the job's type: + + - `manage_data_retention_job` — `data_retention` + - `manage_compliance_export_job` — `message_export` + - `manage_elasticsearch_post_indexing_job` — `elasticsearch_post_indexing` + - `manage_elasticsearch_post_aggregation_job` — `elasticsearch_post_aggregation` + - `manage_ldap_sync_job` — `ldap_sync` + - `manage_jobs` — `migrations`, `plugins`, `product_notices`, `expiry_notify`, `active_users`, `import_process`, `import_delete`, `export_process`, `export_delete`, `cloud`, `extract_content` + - `manage_system` — `access_control_sync` operationId: UpdateJobStatus parameters: - name: job_id diff --git a/api/v4/source/outgoing_oauth_connections.yaml b/api/v4/source/outgoing_oauth_connections.yaml index 1006fa433e55..ba04cd1b31bf 100644 --- a/api/v4/source/outgoing_oauth_connections.yaml +++ b/api/v4/source/outgoing_oauth_connections.yaml @@ -71,7 +71,7 @@ $ref: "#/components/responses/InternalServerError" "501": $ref: "#/components/responses/NotImplemented" - /api/v4/oauth/outgoing_connections/{connection_id}: + /api/v4/oauth/outgoing_connections/{outgoing_oauth_connection_id}: get: tags: - oauth @@ -84,6 +84,12 @@ __Minimum server version__: 9.6 operationId: GetOutgoingOAuthConnection parameters: + - name: outgoing_oauth_connection_id + in: path + description: Outgoing OAuth connection ID + required: true + schema: + type: string - name: team_id in: query description: Current Team ID in integrations backstage @@ -115,6 +121,12 @@ __Minimum server version__: 9.6 operationId: UpdateOutgoingOAuthConnection parameters: + - name: outgoing_oauth_connection_id + in: path + description: Outgoing OAuth connection ID + required: true + schema: + type: string - name: team_id in: query description: Current Team ID in integrations backstage @@ -156,6 +168,12 @@ __Minimum server version__: 9.6 operationId: DeleteOutgoingOAuthConnection parameters: + - name: outgoing_oauth_connection_id + in: path + description: Outgoing OAuth connection ID + required: true + schema: + type: string - name: team_id in: query description: Current Team ID in integrations backstage diff --git a/api/v4/source/plugins.yaml b/api/v4/source/plugins.yaml index 60068c3dc749..23179d5eb58c 100644 --- a/api/v4/source/plugins.yaml +++ b/api/v4/source/plugins.yaml @@ -333,17 +333,7 @@ content: application/json: schema: - type: object - required: - - id - - version - properties: - id: - type: string - description: The ID of the plugin to install. - version: - type: string - description: The version of the plugin to install. + $ref: "#/components/schemas/InstallMarketplacePluginRequest" description: The metadata identifying the plugin to install. required: true responses: @@ -477,3 +467,59 @@ $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalServerError" + + /api/v4/plugins/reattach: + post: + tags: + - plugins + summary: Reattach a plugin process + description: | + Reattaches the server to an already running plugin process. + This endpoint is only exposed over a local socket. + + ##### Permissions + Must have `manage_system` permission. + operationId: ReattachPlugin + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PluginReattachRequest" + responses: + "200": + description: Plugin reattached successfully + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + + "/api/v4/plugins/{plugin_id}/detach": + post: + tags: + - plugins + summary: Detach a reattached plugin process + description: | + Detaches a previously reattached plugin from the server. + This endpoint is only exposed over a local socket. + + ##### Permissions + Must have `manage_system` permission. + operationId: DetachPlugin + parameters: + - name: plugin_id + in: path + description: The ID of the plugin to detach. + required: true + schema: + type: string + responses: + "200": + description: Plugin detached successfully + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" diff --git a/api/v4/source/posts.yaml b/api/v4/source/posts.yaml index c390b71eb500..9cdae53d8da8 100644 --- a/api/v4/source/posts.yaml +++ b/api/v4/source/posts.yaml @@ -127,6 +127,50 @@ $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" + /api/v4/posts/search: + post: + tags: + - posts + summary: Search posts across all teams + description: | + Search posts visible to the current user across all teams. + ##### Permissions + Must be authenticated. + operationId: SearchPostsInAllTeams + requestBody: + content: + application/json: + schema: + type: object + required: + - terms + properties: + terms: + type: string + is_or_search: + type: boolean + time_zone_offset: + type: integer + include_deleted_channels: + type: boolean + page: + type: integer + per_page: + type: integer + required: true + responses: + "200": + description: Post search successful + content: + application/json: + schema: + $ref: "#/components/schemas/PostListWithSearchMatches" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" "/api/v4/posts/{post_id}": get: tags: @@ -550,6 +594,72 @@ $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" + "/api/v4/posts/{post_id}/info": + get: + tags: + - posts + summary: Get post info + description: | + Get additional metadata and access information for a post. + ##### Permissions + Must be able to access the post's team and channel context. + operationId: GetPostInfo + parameters: + - name: post_id + in: path + description: Post ID + required: true + schema: + type: string + responses: + "200": + description: Post info retrieval successful + content: + application/json: + schema: + $ref: "#/components/schemas/PostInfo" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "/api/v4/posts/{post_id}/edit_history": + get: + tags: + - posts + summary: Get post edit history + description: | + Get edit history entries for a post. + ##### Permissions + Must have `edit_post` permission in the channel. For most posts, only the original author can access history. + operationId: GetEditHistoryForPost + parameters: + - name: post_id + in: path + description: Post ID + required: true + schema: + type: string + responses: + "200": + description: Edit history retrieval successful + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Post" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" "/api/v4/channels/{channel_id}/posts": get: tags: diff --git a/api/v4/source/remoteclusters.yaml b/api/v4/source/remoteclusters.yaml index 5e981f2cfd1a..f347f4b8d048 100644 --- a/api/v4/source/remoteclusters.yaml +++ b/api/v4/source/remoteclusters.yaml @@ -297,9 +297,177 @@ content: application/json: schema: - type: object $ref: "#/components/schemas/RemoteCluster" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" + + "/api/v4/remotecluster/ping": + post: + tags: + - remote clusters + summary: Receive a ping from a remote cluster. + description: | + Receives heartbeat traffic from an already linked remote cluster. + This endpoint is authenticated with a remote-cluster token and is + used by the secure connection transport layer. + + ##### Permissions + No user session permissions required. + operationId: RemoteClusterPing + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RemoteClusterFrame" + responses: + "200": + description: Ping response successful + content: + application/json: + schema: + $ref: "#/components/schemas/RemoteClusterPing" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + + "/api/v4/remotecluster/msg": + post: + tags: + - remote clusters + summary: Receive a remote cluster message. + description: | + Receives and processes an incoming transport message from a linked + remote cluster. This endpoint is authenticated with a remote-cluster + token and is part of the secure connection protocol. + + ##### Permissions + No user session permissions required. + operationId: RemoteClusterAcceptMessage + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RemoteClusterFrame" + responses: + "200": + description: Message accepted successfully + content: + application/json: + schema: + $ref: "#/components/schemas/RemoteClusterResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + + "/api/v4/remotecluster/confirm_invite": + post: + tags: + - remote clusters + summary: Confirm an invite with a remote cluster. + description: | + Confirms an invitation handshake from a linked remote cluster. + This endpoint is authenticated with a remote-cluster token and is + used by the secure connection protocol. + + ##### Permissions + No user session permissions required. + operationId: RemoteClusterConfirmInvite + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RemoteClusterFrame" + responses: + "200": + description: Invitation confirmation successful + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + + "/api/v4/remotecluster/upload/{upload_id}": + post: + tags: + - remote clusters + summary: Upload file data for a remote upload session. + description: | + Streams file data into an existing upload session from a linked + remote cluster. This endpoint is authenticated with a remote-cluster token. + + ##### Permissions + No user session permissions required. + operationId: UploadRemoteClusterData + parameters: + - name: upload_id + in: path + description: The upload session ID. + required: true + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + "200": + description: Upload chunk accepted + content: + application/json: + schema: + $ref: "#/components/schemas/FileInfo" + "204": + description: Upload data accepted with no file completion yet + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + + "/api/v4/remotecluster/{user_id}/image": + post: + tags: + - remote clusters + summary: Set profile image for a remote user. + description: | + Uploads and sets a profile image for a remote user managed by the + requesting remote cluster. This endpoint is authenticated with a + remote-cluster token. + + ##### Permissions + No user session permissions required. + operationId: RemoteSetProfileImage + parameters: + - name: user_id + in: path + description: The remote user ID. + required: true + schema: + type: string + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + image: + type: string + format: binary + responses: + "200": + description: Profile image updated successfully + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" diff --git a/api/v4/source/sharedchannels.yaml b/api/v4/source/sharedchannels.yaml index 346a3ded016d..43a8e589c42e 100644 --- a/api/v4/source/sharedchannels.yaml +++ b/api/v4/source/sharedchannels.yaml @@ -9,7 +9,9 @@ __Minimum server version__: 5.50 ##### Permissions - Must be authenticated. + Must be authenticated and have the `view_team` permission for the team. + Results are restricted to channels the user is a member of unless the user has + `manage_shared_channels`. operationId: GetAllSharedChannels parameters: - name: team_id @@ -228,6 +230,8 @@ application/json: schema: $ref: "#/components/schemas/StatusOK" + "204": + description: Channel was not shared with the remote cluster. No action needed. "401": $ref: "#/components/responses/Unauthorized" "403": @@ -277,13 +281,14 @@ - shared channels summary: Check if user can DM another user in shared channels context description: | - Checks if a user can send direct messages to another user, considering shared channel restrictions. - This is specifically for shared channels where DMs require direct connections between clusters. + Checks if a user can send direct messages to another user in a shared channels context. + In addition to user visibility, this evaluates remote-cluster direct-connect restrictions + for remote users. __Minimum server version__: 10.11 ##### Permissions - Must be authenticated and have permission to view the user. + Must be authenticated and able to view the target user. operationId: CanUserDirectMessage parameters: - name: user_id diff --git a/api/v4/source/system.yaml b/api/v4/source/system.yaml index 6ca163fb9c76..780ed1c1aa29 100644 --- a/api/v4/source/system.yaml +++ b/api/v4/source/system.yaml @@ -102,13 +102,105 @@ $ref: "#/components/schemas/SystemStatusResponse" "500": $ref: "#/components/responses/InternalServerError" - "/api/v4/system/notices/{teamId}": + /api/v4/websocket: + get: + tags: + - system + summary: Open a WebSocket connection + description: | + Upgrades the HTTP connection to a WebSocket connection used for real-time events and websocket actions. + + ##### Permissions + No permission required to connect. Authentication can be performed via standard API auth (cookie/header) + or by sending an `authentication_challenge` action after connecting. + operationId: ConnectWebSocket + security: [] + parameters: + - name: connection_id + in: query + description: Existing connection identifier for reconnect flows. + required: false + schema: + type: string + - name: sequence_number + in: query + description: Last received sequence number for reconnect flows. + required: false + schema: + type: string + - name: posted_ack + in: query + description: Whether post acknowledgement events are enabled for this connection. + required: false + schema: + type: boolean + - name: disconnect_err_code + in: query + description: Optional close code used by clients to indicate disconnect reason. + required: false + schema: + type: string + responses: + "101": + description: Switching Protocols + "400": + $ref: "#/components/responses/BadRequest" + + /manualtest: + get: + tags: + - system + summary: Run manual testing helpers + description: | + Invokes manual test helpers used by developers and automated manual test scenarios. + This endpoint is only registered when `ServiceSettings.EnableTesting` is enabled. + + ##### Permissions + + None. Authentication is not required; this route uses the same handler stack as other unauthenticated API handlers (`APIHandler`). + + __Security note:__ Only enable `EnableTesting` on non-production, developer-oriented deployments. + operationId: ManualTest + security: [] + parameters: + - name: test + in: query + description: Name of the manual test to run. + required: true + schema: + type: string + - name: uid + in: query + description: Optional unique value used to randomize generated resources. + required: false + schema: + type: string + - name: username + in: query + description: Optional username used for helper account creation. + required: false + schema: + type: string + - name: teamname + in: query + description: Optional team display name used for helper team creation. + required: false + schema: + type: string + responses: + "307": + description: Manual test setup completed and redirected to the default channel. + "400": + $ref: "#/components/responses/BadRequest" + "500": + $ref: "#/components/responses/InternalServerError" + "/api/v4/system/notices/{team_id}": get: tags: - system summary: Get notices for logged in user in specified team description: > - Will return appropriate product notices for current user in the team specified by teamId parameter. + Will return appropriate product notices for current user in the team specified by team_id parameter. __Minimum server version__: 5.26 @@ -135,7 +227,7 @@ required: true schema: type: string - - name: teamId + - name: team_id in: path description: ID of the team required: true @@ -185,6 +277,68 @@ $ref: "#/components/schemas/StatusOK" "500": $ref: "#/components/responses/InternalServerError" + /api/v4/system/onboarding/complete: + get: + tags: + - system + summary: Get first admin onboarding completion status + description: > + Get whether first admin onboarding is complete. + + ##### Permissions + Must have `manage_system` permission. + operationId: GetOnboardingComplete + responses: + "200": + description: Onboarding completion state retrieval successful + content: + application/json: + schema: + $ref: "#/components/schemas/System" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "500": + $ref: "#/components/responses/InternalServerError" + post: + tags: + - system + summary: Complete first admin onboarding + description: > + Mark first admin onboarding as complete and optionally trigger plugin installation. + + ##### Permissions + Must have `manage_system` permission. + operationId: CompleteOnboarding + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + organization: + type: string + description: Organization name for self-hosted onboarding. + install_plugins: + type: array + description: Marketplace plugin IDs to install as part of onboarding. + items: + type: string + responses: + "200": + description: Onboarding completion successful + content: + application/json: + schema: + $ref: "#/components/schemas/StatusOK" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" /api/v4/system/e2e/ai_bridge: put: tags: @@ -531,6 +685,42 @@ $ref: "#/components/responses/BadRequest" "403": $ref: "#/components/responses/Forbidden" + /api/v4/config/migrate: + post: + tags: + - system + summary: Migrate config storage + description: | + Migrate configuration between storage backends. + This endpoint is only exposed over a local socket. + + ##### Permissions + Must have `manage_system` permission. + operationId: MigrateConfig + requestBody: + content: + application/json: + schema: + type: object + required: + - from + - to + properties: + from: + type: string + description: Source config store name. + to: + type: string + description: Destination config store name. + responses: + "200": + description: Config migration successful + "400": + $ref: "#/components/responses/BadRequest" + "403": + $ref: "#/components/responses/Forbidden" + "500": + $ref: "#/components/responses/InternalServerError" /api/v4/config/client: get: tags: @@ -739,35 +929,6 @@ $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalServerError" - /api/v4/license/renewal: - get: - tags: - - system - summary: Request the license renewal link - description: > - Request the renewal link that would be used to start the license renewal process - - __Minimum server version__: 5.32 - - ##### Permissions - - Must have `sysconsole_write_about` permission. - operationId: RequestLicenseRenewalLink - responses: - "200": - description: License renewal link obtained - content: - application/json: - schema: - $ref: "#/components/schemas/LicenseRenewalLink" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "500": - $ref: "#/components/responses/InternalServerError" /api/v4/trial-license: post: tags: @@ -811,13 +972,13 @@ summary: Get last trial license used operationId: GetPrevTrialLicense description: > - Get the last trial license used on the sevrer + Get the last trial license used on the server __Minimum server version__: 5.36 ##### Permissions - Must have `manage_systems` permissions. + Must have `manage_system` permission. responses: "200": description: License fetched successfully. @@ -967,6 +1128,77 @@ type: string "403": $ref: "#/components/responses/Forbidden" + /api/v4/logs/query: + post: + tags: + - system + summary: Query server logs with filters + description: > + Query server logs using filter criteria. + + ##### Permissions + Must have `get_logs` permission. + operationId: QueryLogs + parameters: + - name: page + in: query + description: The page to select. + schema: + type: integer + default: 0 + - name: logs_per_page + in: query + description: The number of logs per page. + schema: + type: string + default: "10000" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + server_names: + type: array + items: + type: string + log_levels: + type: array + items: + type: string + date_from: + type: string + description: > + Inclusive start of the time range. The server parses this using the layout + `YYYY-MM-DD HH:MM:SS.mmm ±HH:MM` (milliseconds optional; timezone offset required), + matching Go reference time `2006-01-02 15:04:05.999 -07:00`. + example: "2024-01-15 14:30:45.123 -05:00" + date_to: + type: string + description: > + Inclusive end of the time range. Same format as `date_from` + (`YYYY-MM-DD HH:MM:SS.mmm ±HH:MM`, e.g. `2006-01-02 15:04:05.999 -07:00`). + example: "2024-01-15 14:30:45.123 -05:00" + responses: + "200": + description: Log query successful + content: + application/json: + schema: + type: object + additionalProperties: + type: array + items: + type: object + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "500": + $ref: "#/components/responses/InternalServerError" /api/v4/analytics/old: get: tags: @@ -1012,6 +1244,76 @@ $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" + /api/v4/latest_version: + get: + tags: + - system + summary: Get latest public server release information + description: > + Retrieves metadata about the latest Mattermost server release from GitHub. + + ##### Permissions + Must have `manage_system` permission. + operationId: GetLatestVersion + responses: + "200": + description: Latest release metadata retrieval successful + content: + application/json: + schema: + type: object + properties: + id: + type: integer + tag_name: + type: string + name: + type: string + created_at: + type: string + published_at: + type: string + body: + type: string + html_url: + type: string + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "500": + $ref: "#/components/responses/InternalServerError" + /api/v4/system/schema/version: + get: + tags: + - system + summary: Get applied database schema migrations + description: > + Returns the list of applied database schema migrations. + + ##### Permissions + Must have at least one sysconsole read permission. + operationId: GetAppliedSchemaMigrations + responses: + "200": + description: Applied schema migrations retrieval successful + content: + application/json: + schema: + type: array + items: + type: object + properties: + version: + type: integer + name: + type: string + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "500": + $ref: "#/components/responses/InternalServerError" /api/v4/server_busy: post: tags: diff --git a/api/v4/source/teams.yaml b/api/v4/source/teams.yaml index 110bfa53ba61..cc8cd78154a6 100644 --- a/api/v4/source/teams.yaml +++ b/api/v4/source/teams.yaml @@ -378,7 +378,7 @@ $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" - "/api/v4/teams/name/{name}": + "/api/v4/teams/name/{team_name}": get: tags: - teams @@ -391,7 +391,7 @@ Must be authenticated, team type is open and have the `view_team` permission. operationId: GetTeamByName parameters: - - name: name + - name: team_name in: path description: Team Name required: true @@ -499,7 +499,7 @@ $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" - "/api/v4/teams/name/{name}/exists": + "/api/v4/teams/name/{team_name}/exists": get: tags: - teams @@ -510,7 +510,7 @@ Must be authenticated. operationId: TeamExists parameters: - - name: name + - name: team_name in: path description: Team Name required: true diff --git a/api/v4/source/uploads.yaml b/api/v4/source/uploads.yaml index 50d2659aaa7b..57685f90ce25 100644 --- a/api/v4/source/uploads.yaml +++ b/api/v4/source/uploads.yaml @@ -105,9 +105,17 @@ type: string requestBody: content: - application/x-www-form-urlencoded: + application/octet-stream: + schema: + type: string + format: binary + multipart/form-data: schema: type: object + properties: + file: + type: string + format: binary responses: "201": description: Upload successful diff --git a/api/v4/source/usage.yaml b/api/v4/source/usage.yaml index 1c63f44413b8..cc1fb0ad8534 100644 --- a/api/v4/source/usage.yaml +++ b/api/v4/source/usage.yaml @@ -50,3 +50,32 @@ $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalServerError" + /api/v4/usage/teams: + get: + tags: + - usage + summary: Get current usage of teams + description: > + Retrieve rounded total number of teams for this instance. + + ##### Permissions + Must be authenticated. + operationId: GetTeamsUsage + responses: + "200": + description: Total number of teams returned successfully + content: + application/json: + schema: + type: object + properties: + active: + type: integer + cloud_archived: + type: integer + teams: + type: integer + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalServerError" diff --git a/api/v4/source/users.yaml b/api/v4/source/users.yaml index d63c934ed425..572d834c6fd7 100644 --- a/api/v4/source/users.yaml +++ b/api/v4/source/users.yaml @@ -43,6 +43,43 @@ $ref: "#/components/responses/BadRequest" "403": $ref: "#/components/responses/Forbidden" + /api/v4/users/login/desktop_token: + post: + tags: + - users + summary: Login using desktop token + description: > + Login to Mattermost with a short-lived desktop token. + + ##### Permissions + No permission required. + operationId: LoginWithDesktopToken + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - token + properties: + token: + type: string + device_id: + type: string + responses: + "200": + description: Desktop token login successful + content: + application/json: + schema: + $ref: "#/components/schemas/User" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" /api/v4/users/login/cws: post: tags: @@ -294,6 +331,64 @@ $ref: "#/components/responses/BadRequest" "403": $ref: "#/components/responses/Forbidden" + /api/v4/users/notify-admin: + post: + tags: + - users + summary: Save notify-admin intent + description: > + Save a notify-admin request for upgrade or trial flows. + + ##### Permissions + Must be authenticated. + operationId: NotifyAdmin + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NotifyAdminToUpgradeRequest" + responses: + "200": + description: Notify-admin request saved + content: + application/json: + schema: + $ref: "#/components/schemas/StatusOK" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + /api/v4/users/trigger-notify-admin-posts: + post: + tags: + - users + summary: Trigger notify-admin posts + description: > + Trigger admin notification posts manually when enabled by configuration. + + ##### Permissions + Must be authenticated and have `manage_system` permission. + operationId: TriggerNotifyAdminPosts + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NotifyAdminToUpgradeRequest" + responses: + "200": + description: Notify-admin posts triggered successfully + content: + application/json: + schema: + $ref: "#/components/schemas/StatusOK" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" /api/v4/users: post: @@ -1691,45 +1786,6 @@ $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" - /api/v4/users/mfa: - post: - tags: - - users - summary: Check MFA - description: > - Check if a user has multi-factor authentication active on their account - by providing a login id. Used to check whether an MFA code needs to be - provided when logging in. - - ##### Permissions - - No permission required. - operationId: CheckUserMfa - requestBody: - content: - application/json: - schema: - type: object - required: - - login_id - properties: - login_id: - description: The email or username used to login - type: string - required: true - responses: - "200": - description: MFA check successful - content: - application/json: - schema: - type: object - properties: - mfa_required: - description: Value will `true` if MFA is active, `false` otherwise - type: boolean - "400": - $ref: "#/components/responses/BadRequest" "/api/v4/users/{user_id}/password": put: tags: @@ -2969,152 +3025,66 @@ '501': $ref: "#/components/responses/NotImplemented" /api/v4/users/migrate_auth/saml: - post: - tags: - - users - - migrate - - authentication - - SAML - summary: Migrate user accounts authentication type to SAML. - description: > - Migrates accounts from one authentication provider to another. - For example, you can upgrade your authentication provider from email to SAML. - - __Minimum server version__: 5.28 - - ##### Permissions - - Must have `manage_system` permission. - - operationId: MigrateAuthToSaml - requestBody: - content: - application/json: - schema: - type: object - required: - - from - - matches - - auto - properties: - from: - description: The current authentication type for the matched users. - type: string - matches: - description: Users map. - type: object - auto: - type: boolean - responses: - '200': - description: Successfully migrated authentication type to LDAP. - '400': - $ref: "#/components/responses/BadRequest" - '401': - $ref: "#/components/responses/Unauthorized" - '403': - $ref: "#/components/responses/Forbidden" - '501': - $ref: "#/components/responses/NotImplemented" - "/api/v4/users/{user_id}/teams/{team_id}/threads": - get: - tags: - - threads - summary: Get all threads that user is following - description: | - Get all threads that user is following - - __Minimum server version__: 5.29 - - ##### Permissions - Must be logged in as the user or have `edit_other_users` permission. - operationId: GetUserThreads - parameters: - - name: user_id - in: path - description: The ID of the user. This can also be "me" which will point to the current user. - required: true - schema: - type: string - - name: team_id - in: path - description: The ID of the team in which the thread is. - required: true - schema: - type: string - - name: since - in: query - description: Since filters the threads based on their LastUpdateAt timestamp. - required: false - schema: - type: integer - - name: deleted - in: query - description: Deleted will specify that even deleted threads should be returned (For mobile sync). - required: false - schema: - type: boolean - default: false - - name: extended - in: query - description: Extended will enrich the response with participant details. - required: false - schema: - type: boolean - default: false - - name: page - in: query - description: Page specifies which part of the results to return, by per_page. - required: false - schema: - type: integer - default: 0 - - name: per_page - in: query - description: The size of the returned chunk of results. - schema: - type: integer - default: 60 - - name: totalsOnly - in: query - description: Setting this to true will only return the total counts. - required: false - schema: - type: boolean - default: false - - name: threadsOnly - in: query - description: Setting this to true will only return threads. - required: false - schema: - type: boolean - default: false - responses: - "200": - description: User's thread retrieval successful + post: + tags: + - users + - migrate + - authentication + - SAML + summary: Migrate user accounts authentication type to SAML. + description: > + Migrates accounts from one authentication provider to another. + For example, you can upgrade your authentication provider from email to SAML. + + __Minimum server version__: 5.28 + + ##### Permissions + + Must have `manage_system` permission. + + operationId: MigrateAuthToSaml + requestBody: content: application/json: schema: - $ref: "#/components/schemas/UserThreads" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "404": - $ref: "#/components/responses/NotFound" - "/api/v4/users/{user_id}/teams/{team_id}/threads/mention_counts": + type: object + required: + - from + - matches + - auto + properties: + from: + description: The current authentication type for the matched users. + type: string + matches: + description: Users map. + type: object + auto: + type: boolean + responses: + '200': + description: Successfully migrated authentication type to SAML. + '400': + $ref: "#/components/responses/BadRequest" + '401': + $ref: "#/components/responses/Unauthorized" + '403': + $ref: "#/components/responses/Forbidden" + '501': + $ref: "#/components/responses/NotImplemented" + "/api/v4/users/{user_id}/teams/{team_id}/threads": get: tags: - threads - summary: Get all unread mention counts from followed threads, per-channel + summary: Get all threads that user is following description: | - Get all unread mention counts from followed threads + Get all threads that user is following __Minimum server version__: 5.29 ##### Permissions Must be logged in as the user or have `edit_other_users` permission. - operationId: GetThreadMentionCountsByChannel + operationId: GetUserThreads parameters: - name: user_id in: path @@ -3128,9 +3098,60 @@ required: true schema: type: string + - name: since + in: query + description: Since filters the threads based on their LastUpdateAt timestamp. + required: false + schema: + type: integer + - name: deleted + in: query + description: Deleted will specify that even deleted threads should be returned (For mobile sync). + required: false + schema: + type: boolean + default: false + - name: extended + in: query + description: Extended will enrich the response with participant details. + required: false + schema: + type: boolean + default: false + - name: page + in: query + description: Page specifies which part of the results to return, by per_page. + required: false + schema: + type: integer + default: 0 + - name: per_page + in: query + description: The size of the returned chunk of results. + schema: + type: integer + default: 60 + - name: totalsOnly + in: query + description: Setting this to true will only return the total counts. + required: false + schema: + type: boolean + default: false + - name: threadsOnly + in: query + description: Setting this to true will only return threads. + required: false + schema: + type: boolean + default: false responses: "200": - description: Get was successful + description: User's thread retrieval successful + content: + application/json: + schema: + $ref: "#/components/schemas/UserThreads" "400": $ref: "#/components/responses/BadRequest" "401": @@ -3390,6 +3411,159 @@ $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" + /api/v4/drafts: + post: + tags: + - users + - drafts + summary: Upsert synced draft + description: | + Create or update a synced draft for the current user. + ##### Permissions + Must be authenticated, have permission to create posts in the channel, and synced drafts must be enabled. + operationId: UpsertDraft + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DraftUpsertRequest" + responses: + "201": + description: Draft upsert successful. Returns `null` when an empty message deletes the draft. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Draft" + nullable: true + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "501": + $ref: "#/components/responses/NotImplemented" + "/api/v4/users/{user_id}/teams/{team_id}/drafts": + get: + tags: + - users + - drafts + summary: Get synced drafts for a team + description: | + Get synced drafts for the current user in a team. + ##### Permissions + Must have `view_team` permission for the team and synced drafts must be enabled. + operationId: GetDrafts + parameters: + - name: user_id + in: path + description: User ID + required: true + schema: + type: string + - name: team_id + in: path + description: Team ID + required: true + schema: + type: string + responses: + "200": + description: Drafts retrieval successful + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Draft" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "501": + $ref: "#/components/responses/NotImplemented" + "/api/v4/users/{user_id}/channels/{channel_id}/drafts": + delete: + tags: + - users + - drafts + summary: Delete synced draft + description: | + Delete a synced draft for a channel. + ##### Permissions + Must be authenticated as the draft owner and synced drafts must be enabled. + operationId: DeleteDraft + parameters: + - name: user_id + in: path + description: User ID + required: true + schema: + type: string + - name: channel_id + in: path + description: Channel ID + required: true + schema: + type: string + responses: + "200": + description: Draft deletion successful + content: + application/json: + schema: + $ref: "#/components/schemas/StatusOK" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "501": + $ref: "#/components/responses/NotImplemented" + "/api/v4/users/{user_id}/channels/{channel_id}/drafts/{thread_id}": + delete: + tags: + - users + - drafts + summary: Delete synced thread draft + description: | + Delete a synced draft for a channel thread. + ##### Permissions + Must be authenticated as the draft owner and synced drafts must be enabled. + operationId: DeleteDraftForThread + parameters: + - name: user_id + in: path + description: User ID + required: true + schema: + type: string + - name: channel_id + in: path + description: Channel ID + required: true + schema: + type: string + - name: thread_id + in: path + description: Root post ID of the thread + required: true + schema: + type: string + responses: + "200": + description: Thread draft deletion successful + content: + application/json: + schema: + $ref: "#/components/schemas/StatusOK" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "501": + $ref: "#/components/responses/NotImplemented" "/api/v4/users/{user_id}/data_retention/team_policies": get: tags: diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/after_subscription_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/after_subscription_spec.js index bbbe74eab9ff..777270b247e9 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/after_subscription_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/cloud/billing/after_subscription_spec.js @@ -26,7 +26,7 @@ describe('System Console - after subscription scenarios', () => { // # Click Subscribe Now button cy.contains('span', 'Upgrade Now').parent().click(); - cy.intercept('POST', '/api/v4/cloud/payment/confirm').as('confirm'); + cy.intercept('PUT', '/api/v4/cloud/customer').as('customerUpdate'); cy.intercept('GET', '/api/v4/cloud/subscription').as('subscribe'); @@ -47,7 +47,7 @@ describe('System Console - after subscription scenarios', () => { // # Click Subscribe button cy.get('.RHS').find('button').last().should('be.enabled').click(); - cy.wait(['@confirm', '@subscribe']); + cy.wait(['@customerUpdate', '@subscribe']); // * Check for success message cy.findByText('You are now subscribed to Cloud Professional', {timeout: TIMEOUTS.TEN_SEC}).should('be.visible'); @@ -136,9 +136,7 @@ describe('System Console - after subscription scenarios', () => { cy.wait('@customer'); - cy.intercept('POST', '/api/v4/cloud/payment').as('payment'); - - cy.intercept('POST', '/api/v4/cloud/payment/confirm').as('confirm'); + cy.intercept('PUT', '/api/v4/cloud/customer').as('customerUpdate'); cy.intercept('GET', '/api/v4/cloud/subscription').as('subscribe'); @@ -159,7 +157,7 @@ describe('System Console - after subscription scenarios', () => { // # Click Save Credit Card button cy.get('#saveSetting').should('be.enabled').click(); - cy.wait(['@payment', '@confirm']); + cy.wait('@customerUpdate'); cy.wait('@subscribe'); diff --git a/e2e-tests/playwright/specs/functional/channels/channel_privacy/sidebar_icon_realtime_update.spec.ts b/e2e-tests/playwright/specs/functional/channels/channel_privacy/sidebar_icon_realtime_update.spec.ts new file mode 100644 index 000000000000..fec6158ab2d3 --- /dev/null +++ b/e2e-tests/playwright/specs/functional/channels/channel_privacy/sidebar_icon_realtime_update.spec.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@mattermost/playwright-lib'; + +test( + 'sidebar icon updates from globe to lock when channel converted to private via API', + {tag: ['@channels', '@channel_privacy']}, + async ({pw}) => { + // # Initialize setup + const {adminClient, user, team} = await pw.initSetup(); + + // # Create a public channel + const channel = await adminClient.createChannel( + pw.random.channel({ + teamId: team.id, + name: 'privacy-ws-test', + displayName: 'Privacy WS Test', + type: 'O', + unique: true, + }), + ); + await adminClient.addToChannel(user.id, channel.id); + + // # Log in user and navigate to the channel + const {page, channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(team.name, channel.name); + await channelsPage.toBeVisible(); + + // * Verify sidebar shows globe icon (public channel) + const sidebarItem = page.locator(`#sidebarItem_${channel.name}`); + await expect(sidebarItem).toBeVisible(); + await expect(sidebarItem.locator('.icon-globe')).toBeVisible(); + await expect(sidebarItem.locator('.icon-lock-outline')).not.toBeVisible(); + + // # Convert channel to private via admin API (simulates mmctl) + await adminClient.updateChannelPrivacy(channel.id, 'P'); + + // * Verify sidebar icon updates to lock without page refresh + await expect(sidebarItem.locator('.icon-lock-outline')).toBeVisible({timeout: 10000}); + await expect(sidebarItem.locator('.icon-globe')).not.toBeVisible(); + }, +); + +test( + 'sidebar icon updates from lock to globe when channel converted to public via API', + {tag: ['@channels', '@channel_privacy']}, + async ({pw}) => { + // # Initialize setup + const {adminClient, user, team} = await pw.initSetup(); + + // # Create a private channel + const channel = await adminClient.createChannel( + pw.random.channel({ + teamId: team.id, + name: 'privacy-ws-test-p2o', + displayName: 'Privacy WS Test P2O', + type: 'P', + unique: true, + }), + ); + await adminClient.addToChannel(user.id, channel.id); + + // # Log in user and navigate to the channel + const {page, channelsPage} = await pw.testBrowser.login(user); + await channelsPage.goto(team.name, channel.name); + await channelsPage.toBeVisible(); + + // * Verify sidebar shows lock icon (private channel) + const sidebarItem = page.locator(`#sidebarItem_${channel.name}`); + await expect(sidebarItem).toBeVisible(); + await expect(sidebarItem.locator('.icon-lock-outline')).toBeVisible(); + await expect(sidebarItem.locator('.icon-globe')).not.toBeVisible(); + + // # Convert channel to public via admin API (simulates mmctl) + await adminClient.updateChannelPrivacy(channel.id, 'O'); + + // * Verify sidebar icon updates to globe without page refresh + await expect(sidebarItem.locator('.icon-globe')).toBeVisible({timeout: 10000}); + await expect(sidebarItem.locator('.icon-lock-outline')).not.toBeVisible(); + }, +); diff --git a/server/Makefile b/server/Makefile index be9a2ae70248..5730af0caa23 100644 --- a/server/Makefile +++ b/server/Makefile @@ -159,7 +159,7 @@ TEMPLATES_DIR=templates # Plugins Packages PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:) PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.4 -PLUGIN_PACKAGES += mattermost-plugin-github-v2.7.0 +PLUGIN_PACKAGES += mattermost-plugin-github-v2.7.1 PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.12.2 PLUGIN_PACKAGES += mattermost-plugin-jira-v4.7.0 PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.8.1 @@ -233,6 +233,9 @@ ifneq ($(DOCKER_SERVICES_OVERRIDE),true) ifeq (,$(findstring minio,$(ENABLED_DOCKER_SERVICES))) TEMP_DOCKER_SERVICES:=$(TEMP_DOCKER_SERVICES) minio endif + ifeq (,$(findstring azurite,$(ENABLED_DOCKER_SERVICES))) + TEMP_DOCKER_SERVICES:=$(TEMP_DOCKER_SERVICES) azurite + endif ifeq ($(BUILD_ENTERPRISE_READY),true) ifeq (,$(findstring openldap,$(ENABLED_DOCKER_SERVICES))) TEMP_DOCKER_SERVICES:=$(TEMP_DOCKER_SERVICES) openldap diff --git a/server/build/docker-compose-generator/main.go b/server/build/docker-compose-generator/main.go index 7404a3f0596f..e43d38714ccb 100644 --- a/server/build/docker-compose-generator/main.go +++ b/server/build/docker-compose-generator/main.go @@ -26,6 +26,7 @@ func main() { validServices := map[string]int{ "postgres": 5432, "minio": 9000, + "azurite": 10000, "inbucket": 9001, "openldap": 389, "elasticsearch": 9200, diff --git a/server/build/docker-compose.common.yml b/server/build/docker-compose.common.yml index b4147b2a3a42..eb91fcebcfbc 100644 --- a/server/build/docker-compose.common.yml +++ b/server/build/docker-compose.common.yml @@ -34,6 +34,12 @@ services: MINIO_ROOT_USER: minioaccesskey MINIO_ROOT_PASSWORD: miniosecretkey MINIO_KMS_SECRET_KEY: my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw= + azurite: + image: "mcr.microsoft.com/azure-storage/azurite:3.34.0" + logging: *default-logging + command: "azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --skipApiVersionCheck" + networks: + - mm-test inbucket: image: "inbucket/inbucket:3.1.1" logging: *default-logging diff --git a/server/build/docker-compose.yml b/server/build/docker-compose.yml index 9e7cc9c0026f..ee2316b9ca11 100644 --- a/server/build/docker-compose.yml +++ b/server/build/docker-compose.yml @@ -8,6 +8,10 @@ services: extends: file: docker-compose.common.yml service: minio + azurite: + extends: + file: docker-compose.common.yml + service: azurite inbucket: extends: file: docker-compose.common.yml @@ -60,12 +64,13 @@ services: depends_on: - postgres - minio + - azurite - inbucket - openldap - elasticsearch - opensearch - redis - command: postgres:5432 minio:9000 inbucket:9001 openldap:389 elasticsearch:9200 opensearch:9201 redis:6379 + command: postgres:5432 minio:9000 azurite:10000 inbucket:9001 openldap:389 elasticsearch:9200 opensearch:9201 redis:6379 networks: mm-test: diff --git a/server/build/dotenv/test.env b/server/build/dotenv/test.env index 00d55edbd2f1..41eeb99cf3ae 100644 --- a/server/build/dotenv/test.env +++ b/server/build/dotenv/test.env @@ -3,8 +3,10 @@ GOBIN=/mattermost/server/bin CI_INBUCKET_HOST=inbucket CI_MINIO_HOST=minio +CI_AZURITE_HOST=azurite CI_INBUCKET_PORT=9001 CI_MINIO_PORT=9000 +CI_AZURITE_PORT=10000 CI_INBUCKET_SMTP_PORT=10025 CI_LDAP_HOST=openldap IS_CI=true diff --git a/server/channels/api4/command.go b/server/channels/api4/command.go index e50830a1e4e7..4588a62c309a 100644 --- a/server/channels/api4/command.go +++ b/server/channels/api4/command.go @@ -416,6 +416,7 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { commandArgs.UserId = c.AppContext.Session().UserId commandArgs.T = c.AppContext.T commandArgs.SiteURL = c.GetSiteURLHeader() + commandArgs.ConnectionId = r.Header.Get(model.ConnectionId) response, err := c.App.ExecuteCommand(c.AppContext, &commandArgs) if err != nil { diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go index d4f24e97b773..cda19d0a3ff7 100644 --- a/server/channels/app/channel.go +++ b/server/channels/app/channel.go @@ -850,6 +850,7 @@ func (a *App) UpdateChannelPrivacy(rctx request.CTX, oldChannel *model.Channel, messageWs := model.NewWebSocketEvent(model.WebsocketEventChannelConverted, channel.TeamId, "", "", nil, "") messageWs.Add("channel_id", channel.Id) + messageWs.Add("channel_type", string(channel.Type)) a.Publish(messageWs) return channel, nil diff --git a/server/channels/app/channel_test.go b/server/channels/app/channel_test.go index 167b52b80d1e..d70971a2419c 100644 --- a/server/channels/app/channel_test.go +++ b/server/channels/app/channel_test.go @@ -518,6 +518,51 @@ func TestUpdateChannelPrivacy(t *testing.T) { assert.Equal(t, publicChannel.Type, model.ChannelTypeOpen) } +func TestUpdateChannelPrivacyWebSocketEvent(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + t.Run("private to public includes channel_type O in WS event", func(t *testing.T) { + privateChannel := th.createChannel(t, th.BasicTeam, model.ChannelTypePrivate) + + wsMessages, closeWS := connectFakeWebSocket(t, th, th.BasicUser.Id, "", []model.WebsocketEventType{model.WebsocketEventChannelConverted}) + defer closeWS() + + privateChannel.Type = model.ChannelTypeOpen + _, appErr := th.App.UpdateChannelPrivacy(th.Context, privateChannel, th.BasicUser) + require.Nil(t, appErr) + + select { + case received := <-wsMessages: + data := received.GetData() + assert.Equal(t, privateChannel.Id, data["channel_id"]) + assert.Equal(t, string(model.ChannelTypeOpen), data["channel_type"]) + case <-time.After(10 * time.Second): + require.Fail(t, "Did not receive channel_converted websocket event") + } + }) + + t.Run("public to private includes channel_type P in WS event", func(t *testing.T) { + publicChannel := th.createChannel(t, th.BasicTeam, model.ChannelTypeOpen) + + wsMessages, closeWS := connectFakeWebSocket(t, th, th.BasicUser.Id, "", []model.WebsocketEventType{model.WebsocketEventChannelConverted}) + defer closeWS() + + publicChannel.Type = model.ChannelTypePrivate + _, appErr := th.App.UpdateChannelPrivacy(th.Context, publicChannel, th.BasicUser) + require.Nil(t, appErr) + + select { + case received := <-wsMessages: + data := received.GetData() + assert.Equal(t, publicChannel.Id, data["channel_id"]) + assert.Equal(t, string(model.ChannelTypePrivate), data["channel_type"]) + case <-time.After(10 * time.Second): + require.Fail(t, "Did not receive channel_converted websocket event") + } + }) +} + func TestGetOrCreateDirectChannel(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic(t) diff --git a/server/channels/app/context.go b/server/channels/app/context.go index 83b04e1887a0..0d5e1d5a578f 100644 --- a/server/channels/app/context.go +++ b/server/channels/app/context.go @@ -37,6 +37,7 @@ func pluginContext(rctx request.CTX) *plugin.Context { IPAddress: rctx.IPAddress(), AcceptLanguage: rctx.AcceptLanguage(), UserAgent: rctx.UserAgent(), + ConnectionId: rctx.ConnectionId(), } return context } diff --git a/server/channels/app/context_test.go b/server/channels/app/context_test.go new file mode 100644 index 000000000000..51da9585a6ff --- /dev/null +++ b/server/channels/app/context_test.go @@ -0,0 +1,46 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" +) + +func TestPluginContext(t *testing.T) { + t.Run("creates plugin context with all fields from request context", func(t *testing.T) { + rctx := request.TestContext(t) + session := &model.Session{ + Id: "session-id-123", + UserId: "user-id-456", + } + rctx = rctx.WithSession(session).(*request.Context) + rctx = rctx.WithRequestId("request-id-789").(*request.Context) + rctx = rctx.WithIPAddress("192.168.1.1").(*request.Context) + rctx = rctx.WithAcceptLanguage("en-US").(*request.Context) + rctx = rctx.WithUserAgent("TestAgent/1.0").(*request.Context) + rctx = rctx.WithConnectionId("connection-id-abc").(*request.Context) + + ctx := pluginContext(rctx) + + assert.Equal(t, "request-id-789", ctx.RequestId) + assert.Equal(t, "session-id-123", ctx.SessionId) + assert.Equal(t, "192.168.1.1", ctx.IPAddress) + assert.Equal(t, "en-US", ctx.AcceptLanguage) + assert.Equal(t, "TestAgent/1.0", ctx.UserAgent) + assert.Equal(t, "connection-id-abc", ctx.ConnectionId) + }) + + t.Run("creates plugin context with empty connection id when not set", func(t *testing.T) { + rctx := request.TestContext(t) + + ctx := pluginContext(rctx) + + assert.Empty(t, ctx.ConnectionId) + }) +} diff --git a/server/channels/app/imaging/orientation.go b/server/channels/app/imaging/orientation.go index 98fcffac9dca..35a50fed75b8 100644 --- a/server/channels/app/imaging/orientation.go +++ b/server/channels/app/imaging/orientation.go @@ -10,8 +10,8 @@ import ( "io" "strings" - "github.com/anthonynsimon/bild/transform" "github.com/bep/imagemeta" + "github.com/boxes-ltd/imaging" ) const ( @@ -41,19 +41,19 @@ var errStopDecoding = fmt.Errorf("stop decoding") func MakeImageUpright(img image.Image, orientation int) image.Image { switch orientation { case UprightMirrored: - return transform.FlipH(img) + return imaging.FlipH(img) case UpsideDown: - return transform.Rotate(img, 180, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Rotate180(img) case UpsideDownMirrored: - return transform.FlipV(img) + return imaging.FlipV(img) case RotatedCWMirrored: - return transform.Rotate(transform.FlipH(img), -90, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Transpose(img) case RotatedCCW: - return transform.Rotate(img, 90, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Rotate270(img) case RotatedCCWMirrored: - return transform.Rotate(transform.FlipV(img), -90, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Transverse(img) case RotatedCW: - return transform.Rotate(img, 270, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Rotate90(img) default: return img } diff --git a/server/channels/app/imaging/orientation_test.go b/server/channels/app/imaging/orientation_test.go index 862343c659ef..059af620cf7e 100644 --- a/server/channels/app/imaging/orientation_test.go +++ b/server/channels/app/imaging/orientation_test.go @@ -197,3 +197,57 @@ func TestGetImageOrientation(t *testing.T) { }) } } + +func TestMakeImageUpright(t *testing.T) { + // Each case loads the canonical EXIF fixture for orientation N (the + // 128x128 quadrants pattern in its stored, uncorrected form), applies + // MakeImageUpright(., N), and asserts that the result has the same + // pixels as the upright reference. + tcs := []struct { + name string + orientation int + inputName string + }{ + {"Upright (no-op)", Upright, "quadrants-orientation-1.png"}, + {"UprightMirrored (FlipH)", UprightMirrored, "quadrants-orientation-2.png"}, + {"UpsideDown (Rotate180)", UpsideDown, "quadrants-orientation-3.png"}, + {"UpsideDownMirrored (FlipV)", UpsideDownMirrored, "quadrants-orientation-4.png"}, + {"RotatedCWMirrored (Transpose)", RotatedCWMirrored, "quadrants-orientation-5.png"}, + {"RotatedCCW (Rotate270)", RotatedCCW, "quadrants-orientation-6.png"}, + {"RotatedCCWMirrored (Transverse)", RotatedCCWMirrored, "quadrants-orientation-7.png"}, + {"RotatedCW (Rotate90)", RotatedCW, "quadrants-orientation-8.png"}, + // Unsupported orientations fall through to the default branch and + // return the input unchanged. Pass the upright fixture so the + // no-op result still equals the upright reference. + {"unsupported orientation", 99, "quadrants-orientation-1.png"}, + } + + imgDir, ok := fileutils.FindDir("tests/exif_samples") + require.True(t, ok) + + d, err := NewDecoder(DecoderOptions{}) + require.NoError(t, err) + require.NotNil(t, d) + + uprightFile, err := os.Open(filepath.Join(imgDir, "quadrants-orientation-1.png")) + require.NoError(t, err) + defer uprightFile.Close() + + uprightImg, format, err := d.Decode(uprightFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + inputFile, err := os.Open(filepath.Join(imgDir, tc.inputName)) + require.NoError(t, err) + defer inputFile.Close() + + inputImg, format, err := d.Decode(inputFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + requireSameImage(t, uprightImg, MakeImageUpright(inputImg, tc.orientation)) + }) + } +} diff --git a/server/channels/app/imaging/preview.go b/server/channels/app/imaging/preview.go index f4415f0e51cd..0674d66a7b05 100644 --- a/server/channels/app/imaging/preview.go +++ b/server/channels/app/imaging/preview.go @@ -9,7 +9,7 @@ import ( "image" "image/jpeg" - "github.com/anthonynsimon/bild/transform" + "github.com/boxes-ltd/imaging" ) // GeneratePreview generates the preview for the given image. @@ -18,7 +18,7 @@ func GeneratePreview(img image.Image, width int) image.Image { w := img.Bounds().Dx() if w > width { - preview = Resize(img, width, 0, transform.Lanczos) + preview = imaging.Resize(img, width, 0, imaging.Lanczos) } return preview @@ -31,16 +31,16 @@ func GenerateThumbnail(img image.Image, targetWidth, targetHeight int) image.Ima // We keep aspect ratio and ensure the output dimensions are never higher than the provided targets. if width > height { - return Resize(img, targetWidth, 0, transform.Lanczos) + return imaging.Resize(img, targetWidth, 0, imaging.Lanczos) } - return Resize(img, 0, targetHeight, transform.Lanczos) + return imaging.Resize(img, 0, targetHeight, imaging.Lanczos) } // GenerateMiniPreviewImage generates the mini preview for the given image. func GenerateMiniPreviewImage(img image.Image, w, h, q int) ([]byte, error) { var buf bytes.Buffer - preview := Resize(img, w, h, transform.Lanczos) + preview := imaging.Resize(img, w, h, imaging.Lanczos) if err := jpeg.Encode(&buf, preview, &jpeg.Options{Quality: q}); err != nil { return nil, fmt.Errorf("failed to encode image to JPEG format: %w", err) } diff --git a/server/channels/app/imaging/preview_bench_test.go b/server/channels/app/imaging/preview_bench_test.go new file mode 100644 index 000000000000..4c30f21073ed --- /dev/null +++ b/server/channels/app/imaging/preview_bench_test.go @@ -0,0 +1,124 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package imaging + +import ( + "image" + "image/color" + "image/draw" + "testing" +) + +func newRGBAImage(w, h int) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, w, h)) + draw.Draw(img, img.Bounds(), image.NewUniform(color.RGBA{R: 100, G: 150, B: 200, A: 255}), image.Point{}, draw.Src) + return img +} + +func BenchmarkGeneratePreview(b *testing.B) { + cases := []struct { + name string + w, h int + targetWidth int + }{ + {"2000x1500 -> 1024", 2000, 1500, 1024}, + {"4000x3000 -> 1024", 4000, 3000, 1024}, + {"1024x768 -> 1024 (no-op)", 1024, 768, 1024}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + img := newRGBAImage(tc.w, tc.h) + b.ResetTimer() + for b.Loop() { + GeneratePreview(img, tc.targetWidth) + } + }) + } +} + +func BenchmarkGenerateThumbnail(b *testing.B) { + cases := []struct { + name string + w, h int + targetW, targetH int + }{ + {"2000x1500 landscape -> 120x100", 2000, 1500, 120, 100}, + {"1500x2000 portrait -> 120x100", 1500, 2000, 120, 100}, + {"4000x3000 landscape -> 120x100", 4000, 3000, 120, 100}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + img := newRGBAImage(tc.w, tc.h) + b.ResetTimer() + for b.Loop() { + GenerateThumbnail(img, tc.targetW, tc.targetH) + } + }) + } +} + +func BenchmarkGenerateMiniPreviewImage(b *testing.B) { + cases := []struct { + name string + w, h int + targetW, targetH int + quality int + }{ + {"2000x1500 -> 120x100 q50", 2000, 1500, 120, 100, 50}, + {"4000x3000 -> 120x100 q50", 4000, 3000, 120, 100, 50}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + img := newRGBAImage(tc.w, tc.h) + b.ResetTimer() + for b.Loop() { + _, _ = GenerateMiniPreviewImage(img, tc.targetW, tc.targetH, tc.quality) + } + }) + } +} + +func BenchmarkFillCenter(b *testing.B) { + cases := []struct { + name string + w, h int + targetW, targetH int + }{ + {"2000x1500 -> 120x100", 2000, 1500, 120, 100}, + {"4000x3000 -> 120x100", 4000, 3000, 120, 100}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + img := newRGBAImage(tc.w, tc.h) + b.ResetTimer() + for b.Loop() { + FillCenter(img, tc.targetW, tc.targetH) + } + }) + } +} + +func BenchmarkMakeImageUpright(b *testing.B) { + orientations := []struct { + name string + orientation int + }{ + {"Upright (no-op)", Upright}, + {"UpsideDown (rotate 180)", UpsideDown}, + {"RotatedCCW (rotate 270)", RotatedCCW}, + {"RotatedCW (rotate 90)", RotatedCW}, + {"UprightMirrored (flip H)", UprightMirrored}, + {"UpsideDownMirrored (flip V)", UpsideDownMirrored}, + {"RotatedCWMirrored (transpose)", RotatedCWMirrored}, + {"RotatedCCWMirrored (transverse)", RotatedCCWMirrored}, + } + img := newRGBAImage(2000, 1500) + for _, tc := range orientations { + b.Run(tc.name, func(b *testing.B) { + for b.Loop() { + MakeImageUpright(img, tc.orientation) + } + }) + } +} diff --git a/server/channels/app/imaging/preview_test.go b/server/channels/app/imaging/preview_test.go index 635342759604..f9226b9ffb59 100644 --- a/server/channels/app/imaging/preview_test.go +++ b/server/channels/app/imaging/preview_test.go @@ -4,11 +4,14 @@ package imaging import ( + "bytes" "image" - "image/color" + "os" + "path/filepath" "testing" - "github.com/anthonynsimon/bild/transform" + "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" + "github.com/stretchr/testify/require" ) @@ -78,95 +81,64 @@ func TestGenerateThumbnail(t *testing.T) { } } -func createTestImage(t *testing.T, width, height int) image.Image { - t.Helper() - img := image.NewNRGBA(image.Rect(0, 0, width, height)) - for y := range height { - for x := range width { - img.Set(x, y, color.NRGBA{uint8(x % 256), uint8(y % 256), 0, 255}) - } - } - return img +func TestGeneratePreview(t *testing.T) { + imgDir, ok := fileutils.FindDir("tests") + require.True(t, ok) + + d, err := NewDecoder(DecoderOptions{}) + require.NoError(t, err) + require.NotNil(t, d) + + inputFile, err := os.Open(filepath.Join(imgDir, "qa-data-graph.png")) + require.NoError(t, err) + defer inputFile.Close() + + inputImg, format, err := d.Decode(inputFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + expectedFile, err := os.Open(filepath.Join(imgDir, "preview_test_qa_data_graph_1024.png")) + require.NoError(t, err) + defer expectedFile.Close() + + expectedImg, format, err := d.Decode(expectedFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + preview := GeneratePreview(inputImg, 1024) + requireSameImage(t, expectedImg, preview) } -func TestResize(t *testing.T) { - for _, tc := range []struct { - name string - img image.Image - targetW int - targetH int - expectedW int - expectedH int - }{ - { - name: "zero target dimensions", - img: createTestImage(t, 100, 50), - targetW: 0, - targetH: 0, - expectedW: 0, - expectedH: 0, - }, - { - name: "negative target dimensions", - img: createTestImage(t, 100, 50), - targetW: -1, - targetH: 25, - expectedW: 0, - expectedH: 0, - }, - { - name: "zero source dimensions", - img: createTestImage(t, 0, 0), - targetW: 50, - targetH: 25, - expectedW: 0, - expectedH: 0, - }, - { - name: "preserve aspect ratio with width", - img: createTestImage(t, 100, 50), - targetW: 50, - targetH: 0, - expectedW: 50, - expectedH: 25, - }, - { - name: "preserve aspect ratio with width, height > width", - img: createTestImage(t, 50, 100), - targetW: 50, - targetH: 0, - expectedW: 50, - expectedH: 100, - }, - { - name: "preserve aspect ratio with height", - img: createTestImage(t, 100, 50), - targetW: 0, - targetH: 25, - expectedW: 50, - expectedH: 25, - }, - { - name: "preserve aspect ratio with height, height > width", - img: createTestImage(t, 50, 100), - targetW: 0, - targetH: 25, - expectedW: 13, - expectedH: 25, - }, - { - name: "valid target dimensions", - img: createTestImage(t, 100, 50), - targetW: 50, - targetH: 25, - expectedW: 50, - expectedH: 25, - }, - } { - t.Run(tc.name, func(t *testing.T) { - resizedImg := Resize(tc.img, tc.targetW, tc.targetH, transform.Lanczos) - require.Equal(t, tc.expectedW, resizedImg.Bounds().Dx()) - require.Equal(t, tc.expectedH, resizedImg.Bounds().Dy()) - }) - } +func TestGenerateMiniPreviewImage(t *testing.T) { + imgDir, ok := fileutils.FindDir("tests") + require.True(t, ok) + + d, err := NewDecoder(DecoderOptions{}) + require.NoError(t, err) + require.NotNil(t, d) + + inputFile, err := os.Open(filepath.Join(imgDir, "qa-data-graph.png")) + require.NoError(t, err) + defer inputFile.Close() + + inputImg, format, err := d.Decode(inputFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + expectedFile, err := os.Open(filepath.Join(imgDir, "mini_preview_test_qa_data_graph_16x16_q90.jpg")) + require.NoError(t, err) + defer expectedFile.Close() + + expectedImg, format, err := d.Decode(expectedFile) + require.NoError(t, err) + require.Equal(t, "jpeg", format) + + out, err := GenerateMiniPreviewImage(inputImg, 16, 16, 90) + require.NoError(t, err) + + actualImg, format, err := d.Decode(bytes.NewReader(out)) + require.NoError(t, err) + require.Equal(t, "jpeg", format) + + requireSameImage(t, expectedImg, actualImg) } diff --git a/server/channels/app/imaging/utils.go b/server/channels/app/imaging/utils.go index 0c2eb8bd228a..8197cb30f527 100644 --- a/server/channels/app/imaging/utils.go +++ b/server/channels/app/imaging/utils.go @@ -6,10 +6,8 @@ package imaging import ( "image" "image/color" - "math" - "github.com/anthonynsimon/bild/clone" - "github.com/anthonynsimon/bild/transform" + "github.com/boxes-ltd/imaging" ) type rawImg interface { @@ -142,126 +140,14 @@ func FillImageTransparency(img image.Image, c color.Color) { } } -// CropAnchor cuts out a rectangular region with the specified size -// from the image using the specified anchor point and returns the cropped image. -// Adapted from github.com/disintegration/imaging -func CropCenter(img image.Image, w, h int) image.Image { - srcBounds := img.Bounds() - anchorPoint := image.Pt(srcBounds.Min.X+(srcBounds.Dx()-w)/2, srcBounds.Min.Y+(srcBounds.Dy()-h)/2) - r := image.Rect(0, 0, w, h).Add(anchorPoint) - b := srcBounds.Intersect(r) - return transform.Crop(img, b) -} - -// resizeAndCrop resizes the image to the smallest possible size that will cover the specified dimensions, -// crops the resized image to the specified dimensions using a centered anchor point and returns -// the transformed image. -// Adapted from github.com/disintegration/imaging -func resizeAndCropCenter(img image.Image, width, height int) image.Image { - dstW, dstH := width, height - - srcBounds := img.Bounds() - srcW := srcBounds.Dx() - srcH := srcBounds.Dy() - srcAspectRatio := float64(srcW) / float64(srcH) - dstAspectRatio := float64(dstW) / float64(dstH) - - var tmp image.Image - if srcAspectRatio < dstAspectRatio { - tmp = Resize(img, dstW, 0, transform.Lanczos) - } else { - tmp = Resize(img, 0, dstH, transform.Lanczos) - } - - return CropCenter(tmp, dstW, dstH) -} - // FillCenter creates an image with the specified dimensions and fills it with // the centered and scaled source image. -// To achieve the correct aspect ratio without stretching, the source image will be cropped. -// Adapted from github.com/disintegration/imaging -func FillCenter(img image.Image, dstW, dstH int) image.Image { - if dstW <= 0 || dstH <= 0 { - return &image.RGBA{} - } - - srcBounds := img.Bounds() - srcW := srcBounds.Dx() - srcH := srcBounds.Dy() - - if srcW <= 0 || srcH <= 0 { - return &image.RGBA{} - } - - if srcW == dstW && srcH == dstH { - return clone.AsShallowRGBA(img) - } - - return resizeAndCropCenter(img, dstW, dstH) -} - -// Fit scales down the image to fit the specified -// maximum width and height and returns the transformed image. -// Adapted from github.com/disintegration/imaging -func Fit(img image.Image, maxW, maxH int) image.Image { - if maxW <= 0 || maxH <= 0 { - return &image.NRGBA{} - } - - srcBounds := img.Bounds() - srcW := srcBounds.Dx() - srcH := srcBounds.Dy() - - if srcW <= 0 || srcH <= 0 { - return &image.RGBA{} - } - - if srcW <= maxW && srcH <= maxH { - return clone.AsShallowRGBA(img) - } - - srcAspectRatio := float64(srcW) / float64(srcH) - maxAspectRatio := float64(maxW) / float64(maxH) - - var newW, newH int - if srcAspectRatio > maxAspectRatio { - newW = maxW - newH = int(float64(newW) / srcAspectRatio) - } else { - newH = maxH - newW = int(float64(newH) * srcAspectRatio) - } - - return Resize(img, newW, newH, transform.Lanczos) +func FillCenter(img image.Image, w, h int) *image.NRGBA { + return imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos) } -// Resize resizes the image to the specified width and height using the specified resampling filter and returns the transformed image. -// If one of width or height is 0, the image aspect ratio is preserved. -// Adapted from github.com/disintegration/imaging -func Resize(img image.Image, targetWidth, targetHeight int, filter transform.ResampleFilter) image.Image { - if targetWidth < 0 || targetHeight < 0 { - return &image.NRGBA{} - } - - if targetWidth == 0 && targetHeight == 0 { - return &image.NRGBA{} - } - - srcW := img.Bounds().Dx() - srcH := img.Bounds().Dy() - if srcW <= 0 || srcH <= 0 { - return &image.NRGBA{} - } - - // If new width or height is 0 then preserve aspect ratio, minimum 1px. - if targetWidth == 0 { - tmpW := float64(targetHeight) * float64(srcW) / float64(srcH) - targetWidth = int(math.Max(1.0, math.Floor(tmpW+0.5))) - } - if targetHeight == 0 { - tmpH := float64(targetWidth) * float64(srcH) / float64(srcW) - targetHeight = int(math.Max(1.0, math.Floor(tmpH+0.5))) - } - - return transform.Resize(img, targetWidth, targetHeight, filter) +// Fit scales down the image to fit within the specified maximum dimensions, +// preserving the aspect ratio. +func Fit(img image.Image, maxW, maxH int) *image.NRGBA { + return imaging.Fit(img, maxW, maxH, imaging.Lanczos) } diff --git a/server/channels/app/imaging/utils_test.go b/server/channels/app/imaging/utils_test.go index bf04b2f4e011..17fa7f8ef12e 100644 --- a/server/channels/app/imaging/utils_test.go +++ b/server/channels/app/imaging/utils_test.go @@ -8,6 +8,7 @@ import ( "image" "image/color" "os" + "path/filepath" "testing" "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" @@ -15,6 +16,46 @@ import ( "github.com/stretchr/testify/require" ) +// requireSameImage asserts that want and got cover the same bounds and have +// identical RGBA values at every pixel. We compare decoded pixels rather than +// re-encoded byte streams because image/png is not byte-stable across Go +// versions even for identical pixel content. On mismatch we walk every pixel +// before failing so the report includes the total diff rate, not just the +// first divergence. +func requireSameImage(t *testing.T, want, got image.Image) { + t.Helper() + require.Equal(t, want.Bounds(), got.Bounds()) + b := got.Bounds() + total := b.Dx() * b.Dy() + var ( + diff int + firstX int + firstY int + firstWant [4]uint32 + firstGot [4]uint32 + ) + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + wr, wg, wb, wa := want.At(x, y).RGBA() + gr, gg, gb, ga := got.At(x, y).RGBA() + if wr == gr && wg == gg && wb == gb && wa == ga { + continue + } + if diff == 0 { + firstX, firstY = x, y + firstWant = [4]uint32{wr, wg, wb, wa} + firstGot = [4]uint32{gr, gg, gb, ga} + } + diff++ + } + } + if diff > 0 { + t.Fatalf("%d / %d pixels differ (%.2f%%); first at (%d, %d): want %v got %v", + diff, total, 100*float64(diff)/float64(total), + firstX, firstY, firstWant, firstGot) + } +} + func TestFillImageTransparency(t *testing.T) { tcs := []struct { name string @@ -117,233 +158,98 @@ func TestFillImageTransparency(t *testing.T) { }) } -func TestCropCenter(t *testing.T) { - imgDir, ok := fileutils.FindDir("tests") - require.True(t, ok) - - d, err := NewDecoder(DecoderOptions{}) - require.NotNil(t, d) - require.NoError(t, err) - - for _, tc := range []struct { +func TestFillCenter(t *testing.T) { + tcs := []struct { name string - inputName string outputName string width int height int }{ - { - "Crop to center 100x100", - "crop_test_input.png", - "crop_test_output_100x100.png", - 100, - 100, - }, - { - "Crop to center 45x45", - "crop_test_input.png", - "crop_test_output_45x45.png", - 45, - 45, - }, - { - "Crop to center 100x45", - "crop_test_input.png", - "crop_test_output_100x45.png", - 100, - 45, - }, - { - "Crop to center 45x100", - "crop_test_input.png", - "crop_test_output_45x100.png", - 45, - 100, - }, - } { - t.Run(tc.name, func(t *testing.T) { - inputFile, err := os.Open(imgDir + "/" + tc.inputName) - require.NoError(t, err) - require.NotNil(t, inputFile) - defer func() { - require.NoError(t, inputFile.Close()) - }() - - inputImg, format, err := d.Decode(inputFile) - require.NoError(t, err) - require.NotNil(t, inputImg) - require.Equal(t, "png", format) - - expectedFile, err := os.Open(imgDir + "/" + tc.outputName) - require.NoError(t, err) - require.NotNil(t, expectedFile) - defer func() { - require.NoError(t, expectedFile.Close()) - }() - - expectedImg, format, err := d.Decode(expectedFile) - require.NoError(t, err) - require.NotNil(t, expectedImg) - require.Equal(t, "png", format) - - croppedImg := CropCenter(inputImg, tc.width, tc.height) - require.Equal(t, expectedImg.Bounds().Dx(), croppedImg.Bounds().Dx()) - require.Equal(t, expectedImg.Bounds().Dy(), croppedImg.Bounds().Dy()) - require.Equal(t, expectedImg.(*image.RGBA).Pix, croppedImg.(*image.RGBA).Pix) - }) + {"100x100", "fill_test_output_100x100.png", 100, 100}, + {"45x45", "fill_test_output_45x45.png", 45, 45}, + {"100x45", "fill_test_output_100x45.png", 100, 45}, + {"45x100", "fill_test_output_45x100.png", 45, 100}, } -} -func TestFit(t *testing.T) { imgDir, ok := fileutils.FindDir("tests") require.True(t, ok) d, err := NewDecoder(DecoderOptions{}) - require.NotNil(t, d) require.NoError(t, err) + require.NotNil(t, d) - for _, tc := range []struct { - name string - inputName string - outputName string - width int - height int - }{ - { - "Fit to 100x100", - "fit_test_input.png", - "fit_test_output_100x100.png", - 100, - 100, - }, - { - "Fit to 45x45", - "fit_test_input.png", - "fit_test_output_45x45.png", - 45, - 45, - }, - { - "Fit to 100x45", - "fit_test_input.png", - "fit_test_output_100x45.png", - 100, - 45, - }, - { - "Fit to 45x100", - "fit_test_input.png", - "fit_test_output_45x100.png", - 45, - 100, - }, - } { - t.Run(tc.name, func(t *testing.T) { - inputFile, err := os.Open(imgDir + "/" + tc.inputName) - require.NoError(t, err) - require.NotNil(t, inputFile) - defer func() { - require.NoError(t, inputFile.Close()) - }() + inputFile, err := os.Open(filepath.Join(imgDir, "fill_test_input.png")) + require.NoError(t, err) + defer inputFile.Close() - inputImg, format, err := d.Decode(inputFile) - require.NoError(t, err) - require.NotNil(t, inputImg) - require.Equal(t, "png", format) + inputImg, format, err := d.Decode(inputFile) + require.NoError(t, err) + require.Equal(t, "png", format) - expectedFile, err := os.Open(imgDir + "/" + tc.outputName) + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + expectedFile, err := os.Open(filepath.Join(imgDir, tc.outputName)) require.NoError(t, err) - require.NotNil(t, expectedFile) - defer func() { - require.NoError(t, expectedFile.Close()) - }() + defer expectedFile.Close() expectedImg, format, err := d.Decode(expectedFile) require.NoError(t, err) - require.NotNil(t, expectedImg) require.Equal(t, "png", format) - fittedImg := Fit(inputImg, tc.width, tc.height) - require.Equal(t, expectedImg, fittedImg) + out := FillCenter(inputImg, tc.width, tc.height) + requireSameImage(t, expectedImg, out) }) } } -func TestFillCenter(t *testing.T) { - imgDir, ok := fileutils.FindDir("tests") - require.True(t, ok) - - d, err := NewDecoder(DecoderOptions{}) - require.NotNil(t, d) - require.NoError(t, err) - +func TestFit(t *testing.T) { tcs := []struct { - name string - inputName string - outputName string - width int - height int + name string + inputImg image.Image + maxW int + maxH int + expectedWidth int + expectedHeight int }{ { - "Fill center 100x100", - "fill_test_input.png", - "fill_test_output_100x100.png", - 100, - 100, + name: "no resize when smaller than bounds", + inputImg: image.NewRGBA(image.Rect(0, 0, 50, 50)), + maxW: 100, + maxH: 100, + expectedWidth: 50, + expectedHeight: 50, }, { - "Fill center 45x45", - "fill_test_input.png", - "fill_test_output_45x45.png", - 45, - 45, + name: "landscape clamps to width", + inputImg: image.NewRGBA(image.Rect(0, 0, 200, 100)), + maxW: 100, + maxH: 100, + expectedWidth: 100, + expectedHeight: 50, }, { - "Fill center 100x45", - "fill_test_input.png", - "fill_test_output_100x45.png", - 100, - 45, + name: "portrait clamps to height", + inputImg: image.NewRGBA(image.Rect(0, 0, 100, 200)), + maxW: 100, + maxH: 100, + expectedWidth: 50, + expectedHeight: 100, }, { - "Fill center 45x100", - "fill_test_input.png", - "fill_test_output_45x100.png", - 45, - 100, + name: "both dimensions exceed", + inputImg: image.NewRGBA(image.Rect(0, 0, 400, 200)), + maxW: 100, + maxH: 100, + expectedWidth: 100, + expectedHeight: 50, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - inputFile, err := os.Open(imgDir + "/" + tc.inputName) - require.NoError(t, err) - require.NotNil(t, inputFile) - defer func() { - require.NoError(t, inputFile.Close()) - }() - - inputImg, format, err := d.Decode(inputFile) - require.NoError(t, err) - require.NotNil(t, inputImg) - require.Equal(t, "png", format) - - expectedFile, err := os.Open(imgDir + "/" + tc.outputName) - require.NoError(t, err) - require.NotNil(t, expectedFile) - defer func() { - require.NoError(t, expectedFile.Close()) - }() - - expectedImg, format, err := d.Decode(expectedFile) - require.NoError(t, err) - require.NotNil(t, expectedImg) - require.Equal(t, "png", format) - - filledImg := FillCenter(inputImg, tc.width, tc.height) - require.Equal(t, expectedImg.Bounds().Dx(), filledImg.Bounds().Dx()) - require.Equal(t, expectedImg.Bounds().Dy(), filledImg.Bounds().Dy()) - require.Equal(t, expectedImg.(*image.RGBA).Pix, filledImg.(*image.RGBA).Pix) + out := Fit(tc.inputImg, tc.maxW, tc.maxH) + require.Equal(t, tc.expectedWidth, out.Bounds().Dx()) + require.Equal(t, tc.expectedHeight, out.Bounds().Dy()) }) } } diff --git a/server/channels/app/plugin_hooks_test.go b/server/channels/app/plugin_hooks_test.go index 0a595480c3b4..82562d734dff 100644 --- a/server/channels/app/plugin_hooks_test.go +++ b/server/channels/app/plugin_hooks_test.go @@ -712,6 +712,55 @@ func TestHookFileWillBeUploaded(t *testing.T) { require.NoError(t, err) assert.Equal(t, "changedtext", resultBuf.String()) }) + + t.Run("connection id propagated to plugin context", func(t *testing.T) { + mainHelper.Parallel(t) + th := Setup(t).InitBasic(t) + + const connectionID = "test-connection-id-xyz" + + var mockAPI plugintest.API + mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil) + mockAPI.On("LogDebug", "testhook.txt").Return(nil) + mockAPI.On("LogDebug", "inputfile").Return(nil) + mockAPI.On("LogDebug", "connection_id="+connectionID).Return(nil) + tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{ + ` + package main + + import ( + "io" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) { + p.API.LogDebug("connection_id=" + c.ConnectionId) + return nil, "" + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `, + }, th.App, func(*model.Manifest) plugin.API { return &mockAPI }) + defer tearDown() + + rctx := th.Context.WithConnectionId(connectionID) + + _, appErr := th.App.UploadFile(rctx, + []byte("inputfile"), + th.BasicChannel.Id, + "testhook.txt", + ) + require.Nil(t, appErr) + + mockAPI.AssertCalled(t, "LogDebug", "connection_id="+connectionID) + }) } func TestUserWillLogIn_Blocked(t *testing.T) { diff --git a/server/channels/app/plugin_requests.go b/server/channels/app/plugin_requests.go index 52e25f17aea3..ef033f4888d5 100644 --- a/server/channels/app/plugin_requests.go +++ b/server/channels/app/plugin_requests.go @@ -164,6 +164,7 @@ func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, h IPAddress: utils.GetIPAddress(r, ch.cfgSvc.Config().ServiceSettings.TrustedProxyIPHeader), AcceptLanguage: r.Header.Get("Accept-Language"), UserAgent: r.UserAgent(), + ConnectionId: r.Header.Get(model.ConnectionId), } pluginID := mux.Vars(r)["plugin_id"] diff --git a/server/channels/app/plugin_requests_test.go b/server/channels/app/plugin_requests_test.go index 87920a1b9406..44f688ab9eaf 100644 --- a/server/channels/app/plugin_requests_test.go +++ b/server/channels/app/plugin_requests_test.go @@ -451,6 +451,38 @@ func TestServePluginRequest(t *testing.T) { require.True(t, handlerCalled) }) + t.Run("connection id passed to plugin context", func(t *testing.T) { + connectionId := "test-connection-id-abc123" + req := httptest.NewRequest(http.MethodGet, "/plugins/testplugin/endpoint", nil) + req = mux.SetURLVars(req, map[string]string{"plugin_id": "testplugin"}) + req.Header.Set(model.ConnectionId, connectionId) + rr := httptest.NewRecorder() + + handlerCalled := false + mockHandler := func(ctx *plugin.Context, w http.ResponseWriter, r *http.Request) { + handlerCalled = true + assert.Equal(t, connectionId, ctx.ConnectionId) + } + + th.App.ch.servePluginRequest(rr, req, mockHandler) + require.True(t, handlerCalled) + }) + + t.Run("empty connection id when header not present", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/plugins/testplugin/endpoint", nil) + req = mux.SetURLVars(req, map[string]string{"plugin_id": "testplugin"}) + rr := httptest.NewRecorder() + + handlerCalled := false + mockHandler := func(ctx *plugin.Context, w http.ResponseWriter, r *http.Request) { + handlerCalled = true + assert.Empty(t, ctx.ConnectionId) + } + + th.App.ch.servePluginRequest(rr, req, mockHandler) + require.True(t, handlerCalled) + }) + t.Run("subpath handling", func(t *testing.T) { // Set up with subpath th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065/subpath" }) diff --git a/server/channels/web/handlers.go b/server/channels/web/handlers.go index 1ec64fb611e3..8a7f17235940 100644 --- a/server/channels/web/handlers.go +++ b/server/channels/web/handlers.go @@ -191,6 +191,10 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t, ) + if connectionId := r.Header.Get(model.ConnectionId); connectionId != "" { + c.AppContext = c.AppContext.WithConnectionId(connectionId) + } + c.Params = ParamsFromRequest(r) c.Logger = c.App.Log() diff --git a/server/channels/web/handlers_test.go b/server/channels/web/handlers_test.go index 456c3f7c14b4..47d05b7cb1ae 100644 --- a/server/channels/web/handlers_test.go +++ b/server/channels/web/handlers_test.go @@ -1122,6 +1122,50 @@ func TestHandlerServeHTTPRequestPayloadLimit(t *testing.T) { }) } +func TestHandlerConnectionIdHeader(t *testing.T) { + t.Run("should set connection id from header on request context", func(t *testing.T) { + th := SetupWithStoreMock(t) + + connectionId := "test-connection-id-12345" + var capturedConnectionId string + + handlerFunc := func(c *Context, w http.ResponseWriter, r *http.Request) { + capturedConnectionId = c.AppContext.ConnectionId() + } + + web := New(th.Server) + handler := web.NewHandler(handlerFunc) + + request := httptest.NewRequest("GET", "/api/v4/test", nil) + request.Header.Set(model.ConnectionId, connectionId) + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + + assert.Equal(t, http.StatusOK, response.Code) + assert.Equal(t, connectionId, capturedConnectionId) + }) + + t.Run("should have empty connection id when header not present", func(t *testing.T) { + th := SetupWithStoreMock(t) + + var capturedConnectionId string + + handlerFunc := func(c *Context, w http.ResponseWriter, r *http.Request) { + capturedConnectionId = c.AppContext.ConnectionId() + } + + web := New(th.Server) + handler := web.NewHandler(handlerFunc) + + request := httptest.NewRequest("GET", "/api/v4/test", nil) + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + + assert.Equal(t, http.StatusOK, response.Code) + assert.Empty(t, capturedConnectionId) + }) +} + func TestHandleContextErrorZeroStatusCode(t *testing.T) { t.Run("should set StatusCode to 500 when AppError has zero StatusCode", func(t *testing.T) { th := SetupWithStoreMock(t) diff --git a/server/config.mk b/server/config.mk index f022c7ecf5b2..afd9556dd89f 100644 --- a/server/config.mk +++ b/server/config.mk @@ -4,7 +4,7 @@ # Enable services to be run in docker. # -# Possible options: postgres, minio, inbucket, openldap, dejavu, +# Possible options: postgres, minio, azurite, inbucket, openldap, dejavu, # keycloak, elasticsearch, opensearch, redis, prometheus, # grafana, loki and otel-collector. # diff --git a/server/docker-compose.makefile.yml b/server/docker-compose.makefile.yml index 836f658346af..b8cd037d5cdb 100644 --- a/server/docker-compose.makefile.yml +++ b/server/docker-compose.makefile.yml @@ -18,6 +18,14 @@ services: extends: file: build/docker-compose.common.yml service: minio + azurite: + restart: 'no' + container_name: mattermost-azurite + ports: + - "10000:10000" + extends: + file: build/docker-compose.common.yml + service: azurite inbucket: restart: 'no' container_name: mattermost-inbucket diff --git a/server/go.mod b/server/go.mod index b2407cba7a08..1be1ca65204c 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,7 +5,7 @@ go 1.25.9 require ( code.sajari.com/docconv/v2 v2.0.0-pre.4 github.com/Masterminds/semver/v3 v3.4.0 - github.com/anthonynsimon/bild v0.14.0 + github.com/boxes-ltd/imaging v1.7.5 github.com/avct/uasurfer v0.0.0-20250915105040-a942f6fb6edc github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.32.13 diff --git a/server/go.sum b/server/go.sum index bbef0ad28e6d..e7c99b8a9e36 100644 --- a/server/go.sum +++ b/server/go.sum @@ -36,8 +36,6 @@ github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9Pq github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync= -github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs= github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= @@ -99,6 +97,8 @@ github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4 github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/boxes-ltd/imaging v1.7.5 h1:k4kYxJEhysoGhEEN1IEeKoSbnG8/8snjj7M48Ok0fnk= +github.com/boxes-ltd/imaging v1.7.5/go.mod h1:+8H+oRvis3InOFtTpcoCCB1RDXqo6p9tQBtjZfWnrC8= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= diff --git a/server/public/model/command_args.go b/server/public/model/command_args.go index 24781c98c0cf..136bd0205cbd 100644 --- a/server/public/model/command_args.go +++ b/server/public/model/command_args.go @@ -14,6 +14,7 @@ type CommandArgs struct { RootId string `json:"root_id"` ParentId string `json:"parent_id"` TriggerId string `json:"trigger_id,omitempty"` + ConnectionId string `json:"connection_id,omitempty"` Command string `json:"command"` SiteURL string `json:"-"` T i18n.TranslateFunc `json:"-"` @@ -23,14 +24,15 @@ type CommandArgs struct { func (o *CommandArgs) Auditable() map[string]any { return map[string]any{ - "user_id": o.UserId, - "channel_id": o.ChannelId, - "team_id": o.TeamId, - "root_id": o.RootId, - "parent_id": o.ParentId, - "trigger_id": o.TriggerId, - "command": o.Command, - "site_url": o.SiteURL, + "user_id": o.UserId, + "channel_id": o.ChannelId, + "team_id": o.TeamId, + "root_id": o.RootId, + "parent_id": o.ParentId, + "trigger_id": o.TriggerId, + "connection_id": o.ConnectionId, + "command": o.Command, + "site_url": o.SiteURL, } } diff --git a/server/public/model/command_args_test.go b/server/public/model/command_args_test.go index 7f4613eb29eb..970597189991 100644 --- a/server/public/model/command_args_test.go +++ b/server/public/model/command_args_test.go @@ -9,6 +9,45 @@ import ( "github.com/stretchr/testify/require" ) +func TestCommandArgs_Auditable(t *testing.T) { + t.Run("includes connection_id in auditable output", func(t *testing.T) { + args := CommandArgs{ + UserId: "user-id", + ChannelId: "channel-id", + TeamId: "team-id", + RootId: "root-id", + ParentId: "parent-id", + TriggerId: "trigger-id", + ConnectionId: "connection-id-123", + Command: "/test command", + SiteURL: "http://localhost:8065", + } + + auditable := args.Auditable() + + require.Equal(t, "user-id", auditable["user_id"]) + require.Equal(t, "channel-id", auditable["channel_id"]) + require.Equal(t, "team-id", auditable["team_id"]) + require.Equal(t, "root-id", auditable["root_id"]) + require.Equal(t, "parent-id", auditable["parent_id"]) + require.Equal(t, "trigger-id", auditable["trigger_id"]) + require.Equal(t, "connection-id-123", auditable["connection_id"]) + require.Equal(t, "/test command", auditable["command"]) + require.Equal(t, "http://localhost:8065", auditable["site_url"]) + }) + + t.Run("includes empty connection_id when not set", func(t *testing.T) { + args := CommandArgs{ + UserId: "user-id", + Command: "/test", + } + + auditable := args.Auditable() + + require.Equal(t, "", auditable["connection_id"]) + }) +} + func TestCommandArgs_AddUserMention(t *testing.T) { fixture := []struct { args CommandArgs diff --git a/server/public/plugin/context.go b/server/public/plugin/context.go index bfd3c2e54056..33150e5f8fe7 100644 --- a/server/public/plugin/context.go +++ b/server/public/plugin/context.go @@ -8,6 +8,7 @@ package plugin // For hooks, app.PluginContext() is called. type Context struct { SessionId string + ConnectionId string RequestId string IPAddress string AcceptLanguage string diff --git a/server/public/shared/request/context.go b/server/public/shared/request/context.go index ab96e180a1d9..c8e65963dbb9 100644 --- a/server/public/shared/request/context.go +++ b/server/public/shared/request/context.go @@ -22,6 +22,7 @@ type Context struct { path string userAgent string acceptLanguage string + connectionId string logger mlog.LoggerIFace context context.Context } @@ -96,6 +97,17 @@ func (c *Context) AcceptLanguage() string { return c.acceptLanguage } +// ConnectionId returns the identifier of the WebSocket connection associated +// with the request, when present. It is populated from the "Connection-Id" +// HTTP header that authenticated clients set when they have an active +// WebSocket connection, allowing handlers and plugins to correlate an HTTP +// request with its originating WebSocket connection. Returns an empty string +// when the header is absent (e.g., requests from clients without an active +// WebSocket connection or from non-WebSocket integrations). +func (c *Context) ConnectionId() string { + return c.connectionId +} + func (c *Context) Logger() mlog.LoggerIFace { return c.logger } @@ -156,6 +168,12 @@ func (c *Context) WithAcceptLanguage(s string) CTX { return rctx } +func (c *Context) WithConnectionId(s string) CTX { + rctx := c.clone() + rctx.connectionId = s + return rctx +} + func (c *Context) WithContext(ctx context.Context) CTX { rctx := c.clone() rctx.context = ctx @@ -188,6 +206,7 @@ type CTX interface { Path() string UserAgent() string AcceptLanguage() string + ConnectionId() string Logger() mlog.LoggerIFace Context() context.Context WithT(i18n.TranslateFunc) CTX @@ -198,6 +217,7 @@ type CTX interface { WithPath(string) CTX WithUserAgent(string) CTX WithAcceptLanguage(string) CTX + WithConnectionId(string) CTX WithLogger(mlog.LoggerIFace) CTX WithLogFields(fields ...mlog.Field) CTX WithContext(ctx context.Context) CTX diff --git a/server/public/shared/request/context_test.go b/server/public/shared/request/context_test.go index 40cdd3ba6e4f..53fc9fd29a75 100644 --- a/server/public/shared/request/context_test.go +++ b/server/public/shared/request/context_test.go @@ -11,6 +11,32 @@ import ( "github.com/stretchr/testify/require" ) +func TestContext_WithConnectionId(t *testing.T) { + t.Run("returns new context with connection id", func(t *testing.T) { + originalCtx := TestContext(t) + connectionId := "test-connection-id-123" + + newCtx := originalCtx.WithConnectionId(connectionId) + + require.NotNil(t, newCtx) + assert.NotSame(t, originalCtx, newCtx, "should return a new context instance") + assert.Equal(t, connectionId, newCtx.ConnectionId()) + assert.Empty(t, originalCtx.ConnectionId(), "original context should remain unchanged") + }) + + t.Run("returns new context with empty connection id", func(t *testing.T) { + originalCtx := TestContext(t) + originalCtx = originalCtx.WithConnectionId("existing-id").(*Context) + + newCtx := originalCtx.WithConnectionId("") + + require.NotNil(t, newCtx) + assert.NotSame(t, originalCtx, newCtx, "should return a new context instance") + assert.Empty(t, newCtx.ConnectionId()) + assert.Equal(t, "existing-id", originalCtx.ConnectionId(), "original context should remain unchanged") + }) +} + func TestContext_WithSession(t *testing.T) { t.Run("returns new context with empty session when session is nil", func(t *testing.T) { originalCtx := TestContext(t) diff --git a/server/scripts/vet-api-check.sh b/server/scripts/vet-api-check.sh index 7dc1ebc30e5b..96b2570edce4 100755 --- a/server/scripts/vet-api-check.sh +++ b/server/scripts/vet-api-check.sh @@ -2,59 +2,15 @@ set -euo pipefail IFS=$'\n\t' -# Our API vet checks haven't been running for a long time, and there are lots of undocumented APIs. -# To stem the introduction of new, undocumented APIs while we find time to document the old ones, -# filter out all the "known issues" to support the automated CI check. - API_YAML=$ROOT../api/v4/html/static/mattermost-openapi-v4.yaml OUTPUT=$($GO vet -vettool=$GOBIN/mattermost-govet -openApiSync -openApiSync.spec=$API_YAML ./... 2>&1 || true) -echo "All output, some ignored" -echo "========================" +echo "OpenAPI vet output" +echo "==================" echo "$OUTPUT" OUTPUT_EXCLUDING_IGNORED=$(echo "$OUTPUT" | grep -Fv \ -e 'go: downloading' \ - -e 'github.com/mattermost/mattermost/server/v8/channels/api4' \ - -e 'Cannot find /api/v4/channels/members/{user_id}/mark_read method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/channels/members/{user_id}/mark_read method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/channels/stats/member_count method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/channels/{channel_id}/convert_to_channel method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/client_perf method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/cloud/products/selfhosted method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/cloud/subscription/self-serve-status method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/cloud/request-trial method: PUT in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/cloud/validate-business-email method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/cloud/validate-workspace-business-email method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/cloud/check-cws-connection method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/cloud/delete-workspace method: DELETE in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/drafts method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/users/{user_id}/teams/{team_id}/drafts method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/users/{user_id}/channels/{channel_id}/drafts/{thread_id} method: DELETE in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/users/{user_id}/channels/{channel_id}/drafts method: DELETE in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/exports/{export_name:.+\\.zip}/presign-url method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/hosted_customer/signup_available method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/hosted_customer/bootstrap method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/hosted_customer/customer method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/hosted_customer/confirm method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/hosted_customer/confirm-expand method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/hosted_customer/invoices method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/license/review method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/license/review/status method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/posts/{post_id}/edit_history method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/posts/{post_id}/info method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/posts/search method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/logs/query method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/latest_version method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/system/onboarding/complete method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/system/onboarding/complete method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/system/schema/version method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/usage/teams method: GET in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/users/login/desktop_token method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/users/notify-admin method: POST in OpenAPI 3 spec.' \ - -e 'Cannot find /api/v4/users/trigger-notify-admin-posts method: POST in OpenAPI 3 spec.' \ - -e "Handler /api/v4/cloud/subscription is defined with method PUT, but it's not in the spec" \ 2>&1 || true) if [[ ! -z "${OUTPUT_EXCLUDING_IGNORED// }" ]]; then @@ -63,6 +19,6 @@ if [[ ! -z "${OUTPUT_EXCLUDING_IGNORED// }" ]]; then echo "$OUTPUT_EXCLUDING_IGNORED" exit 1 else - echo "Ignoring above errors." + echo "openApiSync passed." exit 0 fi diff --git a/server/tests/crop_test_input.png b/server/tests/crop_test_input.png deleted file mode 100644 index 04f885b319ea..000000000000 Binary files a/server/tests/crop_test_input.png and /dev/null differ diff --git a/server/tests/crop_test_output_100x100.png b/server/tests/crop_test_output_100x100.png deleted file mode 100644 index a01245624ec3..000000000000 Binary files a/server/tests/crop_test_output_100x100.png and /dev/null differ diff --git a/server/tests/crop_test_output_100x45.png b/server/tests/crop_test_output_100x45.png deleted file mode 100644 index 34ef2eb99065..000000000000 Binary files a/server/tests/crop_test_output_100x45.png and /dev/null differ diff --git a/server/tests/crop_test_output_45x100.png b/server/tests/crop_test_output_45x100.png deleted file mode 100644 index d6c58d428eb4..000000000000 Binary files a/server/tests/crop_test_output_45x100.png and /dev/null differ diff --git a/server/tests/crop_test_output_45x45.png b/server/tests/crop_test_output_45x45.png deleted file mode 100644 index e33bbba2fe61..000000000000 Binary files a/server/tests/crop_test_output_45x45.png and /dev/null differ diff --git a/server/tests/exif_samples/quadrants-orientation-2.png b/server/tests/exif_samples/quadrants-orientation-2.png new file mode 100644 index 000000000000..30fa30941dcf Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-2.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-3.png b/server/tests/exif_samples/quadrants-orientation-3.png new file mode 100644 index 000000000000..f635ec402393 Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-3.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-4.png b/server/tests/exif_samples/quadrants-orientation-4.png new file mode 100644 index 000000000000..41c7fb8cb64d Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-4.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-5.png b/server/tests/exif_samples/quadrants-orientation-5.png new file mode 100644 index 000000000000..59967716e2e9 Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-5.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-6.png b/server/tests/exif_samples/quadrants-orientation-6.png new file mode 100644 index 000000000000..c04973877dca Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-6.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-7.png b/server/tests/exif_samples/quadrants-orientation-7.png new file mode 100644 index 000000000000..bf1e4bcff181 Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-7.png differ diff --git a/server/tests/fill_test_output_100x100.png b/server/tests/fill_test_output_100x100.png index 514e639a27cf..736a6e6e1708 100644 Binary files a/server/tests/fill_test_output_100x100.png and b/server/tests/fill_test_output_100x100.png differ diff --git a/server/tests/fill_test_output_100x45.png b/server/tests/fill_test_output_100x45.png index b0ed90495eaf..b624b4c926bf 100644 Binary files a/server/tests/fill_test_output_100x45.png and b/server/tests/fill_test_output_100x45.png differ diff --git a/server/tests/fill_test_output_45x100.png b/server/tests/fill_test_output_45x100.png index 9e8c37b5a9cb..9e0c5d0de185 100644 Binary files a/server/tests/fill_test_output_45x100.png and b/server/tests/fill_test_output_45x100.png differ diff --git a/server/tests/fill_test_output_45x45.png b/server/tests/fill_test_output_45x45.png index 696d8b0a5a2a..12630e1588b1 100644 Binary files a/server/tests/fill_test_output_45x45.png and b/server/tests/fill_test_output_45x45.png differ diff --git a/server/tests/fit_test_input.png b/server/tests/fit_test_input.png deleted file mode 100644 index 50b76a159bab..000000000000 Binary files a/server/tests/fit_test_input.png and /dev/null differ diff --git a/server/tests/fit_test_output_100x100.png b/server/tests/fit_test_output_100x100.png deleted file mode 100644 index 4ffee7a51f36..000000000000 Binary files a/server/tests/fit_test_output_100x100.png and /dev/null differ diff --git a/server/tests/fit_test_output_100x45.png b/server/tests/fit_test_output_100x45.png deleted file mode 100644 index 437ab0a4ce78..000000000000 Binary files a/server/tests/fit_test_output_100x45.png and /dev/null differ diff --git a/server/tests/fit_test_output_45x100.png b/server/tests/fit_test_output_45x100.png deleted file mode 100644 index 3e3521acb007..000000000000 Binary files a/server/tests/fit_test_output_45x100.png and /dev/null differ diff --git a/server/tests/fit_test_output_45x45.png b/server/tests/fit_test_output_45x45.png deleted file mode 100644 index 3e3521acb007..000000000000 Binary files a/server/tests/fit_test_output_45x45.png and /dev/null differ diff --git a/server/tests/mini_preview_test_qa_data_graph_16x16_q90.jpg b/server/tests/mini_preview_test_qa_data_graph_16x16_q90.jpg new file mode 100644 index 000000000000..e2ac536b2806 Binary files /dev/null and b/server/tests/mini_preview_test_qa_data_graph_16x16_q90.jpg differ diff --git a/server/tests/orientation_test_2_expected_preview.jpeg b/server/tests/orientation_test_2_expected_preview.jpeg index 769e4e19300b..262510eb822c 100644 Binary files a/server/tests/orientation_test_2_expected_preview.jpeg and b/server/tests/orientation_test_2_expected_preview.jpeg differ diff --git a/server/tests/orientation_test_3_expected_preview.jpeg b/server/tests/orientation_test_3_expected_preview.jpeg index 26aeb04a256b..ab59f368be5f 100644 Binary files a/server/tests/orientation_test_3_expected_preview.jpeg and b/server/tests/orientation_test_3_expected_preview.jpeg differ diff --git a/server/tests/orientation_test_6_expected_preview.jpeg b/server/tests/orientation_test_6_expected_preview.jpeg index fecf18955df6..5909639e62d9 100644 Binary files a/server/tests/orientation_test_6_expected_preview.jpeg and b/server/tests/orientation_test_6_expected_preview.jpeg differ diff --git a/server/tests/orientation_test_7_expected_preview.jpeg b/server/tests/orientation_test_7_expected_preview.jpeg index 7f5b51980461..8ea4c2ae440e 100644 Binary files a/server/tests/orientation_test_7_expected_preview.jpeg and b/server/tests/orientation_test_7_expected_preview.jpeg differ diff --git a/server/tests/orientation_test_8_expected_preview.jpeg b/server/tests/orientation_test_8_expected_preview.jpeg index 6b5803de0f9e..bfe7b7a63885 100644 Binary files a/server/tests/orientation_test_8_expected_preview.jpeg and b/server/tests/orientation_test_8_expected_preview.jpeg differ diff --git a/server/tests/preview_test_qa_data_graph_1024.png b/server/tests/preview_test_qa_data_graph_1024.png new file mode 100644 index 000000000000..891cbd08ddce Binary files /dev/null and b/server/tests/preview_test_qa_data_graph_1024.png differ diff --git a/server/tests/testgif_expected_thumbnail.jpg b/server/tests/testgif_expected_thumbnail.jpg index 967a6589be55..47711fd2ede4 100644 Binary files a/server/tests/testgif_expected_thumbnail.jpg and b/server/tests/testgif_expected_thumbnail.jpg differ diff --git a/webapp/channels/src/actions/file_actions.ts b/webapp/channels/src/actions/file_actions.ts index 05cf454d0c24..85f1b8aa1a81 100644 --- a/webapp/channels/src/actions/file_actions.ts +++ b/webapp/channels/src/actions/file_actions.ts @@ -11,6 +11,8 @@ import {getLogErrorAction} from 'mattermost-redux/actions/errors'; import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers'; import {Client4} from 'mattermost-redux/client'; +import {getConnectionId} from 'selectors/general'; + import type {FilePreviewInfo} from 'components/file_preview/file_preview'; import {localizeMessage} from 'utils/utils'; @@ -52,6 +54,11 @@ export function uploadFile({file, name, type, rootId, channelId, clientId, onPro xhr.setRequestHeader('Accept', 'application/json'); + const connectionId = getConnectionId(getState()); + if (connectionId) { + xhr.setRequestHeader('Connection-Id', connectionId); + } + const formData = new FormData(); formData.append('channel_id', channelId); formData.append('client_ids', clientId); diff --git a/webapp/channels/src/actions/websocket_actions.test.jsx b/webapp/channels/src/actions/websocket_actions.test.jsx index 2cd203d46cae..d9a2a886f0ba 100644 --- a/webapp/channels/src/actions/websocket_actions.test.jsx +++ b/webapp/channels/src/actions/websocket_actions.test.jsx @@ -1682,3 +1682,143 @@ describe('handleCustomAttributeCRUD', () => { }); }); }); + +describe('handleChannelConvertedEvent', () => { + const channelId = 'converted-channel'; + + beforeEach(() => { + store.dispatch.mockClear(); + mockState = { + ...mockState, + entities: { + ...mockState.entities, + channels: { + ...mockState.entities.channels, + channels: { + ...mockState.entities.channels.channels, + [channelId]: { + id: channelId, + team_id: 'currentTeamId', + type: Constants.PRIVATE_CHANNEL, + name: 'test-channel', + }, + }, + }, + }, + }; + }); + + test('should update channel type from private to public when channel_type is O', () => { + const msg = { + event: 'channel_converted', + data: { + channel_id: channelId, + channel_type: Constants.OPEN_CHANNEL, + }, + }; + + handleEvent(msg); + + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'RECEIVED_CHANNEL', + data: expect.objectContaining({ + id: channelId, + type: Constants.OPEN_CHANNEL, + }), + }); + }); + + test('should update channel type from public to private when channel_type is P', () => { + mockState.entities.channels.channels[channelId].type = Constants.OPEN_CHANNEL; + + const msg = { + event: 'channel_converted', + data: { + channel_id: channelId, + channel_type: Constants.PRIVATE_CHANNEL, + }, + }; + + handleEvent(msg); + + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'RECEIVED_CHANNEL', + data: expect.objectContaining({ + id: channelId, + type: Constants.PRIVATE_CHANNEL, + }), + }); + }); + + test('should fall back to private when channel_type is not present (backwards compat)', () => { + mockState.entities.channels.channels[channelId].type = Constants.OPEN_CHANNEL; + + const msg = { + event: 'channel_converted', + data: { + channel_id: channelId, + }, + }; + + handleEvent(msg); + + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'RECEIVED_CHANNEL', + data: expect.objectContaining({ + id: channelId, + type: Constants.PRIVATE_CHANNEL, + }), + }); + }); + + test('should not dispatch when channel is not in state', () => { + const msg = { + event: 'channel_converted', + data: { + channel_id: 'nonexistent-channel', + channel_type: Constants.OPEN_CHANNEL, + }, + }; + + handleEvent(msg); + + expect(store.dispatch).not.toHaveBeenCalledWith( + expect.objectContaining({type: 'RECEIVED_CHANNEL'}), + ); + }); + + test('should not dispatch when channel_id is missing', () => { + const msg = { + event: 'channel_converted', + data: {}, + }; + + handleEvent(msg); + + expect(store.dispatch).not.toHaveBeenCalledWith( + expect.objectContaining({type: 'RECEIVED_CHANNEL'}), + ); + }); + + test('should preserve other channel properties when updating type', () => { + const msg = { + event: 'channel_converted', + data: { + channel_id: channelId, + channel_type: Constants.OPEN_CHANNEL, + }, + }; + + handleEvent(msg); + + expect(store.dispatch).toHaveBeenCalledWith({ + type: 'RECEIVED_CHANNEL', + data: { + id: channelId, + team_id: 'currentTeamId', + type: Constants.OPEN_CHANNEL, + name: 'test-channel', + }, + }); + }); +}); diff --git a/webapp/channels/src/actions/websocket_actions.ts b/webapp/channels/src/actions/websocket_actions.ts index c76c3d7d0a28..95cc9ca88d6f 100644 --- a/webapp/channels/src/actions/websocket_actions.ts +++ b/webapp/channels/src/actions/websocket_actions.ts @@ -736,15 +736,16 @@ function handleSharedChannelRemoteUpdatedEvent(msg: WebSocketMessages.SharedChan } } -// handleChannelConvertedEvent handles updating of channel which is converted from public to private +// handleChannelConvertedEvent handles updating of channel which is converted between public and private function handleChannelConvertedEvent(msg: WebSocketMessages.ChannelConverted) { const channelId = msg.data.channel_id; if (channelId) { const channel = getChannel(getState(), channelId); if (channel) { + const newType = msg.data.channel_type === General.OPEN_CHANNEL ? General.OPEN_CHANNEL : General.PRIVATE_CHANNEL; dispatch({ type: ChannelTypes.RECEIVED_CHANNEL, - data: {...channel, type: General.PRIVATE_CHANNEL}, + data: {...channel, type: newType}, }); } } diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index 65aaa8f02a08..0b3a36ae5407 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -3873,13 +3873,6 @@ export default class Client4 { ); }; - purgeBleveIndexes = () => { - return this.doFetch( - `${this.getBaseRoute()}/bleve/purge_indexes`, - {method: 'post'}, - ); - }; - uploadLicense = (fileData: File) => { const formData = new FormData(); formData.append('license', fileData); diff --git a/webapp/platform/client/src/websocket_messages.ts b/webapp/platform/client/src/websocket_messages.ts index 75d3f5df9334..5d701cc201aa 100644 --- a/webapp/platform/client/src/websocket_messages.ts +++ b/webapp/platform/client/src/websocket_messages.ts @@ -193,6 +193,7 @@ export type ChannelUpdated = BaseWebSocketMessage; export type SharedChannelRemoteUpdated = BaseWebSocketMessage