2017-04-12 08:27:57 -04:00
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2019-11-29 06:59:40 -05:00
// See LICENSE.txt for license information.
2015-06-15 03:53:32 -04:00
package model
import (
Add cursor-based Posts Reporting API for compliance and auditing (#34252)
* Add cursor-based Posts Reporting API for compliance and auditing
Implements a new admin-only endpoint for retrieving posts with efficient
cursor-based pagination, designed for compliance, auditing, and archival
workflows.
Key Features:
- Cursor-based pagination using composite (time, ID) keys for consistent
performance regardless of dataset size (~10ms per page at any depth)
- Flexible time range queries with optional upper/lower bounds
- Support for both create_at and update_at time fields
- Ascending or descending sort order
- Optional metadata enrichment (files, reactions, acknowledgements)
- System admin only access (requires manage_system permission)
- License enforcement for compliance features
API Endpoint:
POST /api/v4/reports/posts
- Request: JSON body with channel_id, cursor_time, cursor_id, and options
- Response: Posts map + next_cursor object (null when pagination complete)
- Max page size: 1000 posts per request (MaxReportingPerPage constant)
Implementation:
- Store Layer: Direct SQL queries with composite index on (ChannelId, CreateAt, Id)
- App Layer: Permission checks, optional metadata enrichment, post hooks
- API Layer: Parameter validation, system admin enforcement, license checks
- Data Model: ReportPostOptions, ReportPostOptionsCursor, ReportPostListResponse
Code Quality Improvements:
- Added MaxReportingPerPage constant (1000) to eliminate magic numbers
- Removed unused StartTime field from ReportPostOptions
- Added fmt import for dynamic error messages
Testing:
- 14 comprehensive store layer unit tests
- 12 API layer integration tests covering permissions, pagination, filters
- All tests passing
Documentation:
- POSTS_REPORTING.md: Developer reference with Go structs and usage examples
- POSTS_REPORTING_API_SPEC.md: Complete technical specification
- GET_POSTS_API_IMPROVEMENTS.md: Implementation analysis and design rationale
- POSTS_TIME_RANGE_FEATURE.md: Archived time range feature for future use
Performance:
Cursor-based pagination maintains consistent ~10ms query time at any dataset
depth, compared to offset-based pagination which degrades significantly
(Page 1 = 10ms, Page 1000 = 10 seconds).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* lint fixes
* lint fixes
* gofmt
* i18n-extract
* Add Enterprise license requirement to posts reporting API
Enforce Enterprise license (tier 20+) for the new posts reporting endpoint
to align with compliance feature licensing. Professional tier is insufficient.
Changes:
- Add MinimumEnterpriseLicense check in GetPostsForReporting app layer
- Add test coverage for license validation (no license and Professional tier)
All existing tests pass with new license enforcement.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* i18n-extract
* add licensing to api documentation
* Test SSH signing
* Add mmctl command for posts reporting API
Adds mmctl report posts command to retrieve posts from a channel for
administrative reporting purposes. Supports cursor-based pagination with
configurable sorting, filtering, and time range options.
Includes database migration for updateat+id index to support efficient
cursor-based queries when sorting by update_at.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API cursor to opaque token and improve layer separation
This addresses code review feedback by transforming the cursor from exposed fields
to an opaque token and improving architectural layer separation.
**Key Changes:**
1. **Opaque Cursor Implementation**
- Transform cursor from split fields (cursor_time, cursor_id) to single opaque base64-encoded string
- Cursor now self-contained with all query parameters embedded
- When cursor provided, embedded parameters take precedence over request body
- Clients treat cursor as opaque token and pass unchanged
2. **Field Naming**
- Rename ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Now excludes ALL system posts (any type starting with "system_")
- Clearer and more consistent naming
3. **Layer Separation**
- Move cursor decoding from store layer to model layer
- Create ReportPostQueryParams struct for resolved parameters
- Store layer receives pre-resolved parameters (no business logic)
- Add ResolveReportPostQueryParams() function in model layer
4. **Code Quality**
- Add type-safe constants (ReportingTimeFieldCreateAt, ReportingSortDirectionAsc, etc.)
- Replace magic number 9223372036854775807 with math.MaxInt64
- Remove debug SQL logging (info disclosure risk)
- Update mmctl to use constants and fix NextCursor pointer access
5. **Tests**
- Update all 17 store test calls to use new resolution pattern
- Add comprehensive test for DESC + end_time boundary behavior
6. **API Documentation**
- Update OpenAPI spec to reflect opaque cursor format
- Update all request/response examples
- Clarify end_time behavior with sort directions
**Files Changed:**
- Model layer: public/model/post.go
- App layer: channels/app/report.go
- Store layer: channels/store/store.go, channels/store/sqlstore/post_store.go
- Tests: channels/store/storetest/post_store.go
- Mocks: channels/store/storetest/mocks/PostStore.go
- API: channels/api4/report.go, channels/api4/report_test.go
- mmctl: cmd/mmctl/commands/report.go
- Docs: api/v4/source/reports.yaml
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix unhandled parse errors in cursor decoding
Address security finding: cursor decoding was silently ignoring parse errors
from strconv functions, which could lead to unexpected behavior when malformed
cursors are provided.
Changes:
- Add explicit error handling for strconv.Atoi (version parsing)
- Add explicit error handling for strconv.ParseBool (includeDeleted, excludeSystemPosts)
- Add explicit error handling for strconv.ParseInt (timestamp parsing)
- Return clear error messages indicating which field failed to parse
This prevents silent failures where malformed values would default to zero-values
(0, false) and potentially alter query behavior without warning.
Addresses DryRun Security finding: "Unhandled Errors in Cursor Parsing"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues
- Remove unused reportPostCursorV1 struct (unused)
- Remove obsolete +build comment (buildtag)
- Use maps.Copy instead of manual loop (mapsloop)
- Modernize for loop with range over int (rangeint)
- Apply gofmt formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix gofmt formatting issues
Fix alignment in struct literals and constant declarations:
- Align map keys in report_test.go request bodies
- Align struct fields in ReportPostOptions initialization
- Align reporting constant declarations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more lint fixes
* remove index update files
* Remove end_time parameter from Posts Reporting API
Align with other cursor-based APIs in the codebase by removing the end_time
parameter. The caller now controls when to stop pagination by simply not
making another request, which is the same pattern used by GetPostsSinceForSync,
MessageExport, and GetPostsBatchForIndexing.
Changes:
- Remove EndTime field from ReportPostOptions and ReportPostQueryParams
- Remove EndTime filtering logic from store layer
- Remove tests that used end_time parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API for security and validation
Address security review feedback by consolidating parameter resolution
and validation in the API layer, with comprehensive validation of all
cursor fields to prevent SQL injection and invalid queries.
Changes:
- Move parameter resolution from model to API layer for clearer separation
- Add ReportPostQueryParams.Validate() with inline validation for all fields
- Validate ChannelId, TimeField, SortDirection, and CursorId format
- Add start_time parameter for time-bounded queries
- Cap per_page at 100-1000 instead of rejecting invalid values
- Export DecodeReportPostCursorV1() for API layer use
- Simplify app layer to receive pre-validated parameters
- Check channel existence when results are empty (better error messages)
Testing:
- Add 10 model tests for validation and malformed cursor scenarios
- Add 4 API tests for cursors with invalid field values
- Refactor 13 store tests to use buildReportPostQueryParams() helper
- All 31 tests pass
Documentation:
- Update OpenAPI spec with start_time, remove unused end_time
- Update markdown docs with start_time examples
Security improvements:
- Whitelist validation prevents SQL injection in TimeField/SortDirection
- Format validation ensures ChannelId and CursorId are valid IDs
- Single validation point for both cursor and options paths
- Defense in depth: validation + parameterized queries + store layer whitelist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Improve posts reporting query efficiency and safety
Replace SELECT * and nested OR/AND conditions with explicit column
selection and PostgreSQL row value comparison for better performance
and maintainability.
Changes:
- Use postSliceColumns() instead of SELECT * for explicit column selection
- Replace Squirrel OR/AND with row value comparison: (timeField, Id) > (?, ?)
- Use fmt.Sprintf for safer string formatting in WHERE clause
Query improvements:
Before: WHERE (CreateAt > ?) OR (CreateAt = ? AND Id > ?)
After: WHERE (CreateAt, Id) > (?, ?)
Benefits:
- Explicit column selection prevents issues if table schema changes
- Row value comparison is more concise and better optimized by PostgreSQL
- Follows existing patterns in post_store.go (postSliceColumns)
- Standard SQL:2003 syntax
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Change posts reporting response from map to ordered array
Replace the Posts map with an ordered array to preserve query sort order
and provide a more natural API response for sequential processing.
Changes:
- ReportPostListResponse.Posts: map[string]*Post → []*Post
- Store layer returns posts array directly (already sorted by query)
- App layer iterates by index for metadata enrichment
- Remove applyPostsWillBeConsumedHook call (not applicable to reporting)
- Update API tests to iterate arrays instead of map lookups
- Update store tests to convert array to map for deduplication checks
- Remove unused "maps" import
Benefits:
- Preserves query sort order (ASC/DESC, create_at/update_at)
- More natural for sequential processing/export workflows
- Simpler response structure for reporting/compliance use cases
- Aligns with message export/compliance patterns (no plugin hooks)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues in posts reporting tests
Replace inefficient loops with append(...) for better performance.
Changes:
- Use append(postSlice, result.Posts...) instead of loop
- Simplifies code and follows staticcheck recommendations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix store test AppError nil checking
Use require.Nil instead of require.NoError for *AppError returns
to avoid Go interface nil pointer issues.
When DecodeReportPostCursorV1 returns nil *AppError and it's assigned
to error interface, the interface becomes non-nil even though the
pointer is nil. This causes require.NoError to fail incorrectly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2025-11-17 11:02:19 -05:00
"encoding/base64"
2015-06-15 03:53:32 -04:00
"encoding/json"
2020-03-13 16:12:20 -04:00
"errors"
2023-03-15 08:18:33 -04:00
"fmt"
2022-01-21 11:25:33 -05:00
"io"
2025-07-18 06:54:51 -04:00
"maps"
2017-09-15 08:32:11 -04:00
"net/http"
2020-03-03 05:22:49 -05:00
"regexp"
2018-01-22 16:32:50 -05:00
"sort"
Add cursor-based Posts Reporting API for compliance and auditing (#34252)
* Add cursor-based Posts Reporting API for compliance and auditing
Implements a new admin-only endpoint for retrieving posts with efficient
cursor-based pagination, designed for compliance, auditing, and archival
workflows.
Key Features:
- Cursor-based pagination using composite (time, ID) keys for consistent
performance regardless of dataset size (~10ms per page at any depth)
- Flexible time range queries with optional upper/lower bounds
- Support for both create_at and update_at time fields
- Ascending or descending sort order
- Optional metadata enrichment (files, reactions, acknowledgements)
- System admin only access (requires manage_system permission)
- License enforcement for compliance features
API Endpoint:
POST /api/v4/reports/posts
- Request: JSON body with channel_id, cursor_time, cursor_id, and options
- Response: Posts map + next_cursor object (null when pagination complete)
- Max page size: 1000 posts per request (MaxReportingPerPage constant)
Implementation:
- Store Layer: Direct SQL queries with composite index on (ChannelId, CreateAt, Id)
- App Layer: Permission checks, optional metadata enrichment, post hooks
- API Layer: Parameter validation, system admin enforcement, license checks
- Data Model: ReportPostOptions, ReportPostOptionsCursor, ReportPostListResponse
Code Quality Improvements:
- Added MaxReportingPerPage constant (1000) to eliminate magic numbers
- Removed unused StartTime field from ReportPostOptions
- Added fmt import for dynamic error messages
Testing:
- 14 comprehensive store layer unit tests
- 12 API layer integration tests covering permissions, pagination, filters
- All tests passing
Documentation:
- POSTS_REPORTING.md: Developer reference with Go structs and usage examples
- POSTS_REPORTING_API_SPEC.md: Complete technical specification
- GET_POSTS_API_IMPROVEMENTS.md: Implementation analysis and design rationale
- POSTS_TIME_RANGE_FEATURE.md: Archived time range feature for future use
Performance:
Cursor-based pagination maintains consistent ~10ms query time at any dataset
depth, compared to offset-based pagination which degrades significantly
(Page 1 = 10ms, Page 1000 = 10 seconds).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* lint fixes
* lint fixes
* gofmt
* i18n-extract
* Add Enterprise license requirement to posts reporting API
Enforce Enterprise license (tier 20+) for the new posts reporting endpoint
to align with compliance feature licensing. Professional tier is insufficient.
Changes:
- Add MinimumEnterpriseLicense check in GetPostsForReporting app layer
- Add test coverage for license validation (no license and Professional tier)
All existing tests pass with new license enforcement.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* i18n-extract
* add licensing to api documentation
* Test SSH signing
* Add mmctl command for posts reporting API
Adds mmctl report posts command to retrieve posts from a channel for
administrative reporting purposes. Supports cursor-based pagination with
configurable sorting, filtering, and time range options.
Includes database migration for updateat+id index to support efficient
cursor-based queries when sorting by update_at.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API cursor to opaque token and improve layer separation
This addresses code review feedback by transforming the cursor from exposed fields
to an opaque token and improving architectural layer separation.
**Key Changes:**
1. **Opaque Cursor Implementation**
- Transform cursor from split fields (cursor_time, cursor_id) to single opaque base64-encoded string
- Cursor now self-contained with all query parameters embedded
- When cursor provided, embedded parameters take precedence over request body
- Clients treat cursor as opaque token and pass unchanged
2. **Field Naming**
- Rename ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Now excludes ALL system posts (any type starting with "system_")
- Clearer and more consistent naming
3. **Layer Separation**
- Move cursor decoding from store layer to model layer
- Create ReportPostQueryParams struct for resolved parameters
- Store layer receives pre-resolved parameters (no business logic)
- Add ResolveReportPostQueryParams() function in model layer
4. **Code Quality**
- Add type-safe constants (ReportingTimeFieldCreateAt, ReportingSortDirectionAsc, etc.)
- Replace magic number 9223372036854775807 with math.MaxInt64
- Remove debug SQL logging (info disclosure risk)
- Update mmctl to use constants and fix NextCursor pointer access
5. **Tests**
- Update all 17 store test calls to use new resolution pattern
- Add comprehensive test for DESC + end_time boundary behavior
6. **API Documentation**
- Update OpenAPI spec to reflect opaque cursor format
- Update all request/response examples
- Clarify end_time behavior with sort directions
**Files Changed:**
- Model layer: public/model/post.go
- App layer: channels/app/report.go
- Store layer: channels/store/store.go, channels/store/sqlstore/post_store.go
- Tests: channels/store/storetest/post_store.go
- Mocks: channels/store/storetest/mocks/PostStore.go
- API: channels/api4/report.go, channels/api4/report_test.go
- mmctl: cmd/mmctl/commands/report.go
- Docs: api/v4/source/reports.yaml
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix unhandled parse errors in cursor decoding
Address security finding: cursor decoding was silently ignoring parse errors
from strconv functions, which could lead to unexpected behavior when malformed
cursors are provided.
Changes:
- Add explicit error handling for strconv.Atoi (version parsing)
- Add explicit error handling for strconv.ParseBool (includeDeleted, excludeSystemPosts)
- Add explicit error handling for strconv.ParseInt (timestamp parsing)
- Return clear error messages indicating which field failed to parse
This prevents silent failures where malformed values would default to zero-values
(0, false) and potentially alter query behavior without warning.
Addresses DryRun Security finding: "Unhandled Errors in Cursor Parsing"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues
- Remove unused reportPostCursorV1 struct (unused)
- Remove obsolete +build comment (buildtag)
- Use maps.Copy instead of manual loop (mapsloop)
- Modernize for loop with range over int (rangeint)
- Apply gofmt formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix gofmt formatting issues
Fix alignment in struct literals and constant declarations:
- Align map keys in report_test.go request bodies
- Align struct fields in ReportPostOptions initialization
- Align reporting constant declarations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more lint fixes
* remove index update files
* Remove end_time parameter from Posts Reporting API
Align with other cursor-based APIs in the codebase by removing the end_time
parameter. The caller now controls when to stop pagination by simply not
making another request, which is the same pattern used by GetPostsSinceForSync,
MessageExport, and GetPostsBatchForIndexing.
Changes:
- Remove EndTime field from ReportPostOptions and ReportPostQueryParams
- Remove EndTime filtering logic from store layer
- Remove tests that used end_time parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API for security and validation
Address security review feedback by consolidating parameter resolution
and validation in the API layer, with comprehensive validation of all
cursor fields to prevent SQL injection and invalid queries.
Changes:
- Move parameter resolution from model to API layer for clearer separation
- Add ReportPostQueryParams.Validate() with inline validation for all fields
- Validate ChannelId, TimeField, SortDirection, and CursorId format
- Add start_time parameter for time-bounded queries
- Cap per_page at 100-1000 instead of rejecting invalid values
- Export DecodeReportPostCursorV1() for API layer use
- Simplify app layer to receive pre-validated parameters
- Check channel existence when results are empty (better error messages)
Testing:
- Add 10 model tests for validation and malformed cursor scenarios
- Add 4 API tests for cursors with invalid field values
- Refactor 13 store tests to use buildReportPostQueryParams() helper
- All 31 tests pass
Documentation:
- Update OpenAPI spec with start_time, remove unused end_time
- Update markdown docs with start_time examples
Security improvements:
- Whitelist validation prevents SQL injection in TimeField/SortDirection
- Format validation ensures ChannelId and CursorId are valid IDs
- Single validation point for both cursor and options paths
- Defense in depth: validation + parameterized queries + store layer whitelist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Improve posts reporting query efficiency and safety
Replace SELECT * and nested OR/AND conditions with explicit column
selection and PostgreSQL row value comparison for better performance
and maintainability.
Changes:
- Use postSliceColumns() instead of SELECT * for explicit column selection
- Replace Squirrel OR/AND with row value comparison: (timeField, Id) > (?, ?)
- Use fmt.Sprintf for safer string formatting in WHERE clause
Query improvements:
Before: WHERE (CreateAt > ?) OR (CreateAt = ? AND Id > ?)
After: WHERE (CreateAt, Id) > (?, ?)
Benefits:
- Explicit column selection prevents issues if table schema changes
- Row value comparison is more concise and better optimized by PostgreSQL
- Follows existing patterns in post_store.go (postSliceColumns)
- Standard SQL:2003 syntax
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Change posts reporting response from map to ordered array
Replace the Posts map with an ordered array to preserve query sort order
and provide a more natural API response for sequential processing.
Changes:
- ReportPostListResponse.Posts: map[string]*Post → []*Post
- Store layer returns posts array directly (already sorted by query)
- App layer iterates by index for metadata enrichment
- Remove applyPostsWillBeConsumedHook call (not applicable to reporting)
- Update API tests to iterate arrays instead of map lookups
- Update store tests to convert array to map for deduplication checks
- Remove unused "maps" import
Benefits:
- Preserves query sort order (ASC/DESC, create_at/update_at)
- More natural for sequential processing/export workflows
- Simpler response structure for reporting/compliance use cases
- Aligns with message export/compliance patterns (no plugin hooks)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues in posts reporting tests
Replace inefficient loops with append(...) for better performance.
Changes:
- Use append(postSlice, result.Posts...) instead of loop
- Simplifies code and follows staticcheck recommendations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix store test AppError nil checking
Use require.Nil instead of require.NoError for *AppError returns
to avoid Go interface nil pointer issues.
When DecodeReportPostCursorV1 returns nil *AppError and it's assigned
to error interface, the interface becomes non-nil even though the
pointer is nil. This causes require.NoError to fail incorrectly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2025-11-17 11:02:19 -05:00
"strconv"
2017-09-27 11:09:09 -04:00
"strings"
2020-03-13 16:12:20 -04:00
"sync"
2015-11-05 09:25:59 -05:00
"unicode/utf8"
2018-01-22 16:32:50 -05:00
2025-03-20 07:53:50 -04:00
"github.com/hashicorp/go-multierror"
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/shared/markdown"
2025-03-20 07:53:50 -04:00
"github.com/mattermost/mattermost/server/public/shared/mlog"
2015-06-15 03:53:32 -04:00
)
2025-12-11 01:59:50 -05:00
type PostContextKey string
2015-06-15 03:53:32 -04:00
const (
2026-02-06 12:19:06 -05:00
PostSystemMessagePrefix = "system_"
PostTypeDefault = ""
2026-03-10 11:37:21 -04:00
PostTypeMessageAttachment = "slack_attachment"
2026-02-06 12:19:06 -05:00
PostTypeSystemGeneric = "system_generic"
PostTypeJoinLeave = "system_join_leave" // Deprecated, use PostJoinChannel or PostLeaveChannel instead
PostTypeJoinChannel = "system_join_channel"
PostTypeGuestJoinChannel = "system_guest_join_channel"
PostTypeLeaveChannel = "system_leave_channel"
PostTypeJoinTeam = "system_join_team"
PostTypeLeaveTeam = "system_leave_team"
PostTypeAutoResponder = "system_auto_responder"
PostTypeAutotranslationChange = "system_autotranslation"
PostTypeAddRemove = "system_add_remove" // Deprecated, use PostAddToChannel or PostRemoveFromChannel instead
PostTypeAddToChannel = "system_add_to_channel"
PostTypeAddGuestToChannel = "system_add_guest_to_chan"
PostTypeRemoveFromChannel = "system_remove_from_channel"
PostTypeMoveChannel = "system_move_channel"
PostTypeAddToTeam = "system_add_to_team"
PostTypeRemoveFromTeam = "system_remove_from_team"
PostTypeHeaderChange = "system_header_change"
PostTypeDisplaynameChange = "system_displayname_change"
PostTypeConvertChannel = "system_convert_channel"
PostTypePurposeChange = "system_purpose_change"
PostTypeChannelDeleted = "system_channel_deleted"
PostTypeChannelRestored = "system_channel_restored"
PostTypeEphemeral = "system_ephemeral"
PostTypeChangeChannelPrivacy = "system_change_chan_privacy"
PostTypeWrangler = "system_wrangler"
PostTypeGMConvertedToChannel = "system_gm_to_channel"
PostTypeAddBotTeamsChannels = "add_bot_teams_channels"
PostTypeMe = "me"
PostCustomTypePrefix = "custom_"
PostTypeReminder = "reminder"
PostTypeBurnOnRead = "burn_on_read"
Merge the Integrated Boards MVP feature branch (#35796)
* Add CreatedBy and UpdatedBy to the properties fields and values (#34485)
* Add CreatedBy and UpdatedBy to the properties fields and values
* Fix types
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds ObjectType to the property fields table (#34908)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Update ObjectType migration setting an empty value and marking the column as not null (#34915)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds uniqueness mechanisms to the property fields (#35058)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Fixing retrylayer mocks
* Remove retrylayer duplication
* Address review comments
* Fix comment to avoid linter issues
* Address PR comments
* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.down.sql
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.up.sql
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.up.sql
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Update field validation to check only for valid target types
* Update migrations to avoid concurrent index creation within a transaction
* Update migrations to make all index ops concurrent
* Update tests to use valid PSAv2 property fields
* Adds a helper for valid PSAv2 TargetTypes
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Fix property tests (#35388)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds Integrated Boards feature flag (#35378)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds Integrated Boards MVP API changes (#34822)
This PR includes the necessary changes for channels and posts
endpoints and adds a set of generic endpoints to retrieve and manage
property fields and values following the new Property System approach.
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Mattermost Build <build@mattermost.com>
* Property System Architecture permissions for v2 (#35113)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Add the permissions to the migrations, model and update the store calls
* Adds the property field and property group app layer
* Adds authorization helpers for property fields and values
* Make sure that users cannot lock themselves out of property fields
* Migrate permissions from a JSON column to three normalized columns
* Remove the audit comment
* Use target level constants in authorization
* Log authorization membership failures
* Rename admin to sysadmin
* Fix i18n sorting
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Add Views store and app layer (#35361)
* Add Views store and app layer for Integrated Boards
Implements the View entity (model, SQL store, service, app) as described
in the Integrated Boards tech spec. Views are channel-scoped board
configurations with typed props (board, kanban subviews) and soft-delete.
- public/model: View, ViewBoardProps, Subview, ViewPatch types with
PreSave/PreUpdate/IsValid/Patch/Clone/Auditable
- Migration 158: Views table with jsonb Props column and indexes
- SqlViewStore: CRUD with nil-safe Props marshaling (AppendBinaryFlag)
- ViewService: CreateView seeds default kanban subview and links the
boards property field; caches boardPropertyFieldID at startup
- App layer: CreateView/GetView/GetViewsForChannel/UpdateView/DeleteView
with channel-membership permission checks and WebSocket events
(view_created, view_updated, view_deleted)
- doSetupBoardsPropertyField: registers the Boards property group and
board field in NewServer() before ViewService construction
- GetFieldByName now returns store.ErrNotFound instead of raw sql.ErrNoRows
* Move permission checks out of App layer for views
- Remove HasPermissionToChannel calls from all App view methods
- Drop userID params from GetView, GetViewsForChannel, UpdateView, DeleteView
- Fix doSetupBoardsPropertyField to include required TargetType for PSAv2 field
* Make View service generic and enforce board validation in model
- Remove board-specific auto-setup from service and server startup
- Enforce that board views require Props, at least one subview, and at least one linked property in IsValid()
- Move default subview seeding out of app layer; callers must provide valid props
- Call PreSave on subviews during PreUpdate to assign IDs to new subviews
- Update all tests to reflect the new validation requirements
* Restore migrations files to match base branch
* Distinguish ErrNotFound from other errors in view store Get
* Use CONCURRENTLY and nontransactional for index operations in views migration
* Split views index creation into separate nontransactional migrations
* Update migrations.list
* Update i18n translations for views
* Fix makeView helper to include required Props for board view validation
* Rename ctx parameter from c to rctx in OAuthProvider mock
* Remove views service layer, call store directly from app
* Return 500 for unexpected DB errors in GetView, 404 only for not-found
* Harden View model: deep-copy Props, validate linked property IDs
- Add ViewBoardProps.Clone() to deep-copy LinkedProperties and Subviews
- Use it in View.Clone() and View.Patch() to prevent shared-slice aliasing
- Iterate over LinkedProperties in View.IsValid() and reject invalid IDs
with a dedicated i18n key
- Register ViewStore in storetest AssertExpectations so mock expectations
are enforced
- Add tests covering all new behaviours
* Restore autotranslation worker_stopped i18n translation
* Fix view store test IDs and improve error handling in app layer
- Use model.NewId() for linked property IDs in testUpdateView to fix
validation failure (IsValid rejects non-UUID strings)
- Fix import grouping in app/view.go (stdlib imports in one block)
- Return 404 instead of 500 when Update/Delete store calls return
ErrNotFound (e.g. concurrent deletion TOCTOU race)
* Add View store mock to retrylayer test genStore helper
The View store was added to the store interface but the genStore()
helper in retrylayer_test.go was not updated, causing TestRetry to panic.
Also removes the duplicate Recap mock registration.
* Refactor view deletion and websocket event handling; update SQL store methods to use query builder
* revert property field store
* Remove useless migrations
* Add cursor-based pagination to View store GetForChannel
- Add ViewQueryCursor and ViewQueryOpts types with validation
- Return (views, cursor, error) for caller-driven pagination
- PerPage clamping: <=0 defaults to 20, >200 clamps to 200
- Support IncludeDeleted filter
- Add comprehensive store tests for pagination, cursor edge cases,
PerPage clamping, and invalid input rejection
- Add app layer test for empty channelID → 400
- Update interface, retrylayer, timerlayer, and mock signatures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refactor test loops in ViewStore tests for improved readability
* change pagination to limit/offset
* Add upper-bound limits on View Subviews and LinkedProperties
Defense-in-depth validation: cap Subviews at 50 and LinkedProperties
at 500 to prevent abuse below the 300KB payload limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* MM-67388, MM-66528, MM-67750: Add View REST API endpoints, websocket events, and sort order (#35442)
* Add Views store and app layer for Integrated Boards
Implements the View entity (model, SQL store, service, app) as described
in the Integrated Boards tech spec. Views are channel-scoped board
configurations with typed props (board, kanban subviews) and soft-delete.
- public/model: View, ViewBoardProps, Subview, ViewPatch types with
PreSave/PreUpdate/IsValid/Patch/Clone/Auditable
- Migration 158: Views table with jsonb Props column and indexes
- SqlViewStore: CRUD with nil-safe Props marshaling (AppendBinaryFlag)
- ViewService: CreateView seeds default kanban subview and links the
boards property field; caches boardPropertyFieldID at startup
- App layer: CreateView/GetView/GetViewsForChannel/UpdateView/DeleteView
with channel-membership permission checks and WebSocket events
(view_created, view_updated, view_deleted)
- doSetupBoardsPropertyField: registers the Boards property group and
board field in NewServer() before ViewService construction
- GetFieldByName now returns store.ErrNotFound instead of raw sql.ErrNoRows
* Move permission checks out of App layer for views
- Remove HasPermissionToChannel calls from all App view methods
- Drop userID params from GetView, GetViewsForChannel, UpdateView, DeleteView
- Fix doSetupBoardsPropertyField to include required TargetType for PSAv2 field
* Make View service generic and enforce board validation in model
- Remove board-specific auto-setup from service and server startup
- Enforce that board views require Props, at least one subview, and at least one linked property in IsValid()
- Move default subview seeding out of app layer; callers must provide valid props
- Call PreSave on subviews during PreUpdate to assign IDs to new subviews
- Update all tests to reflect the new validation requirements
* Restore migrations files to match base branch
* Distinguish ErrNotFound from other errors in view store Get
* Use CONCURRENTLY and nontransactional for index operations in views migration
* Split views index creation into separate nontransactional migrations
* Update migrations.list
* Update i18n translations for views
* Fix makeView helper to include required Props for board view validation
* Rename ctx parameter from c to rctx in OAuthProvider mock
* Remove views service layer, call store directly from app
* Return 500 for unexpected DB errors in GetView, 404 only for not-found
* Harden View model: deep-copy Props, validate linked property IDs
- Add ViewBoardProps.Clone() to deep-copy LinkedProperties and Subviews
- Use it in View.Clone() and View.Patch() to prevent shared-slice aliasing
- Iterate over LinkedProperties in View.IsValid() and reject invalid IDs
with a dedicated i18n key
- Register ViewStore in storetest AssertExpectations so mock expectations
are enforced
- Add tests covering all new behaviours
* Restore autotranslation worker_stopped i18n translation
* Fix view store test IDs and improve error handling in app layer
- Use model.NewId() for linked property IDs in testUpdateView to fix
validation failure (IsValid rejects non-UUID strings)
- Fix import grouping in app/view.go (stdlib imports in one block)
- Return 404 instead of 500 when Update/Delete store calls return
ErrNotFound (e.g. concurrent deletion TOCTOU race)
* Add View store mock to retrylayer test genStore helper
The View store was added to the store interface but the genStore()
helper in retrylayer_test.go was not updated, causing TestRetry to panic.
Also removes the duplicate Recap mock registration.
* Refactor view deletion and websocket event handling; update SQL store methods to use query builder
* revert property field store
* Add View API endpoints with OpenAPI spec, client methods, and i18n
Implement REST API for channel views (board-type) behind the
IntegratedBoards feature flag. Adds CRUD endpoints under
/api/v4/channels/{channel_id}/views with permission checks
matching the channel bookmark pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove useless migrations
* Add cursor-based pagination to View store GetForChannel
- Add ViewQueryCursor and ViewQueryOpts types with validation
- Return (views, cursor, error) for caller-driven pagination
- PerPage clamping: <=0 defaults to 20, >200 clamps to 200
- Support IncludeDeleted filter
- Add comprehensive store tests for pagination, cursor edge cases,
PerPage clamping, and invalid input rejection
- Add app layer test for empty channelID → 400
- Update interface, retrylayer, timerlayer, and mock signatures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add cursor-based pagination to View API for channel views
* Enhance cursor handling in getViewsForChannel and update tests for pagination
* Refactor test loops in ViewStore tests for improved readability
* Refactor loop in TestGetViewsForChannel for improved readability
* change pagination to limit/offset
* switch to limit/offset pagination
* Add upper-bound limits on View Subviews and LinkedProperties
Defense-in-depth validation: cap Subviews at 50 and LinkedProperties
at 500 to prevent abuse below the 300KB payload limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add view sort order API endpoint
Add POST /api/v4/channels/{channel_id}/views/{view_id}/sort_order
endpoint following the channel bookmarks reorder pattern. Includes
store, app, and API layers with full test coverage at each layer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add connectionId to view WebSocket events and sort_order API spec
Thread connectionId from request header through all view handlers
(create, update, delete, sort_order) to WebSocket events, matching
the channel bookmarks pattern. Add sort_order endpoint to OpenAPI
spec. Update minimum server version to 11.6.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove duplicate View/ViewPatch definitions from definitions.yaml
The merge from integrated-boards-mvp introduced duplicate View and
ViewPatch schema definitions that were already defined earlier in
the file with more detail (including ViewBoardProps ref and enums).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update minimum server version to 11.6 in views API spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add missing translations for view sort order error messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Merge integrated-boards-mvp into ibmvp_api-views; remove spec files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix flaky TestViewStore timestamp test on CI
Add sleep before UpdateSortOrder to ensure timestamps differ,
preventing same-millisecond comparisons on fast CI machines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* remove duplicate views.yaml imclude
* Use c.boolString() for include_deleted query param in GetViewsForChannel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix views.yaml sort order schema: use integer type and require body
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refactor view sort order tests to use named IDs instead of array indices
Extract idA/idB/idC from views slice and add BEFORE/AFTER comments
to make stateful subtest ordering easier to follow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Return 404 instead of 403 for view operations on deleted channels
Deleted channels should appear non-existent to callers rather than
revealing their existence via a 403. Detailed error text explains
the context for debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add missing channel deleteat checks
* Use c.Params.Page instead of manual page query param parsing in getViewsForChannel
c.Params already validates and defaults page/per_page, so the manual
parsing was redundant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add support for total count in views retrieval
* Add tests for handling deleted views in GetViewsForChannel and GetView
* Short-circuit negative newIndex in UpdateSortOrder before opening transaction
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add per-channel limit on views to bound UpdateSortOrder cost
Without a cap, unbounded view creation makes sort-order updates
increasingly expensive (CASE WHEN per view, row locks). Adds
MaxViewsPerChannel=50 constant and enforces it in the app layer
before saving. Includes API and app layer tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove include_deleted support from views API
Soft-deleted views are structural metadata with low risk, but no other
similar endpoint (e.g. channel bookmarks) exposes deleted records without
an admin gate. Rather than adding an admin-only permission check for
consistency, remove the feature entirely since there is no current use case.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update view permissions to require `create_post` instead of channel management permissions
* Remove obsolete view management error messages for direct and group messages
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(migrations): add user tracking and object type to property fields
- Introduced user tracking columns (CreatedBy, UpdatedBy) to PropertyFields and PropertyValues.
- Added ObjectType column to PropertyFields with associated unique indexes for legacy and typed properties.
- Created new migration scripts for adding and dropping these features, including necessary indexes for data integrity.
- Established views for managing property fields with new attributes.
This update enhances the schema to support better tracking and categorization of property fields.
* Add Property System Architecture v2 API endpoints (#35583)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Add the permissions to the migrations, model and update the store calls
* Adds the property field and property group app layer
* Adds authorization helpers for property fields and values
* Make sure that users cannot lock themselves out of property fields
* Migrate permissions from a JSON column to three normalized columns
* Remove the audit comment
* Use target level constants in authorization
* Log authorization membership failures
* Rename admin to sysadmin
* Adds the Property System Architecture v2 API endpoints
* Adds permission checks to the create field endpoint
* Add target access checks to value endpoints
* Add default branches for object_type and target_type and extra guards for cursor client4 methods
* Fix vet API mismatch
* Fix error checks
* Fix linter
* Add merge semantics for property patch logic and API endpoint
* Fix i18n
* Fix duplicated patch elements and early return on bad cursor
* Update docs to use enums
* Fix i18n sorting
* Update app layer to return model.AppError
* Adds a limit to the number of property values that can be patched in the same request
* Require target_type filter when searching property fields
* Add objectType validation as part of field.IsValid()
* Fix linter
* Fix test with bad objecttpye
* Fix test grouping
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* MM-67968: Flatten view model — remove icon, subviews, typed board props (#35726)
* feat(views): flatten view model by removing icon, subview, and board props
Simplifies the View data model as part of MM-67968: removes Icon, Subview,
and ViewBoardProps types; renames ViewTypeBoard to ViewTypeKanban; replaces
typed Props with StringInterface (map[string]any); adds migration 000167
to drop the Icon column from the Views table.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(api): update views OpenAPI spec to reflect flattened model
Removes ViewBoardProps, Subview, and icon from the View and ViewPatch
schemas. Changes type enum from board to kanban. Replaces typed props
with a free-form StringInterface object. Aligns with MM-67968.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* refactor(views): simplify store by dropping dbView and marshalViewProps
StringInterface already implements driver.Valuer and sql.Scanner, so the
manual JSON marshal/unmarshal and the dbView intermediate struct were
redundant. model.View now scans directly from the database. Also removes
the dead ViewMaxLinkedProperties constant and wraps the Commit() error in
UpdateSortOrder.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(api): allow arbitrary JSON in view props OpenAPI schema
The props field was restricted to string values via
additionalProperties: { type: string }, conflicting with the Go model's
StringInterface (map[string]any). Changed to additionalProperties: true
in View, ViewPatch, and inline POST schemas.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* Adds basic implementation of the generic redux store for PSAv2 (#35512)
* Adds basic implementation of the generic redux store for PSAv2
* Add created_by and updated_by to the test fixtures
* Make target_id, target_type and object_type mandatory
* Wrap getPropertyFieldsByIds and getPropertyValuesForTargetByFieldIds with createSelector
* Address PR comments
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds websocket messages for the PSAv2 API events (#35696)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Add the permissions to the migrations, model and update the store calls
* Adds the property field and property group app layer
* Adds authorization helpers for property fields and values
* Make sure that users cannot lock themselves out of property fields
* Migrate permissions from a JSON column to three normalized columns
* Remove the audit comment
* Use target level constants in authorization
* Log authorization membership failures
* Rename admin to sysadmin
* Adds the Property System Architecture v2 API endpoints
* Adds permission checks to the create field endpoint
* Add target access checks to value endpoints
* Add default branches for object_type and target_type and extra guards for cursor client4 methods
* Fix vet API mismatch
* Fix error checks
* Fix linter
* Add merge semantics for property patch logic and API endpoint
* Fix i18n
* Fix duplicated patch elements and early return on bad cursor
* Update docs to use enums
* Fix i18n sorting
* Update app layer to return model.AppError
* Adds a limit to the number of property values that can be patched in the same request
* Adds websocket messages for the PSAv2 API events
* Add IsPSAv2 helper to the property field for clarity
* Add guard against nil returns on field deletion
* Add docs to the websocket endpoints
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* migrations: consolidate views migrations and reorder after master
- Merged 000165 (create Views) with 000167 (drop Icon) since Icon was never needed
- Renumbered branch migrations 159-166 → 160-167 so master's 000159 (deduplicate_policy_names) runs first
- Regenerated migrations.list
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add API endpoint to retrieve posts for a specific view (#35604)
Automatic Merge
* Apply fixes after merge
* Return a more specific error from getting multiple fields
* Prevent getting broadcast params on field deletion if not needed
* Remove duplicated migration code
* Update property conflict code to always use master
* Adds nil guard when iterating on property fields
* Check that permission level is valid before getting rejected by the database
* Validate correctness on TargetID for PSAv2 fields
* Avoid PSAv1 using permissions or protected
* Fix test data after validation change
* Fix flaky search test
* Adds more posts for filter use cases to properly test exclusions
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
Co-authored-by: Julien Tant <julien@craftyx.fr>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Julien Tant <785518+JulienTant@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 05:36:35 -04:00
PostTypeCard = "card"
2021-07-12 14:05:36 -04:00
PostFileidsMaxRunes = 300
PostFilenamesMaxRunes = 4000
PostHashtagsMaxRunes = 1000
PostMessageMaxRunesV1 = 4000
Remove vestigial MySQL support (#34865)
* Remove legacy quoteColumnName() utility
Since Mattermost only supports PostgreSQL, the quoteColumnName() helper
that was designed to handle database-specific column quoting is no longer
needed. The function was a no-op that simply returned the column name
unchanged.
Remove the function from utils.go and update status_store.go to use
the "Manual" column name directly.
* Remove legacy driver checks from store.go
Since Mattermost only supports PostgreSQL, remove conditional checks
for different database drivers:
- Simplify specialSearchChars() to always return PostgreSQL-compatible chars
- Remove driver check from computeBinaryParam()
- Remove driver check from computeDefaultTextSearchConfig()
- Simplify GetDbVersion() to use PostgreSQL syntax directly
- Remove switch statement from ensureMinimumDBVersion()
- Remove unused driver parameter from versionString()
* Remove MySQL alternatives for batch delete operations
Since Mattermost only supports PostgreSQL, remove the MySQL-specific
DELETE...LIMIT syntax and keep only the PostgreSQL array-based approach:
- reaction_store.go: Use PostgreSQL array syntax for PermanentDeleteBatch
- file_info_store.go: Use PostgreSQL array syntax for PermanentDeleteBatch
- preference_store.go: Use PostgreSQL tuple IN subquery for DeleteInvalidVisibleDmsGms
* Remove MySQL alternatives for UPDATE...FROM syntax
Since Mattermost only supports PostgreSQL, remove the MySQL-specific
UPDATE syntax that joins tables differently:
- thread_store.go: Use PostgreSQL UPDATE...FROM syntax in
MarkAllAsReadByChannels and MarkAllAsReadByTeam
- post_store.go: Use PostgreSQL UPDATE...FROM syntax in deleteThreadFiles
* Remove MySQL alternatives for JSON and subquery operations
Since Mattermost only supports PostgreSQL, remove the MySQL-specific
JSON and subquery syntax:
- thread_store.go: Use PostgreSQL JSONB operators for updating participants
- access_control_policy_store.go: Use PostgreSQL JSONB @> operator for
querying JSON imports
- session_store.go: Use PostgreSQL subquery syntax for Cleanup
- job_store.go: Use PostgreSQL subquery syntax for Cleanup
* Remove MySQL alternatives for CTE queries
Since Mattermost only supports PostgreSQL, simplify code that
uses CTEs (Common Table Expressions):
- channel_store.go: Remove MySQL CASE-based fallback in
UpdateLastViewedAt and use PostgreSQL CTE exclusively
- draft_store.go: Remove driver checks in DeleteEmptyDraftsByCreateAtAndUserId,
DeleteOrphanDraftsByCreateAtAndUserId, and determineMaxDraftSize
* Remove driver checks in migrate.go and schema_dump.go
Simplify migration code to use PostgreSQL driver directly since
PostgreSQL is the only supported database.
* Remove driver checks in sqlx_wrapper.go
Always apply lowercase named parameter transformation since PostgreSQL
is the only supported database.
* Remove driver checks in user_store.go
Simplify user store functions to use PostgreSQL-only code paths:
- Remove isPostgreSQL parameter from helper functions
- Use LEFT JOIN pattern instead of subqueries for bot filtering
- Always use case-insensitive LIKE with lower() for search
- Remove MySQL-specific role filtering alternatives
* Remove driver checks in post_store.go
Simplify post_store.go to use PostgreSQL-only code paths:
- Inline getParentsPostsPostgreSQL into getParentsPosts
- Use PostgreSQL TO_CHAR/TO_TIMESTAMP for date formatting in analytics
- Use PostgreSQL array syntax for batch deletes
- Simplify determineMaxPostSize to always use information_schema
- Use PostgreSQL jsonb subtraction for thread participants
- Always execute RefreshPostStats (PostgreSQL materialized views)
- Use materialized views for AnalyticsPostCountsByDay
- Simplify AnalyticsPostCountByTeam to always use countByTeam
* Remove driver checks in channel_store.go
Simplify channel_store.go to use PostgreSQL-only code paths:
- Always use sq.Dollar.ReplacePlaceholders for UNION queries
- Use PostgreSQL LEFT JOIN for retention policy exclusion
- Use PostgreSQL jsonb @> operator for access control policy imports
- Simplify buildLIKEClause to always use LOWER() for case-insensitive search
- Simplify buildFulltextClauseX to always use PostgreSQL to_tsvector/to_tsquery
- Simplify searchGroupChannelsQuery to use ARRAY_TO_STRING/ARRAY_AGG
* Remove driver checks in file_info_store.go
Simplify file_info_store.go to use PostgreSQL-only code paths:
- Always use PostgreSQL to_tsvector/to_tsquery for file search
- Use file_stats materialized view for CountAll()
- Use file_stats materialized view for GetStorageUsage() when not including deleted
- Always execute RefreshFileStats() for materialized view refresh
* Remove driver checks in attributes_store.go
Simplify attributes_store.go to use PostgreSQL-only code paths:
- Always execute RefreshAttributes() for materialized view refresh
- Remove isPostgreSQL parameter from generateSearchQueryForExpression
- Always use PostgreSQL LOWER() LIKE LOWER() syntax for case-insensitive search
* Remove driver checks in retention_policy_store.go
Simplify retention_policy_store.go to use PostgreSQL-only code paths:
- Remove isPostgres parameter from scanRetentionIdsForDeletion
- Always use pq.Array for scanning retention IDs
- Always use pq.Array for inserting retention IDs
- Remove unused json import
* Remove driver checks in property stores
Simplify property_field_store.go and property_value_store.go to use
PostgreSQL-only code paths:
- Always use PostgreSQL type casts (::text, ::jsonb, ::bigint, etc.)
- Remove isPostgres variable and conditionals
* Remove driver checks in channel_member_history_store.go
Simplify PermanentDeleteBatch to use PostgreSQL-only code path:
- Always use ctid-based subquery for DELETE with LIMIT
* Remove remaining driver checks in user_store.go
Simplify user_store.go to use PostgreSQL-only code paths:
- Use LEFT JOIN for bot exclusion in AnalyticsActiveCountForPeriod
- Use LEFT JOIN for bot exclusion in IsEmpty
* Simplify fulltext search by consolidating buildFulltextClause functions
Remove convertMySQLFullTextColumnsToPostgres and consolidate
buildFulltextClause and buildFulltextClauseX into a single function
that takes variadic column arguments and returns sq.Sqlizer.
* Simplify SQL stores leveraging PostgreSQL-only support
- Simplify UpdateMembersRole in channel_store.go and team_store.go
to use UPDATE...RETURNING instead of SELECT + UPDATE
- Simplify GetPostReminders in post_store.go to use DELETE...RETURNING
- Simplify DeleteOrphanedRows queries by removing MySQL workarounds
for subquery locking issues
- Simplify UpdateUserLastSyncAt to use UPDATE...FROM...RETURNING
instead of fetching user first then updating
- Remove MySQL index hint workarounds in ORDER BY clauses
- Update outdated comments referencing MySQL
- Consolidate buildFulltextClause and remove convertMySQLFullTextColumnsToPostgres
* Remove MySQL-specific test artifacts
- Delete unused MySQLStopWords variable and stop_word.go file
- Remove redundant testSearchEmailAddressesWithQuotes test
(already covered by testSearchEmailAddresses)
- Update comment that referenced MySQL query planning
* Remove MySQL references from server code outside sqlstore
- Update config example and DSN parsing docs to reflect PostgreSQL-only support
- Remove mysql:// scheme check from IsDatabaseDSN
- Simplify SanitizeDataSource to only handle PostgreSQL
- Remove outdated MySQL comments from model and plugin code
* Remove MySQL references from test files
- Update test DSNs to use PostgreSQL format
- Remove dead mysql-replica flag and replicaFlag variable
- Simplify tests that had MySQL/PostgreSQL branches
* Update docs and test config to use PostgreSQL
- Update mmctl config set example to use postgres driver
- Update test-config.json to use PostgreSQL DSN format
* Remove MySQL migration scripts, test data, and docker image
Delete MySQL-related files that are no longer needed:
- ESR upgrade scripts (esr.*.mysql.*.sql)
- MySQL schema dumps (mattermost-mysql-*.sql)
- MySQL replication test scripts (replica-*.sh, mysql-migration-test.sh)
- MySQL test warmup data (mysql_migration_warmup.sql)
- MySQL docker image reference from mirror-docker-images.json
* Remove MySQL references from webapp
- Simplify minimumHashtagLength description to remove MySQL-specific configuration note
- Remove unused HIDE_MYSQL_STATS_NOTIFICATION preference constant
- Update en.json i18n source file
* clean up e2e-tests
* rm server/tests/template.load
* Use teamMemberSliceColumns() in UpdateMembersRole RETURNING clause
Refactor to use the existing helper function instead of hardcoding
the column names, ensuring consistency if the columns are updated.
* u.id -> u.Id
* address code review feedback
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-01-20 16:01:59 -05:00
PostMessageMaxBytesV2 = 65535
2021-07-12 14:05:36 -04:00
PostMessageMaxRunesV2 = PostMessageMaxBytesV2 / 4 // Assume a worst-case representation
Add cursor-based Posts Reporting API for compliance and auditing (#34252)
* Add cursor-based Posts Reporting API for compliance and auditing
Implements a new admin-only endpoint for retrieving posts with efficient
cursor-based pagination, designed for compliance, auditing, and archival
workflows.
Key Features:
- Cursor-based pagination using composite (time, ID) keys for consistent
performance regardless of dataset size (~10ms per page at any depth)
- Flexible time range queries with optional upper/lower bounds
- Support for both create_at and update_at time fields
- Ascending or descending sort order
- Optional metadata enrichment (files, reactions, acknowledgements)
- System admin only access (requires manage_system permission)
- License enforcement for compliance features
API Endpoint:
POST /api/v4/reports/posts
- Request: JSON body with channel_id, cursor_time, cursor_id, and options
- Response: Posts map + next_cursor object (null when pagination complete)
- Max page size: 1000 posts per request (MaxReportingPerPage constant)
Implementation:
- Store Layer: Direct SQL queries with composite index on (ChannelId, CreateAt, Id)
- App Layer: Permission checks, optional metadata enrichment, post hooks
- API Layer: Parameter validation, system admin enforcement, license checks
- Data Model: ReportPostOptions, ReportPostOptionsCursor, ReportPostListResponse
Code Quality Improvements:
- Added MaxReportingPerPage constant (1000) to eliminate magic numbers
- Removed unused StartTime field from ReportPostOptions
- Added fmt import for dynamic error messages
Testing:
- 14 comprehensive store layer unit tests
- 12 API layer integration tests covering permissions, pagination, filters
- All tests passing
Documentation:
- POSTS_REPORTING.md: Developer reference with Go structs and usage examples
- POSTS_REPORTING_API_SPEC.md: Complete technical specification
- GET_POSTS_API_IMPROVEMENTS.md: Implementation analysis and design rationale
- POSTS_TIME_RANGE_FEATURE.md: Archived time range feature for future use
Performance:
Cursor-based pagination maintains consistent ~10ms query time at any dataset
depth, compared to offset-based pagination which degrades significantly
(Page 1 = 10ms, Page 1000 = 10 seconds).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* lint fixes
* lint fixes
* gofmt
* i18n-extract
* Add Enterprise license requirement to posts reporting API
Enforce Enterprise license (tier 20+) for the new posts reporting endpoint
to align with compliance feature licensing. Professional tier is insufficient.
Changes:
- Add MinimumEnterpriseLicense check in GetPostsForReporting app layer
- Add test coverage for license validation (no license and Professional tier)
All existing tests pass with new license enforcement.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* i18n-extract
* add licensing to api documentation
* Test SSH signing
* Add mmctl command for posts reporting API
Adds mmctl report posts command to retrieve posts from a channel for
administrative reporting purposes. Supports cursor-based pagination with
configurable sorting, filtering, and time range options.
Includes database migration for updateat+id index to support efficient
cursor-based queries when sorting by update_at.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API cursor to opaque token and improve layer separation
This addresses code review feedback by transforming the cursor from exposed fields
to an opaque token and improving architectural layer separation.
**Key Changes:**
1. **Opaque Cursor Implementation**
- Transform cursor from split fields (cursor_time, cursor_id) to single opaque base64-encoded string
- Cursor now self-contained with all query parameters embedded
- When cursor provided, embedded parameters take precedence over request body
- Clients treat cursor as opaque token and pass unchanged
2. **Field Naming**
- Rename ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Now excludes ALL system posts (any type starting with "system_")
- Clearer and more consistent naming
3. **Layer Separation**
- Move cursor decoding from store layer to model layer
- Create ReportPostQueryParams struct for resolved parameters
- Store layer receives pre-resolved parameters (no business logic)
- Add ResolveReportPostQueryParams() function in model layer
4. **Code Quality**
- Add type-safe constants (ReportingTimeFieldCreateAt, ReportingSortDirectionAsc, etc.)
- Replace magic number 9223372036854775807 with math.MaxInt64
- Remove debug SQL logging (info disclosure risk)
- Update mmctl to use constants and fix NextCursor pointer access
5. **Tests**
- Update all 17 store test calls to use new resolution pattern
- Add comprehensive test for DESC + end_time boundary behavior
6. **API Documentation**
- Update OpenAPI spec to reflect opaque cursor format
- Update all request/response examples
- Clarify end_time behavior with sort directions
**Files Changed:**
- Model layer: public/model/post.go
- App layer: channels/app/report.go
- Store layer: channels/store/store.go, channels/store/sqlstore/post_store.go
- Tests: channels/store/storetest/post_store.go
- Mocks: channels/store/storetest/mocks/PostStore.go
- API: channels/api4/report.go, channels/api4/report_test.go
- mmctl: cmd/mmctl/commands/report.go
- Docs: api/v4/source/reports.yaml
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix unhandled parse errors in cursor decoding
Address security finding: cursor decoding was silently ignoring parse errors
from strconv functions, which could lead to unexpected behavior when malformed
cursors are provided.
Changes:
- Add explicit error handling for strconv.Atoi (version parsing)
- Add explicit error handling for strconv.ParseBool (includeDeleted, excludeSystemPosts)
- Add explicit error handling for strconv.ParseInt (timestamp parsing)
- Return clear error messages indicating which field failed to parse
This prevents silent failures where malformed values would default to zero-values
(0, false) and potentially alter query behavior without warning.
Addresses DryRun Security finding: "Unhandled Errors in Cursor Parsing"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues
- Remove unused reportPostCursorV1 struct (unused)
- Remove obsolete +build comment (buildtag)
- Use maps.Copy instead of manual loop (mapsloop)
- Modernize for loop with range over int (rangeint)
- Apply gofmt formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix gofmt formatting issues
Fix alignment in struct literals and constant declarations:
- Align map keys in report_test.go request bodies
- Align struct fields in ReportPostOptions initialization
- Align reporting constant declarations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more lint fixes
* remove index update files
* Remove end_time parameter from Posts Reporting API
Align with other cursor-based APIs in the codebase by removing the end_time
parameter. The caller now controls when to stop pagination by simply not
making another request, which is the same pattern used by GetPostsSinceForSync,
MessageExport, and GetPostsBatchForIndexing.
Changes:
- Remove EndTime field from ReportPostOptions and ReportPostQueryParams
- Remove EndTime filtering logic from store layer
- Remove tests that used end_time parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API for security and validation
Address security review feedback by consolidating parameter resolution
and validation in the API layer, with comprehensive validation of all
cursor fields to prevent SQL injection and invalid queries.
Changes:
- Move parameter resolution from model to API layer for clearer separation
- Add ReportPostQueryParams.Validate() with inline validation for all fields
- Validate ChannelId, TimeField, SortDirection, and CursorId format
- Add start_time parameter for time-bounded queries
- Cap per_page at 100-1000 instead of rejecting invalid values
- Export DecodeReportPostCursorV1() for API layer use
- Simplify app layer to receive pre-validated parameters
- Check channel existence when results are empty (better error messages)
Testing:
- Add 10 model tests for validation and malformed cursor scenarios
- Add 4 API tests for cursors with invalid field values
- Refactor 13 store tests to use buildReportPostQueryParams() helper
- All 31 tests pass
Documentation:
- Update OpenAPI spec with start_time, remove unused end_time
- Update markdown docs with start_time examples
Security improvements:
- Whitelist validation prevents SQL injection in TimeField/SortDirection
- Format validation ensures ChannelId and CursorId are valid IDs
- Single validation point for both cursor and options paths
- Defense in depth: validation + parameterized queries + store layer whitelist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Improve posts reporting query efficiency and safety
Replace SELECT * and nested OR/AND conditions with explicit column
selection and PostgreSQL row value comparison for better performance
and maintainability.
Changes:
- Use postSliceColumns() instead of SELECT * for explicit column selection
- Replace Squirrel OR/AND with row value comparison: (timeField, Id) > (?, ?)
- Use fmt.Sprintf for safer string formatting in WHERE clause
Query improvements:
Before: WHERE (CreateAt > ?) OR (CreateAt = ? AND Id > ?)
After: WHERE (CreateAt, Id) > (?, ?)
Benefits:
- Explicit column selection prevents issues if table schema changes
- Row value comparison is more concise and better optimized by PostgreSQL
- Follows existing patterns in post_store.go (postSliceColumns)
- Standard SQL:2003 syntax
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Change posts reporting response from map to ordered array
Replace the Posts map with an ordered array to preserve query sort order
and provide a more natural API response for sequential processing.
Changes:
- ReportPostListResponse.Posts: map[string]*Post → []*Post
- Store layer returns posts array directly (already sorted by query)
- App layer iterates by index for metadata enrichment
- Remove applyPostsWillBeConsumedHook call (not applicable to reporting)
- Update API tests to iterate arrays instead of map lookups
- Update store tests to convert array to map for deduplication checks
- Remove unused "maps" import
Benefits:
- Preserves query sort order (ASC/DESC, create_at/update_at)
- More natural for sequential processing/export workflows
- Simpler response structure for reporting/compliance use cases
- Aligns with message export/compliance patterns (no plugin hooks)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues in posts reporting tests
Replace inefficient loops with append(...) for better performance.
Changes:
- Use append(postSlice, result.Posts...) instead of loop
- Simplifies code and follows staticcheck recommendations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix store test AppError nil checking
Use require.Nil instead of require.NoError for *AppError returns
to avoid Go interface nil pointer issues.
When DecodeReportPostCursorV1 returns nil *AppError and it's assigned
to error interface, the interface becomes non-nil even though the
pointer is nil. This causes require.NoError to fail incorrectly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2025-11-17 11:02:19 -05:00
// Reporting API constants
MaxReportingPerPage = 1000 // Maximum number of posts that can be requested per page in reporting endpoints
ReportingTimeFieldCreateAt = "create_at"
ReportingTimeFieldUpdateAt = "update_at"
ReportingSortDirectionAsc = "asc"
ReportingSortDirectionDesc = "desc"
PostPropsMaxRunes = 800000
PostPropsMaxUserRunes = PostPropsMaxRunes - 40000 // Leave some room for system / pre-save modifications
2021-07-12 14:05:36 -04:00
PropsAddChannelMember = "add_channel_member"
2023-10-03 10:51:07 -04:00
PostPropsAddedUserId = "addedUserId"
PostPropsDeleteBy = "deleteBy"
PostPropsOverrideIconURL = "override_icon_url"
PostPropsOverrideIconEmoji = "override_icon_emoji"
PostPropsOverrideUsername = "override_username"
PostPropsFromWebhook = "from_webhook"
PostPropsFromBot = "from_bot"
PostPropsFromOAuthApp = "from_oauth_app"
PostPropsWebhookDisplayName = "webhook_display_name"
2025-03-20 07:53:50 -04:00
PostPropsAttachments = "attachments"
PostPropsFromPlugin = "from_plugin"
2021-07-12 14:05:36 -04:00
PostPropsMentionHighlightDisabled = "mentionHighlightDisabled"
PostPropsGroupHighlightDisabled = "disable_group_highlight"
2023-10-03 10:51:07 -04:00
PostPropsPreviewedPost = "previewed_post"
2024-11-08 07:57:06 -05:00
PostPropsForceNotification = "force_notification"
2025-03-20 07:53:50 -04:00
PostPropsChannelMentions = "channel_mentions"
2026-02-16 15:31:32 -05:00
PostPropsCurrentTeamId = "current_team_id"
2025-03-20 07:53:50 -04:00
PostPropsUnsafeLinks = "unsafe_links"
2025-11-10 16:32:18 -05:00
PostPropsAIGeneratedByUserID = "ai_generated_by"
PostPropsAIGeneratedByUsername = "ai_generated_by_username"
2025-12-11 01:59:50 -05:00
PostPropsExpireAt = "expire_at"
PostPropsReadDurationSeconds = "read_duration"
2022-11-23 14:08:21 -05:00
2025-03-20 07:53:50 -04:00
PostPriorityUrgent = "urgent"
2025-12-11 01:59:50 -05:00
DefaultExpirySeconds = 60 * 60 * 24 * 7 // 7 days
DefaultReadDurationSeconds = 10 * 60 // 10 minutes
PostContextKeyIsScheduledPost PostContextKey = "isScheduledPost"
2015-06-15 03:53:32 -04:00
)
type Post struct {
2018-01-22 16:32:50 -05:00
Id string ` json:"id" `
CreateAt int64 ` json:"create_at" `
UpdateAt int64 ` json:"update_at" `
EditAt int64 ` json:"edit_at" `
DeleteAt int64 ` json:"delete_at" `
IsPinned bool ` json:"is_pinned" `
UserId string ` json:"user_id" `
ChannelId string ` json:"channel_id" `
RootId string ` json:"root_id" `
OriginalId string ` json:"original_id" `
Message string ` json:"message" `
// MessageSource will contain the message as submitted by the user if Message has been modified
// by Mattermost for presentation (e.g if an image proxy is being used). It should be used to
// populate edit boxes if present.
2022-01-31 12:38:30 -05:00
MessageSource string ` json:"message_source,omitempty" `
2018-01-22 16:32:50 -05:00
2015-11-05 17:32:44 -05:00
Type string ` json:"type" `
2020-03-13 16:12:20 -04:00
propsMu sync . RWMutex ` db:"-" ` // Unexported mutex used to guard Post.Props.
Props StringInterface ` json:"props" ` // Deprecated: use GetProps()
2015-11-05 17:32:44 -05:00
Hashtags string ` json:"hashtags" `
2021-07-26 08:32:35 -04:00
Filenames StringArray ` json:"-" ` // Deprecated, do not use this field any more
2025-01-13 07:46:56 -05:00
FileIds StringArray ` json:"file_ids" `
2022-01-31 12:38:30 -05:00
PendingPostId string ` json:"pending_post_id" `
2018-11-22 13:09:27 -05:00
HasReactions bool ` json:"has_reactions,omitempty" `
2021-04-01 13:44:56 -04:00
RemoteId * string ` json:"remote_id,omitempty" `
2018-08-07 16:24:56 -04:00
2018-10-24 04:49:55 -04:00
// Transient data populated before sending a post to the client
2022-01-31 12:38:30 -05:00
ReplyCount int64 ` json:"reply_count" `
LastReplyAt int64 ` json:"last_reply_at" `
Participants [ ] * User ` json:"participants" `
IsFollowing * bool ` json:"is_following,omitempty" ` // for root posts in collapsed thread mode indicates if the current user is following this thread
Metadata * PostMetadata ` json:"metadata,omitempty" `
2015-06-15 03:53:32 -04:00
}
2025-03-31 04:44:34 -04:00
func ( o * Post ) Auditable ( ) map [ string ] any {
2023-06-15 08:23:56 -04:00
var metaData map [ string ] any
if o . Metadata != nil {
metaData = o . Metadata . Auditable ( )
}
2025-03-31 04:44:34 -04:00
return map [ string ] any {
2022-07-14 07:52:46 -04:00
"id" : o . Id ,
"create_at" : o . CreateAt ,
"update_at" : o . UpdateAt ,
"edit_at" : o . EditAt ,
"delete_at" : o . DeleteAt ,
"is_pinned" : o . IsPinned ,
"user_id" : o . UserId ,
"channel_id" : o . ChannelId ,
"root_id" : o . RootId ,
"original_id" : o . OriginalId ,
"type" : o . Type ,
"props" : o . GetProps ( ) ,
"file_ids" : o . FileIds ,
"pending_post_id" : o . PendingPostId ,
"remote_id" : o . RemoteId ,
"reply_count" : o . ReplyCount ,
"last_reply_at" : o . LastReplyAt ,
"is_following" : o . IsFollowing ,
2023-06-15 08:23:56 -04:00
"metadata" : metaData ,
2022-07-14 07:52:46 -04:00
}
}
2023-10-23 14:22:36 -04:00
func ( o * Post ) LogClone ( ) any {
return o . Auditable ( )
}
2018-04-17 08:20:47 -04:00
type PostEphemeral struct {
UserID string ` json:"user_id" `
Post * Post ` json:"post" `
}
2017-03-29 11:06:51 -04:00
type PostPatch struct {
IsPinned * bool ` json:"is_pinned" `
Message * string ` json:"message" `
Props * StringInterface ` json:"props" `
FileIds * StringArray ` json:"file_ids" `
HasReactions * bool ` json:"has_reactions" `
}
2026-03-27 11:29:06 -04:00
func ( o * PostPatch ) IsEmpty ( ) bool {
return o . IsPinned == nil && o . Message == nil && o . Props == nil && o . FileIds == nil && o . HasReactions == nil
}
2022-07-26 06:42:56 -04:00
type PostReminder struct {
TargetTime int64 ` json:"target_time" `
// These fields are only used internally for interacting with DB.
PostId string ` json:",omitempty" `
UserId string ` json:",omitempty" `
}
2022-11-23 14:08:21 -05:00
type PostPriority struct {
Priority * string ` json:"priority" `
RequestedAck * bool ` json:"requested_ack" `
PersistentNotifications * bool ` json:"persistent_notifications" `
// These fields are only used internally for interacting with DB.
PostId string ` json:",omitempty" `
ChannelId string ` json:",omitempty" `
}
2023-05-18 14:14:12 -04:00
type PostPersistentNotifications struct {
PostId string
CreateAt int64
LastSentAt int64
DeleteAt int64
SentCount int16
}
type GetPersistentNotificationsPostsParams struct {
MaxTime int64
MaxSentCount int16
PerPage int
}
2023-12-11 15:27:34 -05:00
type MoveThreadParams struct {
ChannelId string ` json:"channel_id" `
}
2018-08-28 13:09:32 -04:00
type SearchParameter struct {
2018-09-26 10:27:04 -04:00
Terms * string ` json:"terms" `
IsOrSearch * bool ` json:"is_or_search" `
TimeZoneOffset * int ` json:"time_zone_offset" `
Page * int ` json:"page" `
PerPage * int ` json:"per_page" `
IncludeDeletedChannels * bool ` json:"include_deleted_channels" `
2018-08-28 13:09:32 -04:00
}
2025-03-31 04:44:34 -04:00
func ( sp SearchParameter ) Auditable ( ) map [ string ] any {
return map [ string ] any {
2025-01-28 22:21:05 -05:00
"terms" : sp . Terms ,
"is_or_search" : sp . IsOrSearch ,
"time_zone_offset" : sp . TimeZoneOffset ,
"page" : sp . Page ,
"per_page" : sp . PerPage ,
"include_deleted_channels" : sp . IncludeDeletedChannels ,
}
}
func ( sp SearchParameter ) LogClone ( ) any {
return sp . Auditable ( )
}
2019-07-02 17:10:22 -04:00
type AnalyticsPostCountsOptions struct {
TeamId string
BotsOnly bool
YesterdayOnly bool
}
2018-01-22 16:32:50 -05:00
func ( o * PostPatch ) WithRewrittenImageURLs ( f func ( string ) string ) * PostPatch {
2023-06-30 10:42:56 -04:00
pCopy := * o //nolint:revive
if pCopy . Message != nil {
* pCopy . Message = RewriteImageURLs ( * o . Message , f )
2018-01-22 16:32:50 -05:00
}
2023-06-30 10:42:56 -04:00
return & pCopy
2018-01-22 16:32:50 -05:00
}
2025-03-31 04:44:34 -04:00
func ( o * PostPatch ) Auditable ( ) map [ string ] any {
return map [ string ] any {
2022-12-16 10:50:06 -05:00
"is_pinned" : o . IsPinned ,
"props" : o . Props ,
"file_ids" : o . FileIds ,
"has_reactions" : o . HasReactions ,
}
}
2018-09-17 10:51:26 -04:00
type PostForExport struct {
Post
TeamName string
ChannelName string
Username string
ReplyCount int
2024-10-11 06:28:37 -04:00
FlaggedBy StringArray
2018-09-17 10:51:26 -04:00
}
2019-03-15 11:28:43 -04:00
type DirectPostForExport struct {
Post
User string
ChannelMembers * [ ] string
2024-10-11 06:28:37 -04:00
FlaggedBy StringArray
2019-03-15 11:28:43 -04:00
}
2018-09-17 10:51:26 -04:00
type ReplyForExport struct {
Post
2024-10-11 06:28:37 -04:00
Username string
FlaggedBy StringArray
2018-09-17 10:51:26 -04:00
}
2017-07-11 04:09:15 -04:00
type PostForIndexing struct {
Post
TeamId string ` json:"team_id" `
ParentCreateAt * int64 ` json:"parent_create_at" `
2026-03-09 14:07:44 -04:00
ChannelType string ` json:"channel_type" `
2017-07-11 04:09:15 -04:00
}
2021-01-11 09:14:16 -05:00
type FileForIndexing struct {
FileInfo
ChannelId string ` json:"channel_id" `
Content string ` json:"content" `
}
2024-07-26 02:15:42 -04:00
// ShouldIndex tells if a file should be indexed or not.
// index files which are-
// a. not deleted
// b. have an associated post ID, if no post ID, then,
// b.i. the file should belong to the channel's bookmarks, as indicated by the "CreatorId" field.
//
// Files not passing this criteria will be deleted from ES index.
// We're deleting those files from ES index instead of simply skipping them while fetching a batch of files
// because existing ES indexes might have these files already indexed, so we need to remove them from index.
func ( file * FileForIndexing ) ShouldIndex ( ) bool {
// NOTE - this function is used in server as well as Enterprise code.
// Make sure to update public package dependency in both server and Enterprise code when
// updating the logic here and to test both places.
return file != nil && file . DeleteAt == 0 && ( file . PostId != "" || file . CreatorId == BookmarkFileOwner )
}
2020-03-13 16:12:20 -04:00
// ShallowCopy is an utility function to shallow copy a Post to the given
// destination without touching the internal RWMutex.
func ( o * Post ) ShallowCopy ( dst * Post ) error {
if dst == nil {
return errors . New ( "dst cannot be nil" )
}
o . propsMu . RLock ( )
defer o . propsMu . RUnlock ( )
dst . propsMu . Lock ( )
defer dst . propsMu . Unlock ( )
dst . Id = o . Id
dst . CreateAt = o . CreateAt
dst . UpdateAt = o . UpdateAt
dst . EditAt = o . EditAt
dst . DeleteAt = o . DeleteAt
dst . IsPinned = o . IsPinned
dst . UserId = o . UserId
dst . ChannelId = o . ChannelId
dst . RootId = o . RootId
dst . OriginalId = o . OriginalId
dst . Message = o . Message
dst . MessageSource = o . MessageSource
dst . Type = o . Type
dst . Props = o . Props
dst . Hashtags = o . Hashtags
dst . Filenames = o . Filenames
dst . FileIds = o . FileIds
dst . PendingPostId = o . PendingPostId
dst . HasReactions = o . HasReactions
dst . ReplyCount = o . ReplyCount
2021-01-14 06:46:27 -05:00
dst . Participants = o . Participants
dst . LastReplyAt = o . LastReplyAt
2020-03-13 16:12:20 -04:00
dst . Metadata = o . Metadata
2021-06-14 08:25:44 -04:00
if o . IsFollowing != nil {
2024-08-05 23:45:00 -04:00
dst . IsFollowing = NewPointer ( * o . IsFollowing )
2021-06-14 08:25:44 -04:00
}
2021-04-01 13:44:56 -04:00
dst . RemoteId = o . RemoteId
2020-03-13 16:12:20 -04:00
return nil
}
// Clone shallowly copies the post and returns the copy.
2018-08-07 16:24:56 -04:00
func ( o * Post ) Clone ( ) * Post {
2023-06-30 10:42:56 -04:00
pCopy := & Post { } //nolint:revive
o . ShallowCopy ( pCopy )
return pCopy
2018-08-07 16:24:56 -04:00
}
2021-09-01 08:43:12 -04:00
func ( o * Post ) ToJSON ( ) ( string , error ) {
2023-06-30 10:42:56 -04:00
pCopy := o . Clone ( ) //nolint:revive
pCopy . StripActionIntegrations ( )
b , err := json . Marshal ( pCopy )
2021-09-01 08:43:12 -04:00
return string ( b ) , err
2015-06-15 03:53:32 -04:00
}
2022-01-21 11:25:33 -05:00
func ( o * Post ) EncodeJSON ( w io . Writer ) error {
o . StripActionIntegrations ( )
return json . NewEncoder ( w ) . Encode ( o )
}
2024-10-22 10:00:26 -04:00
type CreatePostFlags struct {
2024-11-08 07:57:06 -05:00
TriggerWebhooks bool
SetOnline bool
ForceNotification bool
2024-10-22 10:00:26 -04:00
}
2020-02-05 07:27:35 -05:00
type GetPostsSinceOptions struct {
2021-03-23 07:31:54 -04:00
UserId string
2021-01-14 06:46:27 -05:00
ChannelId string
Time int64
SkipFetchThreads bool
CollapsedThreads bool
CollapsedThreadsExtended bool
2021-04-01 13:44:56 -04:00
SortAscending bool
}
2021-05-20 12:07:40 -04:00
type GetPostsSinceForSyncCursor struct {
LastPostUpdateAt int64
2023-12-04 13:10:20 -05:00
LastPostUpdateID string
LastPostCreateAt int64
LastPostCreateID string
}
func ( c GetPostsSinceForSyncCursor ) IsEmpty ( ) bool {
return c . LastPostCreateAt == 0 && c . LastPostCreateID == "" && c . LastPostUpdateAt == 0 && c . LastPostUpdateID == ""
2021-05-20 12:07:40 -04:00
}
2021-04-01 13:44:56 -04:00
type GetPostsSinceForSyncOptions struct {
2025-05-27 09:15:49 -04:00
ChannelId string
ExcludeRemoteId string
IncludeDeleted bool
Merge the Integrated Boards MVP feature branch (#35796)
* Add CreatedBy and UpdatedBy to the properties fields and values (#34485)
* Add CreatedBy and UpdatedBy to the properties fields and values
* Fix types
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds ObjectType to the property fields table (#34908)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Update ObjectType migration setting an empty value and marking the column as not null (#34915)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds uniqueness mechanisms to the property fields (#35058)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Fixing retrylayer mocks
* Remove retrylayer duplication
* Address review comments
* Fix comment to avoid linter issues
* Address PR comments
* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.down.sql
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.up.sql
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.up.sql
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Update field validation to check only for valid target types
* Update migrations to avoid concurrent index creation within a transaction
* Update migrations to make all index ops concurrent
* Update tests to use valid PSAv2 property fields
* Adds a helper for valid PSAv2 TargetTypes
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Fix property tests (#35388)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds Integrated Boards feature flag (#35378)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds Integrated Boards MVP API changes (#34822)
This PR includes the necessary changes for channels and posts
endpoints and adds a set of generic endpoints to retrieve and manage
property fields and values following the new Property System approach.
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Mattermost Build <build@mattermost.com>
* Property System Architecture permissions for v2 (#35113)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Add the permissions to the migrations, model and update the store calls
* Adds the property field and property group app layer
* Adds authorization helpers for property fields and values
* Make sure that users cannot lock themselves out of property fields
* Migrate permissions from a JSON column to three normalized columns
* Remove the audit comment
* Use target level constants in authorization
* Log authorization membership failures
* Rename admin to sysadmin
* Fix i18n sorting
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Add Views store and app layer (#35361)
* Add Views store and app layer for Integrated Boards
Implements the View entity (model, SQL store, service, app) as described
in the Integrated Boards tech spec. Views are channel-scoped board
configurations with typed props (board, kanban subviews) and soft-delete.
- public/model: View, ViewBoardProps, Subview, ViewPatch types with
PreSave/PreUpdate/IsValid/Patch/Clone/Auditable
- Migration 158: Views table with jsonb Props column and indexes
- SqlViewStore: CRUD with nil-safe Props marshaling (AppendBinaryFlag)
- ViewService: CreateView seeds default kanban subview and links the
boards property field; caches boardPropertyFieldID at startup
- App layer: CreateView/GetView/GetViewsForChannel/UpdateView/DeleteView
with channel-membership permission checks and WebSocket events
(view_created, view_updated, view_deleted)
- doSetupBoardsPropertyField: registers the Boards property group and
board field in NewServer() before ViewService construction
- GetFieldByName now returns store.ErrNotFound instead of raw sql.ErrNoRows
* Move permission checks out of App layer for views
- Remove HasPermissionToChannel calls from all App view methods
- Drop userID params from GetView, GetViewsForChannel, UpdateView, DeleteView
- Fix doSetupBoardsPropertyField to include required TargetType for PSAv2 field
* Make View service generic and enforce board validation in model
- Remove board-specific auto-setup from service and server startup
- Enforce that board views require Props, at least one subview, and at least one linked property in IsValid()
- Move default subview seeding out of app layer; callers must provide valid props
- Call PreSave on subviews during PreUpdate to assign IDs to new subviews
- Update all tests to reflect the new validation requirements
* Restore migrations files to match base branch
* Distinguish ErrNotFound from other errors in view store Get
* Use CONCURRENTLY and nontransactional for index operations in views migration
* Split views index creation into separate nontransactional migrations
* Update migrations.list
* Update i18n translations for views
* Fix makeView helper to include required Props for board view validation
* Rename ctx parameter from c to rctx in OAuthProvider mock
* Remove views service layer, call store directly from app
* Return 500 for unexpected DB errors in GetView, 404 only for not-found
* Harden View model: deep-copy Props, validate linked property IDs
- Add ViewBoardProps.Clone() to deep-copy LinkedProperties and Subviews
- Use it in View.Clone() and View.Patch() to prevent shared-slice aliasing
- Iterate over LinkedProperties in View.IsValid() and reject invalid IDs
with a dedicated i18n key
- Register ViewStore in storetest AssertExpectations so mock expectations
are enforced
- Add tests covering all new behaviours
* Restore autotranslation worker_stopped i18n translation
* Fix view store test IDs and improve error handling in app layer
- Use model.NewId() for linked property IDs in testUpdateView to fix
validation failure (IsValid rejects non-UUID strings)
- Fix import grouping in app/view.go (stdlib imports in one block)
- Return 404 instead of 500 when Update/Delete store calls return
ErrNotFound (e.g. concurrent deletion TOCTOU race)
* Add View store mock to retrylayer test genStore helper
The View store was added to the store interface but the genStore()
helper in retrylayer_test.go was not updated, causing TestRetry to panic.
Also removes the duplicate Recap mock registration.
* Refactor view deletion and websocket event handling; update SQL store methods to use query builder
* revert property field store
* Remove useless migrations
* Add cursor-based pagination to View store GetForChannel
- Add ViewQueryCursor and ViewQueryOpts types with validation
- Return (views, cursor, error) for caller-driven pagination
- PerPage clamping: <=0 defaults to 20, >200 clamps to 200
- Support IncludeDeleted filter
- Add comprehensive store tests for pagination, cursor edge cases,
PerPage clamping, and invalid input rejection
- Add app layer test for empty channelID → 400
- Update interface, retrylayer, timerlayer, and mock signatures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refactor test loops in ViewStore tests for improved readability
* change pagination to limit/offset
* Add upper-bound limits on View Subviews and LinkedProperties
Defense-in-depth validation: cap Subviews at 50 and LinkedProperties
at 500 to prevent abuse below the 300KB payload limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* MM-67388, MM-66528, MM-67750: Add View REST API endpoints, websocket events, and sort order (#35442)
* Add Views store and app layer for Integrated Boards
Implements the View entity (model, SQL store, service, app) as described
in the Integrated Boards tech spec. Views are channel-scoped board
configurations with typed props (board, kanban subviews) and soft-delete.
- public/model: View, ViewBoardProps, Subview, ViewPatch types with
PreSave/PreUpdate/IsValid/Patch/Clone/Auditable
- Migration 158: Views table with jsonb Props column and indexes
- SqlViewStore: CRUD with nil-safe Props marshaling (AppendBinaryFlag)
- ViewService: CreateView seeds default kanban subview and links the
boards property field; caches boardPropertyFieldID at startup
- App layer: CreateView/GetView/GetViewsForChannel/UpdateView/DeleteView
with channel-membership permission checks and WebSocket events
(view_created, view_updated, view_deleted)
- doSetupBoardsPropertyField: registers the Boards property group and
board field in NewServer() before ViewService construction
- GetFieldByName now returns store.ErrNotFound instead of raw sql.ErrNoRows
* Move permission checks out of App layer for views
- Remove HasPermissionToChannel calls from all App view methods
- Drop userID params from GetView, GetViewsForChannel, UpdateView, DeleteView
- Fix doSetupBoardsPropertyField to include required TargetType for PSAv2 field
* Make View service generic and enforce board validation in model
- Remove board-specific auto-setup from service and server startup
- Enforce that board views require Props, at least one subview, and at least one linked property in IsValid()
- Move default subview seeding out of app layer; callers must provide valid props
- Call PreSave on subviews during PreUpdate to assign IDs to new subviews
- Update all tests to reflect the new validation requirements
* Restore migrations files to match base branch
* Distinguish ErrNotFound from other errors in view store Get
* Use CONCURRENTLY and nontransactional for index operations in views migration
* Split views index creation into separate nontransactional migrations
* Update migrations.list
* Update i18n translations for views
* Fix makeView helper to include required Props for board view validation
* Rename ctx parameter from c to rctx in OAuthProvider mock
* Remove views service layer, call store directly from app
* Return 500 for unexpected DB errors in GetView, 404 only for not-found
* Harden View model: deep-copy Props, validate linked property IDs
- Add ViewBoardProps.Clone() to deep-copy LinkedProperties and Subviews
- Use it in View.Clone() and View.Patch() to prevent shared-slice aliasing
- Iterate over LinkedProperties in View.IsValid() and reject invalid IDs
with a dedicated i18n key
- Register ViewStore in storetest AssertExpectations so mock expectations
are enforced
- Add tests covering all new behaviours
* Restore autotranslation worker_stopped i18n translation
* Fix view store test IDs and improve error handling in app layer
- Use model.NewId() for linked property IDs in testUpdateView to fix
validation failure (IsValid rejects non-UUID strings)
- Fix import grouping in app/view.go (stdlib imports in one block)
- Return 404 instead of 500 when Update/Delete store calls return
ErrNotFound (e.g. concurrent deletion TOCTOU race)
* Add View store mock to retrylayer test genStore helper
The View store was added to the store interface but the genStore()
helper in retrylayer_test.go was not updated, causing TestRetry to panic.
Also removes the duplicate Recap mock registration.
* Refactor view deletion and websocket event handling; update SQL store methods to use query builder
* revert property field store
* Add View API endpoints with OpenAPI spec, client methods, and i18n
Implement REST API for channel views (board-type) behind the
IntegratedBoards feature flag. Adds CRUD endpoints under
/api/v4/channels/{channel_id}/views with permission checks
matching the channel bookmark pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove useless migrations
* Add cursor-based pagination to View store GetForChannel
- Add ViewQueryCursor and ViewQueryOpts types with validation
- Return (views, cursor, error) for caller-driven pagination
- PerPage clamping: <=0 defaults to 20, >200 clamps to 200
- Support IncludeDeleted filter
- Add comprehensive store tests for pagination, cursor edge cases,
PerPage clamping, and invalid input rejection
- Add app layer test for empty channelID → 400
- Update interface, retrylayer, timerlayer, and mock signatures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add cursor-based pagination to View API for channel views
* Enhance cursor handling in getViewsForChannel and update tests for pagination
* Refactor test loops in ViewStore tests for improved readability
* Refactor loop in TestGetViewsForChannel for improved readability
* change pagination to limit/offset
* switch to limit/offset pagination
* Add upper-bound limits on View Subviews and LinkedProperties
Defense-in-depth validation: cap Subviews at 50 and LinkedProperties
at 500 to prevent abuse below the 300KB payload limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add view sort order API endpoint
Add POST /api/v4/channels/{channel_id}/views/{view_id}/sort_order
endpoint following the channel bookmarks reorder pattern. Includes
store, app, and API layers with full test coverage at each layer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add connectionId to view WebSocket events and sort_order API spec
Thread connectionId from request header through all view handlers
(create, update, delete, sort_order) to WebSocket events, matching
the channel bookmarks pattern. Add sort_order endpoint to OpenAPI
spec. Update minimum server version to 11.6.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove duplicate View/ViewPatch definitions from definitions.yaml
The merge from integrated-boards-mvp introduced duplicate View and
ViewPatch schema definitions that were already defined earlier in
the file with more detail (including ViewBoardProps ref and enums).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update minimum server version to 11.6 in views API spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add missing translations for view sort order error messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Merge integrated-boards-mvp into ibmvp_api-views; remove spec files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix flaky TestViewStore timestamp test on CI
Add sleep before UpdateSortOrder to ensure timestamps differ,
preventing same-millisecond comparisons on fast CI machines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* remove duplicate views.yaml imclude
* Use c.boolString() for include_deleted query param in GetViewsForChannel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix views.yaml sort order schema: use integer type and require body
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refactor view sort order tests to use named IDs instead of array indices
Extract idA/idB/idC from views slice and add BEFORE/AFTER comments
to make stateful subtest ordering easier to follow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Return 404 instead of 403 for view operations on deleted channels
Deleted channels should appear non-existent to callers rather than
revealing their existence via a 403. Detailed error text explains
the context for debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add missing channel deleteat checks
* Use c.Params.Page instead of manual page query param parsing in getViewsForChannel
c.Params already validates and defaults page/per_page, so the manual
parsing was redundant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add support for total count in views retrieval
* Add tests for handling deleted views in GetViewsForChannel and GetView
* Short-circuit negative newIndex in UpdateSortOrder before opening transaction
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add per-channel limit on views to bound UpdateSortOrder cost
Without a cap, unbounded view creation makes sort-order updates
increasingly expensive (CASE WHEN per view, row locks). Adds
MaxViewsPerChannel=50 constant and enforces it in the app layer
before saving. Includes API and app layer tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove include_deleted support from views API
Soft-deleted views are structural metadata with low risk, but no other
similar endpoint (e.g. channel bookmarks) exposes deleted records without
an admin gate. Rather than adding an admin-only permission check for
consistency, remove the feature entirely since there is no current use case.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update view permissions to require `create_post` instead of channel management permissions
* Remove obsolete view management error messages for direct and group messages
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(migrations): add user tracking and object type to property fields
- Introduced user tracking columns (CreatedBy, UpdatedBy) to PropertyFields and PropertyValues.
- Added ObjectType column to PropertyFields with associated unique indexes for legacy and typed properties.
- Created new migration scripts for adding and dropping these features, including necessary indexes for data integrity.
- Established views for managing property fields with new attributes.
This update enhances the schema to support better tracking and categorization of property fields.
* Add Property System Architecture v2 API endpoints (#35583)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Add the permissions to the migrations, model and update the store calls
* Adds the property field and property group app layer
* Adds authorization helpers for property fields and values
* Make sure that users cannot lock themselves out of property fields
* Migrate permissions from a JSON column to three normalized columns
* Remove the audit comment
* Use target level constants in authorization
* Log authorization membership failures
* Rename admin to sysadmin
* Adds the Property System Architecture v2 API endpoints
* Adds permission checks to the create field endpoint
* Add target access checks to value endpoints
* Add default branches for object_type and target_type and extra guards for cursor client4 methods
* Fix vet API mismatch
* Fix error checks
* Fix linter
* Add merge semantics for property patch logic and API endpoint
* Fix i18n
* Fix duplicated patch elements and early return on bad cursor
* Update docs to use enums
* Fix i18n sorting
* Update app layer to return model.AppError
* Adds a limit to the number of property values that can be patched in the same request
* Require target_type filter when searching property fields
* Add objectType validation as part of field.IsValid()
* Fix linter
* Fix test with bad objecttpye
* Fix test grouping
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* MM-67968: Flatten view model — remove icon, subviews, typed board props (#35726)
* feat(views): flatten view model by removing icon, subview, and board props
Simplifies the View data model as part of MM-67968: removes Icon, Subview,
and ViewBoardProps types; renames ViewTypeBoard to ViewTypeKanban; replaces
typed Props with StringInterface (map[string]any); adds migration 000167
to drop the Icon column from the Views table.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(api): update views OpenAPI spec to reflect flattened model
Removes ViewBoardProps, Subview, and icon from the View and ViewPatch
schemas. Changes type enum from board to kanban. Replaces typed props
with a free-form StringInterface object. Aligns with MM-67968.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* refactor(views): simplify store by dropping dbView and marshalViewProps
StringInterface already implements driver.Valuer and sql.Scanner, so the
manual JSON marshal/unmarshal and the dbView intermediate struct were
redundant. model.View now scans directly from the database. Also removes
the dead ViewMaxLinkedProperties constant and wraps the Commit() error in
UpdateSortOrder.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(api): allow arbitrary JSON in view props OpenAPI schema
The props field was restricted to string values via
additionalProperties: { type: string }, conflicting with the Go model's
StringInterface (map[string]any). Changed to additionalProperties: true
in View, ViewPatch, and inline POST schemas.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* Adds basic implementation of the generic redux store for PSAv2 (#35512)
* Adds basic implementation of the generic redux store for PSAv2
* Add created_by and updated_by to the test fixtures
* Make target_id, target_type and object_type mandatory
* Wrap getPropertyFieldsByIds and getPropertyValuesForTargetByFieldIds with createSelector
* Address PR comments
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds websocket messages for the PSAv2 API events (#35696)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Add the permissions to the migrations, model and update the store calls
* Adds the property field and property group app layer
* Adds authorization helpers for property fields and values
* Make sure that users cannot lock themselves out of property fields
* Migrate permissions from a JSON column to three normalized columns
* Remove the audit comment
* Use target level constants in authorization
* Log authorization membership failures
* Rename admin to sysadmin
* Adds the Property System Architecture v2 API endpoints
* Adds permission checks to the create field endpoint
* Add target access checks to value endpoints
* Add default branches for object_type and target_type and extra guards for cursor client4 methods
* Fix vet API mismatch
* Fix error checks
* Fix linter
* Add merge semantics for property patch logic and API endpoint
* Fix i18n
* Fix duplicated patch elements and early return on bad cursor
* Update docs to use enums
* Fix i18n sorting
* Update app layer to return model.AppError
* Adds a limit to the number of property values that can be patched in the same request
* Adds websocket messages for the PSAv2 API events
* Add IsPSAv2 helper to the property field for clarity
* Add guard against nil returns on field deletion
* Add docs to the websocket endpoints
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* migrations: consolidate views migrations and reorder after master
- Merged 000165 (create Views) with 000167 (drop Icon) since Icon was never needed
- Renumbered branch migrations 159-166 → 160-167 so master's 000159 (deduplicate_policy_names) runs first
- Regenerated migrations.list
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add API endpoint to retrieve posts for a specific view (#35604)
Automatic Merge
* Apply fixes after merge
* Return a more specific error from getting multiple fields
* Prevent getting broadcast params on field deletion if not needed
* Remove duplicated migration code
* Update property conflict code to always use master
* Adds nil guard when iterating on property fields
* Check that permission level is valid before getting rejected by the database
* Validate correctness on TargetID for PSAv2 fields
* Avoid PSAv1 using permissions or protected
* Fix test data after validation change
* Fix flaky search test
* Adds more posts for filter use cases to properly test exclusions
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
Co-authored-by: Julien Tant <julien@craftyx.fr>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Julien Tant <785518+JulienTant@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 05:36:35 -04:00
SinceCreateAt bool // determines whether the cursor will be based on CreateAt or UpdateAt
ExcludeChannelMetadataSystemPosts bool // if true, exclude channel metadata system posts (header, display name, purpose changes)
ExcludedPostTypes [ ] string // post types to exclude from sync
2020-02-05 07:27:35 -05:00
}
type GetPostsOptions struct {
2021-03-23 07:31:54 -04:00
UserId string
2021-01-14 06:46:27 -05:00
ChannelId string
PostId string
Page int
PerPage int
SkipFetchThreads bool
CollapsedThreads bool
CollapsedThreadsExtended bool
2022-03-24 03:21:41 -04:00
FromPost string // PostId after which to send the items
FromCreateAt int64 // CreateAt after which to send the items
MM-56548: [AI assisted]Add support for incremental thread loading using UpdateAt timestamp (#30486)
Every time we load the RHS, we used to load the FULL thread always. Although
the actual ThreadViewer React component is virtualized, and the server side
API call is paginated, we still went through all the pages, to get the full
thread and passed it on to the ThreadViewer. This would be for first loads,
and subsequent loads of the same thread.
This was a bug originally, but then it was a necessity after we applied websocket event scope because
now we won't get emoji reactions of a thread if the user is not on the thread.
To fix that, we enhance the thread loading functionality by adding support for fetching
thread updates based on the UpdateAt timestamp. Now, for subsequent loads,
we only get the changed posts in a thread. The implementation:
- Adds new API parameters: fromUpdateAt and updatesOnly to the GetPostThread endpoint
- Updates database queries to support sorting and filtering by UpdateAt
- Implements thread state management to track the last update timestamp
- Adds client-side support to use incremental loading for improved performance
- Ensures proper validation for parameter combinations and error handling
This change enables more efficient thread loading, particularly for long threads
with frequent updates, by only fetching posts that have been updated since the
last view.
Caveats: For delta updates, the SQL query won't use the best index possible
because we have an index for (CreateAt, Id), but no index for (UpdateAt, Id).
However, from my tests, it is not as bad as it looks:
```
[loadtest] # EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM Posts WHERE Posts.DeleteAt = 0 AND Posts.RootId = 'qbr5gctu9iyg8c36hpcq6f3w8e' AND Posts.UpdateAt > 1623445795824 ORDER BY UpdateAt ASC, Id ASC LIMIT 61;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=8.31..8.31 rows=1 width=216) (actual time=0.047..0.049 rows=0 loops=1)
Buffers: shared hit=2
-> Sort (cost=8.31..8.31 rows=1 width=216) (actual time=0.044..0.045 rows=0 loops=1)
Sort Key: updateat, id
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=2
-> Index Scan using idx_posts_root_id_delete_at on posts (cost=0.28..8.30 rows=1 width=216) (actual time=0.031..0.032 rows=0 loops=1)
Index Cond: (((rootid)::text = 'qbr5gctu9iyg8c36hpcq6f3w8e'::text) AND (deleteat = 0))
Filter: (updateat > '1623445795824'::bigint)
Buffers: shared hit=2
Planning:
Buffers: shared hit=3
Planning Time: 0.508 ms
Execution Time: 0.106 ms
(14 rows)
```
We still get an index scan with index cond. Although there's a filter element, but atleast we get the whole thread with the index.
My thinking is that while the whole thread might be large, but after that, updates on a thread should be incremental.
Therefore, we should be okay without adding yet another index on the posts table.
This is just the first step in what could be potentially improved further.
1. We shouldn't even be loading the full thread always. But rather let the virtualized viewer
load more posts on demand.
2. If a post has been just reacted to, then we need not send the whole post down, but just the
reaction. This further saves bandwidth.
https://mattermost.atlassian.net/browse/MM-56548
TBD: Add load-test coverage to update the thread loading code
```release-note
NONE
```
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
2025-04-22 01:13:13 -04:00
FromUpdateAt int64 // UpdateAt after which to send the items. This cannot be used with FromCreateAt.
2022-03-24 03:21:41 -04:00
Direction string // Only accepts up|down. Indicates the order in which to send the items.
MM-56548: [AI assisted]Add support for incremental thread loading using UpdateAt timestamp (#30486)
Every time we load the RHS, we used to load the FULL thread always. Although
the actual ThreadViewer React component is virtualized, and the server side
API call is paginated, we still went through all the pages, to get the full
thread and passed it on to the ThreadViewer. This would be for first loads,
and subsequent loads of the same thread.
This was a bug originally, but then it was a necessity after we applied websocket event scope because
now we won't get emoji reactions of a thread if the user is not on the thread.
To fix that, we enhance the thread loading functionality by adding support for fetching
thread updates based on the UpdateAt timestamp. Now, for subsequent loads,
we only get the changed posts in a thread. The implementation:
- Adds new API parameters: fromUpdateAt and updatesOnly to the GetPostThread endpoint
- Updates database queries to support sorting and filtering by UpdateAt
- Implements thread state management to track the last update timestamp
- Adds client-side support to use incremental loading for improved performance
- Ensures proper validation for parameter combinations and error handling
This change enables more efficient thread loading, particularly for long threads
with frequent updates, by only fetching posts that have been updated since the
last view.
Caveats: For delta updates, the SQL query won't use the best index possible
because we have an index for (CreateAt, Id), but no index for (UpdateAt, Id).
However, from my tests, it is not as bad as it looks:
```
[loadtest] # EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM Posts WHERE Posts.DeleteAt = 0 AND Posts.RootId = 'qbr5gctu9iyg8c36hpcq6f3w8e' AND Posts.UpdateAt > 1623445795824 ORDER BY UpdateAt ASC, Id ASC LIMIT 61;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=8.31..8.31 rows=1 width=216) (actual time=0.047..0.049 rows=0 loops=1)
Buffers: shared hit=2
-> Sort (cost=8.31..8.31 rows=1 width=216) (actual time=0.044..0.045 rows=0 loops=1)
Sort Key: updateat, id
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=2
-> Index Scan using idx_posts_root_id_delete_at on posts (cost=0.28..8.30 rows=1 width=216) (actual time=0.031..0.032 rows=0 loops=1)
Index Cond: (((rootid)::text = 'qbr5gctu9iyg8c36hpcq6f3w8e'::text) AND (deleteat = 0))
Filter: (updateat > '1623445795824'::bigint)
Buffers: shared hit=2
Planning:
Buffers: shared hit=3
Planning Time: 0.508 ms
Execution Time: 0.106 ms
(14 rows)
```
We still get an index scan with index cond. Although there's a filter element, but atleast we get the whole thread with the index.
My thinking is that while the whole thread might be large, but after that, updates on a thread should be incremental.
Therefore, we should be okay without adding yet another index on the posts table.
This is just the first step in what could be potentially improved further.
1. We shouldn't even be loading the full thread always. But rather let the virtualized viewer
load more posts on demand.
2. If a post has been just reacted to, then we need not send the whole post down, but just the
reaction. This further saves bandwidth.
https://mattermost.atlassian.net/browse/MM-56548
TBD: Add load-test coverage to update the thread loading code
```release-note
NONE
```
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
2025-04-22 01:13:13 -04:00
UpdatesOnly bool // This flag is used to make the API work with the updateAt value.
2022-09-27 14:00:42 -04:00
IncludeDeleted bool
2022-11-23 14:08:21 -05:00
IncludePostPriority bool
2020-02-05 07:27:35 -05:00
}
2022-05-17 07:30:40 -04:00
type PostCountOptions struct {
// Only include posts on a specific team. "" for any team.
2022-12-27 04:04:40 -05:00
TeamId string
MustHaveFile bool
MustHaveHashtag bool
ExcludeDeleted bool
ExcludeSystemPosts bool
UsersPostsOnly bool
2022-05-31 06:55:46 -04:00
// AllowFromCache looks up cache only when ExcludeDeleted and UsersPostsOnly are true and rest are falsy.
AllowFromCache bool
MM-59966 - Compliance Export overhaul - feature branch (#29789)
* [MM-59089] Add a compliance export constant (#27919)
* add a useful constant
* i18n
* another constant
* another i18n
* [MM-60422] Add GetChannelsWithActivityDuring (#28301)
* modify GetUsersInChannelDuring to accept a slice of channelIds
* add GetChannelsWithActivityDuring
* add compliance export progress message; remove unused custom status
* linting
* tests running too fast
* add batch size config settings
* add store tests
* linting
* empty commit
* i18n changes
* fix i18n ordering
* MM-60570 - Server-side changes consolidating the export CLI with server/ent code (#28640)
* add an i18n field; add the CLI's export directory
* int64 -> int
* Add UntilUpdateAt for MessageExport and AnalyticsPostCount
to merge
* remove now-unused i18n strings
* add TranslationsPreInitFromBuffer to allow CLI to use i18n
* use GetBuilder to simplify; rename TranslationsPreInitFromFileBytes
* [MM-59089] Improve compliance export timings (#1733 - Enterprise repo)
* MM-60422 - Performance and logic fixes for Compliance Exports (#1757 - Enterprise repo)
* MM-60570 - Enterprise-side changes consolidating the export CLI with server/ent code (#1769 - Enterprise repo)
* merge conflicts; missed file from ent branch
* MM-61038 - Add an option to sqlstore.New (#28702)
remove useless comment
add test
add an option to sqlstore.New
* MM-60976: Remove RunExport command from Mattermost binary (#28805)
* remove RunExport command from mattermost binary
* remove the code it was calling
* fix i18n
* remove test (was only testing license, not functionality)
* empty commit
* fix flaky GetChannelsWithActivityDuring test
* MM-60063: Dedicated Export Filestore fix, redo of #1772 (enterprise) (#28803)
* redo filestore fix #1772 (enterprise repo) on top of MM-59966 feature
* add new e2e tests for export filestore
* golint
* ok, note to self: shadowing bad, actually (when there's a defer)
* empty commit
* MM-61137 - Message export: Support 7.8.11 era dbs (#28824)
* support 7.8.11 era dbs by wrapping the store using only what we need
* fix flaky GetChannelsWithActivityDuring test
* add a comment
* only need to define the MEFileInfoStore (the one that'll be overridden)
* blank commit
* MM-60974 - Message Export: Add performance metrics (#28836)
* support 7.8.11 era dbs by wrapping the store using only what we need
* fix flaky GetChannelsWithActivityDuring test
* add a comment
* only need to define the MEFileInfoStore (the one that'll be overridden)
* performance metrics
* cleanup unneeded named returns
* blank commit
* MM-60975 - Message export: Add startTime and endTime to export folder name (#28840)
* support 7.8.11 era dbs by wrapping the store using only what we need
* fix flaky GetChannelsWithActivityDuring test
* add a comment
* only need to define the MEFileInfoStore (the one that'll be overridden)
* performance metrics
* output startTime and endTime in export folder
* empty commit
* merge conflict
* MM-60978 - Message export: Improve xml fields; fix delete semantics (#28873)
* support 7.8.11 era dbs by wrapping the store using only what we need
* fix flaky GetChannelsWithActivityDuring test
* add a comment
* only need to define the MEFileInfoStore (the one that'll be overridden)
* performance metrics
* output startTime and endTime in export folder
* empty commit
* add xml fields, omit when empty, tests
* fix delete semantics; test (and test for update semantics)
* clarify comments
* simplify edited post detection, now there's no edge case.
* add some spacing to help fast running tests
* merge conflicts/updates needed for new deleted post semantics
* linting; fixing tests from upstream merge
* use SafeDereference
* linting
* stronger typing; better wrapped errors; better formatting
* blank commit
* goimports formatting
* fix merge mistake
* minor fixes due to changes in master
* MM-61755 - Simplifying and Support reporting to the db from the CLI (#29281)
* finally clean up JobData struct and stringMap; prep for CLI using db
* and now simplify using StringMapToJobDataWithZeroValues
* remove unused fn
* create JobDataExported; clean up errors
* MM-60176 - Message Export: Global relay cleanup (#29168)
* move global relay logic into global_relay_export
* blank commit
* blank commit
* improve errors
* MM-60693 - Refactor CSV to use same codepath as Actiance (#29191)
* move global relay logic into global_relay_export
* blank commit
* refactor (and simplify) ExportParams into shared
* blank commit
* remove unused fn
* csv now uses pre-calculated joins/leaves like actiance
* improve errors
* remove nil post check; remove ignoredPosts metric
* remove unneeded copy
* MM-61696 - Refactor GlobalRelay to use same codepath as Actiance (#29225)
* move global relay logic into global_relay_export
* blank commit
* refactor (and simplify) ExportParams into shared
* blank commit
* remove unused fn
* csv now uses pre-calculated joins/leaves like actiance
* remove newly unneeded function and its test. goodbye.
* refactor GetPostAttachments for csv + global relay to share
* refactor global_relay_export and fix tests (no changes to output)
* improve errors
* remove nil post check; remove ignoredPosts metric
* remove unneeded copy
* remove unneeded nil check
* PR comments
* MM-61715 - Generalize e2e to all export types 🤖 (#29369)
* move global relay logic into global_relay_export
* blank commit
* refactor (and simplify) ExportParams into shared
* blank commit
* remove unused fn
* csv now uses pre-calculated joins/leaves like actiance
* remove newly unneeded function and its test. goodbye.
* refactor GetPostAttachments for csv + global relay to share
* refactor global_relay_export and fix tests (no changes to output)
* improve errors
* remove nil post check; remove ignoredPosts metric
* remove unneeded copy
* remove unneeded nil check
* PR comments
* refactor isDeletedMsg for all export types
* fix start and endtime, nasty csv createAt bug; bring closer to Actiance
* align unit tests with new logic (e.g. starttime / endtime)
* refactor a TimestampConvert fn for code + tests
* bug: pass templates to global relay (hurray for e2e tests, otherwise...)
* add global relay zip to allowed list (only for tests)
* test helpers
* new templates for e2e tests
* e2e tests... phew.
* linting
* merge conflicts
* unexport PostToRow; add test helper marker
* cleanup, shortening, thanks to PR comments
* MM-61972 - Generalize export data path - Actiance (#29399)
* extract and generalize the export data generation functions
* finish moving test (bc of previous extraction)
* lift a function from common -> shared (to break an import cycle)
* actiance now takes general export data, processes it into actiance data
* bring tests in line with correct sorting rules (upadateAt, messageId)
* fixups, PR comments
* turn strings.Repeat into a more descriptive const
amended: one letter fix; bad rebase
* MM-62009 - e2e clock heisenbug (#29434)
* consolidate assertions; output debuggable diffs (keeping for future)
* refactor test output generator to generators file
* waitUntilZeroPosts + pass through until to job = fix all clock issues
* simplify messages to model.NewId(); remove unneeded waitUntilZeroPosts
* model.NewId() -> storetest.NewTestID()
* MM-61980 - Generalize export data path - CSV (#29482)
* simple refactoring
* increase sleep times for (very) rare test failures
* add extra information to the generic export for CSV
* adj Actiance to handle new generic export (no difference in its output)
* no longer need mergePosts (yay), move getJoinLeavePosts for everyone
* adjust tests for new csv semantics (detailed in summary)
* and need to add the new exported data to the export_data_tests
* rearrange csv writing to happen after data export (more logical)
* linting
* remove debug statements
* figured out what was wrong with global relay e2e test 3; solid now
* PR comments
* MM-61718 - Generalize export data path - Global Relay (#29508)
* move global relay over to using the generalized export data
* performance pass -- not much can be done
* Update server/enterprise/message_export/global_relay_export/global_relay_export.go
Co-authored-by: Claudio Costa <cstcld91@gmail.com>
---------
Co-authored-by: Claudio Costa <cstcld91@gmail.com>
* MM-62058 - Align CSV with Actiance (#29551)
* refactoring actiance files and var names for clarity
* bug found in exported attachments (we used to miss some start/ends)
* changes needed for actiance due to new generic exports
* bringing CSV up to actiance standards
* fixing global relay b/c of new semantics (adding a note on an edge case)
* aligning e2e tests, adding comments to clarify what is expected/tested
* necessary changes; 1 more test for added functionality (ignoreDeleted)
* comment style
* MM-62059 - Align Global Relay with Actiance/CSV; many fixes (#29665)
* core logic changes to general export_data and the specific export paths
* unit tests and e2e tests, covering all new edge cases and all logic
* linting
* better var naming, const value, and cleaning up functions calls
* MM-62436 - Temporarily skip cypress tests that require download link (#29772)
---------
Co-authored-by: Claudio Costa <cstcld91@gmail.com>
2025-01-10 16:56:02 -05:00
// retrieves posts in the inclusive range: [SinceUpdateAt + LastPostId, UntilUpdateAt]
SincePostID string
SinceUpdateAt int64
UntilUpdateAt int64
2022-05-17 07:30:40 -04:00
}
2015-06-15 03:53:32 -04:00
func ( o * Post ) Etag ( ) string {
return Etag ( o . Id , o . UpdateAt )
}
Relax 4k post message limit (#8478)
* MM-9661: rename POST_MESSAGE_MAX_RUNES to \0_v1
* MM-9661: s/4000/POST_MESSAGE_MAX_RUNES_V1/ in tests
* MM-9661: introduce POST_MESSAGE_MAX_RUNES_V2
* MM-9661: migrate Postgres Posts.Message column to TEXT from VARCHAR(4000)
This is safe to do in a production instance since the underyling type is
not changing. We explicitly don't do this automatically for MySQL, but
also don't need to since the ORM would have already created a TEXT column
for MySQL in that case.
* MM-9661: emit MaxPostSize in client config
This value remains unconfigurable at this time, but exposes the current
limit to the client. The limit remains at 4k in this commit.
* MM-9661: introduce and use SqlPostStore.GetMaxPostSize
Enforce a byte limitation in the database, and use 1/4 of that value as
the rune count limitation (assuming a worst case UTF-8 representation).
* move maxPostSizeCached, lastPostsCache and lastPostTimeCache out of the global context and onto the SqlPostStore
* address feedback from code review:
* ensure sqlstore unit tests are actually being run
* move global caches into SqlPostStore
* leverage sync.Once to address a race condition
* modify upgrade semantics to match new db semantics
gorp's behaviour on creating columns with a maximum length on Postgres
differs from MySQL:
* Postgres
* gorp uses TEXT for string columns without a maximum length
* gorp uses VARCHAR(N) for string columns with a maximum length of N
* MySQL
* gorp uses TEXT for string columns with a maximum length >= 256
* gorp uses VARCHAR(N) for string columns with a maximum length of N
* gorp defaults to a maximum length of 255, implying VARCHAR(255)
So the Message column has been TEXT on MySQL but VARCHAR(4000) on
Postgres. With the new, longer limits of 65535, and without changes to
gorp, the expected behaviour is TEXT on MySQL and VARCHAR(65535) on
Postgres. This commit makes the upgrade semantics match the new database
semantics.
Ideally, we'd revisit the gorp behaviour at a later time.
* allow TestMaxPostSize test cases to actually run in parallel
* default maxPostSizeCached to POST_MESSAGE_MAX_RUNES_V1 in case the once initializer panics
* fix casting error
* MM-9661: skip the schema migration for Postgres
It turns out resizing VARCHAR requires a rewrite in some versions of
Postgres, but migrating VARCHAR to TEXT does not. Given the increasing
complexity, let's defer the migration to the enduser instead.
2018-03-26 17:55:35 -04:00
func ( o * Post ) IsValid ( maxPostSize int ) * AppError {
2020-05-07 13:27:35 -04:00
if ! IsValidId ( o . Id ) {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.id.app_error" , nil , "" , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
if o . CreateAt == 0 {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.create_at.app_error" , nil , "id=" + o . Id , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
if o . UpdateAt == 0 {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.update_at.app_error" , nil , "id=" + o . Id , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
2020-05-07 13:27:35 -04:00
if ! IsValidId ( o . UserId ) {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.user_id.app_error" , nil , "" , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
2020-05-07 13:27:35 -04:00
if ! IsValidId ( o . ChannelId ) {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.channel_id.app_error" , nil , "" , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
2021-01-25 05:15:17 -05:00
if ! ( IsValidId ( o . RootId ) || o . RootId == "" ) {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.root_id.app_error" , nil , "" , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
2021-01-25 05:15:17 -05:00
if ! ( len ( o . OriginalId ) == 26 || o . OriginalId == "" ) {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.original_id.app_error" , nil , "" , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
Relax 4k post message limit (#8478)
* MM-9661: rename POST_MESSAGE_MAX_RUNES to \0_v1
* MM-9661: s/4000/POST_MESSAGE_MAX_RUNES_V1/ in tests
* MM-9661: introduce POST_MESSAGE_MAX_RUNES_V2
* MM-9661: migrate Postgres Posts.Message column to TEXT from VARCHAR(4000)
This is safe to do in a production instance since the underyling type is
not changing. We explicitly don't do this automatically for MySQL, but
also don't need to since the ORM would have already created a TEXT column
for MySQL in that case.
* MM-9661: emit MaxPostSize in client config
This value remains unconfigurable at this time, but exposes the current
limit to the client. The limit remains at 4k in this commit.
* MM-9661: introduce and use SqlPostStore.GetMaxPostSize
Enforce a byte limitation in the database, and use 1/4 of that value as
the rune count limitation (assuming a worst case UTF-8 representation).
* move maxPostSizeCached, lastPostsCache and lastPostTimeCache out of the global context and onto the SqlPostStore
* address feedback from code review:
* ensure sqlstore unit tests are actually being run
* move global caches into SqlPostStore
* leverage sync.Once to address a race condition
* modify upgrade semantics to match new db semantics
gorp's behaviour on creating columns with a maximum length on Postgres
differs from MySQL:
* Postgres
* gorp uses TEXT for string columns without a maximum length
* gorp uses VARCHAR(N) for string columns with a maximum length of N
* MySQL
* gorp uses TEXT for string columns with a maximum length >= 256
* gorp uses VARCHAR(N) for string columns with a maximum length of N
* gorp defaults to a maximum length of 255, implying VARCHAR(255)
So the Message column has been TEXT on MySQL but VARCHAR(4000) on
Postgres. With the new, longer limits of 65535, and without changes to
gorp, the expected behaviour is TEXT on MySQL and VARCHAR(65535) on
Postgres. This commit makes the upgrade semantics match the new database
semantics.
Ideally, we'd revisit the gorp behaviour at a later time.
* allow TestMaxPostSize test cases to actually run in parallel
* default maxPostSizeCached to POST_MESSAGE_MAX_RUNES_V1 in case the once initializer panics
* fix casting error
* MM-9661: skip the schema migration for Postgres
It turns out resizing VARCHAR requires a rewrite in some versions of
Postgres, but migrating VARCHAR to TEXT does not. Given the increasing
complexity, let's defer the migration to the enduser instead.
2018-03-26 17:55:35 -04:00
if utf8 . RuneCountInString ( o . Message ) > maxPostSize {
2024-12-10 08:32:13 -05:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.message_length.app_error" ,
map [ string ] any { "Length" : utf8 . RuneCountInString ( o . Message ) , "MaxLength" : maxPostSize } , "id=" + o . Id , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
2021-07-12 14:05:36 -04:00
if utf8 . RuneCountInString ( o . Hashtags ) > PostHashtagsMaxRunes {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.hashtags.app_error" , nil , "id=" + o . Id , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
2017-12-08 14:15:33 -05:00
switch o . Type {
case
2021-07-12 14:05:36 -04:00
PostTypeDefault ,
PostTypeSystemGeneric ,
PostTypeJoinLeave ,
PostTypeAutoResponder ,
PostTypeAddRemove ,
PostTypeJoinChannel ,
PostTypeGuestJoinChannel ,
PostTypeLeaveChannel ,
PostTypeJoinTeam ,
PostTypeLeaveTeam ,
PostTypeAddToChannel ,
PostTypeAddGuestToChannel ,
PostTypeRemoveFromChannel ,
PostTypeMoveChannel ,
PostTypeAddToTeam ,
PostTypeRemoveFromTeam ,
2026-03-10 11:37:21 -04:00
PostTypeMessageAttachment ,
2021-07-12 14:05:36 -04:00
PostTypeHeaderChange ,
PostTypePurposeChange ,
PostTypeDisplaynameChange ,
PostTypeConvertChannel ,
PostTypeChannelDeleted ,
PostTypeChannelRestored ,
PostTypeChangeChannelPrivacy ,
PostTypeAddBotTeamsChannels ,
2023-02-10 11:11:57 -05:00
PostTypeReminder ,
2023-09-19 08:41:34 -04:00
PostTypeMe ,
2023-12-11 15:27:34 -05:00
PostTypeWrangler ,
2025-12-11 01:59:50 -05:00
PostTypeGMConvertedToChannel ,
2026-02-06 12:19:06 -05:00
PostTypeAutotranslationChange ,
Merge the Integrated Boards MVP feature branch (#35796)
* Add CreatedBy and UpdatedBy to the properties fields and values (#34485)
* Add CreatedBy and UpdatedBy to the properties fields and values
* Fix types
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds ObjectType to the property fields table (#34908)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Update ObjectType migration setting an empty value and marking the column as not null (#34915)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds uniqueness mechanisms to the property fields (#35058)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Fixing retrylayer mocks
* Remove retrylayer duplication
* Address review comments
* Fix comment to avoid linter issues
* Address PR comments
* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.down.sql
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.up.sql
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Update server/channels/db/migrations/postgres/000157_add_object_type_to_property_fields.up.sql
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Update field validation to check only for valid target types
* Update migrations to avoid concurrent index creation within a transaction
* Update migrations to make all index ops concurrent
* Update tests to use valid PSAv2 property fields
* Adds a helper for valid PSAv2 TargetTypes
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Fix property tests (#35388)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds Integrated Boards feature flag (#35378)
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds Integrated Boards MVP API changes (#34822)
This PR includes the necessary changes for channels and posts
endpoints and adds a set of generic endpoints to retrieve and manage
property fields and values following the new Property System approach.
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Mattermost Build <build@mattermost.com>
* Property System Architecture permissions for v2 (#35113)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Add the permissions to the migrations, model and update the store calls
* Adds the property field and property group app layer
* Adds authorization helpers for property fields and values
* Make sure that users cannot lock themselves out of property fields
* Migrate permissions from a JSON column to three normalized columns
* Remove the audit comment
* Use target level constants in authorization
* Log authorization membership failures
* Rename admin to sysadmin
* Fix i18n sorting
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Add Views store and app layer (#35361)
* Add Views store and app layer for Integrated Boards
Implements the View entity (model, SQL store, service, app) as described
in the Integrated Boards tech spec. Views are channel-scoped board
configurations with typed props (board, kanban subviews) and soft-delete.
- public/model: View, ViewBoardProps, Subview, ViewPatch types with
PreSave/PreUpdate/IsValid/Patch/Clone/Auditable
- Migration 158: Views table with jsonb Props column and indexes
- SqlViewStore: CRUD with nil-safe Props marshaling (AppendBinaryFlag)
- ViewService: CreateView seeds default kanban subview and links the
boards property field; caches boardPropertyFieldID at startup
- App layer: CreateView/GetView/GetViewsForChannel/UpdateView/DeleteView
with channel-membership permission checks and WebSocket events
(view_created, view_updated, view_deleted)
- doSetupBoardsPropertyField: registers the Boards property group and
board field in NewServer() before ViewService construction
- GetFieldByName now returns store.ErrNotFound instead of raw sql.ErrNoRows
* Move permission checks out of App layer for views
- Remove HasPermissionToChannel calls from all App view methods
- Drop userID params from GetView, GetViewsForChannel, UpdateView, DeleteView
- Fix doSetupBoardsPropertyField to include required TargetType for PSAv2 field
* Make View service generic and enforce board validation in model
- Remove board-specific auto-setup from service and server startup
- Enforce that board views require Props, at least one subview, and at least one linked property in IsValid()
- Move default subview seeding out of app layer; callers must provide valid props
- Call PreSave on subviews during PreUpdate to assign IDs to new subviews
- Update all tests to reflect the new validation requirements
* Restore migrations files to match base branch
* Distinguish ErrNotFound from other errors in view store Get
* Use CONCURRENTLY and nontransactional for index operations in views migration
* Split views index creation into separate nontransactional migrations
* Update migrations.list
* Update i18n translations for views
* Fix makeView helper to include required Props for board view validation
* Rename ctx parameter from c to rctx in OAuthProvider mock
* Remove views service layer, call store directly from app
* Return 500 for unexpected DB errors in GetView, 404 only for not-found
* Harden View model: deep-copy Props, validate linked property IDs
- Add ViewBoardProps.Clone() to deep-copy LinkedProperties and Subviews
- Use it in View.Clone() and View.Patch() to prevent shared-slice aliasing
- Iterate over LinkedProperties in View.IsValid() and reject invalid IDs
with a dedicated i18n key
- Register ViewStore in storetest AssertExpectations so mock expectations
are enforced
- Add tests covering all new behaviours
* Restore autotranslation worker_stopped i18n translation
* Fix view store test IDs and improve error handling in app layer
- Use model.NewId() for linked property IDs in testUpdateView to fix
validation failure (IsValid rejects non-UUID strings)
- Fix import grouping in app/view.go (stdlib imports in one block)
- Return 404 instead of 500 when Update/Delete store calls return
ErrNotFound (e.g. concurrent deletion TOCTOU race)
* Add View store mock to retrylayer test genStore helper
The View store was added to the store interface but the genStore()
helper in retrylayer_test.go was not updated, causing TestRetry to panic.
Also removes the duplicate Recap mock registration.
* Refactor view deletion and websocket event handling; update SQL store methods to use query builder
* revert property field store
* Remove useless migrations
* Add cursor-based pagination to View store GetForChannel
- Add ViewQueryCursor and ViewQueryOpts types with validation
- Return (views, cursor, error) for caller-driven pagination
- PerPage clamping: <=0 defaults to 20, >200 clamps to 200
- Support IncludeDeleted filter
- Add comprehensive store tests for pagination, cursor edge cases,
PerPage clamping, and invalid input rejection
- Add app layer test for empty channelID → 400
- Update interface, retrylayer, timerlayer, and mock signatures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refactor test loops in ViewStore tests for improved readability
* change pagination to limit/offset
* Add upper-bound limits on View Subviews and LinkedProperties
Defense-in-depth validation: cap Subviews at 50 and LinkedProperties
at 500 to prevent abuse below the 300KB payload limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* MM-67388, MM-66528, MM-67750: Add View REST API endpoints, websocket events, and sort order (#35442)
* Add Views store and app layer for Integrated Boards
Implements the View entity (model, SQL store, service, app) as described
in the Integrated Boards tech spec. Views are channel-scoped board
configurations with typed props (board, kanban subviews) and soft-delete.
- public/model: View, ViewBoardProps, Subview, ViewPatch types with
PreSave/PreUpdate/IsValid/Patch/Clone/Auditable
- Migration 158: Views table with jsonb Props column and indexes
- SqlViewStore: CRUD with nil-safe Props marshaling (AppendBinaryFlag)
- ViewService: CreateView seeds default kanban subview and links the
boards property field; caches boardPropertyFieldID at startup
- App layer: CreateView/GetView/GetViewsForChannel/UpdateView/DeleteView
with channel-membership permission checks and WebSocket events
(view_created, view_updated, view_deleted)
- doSetupBoardsPropertyField: registers the Boards property group and
board field in NewServer() before ViewService construction
- GetFieldByName now returns store.ErrNotFound instead of raw sql.ErrNoRows
* Move permission checks out of App layer for views
- Remove HasPermissionToChannel calls from all App view methods
- Drop userID params from GetView, GetViewsForChannel, UpdateView, DeleteView
- Fix doSetupBoardsPropertyField to include required TargetType for PSAv2 field
* Make View service generic and enforce board validation in model
- Remove board-specific auto-setup from service and server startup
- Enforce that board views require Props, at least one subview, and at least one linked property in IsValid()
- Move default subview seeding out of app layer; callers must provide valid props
- Call PreSave on subviews during PreUpdate to assign IDs to new subviews
- Update all tests to reflect the new validation requirements
* Restore migrations files to match base branch
* Distinguish ErrNotFound from other errors in view store Get
* Use CONCURRENTLY and nontransactional for index operations in views migration
* Split views index creation into separate nontransactional migrations
* Update migrations.list
* Update i18n translations for views
* Fix makeView helper to include required Props for board view validation
* Rename ctx parameter from c to rctx in OAuthProvider mock
* Remove views service layer, call store directly from app
* Return 500 for unexpected DB errors in GetView, 404 only for not-found
* Harden View model: deep-copy Props, validate linked property IDs
- Add ViewBoardProps.Clone() to deep-copy LinkedProperties and Subviews
- Use it in View.Clone() and View.Patch() to prevent shared-slice aliasing
- Iterate over LinkedProperties in View.IsValid() and reject invalid IDs
with a dedicated i18n key
- Register ViewStore in storetest AssertExpectations so mock expectations
are enforced
- Add tests covering all new behaviours
* Restore autotranslation worker_stopped i18n translation
* Fix view store test IDs and improve error handling in app layer
- Use model.NewId() for linked property IDs in testUpdateView to fix
validation failure (IsValid rejects non-UUID strings)
- Fix import grouping in app/view.go (stdlib imports in one block)
- Return 404 instead of 500 when Update/Delete store calls return
ErrNotFound (e.g. concurrent deletion TOCTOU race)
* Add View store mock to retrylayer test genStore helper
The View store was added to the store interface but the genStore()
helper in retrylayer_test.go was not updated, causing TestRetry to panic.
Also removes the duplicate Recap mock registration.
* Refactor view deletion and websocket event handling; update SQL store methods to use query builder
* revert property field store
* Add View API endpoints with OpenAPI spec, client methods, and i18n
Implement REST API for channel views (board-type) behind the
IntegratedBoards feature flag. Adds CRUD endpoints under
/api/v4/channels/{channel_id}/views with permission checks
matching the channel bookmark pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove useless migrations
* Add cursor-based pagination to View store GetForChannel
- Add ViewQueryCursor and ViewQueryOpts types with validation
- Return (views, cursor, error) for caller-driven pagination
- PerPage clamping: <=0 defaults to 20, >200 clamps to 200
- Support IncludeDeleted filter
- Add comprehensive store tests for pagination, cursor edge cases,
PerPage clamping, and invalid input rejection
- Add app layer test for empty channelID → 400
- Update interface, retrylayer, timerlayer, and mock signatures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add cursor-based pagination to View API for channel views
* Enhance cursor handling in getViewsForChannel and update tests for pagination
* Refactor test loops in ViewStore tests for improved readability
* Refactor loop in TestGetViewsForChannel for improved readability
* change pagination to limit/offset
* switch to limit/offset pagination
* Add upper-bound limits on View Subviews and LinkedProperties
Defense-in-depth validation: cap Subviews at 50 and LinkedProperties
at 500 to prevent abuse below the 300KB payload limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add view sort order API endpoint
Add POST /api/v4/channels/{channel_id}/views/{view_id}/sort_order
endpoint following the channel bookmarks reorder pattern. Includes
store, app, and API layers with full test coverage at each layer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add connectionId to view WebSocket events and sort_order API spec
Thread connectionId from request header through all view handlers
(create, update, delete, sort_order) to WebSocket events, matching
the channel bookmarks pattern. Add sort_order endpoint to OpenAPI
spec. Update minimum server version to 11.6.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove duplicate View/ViewPatch definitions from definitions.yaml
The merge from integrated-boards-mvp introduced duplicate View and
ViewPatch schema definitions that were already defined earlier in
the file with more detail (including ViewBoardProps ref and enums).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update minimum server version to 11.6 in views API spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add missing translations for view sort order error messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Merge integrated-boards-mvp into ibmvp_api-views; remove spec files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix flaky TestViewStore timestamp test on CI
Add sleep before UpdateSortOrder to ensure timestamps differ,
preventing same-millisecond comparisons on fast CI machines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* remove duplicate views.yaml imclude
* Use c.boolString() for include_deleted query param in GetViewsForChannel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix views.yaml sort order schema: use integer type and require body
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refactor view sort order tests to use named IDs instead of array indices
Extract idA/idB/idC from views slice and add BEFORE/AFTER comments
to make stateful subtest ordering easier to follow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Return 404 instead of 403 for view operations on deleted channels
Deleted channels should appear non-existent to callers rather than
revealing their existence via a 403. Detailed error text explains
the context for debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add missing channel deleteat checks
* Use c.Params.Page instead of manual page query param parsing in getViewsForChannel
c.Params already validates and defaults page/per_page, so the manual
parsing was redundant.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add support for total count in views retrieval
* Add tests for handling deleted views in GetViewsForChannel and GetView
* Short-circuit negative newIndex in UpdateSortOrder before opening transaction
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add per-channel limit on views to bound UpdateSortOrder cost
Without a cap, unbounded view creation makes sort-order updates
increasingly expensive (CASE WHEN per view, row locks). Adds
MaxViewsPerChannel=50 constant and enforces it in the app layer
before saving. Includes API and app layer tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove include_deleted support from views API
Soft-deleted views are structural metadata with low risk, but no other
similar endpoint (e.g. channel bookmarks) exposes deleted records without
an admin gate. Rather than adding an admin-only permission check for
consistency, remove the feature entirely since there is no current use case.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update view permissions to require `create_post` instead of channel management permissions
* Remove obsolete view management error messages for direct and group messages
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(migrations): add user tracking and object type to property fields
- Introduced user tracking columns (CreatedBy, UpdatedBy) to PropertyFields and PropertyValues.
- Added ObjectType column to PropertyFields with associated unique indexes for legacy and typed properties.
- Created new migration scripts for adding and dropping these features, including necessary indexes for data integrity.
- Established views for managing property fields with new attributes.
This update enhances the schema to support better tracking and categorization of property fields.
* Add Property System Architecture v2 API endpoints (#35583)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Add the permissions to the migrations, model and update the store calls
* Adds the property field and property group app layer
* Adds authorization helpers for property fields and values
* Make sure that users cannot lock themselves out of property fields
* Migrate permissions from a JSON column to three normalized columns
* Remove the audit comment
* Use target level constants in authorization
* Log authorization membership failures
* Rename admin to sysadmin
* Adds the Property System Architecture v2 API endpoints
* Adds permission checks to the create field endpoint
* Add target access checks to value endpoints
* Add default branches for object_type and target_type and extra guards for cursor client4 methods
* Fix vet API mismatch
* Fix error checks
* Fix linter
* Add merge semantics for property patch logic and API endpoint
* Fix i18n
* Fix duplicated patch elements and early return on bad cursor
* Update docs to use enums
* Fix i18n sorting
* Update app layer to return model.AppError
* Adds a limit to the number of property values that can be patched in the same request
* Require target_type filter when searching property fields
* Add objectType validation as part of field.IsValid()
* Fix linter
* Fix test with bad objecttpye
* Fix test grouping
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* MM-67968: Flatten view model — remove icon, subviews, typed board props (#35726)
* feat(views): flatten view model by removing icon, subview, and board props
Simplifies the View data model as part of MM-67968: removes Icon, Subview,
and ViewBoardProps types; renames ViewTypeBoard to ViewTypeKanban; replaces
typed Props with StringInterface (map[string]any); adds migration 000167
to drop the Icon column from the Views table.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(api): update views OpenAPI spec to reflect flattened model
Removes ViewBoardProps, Subview, and icon from the View and ViewPatch
schemas. Changes type enum from board to kanban. Replaces typed props
with a free-form StringInterface object. Aligns with MM-67968.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* refactor(views): simplify store by dropping dbView and marshalViewProps
StringInterface already implements driver.Valuer and sql.Scanner, so the
manual JSON marshal/unmarshal and the dbView intermediate struct were
redundant. model.View now scans directly from the database. Also removes
the dead ViewMaxLinkedProperties constant and wraps the Commit() error in
UpdateSortOrder.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(api): allow arbitrary JSON in view props OpenAPI schema
The props field was restricted to string values via
additionalProperties: { type: string }, conflicting with the Go model's
StringInterface (map[string]any). Changed to additionalProperties: true
in View, ViewPatch, and inline POST schemas.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* Adds basic implementation of the generic redux store for PSAv2 (#35512)
* Adds basic implementation of the generic redux store for PSAv2
* Add created_by and updated_by to the test fixtures
* Make target_id, target_type and object_type mandatory
* Wrap getPropertyFieldsByIds and getPropertyValuesForTargetByFieldIds with createSelector
* Address PR comments
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* Adds websocket messages for the PSAv2 API events (#35696)
* Adds uniqueness mechanisms to the property fields
After adding ObjectType, this commit ensures that both the PSAv1 and
PSAv2 schemas are supported, and enforces property uniqueness through
both database indexes and a logical check when creating new property
fields.
* Adds uniqueness check to property updates
Updates are covered on this commit and we refactor as well the SQL
code to use the squirrel builder and work better with the conditional
addition of the `existingID` piece of the query.
* Add translations to error messages
* Add the permissions to the migrations, model and update the store calls
* Adds the property field and property group app layer
* Adds authorization helpers for property fields and values
* Make sure that users cannot lock themselves out of property fields
* Migrate permissions from a JSON column to three normalized columns
* Remove the audit comment
* Use target level constants in authorization
* Log authorization membership failures
* Rename admin to sysadmin
* Adds the Property System Architecture v2 API endpoints
* Adds permission checks to the create field endpoint
* Add target access checks to value endpoints
* Add default branches for object_type and target_type and extra guards for cursor client4 methods
* Fix vet API mismatch
* Fix error checks
* Fix linter
* Add merge semantics for property patch logic and API endpoint
* Fix i18n
* Fix duplicated patch elements and early return on bad cursor
* Update docs to use enums
* Fix i18n sorting
* Update app layer to return model.AppError
* Adds a limit to the number of property values that can be patched in the same request
* Adds websocket messages for the PSAv2 API events
* Add IsPSAv2 helper to the property field for clarity
* Add guard against nil returns on field deletion
* Add docs to the websocket endpoints
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
* migrations: consolidate views migrations and reorder after master
- Merged 000165 (create Views) with 000167 (drop Icon) since Icon was never needed
- Renumbered branch migrations 159-166 → 160-167 so master's 000159 (deduplicate_policy_names) runs first
- Regenerated migrations.list
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add API endpoint to retrieve posts for a specific view (#35604)
Automatic Merge
* Apply fixes after merge
* Return a more specific error from getting multiple fields
* Prevent getting broadcast params on field deletion if not needed
* Remove duplicated migration code
* Update property conflict code to always use master
* Adds nil guard when iterating on property fields
* Check that permission level is valid before getting rejected by the database
* Validate correctness on TargetID for PSAv2 fields
* Avoid PSAv1 using permissions or protected
* Fix test data after validation change
* Fix flaky search test
* Adds more posts for filter use cases to properly test exclusions
---------
Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
Co-authored-by: Julien Tant <julien@craftyx.fr>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Julien Tant <785518+JulienTant@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 05:36:35 -04:00
PostTypeBurnOnRead ,
PostTypeCard :
2017-12-08 14:15:33 -05:00
default :
2021-07-12 14:05:36 -04:00
if ! strings . HasPrefix ( o . Type , PostCustomTypePrefix ) {
2017-12-08 14:15:33 -05:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.type.app_error" , nil , "id=" + o . Type , http . StatusBadRequest )
}
2015-06-15 03:53:32 -04:00
}
2021-09-01 08:43:12 -04:00
if utf8 . RuneCountInString ( ArrayToJSON ( o . Filenames ) ) > PostFilenamesMaxRunes {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.filenames.app_error" , nil , "id=" + o . Id , http . StatusBadRequest )
2015-06-15 03:53:32 -04:00
}
2021-09-01 08:43:12 -04:00
if utf8 . RuneCountInString ( ArrayToJSON ( o . FileIds ) ) > PostFileidsMaxRunes {
2021-02-10 14:19:05 -05:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.file_ids.app_error" , nil , "id=" + o . Id , http . StatusBadRequest )
2020-12-14 03:30:02 -05:00
}
2021-09-01 08:43:12 -04:00
if utf8 . RuneCountInString ( StringInterfaceToJSON ( o . GetProps ( ) ) ) > PostPropsMaxRunes {
2017-09-15 08:32:11 -04:00
return NewAppError ( "Post.IsValid" , "model.post.is_valid.props.app_error" , nil , "id=" + o . Id , http . StatusBadRequest )
2015-12-03 11:57:20 -05:00
}
2015-06-15 03:53:32 -04:00
return nil
}
2017-10-20 13:38:26 -04:00
func ( o * Post ) SanitizeProps ( ) {
2021-06-23 07:55:12 -04:00
if o == nil {
return
}
2017-10-20 13:38:26 -04:00
membersToSanitize := [ ] string {
2021-07-12 14:05:36 -04:00
PropsAddChannelMember ,
2024-11-08 07:57:06 -05:00
PostPropsForceNotification ,
2017-10-20 13:38:26 -04:00
}
for _ , member := range membersToSanitize {
2020-03-13 16:12:20 -04:00
if _ , ok := o . GetProps ( ) [ member ] ; ok {
o . DelProp ( member )
2017-10-20 13:38:26 -04:00
}
}
2021-01-14 06:46:27 -05:00
for _ , p := range o . Participants {
p . Sanitize ( map [ string ] bool { } )
}
2017-10-20 13:38:26 -04:00
}
2024-05-23 08:29:19 -04:00
// Remove any input data from the post object that is not user controlled
func ( o * Post ) SanitizeInput ( ) {
o . DeleteAt = 0
2024-08-05 23:45:00 -04:00
o . RemoteId = NewPointer ( "" )
2024-08-06 05:47:31 -04:00
if o . Metadata != nil {
o . Metadata . Embeds = nil
}
2024-05-23 08:29:19 -04:00
}
2023-10-03 10:51:07 -04:00
func ( o * Post ) ContainsIntegrationsReservedProps ( ) [ ] string {
2024-11-04 01:09:35 -05:00
return ContainsIntegrationsReservedProps ( o . GetProps ( ) )
2023-10-03 10:51:07 -04:00
}
func ( o * PostPatch ) ContainsIntegrationsReservedProps ( ) [ ] string {
if o == nil || o . Props == nil {
return nil
}
2024-11-04 01:09:35 -05:00
return ContainsIntegrationsReservedProps ( * o . Props )
2023-10-03 10:51:07 -04:00
}
2024-11-04 01:09:35 -05:00
func ContainsIntegrationsReservedProps ( props StringInterface ) [ ] string {
2023-10-03 10:51:07 -04:00
foundProps := [ ] string { }
if props != nil {
reservedProps := [ ] string {
PostPropsFromWebhook ,
PostPropsOverrideUsername ,
PostPropsWebhookDisplayName ,
PostPropsOverrideIconURL ,
PostPropsOverrideIconEmoji ,
}
for _ , key := range reservedProps {
if _ , ok := props [ key ] ; ok {
foundProps = append ( foundProps , key )
}
}
}
return foundProps
}
2015-06-15 03:53:32 -04:00
func ( o * Post ) PreSave ( ) {
if o . Id == "" {
o . Id = NewId ( )
}
o . OriginalId = ""
2015-10-02 06:16:27 -04:00
if o . CreateAt == 0 {
o . CreateAt = GetMillis ( )
}
2015-06-15 03:53:32 -04:00
o . UpdateAt = o . CreateAt
2017-09-19 17:05:20 -04:00
o . PreCommit ( )
}
2015-06-15 03:53:32 -04:00
2017-09-19 17:05:20 -04:00
func ( o * Post ) PreCommit ( ) {
2020-03-13 16:12:20 -04:00
if o . GetProps ( ) == nil {
2022-07-05 02:46:50 -04:00
o . SetProps ( make ( map [ string ] any ) )
2015-06-15 03:53:32 -04:00
}
if o . Filenames == nil {
o . Filenames = [ ] string { }
}
2016-09-30 11:06:30 -04:00
if o . FileIds == nil {
o . FileIds = [ ] string { }
}
2017-09-19 17:05:20 -04:00
o . GenerateActionIds ( )
2019-01-22 15:58:22 -05:00
// There's a rare bug where the client sends up duplicate FileIds so protect against that
o . FileIds = RemoveDuplicateStrings ( o . FileIds )
2015-06-15 03:53:32 -04:00
}
func ( o * Post ) MakeNonNil ( ) {
2020-03-13 16:12:20 -04:00
if o . GetProps ( ) == nil {
2022-07-05 02:46:50 -04:00
o . SetProps ( make ( map [ string ] any ) )
2020-03-13 16:12:20 -04:00
}
}
func ( o * Post ) DelProp ( key string ) {
o . propsMu . Lock ( )
defer o . propsMu . Unlock ( )
2022-07-05 02:46:50 -04:00
propsCopy := make ( map [ string ] any , len ( o . Props ) - 1 )
2025-07-18 06:54:51 -04:00
maps . Copy ( propsCopy , o . Props )
2020-03-13 16:12:20 -04:00
delete ( propsCopy , key )
o . Props = propsCopy
2015-06-15 03:53:32 -04:00
}
2022-07-05 02:46:50 -04:00
func ( o * Post ) AddProp ( key string , value any ) {
2020-03-13 16:12:20 -04:00
o . propsMu . Lock ( )
defer o . propsMu . Unlock ( )
2022-07-05 02:46:50 -04:00
propsCopy := make ( map [ string ] any , len ( o . Props ) + 1 )
2025-07-18 06:54:51 -04:00
maps . Copy ( propsCopy , o . Props )
2020-03-13 16:12:20 -04:00
propsCopy [ key ] = value
o . Props = propsCopy
}
2015-06-15 03:53:32 -04:00
2020-03-13 16:12:20 -04:00
func ( o * Post ) GetProps ( ) StringInterface {
o . propsMu . RLock ( )
defer o . propsMu . RUnlock ( )
return o . Props
}
func ( o * Post ) SetProps ( props StringInterface ) {
o . propsMu . Lock ( )
defer o . propsMu . Unlock ( )
o . Props = props
}
2015-06-15 03:53:32 -04:00
2022-07-05 02:46:50 -04:00
func ( o * Post ) GetProp ( key string ) any {
2020-03-13 16:12:20 -04:00
o . propsMu . RLock ( )
defer o . propsMu . RUnlock ( )
return o . Props [ key ]
2015-06-15 03:53:32 -04:00
}
2015-08-26 12:49:07 -04:00
2025-03-20 07:53:50 -04:00
// ValidateProps checks all known props for validity.
// Currently, it logs warnings for invalid props rather than returning an error.
// In a future version, this will be updated to return errors for invalid props.
func ( o * Post ) ValidateProps ( logger mlog . LoggerIFace ) {
if err := o . propsIsValid ( ) ; err != nil {
logger . Warn (
"Invalid post props. In a future version this will result in an error. Please update your integration to be compliant." ,
mlog . String ( "post_id" , o . Id ) ,
mlog . Err ( err ) ,
)
}
}
func ( o * Post ) propsIsValid ( ) error {
var multiErr * multierror . Error
props := o . GetProps ( )
// Check basic props validity
if props == nil {
return nil
}
if props [ PostPropsAddedUserId ] != nil {
if addedUserID , ok := props [ PostPropsAddedUserId ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "added_user_id prop must be a string" ) )
} else if ! IsValidId ( addedUserID ) {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "added_user_id prop must be a valid user ID" ) )
}
}
if props [ PostPropsDeleteBy ] != nil {
if deleteByID , ok := props [ PostPropsDeleteBy ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "delete_by prop must be a string" ) )
} else if ! IsValidId ( deleteByID ) {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "delete_by prop must be a valid user ID" ) )
}
}
// Validate integration props
if props [ PostPropsOverrideIconURL ] != nil {
if iconURL , ok := props [ PostPropsOverrideIconURL ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "override_icon_url prop must be a string" ) )
} else if iconURL == "" || ! IsValidHTTPURL ( iconURL ) {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "override_icon_url prop must be a valid URL" ) )
}
}
if props [ PostPropsOverrideIconEmoji ] != nil {
if _ , ok := props [ PostPropsOverrideIconEmoji ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "override_icon_emoji prop must be a string" ) )
}
}
if props [ PostPropsOverrideUsername ] != nil {
if _ , ok := props [ PostPropsOverrideUsername ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "override_username prop must be a string" ) )
}
}
if props [ PostPropsFromWebhook ] != nil {
if fromWebhook , ok := props [ PostPropsFromWebhook ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "from_webhook prop must be a string" ) )
} else if fromWebhook != "true" {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "from_webhook prop must be \"true\"" ) )
}
}
if props [ PostPropsFromBot ] != nil {
if fromBot , ok := props [ PostPropsFromBot ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "from_bot prop must be a string" ) )
} else if fromBot != "true" {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "from_bot prop must be \"true\"" ) )
}
}
if props [ PostPropsFromOAuthApp ] != nil {
if fromOAuthApp , ok := props [ PostPropsFromOAuthApp ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "from_oauth_app prop must be a string" ) )
} else if fromOAuthApp != "true" {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "from_oauth_app prop must be \"true\"" ) )
}
}
if props [ PostPropsFromPlugin ] != nil {
if fromPlugin , ok := props [ PostPropsFromPlugin ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "from_plugin prop must be a string" ) )
} else if fromPlugin != "true" {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "from_plugin prop must be \"true\"" ) )
}
}
if props [ PostPropsUnsafeLinks ] != nil {
if unsafeLinks , ok := props [ PostPropsUnsafeLinks ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "unsafe_links prop must be a string" ) )
} else if unsafeLinks != "true" {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "unsafe_links prop must be \"true\"" ) )
}
}
if props [ PostPropsWebhookDisplayName ] != nil {
if _ , ok := props [ PostPropsWebhookDisplayName ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "webhook_display_name prop must be a string" ) )
}
}
if props [ PostPropsMentionHighlightDisabled ] != nil {
if _ , ok := props [ PostPropsMentionHighlightDisabled ] . ( bool ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "mention_highlight_disabled prop must be a boolean" ) )
}
}
if props [ PostPropsGroupHighlightDisabled ] != nil {
if _ , ok := props [ PostPropsGroupHighlightDisabled ] . ( bool ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "disable_group_highlight prop must be a boolean" ) )
}
}
if props [ PostPropsPreviewedPost ] != nil {
if previewedPostID , ok := props [ PostPropsPreviewedPost ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "previewed_post prop must be a string" ) )
} else if ! IsValidId ( previewedPostID ) {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "previewed_post prop must be a valid post ID" ) )
}
}
if props [ PostPropsForceNotification ] != nil {
if _ , ok := props [ PostPropsForceNotification ] . ( bool ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "force_notification prop must be a boolean" ) )
}
}
2025-11-10 16:32:18 -05:00
if props [ PostPropsAIGeneratedByUserID ] != nil {
if aiGenUserID , ok := props [ PostPropsAIGeneratedByUserID ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "ai_generated_by prop must be a string" ) )
} else if ! IsValidId ( aiGenUserID ) {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "ai_generated_by prop must be a valid user ID" ) )
}
}
if props [ PostPropsAIGeneratedByUsername ] != nil {
if _ , ok := props [ PostPropsAIGeneratedByUsername ] . ( string ) ; ! ok {
multiErr = multierror . Append ( multiErr , fmt . Errorf ( "ai_generated_by_username prop must be a string" ) )
}
}
2025-03-20 07:53:50 -04:00
for i , a := range o . Attachments ( ) {
if err := a . IsValid ( ) ; err != nil {
multiErr = multierror . Append ( multiErr , multierror . Prefix ( err , fmt . Sprintf ( "message attachtment at index %d is invalid:" , i ) ) )
}
}
return multiErr . ErrorOrNil ( )
}
2015-12-03 20:07:47 -05:00
func ( o * Post ) IsSystemMessage ( ) bool {
2021-07-12 14:05:36 -04:00
return len ( o . Type ) >= len ( PostSystemMessagePrefix ) && o . Type [ : len ( PostSystemMessagePrefix ) ] == PostSystemMessagePrefix
2015-12-03 20:07:47 -05:00
}
2017-03-29 11:06:51 -04:00
2021-04-01 13:44:56 -04:00
// IsRemote returns true if the post originated on a remote cluster.
func ( o * Post ) IsRemote ( ) bool {
return o . RemoteId != nil && * o . RemoteId != ""
}
2021-05-20 12:07:40 -04:00
// GetRemoteID safely returns the remoteID or empty string if not remote.
func ( o * Post ) GetRemoteID ( ) string {
if o . RemoteId != nil {
return * o . RemoteId
}
return ""
}
2019-12-03 14:51:50 -05:00
func ( o * Post ) IsJoinLeaveMessage ( ) bool {
2021-07-12 14:05:36 -04:00
return o . Type == PostTypeJoinLeave ||
o . Type == PostTypeAddRemove ||
o . Type == PostTypeJoinChannel ||
o . Type == PostTypeLeaveChannel ||
o . Type == PostTypeJoinTeam ||
o . Type == PostTypeLeaveTeam ||
o . Type == PostTypeAddToChannel ||
o . Type == PostTypeRemoveFromChannel ||
o . Type == PostTypeAddToTeam ||
o . Type == PostTypeRemoveFromTeam
2019-12-03 14:51:50 -05:00
}
2020-01-07 04:47:03 -05:00
func ( o * Post ) Patch ( patch * PostPatch ) {
2017-03-29 11:06:51 -04:00
if patch . IsPinned != nil {
2020-01-07 04:47:03 -05:00
o . IsPinned = * patch . IsPinned
2017-03-29 11:06:51 -04:00
}
if patch . Message != nil {
2020-01-07 04:47:03 -05:00
o . Message = * patch . Message
2017-03-29 11:06:51 -04:00
}
if patch . Props != nil {
2020-03-13 16:12:20 -04:00
newProps := * patch . Props
o . SetProps ( newProps )
2017-03-29 11:06:51 -04:00
}
if patch . FileIds != nil {
2020-01-07 04:47:03 -05:00
o . FileIds = * patch . FileIds
2017-03-29 11:06:51 -04:00
}
if patch . HasReactions != nil {
2020-01-07 04:47:03 -05:00
o . HasReactions = * patch . HasReactions
2017-03-29 11:06:51 -04:00
}
}
2018-06-26 16:46:58 -04:00
func ( o * Post ) ChannelMentions ( ) [ ] string {
return ChannelMentions ( o . Message )
2017-11-28 16:02:56 -05:00
}
2026-02-16 15:31:32 -05:00
// ChannelMentionsAll returns all channel mentions from both the message and attachments.
// This is used by FillInPostProps to populate channel_mentions for rendering.
func ( o * Post ) ChannelMentionsAll ( ) [ ] string {
// Get mentions from message
messageMentions := ChannelMentions ( o . Message )
// Get mentions from attachments
attachmentMentions := ChannelMentionsFromAttachments ( o . Attachments ( ) )
// Combine and deduplicate
alreadyMentioned := make ( map [ string ] bool )
var allMentions [ ] string
for _ , mention := range messageMentions {
if ! alreadyMentioned [ mention ] {
allMentions = append ( allMentions , mention )
alreadyMentioned [ mention ] = true
}
}
for _ , mention := range attachmentMentions {
if ! alreadyMentioned [ mention ] {
allMentions = append ( allMentions , mention )
alreadyMentioned [ mention ] = true
}
}
return allMentions
}
2020-03-03 05:22:49 -05:00
// DisableMentionHighlights disables a posts mention highlighting and returns the first channel mention that was present in the message.
func ( o * Post ) DisableMentionHighlights ( ) string {
mention , hasMentions := findAtChannelMention ( o . Message )
if hasMentions {
2021-07-12 14:05:36 -04:00
o . AddProp ( PostPropsMentionHighlightDisabled , true )
2020-03-03 05:22:49 -05:00
}
return mention
}
// DisableMentionHighlights disables mention highlighting for a post patch if required.
func ( o * PostPatch ) DisableMentionHighlights ( ) {
2020-07-06 10:59:36 -04:00
if o . Message == nil {
return
}
2020-03-03 05:22:49 -05:00
if _ , hasMentions := findAtChannelMention ( * o . Message ) ; hasMentions {
if o . Props == nil {
o . Props = & StringInterface { }
}
2021-07-12 14:05:36 -04:00
( * o . Props ) [ PostPropsMentionHighlightDisabled ] = true
2020-03-03 05:22:49 -05:00
}
}
func findAtChannelMention ( message string ) ( mention string , found bool ) {
re := regexp . MustCompile ( ` (?i)\B@(channel|all|here)\b ` )
matched := re . FindStringSubmatch ( message )
if found = ( len ( matched ) > 0 ) ; found {
mention = strings . ToLower ( matched [ 0 ] )
}
return
}
2026-03-10 11:37:21 -04:00
func ( o * Post ) Attachments ( ) [ ] * MessageAttachment {
if attachments , ok := o . GetProp ( PostPropsAttachments ) . ( [ ] * MessageAttachment ) ; ok {
2017-08-29 17:14:59 -04:00
return attachments
}
2026-03-10 11:37:21 -04:00
var ret [ ] * MessageAttachment
2025-03-20 07:53:50 -04:00
if attachments , ok := o . GetProp ( PostPropsAttachments ) . ( [ ] any ) ; ok {
2017-08-29 17:14:59 -04:00
for _ , attachment := range attachments {
if enc , err := json . Marshal ( attachment ) ; err == nil {
2026-03-10 11:37:21 -04:00
var decoded MessageAttachment
2017-08-29 17:14:59 -04:00
if json . Unmarshal ( enc , & decoded ) == nil {
2021-08-04 10:13:37 -04:00
// Ignoring nil actions
2020-12-24 01:00:11 -05:00
i := 0
for _ , action := range decoded . Actions {
if action != nil {
decoded . Actions [ i ] = action
i ++
}
}
decoded . Actions = decoded . Actions [ : i ]
2021-08-04 10:13:37 -04:00
// Ignoring nil fields
i = 0
for _ , field := range decoded . Fields {
if field != nil {
decoded . Fields [ i ] = field
i ++
}
}
decoded . Fields = decoded . Fields [ : i ]
2017-08-29 17:14:59 -04:00
ret = append ( ret , & decoded )
}
}
}
}
return ret
}
2019-04-04 14:01:21 -04:00
func ( o * Post ) AttachmentsEqual ( input * Post ) bool {
attachments := o . Attachments ( )
inputAttachments := input . Attachments ( )
if len ( attachments ) != len ( inputAttachments ) {
return false
}
for i := range attachments {
if ! attachments [ i ] . Equals ( inputAttachments [ i ] ) {
return false
}
}
return true
}
2018-01-22 16:32:50 -05:00
var markdownDestinationEscaper = strings . NewReplacer (
` \ ` , ` \\ ` ,
` < ` , ` \< ` ,
` > ` , ` \> ` ,
` ( ` , ` \( ` ,
` ) ` , ` \) ` ,
)
// WithRewrittenImageURLs returns a new shallow copy of the post where the message has been
// rewritten via RewriteImageURLs.
func ( o * Post ) WithRewrittenImageURLs ( f func ( string ) string ) * Post {
2023-06-30 10:42:56 -04:00
pCopy := o . Clone ( )
pCopy . Message = RewriteImageURLs ( o . Message , f )
if pCopy . MessageSource == "" && pCopy . Message != o . Message {
pCopy . MessageSource = o . Message
2018-01-22 16:32:50 -05:00
}
2023-06-30 10:42:56 -04:00
return pCopy
2018-01-22 16:32:50 -05:00
}
// RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced
// according to the function f. For each image URL, f will be invoked, and the resulting markdown
// will contain the URL returned by that invocation instead.
//
// Image URLs are destination URLs used in inline images or reference definitions that are used
// anywhere in the input markdown as an image.
func RewriteImageURLs ( message string , f func ( string ) string ) string {
if ! strings . Contains ( message , "![" ) {
return message
}
var ranges [ ] markdown . Range
2022-07-05 02:46:50 -04:00
markdown . Inspect ( message , func ( blockOrInline any ) bool {
2018-01-22 16:32:50 -05:00
switch v := blockOrInline . ( type ) {
case * markdown . ReferenceImage :
ranges = append ( ranges , v . ReferenceDefinition . RawDestination )
case * markdown . InlineImage :
ranges = append ( ranges , v . RawDestination )
default :
return true
}
return true
} )
if ranges == nil {
return message
}
sort . Slice ( ranges , func ( i , j int ) bool {
return ranges [ i ] . Position < ranges [ j ] . Position
} )
copyRanges := make ( [ ] markdown . Range , 0 , len ( ranges ) )
urls := make ( [ ] string , 0 , len ( ranges ) )
resultLength := len ( message )
start := 0
for i , r := range ranges {
switch {
case i == 0 :
case r . Position != ranges [ i - 1 ] . Position :
start = ranges [ i - 1 ] . End
default :
continue
}
original := message [ r . Position : r . End ]
replacement := markdownDestinationEscaper . Replace ( f ( markdown . Unescape ( original ) ) )
resultLength += len ( replacement ) - len ( original )
copyRanges = append ( copyRanges , markdown . Range { Position : start , End : r . Position } )
urls = append ( urls , replacement )
}
result := make ( [ ] byte , resultLength )
offset := 0
for i , r := range copyRanges {
offset += copy ( result [ offset : ] , message [ r . Position : r . End ] )
offset += copy ( result [ offset : ] , urls [ i ] )
}
copy ( result [ offset : ] , message [ ranges [ len ( ranges ) - 1 ] . End : ] )
return string ( result )
}
2021-01-26 10:33:44 -05:00
func ( o * Post ) IsFromOAuthBot ( ) bool {
props := o . GetProps ( )
2025-03-20 07:53:50 -04:00
return props [ PostPropsFromWebhook ] == "true" && props [ PostPropsOverrideUsername ] != ""
2021-01-26 10:33:44 -05:00
}
2021-06-23 07:55:12 -04:00
func ( o * Post ) ToNilIfInvalid ( ) * Post {
if o . Id == "" {
return nil
}
return o
}
2021-08-09 11:33:21 -04:00
2022-07-21 10:11:18 -04:00
func ( o * Post ) ForPlugin ( ) * Post {
p := o . Clone ( )
p . Metadata = nil
2023-03-15 08:18:33 -04:00
if p . Type == fmt . Sprintf ( "%sup_notification" , PostCustomTypePrefix ) {
p . DelProp ( "requested_features" )
}
2022-07-21 10:11:18 -04:00
return p
2021-10-08 12:50:03 -04:00
}
2021-08-09 11:33:21 -04:00
func ( o * Post ) GetPreviewPost ( ) * PreviewPost {
2024-10-21 00:05:32 -04:00
if o . Metadata == nil {
return nil
}
2021-08-09 11:33:21 -04:00
for _ , embed := range o . Metadata . Embeds {
2024-10-21 00:05:32 -04:00
if embed != nil && embed . Type == PostEmbedPermalink {
2021-08-09 11:33:21 -04:00
if previewPost , ok := embed . Data . ( * PreviewPost ) ; ok {
return previewPost
}
}
}
return nil
}
2021-09-17 17:47:00 -04:00
func ( o * Post ) GetPreviewedPostProp ( ) string {
if val , ok := o . GetProp ( PostPropsPreviewedPost ) . ( string ) ; ok {
return val
}
return ""
}
2022-11-23 14:08:21 -05:00
func ( o * Post ) GetPriority ( ) * PostPriority {
2023-05-18 14:14:12 -04:00
if o . Metadata == nil {
return nil
2022-11-23 14:08:21 -05:00
}
2023-05-18 14:14:12 -04:00
return o . Metadata . Priority
}
2022-11-23 14:08:21 -05:00
2023-05-18 14:14:12 -04:00
func ( o * Post ) GetPersistentNotification ( ) * bool {
priority := o . GetPriority ( )
if priority == nil {
return nil
}
return priority . PersistentNotifications
}
func ( o * Post ) GetRequestedAck ( ) * bool {
priority := o . GetPriority ( )
if priority == nil {
return nil
}
return priority . RequestedAck
2022-11-23 14:08:21 -05:00
}
func ( o * Post ) IsUrgent ( ) bool {
postPriority := o . GetPriority ( )
if postPriority == nil {
return false
}
2024-12-05 23:40:41 -05:00
if postPriority . Priority == nil {
return false
}
2022-11-23 14:08:21 -05:00
return * postPriority . Priority == PostPriorityUrgent
}
2023-12-11 15:27:34 -05:00
func ( o * Post ) CleanPost ( ) * Post {
o . Id = ""
o . CreateAt = 0
o . UpdateAt = 0
o . EditAt = 0
return o
}
2025-01-13 07:46:56 -05:00
type UpdatePostOptions struct {
SafeUpdate bool
IsRestorePost bool
}
func DefaultUpdatePostOptions ( ) * UpdatePostOptions {
return & UpdatePostOptions {
SafeUpdate : false ,
IsRestorePost : false ,
}
}
2025-10-02 10:54:29 -04:00
type PreparePostForClientOpts struct {
IsNewPost bool
IsEditPost bool
IncludePriority bool
RetainContent bool
IncludeDeleted bool
}
2025-11-11 09:14:21 -05:00
Add cursor-based Posts Reporting API for compliance and auditing (#34252)
* Add cursor-based Posts Reporting API for compliance and auditing
Implements a new admin-only endpoint for retrieving posts with efficient
cursor-based pagination, designed for compliance, auditing, and archival
workflows.
Key Features:
- Cursor-based pagination using composite (time, ID) keys for consistent
performance regardless of dataset size (~10ms per page at any depth)
- Flexible time range queries with optional upper/lower bounds
- Support for both create_at and update_at time fields
- Ascending or descending sort order
- Optional metadata enrichment (files, reactions, acknowledgements)
- System admin only access (requires manage_system permission)
- License enforcement for compliance features
API Endpoint:
POST /api/v4/reports/posts
- Request: JSON body with channel_id, cursor_time, cursor_id, and options
- Response: Posts map + next_cursor object (null when pagination complete)
- Max page size: 1000 posts per request (MaxReportingPerPage constant)
Implementation:
- Store Layer: Direct SQL queries with composite index on (ChannelId, CreateAt, Id)
- App Layer: Permission checks, optional metadata enrichment, post hooks
- API Layer: Parameter validation, system admin enforcement, license checks
- Data Model: ReportPostOptions, ReportPostOptionsCursor, ReportPostListResponse
Code Quality Improvements:
- Added MaxReportingPerPage constant (1000) to eliminate magic numbers
- Removed unused StartTime field from ReportPostOptions
- Added fmt import for dynamic error messages
Testing:
- 14 comprehensive store layer unit tests
- 12 API layer integration tests covering permissions, pagination, filters
- All tests passing
Documentation:
- POSTS_REPORTING.md: Developer reference with Go structs and usage examples
- POSTS_REPORTING_API_SPEC.md: Complete technical specification
- GET_POSTS_API_IMPROVEMENTS.md: Implementation analysis and design rationale
- POSTS_TIME_RANGE_FEATURE.md: Archived time range feature for future use
Performance:
Cursor-based pagination maintains consistent ~10ms query time at any dataset
depth, compared to offset-based pagination which degrades significantly
(Page 1 = 10ms, Page 1000 = 10 seconds).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* lint fixes
* lint fixes
* gofmt
* i18n-extract
* Add Enterprise license requirement to posts reporting API
Enforce Enterprise license (tier 20+) for the new posts reporting endpoint
to align with compliance feature licensing. Professional tier is insufficient.
Changes:
- Add MinimumEnterpriseLicense check in GetPostsForReporting app layer
- Add test coverage for license validation (no license and Professional tier)
All existing tests pass with new license enforcement.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* i18n-extract
* add licensing to api documentation
* Test SSH signing
* Add mmctl command for posts reporting API
Adds mmctl report posts command to retrieve posts from a channel for
administrative reporting purposes. Supports cursor-based pagination with
configurable sorting, filtering, and time range options.
Includes database migration for updateat+id index to support efficient
cursor-based queries when sorting by update_at.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API cursor to opaque token and improve layer separation
This addresses code review feedback by transforming the cursor from exposed fields
to an opaque token and improving architectural layer separation.
**Key Changes:**
1. **Opaque Cursor Implementation**
- Transform cursor from split fields (cursor_time, cursor_id) to single opaque base64-encoded string
- Cursor now self-contained with all query parameters embedded
- When cursor provided, embedded parameters take precedence over request body
- Clients treat cursor as opaque token and pass unchanged
2. **Field Naming**
- Rename ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Now excludes ALL system posts (any type starting with "system_")
- Clearer and more consistent naming
3. **Layer Separation**
- Move cursor decoding from store layer to model layer
- Create ReportPostQueryParams struct for resolved parameters
- Store layer receives pre-resolved parameters (no business logic)
- Add ResolveReportPostQueryParams() function in model layer
4. **Code Quality**
- Add type-safe constants (ReportingTimeFieldCreateAt, ReportingSortDirectionAsc, etc.)
- Replace magic number 9223372036854775807 with math.MaxInt64
- Remove debug SQL logging (info disclosure risk)
- Update mmctl to use constants and fix NextCursor pointer access
5. **Tests**
- Update all 17 store test calls to use new resolution pattern
- Add comprehensive test for DESC + end_time boundary behavior
6. **API Documentation**
- Update OpenAPI spec to reflect opaque cursor format
- Update all request/response examples
- Clarify end_time behavior with sort directions
**Files Changed:**
- Model layer: public/model/post.go
- App layer: channels/app/report.go
- Store layer: channels/store/store.go, channels/store/sqlstore/post_store.go
- Tests: channels/store/storetest/post_store.go
- Mocks: channels/store/storetest/mocks/PostStore.go
- API: channels/api4/report.go, channels/api4/report_test.go
- mmctl: cmd/mmctl/commands/report.go
- Docs: api/v4/source/reports.yaml
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix unhandled parse errors in cursor decoding
Address security finding: cursor decoding was silently ignoring parse errors
from strconv functions, which could lead to unexpected behavior when malformed
cursors are provided.
Changes:
- Add explicit error handling for strconv.Atoi (version parsing)
- Add explicit error handling for strconv.ParseBool (includeDeleted, excludeSystemPosts)
- Add explicit error handling for strconv.ParseInt (timestamp parsing)
- Return clear error messages indicating which field failed to parse
This prevents silent failures where malformed values would default to zero-values
(0, false) and potentially alter query behavior without warning.
Addresses DryRun Security finding: "Unhandled Errors in Cursor Parsing"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues
- Remove unused reportPostCursorV1 struct (unused)
- Remove obsolete +build comment (buildtag)
- Use maps.Copy instead of manual loop (mapsloop)
- Modernize for loop with range over int (rangeint)
- Apply gofmt formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix gofmt formatting issues
Fix alignment in struct literals and constant declarations:
- Align map keys in report_test.go request bodies
- Align struct fields in ReportPostOptions initialization
- Align reporting constant declarations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more lint fixes
* remove index update files
* Remove end_time parameter from Posts Reporting API
Align with other cursor-based APIs in the codebase by removing the end_time
parameter. The caller now controls when to stop pagination by simply not
making another request, which is the same pattern used by GetPostsSinceForSync,
MessageExport, and GetPostsBatchForIndexing.
Changes:
- Remove EndTime field from ReportPostOptions and ReportPostQueryParams
- Remove EndTime filtering logic from store layer
- Remove tests that used end_time parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API for security and validation
Address security review feedback by consolidating parameter resolution
and validation in the API layer, with comprehensive validation of all
cursor fields to prevent SQL injection and invalid queries.
Changes:
- Move parameter resolution from model to API layer for clearer separation
- Add ReportPostQueryParams.Validate() with inline validation for all fields
- Validate ChannelId, TimeField, SortDirection, and CursorId format
- Add start_time parameter for time-bounded queries
- Cap per_page at 100-1000 instead of rejecting invalid values
- Export DecodeReportPostCursorV1() for API layer use
- Simplify app layer to receive pre-validated parameters
- Check channel existence when results are empty (better error messages)
Testing:
- Add 10 model tests for validation and malformed cursor scenarios
- Add 4 API tests for cursors with invalid field values
- Refactor 13 store tests to use buildReportPostQueryParams() helper
- All 31 tests pass
Documentation:
- Update OpenAPI spec with start_time, remove unused end_time
- Update markdown docs with start_time examples
Security improvements:
- Whitelist validation prevents SQL injection in TimeField/SortDirection
- Format validation ensures ChannelId and CursorId are valid IDs
- Single validation point for both cursor and options paths
- Defense in depth: validation + parameterized queries + store layer whitelist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Improve posts reporting query efficiency and safety
Replace SELECT * and nested OR/AND conditions with explicit column
selection and PostgreSQL row value comparison for better performance
and maintainability.
Changes:
- Use postSliceColumns() instead of SELECT * for explicit column selection
- Replace Squirrel OR/AND with row value comparison: (timeField, Id) > (?, ?)
- Use fmt.Sprintf for safer string formatting in WHERE clause
Query improvements:
Before: WHERE (CreateAt > ?) OR (CreateAt = ? AND Id > ?)
After: WHERE (CreateAt, Id) > (?, ?)
Benefits:
- Explicit column selection prevents issues if table schema changes
- Row value comparison is more concise and better optimized by PostgreSQL
- Follows existing patterns in post_store.go (postSliceColumns)
- Standard SQL:2003 syntax
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Change posts reporting response from map to ordered array
Replace the Posts map with an ordered array to preserve query sort order
and provide a more natural API response for sequential processing.
Changes:
- ReportPostListResponse.Posts: map[string]*Post → []*Post
- Store layer returns posts array directly (already sorted by query)
- App layer iterates by index for metadata enrichment
- Remove applyPostsWillBeConsumedHook call (not applicable to reporting)
- Update API tests to iterate arrays instead of map lookups
- Update store tests to convert array to map for deduplication checks
- Remove unused "maps" import
Benefits:
- Preserves query sort order (ASC/DESC, create_at/update_at)
- More natural for sequential processing/export workflows
- Simpler response structure for reporting/compliance use cases
- Aligns with message export/compliance patterns (no plugin hooks)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues in posts reporting tests
Replace inefficient loops with append(...) for better performance.
Changes:
- Use append(postSlice, result.Posts...) instead of loop
- Simplifies code and follows staticcheck recommendations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix store test AppError nil checking
Use require.Nil instead of require.NoError for *AppError returns
to avoid Go interface nil pointer issues.
When DecodeReportPostCursorV1 returns nil *AppError and it's assigned
to error interface, the interface becomes non-nil even though the
pointer is nil. This causes require.NoError to fail incorrectly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2025-11-17 11:02:19 -05:00
// ReportPostOptions contains options for querying posts for reporting/compliance purposes
type ReportPostOptions struct {
ChannelId string ` json:"channel_id" `
StartTime int64 ` json:"start_time,omitempty" ` // Optional: Start time for query range (unix timestamp in milliseconds)
TimeField string ` json:"time_field,omitempty" ` // "create_at" or "update_at" (default: "create_at")
SortDirection string ` json:"sort_direction,omitempty" ` // "asc" or "desc" (default: "asc")
PerPage int ` json:"per_page,omitempty" ` // Number of posts per page (default: 100, max: MaxReportingPerPage)
IncludeDeleted bool ` json:"include_deleted,omitempty" ` // Include deleted posts
ExcludeSystemPosts bool ` json:"exclude_system_posts,omitempty" ` // Exclude all system posts (any type starting with "system_")
IncludeMetadata bool ` json:"include_metadata,omitempty" ` // Include file info, reactions, etc.
}
2025-11-11 09:14:21 -05:00
type RewriteAction string
const (
RewriteActionCustom RewriteAction = "custom"
RewriteActionShorten RewriteAction = "shorten"
RewriteActionElaborate RewriteAction = "elaborate"
RewriteActionImproveWriting RewriteAction = "improve_writing"
RewriteActionFixSpelling RewriteAction = "fix_spelling"
RewriteActionSimplify RewriteAction = "simplify"
RewriteActionSummarize RewriteAction = "summarize"
)
type RewriteRequest struct {
AgentID string ` json:"agent_id" `
Message string ` json:"message" `
Action RewriteAction ` json:"action" `
CustomPrompt string ` json:"custom_prompt,omitempty" `
2026-01-30 09:14:06 -05:00
RootID string ` json:"root_id,omitempty" `
2025-11-11 09:14:21 -05:00
}
type RewriteResponse struct {
RewrittenText string ` json:"rewritten_text" `
}
2026-01-30 09:14:06 -05:00
const RewriteSystemPrompt = ` You are a JSON API that rewrites text . Your response must be valid JSON only .
Return this exact format : { "rewritten_text" : "content" } .
2025-11-11 09:14:21 -05:00
Do not use markdown , code blocks , or any formatting . Start with { and end with } . `
Add cursor-based Posts Reporting API for compliance and auditing (#34252)
* Add cursor-based Posts Reporting API for compliance and auditing
Implements a new admin-only endpoint for retrieving posts with efficient
cursor-based pagination, designed for compliance, auditing, and archival
workflows.
Key Features:
- Cursor-based pagination using composite (time, ID) keys for consistent
performance regardless of dataset size (~10ms per page at any depth)
- Flexible time range queries with optional upper/lower bounds
- Support for both create_at and update_at time fields
- Ascending or descending sort order
- Optional metadata enrichment (files, reactions, acknowledgements)
- System admin only access (requires manage_system permission)
- License enforcement for compliance features
API Endpoint:
POST /api/v4/reports/posts
- Request: JSON body with channel_id, cursor_time, cursor_id, and options
- Response: Posts map + next_cursor object (null when pagination complete)
- Max page size: 1000 posts per request (MaxReportingPerPage constant)
Implementation:
- Store Layer: Direct SQL queries with composite index on (ChannelId, CreateAt, Id)
- App Layer: Permission checks, optional metadata enrichment, post hooks
- API Layer: Parameter validation, system admin enforcement, license checks
- Data Model: ReportPostOptions, ReportPostOptionsCursor, ReportPostListResponse
Code Quality Improvements:
- Added MaxReportingPerPage constant (1000) to eliminate magic numbers
- Removed unused StartTime field from ReportPostOptions
- Added fmt import for dynamic error messages
Testing:
- 14 comprehensive store layer unit tests
- 12 API layer integration tests covering permissions, pagination, filters
- All tests passing
Documentation:
- POSTS_REPORTING.md: Developer reference with Go structs and usage examples
- POSTS_REPORTING_API_SPEC.md: Complete technical specification
- GET_POSTS_API_IMPROVEMENTS.md: Implementation analysis and design rationale
- POSTS_TIME_RANGE_FEATURE.md: Archived time range feature for future use
Performance:
Cursor-based pagination maintains consistent ~10ms query time at any dataset
depth, compared to offset-based pagination which degrades significantly
(Page 1 = 10ms, Page 1000 = 10 seconds).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* lint fixes
* lint fixes
* gofmt
* i18n-extract
* Add Enterprise license requirement to posts reporting API
Enforce Enterprise license (tier 20+) for the new posts reporting endpoint
to align with compliance feature licensing. Professional tier is insufficient.
Changes:
- Add MinimumEnterpriseLicense check in GetPostsForReporting app layer
- Add test coverage for license validation (no license and Professional tier)
All existing tests pass with new license enforcement.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* i18n-extract
* add licensing to api documentation
* Test SSH signing
* Add mmctl command for posts reporting API
Adds mmctl report posts command to retrieve posts from a channel for
administrative reporting purposes. Supports cursor-based pagination with
configurable sorting, filtering, and time range options.
Includes database migration for updateat+id index to support efficient
cursor-based queries when sorting by update_at.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API cursor to opaque token and improve layer separation
This addresses code review feedback by transforming the cursor from exposed fields
to an opaque token and improving architectural layer separation.
**Key Changes:**
1. **Opaque Cursor Implementation**
- Transform cursor from split fields (cursor_time, cursor_id) to single opaque base64-encoded string
- Cursor now self-contained with all query parameters embedded
- When cursor provided, embedded parameters take precedence over request body
- Clients treat cursor as opaque token and pass unchanged
2. **Field Naming**
- Rename ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Now excludes ALL system posts (any type starting with "system_")
- Clearer and more consistent naming
3. **Layer Separation**
- Move cursor decoding from store layer to model layer
- Create ReportPostQueryParams struct for resolved parameters
- Store layer receives pre-resolved parameters (no business logic)
- Add ResolveReportPostQueryParams() function in model layer
4. **Code Quality**
- Add type-safe constants (ReportingTimeFieldCreateAt, ReportingSortDirectionAsc, etc.)
- Replace magic number 9223372036854775807 with math.MaxInt64
- Remove debug SQL logging (info disclosure risk)
- Update mmctl to use constants and fix NextCursor pointer access
5. **Tests**
- Update all 17 store test calls to use new resolution pattern
- Add comprehensive test for DESC + end_time boundary behavior
6. **API Documentation**
- Update OpenAPI spec to reflect opaque cursor format
- Update all request/response examples
- Clarify end_time behavior with sort directions
**Files Changed:**
- Model layer: public/model/post.go
- App layer: channels/app/report.go
- Store layer: channels/store/store.go, channels/store/sqlstore/post_store.go
- Tests: channels/store/storetest/post_store.go
- Mocks: channels/store/storetest/mocks/PostStore.go
- API: channels/api4/report.go, channels/api4/report_test.go
- mmctl: cmd/mmctl/commands/report.go
- Docs: api/v4/source/reports.yaml
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix unhandled parse errors in cursor decoding
Address security finding: cursor decoding was silently ignoring parse errors
from strconv functions, which could lead to unexpected behavior when malformed
cursors are provided.
Changes:
- Add explicit error handling for strconv.Atoi (version parsing)
- Add explicit error handling for strconv.ParseBool (includeDeleted, excludeSystemPosts)
- Add explicit error handling for strconv.ParseInt (timestamp parsing)
- Return clear error messages indicating which field failed to parse
This prevents silent failures where malformed values would default to zero-values
(0, false) and potentially alter query behavior without warning.
Addresses DryRun Security finding: "Unhandled Errors in Cursor Parsing"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues
- Remove unused reportPostCursorV1 struct (unused)
- Remove obsolete +build comment (buildtag)
- Use maps.Copy instead of manual loop (mapsloop)
- Modernize for loop with range over int (rangeint)
- Apply gofmt formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix gofmt formatting issues
Fix alignment in struct literals and constant declarations:
- Align map keys in report_test.go request bodies
- Align struct fields in ReportPostOptions initialization
- Align reporting constant declarations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Update mmctl tests for opaque cursor and add i18n translations
Update report_test.go to align with the refactored Posts Reporting API:
- Replace split cursor flags (cursor-time, cursor-id) with single opaque cursor flag
- Update field name: ExcludeChannelMetadataSystemPosts → ExcludeSystemPosts
- Update all mock expectations to use new ReportPostOptionsCursor structure
- Replace test cursor values with base64-encoded opaque cursor strings
Add English translations for cursor decoding error messages in i18n/en.json.
Minor API documentation fix in reports.yaml (remove "all" from description).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* more lint fixes
* remove index update files
* Remove end_time parameter from Posts Reporting API
Align with other cursor-based APIs in the codebase by removing the end_time
parameter. The caller now controls when to stop pagination by simply not
making another request, which is the same pattern used by GetPostsSinceForSync,
MessageExport, and GetPostsBatchForIndexing.
Changes:
- Remove EndTime field from ReportPostOptions and ReportPostQueryParams
- Remove EndTime filtering logic from store layer
- Remove tests that used end_time parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Refactor posts reporting API for security and validation
Address security review feedback by consolidating parameter resolution
and validation in the API layer, with comprehensive validation of all
cursor fields to prevent SQL injection and invalid queries.
Changes:
- Move parameter resolution from model to API layer for clearer separation
- Add ReportPostQueryParams.Validate() with inline validation for all fields
- Validate ChannelId, TimeField, SortDirection, and CursorId format
- Add start_time parameter for time-bounded queries
- Cap per_page at 100-1000 instead of rejecting invalid values
- Export DecodeReportPostCursorV1() for API layer use
- Simplify app layer to receive pre-validated parameters
- Check channel existence when results are empty (better error messages)
Testing:
- Add 10 model tests for validation and malformed cursor scenarios
- Add 4 API tests for cursors with invalid field values
- Refactor 13 store tests to use buildReportPostQueryParams() helper
- All 31 tests pass
Documentation:
- Update OpenAPI spec with start_time, remove unused end_time
- Update markdown docs with start_time examples
Security improvements:
- Whitelist validation prevents SQL injection in TimeField/SortDirection
- Format validation ensures ChannelId and CursorId are valid IDs
- Single validation point for both cursor and options paths
- Defense in depth: validation + parameterized queries + store layer whitelist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Improve posts reporting query efficiency and safety
Replace SELECT * and nested OR/AND conditions with explicit column
selection and PostgreSQL row value comparison for better performance
and maintainability.
Changes:
- Use postSliceColumns() instead of SELECT * for explicit column selection
- Replace Squirrel OR/AND with row value comparison: (timeField, Id) > (?, ?)
- Use fmt.Sprintf for safer string formatting in WHERE clause
Query improvements:
Before: WHERE (CreateAt > ?) OR (CreateAt = ? AND Id > ?)
After: WHERE (CreateAt, Id) > (?, ?)
Benefits:
- Explicit column selection prevents issues if table schema changes
- Row value comparison is more concise and better optimized by PostgreSQL
- Follows existing patterns in post_store.go (postSliceColumns)
- Standard SQL:2003 syntax
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Change posts reporting response from map to ordered array
Replace the Posts map with an ordered array to preserve query sort order
and provide a more natural API response for sequential processing.
Changes:
- ReportPostListResponse.Posts: map[string]*Post → []*Post
- Store layer returns posts array directly (already sorted by query)
- App layer iterates by index for metadata enrichment
- Remove applyPostsWillBeConsumedHook call (not applicable to reporting)
- Update API tests to iterate arrays instead of map lookups
- Update store tests to convert array to map for deduplication checks
- Remove unused "maps" import
Benefits:
- Preserves query sort order (ASC/DESC, create_at/update_at)
- More natural for sequential processing/export workflows
- Simpler response structure for reporting/compliance use cases
- Aligns with message export/compliance patterns (no plugin hooks)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix linting issues in posts reporting tests
Replace inefficient loops with append(...) for better performance.
Changes:
- Use append(postSlice, result.Posts...) instead of loop
- Simplifies code and follows staticcheck recommendations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix store test AppError nil checking
Use require.Nil instead of require.NoError for *AppError returns
to avoid Go interface nil pointer issues.
When DecodeReportPostCursorV1 returns nil *AppError and it's assigned
to error interface, the interface becomes non-nil even though the
pointer is nil. This causes require.NoError to fail incorrectly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2025-11-17 11:02:19 -05:00
// ReportPostOptionsCursor contains cursor information for pagination.
// The cursor is an opaque base64-encoded string that encodes all pagination state.
// Clients should treat this as an opaque token and pass it back unchanged.
//
// Internal format (before base64 encoding):
//
// v1: "version:channel_id:time_field:include_deleted:exclude_system_posts:sort_direction:timestamp:post_id"
//
// Field order (general to specific):
// - version: Allows format evolution
// - channel_id: Which channel to query (filter)
// - time_field: Which timestamp column to use for ordering (filter/config)
// - include_deleted: Whether to include deleted posts (filter)
// - exclude_system_posts: Whether to exclude channel metadata system posts (filter)
// - sort_direction: Query direction ASC vs DESC (filter/config)
// - timestamp: The cursor position in time (pagination state)
// - post_id: Tie-breaker for posts with identical timestamps (pagination state)
//
// Version history:
// - v1: Initial format with all query-affecting parameters ordered general→specific, base64-encoded for opacity
// ReportPostOptionsCursor contains the pagination cursor for posts reporting.
//
// The cursor is opaque and self-contained:
// - It's base64-encoded and contains all query parameters (channel_id, time_field, sort_direction, etc.)
// - When a cursor is provided, query parameters in the request body are IGNORED
// - The cursor's embedded parameters take precedence over request body parameters
// - This allows clients to keep sending the same parameters on every page without errors
// - For the first page, omit the cursor field or set it to ""
type ReportPostOptionsCursor struct {
Cursor string ` json:"cursor,omitempty" ` // Optional: Opaque base64-encoded cursor string (omit or use "" for first request)
}
// ReportPostListResponse contains the response for cursor-based post reporting queries
type ReportPostListResponse struct {
Posts [ ] * Post ` json:"posts" `
NextCursor * ReportPostOptionsCursor ` json:"next_cursor,omitempty" ` // nil if no more pages
}
// ReportPostQueryParams contains the fully resolved query parameters for the store layer.
// This struct is used internally after cursor decoding and parameter resolution.
// The store layer receives these concrete parameters and executes the query.
type ReportPostQueryParams struct {
ChannelId string // Required: Channel to query
CursorTime int64 // Pagination cursor time position
CursorId string // Pagination cursor ID for tie-breaking
TimeField string // Resolved: "create_at" or "update_at"
SortDirection string // Resolved: "asc" or "desc"
IncludeDeleted bool // Resolved: include deleted posts
ExcludeSystemPosts bool // Resolved: exclude system posts
PerPage int // Number of posts per page (already validated)
}
// Validate validates the ReportPostQueryParams fields.
// This should be called after parameter resolution (from cursor or options) and before passing to the store layer.
// Note: PerPage is handled separately in the API layer (capped at 100-1000 range).
func ( q * ReportPostQueryParams ) Validate ( ) * AppError {
// Validate ChannelId
if ! IsValidId ( q . ChannelId ) {
return NewAppError ( "ReportPostQueryParams.Validate" , "model.post.query_params.invalid_channel_id" , nil , "channel_id must be a valid 26-character ID" , 400 )
}
// Validate TimeField
if q . TimeField != ReportingTimeFieldCreateAt && q . TimeField != ReportingTimeFieldUpdateAt {
return NewAppError ( "ReportPostQueryParams.Validate" , "model.post.query_params.invalid_time_field" , nil , fmt . Sprintf ( "time_field must be %q or %q" , ReportingTimeFieldCreateAt , ReportingTimeFieldUpdateAt ) , 400 )
}
// Validate SortDirection
if q . SortDirection != ReportingSortDirectionAsc && q . SortDirection != ReportingSortDirectionDesc {
return NewAppError ( "ReportPostQueryParams.Validate" , "model.post.query_params.invalid_sort_direction" , nil , fmt . Sprintf ( "sort_direction must be %q or %q" , ReportingSortDirectionAsc , ReportingSortDirectionDesc ) , 400 )
}
// Validate CursorId - can be empty (first page) or must be a valid ID format (subsequent pages)
if q . CursorId != "" && ! IsValidId ( q . CursorId ) {
return NewAppError ( "ReportPostQueryParams.Validate" , "model.post.query_params.invalid_cursor_id" , nil , "cursor_id must be a valid 26-character ID" , 400 )
}
// CursorTime is validated by the fact it's an int64
// PerPage is handled in API layer before calling Validate()
return nil
}
// EncodeReportPostCursor creates an opaque cursor string from pagination state.
// The cursor encodes all query-affecting parameters to ensure consistency across pages.
// The cursor is base64-encoded to ensure it's truly opaque and URL-safe.
//
// Internal format: "version:channel_id:time_field:include_deleted:exclude_system_posts:sort_direction:timestamp:post_id"
// Example (before encoding): "1:abc123xyz:create_at:false:true:asc:1635724800000:post456def"
func EncodeReportPostCursor ( channelId string , timeField string , includeDeleted bool , excludeSystemPosts bool , sortDirection string , timestamp int64 , postId string ) string {
plainText := fmt . Sprintf ( "1:%s:%s:%t:%t:%s:%d:%s" ,
channelId ,
timeField ,
includeDeleted ,
excludeSystemPosts ,
sortDirection ,
timestamp ,
postId )
return base64 . URLEncoding . EncodeToString ( [ ] byte ( plainText ) )
}
// DecodeReportPostCursorV1 parses an opaque cursor string into query parameters.
// Returns a partially populated ReportPostQueryParams (missing PerPage which comes from the request).
func DecodeReportPostCursorV1 ( cursor string ) ( * ReportPostQueryParams , * AppError ) {
decoded , err := base64 . URLEncoding . DecodeString ( cursor )
if err != nil {
return nil , NewAppError ( "DecodeReportPostCursorV1" , "model.post.decode_cursor.invalid_base64" , nil , err . Error ( ) , 400 )
}
parts := strings . Split ( string ( decoded ) , ":" )
if len ( parts ) != 8 {
return nil , NewAppError ( "DecodeReportPostCursorV1" , "model.post.decode_cursor.invalid_format" , nil , fmt . Sprintf ( "expected 8 parts, got %d" , len ( parts ) ) , 400 )
}
version , err := strconv . Atoi ( parts [ 0 ] )
if err != nil {
return nil , NewAppError ( "DecodeReportPostCursorV1" , "model.post.decode_cursor.invalid_version" , nil , fmt . Sprintf ( "version must be an integer: %s" , err . Error ( ) ) , 400 )
}
if version != 1 {
return nil , NewAppError ( "DecodeReportPostCursorV1" , "model.post.decode_cursor.unsupported_version" , nil , fmt . Sprintf ( "version %d" , version ) , 400 )
}
includeDeleted , err := strconv . ParseBool ( parts [ 3 ] )
if err != nil {
return nil , NewAppError ( "DecodeReportPostCursorV1" , "model.post.decode_cursor.invalid_include_deleted" , nil , fmt . Sprintf ( "include_deleted must be a boolean: %s" , err . Error ( ) ) , 400 )
}
excludeSystemPosts , err := strconv . ParseBool ( parts [ 4 ] )
if err != nil {
return nil , NewAppError ( "DecodeReportPostCursorV1" , "model.post.decode_cursor.invalid_exclude_system_posts" , nil , fmt . Sprintf ( "exclude_system_posts must be a boolean: %s" , err . Error ( ) ) , 400 )
}
timestamp , err := strconv . ParseInt ( parts [ 6 ] , 10 , 64 )
if err != nil {
return nil , NewAppError ( "DecodeReportPostCursorV1" , "model.post.decode_cursor.invalid_timestamp" , nil , fmt . Sprintf ( "timestamp must be an integer: %s" , err . Error ( ) ) , 400 )
}
return & ReportPostQueryParams {
ChannelId : parts [ 1 ] ,
CursorTime : timestamp ,
CursorId : parts [ 7 ] ,
TimeField : parts [ 2 ] ,
SortDirection : parts [ 5 ] ,
IncludeDeleted : includeDeleted ,
ExcludeSystemPosts : excludeSystemPosts ,
} , nil
}