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