github.com/jlevesy/mattermost-server@v5.3.2-0.20181003190404-7468f35cb0c8+incompatible/cmd/mattermost/commands/sampledata.go (about) 1 // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package commands 5 6 import ( 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io/ioutil" 11 "math/rand" 12 "os" 13 "path" 14 "sort" 15 "strings" 16 "time" 17 18 "github.com/icrowley/fake" 19 "github.com/mattermost/mattermost-server/app" 20 "github.com/spf13/cobra" 21 ) 22 23 var SampleDataCmd = &cobra.Command{ 24 Use: "sampledata", 25 Short: "Generate sample data", 26 RunE: sampleDataCmdF, 27 } 28 29 func init() { 30 SampleDataCmd.Flags().Int64P("seed", "s", 1, "Seed used for generating the random data (Different seeds generate different data).") 31 SampleDataCmd.Flags().IntP("teams", "t", 2, "The number of sample teams.") 32 SampleDataCmd.Flags().Int("channels-per-team", 10, "The number of sample channels per team.") 33 SampleDataCmd.Flags().IntP("users", "u", 15, "The number of sample users.") 34 SampleDataCmd.Flags().Int("team-memberships", 2, "The number of sample team memberships per user.") 35 SampleDataCmd.Flags().Int("channel-memberships", 5, "The number of sample channel memberships per user in a team.") 36 SampleDataCmd.Flags().Int("posts-per-channel", 100, "The number of sample post per channel.") 37 SampleDataCmd.Flags().Int("direct-channels", 30, "The number of sample direct message channels.") 38 SampleDataCmd.Flags().Int("posts-per-direct-channel", 15, "The number of sample posts per direct message channel.") 39 SampleDataCmd.Flags().Int("group-channels", 15, "The number of sample group message channels.") 40 SampleDataCmd.Flags().Int("posts-per-group-channel", 30, "The number of sample posts per group message channel.") 41 SampleDataCmd.Flags().IntP("workers", "w", 2, "How many workers to run during the import.") 42 SampleDataCmd.Flags().String("profile-images", "", "Optional. Path to folder with images to randomly pick as user profile image.") 43 SampleDataCmd.Flags().StringP("bulk", "b", "", "Optional. Path to write a JSONL bulk file instead of loading into the database.") 44 RootCmd.AddCommand(SampleDataCmd) 45 } 46 47 func sliceIncludes(vs []string, t string) bool { 48 for _, v := range vs { 49 if v == t { 50 return true 51 } 52 } 53 return false 54 } 55 56 func randomPastTime(seconds int) int64 { 57 now := time.Now() 58 today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.FixedZone("UTC", 0)) 59 return (today.Unix() * 1000) - int64(rand.Intn(seconds*1000)) 60 } 61 62 func sortedRandomDates(size int) []int64 { 63 dates := make([]int64, size) 64 for i := 0; i < size; i++ { 65 dates[i] = randomPastTime(50000) 66 } 67 sort.Slice(dates, func(a, b int) bool { return dates[a] < dates[b] }) 68 return dates 69 } 70 71 func randomEmoji() string { 72 emojis := []string{"+1", "-1", "heart", "blush"} 73 return emojis[rand.Intn(len(emojis))] 74 } 75 76 func randomReaction(users []string, parentCreateAt int64) app.ReactionImportData { 77 user := users[rand.Intn(len(users))] 78 emoji := randomEmoji() 79 date := parentCreateAt + int64(rand.Intn(100000)) 80 return app.ReactionImportData{ 81 User: &user, 82 EmojiName: &emoji, 83 CreateAt: &date, 84 } 85 } 86 87 func randomReply(users []string, parentCreateAt int64) app.ReplyImportData { 88 user := users[rand.Intn(len(users))] 89 message := randomMessage(users) 90 date := parentCreateAt + int64(rand.Intn(100000)) 91 return app.ReplyImportData{ 92 User: &user, 93 Message: &message, 94 CreateAt: &date, 95 } 96 } 97 98 func randomMessage(users []string) string { 99 var message string 100 switch rand.Intn(30) { 101 case 0: 102 mention := users[rand.Intn(len(users))] 103 message = "@" + mention + " " + fake.Sentence() 104 case 1: 105 switch rand.Intn(2) { 106 case 0: 107 mattermostVideos := []string{"Q4MgnxbpZas", "BFo7E9-Kc_E", "LsMLR-BHsKg", "MRmGDhlMhNA", "mUOPxT7VgWc"} 108 message = "https://www.youtube.com/watch?v=" + mattermostVideos[rand.Intn(len(mattermostVideos))] 109 case 1: 110 mattermostTweets := []string{"943119062334353408", "949370809528832005", "948539688171819009", "939122439115681792", "938061722027425797"} 111 message = "https://twitter.com/mattermosthq/status/" + mattermostTweets[rand.Intn(len(mattermostTweets))] 112 } 113 case 2: 114 message = "" 115 if rand.Intn(2) == 0 { 116 message += fake.Sentence() 117 } 118 for i := 0; i < rand.Intn(4)+1; i++ { 119 message += "\n * " + fake.Word() 120 } 121 default: 122 if rand.Intn(2) == 0 { 123 message = fake.Sentence() 124 } else { 125 message = fake.Paragraph() 126 } 127 if rand.Intn(3) == 0 { 128 message += "\n" + fake.Sentence() 129 } 130 if rand.Intn(3) == 0 { 131 message += "\n" + fake.Sentence() 132 } 133 if rand.Intn(3) == 0 { 134 message += "\n" + fake.Sentence() 135 } 136 } 137 return message 138 } 139 140 func sampleDataCmdF(command *cobra.Command, args []string) error { 141 a, err := InitDBCommandContextCobra(command) 142 if err != nil { 143 return err 144 } 145 defer a.Shutdown() 146 147 seed, err := command.Flags().GetInt64("seed") 148 if err != nil { 149 return errors.New("Invalid seed parameter") 150 } 151 bulk, err := command.Flags().GetString("bulk") 152 if err != nil { 153 return errors.New("Invalid bulk parameter") 154 } 155 teams, err := command.Flags().GetInt("teams") 156 if err != nil || teams < 0 { 157 return errors.New("Invalid teams parameter") 158 } 159 channelsPerTeam, err := command.Flags().GetInt("channels-per-team") 160 if err != nil || channelsPerTeam < 0 { 161 return errors.New("Invalid channels-per-team parameter") 162 } 163 users, err := command.Flags().GetInt("users") 164 if err != nil || users < 0 { 165 return errors.New("Invalid users parameter") 166 } 167 teamMemberships, err := command.Flags().GetInt("team-memberships") 168 if err != nil || teamMemberships < 0 { 169 return errors.New("Invalid team-memberships parameter") 170 } 171 channelMemberships, err := command.Flags().GetInt("channel-memberships") 172 if err != nil || channelMemberships < 0 { 173 return errors.New("Invalid channel-memberships parameter") 174 } 175 postsPerChannel, err := command.Flags().GetInt("posts-per-channel") 176 if err != nil || postsPerChannel < 0 { 177 return errors.New("Invalid posts-per-channel parameter") 178 } 179 directChannels, err := command.Flags().GetInt("direct-channels") 180 if err != nil || directChannels < 0 { 181 return errors.New("Invalid direct-channels parameter") 182 } 183 postsPerDirectChannel, err := command.Flags().GetInt("posts-per-direct-channel") 184 if err != nil || postsPerDirectChannel < 0 { 185 return errors.New("Invalid posts-per-direct-channel parameter") 186 } 187 groupChannels, err := command.Flags().GetInt("group-channels") 188 if err != nil || groupChannels < 0 { 189 return errors.New("Invalid group-channels parameter") 190 } 191 postsPerGroupChannel, err := command.Flags().GetInt("posts-per-group-channel") 192 if err != nil || postsPerGroupChannel < 0 { 193 return errors.New("Invalid posts-per-group-channel parameter") 194 } 195 workers, err := command.Flags().GetInt("workers") 196 if err != nil { 197 return errors.New("Invalid workers parameter") 198 } 199 profileImagesPath, err := command.Flags().GetString("profile-images") 200 if err != nil { 201 return errors.New("Invalid profile-images parameter") 202 } 203 profileImages := []string{} 204 if profileImagesPath != "" { 205 profileImagesStat, err := os.Stat(profileImagesPath) 206 if os.IsNotExist(err) { 207 return errors.New("Profile images folder doesn't exists.") 208 } 209 if !profileImagesStat.IsDir() { 210 return errors.New("profile-images parameters must be a folder path.") 211 } 212 profileImagesFiles, err := ioutil.ReadDir(profileImagesPath) 213 if err != nil { 214 return errors.New("Invalid profile-images parameter") 215 } 216 for _, profileImage := range profileImagesFiles { 217 profileImages = append(profileImages, path.Join(profileImagesPath, profileImage.Name())) 218 } 219 sort.Strings(profileImages) 220 } 221 222 if workers < 1 { 223 return errors.New("You must have at least one worker.") 224 } 225 if teamMemberships > teams { 226 return errors.New("You can't have more team memberships than teams.") 227 } 228 if channelMemberships > channelsPerTeam { 229 return errors.New("You can't have more channel memberships than channels per team.") 230 } 231 232 var bulkFile *os.File 233 switch bulk { 234 case "": 235 bulkFile, err = ioutil.TempFile("", ".mattermost-sample-data-") 236 defer os.Remove(bulkFile.Name()) 237 if err != nil { 238 return errors.New("Unable to open temporary file.") 239 } 240 case "-": 241 bulkFile = os.Stdout 242 default: 243 bulkFile, err = os.OpenFile(bulk, os.O_RDWR|os.O_CREATE, 0755) 244 if err != nil { 245 return errors.New("Unable to write into the \"" + bulk + "\" file.") 246 } 247 } 248 249 encoder := json.NewEncoder(bulkFile) 250 version := 1 251 encoder.Encode(app.LineImportData{Type: "version", Version: &version}) 252 253 fake.Seed(seed) 254 rand.Seed(seed) 255 256 teamsAndChannels := make(map[string][]string) 257 for i := 0; i < teams; i++ { 258 teamLine := createTeam(i) 259 teamsAndChannels[*teamLine.Team.Name] = []string{} 260 encoder.Encode(teamLine) 261 } 262 263 teamsList := []string{} 264 for teamName := range teamsAndChannels { 265 teamsList = append(teamsList, teamName) 266 } 267 sort.Strings(teamsList) 268 269 for _, teamName := range teamsList { 270 for i := 0; i < channelsPerTeam; i++ { 271 channelLine := createChannel(i, teamName) 272 teamsAndChannels[teamName] = append(teamsAndChannels[teamName], *channelLine.Channel.Name) 273 encoder.Encode(channelLine) 274 } 275 } 276 277 allUsers := []string{} 278 for i := 0; i < users; i++ { 279 userLine := createUser(i, teamMemberships, channelMemberships, teamsAndChannels, profileImages) 280 encoder.Encode(userLine) 281 allUsers = append(allUsers, *userLine.User.Username) 282 } 283 284 for team, channels := range teamsAndChannels { 285 for _, channel := range channels { 286 dates := sortedRandomDates(postsPerChannel) 287 288 for i := 0; i < postsPerChannel; i++ { 289 postLine := createPost(team, channel, allUsers, dates[i]) 290 encoder.Encode(postLine) 291 } 292 } 293 } 294 295 for i := 0; i < directChannels; i++ { 296 user1 := allUsers[rand.Intn(len(allUsers))] 297 user2 := allUsers[rand.Intn(len(allUsers))] 298 channelLine := createDirectChannel([]string{user1, user2}) 299 encoder.Encode(channelLine) 300 301 dates := sortedRandomDates(postsPerDirectChannel) 302 for j := 0; j < postsPerDirectChannel; j++ { 303 postLine := createDirectPost([]string{user1, user2}, dates[j]) 304 encoder.Encode(postLine) 305 } 306 } 307 308 for i := 0; i < groupChannels; i++ { 309 users := []string{} 310 totalUsers := 3 + rand.Intn(3) 311 for len(users) < totalUsers { 312 user := allUsers[rand.Intn(len(allUsers))] 313 if !sliceIncludes(users, user) { 314 users = append(users, user) 315 } 316 } 317 channelLine := createDirectChannel(users) 318 encoder.Encode(channelLine) 319 320 dates := sortedRandomDates(postsPerGroupChannel) 321 for j := 0; j < postsPerGroupChannel; j++ { 322 postLine := createDirectPost(users, dates[j]) 323 encoder.Encode(postLine) 324 } 325 } 326 327 if bulk == "" { 328 _, err := bulkFile.Seek(0, 0) 329 if err != nil { 330 return errors.New("Unable to read correctly the temporary file.") 331 } 332 importErr, lineNumber := a.BulkImport(bulkFile, false, workers) 333 if importErr != nil { 334 return fmt.Errorf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber) 335 } 336 } else if bulk != "-" { 337 err := bulkFile.Close() 338 if err != nil { 339 return errors.New("Unable to close correctly the output file") 340 } 341 } 342 343 return nil 344 } 345 346 func createUser(idx int, teamMemberships int, channelMemberships int, teamsAndChannels map[string][]string, profileImages []string) app.LineImportData { 347 password := fmt.Sprintf("user-%d", idx) 348 email := fmt.Sprintf("user-%d@sample.mattermost.com", idx) 349 firstName := fake.FirstName() 350 lastName := fake.LastName() 351 username := fmt.Sprintf("%s.%s", strings.ToLower(firstName), strings.ToLower(lastName)) 352 if idx == 0 { 353 username = "sysadmin" 354 password = "sysadmin" 355 email = "sysadmin@sample.mattermost.com" 356 } else if idx == 1 { 357 username = "user-1" 358 } 359 position := fake.JobTitle() 360 roles := "system_user" 361 if idx%5 == 0 { 362 roles = "system_admin system_user" 363 } 364 365 // The 75% of the users have custom profile image 366 var profileImage *string = nil 367 if rand.Intn(4) != 0 { 368 profileImageSelector := rand.Int() 369 if len(profileImages) > 0 { 370 profileImage = &profileImages[profileImageSelector%len(profileImages)] 371 } 372 } 373 374 useMilitaryTime := "false" 375 if idx != 0 && rand.Intn(2) == 0 { 376 useMilitaryTime = "true" 377 } 378 379 collapsePreviews := "false" 380 if idx != 0 && rand.Intn(2) == 0 { 381 collapsePreviews = "true" 382 } 383 384 messageDisplay := "clean" 385 if idx != 0 && rand.Intn(2) == 0 { 386 messageDisplay = "compact" 387 } 388 389 channelDisplayMode := "full" 390 if idx != 0 && rand.Intn(2) == 0 { 391 channelDisplayMode = "centered" 392 } 393 394 // Some users has nickname 395 nickname := "" 396 if rand.Intn(5) == 0 { 397 nickname = fake.Company() 398 } 399 400 // Half of users skip tutorial 401 tutorialStep := "999" 402 switch rand.Intn(6) { 403 case 1: 404 tutorialStep = "1" 405 case 2: 406 tutorialStep = "2" 407 case 3: 408 tutorialStep = "3" 409 } 410 411 teams := []app.UserTeamImportData{} 412 possibleTeams := []string{} 413 for teamName := range teamsAndChannels { 414 possibleTeams = append(possibleTeams, teamName) 415 } 416 sort.Strings(possibleTeams) 417 for x := 0; x < teamMemberships; x++ { 418 if len(possibleTeams) == 0 { 419 break 420 } 421 position := rand.Intn(len(possibleTeams)) 422 team := possibleTeams[position] 423 possibleTeams = append(possibleTeams[:position], possibleTeams[position+1:]...) 424 if teamChannels, err := teamsAndChannels[team]; err { 425 teams = append(teams, createTeamMembership(channelMemberships, teamChannels, &team)) 426 } 427 } 428 429 user := app.UserImportData{ 430 ProfileImage: profileImage, 431 Username: &username, 432 Email: &email, 433 Password: &password, 434 Nickname: &nickname, 435 FirstName: &firstName, 436 LastName: &lastName, 437 Position: &position, 438 Roles: &roles, 439 Teams: &teams, 440 UseMilitaryTime: &useMilitaryTime, 441 CollapsePreviews: &collapsePreviews, 442 MessageDisplay: &messageDisplay, 443 ChannelDisplayMode: &channelDisplayMode, 444 TutorialStep: &tutorialStep, 445 } 446 return app.LineImportData{ 447 Type: "user", 448 User: &user, 449 } 450 } 451 452 func createTeamMembership(numOfchannels int, teamChannels []string, teamName *string) app.UserTeamImportData { 453 roles := "team_user" 454 if rand.Intn(5) == 0 { 455 roles = "team_user team_admin" 456 } 457 channels := []app.UserChannelImportData{} 458 teamChannelsCopy := append([]string(nil), teamChannels...) 459 for x := 0; x < numOfchannels; x++ { 460 if len(teamChannelsCopy) == 0 { 461 break 462 } 463 position := rand.Intn(len(teamChannelsCopy)) 464 channelName := teamChannelsCopy[position] 465 teamChannelsCopy = append(teamChannelsCopy[:position], teamChannelsCopy[position+1:]...) 466 channels = append(channels, createChannelMembership(channelName)) 467 } 468 469 return app.UserTeamImportData{ 470 Name: teamName, 471 Roles: &roles, 472 Channels: &channels, 473 } 474 } 475 476 func createChannelMembership(channelName string) app.UserChannelImportData { 477 roles := "channel_user" 478 if rand.Intn(5) == 0 { 479 roles = "channel_user channel_admin" 480 } 481 favorite := rand.Intn(5) == 0 482 483 return app.UserChannelImportData{ 484 Name: &channelName, 485 Roles: &roles, 486 Favorite: &favorite, 487 } 488 } 489 490 func createTeam(idx int) app.LineImportData { 491 displayName := fake.Word() 492 name := fmt.Sprintf("%s-%d", fake.Word(), idx) 493 allowOpenInvite := rand.Intn(2) == 0 494 495 description := fake.Paragraph() 496 if len(description) > 255 { 497 description = description[0:255] 498 } 499 500 teamType := "O" 501 if rand.Intn(2) == 0 { 502 teamType = "I" 503 } 504 505 team := app.TeamImportData{ 506 DisplayName: &displayName, 507 Name: &name, 508 AllowOpenInvite: &allowOpenInvite, 509 Description: &description, 510 Type: &teamType, 511 } 512 return app.LineImportData{ 513 Type: "team", 514 Team: &team, 515 } 516 } 517 518 func createChannel(idx int, teamName string) app.LineImportData { 519 displayName := fake.Word() 520 name := fmt.Sprintf("%s-%d", fake.Word(), idx) 521 header := fake.Paragraph() 522 purpose := fake.Paragraph() 523 524 if len(purpose) > 250 { 525 purpose = purpose[0:250] 526 } 527 528 channelType := "P" 529 if rand.Intn(2) == 0 { 530 channelType = "O" 531 } 532 533 channel := app.ChannelImportData{ 534 Team: &teamName, 535 Name: &name, 536 DisplayName: &displayName, 537 Type: &channelType, 538 Header: &header, 539 Purpose: &purpose, 540 } 541 return app.LineImportData{ 542 Type: "channel", 543 Channel: &channel, 544 } 545 } 546 547 func createPost(team string, channel string, allUsers []string, createAt int64) app.LineImportData { 548 message := randomMessage(allUsers) 549 create_at := createAt 550 user := allUsers[rand.Intn(len(allUsers))] 551 552 // Some messages are flagged by an user 553 flagged_by := []string{} 554 if rand.Intn(10) == 0 { 555 flagged_by = append(flagged_by, allUsers[rand.Intn(len(allUsers))]) 556 } 557 558 reactions := []app.ReactionImportData{} 559 if rand.Intn(10) == 0 { 560 for { 561 reactions = append(reactions, randomReaction(allUsers, create_at)) 562 if rand.Intn(3) == 0 { 563 break 564 } 565 } 566 } 567 568 replies := []app.ReplyImportData{} 569 if rand.Intn(10) == 0 { 570 for { 571 replies = append(replies, randomReply(allUsers, create_at)) 572 if rand.Intn(4) == 0 { 573 break 574 } 575 } 576 } 577 578 post := app.PostImportData{ 579 Team: &team, 580 Channel: &channel, 581 User: &user, 582 Message: &message, 583 CreateAt: &create_at, 584 FlaggedBy: &flagged_by, 585 Reactions: &reactions, 586 Replies: &replies, 587 } 588 return app.LineImportData{ 589 Type: "post", 590 Post: &post, 591 } 592 } 593 594 func createDirectChannel(members []string) app.LineImportData { 595 header := fake.Sentence() 596 597 channel := app.DirectChannelImportData{ 598 Members: &members, 599 Header: &header, 600 } 601 return app.LineImportData{ 602 Type: "direct_channel", 603 DirectChannel: &channel, 604 } 605 } 606 607 func createDirectPost(members []string, createAt int64) app.LineImportData { 608 message := randomMessage(members) 609 create_at := createAt 610 user := members[rand.Intn(len(members))] 611 612 // Some messages are flagged by an user 613 flagged_by := []string{} 614 if rand.Intn(10) == 0 { 615 flagged_by = append(flagged_by, members[rand.Intn(len(members))]) 616 } 617 618 reactions := []app.ReactionImportData{} 619 if rand.Intn(10) == 0 { 620 for { 621 reactions = append(reactions, randomReaction(members, create_at)) 622 if rand.Intn(3) == 0 { 623 break 624 } 625 } 626 } 627 628 replies := []app.ReplyImportData{} 629 if rand.Intn(10) == 0 { 630 for { 631 replies = append(replies, randomReply(members, create_at)) 632 if rand.Intn(4) == 0 { 633 break 634 } 635 } 636 } 637 638 post := app.DirectPostImportData{ 639 ChannelMembers: &members, 640 User: &user, 641 Message: &message, 642 CreateAt: &create_at, 643 FlaggedBy: &flagged_by, 644 Reactions: &reactions, 645 Replies: &replies, 646 } 647 return app.LineImportData{ 648 Type: "direct_post", 649 DirectPost: &post, 650 } 651 }