github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/slackimport.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package app 5 6 import ( 7 "archive/zip" 8 "bytes" 9 "encoding/json" 10 "io" 11 "mime/multipart" 12 "net/http" 13 "path/filepath" 14 "regexp" 15 "sort" 16 "strconv" 17 "strings" 18 "time" 19 "unicode/utf8" 20 21 "github.com/mattermost/mattermost-server/v5/mlog" 22 "github.com/mattermost/mattermost-server/v5/model" 23 "github.com/mattermost/mattermost-server/v5/utils" 24 ) 25 26 type SlackChannel struct { 27 Id string `json:"id"` 28 Name string `json:"name"` 29 Creator string `json:"creator"` 30 Members []string `json:"members"` 31 Purpose SlackChannelSub `json:"purpose"` 32 Topic SlackChannelSub `json:"topic"` 33 Type string 34 } 35 36 type SlackChannelSub struct { 37 Value string `json:"value"` 38 } 39 40 type SlackProfile struct { 41 FirstName string `json:"first_name"` 42 LastName string `json:"last_name"` 43 Email string `json:"email"` 44 } 45 46 type SlackUser struct { 47 Id string `json:"id"` 48 Username string `json:"name"` 49 Profile SlackProfile `json:"profile"` 50 } 51 52 type SlackFile struct { 53 Id string `json:"id"` 54 Title string `json:"title"` 55 } 56 57 type SlackPost struct { 58 User string `json:"user"` 59 BotId string `json:"bot_id"` 60 BotUsername string `json:"username"` 61 Text string `json:"text"` 62 TimeStamp string `json:"ts"` 63 ThreadTS string `json:"thread_ts"` 64 Type string `json:"type"` 65 SubType string `json:"subtype"` 66 Comment *SlackComment `json:"comment"` 67 Upload bool `json:"upload"` 68 File *SlackFile `json:"file"` 69 Files []*SlackFile `json:"files"` 70 Attachments []*model.SlackAttachment `json:"attachments"` 71 } 72 73 var isValidChannelNameCharacters = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`).MatchString 74 75 const SLACK_IMPORT_MAX_FILE_SIZE = 1024 * 1024 * 70 76 77 type SlackComment struct { 78 User string `json:"user"` 79 Comment string `json:"comment"` 80 } 81 82 func truncateRunes(s string, i int) string { 83 runes := []rune(s) 84 if len(runes) > i { 85 return string(runes[:i]) 86 } 87 return s 88 } 89 90 func SlackConvertTimeStamp(ts string) int64 { 91 timeString := strings.SplitN(ts, ".", 2)[0] 92 93 timeStamp, err := strconv.ParseInt(timeString, 10, 64) 94 if err != nil { 95 mlog.Warn("Slack Import: Bad timestamp detected.") 96 return 1 97 } 98 return timeStamp * 1000 // Convert to milliseconds 99 } 100 101 func SlackConvertChannelName(channelName string, channelId string) string { 102 newName := strings.Trim(channelName, "_-") 103 if len(newName) == 1 { 104 return "slack-channel-" + newName 105 } 106 107 if isValidChannelNameCharacters(newName) { 108 return newName 109 } 110 return strings.ToLower(channelId) 111 } 112 113 func SlackParseChannels(data io.Reader, channelType string) ([]SlackChannel, error) { 114 decoder := json.NewDecoder(data) 115 116 var channels []SlackChannel 117 if err := decoder.Decode(&channels); err != nil { 118 mlog.Warn("Slack Import: Error occurred when parsing some Slack channels. Import may work anyway.") 119 return channels, err 120 } 121 122 for i := range channels { 123 channels[i].Type = channelType 124 } 125 126 return channels, nil 127 } 128 129 func SlackParseUsers(data io.Reader) ([]SlackUser, error) { 130 decoder := json.NewDecoder(data) 131 132 var users []SlackUser 133 err := decoder.Decode(&users) 134 // This actually returns errors that are ignored. 135 // In this case it is erroring because of a null that Slack 136 // introduced. So we just return the users here. 137 return users, err 138 } 139 140 func SlackParsePosts(data io.Reader) ([]SlackPost, error) { 141 decoder := json.NewDecoder(data) 142 143 var posts []SlackPost 144 if err := decoder.Decode(&posts); err != nil { 145 mlog.Warn("Slack Import: Error occurred when parsing some Slack posts. Import may work anyway.") 146 return posts, err 147 } 148 return posts, nil 149 } 150 151 func (a *App) SlackAddUsers(teamId string, slackusers []SlackUser, importerLog *bytes.Buffer) map[string]*model.User { 152 // Log header 153 importerLog.WriteString(utils.T("api.slackimport.slack_add_users.created")) 154 importerLog.WriteString("===============\r\n\r\n") 155 156 addedUsers := make(map[string]*model.User) 157 158 // Need the team 159 team, err := a.Srv().Store.Team().Get(teamId) 160 if err != nil { 161 importerLog.WriteString(utils.T("api.slackimport.slack_import.team_fail")) 162 return addedUsers 163 } 164 165 for _, sUser := range slackusers { 166 firstName := sUser.Profile.FirstName 167 lastName := sUser.Profile.LastName 168 email := sUser.Profile.Email 169 if email == "" { 170 email = sUser.Username + "@example.com" 171 importerLog.WriteString(utils.T("api.slackimport.slack_add_users.missing_email_address", map[string]interface{}{"Email": email, "Username": sUser.Username})) 172 mlog.Warn("Slack Import: User does not have an email address in the Slack export. Used username as a placeholder. The user should update their email address once logged in to the system.", mlog.String("user_email", email), mlog.String("user_name", sUser.Username)) 173 } 174 175 password := model.NewId() 176 177 // Check for email conflict and use existing user if found 178 if existingUser, err := a.Srv().Store.User().GetByEmail(email); err == nil { 179 addedUsers[sUser.Id] = existingUser 180 if err := a.JoinUserToTeam(team, addedUsers[sUser.Id], ""); err != nil { 181 importerLog.WriteString(utils.T("api.slackimport.slack_add_users.merge_existing_failed", map[string]interface{}{"Email": existingUser.Email, "Username": existingUser.Username})) 182 } else { 183 importerLog.WriteString(utils.T("api.slackimport.slack_add_users.merge_existing", map[string]interface{}{"Email": existingUser.Email, "Username": existingUser.Username})) 184 } 185 continue 186 } 187 188 email = strings.ToLower(email) 189 newUser := model.User{ 190 Username: sUser.Username, 191 FirstName: firstName, 192 LastName: lastName, 193 Email: email, 194 Password: password, 195 } 196 197 mUser := a.oldImportUser(team, &newUser) 198 if mUser == nil { 199 importerLog.WriteString(utils.T("api.slackimport.slack_add_users.unable_import", map[string]interface{}{"Username": sUser.Username})) 200 continue 201 } 202 addedUsers[sUser.Id] = mUser 203 importerLog.WriteString(utils.T("api.slackimport.slack_add_users.email_pwd", map[string]interface{}{"Email": newUser.Email, "Password": password})) 204 } 205 206 return addedUsers 207 } 208 209 func (a *App) SlackAddBotUser(teamId string, log *bytes.Buffer) *model.User { 210 team, err := a.Srv().Store.Team().Get(teamId) 211 if err != nil { 212 log.WriteString(utils.T("api.slackimport.slack_import.team_fail")) 213 return nil 214 } 215 216 password := model.NewId() 217 username := "slackimportuser_" + model.NewId() 218 email := username + "@localhost" 219 220 botUser := model.User{ 221 Username: username, 222 FirstName: "", 223 LastName: "", 224 Email: email, 225 Password: password, 226 } 227 228 mUser := a.oldImportUser(team, &botUser) 229 if mUser == nil { 230 log.WriteString(utils.T("api.slackimport.slack_add_bot_user.unable_import", map[string]interface{}{"Username": username})) 231 return nil 232 } 233 234 log.WriteString(utils.T("api.slackimport.slack_add_bot_user.email_pwd", map[string]interface{}{"Email": botUser.Email, "Password": password})) 235 return mUser 236 } 237 238 func (a *App) SlackAddPosts(teamId string, channel *model.Channel, posts []SlackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User) { 239 sort.Slice(posts, func(i, j int) bool { 240 return SlackConvertTimeStamp(posts[i].TimeStamp) < SlackConvertTimeStamp(posts[j].TimeStamp) 241 }) 242 threads := make(map[string]string) 243 for _, sPost := range posts { 244 switch { 245 case sPost.Type == "message" && (sPost.SubType == "" || sPost.SubType == "file_share"): 246 if sPost.User == "" { 247 mlog.Debug("Slack Import: Unable to import the message as the user field is missing.") 248 continue 249 } 250 if users[sPost.User] == nil { 251 mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) 252 continue 253 } 254 newPost := model.Post{ 255 UserId: users[sPost.User].Id, 256 ChannelId: channel.Id, 257 Message: sPost.Text, 258 CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), 259 } 260 if sPost.Upload { 261 if sPost.File != nil { 262 if fileInfo, ok := a.SlackUploadFile(sPost.File, uploads, teamId, newPost.ChannelId, newPost.UserId, sPost.TimeStamp); ok { 263 newPost.FileIds = append(newPost.FileIds, fileInfo.Id) 264 } 265 } else if sPost.Files != nil { 266 for _, file := range sPost.Files { 267 if fileInfo, ok := a.SlackUploadFile(file, uploads, teamId, newPost.ChannelId, newPost.UserId, sPost.TimeStamp); ok { 268 newPost.FileIds = append(newPost.FileIds, fileInfo.Id) 269 } 270 } 271 } 272 } 273 // If post in thread 274 if sPost.ThreadTS != "" && sPost.ThreadTS != sPost.TimeStamp { 275 newPost.RootId = threads[sPost.ThreadTS] 276 newPost.ParentId = threads[sPost.ThreadTS] 277 } 278 postId := a.oldImportPost(&newPost) 279 // If post is thread starter 280 if sPost.ThreadTS == sPost.TimeStamp { 281 threads[sPost.ThreadTS] = postId 282 } 283 case sPost.Type == "message" && sPost.SubType == "file_comment": 284 if sPost.Comment == nil { 285 mlog.Debug("Slack Import: Unable to import the message as it has no comments.") 286 continue 287 } 288 if sPost.Comment.User == "" { 289 mlog.Debug("Slack Import: Unable to import the message as the user field is missing.") 290 continue 291 } 292 if users[sPost.Comment.User] == nil { 293 mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) 294 continue 295 } 296 newPost := model.Post{ 297 UserId: users[sPost.Comment.User].Id, 298 ChannelId: channel.Id, 299 Message: sPost.Comment.Comment, 300 CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), 301 } 302 a.oldImportPost(&newPost) 303 case sPost.Type == "message" && sPost.SubType == "bot_message": 304 if botUser == nil { 305 mlog.Warn("Slack Import: Unable to import the bot message as the bot user does not exist.") 306 continue 307 } 308 if sPost.BotId == "" { 309 mlog.Warn("Slack Import: Unable to import bot message as the BotId field is missing.") 310 continue 311 } 312 313 props := make(model.StringInterface) 314 props["override_username"] = sPost.BotUsername 315 if len(sPost.Attachments) > 0 { 316 props["attachments"] = sPost.Attachments 317 } 318 319 post := &model.Post{ 320 UserId: botUser.Id, 321 ChannelId: channel.Id, 322 CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), 323 Message: sPost.Text, 324 Type: model.POST_SLACK_ATTACHMENT, 325 } 326 327 postId := a.oldImportIncomingWebhookPost(post, props) 328 // If post is thread starter 329 if sPost.ThreadTS == sPost.TimeStamp { 330 threads[sPost.ThreadTS] = postId 331 } 332 case sPost.Type == "message" && (sPost.SubType == "channel_join" || sPost.SubType == "channel_leave"): 333 if sPost.User == "" { 334 mlog.Debug("Slack Import: Unable to import the message as the user field is missing.") 335 continue 336 } 337 if users[sPost.User] == nil { 338 mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) 339 continue 340 } 341 342 var postType string 343 if sPost.SubType == "channel_join" { 344 postType = model.POST_JOIN_CHANNEL 345 } else { 346 postType = model.POST_LEAVE_CHANNEL 347 } 348 349 newPost := model.Post{ 350 UserId: users[sPost.User].Id, 351 ChannelId: channel.Id, 352 Message: sPost.Text, 353 CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), 354 Type: postType, 355 Props: model.StringInterface{ 356 "username": users[sPost.User].Username, 357 }, 358 } 359 a.oldImportPost(&newPost) 360 case sPost.Type == "message" && sPost.SubType == "me_message": 361 if sPost.User == "" { 362 mlog.Debug("Slack Import: Unable to import the message as the user field is missing.") 363 continue 364 } 365 if users[sPost.User] == nil { 366 mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) 367 continue 368 } 369 newPost := model.Post{ 370 UserId: users[sPost.User].Id, 371 ChannelId: channel.Id, 372 Message: "*" + sPost.Text + "*", 373 CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), 374 } 375 postId := a.oldImportPost(&newPost) 376 // If post is thread starter 377 if sPost.ThreadTS == sPost.TimeStamp { 378 threads[sPost.ThreadTS] = postId 379 } 380 case sPost.Type == "message" && sPost.SubType == "channel_topic": 381 if sPost.User == "" { 382 mlog.Debug("Slack Import: Unable to import the message as the user field is missing.") 383 continue 384 } 385 if users[sPost.User] == nil { 386 mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) 387 continue 388 } 389 newPost := model.Post{ 390 UserId: users[sPost.User].Id, 391 ChannelId: channel.Id, 392 Message: sPost.Text, 393 CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), 394 Type: model.POST_HEADER_CHANGE, 395 } 396 a.oldImportPost(&newPost) 397 case sPost.Type == "message" && sPost.SubType == "channel_purpose": 398 if sPost.User == "" { 399 mlog.Debug("Slack Import: Unable to import the message as the user field is missing.") 400 continue 401 } 402 if users[sPost.User] == nil { 403 mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) 404 continue 405 } 406 newPost := model.Post{ 407 UserId: users[sPost.User].Id, 408 ChannelId: channel.Id, 409 Message: sPost.Text, 410 CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), 411 Type: model.POST_PURPOSE_CHANGE, 412 } 413 a.oldImportPost(&newPost) 414 case sPost.Type == "message" && sPost.SubType == "channel_name": 415 if sPost.User == "" { 416 mlog.Debug("Slack Import: Unable to import the message as the user field is missing.") 417 continue 418 } 419 if users[sPost.User] == nil { 420 mlog.Debug("Slack Import: Unable to add the message as the Slack user does not exist in Mattermost.", mlog.String("user", sPost.User)) 421 continue 422 } 423 newPost := model.Post{ 424 UserId: users[sPost.User].Id, 425 ChannelId: channel.Id, 426 Message: sPost.Text, 427 CreateAt: SlackConvertTimeStamp(sPost.TimeStamp), 428 Type: model.POST_DISPLAYNAME_CHANGE, 429 } 430 a.oldImportPost(&newPost) 431 default: 432 mlog.Warn( 433 "Slack Import: Unable to import the message as its type is not supported", 434 mlog.String("post_type", sPost.Type), 435 mlog.String("post_subtype", sPost.SubType), 436 ) 437 } 438 } 439 } 440 441 func (a *App) SlackUploadFile(slackPostFile *SlackFile, uploads map[string]*zip.File, teamId string, channelId string, userId string, slackTimestamp string) (*model.FileInfo, bool) { 442 if slackPostFile == nil { 443 mlog.Warn("Slack Import: Unable to attach the file to the post as the latter has no file section present in Slack export.") 444 return nil, false 445 } 446 file, ok := uploads[slackPostFile.Id] 447 if !ok { 448 mlog.Warn("Slack Import: Unable to import file as the file is missing from the Slack export zip file.", mlog.String("file_id", slackPostFile.Id)) 449 return nil, false 450 } 451 openFile, err := file.Open() 452 if err != nil { 453 mlog.Warn("Slack Import: Unable to open the file from the Slack export.", mlog.String("file_id", slackPostFile.Id), mlog.Err(err)) 454 return nil, false 455 } 456 defer openFile.Close() 457 458 timestamp := utils.TimeFromMillis(SlackConvertTimeStamp(slackTimestamp)) 459 uploadedFile, err := a.oldImportFile(timestamp, openFile, teamId, channelId, userId, filepath.Base(file.Name)) 460 if err != nil { 461 mlog.Warn("Slack Import: An error occurred when uploading file.", mlog.String("file_id", slackPostFile.Id), mlog.Err(err)) 462 return nil, false 463 } 464 465 return uploadedFile, true 466 } 467 468 func (a *App) deactivateSlackBotUser(user *model.User) { 469 if _, err := a.UpdateActive(user, false); err != nil { 470 mlog.Warn("Slack Import: Unable to deactivate the user account used for the bot.") 471 } 472 } 473 474 func (a *App) addSlackUsersToChannel(members []string, users map[string]*model.User, channel *model.Channel, log *bytes.Buffer) { 475 for _, member := range members { 476 user, ok := users[member] 477 if !ok { 478 log.WriteString(utils.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]interface{}{"Username": "?"})) 479 continue 480 } 481 if _, err := a.AddUserToChannel(user, channel); err != nil { 482 log.WriteString(utils.T("api.slackimport.slack_add_channels.failed_to_add_user", map[string]interface{}{"Username": user.Username})) 483 } 484 } 485 } 486 487 func SlackSanitiseChannelProperties(channel model.Channel) model.Channel { 488 if utf8.RuneCountInString(channel.DisplayName) > model.CHANNEL_DISPLAY_NAME_MAX_RUNES { 489 mlog.Warn("Slack Import: Channel display name exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName)) 490 channel.DisplayName = truncateRunes(channel.DisplayName, model.CHANNEL_DISPLAY_NAME_MAX_RUNES) 491 } 492 493 if len(channel.Name) > model.CHANNEL_NAME_MAX_LENGTH { 494 mlog.Warn("Slack Import: Channel handle exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName)) 495 channel.Name = channel.Name[0:model.CHANNEL_NAME_MAX_LENGTH] 496 } 497 498 if utf8.RuneCountInString(channel.Purpose) > model.CHANNEL_PURPOSE_MAX_RUNES { 499 mlog.Warn("Slack Import: Channel purpose exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName)) 500 channel.Purpose = truncateRunes(channel.Purpose, model.CHANNEL_PURPOSE_MAX_RUNES) 501 } 502 503 if utf8.RuneCountInString(channel.Header) > model.CHANNEL_HEADER_MAX_RUNES { 504 mlog.Warn("Slack Import: Channel header exceeds the maximum length. It will be truncated when imported.", mlog.String("channel_display_name", channel.DisplayName)) 505 channel.Header = truncateRunes(channel.Header, model.CHANNEL_HEADER_MAX_RUNES) 506 } 507 508 return channel 509 } 510 511 func (a *App) SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[string][]SlackPost, users map[string]*model.User, uploads map[string]*zip.File, botUser *model.User, importerLog *bytes.Buffer) map[string]*model.Channel { 512 // Write Header 513 importerLog.WriteString(utils.T("api.slackimport.slack_add_channels.added")) 514 importerLog.WriteString("=================\r\n\r\n") 515 516 addedChannels := make(map[string]*model.Channel) 517 for _, sChannel := range slackchannels { 518 newChannel := model.Channel{ 519 TeamId: teamId, 520 Type: sChannel.Type, 521 DisplayName: sChannel.Name, 522 Name: SlackConvertChannelName(sChannel.Name, sChannel.Id), 523 Purpose: sChannel.Purpose.Value, 524 Header: sChannel.Topic.Value, 525 } 526 527 // Direct message channels in Slack don't have a name so we set the id as name or else the messages won't get imported. 528 if newChannel.Type == model.CHANNEL_DIRECT { 529 sChannel.Name = sChannel.Id 530 } 531 532 newChannel = SlackSanitiseChannelProperties(newChannel) 533 534 var mChannel *model.Channel 535 var err error 536 if mChannel, err = a.Srv().Store.Channel().GetByName(teamId, sChannel.Name, true); err == nil { 537 // The channel already exists as an active channel. Merge with the existing one. 538 importerLog.WriteString(utils.T("api.slackimport.slack_add_channels.merge", map[string]interface{}{"DisplayName": newChannel.DisplayName})) 539 } else if _, nErr := a.Srv().Store.Channel().GetDeletedByName(teamId, sChannel.Name); nErr == nil { 540 // The channel already exists but has been deleted. Generate a random string for the handle instead. 541 newChannel.Name = model.NewId() 542 newChannel = SlackSanitiseChannelProperties(newChannel) 543 } 544 545 if mChannel == nil { 546 // Haven't found an existing channel to merge with. Try importing it as a new one. 547 mChannel = a.oldImportChannel(&newChannel, sChannel, users) 548 if mChannel == nil { 549 mlog.Warn("Slack Import: Unable to import Slack channel.", mlog.String("channel_display_name", newChannel.DisplayName)) 550 importerLog.WriteString(utils.T("api.slackimport.slack_add_channels.import_failed", map[string]interface{}{"DisplayName": newChannel.DisplayName})) 551 continue 552 } 553 } 554 555 // Members for direct and group channels are added during the creation of the channel in the oldImportChannel function 556 if sChannel.Type == model.CHANNEL_OPEN || sChannel.Type == model.CHANNEL_PRIVATE { 557 a.addSlackUsersToChannel(sChannel.Members, users, mChannel, importerLog) 558 } 559 importerLog.WriteString(newChannel.DisplayName + "\r\n") 560 addedChannels[sChannel.Id] = mChannel 561 a.SlackAddPosts(teamId, mChannel, posts[sChannel.Name], users, uploads, botUser) 562 } 563 564 return addedChannels 565 } 566 567 func SlackConvertUserMentions(users []SlackUser, posts map[string][]SlackPost) map[string][]SlackPost { 568 var regexes = make(map[string]*regexp.Regexp, len(users)) 569 for _, user := range users { 570 r, err := regexp.Compile("<@" + user.Id + `(\|` + user.Username + ")?>") 571 if err != nil { 572 mlog.Warn("Slack Import: Unable to compile the @mention, matching regular expression for the Slack user.", mlog.String("user_name", user.Username), mlog.String("user_id", user.Id)) 573 continue 574 } 575 regexes["@"+user.Username] = r 576 } 577 578 // Special cases. 579 regexes["@here"], _ = regexp.Compile(`<!here\|@here>`) 580 regexes["@channel"], _ = regexp.Compile("<!channel>") 581 regexes["@all"], _ = regexp.Compile("<!everyone>") 582 583 for channelName, channelPosts := range posts { 584 for postIdx, post := range channelPosts { 585 for mention, r := range regexes { 586 post.Text = r.ReplaceAllString(post.Text, mention) 587 posts[channelName][postIdx] = post 588 } 589 } 590 } 591 592 return posts 593 } 594 595 func SlackConvertChannelMentions(channels []SlackChannel, posts map[string][]SlackPost) map[string][]SlackPost { 596 var regexes = make(map[string]*regexp.Regexp, len(channels)) 597 for _, channel := range channels { 598 r, err := regexp.Compile("<#" + channel.Id + `(\|` + channel.Name + ")?>") 599 if err != nil { 600 mlog.Warn("Slack Import: Unable to compile the !channel, matching regular expression for the Slack channel.", mlog.String("channel_id", channel.Id), mlog.String("channel_name", channel.Name)) 601 continue 602 } 603 regexes["~"+channel.Name] = r 604 } 605 606 for channelName, channelPosts := range posts { 607 for postIdx, post := range channelPosts { 608 for channelReplace, r := range regexes { 609 post.Text = r.ReplaceAllString(post.Text, channelReplace) 610 posts[channelName][postIdx] = post 611 } 612 } 613 } 614 615 return posts 616 } 617 618 func SlackConvertPostsMarkup(posts map[string][]SlackPost) map[string][]SlackPost { 619 regexReplaceAllString := []struct { 620 regex *regexp.Regexp 621 rpl string 622 }{ 623 // URL 624 { 625 regexp.MustCompile(`<([^|<>]+)\|([^|<>]+)>`), 626 "[$2]($1)", 627 }, 628 // bold 629 { 630 regexp.MustCompile(`(^|[\s.;,])\*(\S[^*\n]+)\*`), 631 "$1**$2**", 632 }, 633 // strikethrough 634 { 635 regexp.MustCompile(`(^|[\s.;,])\~(\S[^~\n]+)\~`), 636 "$1~~$2~~", 637 }, 638 // single paragraph blockquote 639 // Slack converts > character to > 640 { 641 regexp.MustCompile(`(?sm)^>`), 642 ">", 643 }, 644 } 645 646 regexReplaceAllStringFunc := []struct { 647 regex *regexp.Regexp 648 fn func(string) string 649 }{ 650 // multiple paragraphs blockquotes 651 { 652 regexp.MustCompile(`(?sm)^>>>(.+)$`), 653 func(src string) string { 654 // remove >>> prefix, might have leading \n 655 prefixRegexp := regexp.MustCompile(`^([\n])?>>>(.*)`) 656 src = prefixRegexp.ReplaceAllString(src, "$1$2") 657 // append > to start of line 658 appendRegexp := regexp.MustCompile(`(?m)^`) 659 return appendRegexp.ReplaceAllString(src, ">$0") 660 }, 661 }, 662 } 663 664 for channelName, channelPosts := range posts { 665 for postIdx, post := range channelPosts { 666 result := post.Text 667 668 for _, rule := range regexReplaceAllString { 669 result = rule.regex.ReplaceAllString(result, rule.rpl) 670 } 671 672 for _, rule := range regexReplaceAllStringFunc { 673 result = rule.regex.ReplaceAllStringFunc(result, rule.fn) 674 } 675 posts[channelName][postIdx].Text = result 676 } 677 } 678 679 return posts 680 } 681 682 func (a *App) SlackImport(fileData multipart.File, fileSize int64, teamID string) (*model.AppError, *bytes.Buffer) { 683 // Create log file 684 log := bytes.NewBufferString(utils.T("api.slackimport.slack_import.log")) 685 686 zipreader, err := zip.NewReader(fileData, fileSize) 687 if err != nil || zipreader.File == nil { 688 log.WriteString(utils.T("api.slackimport.slack_import.zip.app_error")) 689 return model.NewAppError("SlackImport", "api.slackimport.slack_import.zip.app_error", nil, err.Error(), http.StatusBadRequest), log 690 } 691 692 var channels []SlackChannel 693 var publicChannels []SlackChannel 694 var privateChannels []SlackChannel 695 var groupChannels []SlackChannel 696 var directChannels []SlackChannel 697 698 var users []SlackUser 699 posts := make(map[string][]SlackPost) 700 uploads := make(map[string]*zip.File) 701 for _, file := range zipreader.File { 702 if file.UncompressedSize64 > SLACK_IMPORT_MAX_FILE_SIZE { 703 log.WriteString(utils.T("api.slackimport.slack_import.zip.file_too_large", map[string]interface{}{"Filename": file.Name})) 704 continue 705 } 706 reader, err := file.Open() 707 if err != nil { 708 log.WriteString(utils.T("api.slackimport.slack_import.open.app_error", map[string]interface{}{"Filename": file.Name})) 709 return model.NewAppError("SlackImport", "api.slackimport.slack_import.open.app_error", map[string]interface{}{"Filename": file.Name}, err.Error(), http.StatusInternalServerError), log 710 } 711 if file.Name == "channels.json" { 712 publicChannels, _ = SlackParseChannels(reader, model.CHANNEL_OPEN) 713 channels = append(channels, publicChannels...) 714 } else if file.Name == "dms.json" { 715 directChannels, _ = SlackParseChannels(reader, model.CHANNEL_DIRECT) 716 channels = append(channels, directChannels...) 717 } else if file.Name == "groups.json" { 718 privateChannels, _ = SlackParseChannels(reader, model.CHANNEL_PRIVATE) 719 channels = append(channels, privateChannels...) 720 } else if file.Name == "mpims.json" { 721 groupChannels, _ = SlackParseChannels(reader, model.CHANNEL_GROUP) 722 channels = append(channels, groupChannels...) 723 } else if file.Name == "users.json" { 724 users, _ = SlackParseUsers(reader) 725 } else { 726 spl := strings.Split(file.Name, "/") 727 if len(spl) == 2 && strings.HasSuffix(spl[1], ".json") { 728 newposts, _ := SlackParsePosts(reader) 729 channel := spl[0] 730 if _, ok := posts[channel]; !ok { 731 posts[channel] = newposts 732 } else { 733 posts[channel] = append(posts[channel], newposts...) 734 } 735 } else if len(spl) == 3 && spl[0] == "__uploads" { 736 uploads[spl[1]] = file 737 } 738 } 739 } 740 741 posts = SlackConvertUserMentions(users, posts) 742 posts = SlackConvertChannelMentions(channels, posts) 743 posts = SlackConvertPostsMarkup(posts) 744 745 addedUsers := a.SlackAddUsers(teamID, users, log) 746 botUser := a.SlackAddBotUser(teamID, log) 747 748 a.SlackAddChannels(teamID, channels, posts, addedUsers, uploads, botUser, log) 749 750 if botUser != nil { 751 a.deactivateSlackBotUser(botUser) 752 } 753 754 a.Srv().InvalidateAllCaches() 755 756 log.WriteString(utils.T("api.slackimport.slack_import.notes")) 757 log.WriteString("=======\r\n\r\n") 758 759 log.WriteString(utils.T("api.slackimport.slack_import.note1")) 760 log.WriteString(utils.T("api.slackimport.slack_import.note2")) 761 log.WriteString(utils.T("api.slackimport.slack_import.note3")) 762 763 return nil, log 764 } 765 766 // 767 // -- Old SlackImport Functions -- 768 // Import functions are suitable for entering posts and users into the database without 769 // some of the usual checks. (IsValid is still run) 770 // 771 772 func (a *App) oldImportPost(post *model.Post) string { 773 // Workaround for empty messages, which may be the case if they are webhook posts. 774 firstIteration := true 775 firstPostId := "" 776 if post.ParentId != "" { 777 firstPostId = post.ParentId 778 } 779 maxPostSize := a.MaxPostSize() 780 for messageRuneCount := utf8.RuneCountInString(post.Message); messageRuneCount > 0 || firstIteration; messageRuneCount = utf8.RuneCountInString(post.Message) { 781 var remainder string 782 if messageRuneCount > maxPostSize { 783 remainder = string(([]rune(post.Message))[maxPostSize:]) 784 post.Message = truncateRunes(post.Message, maxPostSize) 785 } else { 786 remainder = "" 787 } 788 789 post.Hashtags, _ = model.ParseHashtags(post.Message) 790 791 post.RootId = firstPostId 792 post.ParentId = firstPostId 793 794 _, err := a.Srv().Store.Post().Save(post) 795 if err != nil { 796 mlog.Debug("Error saving post.", mlog.String("user_id", post.UserId), mlog.String("message", post.Message)) 797 } 798 799 if firstIteration { 800 if firstPostId == "" { 801 firstPostId = post.Id 802 } 803 for _, fileId := range post.FileIds { 804 if err := a.Srv().Store.FileInfo().AttachToPost(fileId, post.Id, post.UserId); err != nil { 805 mlog.Error( 806 "Error attaching files to post.", 807 mlog.String("post_id", post.Id), 808 mlog.String("file_ids", strings.Join(post.FileIds, ",")), 809 mlog.String("user_id", post.UserId), 810 mlog.Err(err), 811 ) 812 } 813 } 814 post.FileIds = nil 815 } 816 817 post.Id = "" 818 post.CreateAt++ 819 post.Message = remainder 820 firstIteration = false 821 } 822 return firstPostId 823 } 824 825 func (a *App) oldImportUser(team *model.Team, user *model.User) *model.User { 826 user.MakeNonNil() 827 828 user.Roles = model.SYSTEM_USER_ROLE_ID 829 830 ruser, err := a.Srv().Store.User().Save(user) 831 if err != nil { 832 mlog.Error("Error saving user.", mlog.Err(err)) 833 return nil 834 } 835 836 if _, err = a.Srv().Store.User().VerifyEmail(ruser.Id, ruser.Email); err != nil { 837 mlog.Error("Failed to set email verified.", mlog.Err(err)) 838 } 839 840 if err = a.JoinUserToTeam(team, user, ""); err != nil { 841 mlog.Error("Failed to join team when importing.", mlog.Err(err)) 842 } 843 844 return ruser 845 } 846 847 func (a *App) oldImportChannel(channel *model.Channel, sChannel SlackChannel, users map[string]*model.User) *model.Channel { 848 switch { 849 case channel.Type == model.CHANNEL_DIRECT: 850 if len(sChannel.Members) < 2 { 851 return nil 852 } 853 u1 := users[sChannel.Members[0]] 854 u2 := users[sChannel.Members[1]] 855 if u1 == nil || u2 == nil { 856 mlog.Warn("Either or both of user ids not found in users.json. Ignoring.", mlog.String("id1", sChannel.Members[0]), mlog.String("id2", sChannel.Members[1])) 857 return nil 858 } 859 sc, err := a.createDirectChannel(u1.Id, u2.Id) 860 if err != nil { 861 return nil 862 } 863 864 return sc 865 // check if direct channel has less than 8 members and if not import as private channel instead 866 case channel.Type == model.CHANNEL_GROUP && len(sChannel.Members) < 8: 867 members := make([]string, len(sChannel.Members)) 868 869 for i := range sChannel.Members { 870 u := users[sChannel.Members[i]] 871 if u == nil { 872 mlog.Warn("User not found in users.json. Ignoring.", mlog.String("id", sChannel.Members[i])) 873 continue 874 } 875 members[i] = u.Id 876 } 877 878 creator := users[sChannel.Creator] 879 if creator == nil { 880 return nil 881 } 882 sc, err := a.createGroupChannel(members, creator.Id) 883 if err != nil { 884 return nil 885 } 886 887 return sc 888 case channel.Type == model.CHANNEL_GROUP: 889 channel.Type = model.CHANNEL_PRIVATE 890 sc, err := a.CreateChannel(channel, false) 891 if err != nil { 892 return nil 893 } 894 895 return sc 896 } 897 898 sc, err := a.Srv().Store.Channel().Save(channel, *a.Config().TeamSettings.MaxChannelsPerTeam) 899 if err != nil { 900 return nil 901 } 902 903 return sc 904 } 905 906 func (a *App) oldImportFile(timestamp time.Time, file io.Reader, teamId string, channelId string, userId string, fileName string) (*model.FileInfo, error) { 907 buf := bytes.NewBuffer(nil) 908 io.Copy(buf, file) 909 data := buf.Bytes() 910 911 fileInfo, err := a.DoUploadFile(timestamp, teamId, channelId, userId, fileName, data) 912 if err != nil { 913 return nil, err 914 } 915 916 if fileInfo.IsImage() && fileInfo.MimeType != "image/svg+xml" { 917 img, width, height := prepareImage(data) 918 if img != nil { 919 a.generateThumbnailImage(img, fileInfo.ThumbnailPath, width, height) 920 a.generatePreviewImage(img, fileInfo.PreviewPath, width) 921 } 922 } 923 924 return fileInfo, nil 925 } 926 927 func (a *App) oldImportIncomingWebhookPost(post *model.Post, props model.StringInterface) string { 928 linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) 929 post.Message = linkWithTextRegex.ReplaceAllString(post.Message, "[${2}](${1})") 930 931 post.AddProp("from_webhook", "true") 932 933 if _, ok := props["override_username"]; !ok { 934 post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME) 935 } 936 937 if len(props) > 0 { 938 for key, val := range props { 939 if key == "attachments" { 940 if attachments, success := val.([]*model.SlackAttachment); success { 941 model.ParseSlackAttachment(post, attachments) 942 } 943 } else if key != "from_webhook" { 944 post.AddProp(key, val) 945 } 946 } 947 } 948 949 return a.oldImportPost(post) 950 }