snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package export_users_to_csv
import (
"fmt"
"strconv"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/jobs"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/pkg/errors"
)
const (
timeBetweenBatches = 1 * time.Second
)
// csvExportColumns defines the header row of the user CSV export. Its order must
// match the values returned by model.UserReport.ToReport.
var csvExportColumns = []string{
"Id",
"Username",
"Email",
"CreateAt",
"Name",
"Roles",
"LastLogin",
"LastStatusAt",
"LastPostDate",
"DaysActive",
"TotalPosts",
"ChannelCount",
"Teams",
"DeletedAt",
}
type ExportUsersToCSVAppIFace interface {
jobs.BatchReportWorkerAppIFace
GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError)
}
// MakeWorker creates a batch report worker to generate CSV user reports.
func MakeWorker(jobServer *jobs.JobServer, store store.Store, app ExportUsersToCSVAppIFace) model.Worker {
return jobs.MakeBatchReportWorker(
jobServer,
store,
app,
timeBetweenBatches,
"csv",
csvExportColumns,
getData(app),
)
}
// parseJobMetadata parses the opaque job metadata to return the information needed to decide which
// batch to process next.
func parseJobMetadata(data model.StringMap) (*model.UserReportOptions, error) {
startAt, err := strconv.ParseInt(data["start_at"], 10, 64)
if err != nil {
return nil, err
}
endAt, err := strconv.ParseInt(data["end_at"], 10, 64)
if err != nil {
return nil, err
}
hideInactive := false
if val, ok := data["hide_inactive"]; ok && val != "" {
hideInactive, err = strconv.ParseBool(val)
if err != nil {
return nil, fmt.Errorf("failed to parse hide_inactive: %w", err)
}
}
hideActive := false
if val, ok := data["hide_active"]; ok && val != "" {
hideActive, err = strconv.ParseBool(val)
if err != nil {
return nil, fmt.Errorf("failed to parse hide_active: %w", err)
}
}
options := model.UserReportOptions{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 100,
FromColumnValue: data["last_column_value"],
FromId: data["last_user_id"],
StartAt: startAt,
EndAt: endAt,
},
HideInactive: hideInactive,
HideActive: hideActive,
Role: data["role"],
Team: data["team"],
GuestFilter: data["guest_filter"],
SearchTerm: data["search_term"],
}
return &options, nil
}
// makeJobMetadata encodes the information needed to decide which batch to process next back into
// the opaque job metadata.
func makeJobMetadata(jobData model.StringMap, lastColumnValue string, userID string) model.StringMap {
jobData["last_column_value"] = lastColumnValue
jobData["last_user_id"] = userID
return jobData
}
func getData(app ExportUsersToCSVAppIFace) func(jobData model.StringMap) ([]model.ReportableObject, model.StringMap, bool, error) {
return func(jobData model.StringMap) ([]model.ReportableObject, model.StringMap, bool, error) {
filter, err := parseJobMetadata(jobData)
if err != nil {
return nil, nil, false, errors.Wrap(err, "failed to parse job metadata")
}
users, appErr := app.GetUsersForReporting(filter)
if appErr != nil {
return nil, nil, false, errors.Wrapf(appErr, "failed to get the next batch (column_value=%v, user_id=%v)", filter.FromColumnValue, filter.FromId)
}
if len(users) == 0 {
return nil, nil, true, nil
}
reportableObjects := []model.ReportableObject{}
for i := range users {
reportableObjects = append(reportableObjects, users[i])
}
return reportableObjects, makeJobMetadata(jobData, users[len(users)-1].Username, users[len(users)-1].Id), false, nil
}
}

View File

@@ -0,0 +1,145 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package export_users_to_csv
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
)
func TestCSVExportColumns(t *testing.T) {
t.Run("header has the expected columns in order", func(t *testing.T) {
// Pin the exact header so a rename or reorder is caught. Teams sits
// between ChannelCount and DeletedAt.
require.Equal(t, []string{
"Id",
"Username",
"Email",
"CreateAt",
"Name",
"Roles",
"LastLogin",
"LastStatusAt",
"LastPostDate",
"DaysActive",
"TotalPosts",
"ChannelCount",
"Teams",
"DeletedAt",
}, csvExportColumns)
})
t.Run("header column count matches the report row values", func(t *testing.T) {
// The CSV header is written separately from the data rows, so a count
// mismatch between the header and model.UserReport.ToReport would
// silently shift every column. Keep them in lock-step.
report := (&model.UserReport{}).ToReport()
require.Equal(t, len(csvExportColumns), len(report))
})
}
// recordingExportApp records the filters the export queries users with.
type recordingExportApp struct {
filters []*model.UserReportOptions
users []*model.UserReport
}
func (a *recordingExportApp) SaveReportChunk(format string, prefix string, count int, reportData []model.ReportableObject) *model.AppError {
return nil
}
func (a *recordingExportApp) CompileReportChunks(format string, prefix string, numberOfChunks int, headers []string) *model.AppError {
return nil
}
func (a *recordingExportApp) SendReportToUser(rctx request.CTX, job *model.Job, format string) *model.AppError {
return nil
}
func (a *recordingExportApp) CleanupReportChunks(format string, prefix string, numberOfChunks int) *model.AppError {
return nil
}
func (a *recordingExportApp) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) {
a.filters = append(a.filters, filter)
return a.users, nil
}
func TestGetData(t *testing.T) {
jobData := func() model.StringMap {
return model.StringMap{
"requesting_user_id": "cxzhnzbpqbnufc5qhnr63zcpdw",
"date_range": model.ReportDurationAllTime,
"start_at": "100",
"end_at": "200",
"role": "system_user",
"team": "hn6jhbbutbyd8mmwbtkr3sozbo",
"hide_active": "false",
"hide_inactive": "true",
"guest_filter": "all",
"search_term": "alice",
}
}
batchOfOne := func() []*model.UserReport {
return []*model.UserReport{{
User: model.User{Id: "wbtkr3sozbohn6jhbbutbyd8mm", Username: "alice-smith"},
}}
}
t.Run("should query users with every filter the export was started with", func(t *testing.T) {
app := &recordingExportApp{users: batchOfOne()}
reportData, nextData, done, err := getData(app)(jobData())
require.NoError(t, err)
require.False(t, done)
require.Len(t, reportData, 1)
require.Len(t, app.filters, 1)
filter := app.filters[0]
require.Equal(t, "alice", filter.SearchTerm)
require.Equal(t, "system_user", filter.Role)
require.Equal(t, "hn6jhbbutbyd8mmwbtkr3sozbo", filter.Team)
require.Equal(t, "all", filter.GuestFilter)
require.False(t, filter.HideActive)
require.True(t, filter.HideInactive)
require.Equal(t, int64(100), filter.StartAt)
require.Equal(t, int64(200), filter.EndAt)
require.Equal(t, "alice-smith", nextData["last_column_value"])
require.Equal(t, "wbtkr3sozbohn6jhbbutbyd8mm", nextData["last_user_id"])
})
t.Run("should keep filtering on the search term once the cursor advances", func(t *testing.T) {
app := &recordingExportApp{users: batchOfOne()}
_, nextData, _, err := getData(app)(jobData())
require.NoError(t, err)
_, _, _, err = getData(app)(nextData)
require.NoError(t, err)
require.Len(t, app.filters, 2)
require.Equal(t, "alice", app.filters[1].SearchTerm)
require.Equal(t, "alice-smith", app.filters[1].FromColumnValue)
require.Equal(t, "wbtkr3sozbohn6jhbbutbyd8mm", app.filters[1].FromId)
})
t.Run("should export everything for jobs queued before search terms were recorded", func(t *testing.T) {
app := &recordingExportApp{users: batchOfOne()}
data := jobData()
delete(data, "search_term")
_, _, _, err := getData(app)(data)
require.NoError(t, err)
require.Len(t, app.filters, 1)
require.Empty(t, app.filters[0].SearchTerm)
})
}