Files
mattermost/app/import_functions.go
Claudio Costa df906cad9d [MM-28422] Enable processing of import files through API (#16062)
* Implement unzip function

* Implement FileSize method

* Implement path rewriting for bulk import

* Small improvements

* Add ImportSettings to config

* Implement ListImports API endpoint

* Enable uploading import files

* Implement import process job

* Add missing license headers

* Address reviews

* Make path sanitization a bit smarter

* Clean path before calculating Dir

* [MM-30008] Add mmctl support for file imports (#16301)

* Add mmctl support for import files

* Improve test

* Remove unnecessary handlers

* Use th.TestForSystemAdminAndLocal

* Make nouser id a constant
2020-12-03 11:38:00 +01:00

1758 lines
53 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"bytes"
"context"
"crypto/sha1"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/store"
"github.com/mattermost/mattermost-server/v5/utils"
)
//
// -- Bulk Import Functions --
// These functions import data directly into the database. Security and permission checks are bypassed but validity is
// still enforced.
//
func (a *App) importScheme(data *SchemeImportData, dryRun bool) *model.AppError {
if err := validateSchemeImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
scheme, err := a.GetSchemeByName(*data.Name)
if err != nil {
scheme = new(model.Scheme)
} else if scheme.Scope != *data.Scope {
return model.NewAppError("BulkImport", "app.import.import_scheme.scope_change.error", map[string]interface{}{"SchemeName": scheme.Name}, "", http.StatusBadRequest)
}
scheme.Name = *data.Name
scheme.DisplayName = *data.DisplayName
scheme.Scope = *data.Scope
if data.Description != nil {
scheme.Description = *data.Description
}
if len(scheme.Id) == 0 {
scheme, err = a.CreateScheme(scheme)
} else {
scheme, err = a.UpdateScheme(scheme)
}
if err != nil {
return err
}
if scheme.Scope == model.SCHEME_SCOPE_TEAM {
data.DefaultTeamAdminRole.Name = &scheme.DefaultTeamAdminRole
if err := a.importRole(data.DefaultTeamAdminRole, dryRun, true); err != nil {
return err
}
data.DefaultTeamUserRole.Name = &scheme.DefaultTeamUserRole
if err := a.importRole(data.DefaultTeamUserRole, dryRun, true); err != nil {
return err
}
if data.DefaultTeamGuestRole == nil {
data.DefaultTeamGuestRole = &RoleImportData{
DisplayName: model.NewString("Team Guest Role for Scheme"),
}
}
data.DefaultTeamGuestRole.Name = &scheme.DefaultTeamGuestRole
if err := a.importRole(data.DefaultTeamGuestRole, dryRun, true); err != nil {
return err
}
}
if scheme.Scope == model.SCHEME_SCOPE_TEAM || scheme.Scope == model.SCHEME_SCOPE_CHANNEL {
data.DefaultChannelAdminRole.Name = &scheme.DefaultChannelAdminRole
if err := a.importRole(data.DefaultChannelAdminRole, dryRun, true); err != nil {
return err
}
data.DefaultChannelUserRole.Name = &scheme.DefaultChannelUserRole
if err := a.importRole(data.DefaultChannelUserRole, dryRun, true); err != nil {
return err
}
if data.DefaultChannelGuestRole == nil {
data.DefaultChannelGuestRole = &RoleImportData{
DisplayName: model.NewString("Channel Guest Role for Scheme"),
}
}
data.DefaultChannelGuestRole.Name = &scheme.DefaultChannelGuestRole
if err := a.importRole(data.DefaultChannelGuestRole, dryRun, true); err != nil {
return err
}
}
return nil
}
func (a *App) importRole(data *RoleImportData, dryRun bool, isSchemeRole bool) *model.AppError {
if !isSchemeRole {
if err := validateRoleImportData(data); err != nil {
return err
}
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
role, err := a.GetRoleByName(*data.Name)
if err != nil {
role = new(model.Role)
}
role.Name = *data.Name
if data.DisplayName != nil {
role.DisplayName = *data.DisplayName
}
if data.Description != nil {
role.Description = *data.Description
}
if data.Permissions != nil {
role.Permissions = *data.Permissions
}
if isSchemeRole {
role.SchemeManaged = true
} else {
role.SchemeManaged = false
}
if len(role.Id) == 0 {
_, err = a.CreateRole(role)
} else {
_, err = a.UpdateRole(role)
}
return err
}
func (a *App) importTeam(data *TeamImportData, dryRun bool) *model.AppError {
if err := validateTeamImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
var team *model.Team
team, err := a.Srv().Store.Team().GetByName(*data.Name)
if err != nil {
team = &model.Team{}
}
team.Name = *data.Name
team.DisplayName = *data.DisplayName
team.Type = *data.Type
if data.Description != nil {
team.Description = *data.Description
}
if data.AllowOpenInvite != nil {
team.AllowOpenInvite = *data.AllowOpenInvite
}
if data.Scheme != nil {
scheme, err := a.GetSchemeByName(*data.Scheme)
if err != nil {
return err
}
if scheme.DeleteAt != 0 {
return model.NewAppError("BulkImport", "app.import.import_team.scheme_deleted.error", nil, "", http.StatusBadRequest)
}
if scheme.Scope != model.SCHEME_SCOPE_TEAM {
return model.NewAppError("BulkImport", "app.import.import_team.scheme_wrong_scope.error", nil, "", http.StatusBadRequest)
}
team.SchemeId = &scheme.Id
}
if team.Id == "" {
if _, err := a.CreateTeam(team); err != nil {
return err
}
} else {
if _, err := a.updateTeamUnsanitized(team); err != nil {
return err
}
}
return nil
}
func (a *App) importChannel(data *ChannelImportData, dryRun bool) *model.AppError {
if err := validateChannelImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
team, err := a.Srv().Store.Team().GetByName(*data.Team)
if err != nil {
return model.NewAppError("BulkImport", "app.import.import_channel.team_not_found.error", map[string]interface{}{"TeamName": *data.Team}, err.Error(), http.StatusBadRequest)
}
var channel *model.Channel
if result, err := a.Srv().Store.Channel().GetByNameIncludeDeleted(team.Id, *data.Name, true); err == nil {
channel = result
} else {
channel = &model.Channel{}
}
channel.TeamId = team.Id
channel.Name = *data.Name
channel.DisplayName = *data.DisplayName
channel.Type = *data.Type
if data.Header != nil {
channel.Header = *data.Header
}
if data.Purpose != nil {
channel.Purpose = *data.Purpose
}
if data.Scheme != nil {
scheme, err := a.GetSchemeByName(*data.Scheme)
if err != nil {
return err
}
if scheme.DeleteAt != 0 {
return model.NewAppError("BulkImport", "app.import.import_channel.scheme_deleted.error", nil, "", http.StatusBadRequest)
}
if scheme.Scope != model.SCHEME_SCOPE_CHANNEL {
return model.NewAppError("BulkImport", "app.import.import_channel.scheme_wrong_scope.error", nil, "", http.StatusBadRequest)
}
channel.SchemeId = &scheme.Id
}
if channel.Id == "" {
if _, err := a.CreateChannel(channel, false); err != nil {
return err
}
} else {
if _, err := a.UpdateChannel(channel); err != nil {
return err
}
}
return nil
}
func (a *App) importUser(data *UserImportData, dryRun bool) *model.AppError {
if err := validateUserImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
// We want to avoid database writes if nothing has changed.
hasUserChanged := false
hasNotifyPropsChanged := false
hasUserRolesChanged := false
hasUserAuthDataChanged := false
hasUserEmailVerifiedChanged := false
var user *model.User
var nErr error
user, nErr = a.Srv().Store.User().GetByUsername(*data.Username)
if nErr != nil {
user = &model.User{}
user.MakeNonNil()
user.SetDefaultNotifications()
hasUserChanged = true
}
user.Username = *data.Username
if user.Email != *data.Email {
hasUserChanged = true
hasUserEmailVerifiedChanged = true // Changing the email resets email verified to false by default.
user.Email = *data.Email
user.Email = strings.ToLower(user.Email)
}
var password string
var authService string
var authData *string
if data.AuthService != nil {
if user.AuthService != *data.AuthService {
hasUserAuthDataChanged = true
}
authService = *data.AuthService
}
// AuthData and Password are mutually exclusive.
if data.AuthData != nil {
if user.AuthData == nil || *user.AuthData != *data.AuthData {
hasUserAuthDataChanged = true
}
authData = data.AuthData
password = ""
} else if data.Password != nil {
password = *data.Password
authData = nil
} else {
// If no AuthData or Password is specified, we must generate a password.
password = model.GeneratePassword(*a.Config().PasswordSettings.MinimumLength)
authData = nil
}
user.Password = password
user.AuthService = authService
user.AuthData = authData
// Automatically assume all emails are verified.
emailVerified := true
if user.EmailVerified != emailVerified {
user.EmailVerified = emailVerified
hasUserEmailVerifiedChanged = true
}
if data.Nickname != nil {
if user.Nickname != *data.Nickname {
user.Nickname = *data.Nickname
hasUserChanged = true
}
}
if data.FirstName != nil {
if user.FirstName != *data.FirstName {
user.FirstName = *data.FirstName
hasUserChanged = true
}
}
if data.LastName != nil {
if user.LastName != *data.LastName {
user.LastName = *data.LastName
hasUserChanged = true
}
}
if data.Position != nil {
if user.Position != *data.Position {
user.Position = *data.Position
hasUserChanged = true
}
}
if data.Locale != nil {
if user.Locale != *data.Locale {
user.Locale = *data.Locale
hasUserChanged = true
}
} else {
if user.Locale != *a.Config().LocalizationSettings.DefaultClientLocale {
user.Locale = *a.Config().LocalizationSettings.DefaultClientLocale
hasUserChanged = true
}
}
if data.DeleteAt != nil {
if user.DeleteAt != *data.DeleteAt {
user.DeleteAt = *data.DeleteAt
hasUserChanged = true
}
}
var roles string
if data.Roles != nil {
if user.Roles != *data.Roles {
roles = *data.Roles
hasUserRolesChanged = true
}
} else if len(user.Roles) == 0 {
// Set SYSTEM_USER roles on newly created users by default.
if user.Roles != model.SYSTEM_USER_ROLE_ID {
roles = model.SYSTEM_USER_ROLE_ID
hasUserRolesChanged = true
}
}
user.Roles = roles
if data.NotifyProps != nil {
if data.NotifyProps.Desktop != nil {
if value, ok := user.NotifyProps[model.DESKTOP_NOTIFY_PROP]; !ok || value != *data.NotifyProps.Desktop {
user.AddNotifyProp(model.DESKTOP_NOTIFY_PROP, *data.NotifyProps.Desktop)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.DesktopSound != nil {
if value, ok := user.NotifyProps[model.DESKTOP_SOUND_NOTIFY_PROP]; !ok || value != *data.NotifyProps.DesktopSound {
user.AddNotifyProp(model.DESKTOP_SOUND_NOTIFY_PROP, *data.NotifyProps.DesktopSound)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.Email != nil {
if value, ok := user.NotifyProps[model.EMAIL_NOTIFY_PROP]; !ok || value != *data.NotifyProps.Email {
user.AddNotifyProp(model.EMAIL_NOTIFY_PROP, *data.NotifyProps.Email)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.Mobile != nil {
if value, ok := user.NotifyProps[model.PUSH_NOTIFY_PROP]; !ok || value != *data.NotifyProps.Mobile {
user.AddNotifyProp(model.PUSH_NOTIFY_PROP, *data.NotifyProps.Mobile)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.MobilePushStatus != nil {
if value, ok := user.NotifyProps[model.PUSH_STATUS_NOTIFY_PROP]; !ok || value != *data.NotifyProps.MobilePushStatus {
user.AddNotifyProp(model.PUSH_STATUS_NOTIFY_PROP, *data.NotifyProps.MobilePushStatus)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.ChannelTrigger != nil {
if value, ok := user.NotifyProps[model.CHANNEL_MENTIONS_NOTIFY_PROP]; !ok || value != *data.NotifyProps.ChannelTrigger {
user.AddNotifyProp(model.CHANNEL_MENTIONS_NOTIFY_PROP, *data.NotifyProps.ChannelTrigger)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.CommentsTrigger != nil {
if value, ok := user.NotifyProps[model.COMMENTS_NOTIFY_PROP]; !ok || value != *data.NotifyProps.CommentsTrigger {
user.AddNotifyProp(model.COMMENTS_NOTIFY_PROP, *data.NotifyProps.CommentsTrigger)
hasNotifyPropsChanged = true
}
}
if data.NotifyProps.MentionKeys != nil {
if value, ok := user.NotifyProps[model.MENTION_KEYS_NOTIFY_PROP]; !ok || value != *data.NotifyProps.MentionKeys {
user.AddNotifyProp(model.MENTION_KEYS_NOTIFY_PROP, *data.NotifyProps.MentionKeys)
hasNotifyPropsChanged = true
}
} else {
user.UpdateMentionKeysFromUsername("")
}
}
var savedUser *model.User
var err *model.AppError
if user.Id == "" {
if savedUser, err = a.createUser(user); err != nil {
return err
}
} else {
if hasUserChanged {
if savedUser, err = a.UpdateUser(user, false); err != nil {
return err
}
}
if hasUserRolesChanged {
if savedUser, err = a.UpdateUserRoles(user.Id, roles, false); err != nil {
return err
}
}
if hasNotifyPropsChanged {
if savedUser, err = a.UpdateUserNotifyProps(user.Id, user.NotifyProps); err != nil {
return err
}
}
if len(password) > 0 {
if err = a.UpdatePassword(user, password); err != nil {
return err
}
} else {
if hasUserAuthDataChanged {
if _, nErr := a.Srv().Store.User().UpdateAuthData(user.Id, authService, authData, user.Email, false); nErr != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(nErr, &invErr):
return model.NewAppError("importUser", "app.user.update_auth_data.email_exists.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return model.NewAppError("importUser", "app.user.update_auth_data.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
}
}
if emailVerified {
if hasUserEmailVerifiedChanged {
if err := a.VerifyUserEmail(user.Id, user.Email); err != nil {
return err
}
}
}
}
if savedUser == nil {
savedUser = user
}
if data.ProfileImage != nil {
file, err := os.Open(*data.ProfileImage)
if err != nil {
mlog.Error("Unable to open the profile image.", mlog.Any("err", err))
}
defer file.Close()
if err := a.SetProfileImageFromMultiPartFile(savedUser.Id, file); err != nil {
mlog.Error("Unable to set the profile image from a file.", mlog.Any("err", err))
}
}
// Preferences.
var preferences model.Preferences
if data.Theme != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_THEME,
Name: "",
Value: *data.Theme,
})
}
if data.UseMilitaryTime != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS,
Name: model.PREFERENCE_NAME_USE_MILITARY_TIME,
Value: *data.UseMilitaryTime,
})
}
if data.CollapsePreviews != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS,
Name: model.PREFERENCE_NAME_COLLAPSE_SETTING,
Value: *data.CollapsePreviews,
})
}
if data.MessageDisplay != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS,
Name: model.PREFERENCE_NAME_MESSAGE_DISPLAY,
Value: *data.MessageDisplay,
})
}
if data.ChannelDisplayMode != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS,
Name: "channel_display_mode",
Value: *data.ChannelDisplayMode,
})
}
if data.TutorialStep != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS,
Name: savedUser.Id,
Value: *data.TutorialStep,
})
}
if data.UseMarkdownPreview != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS,
Name: "feature_enabled_markdown_preview",
Value: *data.UseMarkdownPreview,
})
}
if data.UseFormatting != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_ADVANCED_SETTINGS,
Name: "formatting",
Value: *data.UseFormatting,
})
}
if data.ShowUnreadSection != nil {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_SIDEBAR_SETTINGS,
Name: "show_unread_section",
Value: *data.ShowUnreadSection,
})
}
if data.EmailInterval != nil || savedUser.NotifyProps[model.EMAIL_NOTIFY_PROP] == "false" {
var intervalSeconds string
if value := savedUser.NotifyProps[model.EMAIL_NOTIFY_PROP]; value == "false" {
intervalSeconds = "0"
} else {
switch *data.EmailInterval {
case model.PREFERENCE_EMAIL_INTERVAL_IMMEDIATELY:
intervalSeconds = model.PREFERENCE_EMAIL_INTERVAL_NO_BATCHING_SECONDS
case model.PREFERENCE_EMAIL_INTERVAL_FIFTEEN:
intervalSeconds = model.PREFERENCE_EMAIL_INTERVAL_FIFTEEN_AS_SECONDS
case model.PREFERENCE_EMAIL_INTERVAL_HOUR:
intervalSeconds = model.PREFERENCE_EMAIL_INTERVAL_HOUR_AS_SECONDS
}
}
if intervalSeconds != "" {
preferences = append(preferences, model.Preference{
UserId: savedUser.Id,
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
Value: intervalSeconds,
})
}
}
if len(preferences) > 0 {
if err := a.Srv().Store.Preference().Save(&preferences); err != nil {
return model.NewAppError("BulkImport", "app.import.import_user.save_preferences.error", nil, err.Error(), http.StatusInternalServerError)
}
}
return a.importUserTeams(savedUser, data.Teams)
}
func (a *App) importUserTeams(user *model.User, data *[]UserTeamImportData) *model.AppError {
if data == nil {
return nil
}
teamNames := []string{}
for _, tdata := range *data {
teamNames = append(teamNames, *tdata.Name)
}
allTeams, err := a.getTeamsByNames(teamNames)
if err != nil {
return err
}
teamThemePreferencesByID := map[string]model.Preferences{}
channels := map[string][]UserChannelImportData{}
teamsByID := map[string]*model.Team{}
teamMemberByTeamID := map[string]*model.TeamMember{}
newTeamMembers := []*model.TeamMember{}
oldTeamMembers := []*model.TeamMember{}
rolesByTeamId := map[string]string{}
isGuestByTeamId := map[string]bool{}
isUserByTeamId := map[string]bool{}
isAdminByTeamId := map[string]bool{}
existingMemberships, nErr := a.Srv().Store.Team().GetTeamsForUser(context.Background(), user.Id)
if nErr != nil {
return model.NewAppError("importUserTeams", "app.team.get_members.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
existingMembershipsByTeamId := map[string]*model.TeamMember{}
for _, teamMembership := range existingMemberships {
existingMembershipsByTeamId[teamMembership.TeamId] = teamMembership
}
for _, tdata := range *data {
team := allTeams[*tdata.Name]
// Team-specific theme Preferences.
if tdata.Theme != nil {
teamThemePreferencesByID[team.Id] = append(teamThemePreferencesByID[team.Id], model.Preference{
UserId: user.Id,
Category: model.PREFERENCE_CATEGORY_THEME,
Name: team.Id,
Value: *tdata.Theme,
})
}
isGuestByTeamId[team.Id] = false
isUserByTeamId[team.Id] = true
isAdminByTeamId[team.Id] = false
if tdata.Roles == nil {
isUserByTeamId[team.Id] = true
} else {
rawRoles := *tdata.Roles
explicitRoles := []string{}
for _, role := range strings.Fields(rawRoles) {
if role == model.TEAM_GUEST_ROLE_ID {
isGuestByTeamId[team.Id] = true
isUserByTeamId[team.Id] = false
} else if role == model.TEAM_USER_ROLE_ID {
isUserByTeamId[team.Id] = true
} else if role == model.TEAM_ADMIN_ROLE_ID {
isAdminByTeamId[team.Id] = true
} else {
explicitRoles = append(explicitRoles, role)
}
}
rolesByTeamId[team.Id] = strings.Join(explicitRoles, " ")
}
member := &model.TeamMember{
TeamId: team.Id,
UserId: user.Id,
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: team.Email == user.Email && !user.IsGuest(),
}
if !user.IsGuest() {
var userShouldBeAdmin bool
userShouldBeAdmin, err = a.UserIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
if err != nil {
return err
}
member.SchemeAdmin = userShouldBeAdmin
}
if tdata.Channels != nil {
channels[team.Id] = append(channels[team.Id], *tdata.Channels...)
}
if !user.IsGuest() {
channels[team.Id] = append(channels[team.Id], UserChannelImportData{Name: model.NewString(model.DEFAULT_CHANNEL)})
}
teamsByID[team.Id] = team
teamMemberByTeamID[team.Id] = member
if _, ok := existingMembershipsByTeamId[team.Id]; !ok {
newTeamMembers = append(newTeamMembers, member)
} else {
oldTeamMembers = append(oldTeamMembers, member)
}
}
oldMembers, nErr := a.Srv().Store.Team().UpdateMultipleMembers(oldTeamMembers)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("importUserTeams", "app.team.save_member.save.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
newMembers := []*model.TeamMember{}
if len(newTeamMembers) > 0 {
var nErr error
newMembers, nErr = a.Srv().Store.Team().SaveMultipleMembers(newTeamMembers, *a.Config().TeamSettings.MaxUsersPerTeam)
if nErr != nil {
var appErr *model.AppError
var conflictErr *store.ErrConflict
var limitExeededErr *store.ErrLimitExceeded
switch {
case errors.As(nErr, &appErr): // in case we haven't converted to plain error.
return appErr
case errors.As(nErr, &conflictErr):
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_members.conflict.app_error", nil, nErr.Error(), http.StatusBadRequest)
case errors.As(nErr, &limitExeededErr):
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_members.max_accounts.app_error", nil, nErr.Error(), http.StatusBadRequest)
default: // last fallback in case it doesn't map to an existing app error.
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_members.error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
}
for _, member := range append(newMembers, oldMembers...) {
if member.ExplicitRoles != rolesByTeamId[member.TeamId] {
if _, err = a.UpdateTeamMemberRoles(member.TeamId, user.Id, rolesByTeamId[member.TeamId]); err != nil {
return err
}
}
a.UpdateTeamMemberSchemeRoles(member.TeamId, user.Id, isGuestByTeamId[member.TeamId], isUserByTeamId[member.TeamId], isAdminByTeamId[member.TeamId])
}
for _, team := range allTeams {
if len(teamThemePreferencesByID[team.Id]) > 0 {
pref := teamThemePreferencesByID[team.Id]
if err := a.Srv().Store.Preference().Save(&pref); err != nil {
return model.NewAppError("BulkImport", "app.import.import_user_teams.save_preferences.error", nil, err.Error(), http.StatusInternalServerError)
}
}
channelsToImport := channels[team.Id]
if err := a.importUserChannels(user, team, teamMemberByTeamID[team.Id], &channelsToImport); err != nil {
return err
}
}
return nil
}
func (a *App) importUserChannels(user *model.User, team *model.Team, teamMember *model.TeamMember, data *[]UserChannelImportData) *model.AppError {
if data == nil {
return nil
}
channelNames := []string{}
for _, tdata := range *data {
channelNames = append(channelNames, *tdata.Name)
}
allChannels, err := a.getChannelsByNames(channelNames, team.Id)
if err != nil {
return err
}
channelsByID := map[string]*model.Channel{}
channelMemberByChannelID := map[string]*model.ChannelMember{}
newChannelMembers := []*model.ChannelMember{}
oldChannelMembers := []*model.ChannelMember{}
rolesByChannelId := map[string]string{}
channelPreferencesByID := map[string]model.Preferences{}
isGuestByChannelId := map[string]bool{}
isUserByChannelId := map[string]bool{}
isAdminByChannelId := map[string]bool{}
existingMemberships, nErr := a.Srv().Store.Channel().GetMembersForUser(team.Id, user.Id)
if nErr != nil {
return model.NewAppError("importUserChannels", "app.channel.get_members.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
existingMembershipsByChannelId := map[string]model.ChannelMember{}
for _, channelMembership := range *existingMemberships {
existingMembershipsByChannelId[channelMembership.ChannelId] = channelMembership
}
for _, cdata := range *data {
channel, ok := allChannels[*cdata.Name]
if !ok {
return model.NewAppError("BulkImport", "app.import.import_user_channels.channel_not_found.error", nil, "", http.StatusInternalServerError)
}
if _, ok = channelsByID[channel.Id]; ok && *cdata.Name == model.DEFAULT_CHANNEL {
// town-square membership was in the import and added by the importer (skip the added by the importer)
continue
}
isGuestByChannelId[channel.Id] = false
isUserByChannelId[channel.Id] = true
isAdminByChannelId[channel.Id] = false
if cdata.Roles == nil {
isUserByChannelId[channel.Id] = true
} else {
rawRoles := *cdata.Roles
explicitRoles := []string{}
for _, role := range strings.Fields(rawRoles) {
if role == model.CHANNEL_GUEST_ROLE_ID {
isGuestByChannelId[channel.Id] = true
isUserByChannelId[channel.Id] = false
} else if role == model.CHANNEL_USER_ROLE_ID {
isUserByChannelId[channel.Id] = true
} else if role == model.CHANNEL_ADMIN_ROLE_ID {
isAdminByChannelId[channel.Id] = true
} else {
explicitRoles = append(explicitRoles, role)
}
}
rolesByChannelId[channel.Id] = strings.Join(explicitRoles, " ")
}
if cdata.Favorite != nil && *cdata.Favorite {
channelPreferencesByID[channel.Id] = append(channelPreferencesByID[channel.Id], model.Preference{
UserId: user.Id,
Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL,
Name: channel.Id,
Value: "true",
})
}
member := &model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: user.IsGuest(),
SchemeUser: !user.IsGuest(),
SchemeAdmin: false,
}
if !user.IsGuest() {
var userShouldBeAdmin bool
userShouldBeAdmin, err = a.UserIsInAdminRoleGroup(user.Id, team.Id, model.GroupSyncableTypeTeam)
if err != nil {
return err
}
member.SchemeAdmin = userShouldBeAdmin
}
if cdata.NotifyProps != nil {
if cdata.NotifyProps.Desktop != nil {
member.NotifyProps[model.DESKTOP_NOTIFY_PROP] = *cdata.NotifyProps.Desktop
}
if cdata.NotifyProps.Mobile != nil {
member.NotifyProps[model.PUSH_NOTIFY_PROP] = *cdata.NotifyProps.Mobile
}
if cdata.NotifyProps.MarkUnread != nil {
member.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] = *cdata.NotifyProps.MarkUnread
}
}
channelsByID[channel.Id] = channel
channelMemberByChannelID[channel.Id] = member
if _, ok := existingMembershipsByChannelId[channel.Id]; !ok {
newChannelMembers = append(newChannelMembers, member)
} else {
oldChannelMembers = append(oldChannelMembers, member)
}
}
oldMembers, nErr := a.Srv().Store.Channel().UpdateMultipleMembers(oldChannelMembers)
if nErr != nil {
var nfErr *store.ErrNotFound
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return appErr
case errors.As(nErr, &nfErr):
return model.NewAppError("importUserChannels", MISSING_CHANNEL_MEMBER_ERROR, nil, nfErr.Error(), http.StatusNotFound)
default:
return model.NewAppError("importUserChannels", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
newMembers := []*model.ChannelMember{}
if len(newChannelMembers) > 0 {
newMembers, nErr = a.Srv().Store.Channel().SaveMultipleMembers(newChannelMembers)
if nErr != nil {
var cErr *store.ErrConflict
var appErr *model.AppError
switch {
case errors.As(nErr, &cErr):
switch cErr.Resource {
case "ChannelMembers":
return model.NewAppError("importUserChannels", "app.channel.save_member.exists.app_error", nil, cErr.Error(), http.StatusBadRequest)
}
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("importUserChannels", "app.channel.create_direct_channel.internal_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
}
for _, member := range append(newMembers, oldMembers...) {
if member.ExplicitRoles != rolesByChannelId[member.ChannelId] {
if _, err = a.UpdateChannelMemberRoles(member.ChannelId, user.Id, rolesByChannelId[member.ChannelId]); err != nil {
return err
}
}
a.UpdateChannelMemberSchemeRoles(member.ChannelId, user.Id, isGuestByChannelId[member.ChannelId], isUserByChannelId[member.ChannelId], isAdminByChannelId[member.ChannelId])
}
for _, channel := range allChannels {
if len(channelPreferencesByID[channel.Id]) > 0 {
pref := channelPreferencesByID[channel.Id]
if err := a.Srv().Store.Preference().Save(&pref); err != nil {
return model.NewAppError("BulkImport", "app.import.import_user_channels.save_preferences.error", nil, err.Error(), http.StatusInternalServerError)
}
}
}
return nil
}
func (a *App) importReaction(data *ReactionImportData, post *model.Post, dryRun bool) *model.AppError {
if err := validateReactionImportData(data, post.CreateAt); err != nil {
return err
}
var user *model.User
var nErr error
if user, nErr = a.Srv().Store.User().GetByUsername(*data.User); nErr != nil {
return model.NewAppError("BulkImport", "app.import.import_post.user_not_found.error", map[string]interface{}{"Username": data.User}, nErr.Error(), http.StatusBadRequest)
}
reaction := &model.Reaction{
UserId: user.Id,
PostId: post.Id,
EmojiName: *data.EmojiName,
CreateAt: *data.CreateAt,
}
if _, nErr = a.Srv().Store.Reaction().Save(reaction); nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return appErr
default:
return model.NewAppError("importReaction", "app.reaction.save.save.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
return nil
}
func (a *App) importReplies(data []ReplyImportData, post *model.Post, teamId string, dryRun bool) *model.AppError {
var err *model.AppError
usernames := []string{}
for _, replyData := range data {
replyData := replyData
if err = validateReplyImportData(&replyData, post.CreateAt, a.MaxPostSize()); err != nil {
return err
}
usernames = append(usernames, *replyData.User)
}
users, err := a.getUsersByUsernames(usernames)
if err != nil {
return err
}
postsWithData := []postAndData{}
postsForCreateList := []*model.Post{}
postsForOverwriteList := []*model.Post{}
for _, replyData := range data {
replyData := replyData
user := users[*replyData.User]
// Check if this post already exists.
replies, nErr := a.Srv().Store.Post().GetPostsCreatedAt(post.ChannelId, *replyData.CreateAt)
if nErr != nil {
return model.NewAppError("importReplies", "app.post.get_posts_created_at.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
var reply *model.Post
for _, r := range replies {
if r.Message == *replyData.Message && r.RootId == post.Id {
reply = r
break
}
}
if reply == nil {
reply = &model.Post{}
}
reply.UserId = user.Id
reply.ChannelId = post.ChannelId
reply.ParentId = post.Id
reply.RootId = post.Id
reply.Message = *replyData.Message
reply.CreateAt = *replyData.CreateAt
fileIds, err := a.uploadAttachments(replyData.Attachments, reply, teamId, dryRun)
if err != nil {
return err
}
for _, fileID := range reply.FileIds {
if _, ok := fileIds[fileID]; !ok {
a.Srv().Store.FileInfo().PermanentDelete(fileID)
}
}
reply.FileIds = make([]string, 0)
for fileID := range fileIds {
reply.FileIds = append(reply.FileIds, fileID)
}
if len(reply.Id) == 0 {
postsForCreateList = append(postsForCreateList, reply)
} else {
postsForOverwriteList = append(postsForOverwriteList, reply)
}
postsWithData = append(postsWithData, postAndData{post: reply, replyData: &replyData})
}
if len(postsForCreateList) > 0 {
if _, _, err := a.Srv().Store.Post().SaveMultiple(postsForCreateList); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &appErr):
return appErr
case errors.As(err, &invErr):
return model.NewAppError("importReplies", "app.post.save.existing.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
return model.NewAppError("importReplies", "app.post.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
}
if _, _, nErr := a.Srv().Store.Post().OverwriteMultiple(postsForOverwriteList); nErr != nil {
return model.NewAppError("importReplies", "app.post.overwrite.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
for _, postWithData := range postsWithData {
a.updateFileInfoWithPostId(postWithData.post)
}
return nil
}
func (a *App) importAttachment(data *AttachmentImportData, post *model.Post, teamId string, dryRun bool) (*model.FileInfo, *model.AppError) {
file, err := os.Open(*data.Path)
if file == nil || err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.bad_file.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
timestamp := utils.TimeFromMillis(post.CreateAt)
buf := bytes.NewBuffer(nil)
_, _ = io.Copy(buf, file)
// Go over existing files in the post and see if there already exists a file with the same name, size and hash. If so - skip it
if post.Id != "" {
oldFiles, err := a.GetFileInfosForPost(post.Id, true)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
for _, oldFile := range oldFiles {
if oldFile.Name != path.Base(file.Name()) || oldFile.Size != int64(buf.Len()) {
continue
}
// check md5
newHash := sha1.Sum(buf.Bytes())
oldFileData, err := a.GetFile(oldFile.Id)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.attachment.file_upload.error", map[string]interface{}{"FilePath": *data.Path}, "", http.StatusBadRequest)
}
oldHash := sha1.Sum(oldFileData)
if bytes.Equal(oldHash[:], newHash[:]) {
mlog.Info("Skipping uploading of file because name already exists", mlog.Any("file_name", file.Name()))
return oldFile, nil
}
}
}
fileInfo, appErr := a.DoUploadFile(timestamp, teamId, post.ChannelId, post.UserId, file.Name(), buf.Bytes())
if appErr != nil {
mlog.Error("Failed to upload file:", mlog.Err(appErr))
return nil, appErr
}
a.HandleImages([]string{fileInfo.PreviewPath}, []string{fileInfo.ThumbnailPath}, [][]byte{buf.Bytes()})
mlog.Info("Uploading file with name", mlog.String("file_name", file.Name()))
return fileInfo, nil
}
type postAndData struct {
post *model.Post
postData *PostImportData
directPostData *DirectPostImportData
replyData *ReplyImportData
team *model.Team
lineNumber int
}
func (a *App) getUsersByUsernames(usernames []string) (map[string]*model.User, *model.AppError) {
uniqueUsernames := utils.RemoveDuplicatesFromStringArray(usernames)
allUsers, err := a.Srv().Store.User().GetProfilesByUsernames(uniqueUsernames, nil)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.get_users_by_username.some_users_not_found.error", nil, err.Error(), http.StatusBadRequest)
}
if len(allUsers) != len(uniqueUsernames) {
return nil, model.NewAppError("BulkImport", "app.import.get_users_by_username.some_users_not_found.error", nil, "", http.StatusBadRequest)
}
users := make(map[string]*model.User)
for _, user := range allUsers {
users[user.Username] = user
}
return users, nil
}
func (a *App) getTeamsByNames(names []string) (map[string]*model.Team, *model.AppError) {
allTeams, err := a.Srv().Store.Team().GetByNames(names)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.get_teams_by_names.some_teams_not_found.error", nil, err.Error(), http.StatusBadRequest)
}
teams := make(map[string]*model.Team)
for _, team := range allTeams {
teams[team.Name] = team
}
return teams, nil
}
func (a *App) getChannelsByNames(names []string, teamId string) (map[string]*model.Channel, *model.AppError) {
allChannels, err := a.Srv().Store.Channel().GetByNames(teamId, names, true)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.get_teams_by_names.some_teams_not_found.error", nil, err.Error(), http.StatusBadRequest)
}
channels := make(map[string]*model.Channel)
for _, channel := range allChannels {
channels[channel.Name] = channel
}
return channels, nil
}
func (a *App) getChannelsForPosts(teams map[string]*model.Team, data []*PostImportData) (map[string]*model.Channel, *model.AppError) {
channels := make(map[string]*model.Channel)
for _, postData := range data {
team := teams[*postData.Team]
if channel, ok := channels[*postData.Channel]; !ok || channel == nil {
var err error
channel, err = a.Srv().Store.Channel().GetByName(team.Id, *postData.Channel, true)
if err != nil {
return nil, model.NewAppError("BulkImport", "app.import.import_post.channel_not_found.error", map[string]interface{}{"ChannelName": *postData.Channel}, err.Error(), http.StatusBadRequest)
}
channels[*postData.Channel] = channel
}
}
return channels, nil
}
// getPostStrID returns a string ID composed of several post fields to
// uniquely identify a post before it's imported, so it has no ID yet
func getPostStrID(post *model.Post) string {
return fmt.Sprintf("%d%s%s", post.CreateAt, post.ChannelId, post.Message)
}
// importMultiplePostLines will return an error and the line that
// caused it whenever possible
func (a *App) importMultiplePostLines(lines []LineImportWorkerData, dryRun bool) (int, *model.AppError) {
if len(lines) == 0 {
return 0, nil
}
for _, line := range lines {
if err := validatePostImportData(line.Post, a.MaxPostSize()); err != nil {
return line.LineNumber, err
}
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return 0, nil
}
usernames := []string{}
teamNames := make([]string, len(lines))
postsData := make([]*PostImportData, len(lines))
for i, line := range lines {
usernames = append(usernames, *line.Post.User)
if line.Post.FlaggedBy != nil {
usernames = append(usernames, *line.Post.FlaggedBy...)
}
teamNames[i] = *line.Post.Team
postsData[i] = line.Post
}
users, err := a.getUsersByUsernames(usernames)
if err != nil {
return 0, err
}
teams, err := a.getTeamsByNames(teamNames)
if err != nil {
return 0, err
}
channels, err := a.getChannelsForPosts(teams, postsData)
if err != nil {
return 0, err
}
postsWithData := []postAndData{}
postsForCreateList := []*model.Post{}
postsForCreateMap := map[string]int{}
postsForOverwriteList := []*model.Post{}
postsForOverwriteMap := map[string]int{}
for _, line := range lines {
team := teams[*line.Post.Team]
channel := channels[*line.Post.Channel]
user := users[*line.Post.User]
// Check if this post already exists.
posts, nErr := a.Srv().Store.Post().GetPostsCreatedAt(channel.Id, *line.Post.CreateAt)
if nErr != nil {
return line.LineNumber, model.NewAppError("importMultiplePostLines", "app.post.get_posts_created_at.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
var post *model.Post
for _, p := range posts {
if p.Message == *line.Post.Message {
post = p
break
}
}
if post == nil {
post = &model.Post{}
}
post.ChannelId = channel.Id
post.Message = *line.Post.Message
post.UserId = user.Id
post.CreateAt = *line.Post.CreateAt
post.Hashtags, _ = model.ParseHashtags(post.Message)
if line.Post.Props != nil {
post.Props = *line.Post.Props
}
fileIds, appErr := a.uploadAttachments(line.Post.Attachments, post, team.Id, dryRun)
if appErr != nil {
return line.LineNumber, appErr
}
for _, fileID := range post.FileIds {
if _, ok := fileIds[fileID]; !ok {
a.Srv().Store.FileInfo().PermanentDelete(fileID)
}
}
post.FileIds = make([]string, 0)
for fileID := range fileIds {
post.FileIds = append(post.FileIds, fileID)
}
if len(post.Id) == 0 {
postsForCreateList = append(postsForCreateList, post)
postsForCreateMap[getPostStrID(post)] = line.LineNumber
} else {
postsForOverwriteList = append(postsForOverwriteList, post)
postsForOverwriteMap[getPostStrID(post)] = line.LineNumber
}
postsWithData = append(postsWithData, postAndData{post: post, postData: line.Post, team: team, lineNumber: line.LineNumber})
}
if len(postsForCreateList) > 0 {
if _, idx, nErr := a.Srv().Store.Post().SaveMultiple(postsForCreateList); nErr != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var retErr *model.AppError
switch {
case errors.As(nErr, &appErr):
retErr = appErr
case errors.As(nErr, &invErr):
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.existing.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
if idx != -1 && idx < len(postsForCreateList) {
post := postsForCreateList[idx]
if lineNumber, ok := postsForCreateMap[getPostStrID(post)]; ok {
return lineNumber, retErr
}
}
return 0, retErr
}
}
if _, idx, err := a.Srv().Store.Post().OverwriteMultiple(postsForOverwriteList); err != nil {
if idx != -1 && idx < len(postsForOverwriteList) {
post := postsForOverwriteList[idx]
if lineNumber, ok := postsForOverwriteMap[getPostStrID(post)]; ok {
return lineNumber, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return 0, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, postWithData := range postsWithData {
postWithData := postWithData
if postWithData.postData.FlaggedBy != nil {
var preferences model.Preferences
for _, username := range *postWithData.postData.FlaggedBy {
user := users[username]
preferences = append(preferences, model.Preference{
UserId: user.Id,
Category: model.PREFERENCE_CATEGORY_FLAGGED_POST,
Name: postWithData.post.Id,
Value: "true",
})
}
if len(preferences) > 0 {
if err := a.Srv().Store.Preference().Save(&preferences); err != nil {
return postWithData.lineNumber, model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, err.Error(), http.StatusInternalServerError)
}
}
}
if postWithData.postData.Reactions != nil {
for _, reaction := range *postWithData.postData.Reactions {
reaction := reaction
if err := a.importReaction(&reaction, postWithData.post, dryRun); err != nil {
return postWithData.lineNumber, err
}
}
}
if postWithData.postData.Replies != nil && len(*postWithData.postData.Replies) > 0 {
err := a.importReplies(*postWithData.postData.Replies, postWithData.post, postWithData.team.Id, dryRun)
if err != nil {
return postWithData.lineNumber, err
}
}
a.updateFileInfoWithPostId(postWithData.post)
}
return 0, nil
}
// uploadAttachments imports new attachments and returns current attachments of the post as a map
func (a *App) uploadAttachments(attachments *[]AttachmentImportData, post *model.Post, teamId string, dryRun bool) (map[string]bool, *model.AppError) {
if attachments == nil {
return nil, nil
}
fileIds := make(map[string]bool)
for _, attachment := range *attachments {
attachment := attachment
fileInfo, err := a.importAttachment(&attachment, post, teamId, dryRun)
if err != nil {
return nil, err
}
fileIds[fileInfo.Id] = true
}
return fileIds, nil
}
func (a *App) updateFileInfoWithPostId(post *model.Post) {
for _, fileId := range post.FileIds {
if err := a.Srv().Store.FileInfo().AttachToPost(fileId, post.Id, post.UserId); err != nil {
mlog.Error("Error attaching files to post.", mlog.String("post_id", post.Id), mlog.Any("post_file_ids", post.FileIds), mlog.Err(err))
}
}
}
func (a *App) importDirectChannel(data *DirectChannelImportData, dryRun bool) *model.AppError {
var err *model.AppError
if err = validateDirectChannelImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
var userIds []string
userMap, err := a.getUsersByUsernames(*data.Members)
if err != nil {
return err
}
for _, user := range *data.Members {
userIds = append(userIds, userMap[user].Id)
}
var channel *model.Channel
if len(userIds) == 2 {
ch, err := a.createDirectChannel(userIds[0], userIds[1])
if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR {
return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_direct_channel.error", nil, err.Error(), http.StatusBadRequest)
}
channel = ch
} else {
ch, err := a.createGroupChannel(userIds, userIds[0])
if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR {
return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_group_channel.error", nil, err.Error(), http.StatusBadRequest)
}
channel = ch
}
var preferences model.Preferences
for _, userId := range userIds {
preferences = append(preferences, model.Preference{
UserId: userId,
Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
Name: channel.Id,
Value: "true",
})
}
if data.FavoritedBy != nil {
for _, favoriter := range *data.FavoritedBy {
preferences = append(preferences, model.Preference{
UserId: userMap[favoriter].Id,
Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL,
Name: channel.Id,
Value: "true",
})
}
}
if err := a.Srv().Store.Preference().Save(&preferences); err != nil {
var appErr *model.AppError
switch {
case errors.As(err, &appErr):
appErr.StatusCode = http.StatusBadRequest
return appErr
default:
return model.NewAppError("importDirectChannel", "app.preference.save.updating.app_error", nil, err.Error(), http.StatusBadRequest)
}
}
if data.Header != nil {
channel.Header = *data.Header
if _, appErr := a.Srv().Store.Channel().Update(channel); appErr != nil {
return model.NewAppError("BulkImport", "app.import.import_direct_channel.update_header_failed.error", nil, appErr.Error(), http.StatusBadRequest)
}
}
return nil
}
// importMultipleDirectPostLines will return an error and the line
// that caused it whenever possible
func (a *App) importMultipleDirectPostLines(lines []LineImportWorkerData, dryRun bool) (int, *model.AppError) {
if len(lines) == 0 {
return 0, nil
}
for _, line := range lines {
if err := validateDirectPostImportData(line.DirectPost, a.MaxPostSize()); err != nil {
return line.LineNumber, err
}
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return 0, nil
}
usernames := []string{}
for _, line := range lines {
usernames = append(usernames, *line.DirectPost.User)
if line.DirectPost.FlaggedBy != nil {
usernames = append(usernames, *line.DirectPost.FlaggedBy...)
}
usernames = append(usernames, *line.DirectPost.ChannelMembers...)
}
users, err := a.getUsersByUsernames(usernames)
if err != nil {
return 0, err
}
postsWithData := []postAndData{}
postsForCreateList := []*model.Post{}
postsForCreateMap := map[string]int{}
postsForOverwriteList := []*model.Post{}
postsForOverwriteMap := map[string]int{}
for _, line := range lines {
var userIds []string
var err *model.AppError
for _, username := range *line.DirectPost.ChannelMembers {
user := users[username]
userIds = append(userIds, user.Id)
}
var channel *model.Channel
var ch *model.Channel
if len(userIds) == 2 {
ch, err = a.GetOrCreateDirectChannel(userIds[0], userIds[1])
if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR {
return line.LineNumber, model.NewAppError("BulkImport", "app.import.import_direct_post.create_direct_channel.error", nil, err.Error(), http.StatusBadRequest)
}
channel = ch
} else {
ch, err = a.createGroupChannel(userIds, userIds[0])
if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR {
return line.LineNumber, model.NewAppError("BulkImport", "app.import.import_direct_post.create_group_channel.error", nil, err.Error(), http.StatusBadRequest)
}
channel = ch
}
user := users[*line.DirectPost.User]
// Check if this post already exists.
posts, nErr := a.Srv().Store.Post().GetPostsCreatedAt(channel.Id, *line.DirectPost.CreateAt)
if nErr != nil {
return line.LineNumber, model.NewAppError("BulkImport", "app.post.get_posts_created_at.app_error", nil, nErr.Error(), http.StatusInternalServerError)
}
var post *model.Post
for _, p := range posts {
if p.Message == *line.DirectPost.Message {
post = p
break
}
}
if post == nil {
post = &model.Post{}
}
post.ChannelId = channel.Id
post.Message = *line.DirectPost.Message
post.UserId = user.Id
post.CreateAt = *line.DirectPost.CreateAt
post.Hashtags, _ = model.ParseHashtags(post.Message)
if line.DirectPost.Props != nil {
post.Props = *line.DirectPost.Props
}
fileIds, err := a.uploadAttachments(line.DirectPost.Attachments, post, "noteam", dryRun)
if err != nil {
return line.LineNumber, err
}
for _, fileID := range post.FileIds {
if _, ok := fileIds[fileID]; !ok {
a.Srv().Store.FileInfo().PermanentDelete(fileID)
}
}
post.FileIds = make([]string, 0)
for fileID := range fileIds {
post.FileIds = append(post.FileIds, fileID)
}
if len(post.Id) == 0 {
postsForCreateList = append(postsForCreateList, post)
postsForCreateMap[getPostStrID(post)] = line.LineNumber
} else {
postsForOverwriteList = append(postsForOverwriteList, post)
postsForOverwriteMap[getPostStrID(post)] = line.LineNumber
}
postsWithData = append(postsWithData, postAndData{post: post, directPostData: line.DirectPost, lineNumber: line.LineNumber})
}
if len(postsForCreateList) > 0 {
if _, idx, err := a.Srv().Store.Post().SaveMultiple(postsForCreateList); err != nil {
var appErr *model.AppError
var invErr *store.ErrInvalidInput
var retErr *model.AppError
switch {
case errors.As(err, &appErr):
retErr = appErr
case errors.As(err, &invErr):
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.existing.app_error", nil, invErr.Error(), http.StatusBadRequest)
default:
retErr = model.NewAppError("importMultiplePostLines", "app.post.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if idx != -1 && idx < len(postsForCreateList) {
post := postsForCreateList[idx]
if lineNumber, ok := postsForCreateMap[getPostStrID(post)]; ok {
return lineNumber, retErr
}
}
return 0, retErr
}
}
if _, idx, err := a.Srv().Store.Post().OverwriteMultiple(postsForOverwriteList); err != nil {
if idx != -1 && idx < len(postsForOverwriteList) {
post := postsForOverwriteList[idx]
if lineNumber, ok := postsForOverwriteMap[getPostStrID(post)]; ok {
return lineNumber, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
return 0, model.NewAppError("importMultiplePostLines", "app.post.overwrite.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, postWithData := range postsWithData {
if postWithData.directPostData.FlaggedBy != nil {
var preferences model.Preferences
for _, username := range *postWithData.directPostData.FlaggedBy {
user := users[username]
preferences = append(preferences, model.Preference{
UserId: user.Id,
Category: model.PREFERENCE_CATEGORY_FLAGGED_POST,
Name: postWithData.post.Id,
Value: "true",
})
}
if len(preferences) > 0 {
if err := a.Srv().Store.Preference().Save(&preferences); err != nil {
return postWithData.lineNumber, model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, err.Error(), http.StatusInternalServerError)
}
}
}
if postWithData.directPostData.Reactions != nil {
for _, reaction := range *postWithData.directPostData.Reactions {
reaction := reaction
if err := a.importReaction(&reaction, postWithData.post, dryRun); err != nil {
return postWithData.lineNumber, err
}
}
}
if postWithData.directPostData.Replies != nil {
if err := a.importReplies(*postWithData.directPostData.Replies, postWithData.post, "noteam", dryRun); err != nil {
return postWithData.lineNumber, err
}
}
a.updateFileInfoWithPostId(postWithData.post)
}
return 0, nil
}
func (a *App) importEmoji(data *EmojiImportData, dryRun bool) *model.AppError {
if err := validateEmojiImportData(data); err != nil {
return err
}
// If this is a Dry Run, do not continue any further.
if dryRun {
return nil
}
var emoji *model.Emoji
emoji, err := a.Srv().Store.Emoji().GetByName(*data.Name, true)
if err != nil {
var nfErr *store.ErrNotFound
if !errors.As(err, &nfErr) {
return model.NewAppError("importEmoji", "app.emoji.get_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
alreadyExists := emoji != nil
if !alreadyExists {
emoji = &model.Emoji{
Name: *data.Name,
}
emoji.PreSave()
}
file, err := os.Open(*data.Image)
if err != nil {
return model.NewAppError("BulkImport", "app.import.emoji.bad_file.error", map[string]interface{}{"EmojiName": *data.Name}, "", http.StatusBadRequest)
}
defer file.Close()
if _, err := a.WriteFile(file, getEmojiImagePath(emoji.Id)); err != nil {
return err
}
if !alreadyExists {
if _, err := a.Srv().Store.Emoji().Save(emoji); err != nil {
return model.NewAppError("importEmoji", "api.emoji.create.internal_error", nil, err.Error(), http.StatusBadRequest)
}
}
return nil
}