github.com/keys-pub/mattermost-server@v4.10.10+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  }