github.com/rajatvaryani/mattermost-server@v5.11.1+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 var profileImagesStat os.FileInfo 206 profileImagesStat, err = os.Stat(profileImagesPath) 207 if os.IsNotExist(err) { 208 return errors.New("Profile images folder doesn't exists.") 209 } 210 if !profileImagesStat.IsDir() { 211 return errors.New("profile-images parameters must be a folder path.") 212 } 213 var profileImagesFiles []os.FileInfo 214 profileImagesFiles, err = ioutil.ReadDir(profileImagesPath) 215 if err != nil { 216 return errors.New("Invalid profile-images parameter") 217 } 218 for _, profileImage := range profileImagesFiles { 219 profileImages = append(profileImages, path.Join(profileImagesPath, profileImage.Name())) 220 } 221 sort.Strings(profileImages) 222 } 223 224 if workers < 1 { 225 return errors.New("You must have at least one worker.") 226 } 227 if teamMemberships > teams { 228 return errors.New("You can't have more team memberships than teams.") 229 } 230 if channelMemberships > channelsPerTeam { 231 return errors.New("You can't have more channel memberships than channels per team.") 232 } 233 234 var bulkFile *os.File 235 switch bulk { 236 case "": 237 bulkFile, err = ioutil.TempFile("", ".mattermost-sample-data-") 238 defer os.Remove(bulkFile.Name()) 239 if err != nil { 240 return errors.New("Unable to open temporary file.") 241 } 242 case "-": 243 bulkFile = os.Stdout 244 default: 245 bulkFile, err = os.OpenFile(bulk, os.O_RDWR|os.O_CREATE, 0755) 246 if err != nil { 247 return errors.New("Unable to write into the \"" + bulk + "\" file.") 248 } 249 } 250 251 encoder := json.NewEncoder(bulkFile) 252 version := 1 253 encoder.Encode(app.LineImportData{Type: "version", Version: &version}) 254 255 fake.Seed(seed) 256 rand.Seed(seed) 257 258 teamsAndChannels := make(map[string][]string) 259 for i := 0; i < teams; i++ { 260 teamLine := createTeam(i) 261 teamsAndChannels[*teamLine.Team.Name] = []string{} 262 encoder.Encode(teamLine) 263 } 264 265 teamsList := []string{} 266 for teamName := range teamsAndChannels { 267 teamsList = append(teamsList, teamName) 268 } 269 sort.Strings(teamsList) 270 271 for _, teamName := range teamsList { 272 for i := 0; i < channelsPerTeam; i++ { 273 channelLine := createChannel(i, teamName) 274 teamsAndChannels[teamName] = append(teamsAndChannels[teamName], *channelLine.Channel.Name) 275 encoder.Encode(channelLine) 276 } 277 } 278 279 allUsers := []string{} 280 for i := 0; i < users; i++ { 281 userLine := createUser(i, teamMemberships, channelMemberships, teamsAndChannels, profileImages) 282 encoder.Encode(userLine) 283 allUsers = append(allUsers, *userLine.User.Username) 284 } 285 286 for team, channels := range teamsAndChannels { 287 for _, channel := range channels { 288 dates := sortedRandomDates(postsPerChannel) 289 290 for i := 0; i < postsPerChannel; i++ { 291 postLine := createPost(team, channel, allUsers, dates[i]) 292 encoder.Encode(postLine) 293 } 294 } 295 } 296 297 for i := 0; i < directChannels; i++ { 298 user1 := allUsers[rand.Intn(len(allUsers))] 299 user2 := allUsers[rand.Intn(len(allUsers))] 300 channelLine := createDirectChannel([]string{user1, user2}) 301 encoder.Encode(channelLine) 302 303 dates := sortedRandomDates(postsPerDirectChannel) 304 for j := 0; j < postsPerDirectChannel; j++ { 305 postLine := createDirectPost([]string{user1, user2}, dates[j]) 306 encoder.Encode(postLine) 307 } 308 } 309 310 for i := 0; i < groupChannels; i++ { 311 users := []string{} 312 totalUsers := 3 + rand.Intn(3) 313 for len(users) < totalUsers { 314 user := allUsers[rand.Intn(len(allUsers))] 315 if !sliceIncludes(users, user) { 316 users = append(users, user) 317 } 318 } 319 channelLine := createDirectChannel(users) 320 encoder.Encode(channelLine) 321 322 dates := sortedRandomDates(postsPerGroupChannel) 323 for j := 0; j < postsPerGroupChannel; j++ { 324 postLine := createDirectPost(users, dates[j]) 325 encoder.Encode(postLine) 326 } 327 } 328 329 if bulk == "" { 330 _, err := bulkFile.Seek(0, 0) 331 if err != nil { 332 return errors.New("Unable to read correctly the temporary file.") 333 } 334 importErr, lineNumber := a.BulkImport(bulkFile, false, workers) 335 if importErr != nil { 336 return fmt.Errorf("%s: %s, %s (line: %d)", importErr.Where, importErr.Message, importErr.DetailedError, lineNumber) 337 } 338 } else if bulk != "-" { 339 err := bulkFile.Close() 340 if err != nil { 341 return errors.New("Unable to close correctly the output file") 342 } 343 } 344 345 return nil 346 } 347 348 func createUser(idx int, teamMemberships int, channelMemberships int, teamsAndChannels map[string][]string, profileImages []string) app.LineImportData { 349 password := fmt.Sprintf("user-%d", idx) 350 email := fmt.Sprintf("user-%d@sample.mattermost.com", idx) 351 firstName := fake.FirstName() 352 lastName := fake.LastName() 353 username := fmt.Sprintf("%s.%s", strings.ToLower(firstName), strings.ToLower(lastName)) 354 if idx == 0 { 355 username = "sysadmin" 356 password = "sysadmin" 357 email = "sysadmin@sample.mattermost.com" 358 } else if idx == 1 { 359 username = "user-1" 360 } 361 position := fake.JobTitle() 362 roles := "system_user" 363 if idx%5 == 0 { 364 roles = "system_admin system_user" 365 } 366 367 // The 75% of the users have custom profile image 368 var profileImage *string = nil 369 if rand.Intn(4) != 0 { 370 profileImageSelector := rand.Int() 371 if len(profileImages) > 0 { 372 profileImage = &profileImages[profileImageSelector%len(profileImages)] 373 } 374 } 375 376 useMilitaryTime := "false" 377 if idx != 0 && rand.Intn(2) == 0 { 378 useMilitaryTime = "true" 379 } 380 381 collapsePreviews := "false" 382 if idx != 0 && rand.Intn(2) == 0 { 383 collapsePreviews = "true" 384 } 385 386 messageDisplay := "clean" 387 if idx != 0 && rand.Intn(2) == 0 { 388 messageDisplay = "compact" 389 } 390 391 channelDisplayMode := "full" 392 if idx != 0 && rand.Intn(2) == 0 { 393 channelDisplayMode = "centered" 394 } 395 396 // Some users has nickname 397 nickname := "" 398 if rand.Intn(5) == 0 { 399 nickname = fake.Company() 400 } 401 402 // sysadmin, user-1 and user-2 users skip tutorial steps 403 // Other half of users also skip tutorial steps 404 tutorialStep := "999" 405 if idx > 2 { 406 switch rand.Intn(6) { 407 case 1: 408 tutorialStep = "1" 409 case 2: 410 tutorialStep = "2" 411 case 3: 412 tutorialStep = "3" 413 } 414 } 415 416 teams := []app.UserTeamImportData{} 417 possibleTeams := []string{} 418 for teamName := range teamsAndChannels { 419 possibleTeams = append(possibleTeams, teamName) 420 } 421 sort.Strings(possibleTeams) 422 for x := 0; x < teamMemberships; x++ { 423 if len(possibleTeams) == 0 { 424 break 425 } 426 position := rand.Intn(len(possibleTeams)) 427 team := possibleTeams[position] 428 possibleTeams = append(possibleTeams[:position], possibleTeams[position+1:]...) 429 if teamChannels, err := teamsAndChannels[team]; err { 430 teams = append(teams, createTeamMembership(channelMemberships, teamChannels, &team)) 431 } 432 } 433 434 user := app.UserImportData{ 435 ProfileImage: profileImage, 436 Username: &username, 437 Email: &email, 438 Password: &password, 439 Nickname: &nickname, 440 FirstName: &firstName, 441 LastName: &lastName, 442 Position: &position, 443 Roles: &roles, 444 Teams: &teams, 445 UseMilitaryTime: &useMilitaryTime, 446 CollapsePreviews: &collapsePreviews, 447 MessageDisplay: &messageDisplay, 448 ChannelDisplayMode: &channelDisplayMode, 449 TutorialStep: &tutorialStep, 450 } 451 return app.LineImportData{ 452 Type: "user", 453 User: &user, 454 } 455 } 456 457 func createTeamMembership(numOfchannels int, teamChannels []string, teamName *string) app.UserTeamImportData { 458 roles := "team_user" 459 if rand.Intn(5) == 0 { 460 roles = "team_user team_admin" 461 } 462 channels := []app.UserChannelImportData{} 463 teamChannelsCopy := append([]string(nil), teamChannels...) 464 for x := 0; x < numOfchannels; x++ { 465 if len(teamChannelsCopy) == 0 { 466 break 467 } 468 position := rand.Intn(len(teamChannelsCopy)) 469 channelName := teamChannelsCopy[position] 470 teamChannelsCopy = append(teamChannelsCopy[:position], teamChannelsCopy[position+1:]...) 471 channels = append(channels, createChannelMembership(channelName)) 472 } 473 474 return app.UserTeamImportData{ 475 Name: teamName, 476 Roles: &roles, 477 Channels: &channels, 478 } 479 } 480 481 func createChannelMembership(channelName string) app.UserChannelImportData { 482 roles := "channel_user" 483 if rand.Intn(5) == 0 { 484 roles = "channel_user channel_admin" 485 } 486 favorite := rand.Intn(5) == 0 487 488 return app.UserChannelImportData{ 489 Name: &channelName, 490 Roles: &roles, 491 Favorite: &favorite, 492 } 493 } 494 495 func createTeam(idx int) app.LineImportData { 496 displayName := fake.Word() 497 name := fmt.Sprintf("%s-%d", fake.Word(), idx) 498 allowOpenInvite := rand.Intn(2) == 0 499 500 description := fake.Paragraph() 501 if len(description) > 255 { 502 description = description[0:255] 503 } 504 505 teamType := "O" 506 if rand.Intn(2) == 0 { 507 teamType = "I" 508 } 509 510 team := app.TeamImportData{ 511 DisplayName: &displayName, 512 Name: &name, 513 AllowOpenInvite: &allowOpenInvite, 514 Description: &description, 515 Type: &teamType, 516 } 517 return app.LineImportData{ 518 Type: "team", 519 Team: &team, 520 } 521 } 522 523 func createChannel(idx int, teamName string) app.LineImportData { 524 displayName := fake.Word() 525 name := fmt.Sprintf("%s-%d", fake.Word(), idx) 526 header := fake.Paragraph() 527 purpose := fake.Paragraph() 528 529 if len(purpose) > 250 { 530 purpose = purpose[0:250] 531 } 532 533 channelType := "P" 534 if rand.Intn(2) == 0 { 535 channelType = "O" 536 } 537 538 channel := app.ChannelImportData{ 539 Team: &teamName, 540 Name: &name, 541 DisplayName: &displayName, 542 Type: &channelType, 543 Header: &header, 544 Purpose: &purpose, 545 } 546 return app.LineImportData{ 547 Type: "channel", 548 Channel: &channel, 549 } 550 } 551 552 func createPost(team string, channel string, allUsers []string, createAt int64) app.LineImportData { 553 message := randomMessage(allUsers) 554 create_at := createAt 555 user := allUsers[rand.Intn(len(allUsers))] 556 557 // Some messages are flagged by an user 558 flagged_by := []string{} 559 if rand.Intn(10) == 0 { 560 flagged_by = append(flagged_by, allUsers[rand.Intn(len(allUsers))]) 561 } 562 563 reactions := []app.ReactionImportData{} 564 if rand.Intn(10) == 0 { 565 for { 566 reactions = append(reactions, randomReaction(allUsers, create_at)) 567 if rand.Intn(3) == 0 { 568 break 569 } 570 } 571 } 572 573 replies := []app.ReplyImportData{} 574 if rand.Intn(10) == 0 { 575 for { 576 replies = append(replies, randomReply(allUsers, create_at)) 577 if rand.Intn(4) == 0 { 578 break 579 } 580 } 581 } 582 583 post := app.PostImportData{ 584 Team: &team, 585 Channel: &channel, 586 User: &user, 587 Message: &message, 588 CreateAt: &create_at, 589 FlaggedBy: &flagged_by, 590 Reactions: &reactions, 591 Replies: &replies, 592 } 593 return app.LineImportData{ 594 Type: "post", 595 Post: &post, 596 } 597 } 598 599 func createDirectChannel(members []string) app.LineImportData { 600 header := fake.Sentence() 601 602 channel := app.DirectChannelImportData{ 603 Members: &members, 604 Header: &header, 605 } 606 return app.LineImportData{ 607 Type: "direct_channel", 608 DirectChannel: &channel, 609 } 610 } 611 612 func createDirectPost(members []string, createAt int64) app.LineImportData { 613 message := randomMessage(members) 614 create_at := createAt 615 user := members[rand.Intn(len(members))] 616 617 // Some messages are flagged by an user 618 flagged_by := []string{} 619 if rand.Intn(10) == 0 { 620 flagged_by = append(flagged_by, members[rand.Intn(len(members))]) 621 } 622 623 reactions := []app.ReactionImportData{} 624 if rand.Intn(10) == 0 { 625 for { 626 reactions = append(reactions, randomReaction(members, create_at)) 627 if rand.Intn(3) == 0 { 628 break 629 } 630 } 631 } 632 633 replies := []app.ReplyImportData{} 634 if rand.Intn(10) == 0 { 635 for { 636 replies = append(replies, randomReply(members, create_at)) 637 if rand.Intn(4) == 0 { 638 break 639 } 640 } 641 } 642 643 post := app.DirectPostImportData{ 644 ChannelMembers: &members, 645 User: &user, 646 Message: &message, 647 CreateAt: &create_at, 648 FlaggedBy: &flagged_by, 649 Reactions: &reactions, 650 Replies: &replies, 651 } 652 return app.LineImportData{ 653 Type: "direct_post", 654 DirectPost: &post, 655 } 656 }