Files
mattermost/app/import_functions.go
Agniva De Sarker 39b5b601f8 MM-30026: Use DB master when getting team members from a session (#16170)
* MM-30026: Use DB master when getting team members from a session

A race condition happens when the read-replica isn't updated yet
by the time a session expiry message reaches another node in the cluster.

Here is the sequence of events that can cause it:
- Server1 gets any request which has to wipe session cache.

- The SQL query is written to DB master, and a cluster message is propagated
to clear the session cache for that user.

- Now before the read-replica is updated with the master’s update,
the cluster message reaches Server2. The session cache is wiped out for that user.

- _Any random_ request for that user hits Server2. Does NOT have to be
the update team name request. The request does not find the value
in session cache, because it’s wiped off, and picks it up from the DB.
Surprise surprise, it gets the stale value. Sticks it into the cache.

By now, the read-replica is updated. But guess what, we aren’t going to
ask the DB anymore, because we have it in the cache. And the cache has the stale value.

We use a temporary approach for now by introducing a context in the DB calls so that
the useMaster information can be easily passed. And this has the added advantage of
reusing the same context for future DB calls in case it happens. And we can also
add more context keys as needed.

A proper approach needs some architectural changes. See the issue for more details.

```release-note
Fixed a bug where a session will hold on to a cached value
in an HA setup with read-replicas configured.
```

* incorporate review comments

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2020-11-10 10:43:45 +05:30

1756 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))
}
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)
}
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
}