mattermost-community-enterp.../channels/app/report.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

308 lines
9.9 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"encoding/csv"
"fmt"
"net/http"
"strconv"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
)
func (a *App) SaveReportChunk(format string, prefix string, count int, reportData []model.ReportableObject) *model.AppError {
switch format {
case "csv":
return a.saveCSVChunk(prefix, count, reportData)
}
return model.NewAppError("SaveReportChunk", "app.save_report_chunk.unsupported_format", nil, "unsupported report format", http.StatusBadRequest)
}
func (a *App) saveCSVChunk(prefix string, count int, reportData []model.ReportableObject) *model.AppError {
var buf bytes.Buffer
w := csv.NewWriter(&buf)
for _, report := range reportData {
err := w.Write(report.ToReport())
if err != nil {
return model.NewAppError("saveCSVChunk", "app.save_csv_chunk.write_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
w.Flush()
if err := w.Error(); err != nil {
return model.NewAppError("saveCSVChunk", "app.save_csv_chunk.write_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
_, appErr := a.WriteFile(&buf, makeFilePath(prefix, count, "csv"))
return appErr
}
func (a *App) CompileReportChunks(format string, prefix string, numberOfChunks int, headers []string) *model.AppError {
switch format {
case "csv":
return a.compileCSVChunks(prefix, numberOfChunks, headers)
}
return model.NewAppError("CompileReportChunks", "app.compile_report_chunks.unsupported_format", nil, "", http.StatusBadRequest)
}
func (a *App) compileCSVChunks(prefix string, numberOfChunks int, headers []string) *model.AppError {
filePath := makeCompiledFilePath(prefix, "csv")
var compiledBuf bytes.Buffer
w := csv.NewWriter(&compiledBuf)
err := w.Write(headers)
if err != nil {
return model.NewAppError("compileCSVChunks", "app.compile_csv_chunks.header_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
w.Flush()
if err = w.Error(); err != nil {
return model.NewAppError("saveCSVChunk", "app.save_csv_chunk.write_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
for i := range numberOfChunks {
chunkFilePath := makeFilePath(prefix, i, "csv")
chunk, err := a.ReadFile(chunkFilePath)
if err != nil {
return err
}
_, writeErr := compiledBuf.Write(chunk)
if writeErr != nil {
return err
}
}
_, appErr := a.WriteFile(&compiledBuf, filePath)
if appErr != nil {
return appErr
}
return nil
}
func (a *App) SendReportToUser(rctx request.CTX, job *model.Job, format string) *model.AppError {
requestingUserId := job.Data["requesting_user_id"]
if requestingUserId == "" {
return model.NewAppError("SendReportToUser", "app.report.send_report_to_user.missing_user_id", nil, "", http.StatusInternalServerError)
}
dateRange := job.Data["date_range"]
if dateRange == "" {
return model.NewAppError("SendReportToUser", "app.report.send_report_to_user.missing_date_range", nil, "", http.StatusInternalServerError)
}
systemBot, err := a.GetSystemBot(rctx)
if err != nil {
return err
}
path := makeCompiledFilePath(job.Id, format)
size, err := a.FileSize(path)
if err != nil {
return err
}
fileInfo, fileErr := a.Srv().Store().FileInfo().Save(rctx, &model.FileInfo{
Name: makeCompiledFilename(job.Id, format),
Extension: format,
Size: size,
Path: path,
CreatorId: systemBot.UserId,
})
if fileErr != nil {
return model.NewAppError("SendReportToUser", "app.report.send_report_to_user.failed_to_save", nil, "", http.StatusInternalServerError).Wrap(fileErr)
}
channel, err := a.GetOrCreateDirectChannel(rctx, requestingUserId, systemBot.UserId)
if err != nil {
return err
}
user, err := a.GetUser(requestingUserId)
if err != nil {
return err
}
T := i18n.GetUserTranslations(user.Locale)
post := &model.Post{
ChannelId: channel.Id,
Message: T("app.report.send_report_to_user.export_finished", map[string]string{
"DateRange": getTranslatedDateRange(dateRange),
}),
Type: model.PostTypeDefault,
UserId: systemBot.UserId,
FileIds: []string{fileInfo.Id},
}
_, err = a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true})
return err
}
func (a *App) CleanupReportChunks(format string, prefix string, numberOfChunks int) *model.AppError {
switch format {
case "csv":
return a.cleanupCSVChunks(prefix, numberOfChunks)
}
return model.NewAppError("CompileReportChunks", "app.compile_report_chunks.unsupported_format", nil, "", http.StatusBadRequest)
}
func (a *App) cleanupCSVChunks(prefix string, numberOfChunks int) *model.AppError {
for i := range numberOfChunks {
chunkFilePath := makeFilePath(prefix, i, "csv")
if err := a.RemoveFile(chunkFilePath); err != nil {
return err
}
}
return nil
}
func makeFilePath(prefix string, count int, extension string) string {
return fmt.Sprintf("admin_reports/batch_report_%s__%d.%s", prefix, count, extension)
}
func makeCompiledFilePath(prefix string, extension string) string {
return fmt.Sprintf("admin_reports/%s", makeCompiledFilename(prefix, extension))
}
func makeCompiledFilename(prefix string, extension string) string {
return fmt.Sprintf("batch_report_%s.%s", prefix, extension)
}
func (a *App) GetUsersForReporting(filter *model.UserReportOptions) ([]*model.UserReport, *model.AppError) {
if appErr := filter.IsValid(); appErr != nil {
return nil, appErr
}
userReportQuery, err := a.Srv().Store().User().GetUserReport(filter)
if err != nil {
return nil, model.NewAppError("GetUsersForReporting", "app.report.get_user_report.store_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
userReports := make([]*model.UserReport, len(userReportQuery))
for i, user := range userReportQuery {
userReports[i] = user.ToReport()
}
return userReports, nil
}
func (a *App) GetUserCountForReport(filter *model.UserReportOptions) (*int64, *model.AppError) {
count, err := a.Srv().Store().User().GetUserCountForReport(filter)
if err != nil {
return nil, model.NewAppError("GetUserCountForReport", "app.report.get_user_count_for_report.store_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &count, nil
}
func (a *App) StartUsersBatchExport(rctx request.CTX, ro *model.UserReportOptions, startAt int64, endAt int64) *model.AppError {
if !model.MinimumProfessionalLicense(a.Srv().License()) {
return model.NewAppError("StartUsersBatchExport", "app.report.start_users_batch_export.license_error", nil, "", http.StatusBadRequest)
}
options := map[string]string{
"requesting_user_id": rctx.Session().UserId,
"date_range": ro.DateRange,
"role": ro.Role,
"team": ro.Team,
"hide_active": strconv.FormatBool(ro.HideActive),
"hide_inactive": strconv.FormatBool(ro.HideInactive),
"start_at": strconv.FormatInt(startAt, 10),
"end_at": strconv.FormatInt(endAt, 10),
}
// Check for existing jobs
if err := a.checkForExistingJobs(rctx, options, model.JobTypeExportUsersToCSV); err != nil {
return err
}
_, err := a.Srv().Jobs.CreateJob(rctx, model.JobTypeExportUsersToCSV, options)
if err != nil {
return err
}
a.Srv().Go(func() {
systemBot, err := a.GetSystemBot(rctx)
if err != nil {
rctx.Logger().Error("Failed to get the system bot", mlog.Err(err))
return
}
channel, err := a.GetOrCreateDirectChannel(rctx, rctx.Session().UserId, systemBot.UserId)
if err != nil {
rctx.Logger().Error("Failed to get or create the DM", mlog.Err(err))
return
}
user, err := a.GetUser(rctx.Session().UserId)
if err != nil {
rctx.Logger().Error("Failed to get the user", mlog.Err(err))
return
}
T := i18n.GetUserTranslations(user.Locale)
post := &model.Post{
ChannelId: channel.Id,
Message: T("app.report.start_users_batch_export.started_export", map[string]string{"DateRange": getTranslatedDateRange(ro.DateRange)}),
Type: model.PostTypeDefault,
UserId: systemBot.UserId,
}
if _, err := a.CreatePost(rctx, post, channel, model.CreatePostFlags{SetOnline: true}); err != nil {
rctx.Logger().Error("Failed to post batch export message", mlog.Err(err))
}
})
return nil
}
// Helper function to check for existing or pending jobs
func (a *App) checkForExistingJobs(rctx request.CTX, options map[string]string, jobType string) *model.AppError {
checkJobExists := func(jobs []*model.Job, options map[string]string) bool {
for _, job := range jobs {
if job.Data["date_range"] == options["date_range"] &&
job.Data["requesting_user_id"] == options["requesting_user_id"] &&
job.Data["role"] == options["role"] &&
job.Data["team"] == options["team"] &&
job.Data["hide_active"] == options["hide_active"] &&
job.Data["hide_inactive"] == options["hide_inactive"] {
return true
}
}
return false
}
pendingJobs, err := a.Srv().Jobs.GetJobsByTypeAndStatus(rctx, jobType, model.JobStatusPending)
if err != nil {
return err
}
if checkJobExists(pendingJobs, options) {
return model.NewAppError("StartUsersBatchExport", "app.report.start_users_batch_export.job_exists", nil, "", http.StatusBadRequest)
}
inProgressJobs, err := a.Srv().Jobs.GetJobsByTypeAndStatus(rctx, jobType, model.JobStatusInProgress)
if err != nil {
return err
}
if checkJobExists(inProgressJobs, options) {
return model.NewAppError("StartUsersBatchExport", "app.report.start_users_batch_export.job_exists", nil, "", http.StatusBadRequest)
}
return nil
}
func getTranslatedDateRange(dateRange string) string {
switch dateRange {
case model.ReportDurationLast30Days:
return i18n.T("app.report.date_range.last_30_days")
case model.ReportDurationPreviousMonth:
return i18n.T("app.report.date_range.previous_month")
case model.ReportDurationLast6Months:
return i18n.T("app.report.date_range.last_6_months")
default:
return i18n.T("app.report.date_range.all_time")
}
}