diff options
author | Wim <wim@42.be> | 2020-08-10 00:29:54 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-10 00:29:54 +0200 |
commit | 4e50fd864921c556988c919269448efdb90fa961 (patch) | |
tree | a3625f03f8de3c4f3841364000a4ea3aa42c1533 /vendor/github.com/mattermost/mattermost-server/v5/model | |
parent | dfdffa0027334e55ce213fc6eb62206dbf48baf6 (diff) | |
download | matterbridge-msglm-4e50fd864921c556988c919269448efdb90fa961.tar.gz matterbridge-msglm-4e50fd864921c556988c919269448efdb90fa961.tar.bz2 matterbridge-msglm-4e50fd864921c556988c919269448efdb90fa961.zip |
Use mattermost v5 module (#1192)
Diffstat (limited to 'vendor/github.com/mattermost/mattermost-server/v5/model')
112 files changed, 23742 insertions, 0 deletions
diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/access.go b/vendor/github.com/mattermost/mattermost-server/v5/model/access.go new file mode 100644 index 00000000..bbac3601 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/access.go @@ -0,0 +1,96 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" +) + +const ( + ACCESS_TOKEN_GRANT_TYPE = "authorization_code" + ACCESS_TOKEN_TYPE = "bearer" + REFRESH_TOKEN_GRANT_TYPE = "refresh_token" +) + +type AccessData struct { + ClientId string `json:"client_id"` + UserId string `json:"user_id"` + Token string `json:"token"` + RefreshToken string `json:"refresh_token"` + RedirectUri string `json:"redirect_uri"` + ExpiresAt int64 `json:"expires_at"` + Scope string `json:"scope"` +} + +type AccessResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int32 `json:"expires_in"` + Scope string `json:"scope"` + RefreshToken string `json:"refresh_token"` +} + +// IsValid validates the AccessData and returns an error if it isn't configured +// correctly. +func (ad *AccessData) IsValid() *AppError { + + if len(ad.ClientId) == 0 || len(ad.ClientId) > 26 { + return NewAppError("AccessData.IsValid", "model.access.is_valid.client_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(ad.UserId) == 0 || len(ad.UserId) > 26 { + return NewAppError("AccessData.IsValid", "model.access.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(ad.Token) != 26 { + return NewAppError("AccessData.IsValid", "model.access.is_valid.access_token.app_error", nil, "", http.StatusBadRequest) + } + + if len(ad.RefreshToken) > 26 { + return NewAppError("AccessData.IsValid", "model.access.is_valid.refresh_token.app_error", nil, "", http.StatusBadRequest) + } + + if len(ad.RedirectUri) == 0 || len(ad.RedirectUri) > 256 || !IsValidHttpUrl(ad.RedirectUri) { + return NewAppError("AccessData.IsValid", "model.access.is_valid.redirect_uri.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (ad *AccessData) IsExpired() bool { + + if ad.ExpiresAt <= 0 { + return false + } + + if GetMillis() > ad.ExpiresAt { + return true + } + + return false +} + +func (ad *AccessData) ToJson() string { + b, _ := json.Marshal(ad) + return string(b) +} + +func AccessDataFromJson(data io.Reader) *AccessData { + var ad *AccessData + json.NewDecoder(data).Decode(&ad) + return ad +} + +func (ar *AccessResponse) ToJson() string { + b, _ := json.Marshal(ar) + return string(b) +} + +func AccessResponseFromJson(data io.Reader) *AccessResponse { + var ar *AccessResponse + json.NewDecoder(data).Decode(&ar) + return ar +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/analytics_row.go b/vendor/github.com/mattermost/mattermost-server/v5/model/analytics_row.go new file mode 100644 index 00000000..1180ad22 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/analytics_row.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type AnalyticsRow struct { + Name string `json:"name"` + Value float64 `json:"value"` +} + +type AnalyticsRows []*AnalyticsRow + +func (me *AnalyticsRow) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func AnalyticsRowFromJson(data io.Reader) *AnalyticsRow { + var me *AnalyticsRow + json.NewDecoder(data).Decode(&me) + return me +} + +func (me AnalyticsRows) ToJson() string { + if b, err := json.Marshal(me); err != nil { + return "[]" + } else { + return string(b) + } +} + +func AnalyticsRowsFromJson(data io.Reader) AnalyticsRows { + var me AnalyticsRows + json.NewDecoder(data).Decode(&me) + return me +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/at_mentions.go b/vendor/github.com/mattermost/mattermost-server/v5/model/at_mentions.go new file mode 100644 index 00000000..f41d182a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/at_mentions.go @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "regexp" + "strings" +) + +var atMentionRegexp = regexp.MustCompile(`\B@[[:alnum:]][[:alnum:]\.\-_]*`) + +const usernameSpecialChars = ".-_" + +// PossibleAtMentions returns all substrings in message that look like valid @ +// mentions. +func PossibleAtMentions(message string) []string { + var names []string + + if !strings.Contains(message, "@") { + return names + } + + alreadyMentioned := make(map[string]bool) + for _, match := range atMentionRegexp.FindAllString(message, -1) { + name := NormalizeUsername(match[1:]) + if !alreadyMentioned[name] && IsValidUsername(name) { + names = append(names, name) + alreadyMentioned[name] = true + } + } + + return names +} + +// TrimUsernameSpecialChar tries to remove the last character from word if it +// is a special character for usernames (dot, dash or underscore). If not, it +// returns the same string. +func TrimUsernameSpecialChar(word string) (string, bool) { + len := len(word) + + if len > 0 && strings.LastIndexAny(word, usernameSpecialChars) == (len-1) { + return word[:len-1], true + } + + return word, false +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/audit.go b/vendor/github.com/mattermost/mattermost-server/v5/model/audit.go new file mode 100644 index 00000000..dd1e0602 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/audit.go @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type Audit struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UserId string `json:"user_id"` + Action string `json:"action"` + ExtraInfo string `json:"extra_info"` + IpAddress string `json:"ip_address"` + SessionId string `json:"session_id"` +} + +func (o *Audit) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func AuditFromJson(data io.Reader) *Audit { + var o *Audit + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/auditconv.go b/vendor/github.com/mattermost/mattermost-server/v5/model/auditconv.go new file mode 100644 index 00000000..50af2880 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/auditconv.go @@ -0,0 +1,667 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import "github.com/francoispqt/gojay" + +// AuditModelTypeConv converts key model types to something better suited for audit output. +func AuditModelTypeConv(val interface{}) (newVal interface{}, converted bool) { + if val == nil { + return nil, false + } + switch v := val.(type) { + case *Channel: + return newAuditChannel(v), true + case *Team: + return newAuditTeam(v), true + case *User: + return newAuditUser(v), true + case *Command: + return newAuditCommand(v), true + case *CommandArgs: + return newAuditCommandArgs(v), true + case *Bot: + return newAuditBot(v), true + case *ChannelModerationPatch: + return newAuditChannelModerationPatch(v), true + case *Emoji: + return newAuditEmoji(v), true + case *FileInfo: + return newAuditFileInfo(v), true + case *Group: + return newAuditGroup(v), true + case *Job: + return newAuditJob(v), true + case *OAuthApp: + return newAuditOAuthApp(v), true + case *Post: + return newAuditPost(v), true + case *Role: + return newAuditRole(v), true + case *Scheme: + return newAuditScheme(v), true + case *SchemeRoles: + return newAuditSchemeRoles(v), true + case *Session: + return newAuditSession(v), true + case *IncomingWebhook: + return newAuditIncomingWebhook(v), true + case *OutgoingWebhook: + return newAuditOutgoingWebhook(v), true + } + return val, false +} + +type auditChannel struct { + ID string + Name string + Type string +} + +// newAuditChannel creates a simplified representation of Channel for output to audit log. +func newAuditChannel(c *Channel) auditChannel { + var channel auditChannel + if c != nil { + channel.ID = c.Id + channel.Name = c.Name + channel.Type = c.Type + } + return channel +} + +func (c auditChannel) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", c.ID) + enc.StringKey("name", c.Name) + enc.StringKey("type", c.Type) +} + +func (c auditChannel) IsNil() bool { + return false +} + +type auditTeam struct { + ID string + Name string + Type string +} + +// newAuditTeam creates a simplified representation of Team for output to audit log. +func newAuditTeam(t *Team) auditTeam { + var team auditTeam + if t != nil { + team.ID = t.Id + team.Name = t.Name + team.Type = t.Type + } + return team +} + +func (t auditTeam) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", t.ID) + enc.StringKey("name", t.Name) + enc.StringKey("type", t.Type) +} + +func (t auditTeam) IsNil() bool { + return false +} + +type auditUser struct { + ID string + Name string + Roles string +} + +// newAuditUser creates a simplified representation of User for output to audit log. +func newAuditUser(u *User) auditUser { + var user auditUser + if u != nil { + user.ID = u.Id + user.Name = u.Username + user.Roles = u.Roles + } + return user +} + +func (u auditUser) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", u.ID) + enc.StringKey("name", u.Name) + enc.StringKey("roles", u.Roles) +} + +func (u auditUser) IsNil() bool { + return false +} + +type auditCommand struct { + ID string + CreatorID string + TeamID string + Trigger string + Method string + Username string + IconURL string + AutoComplete bool + AutoCompleteDesc string + AutoCompleteHint string + DisplayName string + Description string + URL string +} + +// newAuditCommand creates a simplified representation of Command for output to audit log. +func newAuditCommand(c *Command) auditCommand { + var cmd auditCommand + if c != nil { + cmd.ID = c.Id + cmd.CreatorID = c.CreatorId + cmd.TeamID = c.TeamId + cmd.Trigger = c.Trigger + cmd.Method = c.Method + cmd.Username = c.Username + cmd.IconURL = c.IconURL + cmd.AutoComplete = c.AutoComplete + cmd.AutoCompleteDesc = c.AutoCompleteDesc + cmd.AutoCompleteHint = c.AutoCompleteHint + cmd.DisplayName = c.DisplayName + cmd.Description = c.Description + cmd.URL = c.URL + } + return cmd +} + +func (cmd auditCommand) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", cmd.ID) + enc.StringKey("creator_id", cmd.CreatorID) + enc.StringKey("team_id", cmd.TeamID) + enc.StringKey("trigger", cmd.Trigger) + enc.StringKey("method", cmd.Method) + enc.StringKey("username", cmd.Username) + enc.StringKey("icon_url", cmd.IconURL) + enc.BoolKey("auto_complete", cmd.AutoComplete) + enc.StringKey("auto_complete_desc", cmd.AutoCompleteDesc) + enc.StringKey("auto_complete_hint", cmd.AutoCompleteHint) + enc.StringKey("display", cmd.DisplayName) + enc.StringKey("desc", cmd.Description) + enc.StringKey("url", cmd.URL) +} + +func (cmd auditCommand) IsNil() bool { + return false +} + +type auditCommandArgs struct { + ChannelID string + TeamID string + TriggerID string + Command string +} + +// newAuditCommandArgs creates a simplified representation of CommandArgs for output to audit log. +func newAuditCommandArgs(ca *CommandArgs) auditCommandArgs { + var cmdargs auditCommandArgs + if ca != nil { + cmdargs.ChannelID = ca.ChannelId + cmdargs.TeamID = ca.TeamId + cmdargs.TriggerID = ca.TriggerId + cmdargs.Command = ca.Command + } + return cmdargs +} + +func (ca auditCommandArgs) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("channel_id", ca.ChannelID) + enc.StringKey("team_id", ca.TriggerID) + enc.StringKey("trigger_id", ca.TeamID) + enc.StringKey("command", ca.Command) +} + +func (ca auditCommandArgs) IsNil() bool { + return false +} + +type auditBot struct { + UserID string + Username string + Displayname string +} + +// newAuditBot creates a simplified representation of Bot for output to audit log. +func newAuditBot(b *Bot) auditBot { + var bot auditBot + if b != nil { + bot.UserID = b.UserId + bot.Username = b.Username + bot.Displayname = b.DisplayName + } + return bot +} + +func (b auditBot) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("user_id", b.UserID) + enc.StringKey("username", b.Username) + enc.StringKey("display", b.Displayname) +} + +func (b auditBot) IsNil() bool { + return false +} + +type auditChannelModerationPatch struct { + Name string + RoleGuests bool + RoleMembers bool +} + +// newAuditChannelModerationPatch creates a simplified representation of ChannelModerationPatch for output to audit log. +func newAuditChannelModerationPatch(p *ChannelModerationPatch) auditChannelModerationPatch { + var patch auditChannelModerationPatch + if p != nil { + if p.Name != nil { + patch.Name = *p.Name + } + if p.Roles.Guests != nil { + patch.RoleGuests = *p.Roles.Guests + } + if p.Roles.Members != nil { + patch.RoleMembers = *p.Roles.Members + } + } + return patch +} + +func (p auditChannelModerationPatch) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("name", p.Name) + enc.BoolKey("role_guests", p.RoleGuests) + enc.BoolKey("role_members", p.RoleMembers) +} + +func (p auditChannelModerationPatch) IsNil() bool { + return false +} + +type auditEmoji struct { + ID string + Name string +} + +// newAuditEmoji creates a simplified representation of Emoji for output to audit log. +func newAuditEmoji(e *Emoji) auditEmoji { + var emoji auditEmoji + if e != nil { + emoji.ID = e.Id + emoji.Name = e.Name + } + return emoji +} + +func (e auditEmoji) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", e.ID) + enc.StringKey("name", e.Name) +} + +func (e auditEmoji) IsNil() bool { + return false +} + +type auditFileInfo struct { + ID string + PostID string + Path string + Name string + Extension string + Size int64 +} + +// newAuditFileInfo creates a simplified representation of FileInfo for output to audit log. +func newAuditFileInfo(f *FileInfo) auditFileInfo { + var fi auditFileInfo + if f != nil { + fi.ID = f.Id + fi.PostID = f.PostId + fi.Path = f.Path + fi.Name = f.Name + fi.Extension = f.Extension + fi.Size = f.Size + } + return fi +} + +func (fi auditFileInfo) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", fi.ID) + enc.StringKey("post_id", fi.PostID) + enc.StringKey("path", fi.Path) + enc.StringKey("name", fi.Name) + enc.StringKey("ext", fi.Extension) + enc.Int64Key("size", fi.Size) +} + +func (fi auditFileInfo) IsNil() bool { + return false +} + +type auditGroup struct { + ID string + Name string + DisplayName string + Description string +} + +// newAuditGroup creates a simplified representation of Group for output to audit log. +func newAuditGroup(g *Group) auditGroup { + var group auditGroup + if g != nil { + group.ID = g.Id + if g.Name == nil { + group.Name = "" + } else { + group.Name = *g.Name + } + group.DisplayName = g.DisplayName + group.Description = g.Description + } + return group +} + +func (g auditGroup) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", g.ID) + enc.StringKey("name", g.Name) + enc.StringKey("display", g.DisplayName) + enc.StringKey("desc", g.Description) +} + +func (g auditGroup) IsNil() bool { + return false +} + +type auditJob struct { + ID string + Type string + Priority int64 + StartAt int64 +} + +// newAuditJob creates a simplified representation of Job for output to audit log. +func newAuditJob(j *Job) auditJob { + var job auditJob + if j != nil { + job.ID = j.Id + job.Type = j.Type + job.Priority = j.Priority + job.StartAt = j.StartAt + } + return job +} + +func (j auditJob) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", j.ID) + enc.StringKey("type", j.Type) + enc.Int64Key("priority", j.Priority) + enc.Int64Key("start_at", j.StartAt) +} + +func (j auditJob) IsNil() bool { + return false +} + +type auditOAuthApp struct { + ID string + CreatorID string + Name string + Description string + IsTrusted bool +} + +// newAuditOAuthApp creates a simplified representation of OAuthApp for output to audit log. +func newAuditOAuthApp(o *OAuthApp) auditOAuthApp { + var oauth auditOAuthApp + if o != nil { + oauth.ID = o.Id + oauth.CreatorID = o.CreatorId + oauth.Name = o.Name + oauth.Description = o.Description + oauth.IsTrusted = o.IsTrusted + } + return oauth +} + +func (o auditOAuthApp) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", o.ID) + enc.StringKey("creator_id", o.CreatorID) + enc.StringKey("name", o.Name) + enc.StringKey("desc", o.Description) + enc.BoolKey("trusted", o.IsTrusted) +} + +func (o auditOAuthApp) IsNil() bool { + return false +} + +type auditPost struct { + ID string + ChannelID string + Type string + IsPinned bool +} + +// newAuditPost creates a simplified representation of Post for output to audit log. +func newAuditPost(p *Post) auditPost { + var post auditPost + if p != nil { + post.ID = p.Id + post.ChannelID = p.ChannelId + post.Type = p.Type + post.IsPinned = p.IsPinned + } + return post +} + +func (p auditPost) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", p.ID) + enc.StringKey("channel_id", p.ChannelID) + enc.StringKey("type", p.Type) + enc.BoolKey("pinned", p.IsPinned) +} + +func (p auditPost) IsNil() bool { + return false +} + +type auditRole struct { + ID string + Name string + DisplayName string + Permissions []string + SchemeManaged bool + BuiltIn bool +} + +// newAuditRole creates a simplified representation of Role for output to audit log. +func newAuditRole(r *Role) auditRole { + var role auditRole + if r != nil { + role.ID = r.Id + role.Name = r.Name + role.DisplayName = r.DisplayName + role.Permissions = append(role.Permissions, r.Permissions...) + role.SchemeManaged = r.SchemeManaged + role.BuiltIn = r.BuiltIn + } + return role +} + +func (r auditRole) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", r.ID) + enc.StringKey("name", r.Name) + enc.StringKey("display", r.DisplayName) + enc.SliceStringKey("perms", r.Permissions) + enc.BoolKey("schemeManaged", r.SchemeManaged) + enc.BoolKey("builtin", r.BuiltIn) +} + +func (r auditRole) IsNil() bool { + return false +} + +type auditScheme struct { + ID string + Name string + DisplayName string + Scope string +} + +// newAuditScheme creates a simplified representation of Scheme for output to audit log. +func newAuditScheme(s *Scheme) auditScheme { + var scheme auditScheme + if s != nil { + scheme.ID = s.Id + scheme.Name = s.Name + scheme.DisplayName = s.DisplayName + scheme.Scope = s.Scope + } + return scheme +} + +func (s auditScheme) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", s.ID) + enc.StringKey("name", s.Name) + enc.StringKey("display", s.DisplayName) + enc.StringKey("scope", s.Scope) +} + +func (s auditScheme) IsNil() bool { + return false +} + +type auditSchemeRoles struct { + SchemeAdmin bool + SchemeUser bool + SchemeGuest bool +} + +// newAuditSchemeRoles creates a simplified representation of SchemeRoles for output to audit log. +func newAuditSchemeRoles(s *SchemeRoles) auditSchemeRoles { + var roles auditSchemeRoles + if s != nil { + roles.SchemeAdmin = s.SchemeAdmin + roles.SchemeUser = s.SchemeUser + roles.SchemeGuest = s.SchemeGuest + } + return roles +} + +func (s auditSchemeRoles) MarshalJSONObject(enc *gojay.Encoder) { + enc.BoolKey("admin", s.SchemeAdmin) + enc.BoolKey("user", s.SchemeUser) + enc.BoolKey("guest", s.SchemeGuest) +} + +func (s auditSchemeRoles) IsNil() bool { + return false +} + +type auditSession struct { + ID string + UserId string + DeviceId string +} + +// newAuditSession creates a simplified representation of Session for output to audit log. +func newAuditSession(s *Session) auditSession { + var session auditSession + if s != nil { + session.ID = s.Id + session.UserId = s.UserId + session.DeviceId = s.DeviceId + } + return session +} + +func (s auditSession) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", s.ID) + enc.StringKey("user_id", s.UserId) + enc.StringKey("device_id", s.DeviceId) +} + +func (s auditSession) IsNil() bool { + return false +} + +type auditIncomingWebhook struct { + ID string + ChannelID string + TeamId string + DisplayName string + Description string +} + +// newAuditIncomingWebhook creates a simplified representation of IncomingWebhook for output to audit log. +func newAuditIncomingWebhook(h *IncomingWebhook) auditIncomingWebhook { + var hook auditIncomingWebhook + if h != nil { + hook.ID = h.Id + hook.ChannelID = h.ChannelId + hook.TeamId = h.TeamId + hook.DisplayName = h.DisplayName + hook.Description = h.Description + } + return hook +} + +func (h auditIncomingWebhook) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", h.ID) + enc.StringKey("channel_id", h.ChannelID) + enc.StringKey("team_id", h.TeamId) + enc.StringKey("display", h.DisplayName) + enc.StringKey("desc", h.Description) +} + +func (h auditIncomingWebhook) IsNil() bool { + return false +} + +type auditOutgoingWebhook struct { + ID string + ChannelID string + TeamID string + TriggerWords StringArray + TriggerWhen int + DisplayName string + Description string + ContentType string + Username string +} + +// newAuditOutgoingWebhook creates a simplified representation of OutgoingWebhook for output to audit log. +func newAuditOutgoingWebhook(h *OutgoingWebhook) auditOutgoingWebhook { + var hook auditOutgoingWebhook + if h != nil { + hook.ID = h.Id + hook.ChannelID = h.ChannelId + hook.TeamID = h.TeamId + hook.TriggerWords = h.TriggerWords + hook.TriggerWhen = h.TriggerWhen + hook.DisplayName = h.DisplayName + hook.Description = h.Description + hook.ContentType = h.ContentType + hook.Username = h.Username + } + return hook +} + +func (h auditOutgoingWebhook) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("id", h.ID) + enc.StringKey("channel_id", h.ChannelID) + enc.StringKey("team_id", h.TeamID) + enc.SliceStringKey("trigger_words", h.TriggerWords) + enc.IntKey("trigger_when", h.TriggerWhen) + enc.StringKey("display", h.DisplayName) + enc.StringKey("desc", h.Description) + enc.StringKey("content_type", h.ContentType) + enc.StringKey("username", h.Username) +} + +func (h auditOutgoingWebhook) IsNil() bool { + return false +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/audits.go b/vendor/github.com/mattermost/mattermost-server/v5/model/audits.go new file mode 100644 index 00000000..a8f01e1b --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/audits.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type Audits []Audit + +func (o Audits) Etag() string { + if len(o) > 0 { + // the first in the list is always the most current + return Etag(o[0].CreateAt) + } else { + return "" + } +} + +func (o Audits) ToJson() string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func AuditsFromJson(data io.Reader) Audits { + var o Audits + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/authorize.go b/vendor/github.com/mattermost/mattermost-server/v5/model/authorize.go new file mode 100644 index 00000000..0191a670 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/authorize.go @@ -0,0 +1,142 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" +) + +const ( + AUTHCODE_EXPIRE_TIME = 60 * 10 // 10 minutes + AUTHCODE_RESPONSE_TYPE = "code" + IMPLICIT_RESPONSE_TYPE = "token" + DEFAULT_SCOPE = "user" +) + +type AuthData struct { + ClientId string `json:"client_id"` + UserId string `json:"user_id"` + Code string `json:"code"` + ExpiresIn int32 `json:"expires_in"` + CreateAt int64 `json:"create_at"` + RedirectUri string `json:"redirect_uri"` + State string `json:"state"` + Scope string `json:"scope"` +} + +type AuthorizeRequest struct { + ResponseType string `json:"response_type"` + ClientId string `json:"client_id"` + RedirectUri string `json:"redirect_uri"` + Scope string `json:"scope"` + State string `json:"state"` +} + +// IsValid validates the AuthData and returns an error if it isn't configured +// correctly. +func (ad *AuthData) IsValid() *AppError { + + if !IsValidId(ad.ClientId) { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.client_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(ad.UserId) { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(ad.Code) == 0 || len(ad.Code) > 128 { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.auth_code.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) + } + + if ad.ExpiresIn == 0 { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.expires.app_error", nil, "", http.StatusBadRequest) + } + + if ad.CreateAt <= 0 { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.create_at.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) + } + + if len(ad.RedirectUri) > 256 || !IsValidHttpUrl(ad.RedirectUri) { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) + } + + if len(ad.State) > 1024 { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) + } + + if len(ad.Scope) > 128 { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.scope.app_error", nil, "client_id="+ad.ClientId, http.StatusBadRequest) + } + + return nil +} + +// IsValid validates the AuthorizeRequest and returns an error if it isn't configured +// correctly. +func (ar *AuthorizeRequest) IsValid() *AppError { + + if !IsValidId(ar.ClientId) { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.client_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(ar.ResponseType) == 0 { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.response_type.app_error", nil, "", http.StatusBadRequest) + } + + if len(ar.RedirectUri) == 0 || len(ar.RedirectUri) > 256 || !IsValidHttpUrl(ar.RedirectUri) { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest) + } + + if len(ar.State) > 1024 { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest) + } + + if len(ar.Scope) > 128 { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.scope.app_error", nil, "client_id="+ar.ClientId, http.StatusBadRequest) + } + + return nil +} + +func (ad *AuthData) PreSave() { + if ad.ExpiresIn == 0 { + ad.ExpiresIn = AUTHCODE_EXPIRE_TIME + } + + if ad.CreateAt == 0 { + ad.CreateAt = GetMillis() + } + + if len(ad.Scope) == 0 { + ad.Scope = DEFAULT_SCOPE + } +} + +func (ad *AuthData) ToJson() string { + b, _ := json.Marshal(ad) + return string(b) +} + +func AuthDataFromJson(data io.Reader) *AuthData { + var ad *AuthData + json.NewDecoder(data).Decode(&ad) + return ad +} + +func (ar *AuthorizeRequest) ToJson() string { + b, _ := json.Marshal(ar) + return string(b) +} + +func AuthorizeRequestFromJson(data io.Reader) *AuthorizeRequest { + var ar *AuthorizeRequest + json.NewDecoder(data).Decode(&ar) + return ar +} + +func (ad *AuthData) IsExpired() bool { + return GetMillis() > ad.CreateAt+int64(ad.ExpiresIn*1000) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/bot.go b/vendor/github.com/mattermost/mattermost-server/v5/model/bot.go new file mode 100644 index 00000000..15ef6a70 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/bot.go @@ -0,0 +1,233 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "unicode/utf8" +) + +const ( + BOT_DISPLAY_NAME_MAX_RUNES = USER_FIRST_NAME_MAX_RUNES + BOT_DESCRIPTION_MAX_RUNES = 1024 + BOT_CREATOR_ID_MAX_RUNES = KEY_VALUE_PLUGIN_ID_MAX_RUNES // UserId or PluginId +) + +// Bot is a special type of User meant for programmatic interactions. +// Note that the primary key of a bot is the UserId, and matches the primary key of the +// corresponding user. +type Bot struct { + UserId string `json:"user_id"` + Username string `json:"username"` + DisplayName string `json:"display_name,omitempty"` + Description string `json:"description,omitempty"` + OwnerId string `json:"owner_id"` + LastIconUpdate int64 `json:"last_icon_update,omitempty"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` +} + +// BotPatch is a description of what fields to update on an existing bot. +type BotPatch struct { + Username *string `json:"username"` + DisplayName *string `json:"display_name"` + Description *string `json:"description"` +} + +// BotGetOptions acts as a filter on bulk bot fetching queries. +type BotGetOptions struct { + OwnerId string + IncludeDeleted bool + OnlyOrphaned bool + Page int + PerPage int +} + +// BotList is a list of bots. +type BotList []*Bot + +// Trace describes the minimum information required to identify a bot for the purpose of logging. +func (b *Bot) Trace() map[string]interface{} { + return map[string]interface{}{"user_id": b.UserId} +} + +// Clone returns a shallow copy of the bot. +func (b *Bot) Clone() *Bot { + copy := *b + return © +} + +// IsValid validates the bot and returns an error if it isn't configured correctly. +func (b *Bot) IsValid() *AppError { + if !IsValidId(b.UserId) { + return NewAppError("Bot.IsValid", "model.bot.is_valid.user_id.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if !IsValidUsername(b.Username) { + return NewAppError("Bot.IsValid", "model.bot.is_valid.username.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if utf8.RuneCountInString(b.DisplayName) > BOT_DISPLAY_NAME_MAX_RUNES { + return NewAppError("Bot.IsValid", "model.bot.is_valid.user_id.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if utf8.RuneCountInString(b.Description) > BOT_DESCRIPTION_MAX_RUNES { + return NewAppError("Bot.IsValid", "model.bot.is_valid.description.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if len(b.OwnerId) == 0 || utf8.RuneCountInString(b.OwnerId) > BOT_CREATOR_ID_MAX_RUNES { + return NewAppError("Bot.IsValid", "model.bot.is_valid.creator_id.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if b.CreateAt == 0 { + return NewAppError("Bot.IsValid", "model.bot.is_valid.create_at.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if b.UpdateAt == 0 { + return NewAppError("Bot.IsValid", "model.bot.is_valid.update_at.app_error", b.Trace(), "", http.StatusBadRequest) + } + + return nil +} + +// PreSave should be run before saving a new bot to the database. +func (b *Bot) PreSave() { + b.CreateAt = GetMillis() + b.UpdateAt = b.CreateAt + b.DeleteAt = 0 +} + +// PreUpdate should be run before saving an updated bot to the database. +func (b *Bot) PreUpdate() { + b.UpdateAt = GetMillis() +} + +// Etag generates an etag for caching. +func (b *Bot) Etag() string { + return Etag(b.UserId, b.UpdateAt) +} + +// ToJson serializes the bot to json. +func (b *Bot) ToJson() []byte { + data, _ := json.Marshal(b) + return data +} + +// BotFromJson deserializes a bot from json. +func BotFromJson(data io.Reader) *Bot { + var bot *Bot + json.NewDecoder(data).Decode(&bot) + return bot +} + +// Patch modifies an existing bot with optional fields from the given patch. +func (b *Bot) Patch(patch *BotPatch) { + if patch.Username != nil { + b.Username = *patch.Username + } + + if patch.DisplayName != nil { + b.DisplayName = *patch.DisplayName + } + + if patch.Description != nil { + b.Description = *patch.Description + } +} + +// ToJson serializes the bot patch to json. +func (b *BotPatch) ToJson() []byte { + data, err := json.Marshal(b) + if err != nil { + return nil + } + + return data +} + +// BotPatchFromJson deserializes a bot patch from json. +func BotPatchFromJson(data io.Reader) *BotPatch { + decoder := json.NewDecoder(data) + var botPatch BotPatch + err := decoder.Decode(&botPatch) + if err != nil { + return nil + } + + return &botPatch +} + +// UserFromBot returns a user model describing the bot fields stored in the User store. +func UserFromBot(b *Bot) *User { + return &User{ + Id: b.UserId, + Username: b.Username, + Email: NormalizeEmail(fmt.Sprintf("%s@localhost", b.Username)), + FirstName: b.DisplayName, + Roles: SYSTEM_USER_ROLE_ID, + } +} + +// BotFromUser returns a bot model given a user model +func BotFromUser(u *User) *Bot { + return &Bot{ + OwnerId: u.Id, + UserId: u.Id, + Username: u.Username, + DisplayName: u.GetDisplayName(SHOW_USERNAME), + } +} + +// BotListFromJson deserializes a list of bots from json. +func BotListFromJson(data io.Reader) BotList { + var bots BotList + json.NewDecoder(data).Decode(&bots) + return bots +} + +// ToJson serializes a list of bots to json. +func (l *BotList) ToJson() []byte { + b, _ := json.Marshal(l) + return b +} + +// Etag computes the etag for a list of bots. +func (l *BotList) Etag() string { + id := "0" + var t int64 = 0 + var delta int64 = 0 + + for _, v := range *l { + if v.UpdateAt > t { + t = v.UpdateAt + id = v.UserId + } + + } + + return Etag(id, t, delta, len(*l)) +} + +// MakeBotNotFoundError creates the error returned when a bot does not exist, or when the user isn't allowed to query the bot. +// The errors must the same in both cases to avoid leaking that a user is a bot. +func MakeBotNotFoundError(userId string) *AppError { + return NewAppError("SqlBotStore.Get", "store.sql_bot.get.missing.app_error", map[string]interface{}{"user_id": userId}, "", http.StatusNotFound) +} + +func IsBotDMChannel(channel *Channel, botUserID string) bool { + if channel.Type != CHANNEL_DIRECT { + return false + } + + if !strings.HasPrefix(channel.Name, botUserID+"__") && !strings.HasSuffix(channel.Name, "__"+botUserID) { + return false + } + + return true +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/bot_default_image.go b/vendor/github.com/mattermost/mattermost-server/v5/model/bot_default_image.go new file mode 100644 index 00000000..d9cdd2e2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/bot_default_image.go @@ -0,0 +1,288 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +var BotDefaultImage = []byte{ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x7e, + 0x08, 0x06, 0x00, 0x00, 0x00, 0xec, 0xa6, 0x19, 0xa2, 0x00, 0x00, 0x00, + 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, + 0x05, 0x00, 0x00, 0x0c, 0xe3, 0x49, 0x44, 0x41, 0x54, 0x78, 0x01, 0xed, + 0x5d, 0x5b, 0x88, 0x15, 0xc9, 0x19, 0xae, 0xf1, 0xba, 0x5e, 0xc6, 0x4b, + 0xbc, 0xa0, 0x46, 0xd7, 0x2b, 0xce, 0x2a, 0xba, 0xca, 0x8a, 0xa3, 0x82, + 0x38, 0xa3, 0x46, 0x47, 0x5d, 0x59, 0xa3, 0x2f, 0x9b, 0x28, 0x6e, 0xc2, + 0xea, 0x83, 0x22, 0x04, 0x82, 0x78, 0x41, 0xf2, 0xb2, 0x04, 0xd4, 0x07, + 0x49, 0x22, 0xc4, 0x07, 0xcd, 0x53, 0x30, 0x26, 0x1a, 0xa2, 0x08, 0xee, + 0xaa, 0xa8, 0x71, 0xb2, 0x41, 0x11, 0xbc, 0xdf, 0xd0, 0x8d, 0x6e, 0x24, + 0x82, 0xe3, 0x65, 0x32, 0x9b, 0x19, 0x1d, 0x47, 0xd7, 0xeb, 0xc9, 0xf7, + 0xb5, 0xa7, 0x0f, 0xe7, 0x9c, 0xe9, 0x3e, 0xdd, 0xe7, 0x4c, 0x57, 0x77, + 0x75, 0x75, 0xfd, 0xf0, 0x9f, 0xee, 0xae, 0xaa, 0xae, 0xfa, 0xff, 0xef, + 0xfb, 0x4f, 0x9d, 0xee, 0xaa, 0xea, 0x3e, 0x65, 0x42, 0x4f, 0xe9, 0x0b, + 0xb7, 0x46, 0x43, 0x2b, 0xd2, 0xdb, 0x91, 0xd8, 0xf6, 0x84, 0x96, 0x67, + 0x69, 0xf7, 0xf4, 0x3e, 0x36, 0xa2, 0x19, 0xfa, 0x34, 0xbd, 0xe5, 0x3e, + 0xf5, 0x31, 0xf4, 0xdf, 0xd0, 0x5b, 0xd0, 0x7f, 0xa5, 0xb7, 0x0d, 0xd8, + 0x6a, 0x25, 0x65, 0x1a, 0x78, 0xd3, 0x1b, 0x3e, 0x54, 0x41, 0x67, 0x42, + 0x2b, 0xa1, 0x24, 0x9d, 0x69, 0x32, 0xa4, 0x11, 0x95, 0x32, 0x18, 0xce, + 0x42, 0x6b, 0xa1, 0x5f, 0x43, 0x99, 0x16, 0x5b, 0x89, 0x63, 0x00, 0x74, + 0x03, 0xda, 0xb3, 0xa0, 0x24, 0xbc, 0x1a, 0x3a, 0x01, 0xda, 0x0e, 0x1a, + 0x85, 0xbc, 0x45, 0xa3, 0x57, 0xa0, 0xff, 0x80, 0x32, 0x20, 0x4e, 0x42, + 0x5b, 0xa0, 0x46, 0x02, 0x46, 0xa0, 0x3d, 0xea, 0xab, 0x81, 0xee, 0x86, + 0xb2, 0xab, 0x4e, 0x29, 0xaa, 0xb4, 0x8d, 0x36, 0xd2, 0x56, 0xda, 0x6c, + 0xa4, 0x8d, 0x08, 0x7c, 0x84, 0xf3, 0x7f, 0x03, 0x7d, 0x00, 0x55, 0x95, + 0x74, 0x37, 0xbb, 0x68, 0x33, 0x6d, 0xa7, 0x0f, 0x46, 0x8a, 0x44, 0xe0, + 0x63, 0x94, 0x3f, 0x0d, 0x75, 0x03, 0x37, 0x6e, 0xe9, 0xf4, 0x85, 0x3e, + 0x19, 0x29, 0x80, 0x00, 0xbb, 0xcc, 0x9f, 0x40, 0xf9, 0x9b, 0x1a, 0x37, + 0x82, 0xfd, 0xda, 0x4b, 0xdf, 0xe8, 0xa3, 0xf9, 0x79, 0x00, 0x08, 0xb6, + 0xf0, 0x02, 0xee, 0x73, 0xe8, 0xb7, 0x50, 0xbf, 0x40, 0xc6, 0xbd, 0x1c, + 0x7d, 0xa5, 0xcf, 0x51, 0x5d, 0xbc, 0xa2, 0x69, 0x35, 0x84, 0xb7, 0x6d, + 0xe7, 0xa0, 0x71, 0x27, 0xb4, 0x54, 0xfb, 0xe9, 0x3b, 0x31, 0x48, 0x9c, + 0x70, 0xa0, 0x66, 0x17, 0x94, 0xb7, 0x51, 0xa5, 0x82, 0xa7, 0xcb, 0x79, + 0xc4, 0x80, 0x58, 0x10, 0x13, 0xed, 0x85, 0xe3, 0x0e, 0xab, 0xa0, 0xdf, + 0x41, 0x75, 0x21, 0x30, 0x28, 0x3f, 0x88, 0x09, 0xb1, 0x89, 0xe3, 0xd8, + 0x0c, 0xcc, 0xf6, 0x16, 0x46, 0xf8, 0x57, 0xd0, 0xa0, 0x00, 0xd3, 0xb5, + 0x1e, 0x62, 0xa4, 0x5d, 0x6f, 0x50, 0x0d, 0xa7, 0xea, 0x0c, 0xf9, 0xbe, + 0x83, 0x9f, 0x58, 0x11, 0xb3, 0xd8, 0x0b, 0x6f, 0x77, 0xbe, 0x80, 0xbe, + 0x81, 0xea, 0xfa, 0x8d, 0x95, 0xe5, 0x17, 0x31, 0x23, 0x76, 0xb1, 0xbd, + 0x65, 0xe4, 0x84, 0x4c, 0xad, 0x21, 0xbe, 0xcd, 0x81, 0x4f, 0x0c, 0x89, + 0x65, 0xac, 0xe4, 0x7d, 0x58, 0x7b, 0x03, 0x2a, 0xeb, 0xdb, 0x91, 0xb4, + 0x7a, 0x89, 0x25, 0x31, 0x8d, 0x85, 0x8c, 0x87, 0x95, 0xf7, 0xa0, 0x49, + 0x23, 0x49, 0xb6, 0xbf, 0xc4, 0x94, 0xd8, 0x2a, 0x2d, 0xd5, 0xb0, 0xae, + 0x09, 0x2a, 0x1b, 0x8c, 0xa4, 0xd6, 0x4f, 0x6c, 0x89, 0xb1, 0x92, 0xf2, + 0x09, 0xac, 0xfa, 0x1e, 0x9a, 0x54, 0x72, 0xc2, 0xf2, 0x9b, 0x18, 0x13, + 0x6b, 0xa5, 0xa4, 0x1a, 0xd6, 0x18, 0xf2, 0xc3, 0x0b, 0x7e, 0x62, 0x4d, + 0xcc, 0x95, 0x90, 0x49, 0xb0, 0xe2, 0x09, 0x34, 0xac, 0x6f, 0x80, 0x69, + 0xe7, 0x1d, 0xd6, 0xc4, 0x9c, 0xd8, 0x47, 0x2a, 0x5c, 0x78, 0x59, 0x0f, + 0x35, 0xa4, 0x44, 0x83, 0x01, 0xb1, 0x27, 0x07, 0x91, 0xc8, 0x60, 0xb4, + 0x7a, 0x17, 0x6a, 0xc8, 0x8f, 0x16, 0x03, 0x72, 0x40, 0x2e, 0x42, 0x95, + 0x4e, 0x68, 0x2d, 0xc9, 0xd3, 0xb8, 0xaa, 0x05, 0x3d, 0xb9, 0x20, 0x27, + 0x45, 0x4b, 0xa9, 0xc3, 0x8c, 0xdb, 0xd1, 0xd2, 0x8f, 0x8b, 0x6e, 0xcd, + 0x9c, 0x20, 0x0b, 0x81, 0x41, 0xa8, 0xf8, 0x07, 0xd0, 0xc3, 0xb2, 0x1a, + 0xc8, 0xae, 0xf7, 0x53, 0x1c, 0xa8, 0xf6, 0x0d, 0x30, 0xf6, 0xbc, 0xe3, + 0x84, 0xdc, 0x48, 0x15, 0x5e, 0x70, 0x98, 0x2b, 0x7e, 0x75, 0xbf, 0x00, + 0xe4, 0xa6, 0xa8, 0x8b, 0xc2, 0x62, 0xd6, 0xa4, 0xbd, 0x87, 0xca, 0xff, + 0x06, 0x2d, 0x87, 0x1a, 0x51, 0x13, 0x01, 0x72, 0x43, 0x8e, 0xc8, 0x95, + 0x2f, 0x29, 0x26, 0x00, 0x7e, 0x85, 0x1a, 0x95, 0x1f, 0x8b, 0xf6, 0xe3, + 0xf5, 0x80, 0x01, 0x03, 0xc4, 0xee, 0xdd, 0xbb, 0x45, 0x5d, 0x5d, 0x9d, + 0xa5, 0xdc, 0x67, 0x9a, 0x26, 0x42, 0x8e, 0xc8, 0x55, 0xa0, 0xf2, 0x01, + 0x6a, 0x7b, 0x01, 0x8d, 0xfd, 0x6f, 0x6d, 0xbf, 0x7e, 0xfd, 0x52, 0x0f, + 0x1f, 0x3e, 0x4c, 0xe5, 0x0b, 0xd3, 0x98, 0xa7, 0x83, 0x8f, 0x69, 0xae, + 0xc8, 0x59, 0x60, 0x52, 0x8b, 0x9a, 0xb4, 0x00, 0x67, 0xd7, 0xae, 0x5d, + 0xf9, 0xdc, 0x67, 0x8e, 0x99, 0xa7, 0x8b, 0x9f, 0xf0, 0x83, 0x9c, 0x05, + 0x22, 0x9f, 0xa1, 0x16, 0x6d, 0x80, 0xb9, 0x7d, 0xfb, 0x76, 0x86, 0xf0, + 0xfc, 0x1d, 0xe6, 0xe9, 0xe4, 0x2b, 0x7c, 0x21, 0x77, 0x05, 0xa5, 0xac, + 0x60, 0xee, 0xbb, 0x95, 0x28, 0x7c, 0x1c, 0xba, 0x9f, 0x47, 0xb9, 0xd8, + 0x64, 0xbf, 0x78, 0xf1, 0x42, 0x74, 0xea, 0xe4, 0x3c, 0x66, 0xf2, 0xf2, + 0xe5, 0x4b, 0xd1, 0xb9, 0x73, 0xe7, 0xd8, 0xf8, 0xe2, 0xc3, 0xd0, 0xff, + 0xa2, 0x4c, 0x05, 0xd4, 0xf5, 0x11, 0x76, 0xaf, 0x8b, 0xc0, 0x8d, 0x38, + 0x59, 0x1b, 0xf2, 0x7d, 0x00, 0xa6, 0x5b, 0x11, 0x72, 0x47, 0x0e, 0x5d, + 0xa5, 0x50, 0x0f, 0xc0, 0x91, 0x25, 0x8e, 0x33, 0xf3, 0x4d, 0x1a, 0xda, + 0x48, 0xc2, 0x7a, 0x00, 0xf2, 0xf6, 0x14, 0x3a, 0x14, 0xfa, 0x3f, 0x1e, + 0xe4, 0x4b, 0xa1, 0x1e, 0xe0, 0x97, 0x28, 0xac, 0x15, 0xf9, 0xf9, 0xce, + 0x27, 0xe4, 0x98, 0x1c, 0x92, 0x4b, 0x47, 0x71, 0xeb, 0x01, 0x7a, 0xa1, + 0xf4, 0x7f, 0xa0, 0x3d, 0x1d, 0xcf, 0x8a, 0x71, 0x62, 0x02, 0x7b, 0x00, + 0xb2, 0xf5, 0x18, 0x3a, 0x0c, 0xca, 0x25, 0x65, 0x39, 0xe2, 0xd6, 0x03, + 0xfc, 0x02, 0xa5, 0xb4, 0x23, 0x3f, 0xc7, 0xf3, 0x64, 0x1d, 0x90, 0x4b, + 0x72, 0xda, 0x4a, 0x9c, 0x7a, 0x80, 0x2e, 0x28, 0xc5, 0x15, 0xa8, 0xbc, + 0x06, 0xd0, 0x4e, 0x12, 0xda, 0x03, 0x90, 0x47, 0x5e, 0x03, 0x70, 0xdd, + 0xc0, 0x73, 0x1e, 0xd8, 0xe2, 0xd4, 0x03, 0x70, 0x9a, 0x57, 0x4b, 0xf2, + 0x6d, 0xa7, 0x13, 0xba, 0x25, 0xa7, 0xad, 0xa6, 0xf0, 0x3b, 0x38, 0x80, + 0xf1, 0x73, 0x87, 0xb4, 0xc8, 0x93, 0x78, 0x7f, 0x3e, 0x67, 0xce, 0x1c, + 0x31, 0x7d, 0xfa, 0x74, 0x31, 0x68, 0xd0, 0x20, 0xd1, 0xbb, 0x77, 0xef, + 0x92, 0x6c, 0xea, 0xd8, 0xb1, 0xa3, 0xeb, 0x79, 0xcc, 0x3b, 0x74, 0xe8, + 0x90, 0x6b, 0x7e, 0xa1, 0x8c, 0xc6, 0xc6, 0x46, 0x6b, 0x5e, 0xe1, 0xf4, + 0xe9, 0xd3, 0xe2, 0xf8, 0xf1, 0xe3, 0x82, 0x3d, 0x8d, 0x82, 0xf2, 0x33, + 0xd8, 0xf4, 0x97, 0x42, 0x76, 0xfd, 0x10, 0x99, 0x4a, 0x3d, 0xc7, 0xd7, + 0xa5, 0x4b, 0x97, 0xd4, 0xa6, 0x4d, 0x9b, 0x52, 0x4d, 0x4d, 0x4d, 0xf9, + 0x03, 0x77, 0xca, 0x1e, 0xd3, 0x56, 0xda, 0x4c, 0xdb, 0x81, 0xa7, 0x4a, + 0x4a, 0x6e, 0xc9, 0xb1, 0xab, 0x6c, 0x40, 0x8e, 0x32, 0x06, 0x8f, 0x18, + 0x31, 0x22, 0x75, 0xe3, 0xc6, 0x0d, 0x65, 0x89, 0xf6, 0x32, 0x8c, 0xb6, + 0xd3, 0x07, 0x95, 0x30, 0x85, 0x2d, 0xe4, 0xd8, 0x55, 0x94, 0x79, 0x9e, + 0x6f, 0xd4, 0xa8, 0x51, 0xa9, 0xfa, 0xfa, 0x7a, 0x2f, 0x8c, 0x95, 0xcf, + 0xa7, 0x0f, 0xf4, 0x05, 0x88, 0xab, 0xa2, 0xe4, 0xd8, 0x51, 0x3e, 0x44, + 0xaa, 0x12, 0x46, 0x76, 0xed, 0xda, 0x35, 0x75, 0xed, 0xda, 0x35, 0xe5, + 0xc9, 0xf5, 0x6b, 0x20, 0x7d, 0xa1, 0x4f, 0xaa, 0xe0, 0x0b, 0x3b, 0xc8, + 0xb5, 0x25, 0xd9, 0x77, 0x01, 0x3f, 0xb2, 0x13, 0xa3, 0xde, 0xae, 0x5d, + 0xbb, 0x56, 0x8c, 0x1b, 0x37, 0x2e, 0x6a, 0x33, 0x02, 0x6b, 0x9f, 0xbe, + 0xd0, 0x27, 0x85, 0xc4, 0x91, 0x6b, 0xbe, 0x9a, 0x24, 0xf2, 0x28, 0x2d, + 0x2f, 0x2f, 0x8f, 0xd5, 0x05, 0x9f, 0xdf, 0x5e, 0x80, 0x17, 0x86, 0xf4, + 0x4d, 0x05, 0x8c, 0x61, 0xc3, 0x97, 0x76, 0x30, 0xda, 0x3d, 0x00, 0x6f, + 0x07, 0x67, 0xd8, 0x89, 0x51, 0x6e, 0x6b, 0x6a, 0x6a, 0x44, 0xcf, 0x9e, + 0xfa, 0x0d, 0x42, 0xd2, 0x27, 0xfa, 0xa6, 0x88, 0x54, 0xc1, 0x0e, 0x6b, + 0x08, 0xc0, 0x0e, 0x80, 0x4a, 0x24, 0x28, 0x31, 0xf1, 0x33, 0x77, 0xee, + 0x5c, 0x45, 0x30, 0x0a, 0xde, 0x0c, 0x85, 0x7c, 0x23, 0xd7, 0xe4, 0x3c, + 0xf3, 0xa6, 0xca, 0xd9, 0xc1, 0xbb, 0x5b, 0x5a, 0x8d, 0x43, 0x87, 0x0e, + 0x2d, 0xed, 0xc4, 0x18, 0x9c, 0xa5, 0x98, 0x6f, 0x16, 0xe7, 0x76, 0x0f, + 0x30, 0x5d, 0x15, 0xfc, 0xfa, 0xf7, 0xef, 0xaf, 0x8a, 0x29, 0x81, 0xdb, + 0xa1, 0x98, 0x6f, 0x16, 0xe7, 0x76, 0x00, 0x8c, 0x09, 0xdc, 0xdb, 0x12, + 0x2b, 0xec, 0xd0, 0xc1, 0xfa, 0x69, 0x2a, 0xf1, 0x6c, 0xb5, 0x4f, 0x53, + 0xcc, 0x37, 0x8b, 0x73, 0x06, 0x40, 0x37, 0x28, 0x67, 0x89, 0x8c, 0x24, + 0x0b, 0x01, 0x72, 0xde, 0x8d, 0x01, 0xc0, 0x45, 0x83, 0x4e, 0xd3, 0xc2, + 0xc9, 0x82, 0x23, 0x79, 0xde, 0x92, 0xf3, 0x0a, 0x3b, 0x00, 0x92, 0xe7, + 0xbe, 0xf1, 0x98, 0x08, 0x58, 0x01, 0x10, 0xe8, 0x13, 0x24, 0x51, 0xe0, + 0x7a, 0xe2, 0xc4, 0x09, 0xb1, 0x62, 0xc5, 0x0a, 0x31, 0x71, 0xe2, 0x44, + 0x31, 0x73, 0xe6, 0x4c, 0xb1, 0x7e, 0xfd, 0x7a, 0x71, 0xff, 0xfe, 0xfd, + 0xc0, 0x4c, 0x39, 0x7b, 0xf6, 0xac, 0x58, 0xb9, 0x72, 0xa5, 0x98, 0x30, + 0x61, 0x82, 0x98, 0x3f, 0x7f, 0xbe, 0xd8, 0xb6, 0x6d, 0x9b, 0x78, 0xf5, + 0xea, 0x55, 0x60, 0xf5, 0x47, 0x58, 0x91, 0xc5, 0xfd, 0x9f, 0x61, 0x40, + 0x28, 0x23, 0x54, 0x3d, 0x7a, 0xf4, 0xb0, 0x66, 0xc7, 0x38, 0x43, 0x46, + 0x1d, 0x3c, 0x78, 0x70, 0xab, 0x76, 0x8b, 0x99, 0x03, 0x78, 0xfb, 0xf6, + 0x6d, 0x6a, 0xf5, 0xea, 0xd5, 0xad, 0xea, 0xa0, 0x3f, 0x58, 0x2f, 0x90, + 0x3a, 0x78, 0xf0, 0xa0, 0xdf, 0x81, 0x3a, 0xd7, 0x72, 0x3b, 0x76, 0xec, + 0x48, 0x61, 0x9d, 0x40, 0xab, 0x36, 0x2a, 0x2b, 0x2b, 0x8b, 0x9e, 0xac, + 0xa2, 0x6f, 0xf9, 0x58, 0x13, 0x03, 0x1b, 0x0f, 0x6e, 0x89, 0x51, 0x7e, + 0x19, 0x89, 0xc7, 0xe4, 0x5e, 0x1c, 0x91, 0xd8, 0x80, 0xe5, 0xcc, 0xe4, + 0xc9, 0x93, 0x53, 0x17, 0x2e, 0x5c, 0x48, 0x91, 0xb0, 0x6c, 0x71, 0x02, + 0xa4, 0x98, 0x00, 0xd8, 0xbe, 0x7d, 0x7b, 0x41, 0xb0, 0xba, 0x77, 0xef, + 0x9e, 0xba, 0x73, 0xe7, 0x4e, 0x76, 0x93, 0x45, 0xed, 0x5f, 0xb9, 0x72, + 0x25, 0x85, 0x2b, 0x77, 0xd7, 0x36, 0x96, 0x2d, 0x5b, 0x56, 0x54, 0x7d, + 0x7e, 0xfc, 0x25, 0x46, 0xc4, 0x8a, 0x98, 0xc9, 0xe6, 0x25, 0xcd, 0xbd, + 0x38, 0x25, 0xb3, 0xa1, 0xb1, 0x63, 0xc7, 0xa6, 0xb0, 0x3a, 0xc6, 0x11, + 0x28, 0x3f, 0x80, 0x38, 0x9e, 0x88, 0xc4, 0xe7, 0xcf, 0x9f, 0xfb, 0x9a, + 0x61, 0x5b, 0xba, 0x74, 0xa9, 0x5b, 0x15, 0x9e, 0xe9, 0x4b, 0x96, 0x2c, + 0xf1, 0x24, 0xe1, 0xe6, 0xcd, 0x9b, 0x9e, 0xf5, 0xd8, 0x05, 0x8a, 0xf1, + 0x97, 0x98, 0x11, 0x3b, 0x99, 0xdc, 0xa0, 0xee, 0x53, 0xbc, 0x08, 0xec, + 0x01, 0x95, 0x26, 0x5b, 0xb6, 0x6c, 0x71, 0x7d, 0x14, 0xab, 0x2d, 0x8d, + 0xe2, 0xdb, 0x29, 0x9e, 0x3d, 0x7b, 0xe6, 0x59, 0xc5, 0x99, 0x33, 0x67, + 0x3c, 0xcb, 0xb8, 0x15, 0x38, 0x77, 0x8e, 0xaf, 0xde, 0x29, 0x2c, 0xe7, + 0xcf, 0x9f, 0x2f, 0x5c, 0xa0, 0xc4, 0x5c, 0x3e, 0xbe, 0x46, 0xec, 0x24, + 0x4b, 0x0f, 0x06, 0x80, 0xd4, 0x17, 0x3e, 0x4c, 0x9d, 0x3a, 0x55, 0x8a, + 0x0f, 0x77, 0xef, 0xde, 0xf5, 0x55, 0xef, 0xbd, 0x7b, 0xf7, 0x04, 0xba, + 0x55, 0x5f, 0x65, 0xb3, 0x0b, 0xf1, 0x9c, 0x07, 0x0f, 0x1e, 0x64, 0x27, + 0x39, 0xee, 0xb3, 0x7e, 0x59, 0x22, 0x0b, 0xbb, 0x2c, 0x7b, 0xcb, 0xa5, + 0x07, 0x80, 0xac, 0xe1, 0xcf, 0xf1, 0xe3, 0xfd, 0xbd, 0xab, 0x02, 0xdd, + 0xa8, 0x68, 0xd7, 0x8e, 0x6e, 0x16, 0x27, 0x3c, 0x67, 0xf4, 0xe8, 0xd1, + 0x9e, 0x27, 0x8d, 0x19, 0x23, 0x6f, 0x10, 0x55, 0x16, 0x76, 0x59, 0x4e, + 0xc9, 0x0f, 0x80, 0xac, 0xc6, 0x02, 0xdd, 0xad, 0xa8, 0xa8, 0x10, 0xc3, + 0x87, 0x0f, 0xf7, 0xac, 0x73, 0xde, 0xbc, 0x79, 0x9e, 0x65, 0xdc, 0x0a, + 0xcc, 0x9a, 0x35, 0xcb, 0x2d, 0xcb, 0x4a, 0xc7, 0xa2, 0x4f, 0x11, 0xc2, + 0xb7, 0xb4, 0xa0, 0x0d, 0x6d, 0xcc, 0xb4, 0x7a, 0x7f, 0xa9, 0x6f, 0xfe, + 0xb0, 0x2f, 0x80, 0x9c, 0xb6, 0xc5, 0x5c, 0x14, 0x39, 0x9d, 0x7f, 0xf2, + 0xe4, 0xc9, 0x54, 0x59, 0x59, 0x99, 0xeb, 0x85, 0x12, 0xd7, 0xe2, 0xb5, + 0xb4, 0xb4, 0x38, 0x9d, 0xea, 0x2b, 0xad, 0xa1, 0xa1, 0x21, 0xd5, 0xb7, + 0x6f, 0x5f, 0xd7, 0xfa, 0x37, 0x6f, 0xde, 0xec, 0xab, 0x1e, 0xbb, 0x50, + 0x29, 0xfe, 0x82, 0x60, 0xd7, 0xf6, 0x03, 0xc8, 0x7b, 0xc1, 0xbe, 0xb1, + 0xb9, 0x8d, 0x51, 0x14, 0xd9, 0xe9, 0x1c, 0xf4, 0xd9, 0xbf, 0x7f, 0xbf, + 0xc0, 0xab, 0x5d, 0x5a, 0xd9, 0x30, 0x63, 0xc6, 0x0c, 0x71, 0xec, 0xd8, + 0x31, 0x81, 0xb5, 0x78, 0xad, 0xf2, 0xfc, 0x26, 0xf4, 0xe9, 0xd3, 0x47, + 0x1c, 0x3d, 0x7a, 0x54, 0x8c, 0x1c, 0x39, 0x32, 0xe7, 0x14, 0x04, 0x9d, + 0xd8, 0xb0, 0x61, 0x83, 0x58, 0xb7, 0x6e, 0x5d, 0x4e, 0x7a, 0x0c, 0x0f, + 0x9a, 0x39, 0xf5, 0xc6, 0x00, 0xe8, 0x13, 0x43, 0xe3, 0x2d, 0x93, 0x17, + 0x2f, 0x5e, 0x2c, 0xaa, 0xaa, 0xaa, 0xac, 0x87, 0x31, 0x2e, 0x5e, 0xbc, + 0x28, 0x7a, 0xf5, 0xea, 0x25, 0x70, 0x0f, 0x2d, 0x66, 0xcf, 0x9e, 0x2d, + 0x48, 0x54, 0x5b, 0x65, 0xd2, 0xa4, 0x49, 0xe2, 0xd2, 0xa5, 0x4b, 0xe2, + 0xc8, 0x91, 0x23, 0x82, 0x57, 0xfc, 0x03, 0x07, 0x0e, 0x14, 0xfc, 0x69, + 0xe0, 0xa8, 0xa0, 0x06, 0x62, 0x7d, 0xf9, 0xaf, 0xc2, 0x11, 0x69, 0xdd, + 0x8c, 0xdd, 0xfd, 0x39, 0x6d, 0x4b, 0xe9, 0x12, 0x9d, 0xea, 0x89, 0x4b, + 0x5a, 0x29, 0xfe, 0xca, 0xe4, 0x06, 0x75, 0x5f, 0xe5, 0x4f, 0xc0, 0x13, + 0xa8, 0x91, 0x64, 0x22, 0xf0, 0x24, 0xd6, 0xd7, 0x00, 0xc9, 0xe4, 0x2c, + 0x50, 0xaf, 0x9b, 0x19, 0x00, 0x8d, 0x81, 0x56, 0x69, 0x2a, 0x8b, 0x13, + 0x02, 0x8d, 0x0c, 0x80, 0xdb, 0x71, 0xb2, 0xd8, 0xd8, 0x1a, 0x28, 0x02, + 0xb7, 0x19, 0x00, 0xdf, 0x04, 0x5a, 0xa5, 0xa9, 0x2c, 0x4e, 0x08, 0x7c, + 0xc3, 0x00, 0xe0, 0x7b, 0x00, 0x8d, 0x24, 0x13, 0x81, 0x4c, 0x00, 0xf0, + 0x36, 0xd0, 0x48, 0xb2, 0x10, 0x20, 0xe7, 0xb7, 0xd8, 0x03, 0xb4, 0x40, + 0xe5, 0x4d, 0x69, 0x25, 0x0b, 0xd4, 0x38, 0x79, 0x4b, 0xce, 0x5b, 0x18, + 0x00, 0x94, 0x9b, 0xef, 0x36, 0xe6, 0x33, 0x41, 0x08, 0x58, 0x9c, 0x73, + 0x28, 0x98, 0x72, 0x0a, 0x3a, 0xd7, 0xda, 0x8b, 0xd9, 0xc7, 0x9b, 0x37, + 0x6f, 0x04, 0x46, 0x02, 0x5d, 0xad, 0xce, 0x7f, 0x18, 0x83, 0xf3, 0xfc, + 0x85, 0xd6, 0x07, 0xb4, 0x6f, 0xdf, 0x3e, 0x67, 0x08, 0xf9, 0xf5, 0xeb, + 0xd7, 0xae, 0x75, 0x73, 0xa8, 0x99, 0xe5, 0x63, 0x2a, 0xe4, 0x3c, 0xf3, + 0x6c, 0xe0, 0xdf, 0x65, 0x39, 0xd1, 0xdc, 0x2c, 0x77, 0xae, 0x69, 0xf9, + 0xf2, 0xe5, 0x82, 0x2f, 0x77, 0x72, 0xd3, 0xeb, 0xd7, 0xaf, 0xe7, 0xb8, + 0xb6, 0x75, 0xeb, 0x56, 0xd7, 0xb2, 0xac, 0x63, 0xdf, 0xbe, 0x7d, 0x99, + 0xf2, 0x3c, 0xd7, 0xad, 0x5e, 0xa6, 0xb3, 0x6d, 0x99, 0x22, 0x19, 0x3b, + 0x8b, 0x73, 0xfb, 0x27, 0xe0, 0x2c, 0x1c, 0x91, 0xc2, 0xd4, 0xe5, 0xcb, + 0x97, 0x65, 0x62, 0xa4, 0x75, 0xdd, 0x12, 0xb1, 0x23, 0xd7, 0xe4, 0x3c, + 0xd3, 0x03, 0xb0, 0x9f, 0xfb, 0x27, 0x13, 0x82, 0x16, 0xcc, 0x99, 0x07, + 0x5d, 0xa5, 0xd4, 0xfa, 0x0a, 0xfd, 0x9c, 0x48, 0x6d, 0xd8, 0xa1, 0x72, + 0x89, 0xd8, 0x91, 0x6b, 0xeb, 0xb7, 0xcd, 0xee, 0x01, 0xd8, 0xbc, 0x94, + 0x9f, 0x01, 0xce, 0xa7, 0xaf, 0x5a, 0xb5, 0xca, 0xd7, 0x02, 0x4e, 0x07, + 0x0c, 0x42, 0x4f, 0x0a, 0x62, 0x0a, 0xb9, 0xad, 0x46, 0x73, 0xb1, 0x2b, + 0x31, 0x23, 0x76, 0x92, 0x24, 0xc3, 0xb5, 0x7d, 0x11, 0xc8, 0x76, 0x32, + 0x89, 0x41, 0x37, 0xba, 0x73, 0xe7, 0x4e, 0xb1, 0x77, 0xef, 0x5e, 0x6b, + 0x9e, 0x3e, 0x7b, 0x9d, 0xdb, 0xe3, 0xc7, 0x7c, 0x87, 0x71, 0xdb, 0x64, + 0xcd, 0x9a, 0x35, 0x62, 0xe1, 0xc2, 0x85, 0xae, 0x95, 0x0c, 0x19, 0x32, + 0x24, 0x27, 0x6f, 0xd1, 0xa2, 0x45, 0x62, 0xd8, 0xb0, 0x61, 0x39, 0x69, + 0xd9, 0x07, 0xd3, 0xa6, 0x4d, 0xcb, 0x1c, 0xf2, 0xdc, 0x3d, 0x7b, 0xf6, + 0x64, 0x8e, 0xf3, 0x77, 0x82, 0x78, 0xde, 0x7f, 0xe3, 0xc6, 0x8d, 0x39, + 0x6f, 0x44, 0xc1, 0x5b, 0xc5, 0x04, 0x57, 0x23, 0x07, 0x81, 0x4d, 0xbe, + 0xbd, 0x59, 0xc7, 0xae, 0x5c, 0xdf, 0x40, 0x21, 0x69, 0x6b, 0x03, 0xfc, + 0xd4, 0xcd, 0x39, 0x73, 0x5d, 0xc5, 0x69, 0x3d, 0x80, 0x1f, 0x4c, 0x02, + 0x2e, 0x43, 0x8e, 0x33, 0x92, 0xfd, 0x13, 0xc0, 0xc4, 0x3f, 0x66, 0x72, + 0xcc, 0x8e, 0xae, 0x08, 0xe4, 0x70, 0x9c, 0x1f, 0x00, 0x7f, 0x82, 0xd7, + 0xc5, 0x2f, 0xa2, 0xd7, 0x15, 0x2a, 0xfd, 0xfc, 0x22, 0xb7, 0xe4, 0x38, + 0x23, 0xf9, 0x01, 0x50, 0x87, 0x9c, 0xe3, 0x99, 0xdc, 0x08, 0x76, 0x0a, + 0x0d, 0xbc, 0x44, 0x60, 0x4e, 0xa0, 0x4d, 0x2a, 0xf0, 0x44, 0x31, 0xb9, + 0x25, 0xc7, 0x19, 0xc9, 0x0f, 0x00, 0x66, 0xe4, 0x74, 0x11, 0x99, 0x92, + 0x21, 0xed, 0xe0, 0x0f, 0x1c, 0x43, 0x6a, 0x29, 0xfc, 0x66, 0x1e, 0x3d, + 0x7a, 0x14, 0x7e, 0xa3, 0xb9, 0x2d, 0xb6, 0xe2, 0xd6, 0x29, 0x00, 0x0e, + 0xe2, 0x1c, 0xfe, 0xb9, 0x40, 0x24, 0x22, 0x71, 0xf0, 0x23, 0x12, 0x7f, + 0xb2, 0x1b, 0x8d, 0xd8, 0x37, 0x72, 0x4a, 0x6e, 0x73, 0xc4, 0x29, 0x00, + 0x9e, 0xa3, 0xc4, 0xef, 0x72, 0x4a, 0x85, 0x78, 0x70, 0xe0, 0xc0, 0x81, + 0x10, 0x5b, 0x0b, 0xb7, 0xa9, 0x88, 0x7d, 0x23, 0xa7, 0xe4, 0xd6, 0x97, + 0xf4, 0x42, 0xa9, 0x26, 0x68, 0x24, 0xb7, 0x84, 0x18, 0x00, 0xd1, 0xee, + 0x4e, 0x90, 0x3e, 0x45, 0x85, 0x67, 0x9a, 0x4b, 0x72, 0x5a, 0x94, 0xfc, + 0x1a, 0xa5, 0x23, 0x31, 0x1a, 0x03, 0x35, 0x29, 0x3e, 0x96, 0xa5, 0x8b, + 0xd0, 0x17, 0xfa, 0x14, 0x15, 0x9e, 0x68, 0x97, 0x5c, 0x16, 0x2d, 0xfc, + 0x8f, 0x19, 0x4e, 0x1a, 0x44, 0x62, 0xf8, 0x94, 0x29, 0x53, 0x8a, 0x7e, + 0x05, 0x8b, 0x8a, 0x01, 0xc3, 0xff, 0x0b, 0xa0, 0x2f, 0x51, 0xe1, 0x98, + 0xe6, 0x90, 0x5c, 0x96, 0x24, 0x5b, 0x71, 0x56, 0x64, 0xc6, 0x63, 0x28, + 0x36, 0x85, 0x21, 0xe4, 0x56, 0xaf, 0x96, 0x51, 0x91, 0xe8, 0x7c, 0x9b, + 0xb0, 0xe6, 0xc0, 0xb2, 0x9d, 0x3e, 0x44, 0x89, 0x21, 0xda, 0x26, 0x87, + 0xae, 0x52, 0xe6, 0x9a, 0xf3, 0x2e, 0x83, 0xff, 0xcc, 0xc4, 0x45, 0xa3, + 0xad, 0x9f, 0xbe, 0xf4, 0x38, 0x31, 0xc8, 0x6c, 0xbc, 0x48, 0x49, 0x2c, + 0x58, 0xb0, 0x40, 0xe0, 0x25, 0x4a, 0x25, 0xff, 0x59, 0x54, 0x90, 0xf6, + 0x14, 0xaa, 0x8b, 0x7f, 0x1e, 0x85, 0xf7, 0x12, 0x89, 0xc3, 0x87, 0x0f, + 0x0b, 0x99, 0x2f, 0x8f, 0x28, 0x64, 0x43, 0x56, 0x9e, 0xe7, 0x9f, 0x47, + 0x67, 0x95, 0x75, 0xdd, 0xfd, 0x0c, 0x39, 0x51, 0x47, 0xb1, 0x69, 0xbf, + 0x34, 0x0e, 0x96, 0xbb, 0xb2, 0x5a, 0x64, 0x46, 0xad, 0x09, 0x82, 0xd8, + 0x7d, 0x09, 0xc8, 0x99, 0xa7, 0x78, 0xfd, 0x04, 0xd8, 0x15, 0x7c, 0x80, + 0x9d, 0x2b, 0xd0, 0x4e, 0x76, 0x82, 0xd9, 0x2a, 0x8d, 0xc0, 0x4b, 0x58, + 0xc7, 0xe7, 0xd7, 0x3d, 0x1f, 0xfa, 0xf1, 0xbb, 0xa2, 0xb1, 0x01, 0x95, + 0xbd, 0x07, 0x9d, 0x01, 0x35, 0xa2, 0x3e, 0x02, 0xbc, 0xf0, 0xfb, 0x6b, + 0xd0, 0x66, 0x32, 0x00, 0xa4, 0xbe, 0x4b, 0x00, 0xf5, 0x9b, 0xdf, 0xfa, + 0xb6, 0x63, 0x40, 0x8e, 0xc8, 0x95, 0x14, 0xe1, 0x6b, 0xb3, 0x9e, 0x40, + 0x0d, 0x51, 0x6a, 0x62, 0x40, 0x6e, 0xc8, 0x91, 0x54, 0xf9, 0x14, 0xb5, + 0x9b, 0x00, 0x50, 0x13, 0x03, 0x72, 0x13, 0x8a, 0xfc, 0x1e, 0xad, 0x98, + 0x20, 0x50, 0x0b, 0x03, 0x72, 0x12, 0x9a, 0xf0, 0x6e, 0x80, 0xef, 0x51, + 0x35, 0x41, 0xa0, 0x06, 0x06, 0xe4, 0x22, 0xf4, 0x3b, 0x34, 0xfe, 0xe5, + 0xc8, 0x5d, 0x13, 0x04, 0x91, 0x7f, 0x09, 0xc8, 0x01, 0xb9, 0x88, 0x44, + 0x78, 0xc1, 0x51, 0x0f, 0x35, 0x3d, 0x41, 0x34, 0x18, 0x10, 0x7b, 0xe9, + 0x17, 0x7d, 0x5e, 0x91, 0x35, 0x09, 0x05, 0xcc, 0x9d, 0x41, 0xf8, 0x01, + 0x40, 0xcc, 0x89, 0xbd, 0x12, 0x52, 0x0d, 0x2b, 0xbe, 0x87, 0x9a, 0x9e, + 0x20, 0x1c, 0x0c, 0x88, 0x35, 0x31, 0x57, 0x4a, 0x3e, 0x81, 0x35, 0x26, + 0x08, 0xe4, 0x07, 0x00, 0x31, 0x26, 0xd6, 0x4a, 0x4a, 0x35, 0xac, 0x8a, + 0x6c, 0x29, 0x19, 0xda, 0xd6, 0xbd, 0x07, 0x22, 0xb6, 0xc4, 0x58, 0x69, + 0xe1, 0x8b, 0xfc, 0xef, 0x41, 0x75, 0x27, 0x23, 0x6c, 0xff, 0x88, 0xa9, + 0xbf, 0x3f, 0x49, 0x50, 0x20, 0x3c, 0xde, 0x87, 0x0d, 0x91, 0x3f, 0x67, + 0xa8, 0x51, 0x10, 0x12, 0x4b, 0x62, 0x1a, 0x2b, 0xe1, 0x6a, 0xa2, 0x5a, + 0x68, 0xd8, 0xdf, 0x14, 0xdd, 0xda, 0x23, 0x86, 0xc4, 0x32, 0x96, 0xc2, + 0xe9, 0xe6, 0x2f, 0xa0, 0x6f, 0xa0, 0xba, 0x11, 0x23, 0xdb, 0x1f, 0x62, + 0x46, 0xec, 0xfc, 0x4e, 0xd9, 0xa3, 0xa8, 0xba, 0x52, 0x0d, 0xd3, 0xf8, + 0x4c, 0x9a, 0x6c, 0xd0, 0x74, 0xa9, 0x9f, 0x58, 0x11, 0x33, 0xad, 0xa4, + 0x2f, 0xbc, 0xf9, 0x0a, 0xaa, 0x0b, 0x49, 0xb2, 0xfc, 0x20, 0x46, 0xc4, + 0x4a, 0x4b, 0x29, 0x83, 0x57, 0xab, 0xa0, 0xdf, 0x41, 0x65, 0x01, 0x18, + 0xd7, 0x7a, 0x89, 0x09, 0xb1, 0x21, 0x46, 0xda, 0x0b, 0x23, 0xfc, 0x0f, + 0xd0, 0xb7, 0xd0, 0xb8, 0x12, 0x16, 0x94, 0xdd, 0xc4, 0x80, 0x58, 0x68, + 0xfb, 0xad, 0x87, 0x6f, 0xae, 0x52, 0x89, 0x9c, 0x24, 0x4f, 0x2b, 0xd3, + 0x77, 0x62, 0x90, 0x68, 0xe1, 0x13, 0xca, 0x9f, 0x43, 0xbf, 0x85, 0x06, + 0xf5, 0xad, 0x52, 0xbd, 0x1e, 0xfa, 0x4a, 0x9f, 0xe9, 0xbb, 0x91, 0x34, + 0x02, 0xbc, 0xdd, 0xf9, 0x29, 0x94, 0xcb, 0xcf, 0x55, 0x27, 0xb0, 0x54, + 0xfb, 0xe8, 0x1b, 0x7d, 0xd4, 0xe2, 0xd6, 0x0e, 0x7e, 0x48, 0x93, 0x8f, + 0x51, 0xf3, 0x69, 0x68, 0xa9, 0x40, 0xab, 0x76, 0x1e, 0x7d, 0xa1, 0x4f, + 0x46, 0x8a, 0x44, 0xe0, 0x23, 0x94, 0xff, 0x2d, 0x94, 0xff, 0xe2, 0xac, + 0x1a, 0xa9, 0x5e, 0xf6, 0xd0, 0x66, 0xda, 0x4e, 0x1f, 0x8c, 0xb4, 0x11, + 0x01, 0x76, 0x99, 0x35, 0xd0, 0xdd, 0xd0, 0xa7, 0x50, 0x2f, 0xf0, 0xa3, + 0xca, 0xa7, 0x6d, 0xb4, 0x91, 0xb6, 0xc6, 0xa2, 0x9b, 0x8f, 0xe3, 0x3d, + 0x67, 0x37, 0x80, 0x3b, 0x0b, 0x3a, 0x33, 0xad, 0x1f, 0x62, 0x1b, 0xd5, + 0xc5, 0x14, 0x6f, 0xe1, 0xae, 0x42, 0x6b, 0xd3, 0x7a, 0x12, 0x5b, 0xfe, + 0x01, 0x47, 0x6c, 0x24, 0x8e, 0x01, 0x90, 0x0f, 0x2e, 0x27, 0x4a, 0xaa, + 0xa0, 0x0c, 0x08, 0xde, 0x52, 0x55, 0x40, 0x65, 0x4d, 0x9e, 0xf0, 0x2f, + 0xf6, 0xf8, 0xb8, 0x3c, 0xdf, 0xb4, 0x4d, 0xd2, 0xbf, 0x86, 0x32, 0x2d, + 0xb6, 0xa2, 0x43, 0x00, 0x38, 0x81, 0xcf, 0x41, 0x15, 0x06, 0x02, 0x17, + 0x4c, 0x72, 0x3b, 0x02, 0xca, 0x77, 0xe4, 0x74, 0x87, 0x96, 0xa7, 0xd5, + 0xde, 0xc7, 0xa1, 0xf5, 0x26, 0x14, 0x76, 0xdf, 0x7c, 0x23, 0x0a, 0x95, + 0xfb, 0x5c, 0x7c, 0x71, 0x07, 0x4a, 0xc2, 0x6f, 0xa5, 0xb7, 0x7c, 0x46, + 0x52, 0x2b, 0xf9, 0x3f, 0x92, 0xc9, 0x00, 0xb6, 0x61, 0xee, 0xab, 0xc9, + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/builtin.go b/vendor/github.com/mattermost/mattermost-server/v5/model/builtin.go new file mode 100644 index 00000000..38e01f8b --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/builtin.go @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +func NewBool(b bool) *bool { return &b } +func NewInt(n int) *int { return &n } +func NewInt64(n int64) *int64 { return &n } +func NewString(s string) *string { return &s } diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/bundle_info.go b/vendor/github.com/mattermost/mattermost-server/v5/model/bundle_info.go new file mode 100644 index 00000000..429e1c3d --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/bundle_info.go @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import "github.com/mattermost/mattermost-server/v5/mlog" + +type BundleInfo struct { + Path string + + Manifest *Manifest + ManifestPath string + ManifestError error +} + +func (b *BundleInfo) WrapLogger(logger *mlog.Logger) *mlog.Logger { + if b.Manifest != nil { + return logger.With(mlog.String("plugin_id", b.Manifest.Id)) + } + return logger.With(mlog.String("plugin_path", b.Path)) +} + +// Returns bundle info for the given path. The return value is never nil. +func BundleInfoForPath(path string) *BundleInfo { + m, mpath, err := FindManifest(path) + return &BundleInfo{ + Path: path, + Manifest: m, + ManifestPath: mpath, + ManifestError: err, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel.go new file mode 100644 index 00000000..6a84b355 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel.go @@ -0,0 +1,364 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto/sha1" + "encoding/hex" + "encoding/json" + "io" + "net/http" + "sort" + "strings" + "unicode/utf8" +) + +const ( + CHANNEL_OPEN = "O" + CHANNEL_PRIVATE = "P" + CHANNEL_DIRECT = "D" + CHANNEL_GROUP = "G" + CHANNEL_GROUP_MAX_USERS = 8 + CHANNEL_GROUP_MIN_USERS = 3 + DEFAULT_CHANNEL = "town-square" + CHANNEL_DISPLAY_NAME_MAX_RUNES = 64 + CHANNEL_NAME_MIN_LENGTH = 2 + CHANNEL_NAME_MAX_LENGTH = 64 + CHANNEL_HEADER_MAX_RUNES = 1024 + CHANNEL_PURPOSE_MAX_RUNES = 250 + CHANNEL_CACHE_SIZE = 25000 + + CHANNEL_SORT_BY_USERNAME = "username" + CHANNEL_SORT_BY_STATUS = "status" +) + +type Channel struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + TeamId string `json:"team_id"` + Type string `json:"type"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Header string `json:"header"` + Purpose string `json:"purpose"` + LastPostAt int64 `json:"last_post_at"` + TotalMsgCount int64 `json:"total_msg_count"` + ExtraUpdateAt int64 `json:"extra_update_at"` + CreatorId string `json:"creator_id"` + SchemeId *string `json:"scheme_id"` + Props map[string]interface{} `json:"props" db:"-"` + GroupConstrained *bool `json:"group_constrained"` +} + +type ChannelWithTeamData struct { + Channel + TeamDisplayName string `json:"team_display_name"` + TeamName string `json:"team_name"` + TeamUpdateAt int64 `json:"team_update_at"` +} + +type ChannelsWithCount struct { + Channels *ChannelListWithTeamData `json:"channels"` + TotalCount int64 `json:"total_count"` +} + +type ChannelPatch struct { + DisplayName *string `json:"display_name"` + Name *string `json:"name"` + Header *string `json:"header"` + Purpose *string `json:"purpose"` + GroupConstrained *bool `json:"group_constrained"` +} + +type ChannelForExport struct { + Channel + TeamName string + SchemeName *string +} + +type DirectChannelForExport struct { + Channel + Members *[]string +} + +type ChannelModeration struct { + Name string `json:"name"` + Roles *ChannelModeratedRoles `json:"roles"` +} + +type ChannelModeratedRoles struct { + Guests *ChannelModeratedRole `json:"guests"` + Members *ChannelModeratedRole `json:"members"` +} + +type ChannelModeratedRole struct { + Value bool `json:"value"` + Enabled bool `json:"enabled"` +} + +type ChannelModerationPatch struct { + Name *string `json:"name"` + Roles *ChannelModeratedRolesPatch `json:"roles"` +} + +type ChannelModeratedRolesPatch struct { + Guests *bool `json:"guests"` + Members *bool `json:"members"` +} + +// ChannelSearchOpts contains options for searching channels. +// +// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records. +// ExcludeDefaultChannels will exclude the configured default channels (ex 'town-square' and 'off-topic'). +// IncludeDeleted will include channel records where DeleteAt != 0. +// ExcludeChannelNames will exclude channels from the results by name. +// Paginate whether to paginate the results. +// Page page requested, if results are paginated. +// PerPage number of results per page, if paginated. +// +type ChannelSearchOpts struct { + NotAssociatedToGroup string + ExcludeDefaultChannels bool + IncludeDeleted bool + ExcludeChannelNames []string + Page *int + PerPage *int +} + +type ChannelMemberCountByGroup struct { + GroupId string `db:"-" json:"group_id"` + ChannelMemberCount int64 `db:"-" json:"channel_member_count"` + ChannelMemberTimezonesCount int64 `db:"-" json:"channel_member_timezones_count"` +} + +func (o *Channel) DeepCopy() *Channel { + copy := *o + if copy.SchemeId != nil { + copy.SchemeId = NewString(*o.SchemeId) + } + return © +} + +func (o *Channel) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func (o *ChannelPatch) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func (o *ChannelsWithCount) ToJson() []byte { + b, _ := json.Marshal(o) + return b +} + +func ChannelsWithCountFromJson(data io.Reader) *ChannelsWithCount { + var o *ChannelsWithCount + json.NewDecoder(data).Decode(&o) + return o +} + +func ChannelFromJson(data io.Reader) *Channel { + var o *Channel + json.NewDecoder(data).Decode(&o) + return o +} + +func ChannelPatchFromJson(data io.Reader) *ChannelPatch { + var o *ChannelPatch + json.NewDecoder(data).Decode(&o) + return o +} + +func ChannelModerationsFromJson(data io.Reader) []*ChannelModeration { + var o []*ChannelModeration + json.NewDecoder(data).Decode(&o) + return o +} + +func ChannelModerationsPatchFromJson(data io.Reader) []*ChannelModerationPatch { + var o []*ChannelModerationPatch + json.NewDecoder(data).Decode(&o) + return o +} + +func ChannelMemberCountsByGroupFromJson(data io.Reader) []*ChannelMemberCountByGroup { + var o []*ChannelMemberCountByGroup + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *Channel) Etag() string { + return Etag(o.Id, o.UpdateAt) +} + +func (o *Channel) IsValid() *AppError { + if !IsValidId(o.Id) { + return NewAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("Channel.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if o.UpdateAt == 0 { + return NewAppError("Channel.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.DisplayName) > CHANNEL_DISPLAY_NAME_MAX_RUNES { + return NewAppError("Channel.IsValid", "model.channel.is_valid.display_name.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if !IsValidChannelIdentifier(o.Name) { + return NewAppError("Channel.IsValid", "model.channel.is_valid.2_or_more.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if !(o.Type == CHANNEL_OPEN || o.Type == CHANNEL_PRIVATE || o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP) { + return NewAppError("Channel.IsValid", "model.channel.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.Header) > CHANNEL_HEADER_MAX_RUNES { + return NewAppError("Channel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.Purpose) > CHANNEL_PURPOSE_MAX_RUNES { + return NewAppError("Channel.IsValid", "model.channel.is_valid.purpose.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.CreatorId) > 26 { + return NewAppError("Channel.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "", http.StatusBadRequest) + } + + userIds := strings.Split(o.Name, "__") + if o.Type != CHANNEL_DIRECT && len(userIds) == 2 && IsValidId(userIds[0]) && IsValidId(userIds[1]) { + return NewAppError("Channel.IsValid", "model.channel.is_valid.name.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (o *Channel) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + o.Name = SanitizeUnicode(o.Name) + o.DisplayName = SanitizeUnicode(o.DisplayName) + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt + o.ExtraUpdateAt = 0 +} + +func (o *Channel) PreUpdate() { + o.UpdateAt = GetMillis() + o.Name = SanitizeUnicode(o.Name) + o.DisplayName = SanitizeUnicode(o.DisplayName) +} + +func (o *Channel) IsGroupOrDirect() bool { + return o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP +} + +func (o *Channel) IsOpen() bool { + return o.Type == CHANNEL_OPEN +} + +func (o *Channel) Patch(patch *ChannelPatch) { + if patch.DisplayName != nil { + o.DisplayName = *patch.DisplayName + } + + if patch.Name != nil { + o.Name = *patch.Name + } + + if patch.Header != nil { + o.Header = *patch.Header + } + + if patch.Purpose != nil { + o.Purpose = *patch.Purpose + } + + if patch.GroupConstrained != nil { + o.GroupConstrained = patch.GroupConstrained + } +} + +func (o *Channel) MakeNonNil() { + if o.Props == nil { + o.Props = make(map[string]interface{}) + } +} + +func (o *Channel) AddProp(key string, value interface{}) { + o.MakeNonNil() + + o.Props[key] = value +} + +func (o *Channel) IsGroupConstrained() bool { + return o.GroupConstrained != nil && *o.GroupConstrained +} + +func (o *Channel) GetOtherUserIdForDM(userId string) string { + if o.Type != CHANNEL_DIRECT { + return "" + } + + userIds := strings.Split(o.Name, "__") + + var otherUserId string + + if userIds[0] != userIds[1] { + if userIds[0] == userId { + otherUserId = userIds[1] + } else { + otherUserId = userIds[0] + } + } + + return otherUserId +} + +func GetDMNameFromIds(userId1, userId2 string) string { + if userId1 > userId2 { + return userId2 + "__" + userId1 + } else { + return userId1 + "__" + userId2 + } +} + +func GetGroupDisplayNameFromUsers(users []*User, truncate bool) string { + usernames := make([]string, len(users)) + for index, user := range users { + usernames[index] = user.Username + } + + sort.Strings(usernames) + + name := strings.Join(usernames, ", ") + + if truncate && len(name) > CHANNEL_NAME_MAX_LENGTH { + name = name[:CHANNEL_NAME_MAX_LENGTH] + } + + return name +} + +func GetGroupNameFromUserIds(userIds []string) string { + sort.Strings(userIds) + + h := sha1.New() + for _, id := range userIds { + io.WriteString(h, id) + } + + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_count.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_count.go new file mode 100644 index 00000000..11ddeec4 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_count.go @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto/md5" + "encoding/json" + "fmt" + "io" + "sort" + "strconv" +) + +type ChannelCounts struct { + Counts map[string]int64 `json:"counts"` + UpdateTimes map[string]int64 `json:"update_times"` +} + +func (o *ChannelCounts) Etag() string { + + ids := []string{} + for id := range o.Counts { + ids = append(ids, id) + } + sort.Strings(ids) + + str := "" + for _, id := range ids { + str += id + strconv.FormatInt(o.Counts[id], 10) + } + + md5Counts := fmt.Sprintf("%x", md5.Sum([]byte(str))) + + var update int64 = 0 + for _, u := range o.UpdateTimes { + if u > update { + update = u + } + } + + return Etag(md5Counts, update) +} + +func (o *ChannelCounts) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func ChannelCountsFromJson(data io.Reader) *ChannelCounts { + var o *ChannelCounts + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_data.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_data.go new file mode 100644 index 00000000..0a1e0d57 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_data.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ChannelData struct { + Channel *Channel `json:"channel"` + Member *ChannelMember `json:"member"` +} + +func (o *ChannelData) Etag() string { + var mt int64 = 0 + if o.Member != nil { + mt = o.Member.LastUpdateAt + } + + return Etag(o.Channel.Id, o.Channel.UpdateAt, o.Channel.LastPostAt, mt) +} + +func (o *ChannelData) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func ChannelDataFromJson(data io.Reader) *ChannelData { + var o *ChannelData + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_list.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_list.go new file mode 100644 index 00000000..b47077ae --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_list.go @@ -0,0 +1,95 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ChannelList []*Channel + +func (o *ChannelList) ToJson() string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func (o *ChannelList) Etag() string { + + id := "0" + var t int64 = 0 + var delta int64 = 0 + + for _, v := range *o { + if v.LastPostAt > t { + t = v.LastPostAt + id = v.Id + } + + if v.UpdateAt > t { + t = v.UpdateAt + id = v.Id + } + + } + + return Etag(id, t, delta, len(*o)) +} + +func ChannelListFromJson(data io.Reader) *ChannelList { + var o *ChannelList + json.NewDecoder(data).Decode(&o) + return o +} + +func ChannelSliceFromJson(data io.Reader) []*Channel { + var o []*Channel + json.NewDecoder(data).Decode(&o) + return o +} + +type ChannelListWithTeamData []*ChannelWithTeamData + +func (o *ChannelListWithTeamData) ToJson() string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func (o *ChannelListWithTeamData) Etag() string { + + id := "0" + var t int64 = 0 + var delta int64 = 0 + + for _, v := range *o { + if v.LastPostAt > t { + t = v.LastPostAt + id = v.Id + } + + if v.UpdateAt > t { + t = v.UpdateAt + id = v.Id + } + + if v.TeamUpdateAt > t { + t = v.TeamUpdateAt + id = v.Id + } + } + + return Etag(id, t, delta, len(*o)) +} + +func ChannelListWithTeamDataFromJson(data io.Reader) *ChannelListWithTeamData { + var o *ChannelListWithTeamData + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_member.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_member.go new file mode 100644 index 00000000..e38bfffe --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_member.go @@ -0,0 +1,194 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "strings" +) + +const ( + CHANNEL_NOTIFY_DEFAULT = "default" + CHANNEL_NOTIFY_ALL = "all" + CHANNEL_NOTIFY_MENTION = "mention" + CHANNEL_NOTIFY_NONE = "none" + CHANNEL_MARK_UNREAD_ALL = "all" + CHANNEL_MARK_UNREAD_MENTION = "mention" + IGNORE_CHANNEL_MENTIONS_DEFAULT = "default" + IGNORE_CHANNEL_MENTIONS_OFF = "off" + IGNORE_CHANNEL_MENTIONS_ON = "on" + IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP = "ignore_channel_mentions" +) + +type ChannelUnread struct { + TeamId string `json:"team_id"` + ChannelId string `json:"channel_id"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + NotifyProps StringMap `json:"-"` +} + +type ChannelUnreadAt struct { + TeamId string `json:"team_id"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + LastViewedAt int64 `json:"last_viewed_at"` + NotifyProps StringMap `json:"-"` +} + +type ChannelMember struct { + ChannelId string `json:"channel_id"` + UserId string `json:"user_id"` + Roles string `json:"roles"` + LastViewedAt int64 `json:"last_viewed_at"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + NotifyProps StringMap `json:"notify_props"` + LastUpdateAt int64 `json:"last_update_at"` + SchemeGuest bool `json:"scheme_guest"` + SchemeUser bool `json:"scheme_user"` + SchemeAdmin bool `json:"scheme_admin"` + ExplicitRoles string `json:"explicit_roles"` +} + +type ChannelMembers []ChannelMember + +type ChannelMemberForExport struct { + ChannelMember + ChannelName string + Username string +} + +func (o *ChannelMembers) ToJson() string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func (o *ChannelUnread) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func (o *ChannelUnreadAt) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func ChannelMembersFromJson(data io.Reader) *ChannelMembers { + var o *ChannelMembers + json.NewDecoder(data).Decode(&o) + return o +} + +func ChannelUnreadFromJson(data io.Reader) *ChannelUnread { + var o *ChannelUnread + json.NewDecoder(data).Decode(&o) + return o +} + +func ChannelUnreadAtFromJson(data io.Reader) *ChannelUnreadAt { + var o *ChannelUnreadAt + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *ChannelMember) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func ChannelMemberFromJson(data io.Reader) *ChannelMember { + var o *ChannelMember + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *ChannelMember) IsValid() *AppError { + + if !IsValidId(o.ChannelId) { + return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.UserId) { + return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + notifyLevel := o.NotifyProps[DESKTOP_NOTIFY_PROP] + if len(notifyLevel) > 20 || !IsChannelNotifyLevelValid(notifyLevel) { + return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.notify_level.app_error", nil, "notify_level="+notifyLevel, http.StatusBadRequest) + } + + markUnreadLevel := o.NotifyProps[MARK_UNREAD_NOTIFY_PROP] + if len(markUnreadLevel) > 20 || !IsChannelMarkUnreadLevelValid(markUnreadLevel) { + return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.unread_level.app_error", nil, "mark_unread_level="+markUnreadLevel, http.StatusBadRequest) + } + + if pushLevel, ok := o.NotifyProps[PUSH_NOTIFY_PROP]; ok { + if len(pushLevel) > 20 || !IsChannelNotifyLevelValid(pushLevel) { + return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.push_level.app_error", nil, "push_notification_level="+pushLevel, http.StatusBadRequest) + } + } + + if sendEmail, ok := o.NotifyProps[EMAIL_NOTIFY_PROP]; ok { + if len(sendEmail) > 20 || !IsSendEmailValid(sendEmail) { + return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.email_value.app_error", nil, "push_notification_level="+sendEmail, http.StatusBadRequest) + } + } + + if ignoreChannelMentions, ok := o.NotifyProps[IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP]; ok { + if len(ignoreChannelMentions) > 40 || !IsIgnoreChannelMentionsValid(ignoreChannelMentions) { + return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.ignore_channel_mentions_value.app_error", nil, "ignore_channel_mentions="+ignoreChannelMentions, http.StatusBadRequest) + } + } + + return nil +} + +func (o *ChannelMember) PreSave() { + o.LastUpdateAt = GetMillis() +} + +func (o *ChannelMember) PreUpdate() { + o.LastUpdateAt = GetMillis() +} + +func (o *ChannelMember) GetRoles() []string { + return strings.Fields(o.Roles) +} + +func IsChannelNotifyLevelValid(notifyLevel string) bool { + return notifyLevel == CHANNEL_NOTIFY_DEFAULT || + notifyLevel == CHANNEL_NOTIFY_ALL || + notifyLevel == CHANNEL_NOTIFY_MENTION || + notifyLevel == CHANNEL_NOTIFY_NONE +} + +func IsChannelMarkUnreadLevelValid(markUnreadLevel string) bool { + return markUnreadLevel == CHANNEL_MARK_UNREAD_ALL || markUnreadLevel == CHANNEL_MARK_UNREAD_MENTION +} + +func IsSendEmailValid(sendEmail string) bool { + return sendEmail == CHANNEL_NOTIFY_DEFAULT || sendEmail == "true" || sendEmail == "false" +} + +func IsIgnoreChannelMentionsValid(ignoreChannelMentions string) bool { + return ignoreChannelMentions == IGNORE_CHANNEL_MENTIONS_ON || ignoreChannelMentions == IGNORE_CHANNEL_MENTIONS_OFF || ignoreChannelMentions == IGNORE_CHANNEL_MENTIONS_DEFAULT +} + +func GetDefaultChannelNotifyProps() StringMap { + return StringMap{ + DESKTOP_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT, + MARK_UNREAD_NOTIFY_PROP: CHANNEL_MARK_UNREAD_ALL, + PUSH_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT, + EMAIL_NOTIFY_PROP: CHANNEL_NOTIFY_DEFAULT, + IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP: IGNORE_CHANNEL_MENTIONS_DEFAULT, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_member_history.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_member_history.go new file mode 100644 index 00000000..b77e0ff9 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_member_history.go @@ -0,0 +1,11 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type ChannelMemberHistory struct { + ChannelId string + UserId string + JoinTime int64 + LeaveTime *int64 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_member_history_result.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_member_history_result.go new file mode 100644 index 00000000..6197d410 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_member_history_result.go @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type ChannelMemberHistoryResult struct { + ChannelId string + UserId string + JoinTime int64 + LeaveTime *int64 + + // these two fields are never set in the database - when we SELECT, we join on Users to get them + UserEmail string `db:"Email"` + Username string + IsBot bool +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_mentions.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_mentions.go new file mode 100644 index 00000000..eb14e8ed --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_mentions.go @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "regexp" + "strings" +) + +var channelMentionRegexp = regexp.MustCompile(`\B~[a-zA-Z0-9\-_]+`) + +func ChannelMentions(message string) []string { + var names []string + + if strings.Contains(message, "~") { + alreadyMentioned := make(map[string]bool) + for _, match := range channelMentionRegexp.FindAllString(message, -1) { + name := match[1:] + if !alreadyMentioned[name] { + names = append(names, name) + alreadyMentioned[name] = true + } + } + } + + return names +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_search.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_search.go new file mode 100644 index 00000000..2e994227 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_search.go @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const CHANNEL_SEARCH_DEFAULT_LIMIT = 50 + +type ChannelSearch struct { + Term string `json:"term"` + ExcludeDefaultChannels bool `json:"exclude_default_channels"` + NotAssociatedToGroup string `json:"not_associated_to_group"` + Page *int `json:"page,omitempty"` + PerPage *int `json:"per_page,omitempty"` +} + +// ToJson convert a Channel to a json string +func (c *ChannelSearch) ToJson() string { + b, _ := json.Marshal(c) + return string(b) +} + +// ChannelSearchFromJson will decode the input and return a Channel +func ChannelSearchFromJson(data io.Reader) *ChannelSearch { + var cs *ChannelSearch + json.NewDecoder(data).Decode(&cs) + return cs +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_stats.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_stats.go new file mode 100644 index 00000000..76f682aa --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_stats.go @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ChannelStats struct { + ChannelId string `json:"channel_id"` + MemberCount int64 `json:"member_count"` + GuestCount int64 `json:"guest_count"` + PinnedPostCount int64 `json:"pinnedpost_count"` +} + +func (o *ChannelStats) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func ChannelStatsFromJson(data io.Reader) *ChannelStats { + var o *ChannelStats + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/channel_view.go b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_view.go new file mode 100644 index 00000000..42fcac3a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/channel_view.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ChannelView struct { + ChannelId string `json:"channel_id"` + PrevChannelId string `json:"prev_channel_id"` +} + +func (o *ChannelView) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func ChannelViewFromJson(data io.Reader) *ChannelView { + var o *ChannelView + json.NewDecoder(data).Decode(&o) + return o +} + +type ChannelViewResponse struct { + Status string `json:"status"` + LastViewedAtTimes map[string]int64 `json:"last_viewed_at_times"` +} + +func (o *ChannelViewResponse) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func ChannelViewResponseFromJson(data io.Reader) *ChannelViewResponse { + var o *ChannelViewResponse + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/client4.go b/vendor/github.com/mattermost/mattermost-server/v5/model/client4.go new file mode 100644 index 00000000..b522ecb8 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/client4.go @@ -0,0 +1,5095 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +const ( + HEADER_REQUEST_ID = "X-Request-ID" + HEADER_VERSION_ID = "X-Version-ID" + HEADER_CLUSTER_ID = "X-Cluster-ID" + HEADER_ETAG_SERVER = "ETag" + HEADER_ETAG_CLIENT = "If-None-Match" + HEADER_FORWARDED = "X-Forwarded-For" + HEADER_REAL_IP = "X-Real-IP" + HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" + HEADER_TOKEN = "token" + HEADER_CSRF_TOKEN = "X-CSRF-Token" + HEADER_BEARER = "BEARER" + HEADER_AUTH = "Authorization" + HEADER_REQUESTED_WITH = "X-Requested-With" + HEADER_REQUESTED_WITH_XML = "XMLHttpRequest" + STATUS = "status" + STATUS_OK = "OK" + STATUS_FAIL = "FAIL" + STATUS_UNHEALTHY = "UNHEALTHY" + STATUS_REMOVE = "REMOVE" + + CLIENT_DIR = "client" + + API_URL_SUFFIX_V1 = "/api/v1" + API_URL_SUFFIX_V4 = "/api/v4" + API_URL_SUFFIX = API_URL_SUFFIX_V4 +) + +type Response struct { + StatusCode int + Error *AppError + RequestId string + Etag string + ServerVersion string + Header http.Header +} + +type Client4 struct { + Url string // The location of the server, for example "http://localhost:8065" + ApiUrl string // The api location of the server, for example "http://localhost:8065/api/v4" + HttpClient *http.Client // The http client + AuthToken string + AuthType string + HttpHeader map[string]string // Headers to be copied over for each request +} + +func closeBody(r *http.Response) { + if r.Body != nil { + _, _ = io.Copy(ioutil.Discard, r.Body) + _ = r.Body.Close() + } +} + +// Must is a convenience function used for testing. +func (c *Client4) Must(result interface{}, resp *Response) interface{} { + if resp.Error != nil { + time.Sleep(time.Second) + panic(resp.Error) + } + + return result +} + +func NewAPIv4Client(url string) *Client4 { + return &Client4{url, url + API_URL_SUFFIX, &http.Client{}, "", "", map[string]string{}} +} + +func BuildErrorResponse(r *http.Response, err *AppError) *Response { + var statusCode int + var header http.Header + if r != nil { + statusCode = r.StatusCode + header = r.Header + } else { + statusCode = 0 + header = make(http.Header) + } + + return &Response{ + StatusCode: statusCode, + Error: err, + Header: header, + } +} + +func BuildResponse(r *http.Response) *Response { + return &Response{ + StatusCode: r.StatusCode, + RequestId: r.Header.Get(HEADER_REQUEST_ID), + Etag: r.Header.Get(HEADER_ETAG_SERVER), + ServerVersion: r.Header.Get(HEADER_VERSION_ID), + Header: r.Header, + } +} + +func (c *Client4) SetToken(token string) { + c.AuthToken = token + c.AuthType = HEADER_BEARER +} + +// MockSession is deprecated in favour of SetToken +func (c *Client4) MockSession(token string) { + c.SetToken(token) +} + +func (c *Client4) SetOAuthToken(token string) { + c.AuthToken = token + c.AuthType = HEADER_TOKEN +} + +func (c *Client4) ClearOAuthToken() { + c.AuthToken = "" + c.AuthType = HEADER_BEARER +} + +func (c *Client4) GetUsersRoute() string { + return "/users" +} + +func (c *Client4) GetUserRoute(userId string) string { + return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId) +} + +func (c *Client4) GetUserAccessTokensRoute() string { + return fmt.Sprintf(c.GetUsersRoute() + "/tokens") +} + +func (c *Client4) GetUserAccessTokenRoute(tokenId string) string { + return fmt.Sprintf(c.GetUsersRoute()+"/tokens/%v", tokenId) +} + +func (c *Client4) GetUserByUsernameRoute(userName string) string { + return fmt.Sprintf(c.GetUsersRoute()+"/username/%v", userName) +} + +func (c *Client4) GetUserByEmailRoute(email string) string { + return fmt.Sprintf(c.GetUsersRoute()+"/email/%v", email) +} + +func (c *Client4) GetBotsRoute() string { + return "/bots" +} + +func (c *Client4) GetBotRoute(botUserId string) string { + return fmt.Sprintf("%s/%s", c.GetBotsRoute(), botUserId) +} + +func (c *Client4) GetTeamsRoute() string { + return "/teams" +} + +func (c *Client4) GetTeamRoute(teamId string) string { + return fmt.Sprintf(c.GetTeamsRoute()+"/%v", teamId) +} + +func (c *Client4) GetTeamAutoCompleteCommandsRoute(teamId string) string { + return fmt.Sprintf(c.GetTeamsRoute()+"/%v/commands/autocomplete", teamId) +} + +func (c *Client4) GetTeamByNameRoute(teamName string) string { + return fmt.Sprintf(c.GetTeamsRoute()+"/name/%v", teamName) +} + +func (c *Client4) GetTeamMemberRoute(teamId, userId string) string { + return fmt.Sprintf(c.GetTeamRoute(teamId)+"/members/%v", userId) +} + +func (c *Client4) GetTeamMembersRoute(teamId string) string { + return fmt.Sprintf(c.GetTeamRoute(teamId) + "/members") +} + +func (c *Client4) GetTeamStatsRoute(teamId string) string { + return fmt.Sprintf(c.GetTeamRoute(teamId) + "/stats") +} + +func (c *Client4) GetTeamImportRoute(teamId string) string { + return fmt.Sprintf(c.GetTeamRoute(teamId) + "/import") +} + +func (c *Client4) GetChannelsRoute() string { + return "/channels" +} + +func (c *Client4) GetChannelsForTeamRoute(teamId string) string { + return fmt.Sprintf(c.GetTeamRoute(teamId) + "/channels") +} + +func (c *Client4) GetChannelRoute(channelId string) string { + return fmt.Sprintf(c.GetChannelsRoute()+"/%v", channelId) +} + +func (c *Client4) GetChannelByNameRoute(channelName, teamId string) string { + return fmt.Sprintf(c.GetTeamRoute(teamId)+"/channels/name/%v", channelName) +} + +func (c *Client4) GetChannelsForTeamForUserRoute(teamId, userId string, includeDeleted bool) string { + route := fmt.Sprintf(c.GetUserRoute(userId) + c.GetTeamRoute(teamId) + "/channels") + if includeDeleted { + query := fmt.Sprintf("?include_deleted=%v", includeDeleted) + return route + query + } + return route +} + +func (c *Client4) GetChannelByNameForTeamNameRoute(channelName, teamName string) string { + return fmt.Sprintf(c.GetTeamByNameRoute(teamName)+"/channels/name/%v", channelName) +} + +func (c *Client4) GetChannelMembersRoute(channelId string) string { + return fmt.Sprintf(c.GetChannelRoute(channelId) + "/members") +} + +func (c *Client4) GetChannelMemberRoute(channelId, userId string) string { + return fmt.Sprintf(c.GetChannelMembersRoute(channelId)+"/%v", userId) +} + +func (c *Client4) GetPostsRoute() string { + return "/posts" +} + +func (c *Client4) GetPostsEphemeralRoute() string { + return "/posts/ephemeral" +} + +func (c *Client4) GetConfigRoute() string { + return "/config" +} + +func (c *Client4) GetLicenseRoute() string { + return "/license" +} + +func (c *Client4) GetPostRoute(postId string) string { + return fmt.Sprintf(c.GetPostsRoute()+"/%v", postId) +} + +func (c *Client4) GetFilesRoute() string { + return "/files" +} + +func (c *Client4) GetFileRoute(fileId string) string { + return fmt.Sprintf(c.GetFilesRoute()+"/%v", fileId) +} + +func (c *Client4) GetPluginsRoute() string { + return "/plugins" +} + +func (c *Client4) GetPluginRoute(pluginId string) string { + return fmt.Sprintf(c.GetPluginsRoute()+"/%v", pluginId) +} + +func (c *Client4) GetSystemRoute() string { + return "/system" +} + +func (c *Client4) GetTestEmailRoute() string { + return "/email/test" +} + +func (c *Client4) GetTestSiteURLRoute() string { + return "/site_url/test" +} + +func (c *Client4) GetTestS3Route() string { + return "/file/s3_test" +} + +func (c *Client4) GetDatabaseRoute() string { + return "/database" +} + +func (c *Client4) GetCacheRoute() string { + return "/caches" +} + +func (c *Client4) GetClusterRoute() string { + return "/cluster" +} + +func (c *Client4) GetIncomingWebhooksRoute() string { + return "/hooks/incoming" +} + +func (c *Client4) GetIncomingWebhookRoute(hookID string) string { + return fmt.Sprintf(c.GetIncomingWebhooksRoute()+"/%v", hookID) +} + +func (c *Client4) GetComplianceReportsRoute() string { + return "/compliance/reports" +} + +func (c *Client4) GetComplianceReportRoute(reportId string) string { + return fmt.Sprintf("/compliance/reports/%v", reportId) +} + +func (c *Client4) GetOutgoingWebhooksRoute() string { + return "/hooks/outgoing" +} + +func (c *Client4) GetOutgoingWebhookRoute(hookID string) string { + return fmt.Sprintf(c.GetOutgoingWebhooksRoute()+"/%v", hookID) +} + +func (c *Client4) GetPreferencesRoute(userId string) string { + return fmt.Sprintf(c.GetUserRoute(userId) + "/preferences") +} + +func (c *Client4) GetUserStatusRoute(userId string) string { + return fmt.Sprintf(c.GetUserRoute(userId) + "/status") +} + +func (c *Client4) GetUserStatusesRoute() string { + return fmt.Sprintf(c.GetUsersRoute() + "/status") +} + +func (c *Client4) GetSamlRoute() string { + return "/saml" +} + +func (c *Client4) GetLdapRoute() string { + return "/ldap" +} + +func (c *Client4) GetBrandRoute() string { + return "/brand" +} + +func (c *Client4) GetDataRetentionRoute() string { + return "/data_retention" +} + +func (c *Client4) GetElasticsearchRoute() string { + return "/elasticsearch" +} + +func (c *Client4) GetBleveRoute() string { + return "/bleve" +} + +func (c *Client4) GetCommandsRoute() string { + return "/commands" +} + +func (c *Client4) GetCommandRoute(commandId string) string { + return fmt.Sprintf(c.GetCommandsRoute()+"/%v", commandId) +} + +func (c *Client4) GetCommandMoveRoute(commandId string) string { + return fmt.Sprintf(c.GetCommandsRoute()+"/%v/move", commandId) +} + +func (c *Client4) GetEmojisRoute() string { + return "/emoji" +} + +func (c *Client4) GetEmojiRoute(emojiId string) string { + return fmt.Sprintf(c.GetEmojisRoute()+"/%v", emojiId) +} + +func (c *Client4) GetEmojiByNameRoute(name string) string { + return fmt.Sprintf(c.GetEmojisRoute()+"/name/%v", name) +} + +func (c *Client4) GetReactionsRoute() string { + return "/reactions" +} + +func (c *Client4) GetOAuthAppsRoute() string { + return "/oauth/apps" +} + +func (c *Client4) GetOAuthAppRoute(appId string) string { + return fmt.Sprintf("/oauth/apps/%v", appId) +} + +func (c *Client4) GetOpenGraphRoute() string { + return "/opengraph" +} + +func (c *Client4) GetJobsRoute() string { + return "/jobs" +} + +func (c *Client4) GetRolesRoute() string { + return "/roles" +} + +func (c *Client4) GetSchemesRoute() string { + return "/schemes" +} + +func (c *Client4) GetSchemeRoute(id string) string { + return c.GetSchemesRoute() + fmt.Sprintf("/%v", id) +} + +func (c *Client4) GetAnalyticsRoute() string { + return "/analytics" +} + +func (c *Client4) GetTimezonesRoute() string { + return fmt.Sprintf(c.GetSystemRoute() + "/timezones") +} + +func (c *Client4) GetChannelSchemeRoute(channelId string) string { + return fmt.Sprintf(c.GetChannelsRoute()+"/%v/scheme", channelId) +} + +func (c *Client4) GetTeamSchemeRoute(teamId string) string { + return fmt.Sprintf(c.GetTeamsRoute()+"/%v/scheme", teamId) +} + +func (c *Client4) GetTotalUsersStatsRoute() string { + return fmt.Sprintf(c.GetUsersRoute() + "/stats") +} + +func (c *Client4) GetRedirectLocationRoute() string { + return "/redirect_location" +} + +func (c *Client4) GetServerBusyRoute() string { + return "/server_busy" +} + +func (c *Client4) GetUserTermsOfServiceRoute(userId string) string { + return c.GetUserRoute(userId) + "/terms_of_service" +} + +func (c *Client4) GetTermsOfServiceRoute() string { + return "/terms_of_service" +} + +func (c *Client4) GetGroupsRoute() string { + return "/groups" +} + +func (c *Client4) GetGroupRoute(groupID string) string { + return fmt.Sprintf("%s/%s", c.GetGroupsRoute(), groupID) +} + +func (c *Client4) GetGroupSyncableRoute(groupID, syncableID string, syncableType GroupSyncableType) string { + return fmt.Sprintf("%s/%ss/%s", c.GetGroupRoute(groupID), strings.ToLower(syncableType.String()), syncableID) +} + +func (c *Client4) GetGroupSyncablesRoute(groupID string, syncableType GroupSyncableType) string { + return fmt.Sprintf("%s/%ss", c.GetGroupRoute(groupID), strings.ToLower(syncableType.String())) +} + +func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) { + return c.DoApiRequest(http.MethodGet, c.ApiUrl+url, "", etag) +} + +func (c *Client4) DoApiPost(url string, data string) (*http.Response, *AppError) { + return c.DoApiRequest(http.MethodPost, c.ApiUrl+url, data, "") +} + +func (c *Client4) doApiPostBytes(url string, data []byte) (*http.Response, *AppError) { + return c.doApiRequestBytes(http.MethodPost, c.ApiUrl+url, data, "") +} + +func (c *Client4) DoApiPut(url string, data string) (*http.Response, *AppError) { + return c.DoApiRequest(http.MethodPut, c.ApiUrl+url, data, "") +} + +func (c *Client4) doApiPutBytes(url string, data []byte) (*http.Response, *AppError) { + return c.doApiRequestBytes(http.MethodPut, c.ApiUrl+url, data, "") +} + +func (c *Client4) DoApiDelete(url string) (*http.Response, *AppError) { + return c.DoApiRequest(http.MethodDelete, c.ApiUrl+url, "", "") +} + +func (c *Client4) DoApiRequest(method, url, data, etag string) (*http.Response, *AppError) { + return c.doApiRequestReader(method, url, strings.NewReader(data), etag) +} + +func (c *Client4) doApiRequestBytes(method, url string, data []byte, etag string) (*http.Response, *AppError) { + return c.doApiRequestReader(method, url, bytes.NewReader(data), etag) +} + +func (c *Client4) doApiRequestReader(method, url string, data io.Reader, etag string) (*http.Response, *AppError) { + rq, err := http.NewRequest(method, url, data) + if err != nil { + return nil, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if len(etag) > 0 { + rq.Header.Set(HEADER_ETAG_CLIENT, etag) + } + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + if c.HttpHeader != nil && len(c.HttpHeader) > 0 { + for k, v := range c.HttpHeader { + rq.Header.Set(k, v) + } + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return nil, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0) + } + + if rp.StatusCode == 304 { + return rp, nil + } + + if rp.StatusCode >= 300 { + defer closeBody(rp) + return rp, AppErrorFromJson(rp.Body) + } + + return rp, nil +} + +func (c *Client4) DoUploadFile(url string, data []byte, contentType string) (*FileUploadResponse, *Response) { + return c.doUploadFile(url, bytes.NewReader(data), contentType, 0) +} + +func (c *Client4) doUploadFile(url string, body io.Reader, contentType string, contentLength int64) (*FileUploadResponse, *Response) { + rq, err := http.NewRequest("POST", c.ApiUrl+url, body) + if err != nil { + return nil, &Response{Error: NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + if contentLength != 0 { + rq.ContentLength = contentLength + } + rq.Header.Set("Content-Type", contentType) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return nil, BuildErrorResponse(rp, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0)) + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return FileUploadResponseFromJson(rp.Body), BuildResponse(rp) +} + +func (c *Client4) DoEmojiUploadFile(url string, data []byte, contentType string) (*Emoji, *Response) { + rq, err := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data)) + if err != nil { + return nil, &Response{Error: NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + rq.Header.Set("Content-Type", contentType) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return nil, BuildErrorResponse(rp, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0)) + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return EmojiFromJson(rp.Body), BuildResponse(rp) +} + +func (c *Client4) DoUploadImportTeam(url string, data []byte, contentType string) (map[string]string, *Response) { + rq, err := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data)) + if err != nil { + return nil, &Response{Error: NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + rq.Header.Set("Content-Type", contentType) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return nil, BuildErrorResponse(rp, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0)) + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return MapFromJson(rp.Body), BuildResponse(rp) +} + +// CheckStatusOK is a convenience function for checking the standard OK response +// from the web service. +func CheckStatusOK(r *http.Response) bool { + m := MapFromJson(r.Body) + defer closeBody(r) + + if m != nil && m[STATUS] == STATUS_OK { + return true + } + + return false +} + +// Authentication Section + +// LoginById authenticates a user by user id and password. +func (c *Client4) LoginById(id string, password string) (*User, *Response) { + m := make(map[string]string) + m["id"] = id + m["password"] = password + return c.login(m) +} + +// Login authenticates a user by login id, which can be username, email or some sort +// of SSO identifier based on server configuration, and a password. +func (c *Client4) Login(loginId string, password string) (*User, *Response) { + m := make(map[string]string) + m["login_id"] = loginId + m["password"] = password + return c.login(m) +} + +// LoginByLdap authenticates a user by LDAP id and password. +func (c *Client4) LoginByLdap(loginId string, password string) (*User, *Response) { + m := make(map[string]string) + m["login_id"] = loginId + m["password"] = password + m["ldap_only"] = "true" + return c.login(m) +} + +// LoginWithDevice authenticates a user by login id (username, email or some sort +// of SSO identifier based on configuration), password and attaches a device id to +// the session. +func (c *Client4) LoginWithDevice(loginId string, password string, deviceId string) (*User, *Response) { + m := make(map[string]string) + m["login_id"] = loginId + m["password"] = password + m["device_id"] = deviceId + return c.login(m) +} + +// LoginWithMFA logs a user in with a MFA token +func (c *Client4) LoginWithMFA(loginId, password, mfaToken string) (*User, *Response) { + m := make(map[string]string) + m["login_id"] = loginId + m["password"] = password + m["token"] = mfaToken + return c.login(m) +} + +func (c *Client4) login(m map[string]string) (*User, *Response) { + r, err := c.DoApiPost("/users/login", MapToJson(m)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + c.AuthToken = r.Header.Get(HEADER_TOKEN) + c.AuthType = HEADER_BEARER + return UserFromJson(r.Body), BuildResponse(r) +} + +// Logout terminates the current user's session. +func (c *Client4) Logout() (bool, *Response) { + r, err := c.DoApiPost("/users/logout", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + c.AuthToken = "" + c.AuthType = HEADER_BEARER + return CheckStatusOK(r), BuildResponse(r) +} + +// SwitchAccountType changes a user's login type from one type to another. +func (c *Client4) SwitchAccountType(switchRequest *SwitchRequest) (string, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/login/switch", switchRequest.ToJson()) + if err != nil { + return "", BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body)["follow_link"], BuildResponse(r) +} + +// User Section + +// CreateUser creates a user in the system based on the provided user struct. +func (c *Client4) CreateUser(user *User) (*User, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute(), user.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) +} + +// CreateUserWithToken creates a user in the system based on the provided tokenId. +func (c *Client4) CreateUserWithToken(user *User, tokenId string) (*User, *Response) { + if tokenId == "" { + err := NewAppError("MissingHashOrData", "api.user.create_user.missing_token.app_error", nil, "", http.StatusBadRequest) + return nil, &Response{StatusCode: err.StatusCode, Error: err} + } + + query := fmt.Sprintf("?t=%v", tokenId) + r, err := c.DoApiPost(c.GetUsersRoute()+query, user.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return UserFromJson(r.Body), BuildResponse(r) +} + +// CreateUserWithInviteId creates a user in the system based on the provided invited id. +func (c *Client4) CreateUserWithInviteId(user *User, inviteId string) (*User, *Response) { + if inviteId == "" { + err := NewAppError("MissingInviteId", "api.user.create_user.missing_invite_id.app_error", nil, "", http.StatusBadRequest) + return nil, &Response{StatusCode: err.StatusCode, Error: err} + } + + query := fmt.Sprintf("?iid=%v", url.QueryEscape(inviteId)) + r, err := c.DoApiPost(c.GetUsersRoute()+query, user.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return UserFromJson(r.Body), BuildResponse(r) +} + +// GetMe returns the logged in user. +func (c *Client4) GetMe(etag string) (*User, *Response) { + r, err := c.DoApiGet(c.GetUserRoute(ME), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) +} + +// GetUser returns a user based on the provided user id string. +func (c *Client4) GetUser(userId, etag string) (*User, *Response) { + r, err := c.DoApiGet(c.GetUserRoute(userId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) +} + +// GetUserByUsername returns a user based on the provided user name string. +func (c *Client4) GetUserByUsername(userName, etag string) (*User, *Response) { + r, err := c.DoApiGet(c.GetUserByUsernameRoute(userName), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) +} + +// GetUserByEmail returns a user based on the provided user email string. +func (c *Client4) GetUserByEmail(email, etag string) (*User, *Response) { + r, err := c.DoApiGet(c.GetUserByEmailRoute(email), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) +} + +// AutocompleteUsersInTeam returns the users on a team based on search term. +func (c *Client4) AutocompleteUsersInTeam(teamId string, username string, limit int, etag string) (*UserAutocomplete, *Response) { + query := fmt.Sprintf("?in_team=%v&name=%v&limit=%d", teamId, username, limit) + r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserAutocompleteFromJson(r.Body), BuildResponse(r) +} + +// AutocompleteUsersInChannel returns the users in a channel based on search term. +func (c *Client4) AutocompleteUsersInChannel(teamId string, channelId string, username string, limit int, etag string) (*UserAutocomplete, *Response) { + query := fmt.Sprintf("?in_team=%v&in_channel=%v&name=%v&limit=%d", teamId, channelId, username, limit) + r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserAutocompleteFromJson(r.Body), BuildResponse(r) +} + +// AutocompleteUsers returns the users in the system based on search term. +func (c *Client4) AutocompleteUsers(username string, limit int, etag string) (*UserAutocomplete, *Response) { + query := fmt.Sprintf("?name=%v&limit=%d", username, limit) + r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserAutocompleteFromJson(r.Body), BuildResponse(r) +} + +// GetDefaultProfileImage gets the default user's profile image. Must be logged in. +func (c *Client4) GetDefaultProfileImage(userId string) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetUserRoute(userId)+"/image/default", "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetDefaultProfileImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + + return data, BuildResponse(r) +} + +// GetProfileImage gets user's profile image. Must be logged in. +func (c *Client4) GetProfileImage(userId, etag string) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetUserRoute(userId)+"/image", etag) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetProfileImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + return data, BuildResponse(r) +} + +// GetUsers returns a page of users on the system. Page counting starts at 0. +func (c *Client4) GetUsers(page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersInTeam returns a page of users on a team. Page counting starts at 0. +func (c *Client4) GetUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetNewUsersInTeam returns a page of users on a team. Page counting starts at 0. +func (c *Client4) GetNewUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?sort=create_at&in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetRecentlyActiveUsersInTeam returns a page of users on a team. Page counting starts at 0. +func (c *Client4) GetRecentlyActiveUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?sort=last_activity_at&in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetActiveUsersInTeam returns a page of users on a team. Page counting starts at 0. +func (c *Client4) GetActiveUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?active=true&in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersNotInTeam returns a page of users who are not in a team. Page counting starts at 0. +func (c *Client4) GetUsersNotInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?not_in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersInChannel returns a page of users in a channel. Page counting starts at 0. +func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v", channelId, page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersInChannelByStatus returns a page of users in a channel. Page counting starts at 0. Sorted by Status +func (c *Client4) GetUsersInChannelByStatus(channelId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v&sort=status", channelId, page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersNotInChannel returns a page of users not in a channel. Page counting starts at 0. +func (c *Client4) GetUsersNotInChannel(teamId, channelId string, page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?in_team=%v¬_in_channel=%v&page=%v&per_page=%v", teamId, channelId, page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersWithoutTeam returns a page of users on the system that aren't on any teams. Page counting starts at 0. +func (c *Client4) GetUsersWithoutTeam(page int, perPage int, etag string) ([]*User, *Response) { + query := fmt.Sprintf("?without_team=1&page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetUsersRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersByIds returns a list of users based on the provided user ids. +func (c *Client4) GetUsersByIds(userIds []string) ([]*User, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/ids", ArrayToJson(userIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersByIds returns a list of users based on the provided user ids. +func (c *Client4) GetUsersByIdsWithOptions(userIds []string, options *UserGetByIdsOptions) ([]*User, *Response) { + v := url.Values{} + if options.Since != 0 { + v.Set("since", fmt.Sprintf("%d", options.Since)) + } + + url := c.GetUsersRoute() + "/ids" + if len(v) > 0 { + url += "?" + v.Encode() + } + + r, err := c.DoApiPost(url, ArrayToJson(userIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersByUsernames returns a list of users based on the provided usernames. +func (c *Client4) GetUsersByUsernames(usernames []string) ([]*User, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/usernames", ArrayToJson(usernames)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// GetUsersByGroupChannelIds returns a map with channel ids as keys +// and a list of users as values based on the provided user ids. +func (c *Client4) GetUsersByGroupChannelIds(groupChannelIds []string) (map[string][]*User, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/group_channels", ArrayToJson(groupChannelIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + usersByChannelId := map[string][]*User{} + json.NewDecoder(r.Body).Decode(&usersByChannelId) + return usersByChannelId, BuildResponse(r) +} + +// SearchUsers returns a list of users based on some search criteria. +func (c *Client4) SearchUsers(search *UserSearch) ([]*User, *Response) { + r, err := c.doApiPostBytes(c.GetUsersRoute()+"/search", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserListFromJson(r.Body), BuildResponse(r) +} + +// UpdateUser updates a user in the system based on the provided user struct. +func (c *Client4) UpdateUser(user *User) (*User, *Response) { + r, err := c.DoApiPut(c.GetUserRoute(user.Id), user.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) +} + +// PatchUser partially updates a user in the system. Any missing fields are not updated. +func (c *Client4) PatchUser(userId string, patch *UserPatch) (*User, *Response) { + r, err := c.DoApiPut(c.GetUserRoute(userId)+"/patch", patch.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserFromJson(r.Body), BuildResponse(r) +} + +// UpdateUserAuth updates a user AuthData (uthData, authService and password) in the system. +func (c *Client4) UpdateUserAuth(userId string, userAuth *UserAuth) (*UserAuth, *Response) { + r, err := c.DoApiPut(c.GetUserRoute(userId)+"/auth", userAuth.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserAuthFromJson(r.Body), BuildResponse(r) +} + +// UpdateUserMfa activates multi-factor authentication for a user if activate +// is true and a valid code is provided. If activate is false, then code is not +// required and multi-factor authentication is disabled for the user. +func (c *Client4) UpdateUserMfa(userId, code string, activate bool) (bool, *Response) { + requestBody := make(map[string]interface{}) + requestBody["activate"] = activate + requestBody["code"] = code + + r, err := c.DoApiPut(c.GetUserRoute(userId)+"/mfa", StringInterfaceToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// CheckUserMfa checks whether a user has MFA active on their account or not based on the +// provided login id. +// Deprecated: Clients should use Login method and check for MFA Error +func (c *Client4) CheckUserMfa(loginId string) (bool, *Response) { + requestBody := make(map[string]interface{}) + requestBody["login_id"] = loginId + r, err := c.DoApiPost(c.GetUsersRoute()+"/mfa", StringInterfaceToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + data := StringInterfaceFromJson(r.Body) + mfaRequired, ok := data["mfa_required"].(bool) + if !ok { + return false, BuildResponse(r) + } + return mfaRequired, BuildResponse(r) +} + +// GenerateMfaSecret will generate a new MFA secret for a user and return it as a string and +// as a base64 encoded image QR code. +func (c *Client4) GenerateMfaSecret(userId string) (*MfaSecret, *Response) { + r, err := c.DoApiPost(c.GetUserRoute(userId)+"/mfa/generate", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return MfaSecretFromJson(r.Body), BuildResponse(r) +} + +// UpdateUserPassword updates a user's password. Must be logged in as the user or be a system administrator. +func (c *Client4) UpdateUserPassword(userId, currentPassword, newPassword string) (bool, *Response) { + requestBody := map[string]string{"current_password": currentPassword, "new_password": newPassword} + r, err := c.DoApiPut(c.GetUserRoute(userId)+"/password", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// PromoteGuestToUser convert a guest into a regular user +func (c *Client4) PromoteGuestToUser(guestId string) (bool, *Response) { + r, err := c.DoApiPost(c.GetUserRoute(guestId)+"/promote", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// DemoteUserToGuest convert a regular user into a guest +func (c *Client4) DemoteUserToGuest(guestId string) (bool, *Response) { + r, err := c.DoApiPost(c.GetUserRoute(guestId)+"/demote", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UpdateUserRoles updates a user's roles in the system. A user can have "system_user" and "system_admin" roles. +func (c *Client4) UpdateUserRoles(userId, roles string) (bool, *Response) { + requestBody := map[string]string{"roles": roles} + r, err := c.DoApiPut(c.GetUserRoute(userId)+"/roles", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UpdateUserActive updates status of a user whether active or not. +func (c *Client4) UpdateUserActive(userId string, active bool) (bool, *Response) { + requestBody := make(map[string]interface{}) + requestBody["active"] = active + r, err := c.DoApiPut(c.GetUserRoute(userId)+"/active", StringInterfaceToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return CheckStatusOK(r), BuildResponse(r) +} + +// DeleteUser deactivates a user in the system based on the provided user id string. +func (c *Client4) DeleteUser(userId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetUserRoute(userId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// SendPasswordResetEmail will send a link for password resetting to a user with the +// provided email. +func (c *Client4) SendPasswordResetEmail(email string) (bool, *Response) { + requestBody := map[string]string{"email": email} + r, err := c.DoApiPost(c.GetUsersRoute()+"/password/reset/send", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// ResetPassword uses a recovery code to update reset a user's password. +func (c *Client4) ResetPassword(token, newPassword string) (bool, *Response) { + requestBody := map[string]string{"token": token, "new_password": newPassword} + r, err := c.DoApiPost(c.GetUsersRoute()+"/password/reset", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetSessions returns a list of sessions based on the provided user id string. +func (c *Client4) GetSessions(userId, etag string) ([]*Session, *Response) { + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/sessions", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return SessionsFromJson(r.Body), BuildResponse(r) +} + +// RevokeSession revokes a user session based on the provided user id and session id strings. +func (c *Client4) RevokeSession(userId, sessionId string) (bool, *Response) { + requestBody := map[string]string{"session_id": sessionId} + r, err := c.DoApiPost(c.GetUserRoute(userId)+"/sessions/revoke", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// RevokeAllSessions revokes all sessions for the provided user id string. +func (c *Client4) RevokeAllSessions(userId string) (bool, *Response) { + r, err := c.DoApiPost(c.GetUserRoute(userId)+"/sessions/revoke/all", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// RevokeAllSessions revokes all sessions for all the users. +func (c *Client4) RevokeSessionsFromAllUsers() (bool, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/sessions/revoke/all", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// AttachDeviceId attaches a mobile device ID to the current session. +func (c *Client4) AttachDeviceId(deviceId string) (bool, *Response) { + requestBody := map[string]string{"device_id": deviceId} + r, err := c.DoApiPut(c.GetUsersRoute()+"/sessions/device", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetTeamsUnreadForUser will return an array with TeamUnread objects that contain the amount +// of unread messages and mentions the current user has for the teams it belongs to. +// An optional team ID can be set to exclude that team from the results. Must be authenticated. +func (c *Client4) GetTeamsUnreadForUser(userId, teamIdToExclude string) ([]*TeamUnread, *Response) { + var optional string + if teamIdToExclude != "" { + optional += fmt.Sprintf("?exclude_team=%s", url.QueryEscape(teamIdToExclude)) + } + + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/teams/unread"+optional, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamsUnreadFromJson(r.Body), BuildResponse(r) +} + +// GetUserAudits returns a list of audit based on the provided user id string. +func (c *Client4) GetUserAudits(userId string, page int, perPage int, etag string) (Audits, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/audits"+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return AuditsFromJson(r.Body), BuildResponse(r) +} + +// VerifyUserEmail will verify a user's email using the supplied token. +func (c *Client4) VerifyUserEmail(token string) (bool, *Response) { + requestBody := map[string]string{"token": token} + r, err := c.DoApiPost(c.GetUsersRoute()+"/email/verify", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// SendVerificationEmail will send an email to the user with the provided email address, if +// that user exists. The email will contain a link that can be used to verify the user's +// email address. +func (c *Client4) SendVerificationEmail(email string) (bool, *Response) { + requestBody := map[string]string{"email": email} + r, err := c.DoApiPost(c.GetUsersRoute()+"/email/verify/send", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// SetDefaultProfileImage resets the profile image to a default generated one. +func (c *Client4) SetDefaultProfileImage(userId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetUserRoute(userId) + "/image") + if err != nil { + return false, BuildErrorResponse(r, err) + } + return CheckStatusOK(r), BuildResponse(r) +} + +// SetProfileImage sets profile image of the user. +func (c *Client4) SetProfileImage(userId string, data []byte) (bool, *Response) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("image", "profile.png") + if err != nil { + return false, &Response{Error: NewAppError("SetProfileImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return false, &Response{Error: NewAppError("SetProfileImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if err = writer.Close(); err != nil { + return false, &Response{Error: NewAppError("SetProfileImage", "model.client.set_profile_user.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + rq, err := http.NewRequest("POST", c.ApiUrl+c.GetUserRoute(userId)+"/image", bytes.NewReader(body.Bytes())) + if err != nil { + return false, &Response{Error: NewAppError("SetProfileImage", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetUserRoute(userId)+"/image", "model.client.connecting.app_error", nil, err.Error(), http.StatusForbidden)} + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return CheckStatusOK(rp), BuildResponse(rp) +} + +// CreateUserAccessToken will generate a user access token that can be used in place +// of a session token to access the REST API. Must have the 'create_user_access_token' +// permission and if generating for another user, must have the 'edit_other_users' +// permission. A non-blank description is required. +func (c *Client4) CreateUserAccessToken(userId, description string) (*UserAccessToken, *Response) { + requestBody := map[string]string{"description": description} + r, err := c.DoApiPost(c.GetUserRoute(userId)+"/tokens", MapToJson(requestBody)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserAccessTokenFromJson(r.Body), BuildResponse(r) +} + +// GetUserAccessTokens will get a page of access tokens' id, description, is_active +// and the user_id in the system. The actual token will not be returned. Must have +// the 'manage_system' permission. +func (c *Client4) GetUserAccessTokens(page int, perPage int) ([]*UserAccessToken, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetUserAccessTokensRoute()+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserAccessTokenListFromJson(r.Body), BuildResponse(r) +} + +// GetUserAccessToken will get a user access tokens' id, description, is_active +// and the user_id of the user it is for. The actual token will not be returned. +// Must have the 'read_user_access_token' permission and if getting for another +// user, must have the 'edit_other_users' permission. +func (c *Client4) GetUserAccessToken(tokenId string) (*UserAccessToken, *Response) { + r, err := c.DoApiGet(c.GetUserAccessTokenRoute(tokenId), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserAccessTokenFromJson(r.Body), BuildResponse(r) +} + +// GetUserAccessTokensForUser will get a paged list of user access tokens showing id, +// description and user_id for each. The actual tokens will not be returned. Must have +// the 'read_user_access_token' permission and if getting for another user, must have the +// 'edit_other_users' permission. +func (c *Client4) GetUserAccessTokensForUser(userId string, page, perPage int) ([]*UserAccessToken, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/tokens"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserAccessTokenListFromJson(r.Body), BuildResponse(r) +} + +// RevokeUserAccessToken will revoke a user access token by id. Must have the +// 'revoke_user_access_token' permission and if revoking for another user, must have the +// 'edit_other_users' permission. +func (c *Client4) RevokeUserAccessToken(tokenId string) (bool, *Response) { + requestBody := map[string]string{"token_id": tokenId} + r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/revoke", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// SearchUserAccessTokens returns user access tokens matching the provided search term. +func (c *Client4) SearchUserAccessTokens(search *UserAccessTokenSearch) ([]*UserAccessToken, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/search", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserAccessTokenListFromJson(r.Body), BuildResponse(r) +} + +// DisableUserAccessToken will disable a user access token by id. Must have the +// 'revoke_user_access_token' permission and if disabling for another user, must have the +// 'edit_other_users' permission. +func (c *Client4) DisableUserAccessToken(tokenId string) (bool, *Response) { + requestBody := map[string]string{"token_id": tokenId} + r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/disable", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// EnableUserAccessToken will enable a user access token by id. Must have the +// 'create_user_access_token' permission and if enabling for another user, must have the +// 'edit_other_users' permission. +func (c *Client4) EnableUserAccessToken(tokenId string) (bool, *Response) { + requestBody := map[string]string{"token_id": tokenId} + r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/enable", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// Bots section + +// CreateBot creates a bot in the system based on the provided bot struct. +func (c *Client4) CreateBot(bot *Bot) (*Bot, *Response) { + r, err := c.doApiPostBytes(c.GetBotsRoute(), bot.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// PatchBot partially updates a bot. Any missing fields are not updated. +func (c *Client4) PatchBot(userId string, patch *BotPatch) (*Bot, *Response) { + r, err := c.doApiPutBytes(c.GetBotRoute(userId), patch.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// GetBot fetches the given, undeleted bot. +func (c *Client4) GetBot(userId string, etag string) (*Bot, *Response) { + r, err := c.DoApiGet(c.GetBotRoute(userId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// GetBot fetches the given bot, even if it is deleted. +func (c *Client4) GetBotIncludeDeleted(userId string, etag string) (*Bot, *Response) { + r, err := c.DoApiGet(c.GetBotRoute(userId)+"?include_deleted=true", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// GetBots fetches the given page of bots, excluding deleted. +func (c *Client4) GetBots(page, perPage int, etag string) ([]*Bot, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetBotsRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotListFromJson(r.Body), BuildResponse(r) +} + +// GetBotsIncludeDeleted fetches the given page of bots, including deleted. +func (c *Client4) GetBotsIncludeDeleted(page, perPage int, etag string) ([]*Bot, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&include_deleted=true", page, perPage) + r, err := c.DoApiGet(c.GetBotsRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotListFromJson(r.Body), BuildResponse(r) +} + +// GetBotsOrphaned fetches the given page of bots, only including orphanded bots. +func (c *Client4) GetBotsOrphaned(page, perPage int, etag string) ([]*Bot, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&only_orphaned=true", page, perPage) + r, err := c.DoApiGet(c.GetBotsRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotListFromJson(r.Body), BuildResponse(r) +} + +// DisableBot disables the given bot in the system. +func (c *Client4) DisableBot(botUserId string) (*Bot, *Response) { + r, err := c.doApiPostBytes(c.GetBotRoute(botUserId)+"/disable", nil) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// EnableBot disables the given bot in the system. +func (c *Client4) EnableBot(botUserId string) (*Bot, *Response) { + r, err := c.doApiPostBytes(c.GetBotRoute(botUserId)+"/enable", nil) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// AssignBot assigns the given bot to the given user +func (c *Client4) AssignBot(botUserId, newOwnerId string) (*Bot, *Response) { + r, err := c.doApiPostBytes(c.GetBotRoute(botUserId)+"/assign/"+newOwnerId, nil) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return BotFromJson(r.Body), BuildResponse(r) +} + +// SetBotIconImage sets LHS bot icon image. +func (c *Client4) SetBotIconImage(botUserId string, data []byte) (bool, *Response) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("image", "icon.svg") + if err != nil { + return false, &Response{Error: NewAppError("SetBotIconImage", "model.client.set_bot_icon_image.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return false, &Response{Error: NewAppError("SetBotIconImage", "model.client.set_bot_icon_image.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if err = writer.Close(); err != nil { + return false, &Response{Error: NewAppError("SetBotIconImage", "model.client.set_bot_icon_image.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + rq, err := http.NewRequest("POST", c.ApiUrl+c.GetBotRoute(botUserId)+"/icon", bytes.NewReader(body.Bytes())) + if err != nil { + return false, &Response{Error: NewAppError("SetBotIconImage", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetBotRoute(botUserId)+"/icon", "model.client.connecting.app_error", nil, err.Error(), http.StatusForbidden)} + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return CheckStatusOK(rp), BuildResponse(rp) +} + +// GetBotIconImage gets LHS bot icon image. Must be logged in. +func (c *Client4) GetBotIconImage(botUserId string) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetBotRoute(botUserId)+"/icon", "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetBotIconImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + return data, BuildResponse(r) +} + +// DeleteBotIconImage deletes LHS bot icon image. Must be logged in. +func (c *Client4) DeleteBotIconImage(botUserId string) (bool, *Response) { + r, appErr := c.DoApiDelete(c.GetBotRoute(botUserId) + "/icon") + if appErr != nil { + return false, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// Team Section + +// CreateTeam creates a team in the system based on the provided team struct. +func (c *Client4) CreateTeam(team *Team) (*Team, *Response) { + r, err := c.DoApiPost(c.GetTeamsRoute(), team.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamFromJson(r.Body), BuildResponse(r) +} + +// GetTeam returns a team based on the provided team id string. +func (c *Client4) GetTeam(teamId, etag string) (*Team, *Response) { + r, err := c.DoApiGet(c.GetTeamRoute(teamId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamFromJson(r.Body), BuildResponse(r) +} + +// GetAllTeams returns all teams based on permissions. +func (c *Client4) GetAllTeams(etag string, page int, perPage int) ([]*Team, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetTeamsRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamListFromJson(r.Body), BuildResponse(r) +} + +// GetAllTeamsWithTotalCount returns all teams based on permissions. +func (c *Client4) GetAllTeamsWithTotalCount(etag string, page int, perPage int) ([]*Team, int64, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&include_total_count=true", page, perPage) + r, err := c.DoApiGet(c.GetTeamsRoute()+query, etag) + if err != nil { + return nil, 0, BuildErrorResponse(r, err) + } + defer closeBody(r) + teamsListWithCount := TeamsWithCountFromJson(r.Body) + return teamsListWithCount.Teams, teamsListWithCount.TotalCount, BuildResponse(r) +} + +// GetTeamByName returns a team based on the provided team name string. +func (c *Client4) GetTeamByName(name, etag string) (*Team, *Response) { + r, err := c.DoApiGet(c.GetTeamByNameRoute(name), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamFromJson(r.Body), BuildResponse(r) +} + +// SearchTeams returns teams matching the provided search term. +func (c *Client4) SearchTeams(search *TeamSearch) ([]*Team, *Response) { + r, err := c.DoApiPost(c.GetTeamsRoute()+"/search", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamListFromJson(r.Body), BuildResponse(r) +} + +// SearchTeamsPaged returns a page of teams and the total count matching the provided search term. +func (c *Client4) SearchTeamsPaged(search *TeamSearch) ([]*Team, int64, *Response) { + if search.Page == nil { + search.Page = NewInt(0) + } + if search.PerPage == nil { + search.PerPage = NewInt(100) + } + r, err := c.DoApiPost(c.GetTeamsRoute()+"/search", search.ToJson()) + if err != nil { + return nil, 0, BuildErrorResponse(r, err) + } + defer closeBody(r) + twc := TeamsWithCountFromJson(r.Body) + return twc.Teams, twc.TotalCount, BuildResponse(r) +} + +// TeamExists returns true or false if the team exist or not. +func (c *Client4) TeamExists(name, etag string) (bool, *Response) { + r, err := c.DoApiGet(c.GetTeamByNameRoute(name)+"/exists", etag) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapBoolFromJson(r.Body)["exists"], BuildResponse(r) +} + +// GetTeamsForUser returns a list of teams a user is on. Must be logged in as the user +// or be a system administrator. +func (c *Client4) GetTeamsForUser(userId, etag string) ([]*Team, *Response) { + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/teams", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamListFromJson(r.Body), BuildResponse(r) +} + +// GetTeamMember returns a team member based on the provided team and user id strings. +func (c *Client4) GetTeamMember(teamId, userId, etag string) (*TeamMember, *Response) { + r, err := c.DoApiGet(c.GetTeamMemberRoute(teamId, userId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMemberFromJson(r.Body), BuildResponse(r) +} + +// UpdateTeamMemberRoles will update the roles on a team for a user. +func (c *Client4) UpdateTeamMemberRoles(teamId, userId, newRoles string) (bool, *Response) { + requestBody := map[string]string{"roles": newRoles} + r, err := c.DoApiPut(c.GetTeamMemberRoute(teamId, userId)+"/roles", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UpdateTeamMemberSchemeRoles will update the scheme-derived roles on a team for a user. +func (c *Client4) UpdateTeamMemberSchemeRoles(teamId string, userId string, schemeRoles *SchemeRoles) (bool, *Response) { + r, err := c.DoApiPut(c.GetTeamMemberRoute(teamId, userId)+"/schemeRoles", schemeRoles.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UpdateTeam will update a team. +func (c *Client4) UpdateTeam(team *Team) (*Team, *Response) { + r, err := c.DoApiPut(c.GetTeamRoute(team.Id), team.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamFromJson(r.Body), BuildResponse(r) +} + +// PatchTeam partially updates a team. Any missing fields are not updated. +func (c *Client4) PatchTeam(teamId string, patch *TeamPatch) (*Team, *Response) { + r, err := c.DoApiPut(c.GetTeamRoute(teamId)+"/patch", patch.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamFromJson(r.Body), BuildResponse(r) +} + +// RestoreTeam restores a previously deleted team. +func (c *Client4) RestoreTeam(teamId string) (*Team, *Response) { + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/restore", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamFromJson(r.Body), BuildResponse(r) +} + +// RegenerateTeamInviteId requests a new invite ID to be generated. +func (c *Client4) RegenerateTeamInviteId(teamId string) (*Team, *Response) { + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/regenerate_invite_id", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamFromJson(r.Body), BuildResponse(r) +} + +// SoftDeleteTeam deletes the team softly (archive only, not permanent delete). +func (c *Client4) SoftDeleteTeam(teamId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetTeamRoute(teamId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// PermanentDeleteTeam deletes the team, should only be used when needed for +// compliance and the like. +func (c *Client4) PermanentDeleteTeam(teamId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetTeamRoute(teamId) + "?permanent=true") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UpdateTeamPrivacy modifies the team type (model.TEAM_OPEN <--> model.TEAM_INVITE) and sets +// the corresponding AllowOpenInvite appropriately. +func (c *Client4) UpdateTeamPrivacy(teamId string, privacy string) (*Team, *Response) { + requestBody := map[string]string{"privacy": privacy} + r, err := c.DoApiPut(c.GetTeamRoute(teamId)+"/privacy", MapToJson(requestBody)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamFromJson(r.Body), BuildResponse(r) +} + +// GetTeamMembers returns team members based on the provided team id string. +func (c *Client4) GetTeamMembers(teamId string, page int, perPage int, etag string) ([]*TeamMember, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetTeamMembersRoute(teamId)+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMembersFromJson(r.Body), BuildResponse(r) +} + +// GetTeamMembersWithoutDeletedUsers returns team members based on the provided team id string. Additional parameters of sort and exclude_deleted_users accepted as well +// Could not add it to above function due to it be a breaking change. +func (c *Client4) GetTeamMembersSortAndWithoutDeletedUsers(teamId string, page int, perPage int, sort string, exclude_deleted_users bool, etag string) ([]*TeamMember, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&sort=%v&exclude_deleted_users=%v", page, perPage, sort, exclude_deleted_users) + r, err := c.DoApiGet(c.GetTeamMembersRoute(teamId)+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMembersFromJson(r.Body), BuildResponse(r) +} + +// GetTeamMembersForUser returns the team members for a user. +func (c *Client4) GetTeamMembersForUser(userId string, etag string) ([]*TeamMember, *Response) { + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/teams/members", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMembersFromJson(r.Body), BuildResponse(r) +} + +// GetTeamMembersByIds will return an array of team members based on the +// team id and a list of user ids provided. Must be authenticated. +func (c *Client4) GetTeamMembersByIds(teamId string, userIds []string) ([]*TeamMember, *Response) { + r, err := c.DoApiPost(fmt.Sprintf("/teams/%v/members/ids", teamId), ArrayToJson(userIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMembersFromJson(r.Body), BuildResponse(r) +} + +// AddTeamMember adds user to a team and return a team member. +func (c *Client4) AddTeamMember(teamId, userId string) (*TeamMember, *Response) { + member := &TeamMember{TeamId: teamId, UserId: userId} + r, err := c.DoApiPost(c.GetTeamMembersRoute(teamId), member.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMemberFromJson(r.Body), BuildResponse(r) +} + +// AddTeamMemberFromInvite adds a user to a team and return a team member using an invite id +// or an invite token/data pair. +func (c *Client4) AddTeamMemberFromInvite(token, inviteId string) (*TeamMember, *Response) { + var query string + + if inviteId != "" { + query += fmt.Sprintf("?invite_id=%v", inviteId) + } + + if token != "" { + query += fmt.Sprintf("?token=%v", token) + } + + r, err := c.DoApiPost(c.GetTeamsRoute()+"/members/invite"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMemberFromJson(r.Body), BuildResponse(r) +} + +// AddTeamMembers adds a number of users to a team and returns the team members. +func (c *Client4) AddTeamMembers(teamId string, userIds []string) ([]*TeamMember, *Response) { + var members []*TeamMember + for _, userId := range userIds { + member := &TeamMember{TeamId: teamId, UserId: userId} + members = append(members, member) + } + + r, err := c.DoApiPost(c.GetTeamMembersRoute(teamId)+"/batch", TeamMembersToJson(members)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMembersFromJson(r.Body), BuildResponse(r) +} + +// AddTeamMembers adds a number of users to a team and returns the team members. +func (c *Client4) AddTeamMembersGracefully(teamId string, userIds []string) ([]*TeamMemberWithError, *Response) { + var members []*TeamMember + for _, userId := range userIds { + member := &TeamMember{TeamId: teamId, UserId: userId} + members = append(members, member) + } + + r, err := c.DoApiPost(c.GetTeamMembersRoute(teamId)+"/batch?graceful=true", TeamMembersToJson(members)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMembersWithErrorFromJson(r.Body), BuildResponse(r) +} + +// RemoveTeamMember will remove a user from a team. +func (c *Client4) RemoveTeamMember(teamId, userId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetTeamMemberRoute(teamId, userId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetTeamStats returns a team stats based on the team id string. +// Must be authenticated. +func (c *Client4) GetTeamStats(teamId, etag string) (*TeamStats, *Response) { + r, err := c.DoApiGet(c.GetTeamStatsRoute(teamId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamStatsFromJson(r.Body), BuildResponse(r) +} + +// GetTotalUsersStats returns a total system user stats. +// Must be authenticated. +func (c *Client4) GetTotalUsersStats(etag string) (*UsersStats, *Response) { + r, err := c.DoApiGet(c.GetTotalUsersStatsRoute(), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UsersStatsFromJson(r.Body), BuildResponse(r) +} + +// GetTeamUnread will return a TeamUnread object that contains the amount of +// unread messages and mentions the user has for the specified team. +// Must be authenticated. +func (c *Client4) GetTeamUnread(teamId, userId string) (*TeamUnread, *Response) { + r, err := c.DoApiGet(c.GetUserRoute(userId)+c.GetTeamRoute(teamId)+"/unread", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamUnreadFromJson(r.Body), BuildResponse(r) +} + +// ImportTeam will import an exported team from other app into a existing team. +func (c *Client4) ImportTeam(data []byte, filesize int, importFrom, filename, teamId string) (map[string]string, *Response) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("file", filename) + if err != nil { + return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + part, err = writer.CreateFormField("filesize") + if err != nil { + return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.file_size.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if _, err = io.Copy(part, strings.NewReader(strconv.Itoa(filesize))); err != nil { + return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.file_size.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + part, err = writer.CreateFormField("importFrom") + if err != nil { + return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.import_from.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if _, err := io.Copy(part, strings.NewReader(importFrom)); err != nil { + return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.import_from.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if err := writer.Close(); err != nil { + return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + return c.DoUploadImportTeam(c.GetTeamImportRoute(teamId), body.Bytes(), writer.FormDataContentType()) +} + +// InviteUsersToTeam invite users by email to the team. +func (c *Client4) InviteUsersToTeam(teamId string, userEmails []string) (bool, *Response) { + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/invite/email", ArrayToJson(userEmails)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// InviteGuestsToTeam invite guest by email to some channels in a team. +func (c *Client4) InviteGuestsToTeam(teamId string, userEmails []string, channels []string, message string) (bool, *Response) { + guestsInvite := GuestsInvite{ + Emails: userEmails, + Channels: channels, + Message: message, + } + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/invite-guests/email", guestsInvite.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// InviteUsersToTeam invite users by email to the team. +func (c *Client4) InviteUsersToTeamGracefully(teamId string, userEmails []string) ([]*EmailInviteWithError, *Response) { + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/invite/email?graceful=true", ArrayToJson(userEmails)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmailInviteWithErrorFromJson(r.Body), BuildResponse(r) +} + +// InviteGuestsToTeam invite guest by email to some channels in a team. +func (c *Client4) InviteGuestsToTeamGracefully(teamId string, userEmails []string, channels []string, message string) ([]*EmailInviteWithError, *Response) { + guestsInvite := GuestsInvite{ + Emails: userEmails, + Channels: channels, + Message: message, + } + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/invite-guests/email?graceful=true", guestsInvite.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmailInviteWithErrorFromJson(r.Body), BuildResponse(r) +} + +// InvalidateEmailInvites will invalidate active email invitations that have not been accepted by the user. +func (c *Client4) InvalidateEmailInvites() (bool, *Response) { + r, err := c.DoApiDelete(c.GetTeamsRoute() + "/invites/email") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetTeamInviteInfo returns a team object from an invite id containing sanitized information. +func (c *Client4) GetTeamInviteInfo(inviteId string) (*Team, *Response) { + r, err := c.DoApiGet(c.GetTeamsRoute()+"/invite/"+inviteId, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamFromJson(r.Body), BuildResponse(r) +} + +// SetTeamIcon sets team icon of the team. +func (c *Client4) SetTeamIcon(teamId string, data []byte) (bool, *Response) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("image", "teamIcon.png") + if err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if err = writer.Close(); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + rq, err := http.NewRequest("POST", c.ApiUrl+c.GetTeamRoute(teamId)+"/image", bytes.NewReader(body.Bytes())) + if err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + // set to http.StatusForbidden(403) + return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetTeamRoute(teamId)+"/image", "model.client.connecting.app_error", nil, err.Error(), 403)} + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return CheckStatusOK(rp), BuildResponse(rp) +} + +// GetTeamIcon gets the team icon of the team. +func (c *Client4) GetTeamIcon(teamId, etag string) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetTeamRoute(teamId)+"/image", etag) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetTeamIcon", "model.client.get_team_icon.app_error", nil, err.Error(), r.StatusCode)) + } + return data, BuildResponse(r) +} + +// RemoveTeamIcon updates LastTeamIconUpdate to 0 which indicates team icon is removed. +func (c *Client4) RemoveTeamIcon(teamId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetTeamRoute(teamId) + "/image") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// Channel Section + +// GetAllChannels get all the channels. Must be a system administrator. +func (c *Client4) GetAllChannels(page int, perPage int, etag string) (*ChannelListWithTeamData, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetChannelsRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelListWithTeamDataFromJson(r.Body), BuildResponse(r) +} + +// GetAllChannelsWithCount get all the channels including the total count. Must be a system administrator. +func (c *Client4) GetAllChannelsWithCount(page int, perPage int, etag string) (*ChannelListWithTeamData, int64, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&include_total_count=true", page, perPage) + r, err := c.DoApiGet(c.GetChannelsRoute()+query, etag) + if err != nil { + return nil, 0, BuildErrorResponse(r, err) + } + defer closeBody(r) + cwc := ChannelsWithCountFromJson(r.Body) + return cwc.Channels, cwc.TotalCount, BuildResponse(r) +} + +// CreateChannel creates a channel based on the provided channel struct. +func (c *Client4) CreateChannel(channel *Channel) (*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelsRoute(), channel.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// UpdateChannel updates a channel based on the provided channel struct. +func (c *Client4) UpdateChannel(channel *Channel) (*Channel, *Response) { + r, err := c.DoApiPut(c.GetChannelRoute(channel.Id), channel.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// PatchChannel partially updates a channel. Any missing fields are not updated. +func (c *Client4) PatchChannel(channelId string, patch *ChannelPatch) (*Channel, *Response) { + r, err := c.DoApiPut(c.GetChannelRoute(channelId)+"/patch", patch.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// ConvertChannelToPrivate converts public to private channel. +func (c *Client4) ConvertChannelToPrivate(channelId string) (*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/convert", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// UpdateChannelPrivacy updates channel privacy +func (c *Client4) UpdateChannelPrivacy(channelId string, privacy string) (*Channel, *Response) { + requestBody := map[string]string{"privacy": privacy} + r, err := c.DoApiPut(c.GetChannelRoute(channelId)+"/privacy", MapToJson(requestBody)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// RestoreChannel restores a previously deleted channel. Any missing fields are not updated. +func (c *Client4) RestoreChannel(channelId string) (*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/restore", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// CreateDirectChannel creates a direct message channel based on the two user +// ids provided. +func (c *Client4) CreateDirectChannel(userId1, userId2 string) (*Channel, *Response) { + requestBody := []string{userId1, userId2} + r, err := c.DoApiPost(c.GetChannelsRoute()+"/direct", ArrayToJson(requestBody)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// CreateGroupChannel creates a group message channel based on userIds provided. +func (c *Client4) CreateGroupChannel(userIds []string) (*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelsRoute()+"/group", ArrayToJson(userIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// GetChannel returns a channel based on the provided channel id string. +func (c *Client4) GetChannel(channelId, etag string) (*Channel, *Response) { + r, err := c.DoApiGet(c.GetChannelRoute(channelId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// GetChannelStats returns statistics for a channel. +func (c *Client4) GetChannelStats(channelId string, etag string) (*ChannelStats, *Response) { + r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/stats", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelStatsFromJson(r.Body), BuildResponse(r) +} + +// GetChannelMembersTimezones gets a list of timezones for a channel. +func (c *Client4) GetChannelMembersTimezones(channelId string) ([]string, *Response) { + r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/timezones", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ArrayFromJson(r.Body), BuildResponse(r) +} + +// GetPinnedPosts gets a list of pinned posts. +func (c *Client4) GetPinnedPosts(channelId string, etag string) (*PostList, *Response) { + r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/pinned", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// GetPublicChannelsForTeam returns a list of public channels based on the provided team id string. +func (c *Client4) GetPublicChannelsForTeam(teamId string, page int, perPage int, etag string) ([]*Channel, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelSliceFromJson(r.Body), BuildResponse(r) +} + +// GetDeletedChannelsForTeam returns a list of public channels based on the provided team id string. +func (c *Client4) GetDeletedChannelsForTeam(teamId string, page int, perPage int, etag string) ([]*Channel, *Response) { + query := fmt.Sprintf("/deleted?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelSliceFromJson(r.Body), BuildResponse(r) +} + +// GetPublicChannelsByIdsForTeam returns a list of public channels based on provided team id string. +func (c *Client4) GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) ([]*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelsForTeamRoute(teamId)+"/ids", ArrayToJson(channelIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelSliceFromJson(r.Body), BuildResponse(r) +} + +// GetChannelsForTeamForUser returns a list channels of on a team for a user. +func (c *Client4) GetChannelsForTeamForUser(teamId, userId string, includeDeleted bool, etag string) ([]*Channel, *Response) { + r, err := c.DoApiGet(c.GetChannelsForTeamForUserRoute(teamId, userId, includeDeleted), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelSliceFromJson(r.Body), BuildResponse(r) +} + +// SearchChannels returns the channels on a team matching the provided search term. +func (c *Client4) SearchChannels(teamId string, search *ChannelSearch) ([]*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelsForTeamRoute(teamId)+"/search", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelSliceFromJson(r.Body), BuildResponse(r) +} + +// SearchArchivedChannels returns the archived channels on a team matching the provided search term. +func (c *Client4) SearchArchivedChannels(teamId string, search *ChannelSearch) ([]*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelsForTeamRoute(teamId)+"/search_archived", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelSliceFromJson(r.Body), BuildResponse(r) +} + +// SearchAllChannels search in all the channels. Must be a system administrator. +func (c *Client4) SearchAllChannels(search *ChannelSearch) (*ChannelListWithTeamData, *Response) { + r, err := c.DoApiPost(c.GetChannelsRoute()+"/search", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelListWithTeamDataFromJson(r.Body), BuildResponse(r) +} + +// SearchAllChannelsPaged searches all the channels and returns the results paged with the total count. +func (c *Client4) SearchAllChannelsPaged(search *ChannelSearch) (*ChannelsWithCount, *Response) { + r, err := c.DoApiPost(c.GetChannelsRoute()+"/search", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelsWithCountFromJson(r.Body), BuildResponse(r) +} + +// SearchGroupChannels returns the group channels of the user whose members' usernames match the search term. +func (c *Client4) SearchGroupChannels(search *ChannelSearch) ([]*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelsRoute()+"/group/search", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelSliceFromJson(r.Body), BuildResponse(r) +} + +// DeleteChannel deletes channel based on the provided channel id string. +func (c *Client4) DeleteChannel(channelId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetChannelRoute(channelId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetChannelByName returns a channel based on the provided channel name and team id strings. +func (c *Client4) GetChannelByName(channelName, teamId string, etag string) (*Channel, *Response) { + r, err := c.DoApiGet(c.GetChannelByNameRoute(channelName, teamId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// GetChannelByNameIncludeDeleted returns a channel based on the provided channel name and team id strings. Other then GetChannelByName it will also return deleted channels. +func (c *Client4) GetChannelByNameIncludeDeleted(channelName, teamId string, etag string) (*Channel, *Response) { + r, err := c.DoApiGet(c.GetChannelByNameRoute(channelName, teamId)+"?include_deleted=true", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// GetChannelByNameForTeamName returns a channel based on the provided channel name and team name strings. +func (c *Client4) GetChannelByNameForTeamName(channelName, teamName string, etag string) (*Channel, *Response) { + r, err := c.DoApiGet(c.GetChannelByNameForTeamNameRoute(channelName, teamName), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// GetChannelByNameForTeamNameIncludeDeleted returns a channel based on the provided channel name and team name strings. Other then GetChannelByNameForTeamName it will also return deleted channels. +func (c *Client4) GetChannelByNameForTeamNameIncludeDeleted(channelName, teamName string, etag string) (*Channel, *Response) { + r, err := c.DoApiGet(c.GetChannelByNameForTeamNameRoute(channelName, teamName)+"?include_deleted=true", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelFromJson(r.Body), BuildResponse(r) +} + +// GetChannelMembers gets a page of channel members. +func (c *Client4) GetChannelMembers(channelId string, page, perPage int, etag string) (*ChannelMembers, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetChannelMembersRoute(channelId)+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelMembersFromJson(r.Body), BuildResponse(r) +} + +// GetChannelMembersByIds gets the channel members in a channel for a list of user ids. +func (c *Client4) GetChannelMembersByIds(channelId string, userIds []string) (*ChannelMembers, *Response) { + r, err := c.DoApiPost(c.GetChannelMembersRoute(channelId)+"/ids", ArrayToJson(userIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelMembersFromJson(r.Body), BuildResponse(r) +} + +// GetChannelMember gets a channel member. +func (c *Client4) GetChannelMember(channelId, userId, etag string) (*ChannelMember, *Response) { + r, err := c.DoApiGet(c.GetChannelMemberRoute(channelId, userId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelMemberFromJson(r.Body), BuildResponse(r) +} + +// GetChannelMembersForUser gets all the channel members for a user on a team. +func (c *Client4) GetChannelMembersForUser(userId, teamId, etag string) (*ChannelMembers, *Response) { + r, err := c.DoApiGet(fmt.Sprintf(c.GetUserRoute(userId)+"/teams/%v/channels/members", teamId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelMembersFromJson(r.Body), BuildResponse(r) +} + +// ViewChannel performs a view action for a user. Synonymous with switching channels or marking channels as read by a user. +func (c *Client4) ViewChannel(userId string, view *ChannelView) (*ChannelViewResponse, *Response) { + url := fmt.Sprintf(c.GetChannelsRoute()+"/members/%v/view", userId) + r, err := c.DoApiPost(url, view.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelViewResponseFromJson(r.Body), BuildResponse(r) +} + +// GetChannelUnread will return a ChannelUnread object that contains the number of +// unread messages and mentions for a user. +func (c *Client4) GetChannelUnread(channelId, userId string) (*ChannelUnread, *Response) { + r, err := c.DoApiGet(c.GetUserRoute(userId)+c.GetChannelRoute(channelId)+"/unread", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelUnreadFromJson(r.Body), BuildResponse(r) +} + +// UpdateChannelRoles will update the roles on a channel for a user. +func (c *Client4) UpdateChannelRoles(channelId, userId, roles string) (bool, *Response) { + requestBody := map[string]string{"roles": roles} + r, err := c.DoApiPut(c.GetChannelMemberRoute(channelId, userId)+"/roles", MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UpdateChannelMemberSchemeRoles will update the scheme-derived roles on a channel for a user. +func (c *Client4) UpdateChannelMemberSchemeRoles(channelId string, userId string, schemeRoles *SchemeRoles) (bool, *Response) { + r, err := c.DoApiPut(c.GetChannelMemberRoute(channelId, userId)+"/schemeRoles", schemeRoles.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UpdateChannelNotifyProps will update the notification properties on a channel for a user. +func (c *Client4) UpdateChannelNotifyProps(channelId, userId string, props map[string]string) (bool, *Response) { + r, err := c.DoApiPut(c.GetChannelMemberRoute(channelId, userId)+"/notify_props", MapToJson(props)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// AddChannelMember adds user to channel and return a channel member. +func (c *Client4) AddChannelMember(channelId, userId string) (*ChannelMember, *Response) { + requestBody := map[string]string{"user_id": userId} + r, err := c.DoApiPost(c.GetChannelMembersRoute(channelId)+"", MapToJson(requestBody)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelMemberFromJson(r.Body), BuildResponse(r) +} + +// AddChannelMemberWithRootId adds user to channel and return a channel member. Post add to channel message has the postRootId. +func (c *Client4) AddChannelMemberWithRootId(channelId, userId, postRootId string) (*ChannelMember, *Response) { + requestBody := map[string]string{"user_id": userId, "post_root_id": postRootId} + r, err := c.DoApiPost(c.GetChannelMembersRoute(channelId)+"", MapToJson(requestBody)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelMemberFromJson(r.Body), BuildResponse(r) +} + +// RemoveUserFromChannel will delete the channel member object for a user, effectively removing the user from a channel. +func (c *Client4) RemoveUserFromChannel(channelId, userId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetChannelMemberRoute(channelId, userId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// AutocompleteChannelsForTeam will return an ordered list of channels autocomplete suggestions. +func (c *Client4) AutocompleteChannelsForTeam(teamId, name string) (*ChannelList, *Response) { + query := fmt.Sprintf("?name=%v", name) + r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+"/autocomplete"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelListFromJson(r.Body), BuildResponse(r) +} + +// AutocompleteChannelsForTeamForSearch will return an ordered list of your channels autocomplete suggestions. +func (c *Client4) AutocompleteChannelsForTeamForSearch(teamId, name string) (*ChannelList, *Response) { + query := fmt.Sprintf("?name=%v", name) + r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+"/search_autocomplete"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelListFromJson(r.Body), BuildResponse(r) +} + +// Post Section + +// CreatePost creates a post based on the provided post struct. +func (c *Client4) CreatePost(post *Post) (*Post, *Response) { + r, err := c.DoApiPost(c.GetPostsRoute(), post.ToUnsanitizedJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostFromJson(r.Body), BuildResponse(r) +} + +// CreatePostEphemeral creates a ephemeral post based on the provided post struct which is send to the given user id. +func (c *Client4) CreatePostEphemeral(post *PostEphemeral) (*Post, *Response) { + r, err := c.DoApiPost(c.GetPostsEphemeralRoute(), post.ToUnsanitizedJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostFromJson(r.Body), BuildResponse(r) +} + +// UpdatePost updates a post based on the provided post struct. +func (c *Client4) UpdatePost(postId string, post *Post) (*Post, *Response) { + r, err := c.DoApiPut(c.GetPostRoute(postId), post.ToUnsanitizedJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostFromJson(r.Body), BuildResponse(r) +} + +// PatchPost partially updates a post. Any missing fields are not updated. +func (c *Client4) PatchPost(postId string, patch *PostPatch) (*Post, *Response) { + r, err := c.DoApiPut(c.GetPostRoute(postId)+"/patch", patch.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostFromJson(r.Body), BuildResponse(r) +} + +// SetPostUnread marks channel where post belongs as unread on the time of the provided post. +func (c *Client4) SetPostUnread(userId string, postId string) *Response { + r, err := c.DoApiPost(c.GetUserRoute(userId)+c.GetPostRoute(postId)+"/set_unread", "") + if err != nil { + return BuildErrorResponse(r, err) + } + defer closeBody(r) + return BuildResponse(r) +} + +// PinPost pin a post based on provided post id string. +func (c *Client4) PinPost(postId string) (bool, *Response) { + r, err := c.DoApiPost(c.GetPostRoute(postId)+"/pin", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UnpinPost unpin a post based on provided post id string. +func (c *Client4) UnpinPost(postId string) (bool, *Response) { + r, err := c.DoApiPost(c.GetPostRoute(postId)+"/unpin", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetPost gets a single post. +func (c *Client4) GetPost(postId string, etag string) (*Post, *Response) { + r, err := c.DoApiGet(c.GetPostRoute(postId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostFromJson(r.Body), BuildResponse(r) +} + +// DeletePost deletes a post from the provided post id string. +func (c *Client4) DeletePost(postId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetPostRoute(postId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetPostThread gets a post with all the other posts in the same thread. +func (c *Client4) GetPostThread(postId string, etag string) (*PostList, *Response) { + r, err := c.DoApiGet(c.GetPostRoute(postId)+"/thread", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// GetPostsForChannel gets a page of posts with an array for ordering for a channel. +func (c *Client4) GetPostsForChannel(channelId string, page, perPage int, etag string) (*PostList, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/posts"+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// GetFlaggedPostsForUser returns flagged posts of a user based on user id string. +func (c *Client4) GetFlaggedPostsForUser(userId string, page int, perPage int) (*PostList, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/posts/flagged"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// GetFlaggedPostsForUserInTeam returns flagged posts in team of a user based on user id string. +func (c *Client4) GetFlaggedPostsForUserInTeam(userId string, teamId string, page int, perPage int) (*PostList, *Response) { + if !IsValidId(teamId) { + return nil, &Response{StatusCode: http.StatusBadRequest, Error: NewAppError("GetFlaggedPostsForUserInTeam", "model.client.get_flagged_posts_in_team.missing_parameter.app_error", nil, "", http.StatusBadRequest)} + } + + query := fmt.Sprintf("?team_id=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/posts/flagged"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// GetFlaggedPostsForUserInChannel returns flagged posts in channel of a user based on user id string. +func (c *Client4) GetFlaggedPostsForUserInChannel(userId string, channelId string, page int, perPage int) (*PostList, *Response) { + if !IsValidId(channelId) { + return nil, &Response{StatusCode: http.StatusBadRequest, Error: NewAppError("GetFlaggedPostsForUserInChannel", "model.client.get_flagged_posts_in_channel.missing_parameter.app_error", nil, "", http.StatusBadRequest)} + } + + query := fmt.Sprintf("?channel_id=%v&page=%v&per_page=%v", channelId, page, perPage) + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/posts/flagged"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// GetPostsSince gets posts created after a specified time as Unix time in milliseconds. +func (c *Client4) GetPostsSince(channelId string, time int64) (*PostList, *Response) { + query := fmt.Sprintf("?since=%v", time) + r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/posts"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// GetPostsAfter gets a page of posts that were posted after the post provided. +func (c *Client4) GetPostsAfter(channelId, postId string, page, perPage int, etag string) (*PostList, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&after=%v", page, perPage, postId) + r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/posts"+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// GetPostsBefore gets a page of posts that were posted before the post provided. +func (c *Client4) GetPostsBefore(channelId, postId string, page, perPage int, etag string) (*PostList, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&before=%v", page, perPage, postId) + r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/posts"+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// GetPostsAroundLastUnread gets a list of posts around last unread post by a user in a channel. +func (c *Client4) GetPostsAroundLastUnread(userId, channelId string, limitBefore, limitAfter int) (*PostList, *Response) { + query := fmt.Sprintf("?limit_before=%v&limit_after=%v", limitBefore, limitAfter) + if r, err := c.DoApiGet(c.GetUserRoute(userId)+c.GetChannelRoute(channelId)+"/posts/unread"+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) + } +} + +// SearchPosts returns any posts with matching terms string. +func (c *Client4) SearchPosts(teamId string, terms string, isOrSearch bool) (*PostList, *Response) { + params := SearchParameter{ + Terms: &terms, + IsOrSearch: &isOrSearch, + } + return c.SearchPostsWithParams(teamId, ¶ms) +} + +// SearchPostsWithParams returns any posts with matching terms string. +func (c *Client4) SearchPostsWithParams(teamId string, params *SearchParameter) (*PostList, *Response) { + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/posts/search", params.SearchParameterToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) +} + +// SearchPostsWithMatches returns any posts with matching terms string, including. +func (c *Client4) SearchPostsWithMatches(teamId string, terms string, isOrSearch bool) (*PostSearchResults, *Response) { + requestBody := map[string]interface{}{"terms": terms, "is_or_search": isOrSearch} + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/posts/search", StringInterfaceToJson(requestBody)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PostSearchResultsFromJson(r.Body), BuildResponse(r) +} + +// DoPostAction performs a post action. +func (c *Client4) DoPostAction(postId, actionId string) (bool, *Response) { + r, err := c.DoApiPost(c.GetPostRoute(postId)+"/actions/"+actionId, "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// DoPostActionWithCookie performs a post action with extra arguments +func (c *Client4) DoPostActionWithCookie(postId, actionId, selected, cookieStr string) (bool, *Response) { + var body []byte + if selected != "" || cookieStr != "" { + body, _ = json.Marshal(DoPostActionRequest{ + SelectedOption: selected, + Cookie: cookieStr, + }) + } + r, err := c.DoApiPost(c.GetPostRoute(postId)+"/actions/"+actionId, string(body)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// OpenInteractiveDialog sends a WebSocket event to a user's clients to +// open interactive dialogs, based on the provided trigger ID and other +// provided data. Used with interactive message buttons, menus and +// slash commands. +func (c *Client4) OpenInteractiveDialog(request OpenDialogRequest) (bool, *Response) { + b, _ := json.Marshal(request) + r, err := c.DoApiPost("/actions/dialogs/open", string(b)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// SubmitInteractiveDialog will submit the provided dialog data to the integration +// configured by the URL. Used with the interactive dialogs integration feature. +func (c *Client4) SubmitInteractiveDialog(request SubmitDialogRequest) (*SubmitDialogResponse, *Response) { + b, _ := json.Marshal(request) + r, err := c.DoApiPost("/actions/dialogs/submit", string(b)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + var resp SubmitDialogResponse + json.NewDecoder(r.Body).Decode(&resp) + return &resp, BuildResponse(r) +} + +// UploadFile will upload a file to a channel using a multipart request, to be later attached to a post. +// This method is functionally equivalent to Client4.UploadFileAsRequestBody. +func (c *Client4) UploadFile(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormField("channel_id") + if err != nil { + return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + _, err = io.Copy(part, strings.NewReader(channelId)) + if err != nil { + return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + part, err = writer.CreateFormFile("files", filename) + if err != nil { + return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + _, err = io.Copy(part, bytes.NewBuffer(data)) + if err != nil { + return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + err = writer.Close() + if err != nil { + return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + return c.DoUploadFile(c.GetFilesRoute(), body.Bytes(), writer.FormDataContentType()) +} + +// UploadFileAsRequestBody will upload a file to a channel as the body of a request, to be later attached +// to a post. This method is functionally equivalent to Client4.UploadFile. +func (c *Client4) UploadFileAsRequestBody(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) { + return c.DoUploadFile(c.GetFilesRoute()+fmt.Sprintf("?channel_id=%v&filename=%v", url.QueryEscape(channelId), url.QueryEscape(filename)), data, http.DetectContentType(data)) +} + +// GetFile gets the bytes for a file by id. +func (c *Client4) GetFile(fileId string) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetFileRoute(fileId), "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetFile", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + return data, BuildResponse(r) +} + +// DownloadFile gets the bytes for a file by id, optionally adding headers to force the browser to download it. +func (c *Client4) DownloadFile(fileId string, download bool) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+fmt.Sprintf("?download=%v", download), "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("DownloadFile", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + return data, BuildResponse(r) +} + +// GetFileThumbnail gets the bytes for a file by id. +func (c *Client4) GetFileThumbnail(fileId string) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+"/thumbnail", "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetFileThumbnail", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + return data, BuildResponse(r) +} + +// DownloadFileThumbnail gets the bytes for a file by id, optionally adding headers to force the browser to download it. +func (c *Client4) DownloadFileThumbnail(fileId string, download bool) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+fmt.Sprintf("/thumbnail?download=%v", download), "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("DownloadFileThumbnail", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + return data, BuildResponse(r) +} + +// GetFileLink gets the public link of a file by id. +func (c *Client4) GetFileLink(fileId string) (string, *Response) { + r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/link", "") + if err != nil { + return "", BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body)["link"], BuildResponse(r) +} + +// GetFilePreview gets the bytes for a file by id. +func (c *Client4) GetFilePreview(fileId string) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+"/preview", "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetFilePreview", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + return data, BuildResponse(r) +} + +// DownloadFilePreview gets the bytes for a file by id. +func (c *Client4) DownloadFilePreview(fileId string, download bool) ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+fmt.Sprintf("/preview?download=%v", download), "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("DownloadFilePreview", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + return data, BuildResponse(r) +} + +// GetFileInfo gets all the file info objects. +func (c *Client4) GetFileInfo(fileId string) (*FileInfo, *Response) { + r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/info", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return FileInfoFromJson(r.Body), BuildResponse(r) +} + +// GetFileInfosForPost gets all the file info objects attached to a post. +func (c *Client4) GetFileInfosForPost(postId string, etag string) ([]*FileInfo, *Response) { + r, err := c.DoApiGet(c.GetPostRoute(postId)+"/files/info", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return FileInfosFromJson(r.Body), BuildResponse(r) +} + +// General/System Section + +// GetPing will return ok if the running goRoutines are below the threshold and unhealthy for above. +func (c *Client4) GetPing() (string, *Response) { + r, err := c.DoApiGet(c.GetSystemRoute()+"/ping", "") + if r != nil && r.StatusCode == 500 { + defer r.Body.Close() + return STATUS_UNHEALTHY, BuildErrorResponse(r, err) + } + if err != nil { + return "", BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body)["status"], BuildResponse(r) +} + +// GetPingWithServerStatus will return ok if several basic server health checks +// all pass successfully. +func (c *Client4) GetPingWithServerStatus() (string, *Response) { + r, err := c.DoApiGet(c.GetSystemRoute()+"/ping?get_server_status=true", "") + if r != nil && r.StatusCode == 500 { + defer r.Body.Close() + return STATUS_UNHEALTHY, BuildErrorResponse(r, err) + } + if err != nil { + return "", BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body)["status"], BuildResponse(r) +} + +// TestEmail will attempt to connect to the configured SMTP server. +func (c *Client4) TestEmail(config *Config) (bool, *Response) { + r, err := c.DoApiPost(c.GetTestEmailRoute(), config.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// TestSiteURL will test the validity of a site URL. +func (c *Client4) TestSiteURL(siteURL string) (bool, *Response) { + requestBody := make(map[string]string) + requestBody["site_url"] = siteURL + r, err := c.DoApiPost(c.GetTestSiteURLRoute(), MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// TestS3Connection will attempt to connect to the AWS S3. +func (c *Client4) TestS3Connection(config *Config) (bool, *Response) { + r, err := c.DoApiPost(c.GetTestS3Route(), config.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetConfig will retrieve the server config with some sanitized items. +func (c *Client4) GetConfig() (*Config, *Response) { + r, err := c.DoApiGet(c.GetConfigRoute(), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ConfigFromJson(r.Body), BuildResponse(r) +} + +// ReloadConfig will reload the server configuration. +func (c *Client4) ReloadConfig() (bool, *Response) { + r, err := c.DoApiPost(c.GetConfigRoute()+"/reload", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetOldClientConfig will retrieve the parts of the server configuration needed by the +// client, formatted in the old format. +func (c *Client4) GetOldClientConfig(etag string) (map[string]string, *Response) { + r, err := c.DoApiGet(c.GetConfigRoute()+"/client?format=old", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body), BuildResponse(r) +} + +// GetEnvironmentConfig will retrieve a map mirroring the server configuration where fields +// are set to true if the corresponding config setting is set through an environment variable. +// Settings that haven't been set through environment variables will be missing from the map. +func (c *Client4) GetEnvironmentConfig() (map[string]interface{}, *Response) { + r, err := c.DoApiGet(c.GetConfigRoute()+"/environment", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return StringInterfaceFromJson(r.Body), BuildResponse(r) +} + +// GetOldClientLicense will retrieve the parts of the server license needed by the +// client, formatted in the old format. +func (c *Client4) GetOldClientLicense(etag string) (map[string]string, *Response) { + r, err := c.DoApiGet(c.GetLicenseRoute()+"/client?format=old", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body), BuildResponse(r) +} + +// DatabaseRecycle will recycle the connections. Discard current connection and get new one. +func (c *Client4) DatabaseRecycle() (bool, *Response) { + r, err := c.DoApiPost(c.GetDatabaseRoute()+"/recycle", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// InvalidateCaches will purge the cache and can affect the performance while is cleaning. +func (c *Client4) InvalidateCaches() (bool, *Response) { + r, err := c.DoApiPost(c.GetCacheRoute()+"/invalidate", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UpdateConfig will update the server configuration. +func (c *Client4) UpdateConfig(config *Config) (*Config, *Response) { + r, err := c.DoApiPut(c.GetConfigRoute(), config.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ConfigFromJson(r.Body), BuildResponse(r) +} + +// UploadLicenseFile will add a license file to the system. +func (c *Client4) UploadLicenseFile(data []byte) (bool, *Response) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("license", "test-license.mattermost-license") + if err != nil { + return false, &Response{Error: NewAppError("UploadLicenseFile", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return false, &Response{Error: NewAppError("UploadLicenseFile", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if err = writer.Close(); err != nil { + return false, &Response{Error: NewAppError("UploadLicenseFile", "model.client.set_profile_user.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + rq, err := http.NewRequest("POST", c.ApiUrl+c.GetLicenseRoute(), bytes.NewReader(body.Bytes())) + if err != nil { + return false, &Response{Error: NewAppError("UploadLicenseFile", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetLicenseRoute(), "model.client.connecting.app_error", nil, err.Error(), http.StatusForbidden)} + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return CheckStatusOK(rp), BuildResponse(rp) +} + +// RemoveLicenseFile will remove the server license it exists. Note that this will +// disable all enterprise features. +func (c *Client4) RemoveLicenseFile() (bool, *Response) { + r, err := c.DoApiDelete(c.GetLicenseRoute()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetAnalyticsOld will retrieve analytics using the old format. New format is not +// available but the "/analytics" endpoint is reserved for it. The "name" argument is optional +// and defaults to "standard". The "teamId" argument is optional and will limit results +// to a specific team. +func (c *Client4) GetAnalyticsOld(name, teamId string) (AnalyticsRows, *Response) { + query := fmt.Sprintf("?name=%v&team_id=%v", name, teamId) + r, err := c.DoApiGet(c.GetAnalyticsRoute()+"/old"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return AnalyticsRowsFromJson(r.Body), BuildResponse(r) +} + +// Webhooks Section + +// CreateIncomingWebhook creates an incoming webhook for a channel. +func (c *Client4) CreateIncomingWebhook(hook *IncomingWebhook) (*IncomingWebhook, *Response) { + r, err := c.DoApiPost(c.GetIncomingWebhooksRoute(), hook.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return IncomingWebhookFromJson(r.Body), BuildResponse(r) +} + +// UpdateIncomingWebhook updates an incoming webhook for a channel. +func (c *Client4) UpdateIncomingWebhook(hook *IncomingWebhook) (*IncomingWebhook, *Response) { + r, err := c.DoApiPut(c.GetIncomingWebhookRoute(hook.Id), hook.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return IncomingWebhookFromJson(r.Body), BuildResponse(r) +} + +// GetIncomingWebhooks returns a page of incoming webhooks on the system. Page counting starts at 0. +func (c *Client4) GetIncomingWebhooks(page int, perPage int, etag string) ([]*IncomingWebhook, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetIncomingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return IncomingWebhookListFromJson(r.Body), BuildResponse(r) +} + +// GetIncomingWebhooksForTeam returns a page of incoming webhooks for a team. Page counting starts at 0. +func (c *Client4) GetIncomingWebhooksForTeam(teamId string, page int, perPage int, etag string) ([]*IncomingWebhook, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId) + r, err := c.DoApiGet(c.GetIncomingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return IncomingWebhookListFromJson(r.Body), BuildResponse(r) +} + +// GetIncomingWebhook returns an Incoming webhook given the hook ID. +func (c *Client4) GetIncomingWebhook(hookID string, etag string) (*IncomingWebhook, *Response) { + r, err := c.DoApiGet(c.GetIncomingWebhookRoute(hookID), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return IncomingWebhookFromJson(r.Body), BuildResponse(r) +} + +// DeleteIncomingWebhook deletes and Incoming Webhook given the hook ID. +func (c *Client4) DeleteIncomingWebhook(hookID string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetIncomingWebhookRoute(hookID)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// CreateOutgoingWebhook creates an outgoing webhook for a team or channel. +func (c *Client4) CreateOutgoingWebhook(hook *OutgoingWebhook) (*OutgoingWebhook, *Response) { + r, err := c.DoApiPost(c.GetOutgoingWebhooksRoute(), hook.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OutgoingWebhookFromJson(r.Body), BuildResponse(r) +} + +// UpdateOutgoingWebhook creates an outgoing webhook for a team or channel. +func (c *Client4) UpdateOutgoingWebhook(hook *OutgoingWebhook) (*OutgoingWebhook, *Response) { + r, err := c.DoApiPut(c.GetOutgoingWebhookRoute(hook.Id), hook.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OutgoingWebhookFromJson(r.Body), BuildResponse(r) +} + +// GetOutgoingWebhooks returns a page of outgoing webhooks on the system. Page counting starts at 0. +func (c *Client4) GetOutgoingWebhooks(page int, perPage int, etag string) ([]*OutgoingWebhook, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetOutgoingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OutgoingWebhookListFromJson(r.Body), BuildResponse(r) +} + +// GetOutgoingWebhook outgoing webhooks on the system requested by Hook Id. +func (c *Client4) GetOutgoingWebhook(hookId string) (*OutgoingWebhook, *Response) { + r, err := c.DoApiGet(c.GetOutgoingWebhookRoute(hookId), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OutgoingWebhookFromJson(r.Body), BuildResponse(r) +} + +// GetOutgoingWebhooksForChannel returns a page of outgoing webhooks for a channel. Page counting starts at 0. +func (c *Client4) GetOutgoingWebhooksForChannel(channelId string, page int, perPage int, etag string) ([]*OutgoingWebhook, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&channel_id=%v", page, perPage, channelId) + r, err := c.DoApiGet(c.GetOutgoingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OutgoingWebhookListFromJson(r.Body), BuildResponse(r) +} + +// GetOutgoingWebhooksForTeam returns a page of outgoing webhooks for a team. Page counting starts at 0. +func (c *Client4) GetOutgoingWebhooksForTeam(teamId string, page int, perPage int, etag string) ([]*OutgoingWebhook, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId) + r, err := c.DoApiGet(c.GetOutgoingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OutgoingWebhookListFromJson(r.Body), BuildResponse(r) +} + +// RegenOutgoingHookToken regenerate the outgoing webhook token. +func (c *Client4) RegenOutgoingHookToken(hookId string) (*OutgoingWebhook, *Response) { + r, err := c.DoApiPost(c.GetOutgoingWebhookRoute(hookId)+"/regen_token", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OutgoingWebhookFromJson(r.Body), BuildResponse(r) +} + +// DeleteOutgoingWebhook delete the outgoing webhook on the system requested by Hook Id. +func (c *Client4) DeleteOutgoingWebhook(hookId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetOutgoingWebhookRoute(hookId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// Preferences Section + +// GetPreferences returns the user's preferences. +func (c *Client4) GetPreferences(userId string) (Preferences, *Response) { + r, err := c.DoApiGet(c.GetPreferencesRoute(userId), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + preferences, _ := PreferencesFromJson(r.Body) + return preferences, BuildResponse(r) +} + +// UpdatePreferences saves the user's preferences. +func (c *Client4) UpdatePreferences(userId string, preferences *Preferences) (bool, *Response) { + r, err := c.DoApiPut(c.GetPreferencesRoute(userId), preferences.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return true, BuildResponse(r) +} + +// DeletePreferences deletes the user's preferences. +func (c *Client4) DeletePreferences(userId string, preferences *Preferences) (bool, *Response) { + r, err := c.DoApiPost(c.GetPreferencesRoute(userId)+"/delete", preferences.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return true, BuildResponse(r) +} + +// GetPreferencesByCategory returns the user's preferences from the provided category string. +func (c *Client4) GetPreferencesByCategory(userId string, category string) (Preferences, *Response) { + url := fmt.Sprintf(c.GetPreferencesRoute(userId)+"/%s", category) + r, err := c.DoApiGet(url, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + preferences, _ := PreferencesFromJson(r.Body) + return preferences, BuildResponse(r) +} + +// GetPreferenceByCategoryAndName returns the user's preferences from the provided category and preference name string. +func (c *Client4) GetPreferenceByCategoryAndName(userId string, category string, preferenceName string) (*Preference, *Response) { + url := fmt.Sprintf(c.GetPreferencesRoute(userId)+"/%s/name/%v", category, preferenceName) + r, err := c.DoApiGet(url, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PreferenceFromJson(r.Body), BuildResponse(r) +} + +// SAML Section + +// GetSamlMetadata returns metadata for the SAML configuration. +func (c *Client4) GetSamlMetadata() (string, *Response) { + r, err := c.DoApiGet(c.GetSamlRoute()+"/metadata", "") + if err != nil { + return "", BuildErrorResponse(r, err) + } + defer closeBody(r) + buf := new(bytes.Buffer) + _, _ = buf.ReadFrom(r.Body) + return buf.String(), BuildResponse(r) +} + +func samlFileToMultipart(data []byte, filename string) ([]byte, *multipart.Writer, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("certificate", filename) + if err != nil { + return nil, nil, err + } + + if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return nil, nil, err + } + + if err := writer.Close(); err != nil { + return nil, nil, err + } + + return body.Bytes(), writer, nil +} + +// UploadSamlIdpCertificate will upload an IDP certificate for SAML and set the config to use it. +// The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk. +func (c *Client4) UploadSamlIdpCertificate(data []byte, filename string) (bool, *Response) { + body, writer, err := samlFileToMultipart(data, filename) + if err != nil { + return false, &Response{Error: NewAppError("UploadSamlIdpCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + _, resp := c.DoUploadFile(c.GetSamlRoute()+"/certificate/idp", body, writer.FormDataContentType()) + return resp.Error == nil, resp +} + +// UploadSamlPublicCertificate will upload a public certificate for SAML and set the config to use it. +// The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk. +func (c *Client4) UploadSamlPublicCertificate(data []byte, filename string) (bool, *Response) { + body, writer, err := samlFileToMultipart(data, filename) + if err != nil { + return false, &Response{Error: NewAppError("UploadSamlPublicCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + _, resp := c.DoUploadFile(c.GetSamlRoute()+"/certificate/public", body, writer.FormDataContentType()) + return resp.Error == nil, resp +} + +// UploadSamlPrivateCertificate will upload a private key for SAML and set the config to use it. +// The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk. +func (c *Client4) UploadSamlPrivateCertificate(data []byte, filename string) (bool, *Response) { + body, writer, err := samlFileToMultipart(data, filename) + if err != nil { + return false, &Response{Error: NewAppError("UploadSamlPrivateCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + _, resp := c.DoUploadFile(c.GetSamlRoute()+"/certificate/private", body, writer.FormDataContentType()) + return resp.Error == nil, resp +} + +// DeleteSamlIdpCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML. +func (c *Client4) DeleteSamlIdpCertificate() (bool, *Response) { + r, err := c.DoApiDelete(c.GetSamlRoute() + "/certificate/idp") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// DeleteSamlPublicCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML. +func (c *Client4) DeleteSamlPublicCertificate() (bool, *Response) { + r, err := c.DoApiDelete(c.GetSamlRoute() + "/certificate/public") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// DeleteSamlPrivateCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML. +func (c *Client4) DeleteSamlPrivateCertificate() (bool, *Response) { + r, err := c.DoApiDelete(c.GetSamlRoute() + "/certificate/private") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetSamlCertificateStatus returns metadata for the SAML configuration. +func (c *Client4) GetSamlCertificateStatus() (*SamlCertificateStatus, *Response) { + r, err := c.DoApiGet(c.GetSamlRoute()+"/certificate/status", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return SamlCertificateStatusFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) GetSamlMetadataFromIdp(samlMetadataURL string) (*SamlMetadataResponse, *Response) { + requestBody := make(map[string]string) + requestBody["saml_metadata_url"] = samlMetadataURL + r, err := c.DoApiPost(c.GetSamlRoute()+"/metadatafromidp", MapToJson(requestBody)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + + defer closeBody(r) + return SamlMetadataResponseFromJson(r.Body), BuildResponse(r) +} + +// Compliance Section + +// CreateComplianceReport creates an incoming webhook for a channel. +func (c *Client4) CreateComplianceReport(report *Compliance) (*Compliance, *Response) { + r, err := c.DoApiPost(c.GetComplianceReportsRoute(), report.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ComplianceFromJson(r.Body), BuildResponse(r) +} + +// GetComplianceReports returns list of compliance reports. +func (c *Client4) GetComplianceReports(page, perPage int) (Compliances, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetComplianceReportsRoute()+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CompliancesFromJson(r.Body), BuildResponse(r) +} + +// GetComplianceReport returns a compliance report. +func (c *Client4) GetComplianceReport(reportId string) (*Compliance, *Response) { + r, err := c.DoApiGet(c.GetComplianceReportRoute(reportId), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ComplianceFromJson(r.Body), BuildResponse(r) +} + +// DownloadComplianceReport returns a full compliance report as a file. +func (c *Client4) DownloadComplianceReport(reportId string) ([]byte, *Response) { + rq, err := http.NewRequest("GET", c.ApiUrl+c.GetComplianceReportRoute(reportId), nil) + if err != nil { + return nil, &Response{Error: NewAppError("DownloadComplianceReport", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return nil, &Response{Error: NewAppError("DownloadComplianceReport", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + data, err := ioutil.ReadAll(rp.Body) + if err != nil { + return nil, BuildErrorResponse(rp, NewAppError("DownloadComplianceReport", "model.client.read_file.app_error", nil, err.Error(), rp.StatusCode)) + } + + return data, BuildResponse(rp) +} + +// Cluster Section + +// GetClusterStatus returns the status of all the configured cluster nodes. +func (c *Client4) GetClusterStatus() ([]*ClusterInfo, *Response) { + r, err := c.DoApiGet(c.GetClusterRoute()+"/status", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ClusterInfosFromJson(r.Body), BuildResponse(r) +} + +// LDAP Section + +// SyncLdap will force a sync with the configured LDAP server. +func (c *Client4) SyncLdap() (bool, *Response) { + r, err := c.DoApiPost(c.GetLdapRoute()+"/sync", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// TestLdap will attempt to connect to the configured LDAP server and return OK if configured +// correctly. +func (c *Client4) TestLdap() (bool, *Response) { + r, err := c.DoApiPost(c.GetLdapRoute()+"/test", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetLdapGroups retrieves the immediate child groups of the given parent group. +func (c *Client4) GetLdapGroups() ([]*Group, *Response) { + path := fmt.Sprintf("%s/groups", c.GetLdapRoute()) + + r, appErr := c.DoApiGet(path, "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + return GroupsFromJson(r.Body), BuildResponse(r) +} + +// LinkLdapGroup creates or undeletes a Mattermost group and associates it to the given LDAP group DN. +func (c *Client4) LinkLdapGroup(dn string) (*Group, *Response) { + path := fmt.Sprintf("%s/groups/%s/link", c.GetLdapRoute(), dn) + + r, appErr := c.DoApiPost(path, "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + return GroupFromJson(r.Body), BuildResponse(r) +} + +// UnlinkLdapGroup deletes the Mattermost group associated with the given LDAP group DN. +func (c *Client4) UnlinkLdapGroup(dn string) (*Group, *Response) { + path := fmt.Sprintf("%s/groups/%s/link", c.GetLdapRoute(), dn) + + r, appErr := c.DoApiDelete(path) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + return GroupFromJson(r.Body), BuildResponse(r) +} + +// GetGroupsByChannel retrieves the Mattermost Groups associated with a given channel +func (c *Client4) GetGroupsByChannel(channelId string, opts GroupSearchOpts) ([]*GroupWithSchemeAdmin, int, *Response) { + path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v&filter_allow_reference=%v", c.GetChannelRoute(channelId), opts.Q, opts.IncludeMemberCount, opts.FilterAllowReference) + if opts.PageOpts != nil { + path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage) + } + r, appErr := c.DoApiGet(path, "") + if appErr != nil { + return nil, 0, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + responseData := struct { + Groups []*GroupWithSchemeAdmin `json:"groups"` + Count int `json:"total_group_count"` + }{} + if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil { + appErr := NewAppError("Api4.GetGroupsByChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + return nil, 0, BuildErrorResponse(r, appErr) + } + + return responseData.Groups, responseData.Count, BuildResponse(r) +} + +// GetGroupsByTeam retrieves the Mattermost Groups associated with a given team +func (c *Client4) GetGroupsByTeam(teamId string, opts GroupSearchOpts) ([]*GroupWithSchemeAdmin, int, *Response) { + path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v&filter_allow_reference=%v", c.GetTeamRoute(teamId), opts.Q, opts.IncludeMemberCount, opts.FilterAllowReference) + if opts.PageOpts != nil { + path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage) + } + r, appErr := c.DoApiGet(path, "") + if appErr != nil { + return nil, 0, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + responseData := struct { + Groups []*GroupWithSchemeAdmin `json:"groups"` + Count int `json:"total_group_count"` + }{} + if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil { + appErr := NewAppError("Api4.GetGroupsByTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + return nil, 0, BuildErrorResponse(r, appErr) + } + + return responseData.Groups, responseData.Count, BuildResponse(r) +} + +// GetGroupsAssociatedToChannelsByTeam retrieves the Mattermost Groups associated with channels in a given team +func (c *Client4) GetGroupsAssociatedToChannelsByTeam(teamId string, opts GroupSearchOpts) (map[string][]*GroupWithSchemeAdmin, *Response) { + path := fmt.Sprintf("%s/groups_by_channels?q=%v&filter_allow_reference=%v", c.GetTeamRoute(teamId), opts.Q, opts.FilterAllowReference) + if opts.PageOpts != nil { + path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage) + } + r, appErr := c.DoApiGet(path, "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + responseData := struct { + GroupsAssociatedToChannels map[string][]*GroupWithSchemeAdmin `json:"groups"` + }{} + if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil { + appErr := NewAppError("Api4.GetGroupsAssociatedToChannelsByTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + return nil, BuildErrorResponse(r, appErr) + } + + return responseData.GroupsAssociatedToChannels, BuildResponse(r) +} + +// GetGroups retrieves Mattermost Groups +func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response) { + path := fmt.Sprintf( + "%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v", + c.GetGroupsRoute(), + opts.IncludeMemberCount, + opts.NotAssociatedToTeam, + opts.NotAssociatedToChannel, + opts.FilterAllowReference, + opts.Q, + opts.FilterParentTeamPermitted, + ) + if opts.Since > 0 { + path = fmt.Sprintf("%s&since=%v", path, opts.Since) + } + if opts.PageOpts != nil { + path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage) + } + r, appErr := c.DoApiGet(path, "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + return GroupsFromJson(r.Body), BuildResponse(r) +} + +// GetGroupsByUserId retrieves Mattermost Groups for a user +func (c *Client4) GetGroupsByUserId(userId string) ([]*Group, *Response) { + path := fmt.Sprintf( + "%s/%v/groups", + c.GetUsersRoute(), + userId, + ) + + r, appErr := c.DoApiGet(path, "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return GroupsFromJson(r.Body), BuildResponse(r) +} + +// Audits Section + +// GetAudits returns a list of audits for the whole system. +func (c *Client4) GetAudits(page int, perPage int, etag string) (Audits, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet("/audits"+query, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return AuditsFromJson(r.Body), BuildResponse(r) +} + +// Brand Section + +// GetBrandImage retrieves the previously uploaded brand image. +func (c *Client4) GetBrandImage() ([]byte, *Response) { + r, appErr := c.DoApiGet(c.GetBrandRoute()+"/image", "") + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + + if r.StatusCode >= 300 { + return nil, BuildErrorResponse(r, AppErrorFromJson(r.Body)) + } + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetBrandImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + + return data, BuildResponse(r) +} + +// DeleteBrandImage deletes the brand image for the system. +func (c *Client4) DeleteBrandImage() *Response { + r, err := c.DoApiDelete(c.GetBrandRoute() + "/image") + if err != nil { + return BuildErrorResponse(r, err) + } + return BuildResponse(r) +} + +// UploadBrandImage sets the brand image for the system. +func (c *Client4) UploadBrandImage(data []byte) (bool, *Response) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("image", "brand.png") + if err != nil { + return false, &Response{Error: NewAppError("UploadBrandImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return false, &Response{Error: NewAppError("UploadBrandImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if err = writer.Close(); err != nil { + return false, &Response{Error: NewAppError("UploadBrandImage", "model.client.set_profile_user.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + rq, err := http.NewRequest("POST", c.ApiUrl+c.GetBrandRoute()+"/image", bytes.NewReader(body.Bytes())) + if err != nil { + return false, &Response{Error: NewAppError("UploadBrandImage", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetBrandRoute()+"/image", "model.client.connecting.app_error", nil, err.Error(), http.StatusForbidden)} + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return CheckStatusOK(rp), BuildResponse(rp) +} + +// Logs Section + +// GetLogs page of logs as a string array. +func (c *Client4) GetLogs(page, perPage int) ([]string, *Response) { + query := fmt.Sprintf("?page=%v&logs_per_page=%v", page, perPage) + r, err := c.DoApiGet("/logs"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ArrayFromJson(r.Body), BuildResponse(r) +} + +// PostLog is a convenience Web Service call so clients can log messages into +// the server-side logs. For example we typically log javascript error messages +// into the server-side. It returns the log message if the logging was successful. +func (c *Client4) PostLog(message map[string]string) (map[string]string, *Response) { + r, err := c.DoApiPost("/logs", MapToJson(message)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body), BuildResponse(r) +} + +// OAuth Section + +// CreateOAuthApp will register a new OAuth 2.0 client application with Mattermost acting as an OAuth 2.0 service provider. +func (c *Client4) CreateOAuthApp(app *OAuthApp) (*OAuthApp, *Response) { + r, err := c.DoApiPost(c.GetOAuthAppsRoute(), app.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OAuthAppFromJson(r.Body), BuildResponse(r) +} + +// UpdateOAuthApp updates a page of registered OAuth 2.0 client applications with Mattermost acting as an OAuth 2.0 service provider. +func (c *Client4) UpdateOAuthApp(app *OAuthApp) (*OAuthApp, *Response) { + r, err := c.DoApiPut(c.GetOAuthAppRoute(app.Id), app.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OAuthAppFromJson(r.Body), BuildResponse(r) +} + +// GetOAuthApps gets a page of registered OAuth 2.0 client applications with Mattermost acting as an OAuth 2.0 service provider. +func (c *Client4) GetOAuthApps(page, perPage int) ([]*OAuthApp, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetOAuthAppsRoute()+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OAuthAppListFromJson(r.Body), BuildResponse(r) +} + +// GetOAuthApp gets a registered OAuth 2.0 client application with Mattermost acting as an OAuth 2.0 service provider. +func (c *Client4) GetOAuthApp(appId string) (*OAuthApp, *Response) { + r, err := c.DoApiGet(c.GetOAuthAppRoute(appId), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OAuthAppFromJson(r.Body), BuildResponse(r) +} + +// GetOAuthAppInfo gets a sanitized version of a registered OAuth 2.0 client application with Mattermost acting as an OAuth 2.0 service provider. +func (c *Client4) GetOAuthAppInfo(appId string) (*OAuthApp, *Response) { + r, err := c.DoApiGet(c.GetOAuthAppRoute(appId)+"/info", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OAuthAppFromJson(r.Body), BuildResponse(r) +} + +// DeleteOAuthApp deletes a registered OAuth 2.0 client application. +func (c *Client4) DeleteOAuthApp(appId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetOAuthAppRoute(appId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// RegenerateOAuthAppSecret regenerates the client secret for a registered OAuth 2.0 client application. +func (c *Client4) RegenerateOAuthAppSecret(appId string) (*OAuthApp, *Response) { + r, err := c.DoApiPost(c.GetOAuthAppRoute(appId)+"/regen_secret", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OAuthAppFromJson(r.Body), BuildResponse(r) +} + +// GetAuthorizedOAuthAppsForUser gets a page of OAuth 2.0 client applications the user has authorized to use access their account. +func (c *Client4) GetAuthorizedOAuthAppsForUser(userId string, page, perPage int) ([]*OAuthApp, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetUserRoute(userId)+"/oauth/apps/authorized"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return OAuthAppListFromJson(r.Body), BuildResponse(r) +} + +// AuthorizeOAuthApp will authorize an OAuth 2.0 client application to access a user's account and provide a redirect link to follow. +func (c *Client4) AuthorizeOAuthApp(authRequest *AuthorizeRequest) (string, *Response) { + r, err := c.DoApiRequest(http.MethodPost, c.Url+"/oauth/authorize", authRequest.ToJson(), "") + if err != nil { + return "", BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body)["redirect"], BuildResponse(r) +} + +// DeauthorizeOAuthApp will deauthorize an OAuth 2.0 client application from accessing a user's account. +func (c *Client4) DeauthorizeOAuthApp(appId string) (bool, *Response) { + requestData := map[string]string{"client_id": appId} + r, err := c.DoApiRequest(http.MethodPost, c.Url+"/oauth/deauthorize", MapToJson(requestData), "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetOAuthAccessToken is a test helper function for the OAuth access token endpoint. +func (c *Client4) GetOAuthAccessToken(data url.Values) (*AccessResponse, *Response) { + rq, err := http.NewRequest(http.MethodPost, c.Url+"/oauth/access_token", strings.NewReader(data.Encode())) + if err != nil { + return nil, &Response{Error: NewAppError(c.Url+"/oauth/access_token", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + rq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.Url+"/oauth/access_token", "model.client.connecting.app_error", nil, err.Error(), 403)} + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return AccessResponseFromJson(rp.Body), BuildResponse(rp) +} + +// Elasticsearch Section + +// TestElasticsearch will attempt to connect to the configured Elasticsearch server and return OK if configured. +// correctly. +func (c *Client4) TestElasticsearch() (bool, *Response) { + r, err := c.DoApiPost(c.GetElasticsearchRoute()+"/test", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// PurgeElasticsearchIndexes immediately deletes all Elasticsearch indexes. +func (c *Client4) PurgeElasticsearchIndexes() (bool, *Response) { + r, err := c.DoApiPost(c.GetElasticsearchRoute()+"/purge_indexes", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// Bleve Section + +// PurgeBleveIndexes immediately deletes all Bleve indexes. +func (c *Client4) PurgeBleveIndexes() (bool, *Response) { + r, err := c.DoApiPost(c.GetBleveRoute()+"/purge_indexes", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// Data Retention Section + +// GetDataRetentionPolicy will get the current server data retention policy details. +func (c *Client4) GetDataRetentionPolicy() (*DataRetentionPolicy, *Response) { + r, err := c.DoApiGet(c.GetDataRetentionRoute()+"/policy", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return DataRetentionPolicyFromJson(r.Body), BuildResponse(r) +} + +// Commands Section + +// CreateCommand will create a new command if the user have the right permissions. +func (c *Client4) CreateCommand(cmd *Command) (*Command, *Response) { + r, err := c.DoApiPost(c.GetCommandsRoute(), cmd.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CommandFromJson(r.Body), BuildResponse(r) +} + +// UpdateCommand updates a command based on the provided Command struct. +func (c *Client4) UpdateCommand(cmd *Command) (*Command, *Response) { + r, err := c.DoApiPut(c.GetCommandRoute(cmd.Id), cmd.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CommandFromJson(r.Body), BuildResponse(r) +} + +// MoveCommand moves a command to a different team. +func (c *Client4) MoveCommand(teamId string, commandId string) (bool, *Response) { + cmr := CommandMoveRequest{TeamId: teamId} + r, err := c.DoApiPut(c.GetCommandMoveRoute(commandId), cmr.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// DeleteCommand deletes a command based on the provided command id string. +func (c *Client4) DeleteCommand(commandId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetCommandRoute(commandId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// ListCommands will retrieve a list of commands available in the team. +func (c *Client4) ListCommands(teamId string, customOnly bool) ([]*Command, *Response) { + query := fmt.Sprintf("?team_id=%v&custom_only=%v", teamId, customOnly) + r, err := c.DoApiGet(c.GetCommandsRoute()+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CommandListFromJson(r.Body), BuildResponse(r) +} + +// ListCommandAutocompleteSuggestions will retrieve a list of suggestions for a userInput. +func (c *Client4) ListCommandAutocompleteSuggestions(userInput, teamId string) ([]AutocompleteSuggestion, *Response) { + query := fmt.Sprintf("/commands/autocomplete_suggestions?user_input=%v", userInput) + r, err := c.DoApiGet(c.GetTeamRoute(teamId)+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return AutocompleteSuggestionsFromJSON(r.Body), BuildResponse(r) +} + +// GetCommandById will retrieve a command by id. +func (c *Client4) GetCommandById(cmdId string) (*Command, *Response) { + url := fmt.Sprintf("%s/%s", c.GetCommandsRoute(), cmdId) + r, err := c.DoApiGet(url, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CommandFromJson(r.Body), BuildResponse(r) +} + +// ExecuteCommand executes a given slash command. +func (c *Client4) ExecuteCommand(channelId, command string) (*CommandResponse, *Response) { + commandArgs := &CommandArgs{ + ChannelId: channelId, + Command: command, + } + r, err := c.DoApiPost(c.GetCommandsRoute()+"/execute", commandArgs.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + response, _ := CommandResponseFromJson(r.Body) + return response, BuildResponse(r) +} + +// ExecuteCommandWithTeam executes a given slash command against the specified team. +// Use this when executing slash commands in a DM/GM, since the team id cannot be inferred in that case. +func (c *Client4) ExecuteCommandWithTeam(channelId, teamId, command string) (*CommandResponse, *Response) { + commandArgs := &CommandArgs{ + ChannelId: channelId, + TeamId: teamId, + Command: command, + } + r, err := c.DoApiPost(c.GetCommandsRoute()+"/execute", commandArgs.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + response, _ := CommandResponseFromJson(r.Body) + return response, BuildResponse(r) +} + +// ListAutocompleteCommands will retrieve a list of commands available in the team. +func (c *Client4) ListAutocompleteCommands(teamId string) ([]*Command, *Response) { + r, err := c.DoApiGet(c.GetTeamAutoCompleteCommandsRoute(teamId), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CommandListFromJson(r.Body), BuildResponse(r) +} + +// RegenCommandToken will create a new token if the user have the right permissions. +func (c *Client4) RegenCommandToken(commandId string) (string, *Response) { + r, err := c.DoApiPut(c.GetCommandRoute(commandId)+"/regen_token", "") + if err != nil { + return "", BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body)["token"], BuildResponse(r) +} + +// Status Section + +// GetUserStatus returns a user based on the provided user id string. +func (c *Client4) GetUserStatus(userId, etag string) (*Status, *Response) { + r, err := c.DoApiGet(c.GetUserStatusRoute(userId), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return StatusFromJson(r.Body), BuildResponse(r) +} + +// GetUsersStatusesByIds returns a list of users status based on the provided user ids. +func (c *Client4) GetUsersStatusesByIds(userIds []string) ([]*Status, *Response) { + r, err := c.DoApiPost(c.GetUserStatusesRoute()+"/ids", ArrayToJson(userIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return StatusListFromJson(r.Body), BuildResponse(r) +} + +// UpdateUserStatus sets a user's status based on the provided user id string. +func (c *Client4) UpdateUserStatus(userId string, userStatus *Status) (*Status, *Response) { + r, err := c.DoApiPut(c.GetUserStatusRoute(userId), userStatus.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return StatusFromJson(r.Body), BuildResponse(r) +} + +// Emoji Section + +// CreateEmoji will save an emoji to the server if the current user has permission +// to do so. If successful, the provided emoji will be returned with its Id field +// filled in. Otherwise, an error will be returned. +func (c *Client4) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoji, *Response) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("image", filename) + if err != nil { + return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error(), 0)} + } + + if _, err := io.Copy(part, bytes.NewBuffer(image)); err != nil { + return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error(), 0)} + } + + if err := writer.WriteField("emoji", emoji.ToJson()); err != nil { + return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError("CreateEmoji", "model.client.create_emoji.emoji.app_error", nil, err.Error(), 0)} + } + + if err := writer.Close(); err != nil { + return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError("CreateEmoji", "model.client.create_emoji.writer.app_error", nil, err.Error(), 0)} + } + + return c.DoEmojiUploadFile(c.GetEmojisRoute(), body.Bytes(), writer.FormDataContentType()) +} + +// GetEmojiList returns a page of custom emoji on the system. +func (c *Client4) GetEmojiList(page, perPage int) ([]*Emoji, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoApiGet(c.GetEmojisRoute()+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) +} + +// GetSortedEmojiList returns a page of custom emoji on the system sorted based on the sort +// parameter, blank for no sorting and "name" to sort by emoji names. +func (c *Client4) GetSortedEmojiList(page, perPage int, sort string) ([]*Emoji, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&sort=%v", page, perPage, sort) + r, err := c.DoApiGet(c.GetEmojisRoute()+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) +} + +// DeleteEmoji delete an custom emoji on the provided emoji id string. +func (c *Client4) DeleteEmoji(emojiId string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetEmojiRoute(emojiId)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetEmoji returns a custom emoji based on the emojiId string. +func (c *Client4) GetEmoji(emojiId string) (*Emoji, *Response) { + r, err := c.DoApiGet(c.GetEmojiRoute(emojiId), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmojiFromJson(r.Body), BuildResponse(r) +} + +// GetEmojiByName returns a custom emoji based on the name string. +func (c *Client4) GetEmojiByName(name string) (*Emoji, *Response) { + r, err := c.DoApiGet(c.GetEmojiByNameRoute(name), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmojiFromJson(r.Body), BuildResponse(r) +} + +// GetEmojiImage returns the emoji image. +func (c *Client4) GetEmojiImage(emojiId string) ([]byte, *Response) { + r, apErr := c.DoApiGet(c.GetEmojiRoute(emojiId)+"/image", "") + if apErr != nil { + return nil, BuildErrorResponse(r, apErr) + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetEmojiImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode)) + } + + return data, BuildResponse(r) +} + +// SearchEmoji returns a list of emoji matching some search criteria. +func (c *Client4) SearchEmoji(search *EmojiSearch) ([]*Emoji, *Response) { + r, err := c.DoApiPost(c.GetEmojisRoute()+"/search", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) +} + +// AutocompleteEmoji returns a list of emoji starting with or matching name. +func (c *Client4) AutocompleteEmoji(name string, etag string) ([]*Emoji, *Response) { + query := fmt.Sprintf("?name=%v", name) + r, err := c.DoApiGet(c.GetEmojisRoute()+"/autocomplete"+query, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) +} + +// Reaction Section + +// SaveReaction saves an emoji reaction for a post. Returns the saved reaction if successful, otherwise an error will be returned. +func (c *Client4) SaveReaction(reaction *Reaction) (*Reaction, *Response) { + r, err := c.DoApiPost(c.GetReactionsRoute(), reaction.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ReactionFromJson(r.Body), BuildResponse(r) +} + +// GetReactions returns a list of reactions to a post. +func (c *Client4) GetReactions(postId string) ([]*Reaction, *Response) { + r, err := c.DoApiGet(c.GetPostRoute(postId)+"/reactions", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ReactionsFromJson(r.Body), BuildResponse(r) +} + +// DeleteReaction deletes reaction of a user in a post. +func (c *Client4) DeleteReaction(reaction *Reaction) (bool, *Response) { + r, err := c.DoApiDelete(c.GetUserRoute(reaction.UserId) + c.GetPostRoute(reaction.PostId) + fmt.Sprintf("/reactions/%v", reaction.EmojiName)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// FetchBulkReactions returns a map of postIds and corresponding reactions +func (c *Client4) GetBulkReactions(postIds []string) (map[string][]*Reaction, *Response) { + r, err := c.DoApiPost(c.GetPostsRoute()+"/ids/reactions", ArrayToJson(postIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapPostIdToReactionsFromJson(r.Body), BuildResponse(r) +} + +// Timezone Section + +// GetSupportedTimezone returns a page of supported timezones on the system. +func (c *Client4) GetSupportedTimezone() ([]string, *Response) { + r, err := c.DoApiGet(c.GetTimezonesRoute(), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + var timezones []string + json.NewDecoder(r.Body).Decode(&timezones) + return timezones, BuildResponse(r) +} + +// Open Graph Metadata Section + +// OpenGraph return the open graph metadata for a particular url if the site have the metadata. +func (c *Client4) OpenGraph(url string) (map[string]string, *Response) { + requestBody := make(map[string]string) + requestBody["url"] = url + + r, err := c.DoApiPost(c.GetOpenGraphRoute(), MapToJson(requestBody)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body), BuildResponse(r) +} + +// Jobs Section + +// GetJob gets a single job. +func (c *Client4) GetJob(id string) (*Job, *Response) { + r, err := c.DoApiGet(c.GetJobsRoute()+fmt.Sprintf("/%v", id), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return JobFromJson(r.Body), BuildResponse(r) +} + +// GetJobs gets all jobs, sorted with the job that was created most recently first. +func (c *Client4) GetJobs(page int, perPage int) ([]*Job, *Response) { + r, err := c.DoApiGet(c.GetJobsRoute()+fmt.Sprintf("?page=%v&per_page=%v", page, perPage), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return JobsFromJson(r.Body), BuildResponse(r) +} + +// GetJobsByType gets all jobs of a given type, sorted with the job that was created most recently first. +func (c *Client4) GetJobsByType(jobType string, page int, perPage int) ([]*Job, *Response) { + r, err := c.DoApiGet(c.GetJobsRoute()+fmt.Sprintf("/type/%v?page=%v&per_page=%v", jobType, page, perPage), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return JobsFromJson(r.Body), BuildResponse(r) +} + +// CreateJob creates a job based on the provided job struct. +func (c *Client4) CreateJob(job *Job) (*Job, *Response) { + r, err := c.DoApiPost(c.GetJobsRoute(), job.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return JobFromJson(r.Body), BuildResponse(r) +} + +// CancelJob requests the cancellation of the job with the provided Id. +func (c *Client4) CancelJob(jobId string) (bool, *Response) { + r, err := c.DoApiPost(c.GetJobsRoute()+fmt.Sprintf("/%v/cancel", jobId), "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// Roles Section + +// GetRole gets a single role by ID. +func (c *Client4) GetRole(id string) (*Role, *Response) { + r, err := c.DoApiGet(c.GetRolesRoute()+fmt.Sprintf("/%v", id), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return RoleFromJson(r.Body), BuildResponse(r) +} + +// GetRoleByName gets a single role by Name. +func (c *Client4) GetRoleByName(name string) (*Role, *Response) { + r, err := c.DoApiGet(c.GetRolesRoute()+fmt.Sprintf("/name/%v", name), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return RoleFromJson(r.Body), BuildResponse(r) +} + +// GetRolesByNames returns a list of roles based on the provided role names. +func (c *Client4) GetRolesByNames(roleNames []string) ([]*Role, *Response) { + r, err := c.DoApiPost(c.GetRolesRoute()+"/names", ArrayToJson(roleNames)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return RoleListFromJson(r.Body), BuildResponse(r) +} + +// PatchRole partially updates a role in the system. Any missing fields are not updated. +func (c *Client4) PatchRole(roleId string, patch *RolePatch) (*Role, *Response) { + r, err := c.DoApiPut(c.GetRolesRoute()+fmt.Sprintf("/%v/patch", roleId), patch.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return RoleFromJson(r.Body), BuildResponse(r) +} + +// Schemes Section + +// CreateScheme creates a new Scheme. +func (c *Client4) CreateScheme(scheme *Scheme) (*Scheme, *Response) { + r, err := c.DoApiPost(c.GetSchemesRoute(), scheme.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return SchemeFromJson(r.Body), BuildResponse(r) +} + +// GetScheme gets a single scheme by ID. +func (c *Client4) GetScheme(id string) (*Scheme, *Response) { + r, err := c.DoApiGet(c.GetSchemeRoute(id), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return SchemeFromJson(r.Body), BuildResponse(r) +} + +// GetSchemes gets all schemes, sorted with the most recently created first, optionally filtered by scope. +func (c *Client4) GetSchemes(scope string, page int, perPage int) ([]*Scheme, *Response) { + r, err := c.DoApiGet(c.GetSchemesRoute()+fmt.Sprintf("?scope=%v&page=%v&per_page=%v", scope, page, perPage), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return SchemesFromJson(r.Body), BuildResponse(r) +} + +// DeleteScheme deletes a single scheme by ID. +func (c *Client4) DeleteScheme(id string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetSchemeRoute(id)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// PatchScheme partially updates a scheme in the system. Any missing fields are not updated. +func (c *Client4) PatchScheme(id string, patch *SchemePatch) (*Scheme, *Response) { + r, err := c.DoApiPut(c.GetSchemeRoute(id)+"/patch", patch.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return SchemeFromJson(r.Body), BuildResponse(r) +} + +// GetTeamsForScheme gets the teams using this scheme, sorted alphabetically by display name. +func (c *Client4) GetTeamsForScheme(schemeId string, page int, perPage int) ([]*Team, *Response) { + r, err := c.DoApiGet(c.GetSchemeRoute(schemeId)+fmt.Sprintf("/teams?page=%v&per_page=%v", page, perPage), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamListFromJson(r.Body), BuildResponse(r) +} + +// GetChannelsForScheme gets the channels using this scheme, sorted alphabetically by display name. +func (c *Client4) GetChannelsForScheme(schemeId string, page int, perPage int) (ChannelList, *Response) { + r, err := c.DoApiGet(c.GetSchemeRoute(schemeId)+fmt.Sprintf("/channels?page=%v&per_page=%v", page, perPage), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return *ChannelListFromJson(r.Body), BuildResponse(r) +} + +// Plugin Section + +// UploadPlugin takes an io.Reader stream pointing to the contents of a .tar.gz plugin. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) UploadPlugin(file io.Reader) (*Manifest, *Response) { + return c.uploadPlugin(file, false) +} + +func (c *Client4) UploadPluginForced(file io.Reader) (*Manifest, *Response) { + return c.uploadPlugin(file, true) +} + +func (c *Client4) uploadPlugin(file io.Reader, force bool) (*Manifest, *Response) { + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + + if force { + err := writer.WriteField("force", "true") + if err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } + } + + part, err := writer.CreateFormFile("plugin", "plugin.tar.gz") + if err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } + + if _, err = io.Copy(part, file); err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } + + if err = writer.Close(); err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } + + rq, err := http.NewRequest("POST", c.ApiUrl+c.GetPluginsRoute(), body) + if err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)} + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HttpClient.Do(rq) + if err != nil || rp == nil { + return nil, BuildErrorResponse(rp, NewAppError("UploadPlugin", "model.client.connecting.app_error", nil, err.Error(), 0)) + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } + + return ManifestFromJson(rp.Body), BuildResponse(rp) +} + +func (c *Client4) InstallPluginFromUrl(downloadUrl string, force bool) (*Manifest, *Response) { + forceStr := "false" + if force { + forceStr = "true" + } + + url := fmt.Sprintf("%s?plugin_download_url=%s&force=%s", c.GetPluginsRoute()+"/install_from_url", url.QueryEscape(downloadUrl), forceStr) + r, err := c.DoApiPost(url, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ManifestFromJson(r.Body), BuildResponse(r) +} + +// InstallMarketplacePlugin will install marketplace plugin. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) InstallMarketplacePlugin(request *InstallMarketplacePluginRequest) (*Manifest, *Response) { + json, err := request.ToJson() + if err != nil { + return nil, &Response{Error: NewAppError("InstallMarketplacePlugin", "model.client.plugin_request_to_json.app_error", nil, err.Error(), http.StatusBadRequest)} + } + r, appErr := c.DoApiPost(c.GetPluginsRoute()+"/marketplace", json) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return ManifestFromJson(r.Body), BuildResponse(r) +} + +// GetPlugins will return a list of plugin manifests for currently active plugins. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) GetPlugins() (*PluginsResponse, *Response) { + r, err := c.DoApiGet(c.GetPluginsRoute(), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PluginsResponseFromJson(r.Body), BuildResponse(r) +} + +// GetPluginStatuses will return the plugins installed on any server in the cluster, for reporting +// to the administrator via the system console. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) GetPluginStatuses() (PluginStatuses, *Response) { + r, err := c.DoApiGet(c.GetPluginsRoute()+"/statuses", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return PluginStatusesFromJson(r.Body), BuildResponse(r) +} + +// RemovePlugin will disable and delete a plugin. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) RemovePlugin(id string) (bool, *Response) { + r, err := c.DoApiDelete(c.GetPluginRoute(id)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetWebappPlugins will return a list of plugins that the webapp should download. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) GetWebappPlugins() ([]*Manifest, *Response) { + r, err := c.DoApiGet(c.GetPluginsRoute()+"/webapp", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ManifestListFromJson(r.Body), BuildResponse(r) +} + +// EnablePlugin will enable an plugin installed. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) EnablePlugin(id string) (bool, *Response) { + r, err := c.DoApiPost(c.GetPluginRoute(id)+"/enable", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// DisablePlugin will disable an enabled plugin. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) DisablePlugin(id string) (bool, *Response) { + r, err := c.DoApiPost(c.GetPluginRoute(id)+"/disable", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetMarketplacePlugins will return a list of plugins that an admin can install. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) GetMarketplacePlugins(filter *MarketplacePluginFilter) ([]*MarketplacePlugin, *Response) { + route := c.GetPluginsRoute() + "/marketplace" + u, parseErr := url.Parse(route) + if parseErr != nil { + return nil, &Response{Error: NewAppError("GetMarketplacePlugins", "model.client.parse_plugins.app_error", nil, parseErr.Error(), http.StatusBadRequest)} + } + + filter.ApplyToURL(u) + + r, err := c.DoApiGet(u.String(), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + plugins, readerErr := MarketplacePluginsFromReader(r.Body) + if readerErr != nil { + return nil, BuildErrorResponse(r, NewAppError(route, "model.client.parse_plugins.app_error", nil, err.Error(), http.StatusBadRequest)) + } + + return plugins, BuildResponse(r) +} + +// UpdateChannelScheme will update a channel's scheme. +func (c *Client4) UpdateChannelScheme(channelId, schemeId string) (bool, *Response) { + sip := &SchemeIDPatch{SchemeID: &schemeId} + r, err := c.DoApiPut(c.GetChannelSchemeRoute(channelId), sip.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// UpdateTeamScheme will update a team's scheme. +func (c *Client4) UpdateTeamScheme(teamId, schemeId string) (bool, *Response) { + sip := &SchemeIDPatch{SchemeID: &schemeId} + r, err := c.DoApiPut(c.GetTeamSchemeRoute(teamId), sip.ToJson()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetRedirectLocation retrieves the value of the 'Location' header of an HTTP response for a given URL. +func (c *Client4) GetRedirectLocation(urlParam, etag string) (string, *Response) { + url := fmt.Sprintf("%s?url=%s", c.GetRedirectLocationRoute(), url.QueryEscape(urlParam)) + r, err := c.DoApiGet(url, etag) + if err != nil { + return "", BuildErrorResponse(r, err) + } + defer closeBody(r) + return MapFromJson(r.Body)["location"], BuildResponse(r) +} + +// SetServerBusy will mark the server as busy, which disables non-critical services for `secs` seconds. +func (c *Client4) SetServerBusy(secs int) (bool, *Response) { + url := fmt.Sprintf("%s?seconds=%d", c.GetServerBusyRoute(), secs) + r, err := c.DoApiPost(url, "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// ClearServerBusy will mark the server as not busy. +func (c *Client4) ClearServerBusy() (bool, *Response) { + r, err := c.DoApiDelete(c.GetServerBusyRoute()) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + +// GetServerBusy returns the current ServerBusyState including the time when a server marked busy +// will automatically have the flag cleared. +func (c *Client4) GetServerBusy() (*ServerBusyState, *Response) { + r, err := c.DoApiGet(c.GetServerBusyRoute(), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + sbs := ServerBusyStateFromJson(r.Body) + return sbs, BuildResponse(r) +} + +// GetServerBusyExpires returns the time when a server marked busy +// will automatically have the flag cleared. +// +// Deprecated: Use GetServerBusy instead. +func (c *Client4) GetServerBusyExpires() (*time.Time, *Response) { + r, err := c.DoApiGet(c.GetServerBusyRoute(), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + sbs := ServerBusyStateFromJson(r.Body) + expires := time.Unix(sbs.Expires, 0) + return &expires, BuildResponse(r) +} + +// RegisterTermsOfServiceAction saves action performed by a user against a specific terms of service. +func (c *Client4) RegisterTermsOfServiceAction(userId, termsOfServiceId string, accepted bool) (*bool, *Response) { + url := c.GetUserTermsOfServiceRoute(userId) + data := map[string]interface{}{"termsOfServiceId": termsOfServiceId, "accepted": accepted} + r, err := c.DoApiPost(url, StringInterfaceToJson(data)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return NewBool(CheckStatusOK(r)), BuildResponse(r) +} + +// GetTermsOfService fetches the latest terms of service +func (c *Client4) GetTermsOfService(etag string) (*TermsOfService, *Response) { + url := c.GetTermsOfServiceRoute() + r, err := c.DoApiGet(url, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TermsOfServiceFromJson(r.Body), BuildResponse(r) +} + +// GetUserTermsOfService fetches user's latest terms of service action if the latest action was for acceptance. +func (c *Client4) GetUserTermsOfService(userId, etag string) (*UserTermsOfService, *Response) { + url := c.GetUserTermsOfServiceRoute(userId) + r, err := c.DoApiGet(url, etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return UserTermsOfServiceFromJson(r.Body), BuildResponse(r) +} + +// CreateTermsOfService creates new terms of service. +func (c *Client4) CreateTermsOfService(text, userId string) (*TermsOfService, *Response) { + url := c.GetTermsOfServiceRoute() + data := map[string]interface{}{"text": text} + r, err := c.DoApiPost(url, StringInterfaceToJson(data)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TermsOfServiceFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) GetGroup(groupID, etag string) (*Group, *Response) { + r, appErr := c.DoApiGet(c.GetGroupRoute(groupID), etag) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return GroupFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) PatchGroup(groupID string, patch *GroupPatch) (*Group, *Response) { + payload, _ := json.Marshal(patch) + r, appErr := c.DoApiPut(c.GetGroupRoute(groupID)+"/patch", string(payload)) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return GroupFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) LinkGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, patch *GroupSyncablePatch) (*GroupSyncable, *Response) { + payload, _ := json.Marshal(patch) + url := fmt.Sprintf("%s/link", c.GetGroupSyncableRoute(groupID, syncableID, syncableType)) + r, appErr := c.DoApiPost(url, string(payload)) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return GroupSyncableFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) UnlinkGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType) *Response { + url := fmt.Sprintf("%s/link", c.GetGroupSyncableRoute(groupID, syncableID, syncableType)) + r, appErr := c.DoApiDelete(url) + if appErr != nil { + return BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return BuildResponse(r) +} + +func (c *Client4) GetGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, etag string) (*GroupSyncable, *Response) { + r, appErr := c.DoApiGet(c.GetGroupSyncableRoute(groupID, syncableID, syncableType), etag) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return GroupSyncableFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) GetGroupSyncables(groupID string, syncableType GroupSyncableType, etag string) ([]*GroupSyncable, *Response) { + r, appErr := c.DoApiGet(c.GetGroupSyncablesRoute(groupID, syncableType), etag) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return GroupSyncablesFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) PatchGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, patch *GroupSyncablePatch) (*GroupSyncable, *Response) { + payload, _ := json.Marshal(patch) + r, appErr := c.DoApiPut(c.GetGroupSyncableRoute(groupID, syncableID, syncableType)+"/patch", string(payload)) + if appErr != nil { + return nil, BuildErrorResponse(r, appErr) + } + defer closeBody(r) + return GroupSyncableFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int, etag string) ([]*UserWithGroups, int64, *Response) { + groupIDStr := strings.Join(groupIDs, ",") + query := fmt.Sprintf("?group_ids=%s&page=%d&per_page=%d", groupIDStr, page, perPage) + r, err := c.DoApiGet(c.GetTeamRoute(teamID)+"/members_minus_group_members"+query, etag) + if err != nil { + return nil, 0, BuildErrorResponse(r, err) + } + defer closeBody(r) + ugc := UsersWithGroupsAndCountFromJson(r.Body) + return ugc.Users, ugc.Count, BuildResponse(r) +} + +func (c *Client4) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int, etag string) ([]*UserWithGroups, int64, *Response) { + groupIDStr := strings.Join(groupIDs, ",") + query := fmt.Sprintf("?group_ids=%s&page=%d&per_page=%d", groupIDStr, page, perPage) + r, err := c.DoApiGet(c.GetChannelRoute(channelID)+"/members_minus_group_members"+query, etag) + if err != nil { + return nil, 0, BuildErrorResponse(r, err) + } + defer closeBody(r) + ugc := UsersWithGroupsAndCountFromJson(r.Body) + return ugc.Users, ugc.Count, BuildResponse(r) +} + +func (c *Client4) PatchConfig(config *Config) (*Config, *Response) { + r, err := c.DoApiPut(c.GetConfigRoute()+"/patch", config.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ConfigFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) GetChannelModerations(channelID string, etag string) ([]*ChannelModeration, *Response) { + r, err := c.DoApiGet(c.GetChannelRoute(channelID)+"/moderations", etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelModerationsFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) PatchChannelModerations(channelID string, patch []*ChannelModerationPatch) ([]*ChannelModeration, *Response) { + payload, _ := json.Marshal(patch) + r, err := c.DoApiPut(c.GetChannelRoute(channelID)+"/moderations/patch", string(payload)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelModerationsFromJson(r.Body), BuildResponse(r) +} + +func (c *Client4) GetKnownUsers() ([]string, *Response) { + r, err := c.DoApiGet(c.GetUsersRoute()+"/known", "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + var userIds []string + json.NewDecoder(r.Body).Decode(&userIds) + return userIds, BuildResponse(r) +} + +func (c *Client4) GetChannelMemberCountsByGroup(channelID string, includeTimezones bool, etag string) ([]*ChannelMemberCountByGroup, *Response) { + r, err := c.DoApiGet(c.GetChannelRoute(channelID)+"/member_counts_by_group?include_timezones="+strconv.FormatBool(includeTimezones), etag) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelMemberCountsByGroupFromJson(r.Body), BuildResponse(r) +} + +// RequestTrialLicense will request a trial license and install it in the server +func (c *Client4) RequestTrialLicense(users int) (bool, *Response) { + b, _ := json.Marshal(map[string]int{"users": users}) + r, err := c.DoApiPost("/trial-license", string(b)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_discovery.go b/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_discovery.go new file mode 100644 index 00000000..f6c9275a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_discovery.go @@ -0,0 +1,137 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "os" +) + +const ( + CDS_OFFLINE_AFTER_MILLIS = 1000 * 60 * 30 // 30 minutes + CDS_TYPE_APP = "mattermost_app" +) + +type ClusterDiscovery struct { + Id string `json:"id"` + Type string `json:"type"` + ClusterName string `json:"cluster_name"` + Hostname string `json:"hostname"` + GossipPort int32 `json:"gossip_port"` + Port int32 `json:"port"` + CreateAt int64 `json:"create_at"` + LastPingAt int64 `json:"last_ping_at"` +} + +func (o *ClusterDiscovery) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + if o.CreateAt == 0 { + o.CreateAt = GetMillis() + o.LastPingAt = o.CreateAt + } +} + +func (o *ClusterDiscovery) AutoFillHostname() { + // attempt to set the hostname from the OS + if len(o.Hostname) == 0 { + if hn, err := os.Hostname(); err == nil { + o.Hostname = hn + } + } +} + +func (o *ClusterDiscovery) AutoFillIpAddress(iface string, ipAddress string) { + // attempt to set the hostname to the first non-local IP address + if len(o.Hostname) == 0 { + if len(ipAddress) > 0 { + o.Hostname = ipAddress + } else { + o.Hostname = GetServerIpAddress(iface) + } + } +} + +func (o *ClusterDiscovery) IsEqual(in *ClusterDiscovery) bool { + if in == nil { + return false + } + + if o.Type != in.Type { + return false + } + + if o.ClusterName != in.ClusterName { + return false + } + + if o.Hostname != in.Hostname { + return false + } + + return true +} + +func FilterClusterDiscovery(vs []*ClusterDiscovery, f func(*ClusterDiscovery) bool) []*ClusterDiscovery { + copy := make([]*ClusterDiscovery, 0) + for _, v := range vs { + if f(v) { + copy = append(copy, v) + } + } + + return copy +} + +func (o *ClusterDiscovery) IsValid() *AppError { + if !IsValidId(o.Id) { + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.ClusterName) == 0 { + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.name.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Type) == 0 { + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.type.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Hostname) == 0 { + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.hostname.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) + } + + if o.LastPingAt == 0 { + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.last_ping_at.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (o *ClusterDiscovery) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } + + return string(b) +} + +func ClusterDiscoveryFromJson(data io.Reader) *ClusterDiscovery { + decoder := json.NewDecoder(data) + var me ClusterDiscovery + err := decoder.Decode(&me) + if err == nil { + return &me + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_info.go b/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_info.go new file mode 100644 index 00000000..82437469 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_info.go @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ClusterInfo struct { + Id string `json:"id"` + Version string `json:"version"` + ConfigHash string `json:"config_hash"` + IpAddress string `json:"ipaddress"` + Hostname string `json:"hostname"` +} + +func (me *ClusterInfo) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func ClusterInfoFromJson(data io.Reader) *ClusterInfo { + var me *ClusterInfo + json.NewDecoder(data).Decode(&me) + return me +} + +func ClusterInfosToJson(objmap []*ClusterInfo) string { + b, _ := json.Marshal(objmap) + return string(b) +} + +func ClusterInfosFromJson(data io.Reader) []*ClusterInfo { + decoder := json.NewDecoder(data) + + var objmap []*ClusterInfo + if err := decoder.Decode(&objmap); err != nil { + return make([]*ClusterInfo, 0) + } else { + return objmap + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_message.go b/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_message.go new file mode 100644 index 00000000..86113d78 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_message.go @@ -0,0 +1,68 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + CLUSTER_EVENT_PUBLISH = "publish" + CLUSTER_EVENT_UPDATE_STATUS = "update_status" + CLUSTER_EVENT_INVALIDATE_ALL_CACHES = "inv_all_caches" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_REACTIONS = "inv_reactions" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_WEBHOOK = "inv_webhook" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_POSTS = "inv_channel_posts" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_MEMBERS_NOTIFY_PROPS = "inv_channel_members_notify_props" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_MEMBERS = "inv_channel_members" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_BY_NAME = "inv_channel_name" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL = "inv_channel" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_GUEST_COUNT = "inv_channel_guest_count" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER = "inv_user" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER_TEAMS = "inv_user_teams" + CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_USER = "clear_session_user" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES = "inv_roles" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLE_PERMISSIONS = "inv_role_permissions" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_PROFILE_BY_IDS = "inv_profile_ids" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_PROFILE_IN_CHANNEL = "inv_profile_in_channel" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_SCHEMES = "inv_schemes" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_FILE_INFOS = "inv_file_infos" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_WEBHOOKS = "inv_webhooks" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_EMOJIS_BY_ID = "inv_emojis_by_id" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_EMOJIS_ID_BY_NAME = "inv_emojis_id_by_name" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_PINNEDPOSTS_COUNTS = "inv_channel_pinnedposts_counts" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_CHANNEL_MEMBER_COUNTS = "inv_channel_member_counts" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_LAST_POSTS = "inv_last_posts" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_LAST_POST_TIME = "inv_last_post_time" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_TEAMS = "inv_teams" + CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_ALL_USERS = "inv_all_user_sessions" + CLUSTER_EVENT_INSTALL_PLUGIN = "install_plugin" + CLUSTER_EVENT_REMOVE_PLUGIN = "remove_plugin" + CLUSTER_EVENT_INVALIDATE_CACHE_FOR_TERMS_OF_SERVICE = "inv_terms_of_service" + CLUSTER_EVENT_BUSY_STATE_CHANGED = "busy_state_change" + + // SendTypes for ClusterMessage. + CLUSTER_SEND_BEST_EFFORT = "best_effort" + CLUSTER_SEND_RELIABLE = "reliable" +) + +type ClusterMessage struct { + Event string `json:"event"` + SendType string `json:"-"` + WaitForAllToSend bool `json:"-"` + Data string `json:"data,omitempty"` + Props map[string]string `json:"props,omitempty"` +} + +func (o *ClusterMessage) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func ClusterMessageFromJson(data io.Reader) *ClusterMessage { + var o *ClusterMessage + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_stats.go b/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_stats.go new file mode 100644 index 00000000..afc2ab44 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/cluster_stats.go @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ClusterStats struct { + Id string `json:"id"` + TotalWebsocketConnections int `json:"total_websocket_connections"` + TotalReadDbConnections int `json:"total_read_db_connections"` + TotalMasterDbConnections int `json:"total_master_db_connections"` +} + +func (me *ClusterStats) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func ClusterStatsFromJson(data io.Reader) *ClusterStats { + var me *ClusterStats + json.NewDecoder(data).Decode(&me) + return me +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/command.go b/vendor/github.com/mattermost/mattermost-server/v5/model/command.go new file mode 100644 index 00000000..6dcf52ae --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/command.go @@ -0,0 +1,148 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "strings" +) + +const ( + COMMAND_METHOD_POST = "P" + COMMAND_METHOD_GET = "G" + MIN_TRIGGER_LENGTH = 1 + MAX_TRIGGER_LENGTH = 128 +) + +type Command struct { + Id string `json:"id"` + Token string `json:"token"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + CreatorId string `json:"creator_id"` + TeamId string `json:"team_id"` + Trigger string `json:"trigger"` + Method string `json:"method"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + AutoComplete bool `json:"auto_complete"` + AutoCompleteDesc string `json:"auto_complete_desc"` + AutoCompleteHint string `json:"auto_complete_hint"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + URL string `json:"url"` + AutocompleteData *AutocompleteData `db:"-" json:"autocomplete_data,omitempty"` + // AutocompleteIconData is a base64 encoded svg + AutocompleteIconData string `db:"-" json:"autocomplete_icon_data,omitempty"` +} + +func (o *Command) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func CommandFromJson(data io.Reader) *Command { + var o *Command + json.NewDecoder(data).Decode(&o) + return o +} + +func CommandListToJson(l []*Command) string { + b, _ := json.Marshal(l) + return string(b) +} + +func CommandListFromJson(data io.Reader) []*Command { + var o []*Command + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *Command) IsValid() *AppError { + + if !IsValidId(o.Id) { + return NewAppError("Command.IsValid", "model.command.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Token) != 26 { + return NewAppError("Command.IsValid", "model.command.is_valid.token.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("Command.IsValid", "model.command.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) + } + + if o.UpdateAt == 0 { + return NewAppError("Command.IsValid", "model.command.is_valid.update_at.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.CreatorId) { + return NewAppError("Command.IsValid", "model.command.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.TeamId) { + return NewAppError("Command.IsValid", "model.command.is_valid.team_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Trigger) < MIN_TRIGGER_LENGTH || len(o.Trigger) > MAX_TRIGGER_LENGTH || strings.Index(o.Trigger, "/") == 0 || strings.Contains(o.Trigger, " ") { + return NewAppError("Command.IsValid", "model.command.is_valid.trigger.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.URL) == 0 || len(o.URL) > 1024 { + return NewAppError("Command.IsValid", "model.command.is_valid.url.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidHttpUrl(o.URL) { + return NewAppError("Command.IsValid", "model.command.is_valid.url_http.app_error", nil, "", http.StatusBadRequest) + } + + if !(o.Method == COMMAND_METHOD_GET || o.Method == COMMAND_METHOD_POST) { + return NewAppError("Command.IsValid", "model.command.is_valid.method.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.DisplayName) > 64 { + return NewAppError("Command.IsValid", "model.command.is_valid.display_name.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Description) > 128 { + return NewAppError("Command.IsValid", "model.command.is_valid.description.app_error", nil, "", http.StatusBadRequest) + } + + if o.AutocompleteData != nil { + if err := o.AutocompleteData.IsValid(); err != nil { + return NewAppError("Command.IsValid", "model.command.is_valid.autocomplete_data.app_error", nil, err.Error(), http.StatusBadRequest) + } + } + + return nil +} + +func (o *Command) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + if o.Token == "" { + o.Token = NewId() + } + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt +} + +func (o *Command) PreUpdate() { + o.UpdateAt = GetMillis() +} + +func (o *Command) Sanitize() { + o.Token = "" + o.CreatorId = "" + o.Method = "" + o.URL = "" + o.Username = "" + o.IconURL = "" +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/command_args.go b/vendor/github.com/mattermost/mattermost-server/v5/model/command_args.go new file mode 100644 index 00000000..a3bbb4c9 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/command_args.go @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + + goi18n "github.com/mattermost/go-i18n/i18n" +) + +type CommandArgs struct { + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + TeamId string `json:"team_id"` + RootId string `json:"root_id"` + ParentId string `json:"parent_id"` + TriggerId string `json:"trigger_id,omitempty"` + Command string `json:"command"` + SiteURL string `json:"-"` + T goi18n.TranslateFunc `json:"-"` + Session Session `json:"-"` + UserMentions UserMentionMap `json:"-"` + ChannelMentions ChannelMentionMap `json:"-"` +} + +func (o *CommandArgs) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func CommandArgsFromJson(data io.Reader) *CommandArgs { + var o *CommandArgs + json.NewDecoder(data).Decode(&o) + return o +} + +// AddUserMention adds or overrides an entry in UserMentions with name username +// and identifier userId +func (o *CommandArgs) AddUserMention(username, userId string) { + if o.UserMentions == nil { + o.UserMentions = make(UserMentionMap) + } + + o.UserMentions[username] = userId +} + +// AddChannelMention adds or overrides an entry in ChannelMentions with name +// channelName and identifier channelId +func (o *CommandArgs) AddChannelMention(channelName, channelId string) { + if o.ChannelMentions == nil { + o.ChannelMentions = make(ChannelMentionMap) + } + + o.ChannelMentions[channelName] = channelId +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/command_autocomplete.go b/vendor/github.com/mattermost/mattermost-server/v5/model/command_autocomplete.go new file mode 100644 index 00000000..68d91b23 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/command_autocomplete.go @@ -0,0 +1,455 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/url" + "path" + "reflect" + "strings" + + "github.com/pkg/errors" +) + +// AutocompleteArgType describes autocomplete argument type +type AutocompleteArgType string + +// Argument types +const ( + AutocompleteArgTypeText AutocompleteArgType = "TextInput" + AutocompleteArgTypeStaticList AutocompleteArgType = "StaticList" + AutocompleteArgTypeDynamicList AutocompleteArgType = "DynamicList" +) + +// AutocompleteData describes slash command autocomplete information. +type AutocompleteData struct { + // Trigger of the command + Trigger string + // Hint of a command + Hint string + // Text displayed to the user to help with the autocomplete description + HelpText string + // Role of the user who should be able to see the autocomplete info of this command + RoleID string + // Arguments of the command. Arguments can be named or positional. + // If they are positional order in the list matters, if they are named order does not matter. + // All arguments should be either named or positional, no mixing allowed. + Arguments []*AutocompleteArg + // Subcommands of the command + SubCommands []*AutocompleteData +} + +// AutocompleteArg describes an argument of the command. Arguments can be named or positional. +// If Name is empty string Argument is positional otherwise it is named argument. +// Named arguments are passed as --Name Argument_Value. +type AutocompleteArg struct { + // Name of the argument + Name string + // Text displayed to the user to help with the autocomplete + HelpText string + // Type of the argument + Type AutocompleteArgType + // Required determins if argument is optional or not. + Required bool + // Actual data of the argument (depends on the Type) + Data interface{} +} + +// AutocompleteTextArg describes text user can input as an argument. +type AutocompleteTextArg struct { + // Hint of the input text + Hint string + // Regex pattern to match + Pattern string +} + +// AutocompleteListItem describes an item in the AutocompleteStaticListArg. +type AutocompleteListItem struct { + Item string + Hint string + HelpText string +} + +// AutocompleteStaticListArg is used to input one of the arguments from the list, +// for example [yes, no], [on, off], and so on. +type AutocompleteStaticListArg struct { + PossibleArguments []AutocompleteListItem +} + +// AutocompleteDynamicListArg is used when user wants to download possible argument list from the URL. +type AutocompleteDynamicListArg struct { + FetchURL string +} + +// AutocompleteSuggestion describes a single suggestion item sent to the front-end +// Example: for user input `/jira cre` - +// Complete might be `/jira create` +// Suggestion might be `create`, +// Hint might be `[issue text]`, +// Description might be `Create a new Issue` +type AutocompleteSuggestion struct { + // Complete describes completed suggestion + Complete string + // Suggestion describes what user might want to input next + Suggestion string + // Hint describes a hint about the suggested input + Hint string + // Description of the command or a suggestion + Description string + // IconData is base64 encoded svg image + IconData string +} + +// NewAutocompleteData returns new Autocomplete data. +func NewAutocompleteData(trigger, hint, helpText string) *AutocompleteData { + return &AutocompleteData{ + Trigger: trigger, + Hint: hint, + HelpText: helpText, + RoleID: SYSTEM_USER_ROLE_ID, + Arguments: []*AutocompleteArg{}, + SubCommands: []*AutocompleteData{}, + } +} + +// AddCommand adds a subcommand to the autocomplete data. +func (ad *AutocompleteData) AddCommand(command *AutocompleteData) { + ad.SubCommands = append(ad.SubCommands, command) +} + +// AddTextArgument adds positional AutocompleteArgTypeText argument to the command. +func (ad *AutocompleteData) AddTextArgument(helpText, hint, pattern string) { + ad.AddNamedTextArgument("", helpText, hint, pattern, true) +} + +// AddNamedTextArgument adds named AutocompleteArgTypeText argument to the command. +func (ad *AutocompleteData) AddNamedTextArgument(name, helpText, hint, pattern string, required bool) { + argument := AutocompleteArg{ + Name: name, + HelpText: helpText, + Type: AutocompleteArgTypeText, + Required: required, + Data: &AutocompleteTextArg{Hint: hint, Pattern: pattern}, + } + ad.Arguments = append(ad.Arguments, &argument) +} + +// AddStaticListArgument adds positional AutocompleteArgTypeStaticList argument to the command. +func (ad *AutocompleteData) AddStaticListArgument(helpText string, required bool, items []AutocompleteListItem) { + ad.AddNamedStaticListArgument("", helpText, required, items) +} + +// AddNamedStaticListArgument adds named AutocompleteArgTypeStaticList argument to the command. +func (ad *AutocompleteData) AddNamedStaticListArgument(name, helpText string, required bool, items []AutocompleteListItem) { + argument := AutocompleteArg{ + Name: name, + HelpText: helpText, + Type: AutocompleteArgTypeStaticList, + Required: required, + Data: &AutocompleteStaticListArg{PossibleArguments: items}, + } + ad.Arguments = append(ad.Arguments, &argument) +} + +// AddDynamicListArgument adds positional AutocompleteArgTypeDynamicList argument to the command. +func (ad *AutocompleteData) AddDynamicListArgument(helpText, url string, required bool) { + ad.AddNamedDynamicListArgument("", helpText, url, required) +} + +// AddNamedDynamicListArgument adds named AutocompleteArgTypeDynamicList argument to the command. +func (ad *AutocompleteData) AddNamedDynamicListArgument(name, helpText, url string, required bool) { + argument := AutocompleteArg{ + Name: name, + HelpText: helpText, + Type: AutocompleteArgTypeDynamicList, + Required: required, + Data: &AutocompleteDynamicListArg{FetchURL: url}, + } + ad.Arguments = append(ad.Arguments, &argument) +} + +// Equals method checks if command is the same. +func (ad *AutocompleteData) Equals(command *AutocompleteData) bool { + if !(ad.Trigger == command.Trigger && ad.HelpText == command.HelpText && ad.RoleID == command.RoleID && ad.Hint == command.Hint) { + return false + } + if len(ad.Arguments) != len(command.Arguments) || len(ad.SubCommands) != len(command.SubCommands) { + return false + } + for i := range ad.Arguments { + if !ad.Arguments[i].Equals(command.Arguments[i]) { + return false + } + } + for i := range ad.SubCommands { + if !ad.SubCommands[i].Equals(command.SubCommands[i]) { + return false + } + } + return true +} + +// UpdateRelativeURLsForPluginCommands method updates relative urls for plugin commands +func (ad *AutocompleteData) UpdateRelativeURLsForPluginCommands(baseURL *url.URL) error { + for _, arg := range ad.Arguments { + if arg.Type != AutocompleteArgTypeDynamicList { + continue + } + dynamicList, ok := arg.Data.(*AutocompleteDynamicListArg) + if !ok { + return errors.New("Not a proper DynamicList type argument") + } + dynamicListURL, err := url.Parse(dynamicList.FetchURL) + if err != nil { + return errors.Wrapf(err, "FetchURL is not a proper url") + } + if !dynamicListURL.IsAbs() { + absURL := &url.URL{} + *absURL = *baseURL + absURL.Path = path.Join(absURL.Path, dynamicList.FetchURL) + dynamicList.FetchURL = absURL.String() + } + + } + for _, command := range ad.SubCommands { + err := command.UpdateRelativeURLsForPluginCommands(baseURL) + if err != nil { + return err + } + } + return nil +} + +// IsValid method checks if autocomplete data is valid. +func (ad *AutocompleteData) IsValid() error { + if ad == nil { + return errors.New("No nil commands are allowed in AutocompleteData") + } + if ad.Trigger == "" { + return errors.New("An empty command name in the autocomplete data") + } + if strings.ToLower(ad.Trigger) != ad.Trigger { + return errors.New("Command should be lowercase") + } + roles := []string{SYSTEM_ADMIN_ROLE_ID, SYSTEM_USER_ROLE_ID, ""} + if stringNotInSlice(ad.RoleID, roles) { + return errors.New("Wrong role in the autocomplete data") + } + if len(ad.Arguments) > 0 && len(ad.SubCommands) > 0 { + return errors.New("Command can't have arguments and subcommands") + } + if len(ad.Arguments) > 0 { + namedArgumentIndex := -1 + for i, arg := range ad.Arguments { + if arg.Name != "" { // it's a named argument + if namedArgumentIndex == -1 { // first named argument + namedArgumentIndex = i + } + } else { // it's a positional argument + if namedArgumentIndex != -1 { + return errors.New("Named argument should not be before positional argument") + } + } + if arg.Type == AutocompleteArgTypeDynamicList { + dynamicList, ok := arg.Data.(*AutocompleteDynamicListArg) + if !ok { + return errors.New("Not a proper DynamicList type argument") + } + _, err := url.Parse(dynamicList.FetchURL) + if err != nil { + return errors.Wrapf(err, "FetchURL is not a proper url") + } + } else if arg.Type == AutocompleteArgTypeStaticList { + staticList, ok := arg.Data.(*AutocompleteStaticListArg) + if !ok { + return errors.New("Not a proper StaticList type argument") + } + for _, arg := range staticList.PossibleArguments { + if arg.Item == "" { + return errors.New("Possible argument name not set in StaticList argument") + } + } + } else if arg.Type == AutocompleteArgTypeText { + if _, ok := arg.Data.(*AutocompleteTextArg); !ok { + return errors.New("Not a proper TextInput type argument") + } + if arg.Name == "" && !arg.Required { + return errors.New("Positional argument can not be optional") + } + } + } + } + for _, command := range ad.SubCommands { + err := command.IsValid() + if err != nil { + return err + } + } + return nil +} + +// ToJSON encodes AutocompleteData struct to the json +func (ad *AutocompleteData) ToJSON() ([]byte, error) { + b, err := json.Marshal(ad) + if err != nil { + return nil, errors.Wrapf(err, "can't marshal slash command %s", ad.Trigger) + } + return b, nil +} + +// AutocompleteDataFromJSON decodes AutocompleteData struct from the json +func AutocompleteDataFromJSON(data []byte) (*AutocompleteData, error) { + var ad AutocompleteData + if err := json.Unmarshal(data, &ad); err != nil { + return nil, errors.Wrap(err, "can't unmarshal AutocompleteData") + } + return &ad, nil +} + +// Equals method checks if argument is the same. +func (a *AutocompleteArg) Equals(arg *AutocompleteArg) bool { + if a.Name != arg.Name || + a.HelpText != arg.HelpText || + a.Type != arg.Type || + a.Required != arg.Required || + !reflect.DeepEqual(a.Data, arg.Data) { + return false + } + return true +} + +// UnmarshalJSON will unmarshal argument +func (a *AutocompleteArg) UnmarshalJSON(b []byte) error { + var arg map[string]interface{} + if err := json.Unmarshal(b, &arg); err != nil { + return errors.Wrapf(err, "Can't unmarshal argument %s", string(b)) + } + var ok bool + a.Name, ok = arg["Name"].(string) + if !ok { + return errors.Errorf("No field Name in the argument %s", string(b)) + } + + a.HelpText, ok = arg["HelpText"].(string) + if !ok { + return errors.Errorf("No field HelpText in the argument %s", string(b)) + } + + t, ok := arg["Type"].(string) + if !ok { + return errors.Errorf("No field Type in the argument %s", string(b)) + } + a.Type = AutocompleteArgType(t) + + a.Required, ok = arg["Required"].(bool) + if !ok { + return errors.Errorf("No field Required in the argument %s", string(b)) + } + + data, ok := arg["Data"] + if !ok { + return errors.Errorf("No field Data in the argument %s", string(b)) + } + + if a.Type == AutocompleteArgTypeText { + m, ok := data.(map[string]interface{}) + if !ok { + return errors.Errorf("Wrong Data type in the TextInput argument %s", string(b)) + } + pattern, ok := m["Pattern"].(string) + if !ok { + return errors.Errorf("No field Pattern in the TextInput argument %s", string(b)) + } + hint, ok := m["Hint"].(string) + if !ok { + return errors.Errorf("No field Hint in the TextInput argument %s", string(b)) + } + a.Data = &AutocompleteTextArg{Hint: hint, Pattern: pattern} + } else if a.Type == AutocompleteArgTypeStaticList { + m, ok := data.(map[string]interface{}) + if !ok { + return errors.Errorf("Wrong Data type in the StaticList argument %s", string(b)) + } + list, ok := m["PossibleArguments"].([]interface{}) + if !ok { + return errors.Errorf("No field PossibleArguments in the StaticList argument %s", string(b)) + } + + possibleArguments := []AutocompleteListItem{} + for i := range list { + args, ok := list[i].(map[string]interface{}) + if !ok { + return errors.Errorf("Wrong AutocompleteStaticListItem type in the StaticList argument %s", string(b)) + } + item, ok := args["Item"].(string) + if !ok { + return errors.Errorf("No field Item in the StaticList's possible arguments %s", string(b)) + } + + hint, ok := args["Hint"].(string) + if !ok { + return errors.Errorf("No field Hint in the StaticList's possible arguments %s", string(b)) + } + helpText, ok := args["HelpText"].(string) + if !ok { + return errors.Errorf("No field Hint in the StaticList's possible arguments %s", string(b)) + } + + possibleArguments = append(possibleArguments, AutocompleteListItem{ + Item: item, + Hint: hint, + HelpText: helpText, + }) + } + a.Data = &AutocompleteStaticListArg{PossibleArguments: possibleArguments} + } else if a.Type == AutocompleteArgTypeDynamicList { + m, ok := data.(map[string]interface{}) + if !ok { + return errors.Errorf("Wrong type in the DynamicList argument %s", string(b)) + } + url, ok := m["FetchURL"].(string) + if !ok { + return errors.Errorf("No field FetchURL in the DynamicList's argument %s", string(b)) + } + a.Data = &AutocompleteDynamicListArg{FetchURL: url} + } + return nil +} + +// AutocompleteSuggestionsToJSON returns json for a list of AutocompleteSuggestion objects +func AutocompleteSuggestionsToJSON(suggestions []AutocompleteSuggestion) []byte { + b, _ := json.Marshal(suggestions) + return b +} + +// AutocompleteSuggestionsFromJSON returns list of AutocompleteSuggestions from json. +func AutocompleteSuggestionsFromJSON(data io.Reader) []AutocompleteSuggestion { + var o []AutocompleteSuggestion + json.NewDecoder(data).Decode(&o) + return o +} + +// AutocompleteStaticListItemsToJSON returns json for a list of AutocompleteStaticListItem objects +func AutocompleteStaticListItemsToJSON(items []AutocompleteListItem) []byte { + b, _ := json.Marshal(items) + return b +} + +// AutocompleteStaticListItemsFromJSON returns list of AutocompleteStaticListItem from json. +func AutocompleteStaticListItemsFromJSON(data io.Reader) []AutocompleteListItem { + var o []AutocompleteListItem + json.NewDecoder(data).Decode(&o) + return o +} + +func stringNotInSlice(a string, slice []string) bool { + for _, b := range slice { + if b == a { + return false + } + } + return true +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/command_request.go b/vendor/github.com/mattermost/mattermost-server/v5/model/command_request.go new file mode 100644 index 00000000..9a4e40c8 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/command_request.go @@ -0,0 +1,31 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type CommandMoveRequest struct { + TeamId string `json:"team_id"` +} + +func CommandMoveRequestFromJson(data io.Reader) (*CommandMoveRequest, error) { + decoder := json.NewDecoder(data) + var cmr CommandMoveRequest + err := decoder.Decode(&cmr) + if err != nil { + return nil, err + } + return &cmr, nil +} + +func (cmr *CommandMoveRequest) ToJson() string { + b, err := json.Marshal(cmr) + if err != nil { + return "" + } + return string(b) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/command_response.go b/vendor/github.com/mattermost/mattermost-server/v5/model/command_response.go new file mode 100644 index 00000000..26b6cceb --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/command_response.go @@ -0,0 +1,77 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "io/ioutil" + "strings" + + "github.com/mattermost/mattermost-server/v5/utils/jsonutils" +) + +const ( + COMMAND_RESPONSE_TYPE_IN_CHANNEL = "in_channel" + COMMAND_RESPONSE_TYPE_EPHEMERAL = "ephemeral" +) + +type CommandResponse struct { + ResponseType string `json:"response_type"` + Text string `json:"text"` + Username string `json:"username"` + ChannelId string `json:"channel_id"` + IconURL string `json:"icon_url"` + Type string `json:"type"` + Props StringInterface `json:"props"` + GotoLocation string `json:"goto_location"` + TriggerId string `json:"trigger_id"` + SkipSlackParsing bool `json:"skip_slack_parsing"` // Set to `true` to skip the Slack-compatibility handling of Text. + Attachments []*SlackAttachment `json:"attachments"` + ExtraResponses []*CommandResponse `json:"extra_responses"` +} + +func (o *CommandResponse) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func CommandResponseFromHTTPBody(contentType string, body io.Reader) (*CommandResponse, error) { + if strings.TrimSpace(strings.Split(contentType, ";")[0]) == "application/json" { + return CommandResponseFromJson(body) + } + if b, err := ioutil.ReadAll(body); err == nil { + return CommandResponseFromPlainText(string(b)), nil + } + return nil, nil +} + +func CommandResponseFromPlainText(text string) *CommandResponse { + return &CommandResponse{ + Text: text, + } +} + +func CommandResponseFromJson(data io.Reader) (*CommandResponse, error) { + b, err := ioutil.ReadAll(data) + if err != nil { + return nil, err + } + + var o CommandResponse + err = json.Unmarshal(b, &o) + if err != nil { + return nil, jsonutils.HumanizeJsonError(err, b) + } + + o.Attachments = StringifySlackFieldValue(o.Attachments) + + if o.ExtraResponses != nil { + for _, resp := range o.ExtraResponses { + resp.Attachments = StringifySlackFieldValue(resp.Attachments) + } + } + + return &o, nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/command_webhook.go b/vendor/github.com/mattermost/mattermost-server/v5/model/command_webhook.go new file mode 100644 index 00000000..42a16cc7 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/command_webhook.go @@ -0,0 +1,65 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" +) + +type CommandWebhook struct { + Id string + CreateAt int64 + CommandId string + UserId string + ChannelId string + RootId string + ParentId string + UseCount int +} + +const ( + COMMAND_WEBHOOK_LIFETIME = 1000 * 60 * 30 +) + +func (o *CommandWebhook) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + if o.CreateAt == 0 { + o.CreateAt = GetMillis() + } +} + +func (o *CommandWebhook) IsValid() *AppError { + if !IsValidId(o.Id) { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.id.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if !IsValidId(o.CommandId) { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.command_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.UserId) { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.ChannelId) { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.channel_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.RootId) != 0 && !IsValidId(o.RootId) { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.root_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.ParentId) != 0 && !IsValidId(o.ParentId) { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.parent_id.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/compliance.go b/vendor/github.com/mattermost/mattermost-server/v5/model/compliance.go new file mode 100644 index 00000000..a86087c1 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/compliance.go @@ -0,0 +1,119 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "strings" +) + +const ( + COMPLIANCE_STATUS_CREATED = "created" + COMPLIANCE_STATUS_RUNNING = "running" + COMPLIANCE_STATUS_FINISHED = "finished" + COMPLIANCE_STATUS_FAILED = "failed" + COMPLIANCE_STATUS_REMOVED = "removed" + + COMPLIANCE_TYPE_DAILY = "daily" + COMPLIANCE_TYPE_ADHOC = "adhoc" +) + +type Compliance struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UserId string `json:"user_id"` + Status string `json:"status"` + Count int `json:"count"` + Desc string `json:"desc"` + Type string `json:"type"` + StartAt int64 `json:"start_at"` + EndAt int64 `json:"end_at"` + Keywords string `json:"keywords"` + Emails string `json:"emails"` +} + +type Compliances []Compliance + +func (c *Compliance) ToJson() string { + b, _ := json.Marshal(c) + return string(b) +} + +func (c *Compliance) PreSave() { + if c.Id == "" { + c.Id = NewId() + } + + if c.Status == "" { + c.Status = COMPLIANCE_STATUS_CREATED + } + + c.Count = 0 + c.Emails = NormalizeEmail(c.Emails) + c.Keywords = strings.ToLower(c.Keywords) + + c.CreateAt = GetMillis() +} + +func (c *Compliance) JobName() string { + jobName := c.Type + if c.Type == COMPLIANCE_TYPE_DAILY { + jobName += "-" + c.Desc + } + + jobName += "-" + c.Id + + return jobName +} + +func (c *Compliance) IsValid() *AppError { + + if !IsValidId(c.Id) { + return NewAppError("Compliance.IsValid", "model.compliance.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if c.CreateAt == 0 { + return NewAppError("Compliance.IsValid", "model.compliance.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) + } + + if len(c.Desc) > 512 || len(c.Desc) == 0 { + return NewAppError("Compliance.IsValid", "model.compliance.is_valid.desc.app_error", nil, "", http.StatusBadRequest) + } + + if c.StartAt == 0 { + return NewAppError("Compliance.IsValid", "model.compliance.is_valid.start_at.app_error", nil, "", http.StatusBadRequest) + } + + if c.EndAt == 0 { + return NewAppError("Compliance.IsValid", "model.compliance.is_valid.end_at.app_error", nil, "", http.StatusBadRequest) + } + + if c.EndAt <= c.StartAt { + return NewAppError("Compliance.IsValid", "model.compliance.is_valid.start_end_at.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func ComplianceFromJson(data io.Reader) *Compliance { + var c *Compliance + json.NewDecoder(data).Decode(&c) + return c +} + +func (c Compliances) ToJson() string { + if b, err := json.Marshal(c); err != nil { + return "[]" + } else { + return string(b) + } +} + +func CompliancesFromJson(data io.Reader) Compliances { + var o Compliances + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/compliance_post.go b/vendor/github.com/mattermost/mattermost-server/v5/model/compliance_post.go new file mode 100644 index 00000000..fcf65075 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/compliance_post.go @@ -0,0 +1,126 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "regexp" + "time" +) + +type CompliancePost struct { + + // From Team + TeamName string + TeamDisplayName string + + // From Channel + ChannelName string + ChannelDisplayName string + ChannelType string + + // From User + UserUsername string + UserEmail string + UserNickname string + + // From Post + PostId string + PostCreateAt int64 + PostUpdateAt int64 + PostDeleteAt int64 + PostRootId string + PostParentId string + PostOriginalId string + PostMessage string + PostType string + PostProps string + PostHashtags string + PostFileIds string + + IsBot bool +} + +func CompliancePostHeader() []string { + return []string{ + "TeamName", + "TeamDisplayName", + + "ChannelName", + "ChannelDisplayName", + "ChannelType", + + "UserUsername", + "UserEmail", + "UserNickname", + + "PostId", + "PostCreateAt", + "PostUpdateAt", + "PostDeleteAt", + "PostRootId", + "PostParentId", + "PostOriginalId", + "PostMessage", + "PostType", + "PostProps", + "PostHashtags", + "PostFileIds", + "UserType", + } +} + +func cleanComplianceStrings(in string) string { + if matched, _ := regexp.MatchString("^\\s*(=|\\+|\\-)", in); matched { + return "'" + in + + } else { + return in + } +} + +func (me *CompliancePost) Row() []string { + + postDeleteAt := "" + if me.PostDeleteAt > 0 { + postDeleteAt = time.Unix(0, me.PostDeleteAt*int64(1000*1000)).Format(time.RFC3339) + } + + postUpdateAt := "" + if me.PostUpdateAt != me.PostCreateAt { + postUpdateAt = time.Unix(0, me.PostUpdateAt*int64(1000*1000)).Format(time.RFC3339) + } + + userType := "user" + if me.IsBot { + userType = "bot" + } + + return []string{ + cleanComplianceStrings(me.TeamName), + cleanComplianceStrings(me.TeamDisplayName), + + cleanComplianceStrings(me.ChannelName), + cleanComplianceStrings(me.ChannelDisplayName), + cleanComplianceStrings(me.ChannelType), + + cleanComplianceStrings(me.UserUsername), + cleanComplianceStrings(me.UserEmail), + cleanComplianceStrings(me.UserNickname), + userType, + + me.PostId, + time.Unix(0, me.PostCreateAt*int64(1000*1000)).Format(time.RFC3339), + postUpdateAt, + postDeleteAt, + + me.PostRootId, + me.PostParentId, + me.PostOriginalId, + cleanComplianceStrings(me.PostMessage), + me.PostType, + me.PostProps, + me.PostHashtags, + me.PostFileIds, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/config.go b/vendor/github.com/mattermost/mattermost-server/v5/model/config.go new file mode 100644 index 00000000..4ca62e79 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/config.go @@ -0,0 +1,3451 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto/tls" + "encoding/json" + "io" + "math" + "net" + "net/http" + "net/url" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/mattermost/ldap" +) + +const ( + CONN_SECURITY_NONE = "" + CONN_SECURITY_PLAIN = "PLAIN" + CONN_SECURITY_TLS = "TLS" + CONN_SECURITY_STARTTLS = "STARTTLS" + + IMAGE_DRIVER_LOCAL = "local" + IMAGE_DRIVER_S3 = "amazons3" + + DATABASE_DRIVER_SQLITE = "sqlite3" + DATABASE_DRIVER_MYSQL = "mysql" + DATABASE_DRIVER_POSTGRES = "postgres" + + MINIO_ACCESS_KEY = "minioaccesskey" + MINIO_SECRET_KEY = "miniosecretkey" + MINIO_BUCKET = "mattermost-test" + + PASSWORD_MAXIMUM_LENGTH = 64 + PASSWORD_MINIMUM_LENGTH = 5 + + SERVICE_GITLAB = "gitlab" + SERVICE_GOOGLE = "google" + SERVICE_OFFICE365 = "office365" + + GENERIC_NO_CHANNEL_NOTIFICATION = "generic_no_channel" + GENERIC_NOTIFICATION = "generic" + GENERIC_NOTIFICATION_SERVER = "https://push-test.mattermost.com" + FULL_NOTIFICATION = "full" + ID_LOADED_NOTIFICATION = "id_loaded" + + DIRECT_MESSAGE_ANY = "any" + DIRECT_MESSAGE_TEAM = "team" + + SHOW_USERNAME = "username" + SHOW_NICKNAME_FULLNAME = "nickname_full_name" + SHOW_FULLNAME = "full_name" + + PERMISSIONS_ALL = "all" + PERMISSIONS_CHANNEL_ADMIN = "channel_admin" + PERMISSIONS_TEAM_ADMIN = "team_admin" + PERMISSIONS_SYSTEM_ADMIN = "system_admin" + + FAKE_SETTING = "********************************" + + RESTRICT_EMOJI_CREATION_ALL = "all" + RESTRICT_EMOJI_CREATION_ADMIN = "admin" + RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN = "system_admin" + + PERMISSIONS_DELETE_POST_ALL = "all" + PERMISSIONS_DELETE_POST_TEAM_ADMIN = "team_admin" + PERMISSIONS_DELETE_POST_SYSTEM_ADMIN = "system_admin" + + ALLOW_EDIT_POST_ALWAYS = "always" + ALLOW_EDIT_POST_NEVER = "never" + ALLOW_EDIT_POST_TIME_LIMIT = "time_limit" + + GROUP_UNREAD_CHANNELS_DISABLED = "disabled" + GROUP_UNREAD_CHANNELS_DEFAULT_ON = "default_on" + GROUP_UNREAD_CHANNELS_DEFAULT_OFF = "default_off" + + EMAIL_BATCHING_BUFFER_SIZE = 256 + EMAIL_BATCHING_INTERVAL = 30 + + EMAIL_NOTIFICATION_CONTENTS_FULL = "full" + EMAIL_NOTIFICATION_CONTENTS_GENERIC = "generic" + + SITENAME_MAX_LENGTH = 30 + + SERVICE_SETTINGS_DEFAULT_SITE_URL = "http://localhost:8065" + SERVICE_SETTINGS_DEFAULT_TLS_CERT_FILE = "" + SERVICE_SETTINGS_DEFAULT_TLS_KEY_FILE = "" + SERVICE_SETTINGS_DEFAULT_READ_TIMEOUT = 300 + SERVICE_SETTINGS_DEFAULT_WRITE_TIMEOUT = 300 + SERVICE_SETTINGS_DEFAULT_IDLE_TIMEOUT = 60 + SERVICE_SETTINGS_DEFAULT_MAX_LOGIN_ATTEMPTS = 10 + SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM = "" + SERVICE_SETTINGS_DEFAULT_LISTEN_AND_ADDRESS = ":8065" + SERVICE_SETTINGS_DEFAULT_GFYCAT_API_KEY = "2_KtH_W5" + SERVICE_SETTINGS_DEFAULT_GFYCAT_API_SECRET = "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof" + + TEAM_SETTINGS_DEFAULT_SITE_NAME = "Mattermost" + TEAM_SETTINGS_DEFAULT_MAX_USERS_PER_TEAM = 50 + TEAM_SETTINGS_DEFAULT_CUSTOM_BRAND_TEXT = "" + TEAM_SETTINGS_DEFAULT_CUSTOM_DESCRIPTION_TEXT = "" + TEAM_SETTINGS_DEFAULT_USER_STATUS_AWAY_TIMEOUT = 300 + + SQL_SETTINGS_DEFAULT_DATA_SOURCE = "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s" + + FILE_SETTINGS_DEFAULT_DIRECTORY = "./data/" + + EMAIL_SETTINGS_DEFAULT_FEEDBACK_ORGANIZATION = "" + + SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK = "https://about.mattermost.com/default-terms/" + SUPPORT_SETTINGS_DEFAULT_PRIVACY_POLICY_LINK = "https://about.mattermost.com/default-privacy-policy/" + SUPPORT_SETTINGS_DEFAULT_ABOUT_LINK = "https://about.mattermost.com/default-about/" + SUPPORT_SETTINGS_DEFAULT_HELP_LINK = "https://about.mattermost.com/default-help/" + SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK = "https://about.mattermost.com/default-report-a-problem/" + SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL = "feedback@mattermost.com" + SUPPORT_SETTINGS_DEFAULT_RE_ACCEPTANCE_PERIOD = 365 + + LDAP_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE = "" + LDAP_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE = "" + LDAP_SETTINGS_DEFAULT_EMAIL_ATTRIBUTE = "" + LDAP_SETTINGS_DEFAULT_USERNAME_ATTRIBUTE = "" + LDAP_SETTINGS_DEFAULT_NICKNAME_ATTRIBUTE = "" + LDAP_SETTINGS_DEFAULT_ID_ATTRIBUTE = "" + LDAP_SETTINGS_DEFAULT_POSITION_ATTRIBUTE = "" + LDAP_SETTINGS_DEFAULT_LOGIN_FIELD_NAME = "" + LDAP_SETTINGS_DEFAULT_GROUP_DISPLAY_NAME_ATTRIBUTE = "" + LDAP_SETTINGS_DEFAULT_GROUP_ID_ATTRIBUTE = "" + LDAP_SETTINGS_DEFAULT_PICTURE_ATTRIBUTE = "" + + SAML_SETTINGS_DEFAULT_ID_ATTRIBUTE = "" + SAML_SETTINGS_DEFAULT_GUEST_ATTRIBUTE = "" + SAML_SETTINGS_DEFAULT_ADMIN_ATTRIBUTE = "" + SAML_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE = "" + SAML_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE = "" + SAML_SETTINGS_DEFAULT_EMAIL_ATTRIBUTE = "" + SAML_SETTINGS_DEFAULT_USERNAME_ATTRIBUTE = "" + SAML_SETTINGS_DEFAULT_NICKNAME_ATTRIBUTE = "" + SAML_SETTINGS_DEFAULT_LOCALE_ATTRIBUTE = "" + SAML_SETTINGS_DEFAULT_POSITION_ATTRIBUTE = "" + + SAML_SETTINGS_SIGNATURE_ALGORITHM_SHA1 = "RSAwithSHA1" + SAML_SETTINGS_SIGNATURE_ALGORITHM_SHA256 = "RSAwithSHA256" + SAML_SETTINGS_SIGNATURE_ALGORITHM_SHA512 = "RSAwithSHA512" + SAML_SETTINGS_DEFAULT_SIGNATURE_ALGORITHM = SAML_SETTINGS_SIGNATURE_ALGORITHM_SHA1 + + SAML_SETTINGS_CANONICAL_ALGORITHM_C14N = "Canonical1.0" + SAML_SETTINGS_CANONICAL_ALGORITHM_C14N11 = "Canonical1.1" + SAML_SETTINGS_DEFAULT_CANONICAL_ALGORITHM = SAML_SETTINGS_CANONICAL_ALGORITHM_C14N + + NATIVEAPP_SETTINGS_DEFAULT_APP_DOWNLOAD_LINK = "https://mattermost.com/download/#mattermostApps" + NATIVEAPP_SETTINGS_DEFAULT_ANDROID_APP_DOWNLOAD_LINK = "https://about.mattermost.com/mattermost-android-app/" + NATIVEAPP_SETTINGS_DEFAULT_IOS_APP_DOWNLOAD_LINK = "https://about.mattermost.com/mattermost-ios-app/" + + EXPERIMENTAL_SETTINGS_DEFAULT_LINK_METADATA_TIMEOUT_MILLISECONDS = 5000 + + ANALYTICS_SETTINGS_DEFAULT_MAX_USERS_FOR_STATISTICS = 2500 + + ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_COLOR = "#f2a93b" + ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_TEXT_COLOR = "#333333" + + TEAM_SETTINGS_DEFAULT_TEAM_TEXT = "default" + + ELASTICSEARCH_SETTINGS_DEFAULT_CONNECTION_URL = "http://localhost:9200" + ELASTICSEARCH_SETTINGS_DEFAULT_USERNAME = "elastic" + ELASTICSEARCH_SETTINGS_DEFAULT_PASSWORD = "changeme" + ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_REPLICAS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_SHARDS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_CHANNEL_INDEX_REPLICAS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_CHANNEL_INDEX_SHARDS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_USER_INDEX_REPLICAS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_USER_INDEX_SHARDS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_AGGREGATE_POSTS_AFTER_DAYS = 365 + ELASTICSEARCH_SETTINGS_DEFAULT_POSTS_AGGREGATOR_JOB_START_TIME = "03:00" + ELASTICSEARCH_SETTINGS_DEFAULT_INDEX_PREFIX = "" + ELASTICSEARCH_SETTINGS_DEFAULT_LIVE_INDEXING_BATCH_SIZE = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_BULK_INDEXING_TIME_WINDOW_SECONDS = 3600 + ELASTICSEARCH_SETTINGS_DEFAULT_REQUEST_TIMEOUT_SECONDS = 30 + + BLEVE_SETTINGS_DEFAULT_INDEX_DIR = "" + BLEVE_SETTINGS_DEFAULT_BULK_INDEXING_TIME_WINDOW_SECONDS = 3600 + + DATA_RETENTION_SETTINGS_DEFAULT_MESSAGE_RETENTION_DAYS = 365 + DATA_RETENTION_SETTINGS_DEFAULT_FILE_RETENTION_DAYS = 365 + DATA_RETENTION_SETTINGS_DEFAULT_DELETION_JOB_START_TIME = "02:00" + + PLUGIN_SETTINGS_DEFAULT_DIRECTORY = "./plugins" + PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY = "./client/plugins" + PLUGIN_SETTINGS_DEFAULT_ENABLE_MARKETPLACE = true + PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL = "https://api.integrations.mattermost.com" + PLUGIN_SETTINGS_OLD_MARKETPLACE_URL = "https://marketplace.integrations.mattermost.com" + + COMPLIANCE_EXPORT_TYPE_CSV = "csv" + COMPLIANCE_EXPORT_TYPE_ACTIANCE = "actiance" + COMPLIANCE_EXPORT_TYPE_GLOBALRELAY = "globalrelay" + COMPLIANCE_EXPORT_TYPE_GLOBALRELAY_ZIP = "globalrelay-zip" + GLOBALRELAY_CUSTOMER_TYPE_A9 = "A9" + GLOBALRELAY_CUSTOMER_TYPE_A10 = "A10" + + CLIENT_SIDE_CERT_CHECK_PRIMARY_AUTH = "primary" + CLIENT_SIDE_CERT_CHECK_SECONDARY_AUTH = "secondary" + + IMAGE_PROXY_TYPE_LOCAL = "local" + IMAGE_PROXY_TYPE_ATMOS_CAMO = "atmos/camo" + + GOOGLE_SETTINGS_DEFAULT_SCOPE = "profile email" + GOOGLE_SETTINGS_DEFAULT_AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth" + GOOGLE_SETTINGS_DEFAULT_TOKEN_ENDPOINT = "https://www.googleapis.com/oauth2/v4/token" + GOOGLE_SETTINGS_DEFAULT_USER_API_ENDPOINT = "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata" + + OFFICE365_SETTINGS_DEFAULT_SCOPE = "User.Read" + OFFICE365_SETTINGS_DEFAULT_AUTH_ENDPOINT = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" + OFFICE365_SETTINGS_DEFAULT_TOKEN_ENDPOINT = "https://login.microsoftonline.com/common/oauth2/v2.0/token" + OFFICE365_SETTINGS_DEFAULT_USER_API_ENDPOINT = "https://graph.microsoft.com/v1.0/me" + + LOCAL_MODE_SOCKET_PATH = "/var/tmp/mattermost_local.socket" +) + +var ServerTLSSupportedCiphers = map[string]uint16{ + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, +} + +type ServiceSettings struct { + SiteURL *string `restricted:"true"` + WebsocketURL *string `restricted:"true"` + LicenseFileLocation *string `restricted:"true"` + ListenAddress *string `restricted:"true"` + ConnectionSecurity *string `restricted:"true"` + TLSCertFile *string `restricted:"true"` + TLSKeyFile *string `restricted:"true"` + TLSMinVer *string `restricted:"true"` + TLSStrictTransport *bool `restricted:"true"` + TLSStrictTransportMaxAge *int64 `restricted:"true"` + TLSOverwriteCiphers []string `restricted:"true"` + UseLetsEncrypt *bool `restricted:"true"` + LetsEncryptCertificateCacheFile *string `restricted:"true"` + Forward80To443 *bool `restricted:"true"` + TrustedProxyIPHeader []string `restricted:"true"` + ReadTimeout *int `restricted:"true"` + WriteTimeout *int `restricted:"true"` + IdleTimeout *int `restricted:"true"` + MaximumLoginAttempts *int `restricted:"true"` + GoroutineHealthThreshold *int `restricted:"true"` + GoogleDeveloperKey *string `restricted:"true"` + EnableOAuthServiceProvider *bool + EnableIncomingWebhooks *bool + EnableOutgoingWebhooks *bool + EnableCommands *bool + DEPRECATED_DO_NOT_USE_EnableOnlyAdminIntegrations *bool `json:"EnableOnlyAdminIntegrations" mapstructure:"EnableOnlyAdminIntegrations"` // This field is deprecated and must not be used. + EnablePostUsernameOverride *bool + EnablePostIconOverride *bool + EnableLinkPreviews *bool + EnableTesting *bool `restricted:"true"` + EnableDeveloper *bool `restricted:"true"` + EnableOpenTracing *bool `restricted:"true"` + EnableSecurityFixAlert *bool `restricted:"true"` + EnableInsecureOutgoingConnections *bool `restricted:"true"` + AllowedUntrustedInternalConnections *string `restricted:"true"` + EnableMultifactorAuthentication *bool + EnforceMultifactorAuthentication *bool + EnableUserAccessTokens *bool + AllowCorsFrom *string `restricted:"true"` + CorsExposedHeaders *string `restricted:"true"` + CorsAllowCredentials *bool `restricted:"true"` + CorsDebug *bool `restricted:"true"` + AllowCookiesForSubdomains *bool `restricted:"true"` + ExtendSessionLengthWithActivity *bool `restricted:"true"` + SessionLengthWebInDays *int `restricted:"true"` + SessionLengthMobileInDays *int `restricted:"true"` + SessionLengthSSOInDays *int `restricted:"true"` + SessionCacheInMinutes *int `restricted:"true"` + SessionIdleTimeoutInMinutes *int `restricted:"true"` + WebsocketSecurePort *int `restricted:"true"` + WebsocketPort *int `restricted:"true"` + WebserverMode *string `restricted:"true"` + EnableCustomEmoji *bool + EnableEmojiPicker *bool + EnableGifPicker *bool + GfycatApiKey *string + GfycatApiSecret *string + DEPRECATED_DO_NOT_USE_RestrictCustomEmojiCreation *string `json:"RestrictCustomEmojiCreation" mapstructure:"RestrictCustomEmojiCreation"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_RestrictPostDelete *string `json:"RestrictPostDelete" mapstructure:"RestrictPostDelete"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_AllowEditPost *string `json:"AllowEditPost" mapstructure:"AllowEditPost"` // This field is deprecated and must not be used. + PostEditTimeLimit *int + TimeBetweenUserTypingUpdatesMilliseconds *int64 `restricted:"true"` + EnablePostSearch *bool `restricted:"true"` + MinimumHashtagLength *int `restricted:"true"` + EnableUserTypingMessages *bool `restricted:"true"` + EnableChannelViewedMessages *bool `restricted:"true"` + EnableUserStatuses *bool `restricted:"true"` + ExperimentalEnableAuthenticationTransfer *bool `restricted:"true"` + ClusterLogTimeoutMilliseconds *int `restricted:"true"` + CloseUnusedDirectMessages *bool + EnablePreviewFeatures *bool + EnableTutorial *bool + ExperimentalEnableDefaultChannelLeaveJoinMessages *bool + ExperimentalGroupUnreadChannels *string + ExperimentalChannelOrganization *bool + ExperimentalChannelSidebarOrganization *string + DEPRECATED_DO_NOT_USE_ImageProxyType *string `json:"ImageProxyType" mapstructure:"ImageProxyType"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_ImageProxyURL *string `json:"ImageProxyURL" mapstructure:"ImageProxyURL"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_ImageProxyOptions *string `json:"ImageProxyOptions" mapstructure:"ImageProxyOptions"` // This field is deprecated and must not be used. + EnableAPITeamDeletion *bool + ExperimentalEnableHardenedMode *bool + DisableLegacyMFA *bool `restricted:"true"` + ExperimentalStrictCSRFEnforcement *bool `restricted:"true"` + EnableEmailInvitations *bool + DisableBotsWhenOwnerIsDeactivated *bool `restricted:"true"` + EnableBotAccountCreation *bool + EnableSVGs *bool + EnableLatex *bool + EnableLocalMode *bool + LocalModeSocketLocation *string +} + +func (s *ServiceSettings) SetDefaults(isUpdate bool) { + if s.EnableEmailInvitations == nil { + // If the site URL is also not present then assume this is a clean install + if s.SiteURL == nil { + s.EnableEmailInvitations = NewBool(false) + } else { + s.EnableEmailInvitations = NewBool(true) + } + } + + if s.SiteURL == nil { + if s.EnableDeveloper != nil && *s.EnableDeveloper { + s.SiteURL = NewString(SERVICE_SETTINGS_DEFAULT_SITE_URL) + } else { + s.SiteURL = NewString("") + } + } + + if s.WebsocketURL == nil { + s.WebsocketURL = NewString("") + } + + if s.LicenseFileLocation == nil { + s.LicenseFileLocation = NewString("") + } + + if s.ListenAddress == nil { + s.ListenAddress = NewString(SERVICE_SETTINGS_DEFAULT_LISTEN_AND_ADDRESS) + } + + if s.EnableLinkPreviews == nil { + s.EnableLinkPreviews = NewBool(true) + } + + if s.EnableTesting == nil { + s.EnableTesting = NewBool(false) + } + + if s.EnableDeveloper == nil { + s.EnableDeveloper = NewBool(false) + } + + if s.EnableOpenTracing == nil { + s.EnableOpenTracing = NewBool(false) + } + + if s.EnableSecurityFixAlert == nil { + s.EnableSecurityFixAlert = NewBool(true) + } + + if s.EnableInsecureOutgoingConnections == nil { + s.EnableInsecureOutgoingConnections = NewBool(false) + } + + if s.AllowedUntrustedInternalConnections == nil { + s.AllowedUntrustedInternalConnections = NewString("") + } + + if s.EnableMultifactorAuthentication == nil { + s.EnableMultifactorAuthentication = NewBool(false) + } + + if s.EnforceMultifactorAuthentication == nil { + s.EnforceMultifactorAuthentication = NewBool(false) + } + + if s.EnableUserAccessTokens == nil { + s.EnableUserAccessTokens = NewBool(false) + } + + if s.GoroutineHealthThreshold == nil { + s.GoroutineHealthThreshold = NewInt(-1) + } + + if s.GoogleDeveloperKey == nil { + s.GoogleDeveloperKey = NewString("") + } + + if s.EnableOAuthServiceProvider == nil { + s.EnableOAuthServiceProvider = NewBool(false) + } + + if s.EnableIncomingWebhooks == nil { + s.EnableIncomingWebhooks = NewBool(true) + } + + if s.EnableOutgoingWebhooks == nil { + s.EnableOutgoingWebhooks = NewBool(true) + } + + if s.ConnectionSecurity == nil { + s.ConnectionSecurity = NewString("") + } + + if s.TLSKeyFile == nil { + s.TLSKeyFile = NewString(SERVICE_SETTINGS_DEFAULT_TLS_KEY_FILE) + } + + if s.TLSCertFile == nil { + s.TLSCertFile = NewString(SERVICE_SETTINGS_DEFAULT_TLS_CERT_FILE) + } + + if s.TLSMinVer == nil { + s.TLSMinVer = NewString("1.2") + } + + if s.TLSStrictTransport == nil { + s.TLSStrictTransport = NewBool(false) + } + + if s.TLSStrictTransportMaxAge == nil { + s.TLSStrictTransportMaxAge = NewInt64(63072000) + } + + if s.TLSOverwriteCiphers == nil { + s.TLSOverwriteCiphers = []string{} + } + + if s.UseLetsEncrypt == nil { + s.UseLetsEncrypt = NewBool(false) + } + + if s.LetsEncryptCertificateCacheFile == nil { + s.LetsEncryptCertificateCacheFile = NewString("./config/letsencrypt.cache") + } + + if s.ReadTimeout == nil { + s.ReadTimeout = NewInt(SERVICE_SETTINGS_DEFAULT_READ_TIMEOUT) + } + + if s.WriteTimeout == nil { + s.WriteTimeout = NewInt(SERVICE_SETTINGS_DEFAULT_WRITE_TIMEOUT) + } + + if s.IdleTimeout == nil { + s.IdleTimeout = NewInt(SERVICE_SETTINGS_DEFAULT_IDLE_TIMEOUT) + } + + if s.MaximumLoginAttempts == nil { + s.MaximumLoginAttempts = NewInt(SERVICE_SETTINGS_DEFAULT_MAX_LOGIN_ATTEMPTS) + } + + if s.Forward80To443 == nil { + s.Forward80To443 = NewBool(false) + } + + if isUpdate { + // When updating an existing configuration, ensure that defaults are set. + if s.TrustedProxyIPHeader == nil { + s.TrustedProxyIPHeader = []string{HEADER_FORWARDED, HEADER_REAL_IP} + } + } else { + // When generating a blank configuration, leave the list empty. + s.TrustedProxyIPHeader = []string{} + } + + if s.TimeBetweenUserTypingUpdatesMilliseconds == nil { + s.TimeBetweenUserTypingUpdatesMilliseconds = NewInt64(5000) + } + + if s.EnablePostSearch == nil { + s.EnablePostSearch = NewBool(true) + } + + if s.MinimumHashtagLength == nil { + s.MinimumHashtagLength = NewInt(3) + } + + if s.EnableUserTypingMessages == nil { + s.EnableUserTypingMessages = NewBool(true) + } + + if s.EnableChannelViewedMessages == nil { + s.EnableChannelViewedMessages = NewBool(true) + } + + if s.EnableUserStatuses == nil { + s.EnableUserStatuses = NewBool(true) + } + + if s.ClusterLogTimeoutMilliseconds == nil { + s.ClusterLogTimeoutMilliseconds = NewInt(2000) + } + + if s.CloseUnusedDirectMessages == nil { + s.CloseUnusedDirectMessages = NewBool(false) + } + + if s.EnableTutorial == nil { + s.EnableTutorial = NewBool(true) + } + + // Must be manually enabled for existing installations. + if s.ExtendSessionLengthWithActivity == nil { + s.ExtendSessionLengthWithActivity = NewBool(!isUpdate) + } + + if s.SessionLengthWebInDays == nil { + if isUpdate { + s.SessionLengthWebInDays = NewInt(180) + } else { + s.SessionLengthWebInDays = NewInt(30) + } + } + + if s.SessionLengthMobileInDays == nil { + if isUpdate { + s.SessionLengthMobileInDays = NewInt(180) + } else { + s.SessionLengthMobileInDays = NewInt(30) + } + } + + if s.SessionLengthSSOInDays == nil { + s.SessionLengthSSOInDays = NewInt(30) + } + + if s.SessionCacheInMinutes == nil { + s.SessionCacheInMinutes = NewInt(10) + } + + if s.SessionIdleTimeoutInMinutes == nil { + s.SessionIdleTimeoutInMinutes = NewInt(43200) + } + + if s.EnableCommands == nil { + s.EnableCommands = NewBool(true) + } + + if s.DEPRECATED_DO_NOT_USE_EnableOnlyAdminIntegrations == nil { + s.DEPRECATED_DO_NOT_USE_EnableOnlyAdminIntegrations = NewBool(true) + } + + if s.EnablePostUsernameOverride == nil { + s.EnablePostUsernameOverride = NewBool(false) + } + + if s.EnablePostIconOverride == nil { + s.EnablePostIconOverride = NewBool(false) + } + + if s.WebsocketPort == nil { + s.WebsocketPort = NewInt(80) + } + + if s.WebsocketSecurePort == nil { + s.WebsocketSecurePort = NewInt(443) + } + + if s.AllowCorsFrom == nil { + s.AllowCorsFrom = NewString(SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM) + } + + if s.CorsExposedHeaders == nil { + s.CorsExposedHeaders = NewString("") + } + + if s.CorsAllowCredentials == nil { + s.CorsAllowCredentials = NewBool(false) + } + + if s.CorsDebug == nil { + s.CorsDebug = NewBool(false) + } + + if s.AllowCookiesForSubdomains == nil { + s.AllowCookiesForSubdomains = NewBool(false) + } + + if s.WebserverMode == nil { + s.WebserverMode = NewString("gzip") + } else if *s.WebserverMode == "regular" { + *s.WebserverMode = "gzip" + } + + if s.EnableCustomEmoji == nil { + s.EnableCustomEmoji = NewBool(false) + } + + if s.EnableEmojiPicker == nil { + s.EnableEmojiPicker = NewBool(true) + } + + if s.EnableGifPicker == nil { + s.EnableGifPicker = NewBool(false) + } + + if s.GfycatApiKey == nil || *s.GfycatApiKey == "" { + s.GfycatApiKey = NewString(SERVICE_SETTINGS_DEFAULT_GFYCAT_API_KEY) + } + + if s.GfycatApiSecret == nil || *s.GfycatApiSecret == "" { + s.GfycatApiSecret = NewString(SERVICE_SETTINGS_DEFAULT_GFYCAT_API_SECRET) + } + + if s.DEPRECATED_DO_NOT_USE_RestrictCustomEmojiCreation == nil { + s.DEPRECATED_DO_NOT_USE_RestrictCustomEmojiCreation = NewString(RESTRICT_EMOJI_CREATION_ALL) + } + + if s.DEPRECATED_DO_NOT_USE_RestrictPostDelete == nil { + s.DEPRECATED_DO_NOT_USE_RestrictPostDelete = NewString(PERMISSIONS_DELETE_POST_ALL) + } + + if s.DEPRECATED_DO_NOT_USE_AllowEditPost == nil { + s.DEPRECATED_DO_NOT_USE_AllowEditPost = NewString(ALLOW_EDIT_POST_ALWAYS) + } + + if s.ExperimentalEnableAuthenticationTransfer == nil { + s.ExperimentalEnableAuthenticationTransfer = NewBool(true) + } + + if s.PostEditTimeLimit == nil { + s.PostEditTimeLimit = NewInt(-1) + } + + if s.EnablePreviewFeatures == nil { + s.EnablePreviewFeatures = NewBool(true) + } + + if s.ExperimentalEnableDefaultChannelLeaveJoinMessages == nil { + s.ExperimentalEnableDefaultChannelLeaveJoinMessages = NewBool(true) + } + + if s.ExperimentalGroupUnreadChannels == nil { + s.ExperimentalGroupUnreadChannels = NewString(GROUP_UNREAD_CHANNELS_DISABLED) + } else if *s.ExperimentalGroupUnreadChannels == "0" { + s.ExperimentalGroupUnreadChannels = NewString(GROUP_UNREAD_CHANNELS_DISABLED) + } else if *s.ExperimentalGroupUnreadChannels == "1" { + s.ExperimentalGroupUnreadChannels = NewString(GROUP_UNREAD_CHANNELS_DEFAULT_ON) + } + + if s.ExperimentalChannelOrganization == nil { + experimentalUnreadEnabled := *s.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DISABLED + s.ExperimentalChannelOrganization = NewBool(experimentalUnreadEnabled) + } + + if s.ExperimentalChannelSidebarOrganization == nil { + s.ExperimentalChannelSidebarOrganization = NewString("disabled") + } + + if s.DEPRECATED_DO_NOT_USE_ImageProxyType == nil { + s.DEPRECATED_DO_NOT_USE_ImageProxyType = NewString("") + } + + if s.DEPRECATED_DO_NOT_USE_ImageProxyURL == nil { + s.DEPRECATED_DO_NOT_USE_ImageProxyURL = NewString("") + } + + if s.DEPRECATED_DO_NOT_USE_ImageProxyOptions == nil { + s.DEPRECATED_DO_NOT_USE_ImageProxyOptions = NewString("") + } + + if s.EnableAPITeamDeletion == nil { + s.EnableAPITeamDeletion = NewBool(false) + } + + if s.ExperimentalEnableHardenedMode == nil { + s.ExperimentalEnableHardenedMode = NewBool(false) + } + + if s.DisableLegacyMFA == nil { + s.DisableLegacyMFA = NewBool(!isUpdate) + } + + if s.ExperimentalStrictCSRFEnforcement == nil { + s.ExperimentalStrictCSRFEnforcement = NewBool(false) + } + + if s.DisableBotsWhenOwnerIsDeactivated == nil { + s.DisableBotsWhenOwnerIsDeactivated = NewBool(true) + } + + if s.EnableBotAccountCreation == nil { + s.EnableBotAccountCreation = NewBool(false) + } + + if s.EnableSVGs == nil { + if isUpdate { + s.EnableSVGs = NewBool(true) + } else { + s.EnableSVGs = NewBool(false) + } + } + + if s.EnableLatex == nil { + if isUpdate { + s.EnableLatex = NewBool(true) + } else { + s.EnableLatex = NewBool(false) + } + } + + if s.EnableLocalMode == nil { + s.EnableLocalMode = NewBool(false) + } + + if s.LocalModeSocketLocation == nil { + s.LocalModeSocketLocation = NewString(LOCAL_MODE_SOCKET_PATH) + } +} + +type ClusterSettings struct { + Enable *bool `restricted:"true"` + ClusterName *string `restricted:"true"` + OverrideHostname *string `restricted:"true"` + NetworkInterface *string `restricted:"true"` + BindAddress *string `restricted:"true"` + AdvertiseAddress *string `restricted:"true"` + UseIpAddress *bool `restricted:"true"` + UseExperimentalGossip *bool `restricted:"true"` + ReadOnlyConfig *bool `restricted:"true"` + GossipPort *int `restricted:"true"` + StreamingPort *int `restricted:"true"` + MaxIdleConns *int `restricted:"true"` + MaxIdleConnsPerHost *int `restricted:"true"` + IdleConnTimeoutMilliseconds *int `restricted:"true"` +} + +func (s *ClusterSettings) SetDefaults() { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.ClusterName == nil { + s.ClusterName = NewString("") + } + + if s.OverrideHostname == nil { + s.OverrideHostname = NewString("") + } + + if s.NetworkInterface == nil { + s.NetworkInterface = NewString("") + } + + if s.BindAddress == nil { + s.BindAddress = NewString("") + } + + if s.AdvertiseAddress == nil { + s.AdvertiseAddress = NewString("") + } + + if s.UseIpAddress == nil { + s.UseIpAddress = NewBool(true) + } + + if s.UseExperimentalGossip == nil { + s.UseExperimentalGossip = NewBool(false) + } + + if s.ReadOnlyConfig == nil { + s.ReadOnlyConfig = NewBool(true) + } + + if s.GossipPort == nil { + s.GossipPort = NewInt(8074) + } + + if s.StreamingPort == nil { + s.StreamingPort = NewInt(8075) + } + + if s.MaxIdleConns == nil { + s.MaxIdleConns = NewInt(100) + } + + if s.MaxIdleConnsPerHost == nil { + s.MaxIdleConnsPerHost = NewInt(128) + } + + if s.IdleConnTimeoutMilliseconds == nil { + s.IdleConnTimeoutMilliseconds = NewInt(90000) + } +} + +type MetricsSettings struct { + Enable *bool `restricted:"true"` + BlockProfileRate *int `restricted:"true"` + ListenAddress *string `restricted:"true"` +} + +func (s *MetricsSettings) SetDefaults() { + if s.ListenAddress == nil { + s.ListenAddress = NewString(":8067") + } + + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.BlockProfileRate == nil { + s.BlockProfileRate = NewInt(0) + } +} + +type ExperimentalSettings struct { + ClientSideCertEnable *bool + ClientSideCertCheck *string + EnableClickToReply *bool `restricted:"true"` + LinkMetadataTimeoutMilliseconds *int64 `restricted:"true"` + RestrictSystemAdmin *bool `restricted:"true"` + UseNewSAMLLibrary *bool +} + +func (s *ExperimentalSettings) SetDefaults() { + if s.ClientSideCertEnable == nil { + s.ClientSideCertEnable = NewBool(false) + } + + if s.ClientSideCertCheck == nil { + s.ClientSideCertCheck = NewString(CLIENT_SIDE_CERT_CHECK_SECONDARY_AUTH) + } + + if s.EnableClickToReply == nil { + s.EnableClickToReply = NewBool(false) + } + + if s.LinkMetadataTimeoutMilliseconds == nil { + s.LinkMetadataTimeoutMilliseconds = NewInt64(EXPERIMENTAL_SETTINGS_DEFAULT_LINK_METADATA_TIMEOUT_MILLISECONDS) + } + + if s.RestrictSystemAdmin == nil { + s.RestrictSystemAdmin = NewBool(false) + } + if s.UseNewSAMLLibrary == nil { + s.UseNewSAMLLibrary = NewBool(false) + } +} + +type AnalyticsSettings struct { + MaxUsersForStatistics *int `restricted:"true"` +} + +func (s *AnalyticsSettings) SetDefaults() { + if s.MaxUsersForStatistics == nil { + s.MaxUsersForStatistics = NewInt(ANALYTICS_SETTINGS_DEFAULT_MAX_USERS_FOR_STATISTICS) + } +} + +type SSOSettings struct { + Enable *bool + Secret *string + Id *string + Scope *string + AuthEndpoint *string + TokenEndpoint *string + UserApiEndpoint *string +} + +func (s *SSOSettings) setDefaults(scope, authEndpoint, tokenEndpoint, userApiEndpoint string) { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.Secret == nil { + s.Secret = NewString("") + } + + if s.Id == nil { + s.Id = NewString("") + } + + if s.Scope == nil { + s.Scope = NewString(scope) + } + + if s.AuthEndpoint == nil { + s.AuthEndpoint = NewString(authEndpoint) + } + + if s.TokenEndpoint == nil { + s.TokenEndpoint = NewString(tokenEndpoint) + } + + if s.UserApiEndpoint == nil { + s.UserApiEndpoint = NewString(userApiEndpoint) + } +} + +type Office365Settings struct { + Enable *bool + Secret *string + Id *string + Scope *string + AuthEndpoint *string + TokenEndpoint *string + UserApiEndpoint *string + DirectoryId *string +} + +func (s *Office365Settings) setDefaults() { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.Id == nil { + s.Id = NewString("") + } + + if s.Secret == nil { + s.Secret = NewString("") + } + + if s.Scope == nil { + s.Scope = NewString(OFFICE365_SETTINGS_DEFAULT_SCOPE) + } + + if s.AuthEndpoint == nil { + s.AuthEndpoint = NewString(OFFICE365_SETTINGS_DEFAULT_AUTH_ENDPOINT) + } + + if s.TokenEndpoint == nil { + s.TokenEndpoint = NewString(OFFICE365_SETTINGS_DEFAULT_TOKEN_ENDPOINT) + } + + if s.UserApiEndpoint == nil { + s.UserApiEndpoint = NewString(OFFICE365_SETTINGS_DEFAULT_USER_API_ENDPOINT) + } + + if s.DirectoryId == nil { + s.DirectoryId = NewString("") + } +} + +func (s *Office365Settings) SSOSettings() *SSOSettings { + ssoSettings := SSOSettings{} + ssoSettings.Enable = s.Enable + ssoSettings.Secret = s.Secret + ssoSettings.Id = s.Id + ssoSettings.Scope = s.Scope + ssoSettings.AuthEndpoint = s.AuthEndpoint + ssoSettings.TokenEndpoint = s.TokenEndpoint + ssoSettings.UserApiEndpoint = s.UserApiEndpoint + return &ssoSettings +} + +type SqlSettings struct { + DriverName *string `restricted:"true"` + DataSource *string `restricted:"true"` + DataSourceReplicas []string `restricted:"true"` + DataSourceSearchReplicas []string `restricted:"true"` + MaxIdleConns *int `restricted:"true"` + ConnMaxLifetimeMilliseconds *int `restricted:"true"` + MaxOpenConns *int `restricted:"true"` + Trace *bool `restricted:"true"` + AtRestEncryptKey *string `restricted:"true"` + QueryTimeout *int `restricted:"true"` + DisableDatabaseSearch *bool `restricted:"true"` +} + +func (s *SqlSettings) SetDefaults(isUpdate bool) { + if s.DriverName == nil { + s.DriverName = NewString(DATABASE_DRIVER_MYSQL) + } + + if s.DataSource == nil { + s.DataSource = NewString(SQL_SETTINGS_DEFAULT_DATA_SOURCE) + } + + if s.DataSourceReplicas == nil { + s.DataSourceReplicas = []string{} + } + + if s.DataSourceSearchReplicas == nil { + s.DataSourceSearchReplicas = []string{} + } + + if isUpdate { + // When updating an existing configuration, ensure an encryption key has been specified. + if s.AtRestEncryptKey == nil || len(*s.AtRestEncryptKey) == 0 { + s.AtRestEncryptKey = NewString(NewRandomString(32)) + } + } else { + // When generating a blank configuration, leave this key empty to be generated on server start. + s.AtRestEncryptKey = NewString("") + } + + if s.MaxIdleConns == nil { + s.MaxIdleConns = NewInt(20) + } + + if s.MaxOpenConns == nil { + s.MaxOpenConns = NewInt(300) + } + + if s.ConnMaxLifetimeMilliseconds == nil { + s.ConnMaxLifetimeMilliseconds = NewInt(3600000) + } + + if s.Trace == nil { + s.Trace = NewBool(false) + } + + if s.QueryTimeout == nil { + s.QueryTimeout = NewInt(30) + } + + if s.DisableDatabaseSearch == nil { + s.DisableDatabaseSearch = NewBool(false) + } +} + +type LogSettings struct { + EnableConsole *bool `restricted:"true"` + ConsoleLevel *string `restricted:"true"` + ConsoleJson *bool `restricted:"true"` + EnableFile *bool `restricted:"true"` + FileLevel *string `restricted:"true"` + FileJson *bool `restricted:"true"` + FileLocation *string `restricted:"true"` + EnableWebhookDebugging *bool `restricted:"true"` + EnableDiagnostics *bool `restricted:"true"` +} + +func (s *LogSettings) SetDefaults() { + if s.EnableConsole == nil { + s.EnableConsole = NewBool(true) + } + + if s.ConsoleLevel == nil { + s.ConsoleLevel = NewString("DEBUG") + } + + if s.EnableFile == nil { + s.EnableFile = NewBool(true) + } + + if s.FileLevel == nil { + s.FileLevel = NewString("INFO") + } + + if s.FileLocation == nil { + s.FileLocation = NewString("") + } + + if s.EnableWebhookDebugging == nil { + s.EnableWebhookDebugging = NewBool(true) + } + + if s.EnableDiagnostics == nil { + s.EnableDiagnostics = NewBool(true) + } + + if s.ConsoleJson == nil { + s.ConsoleJson = NewBool(true) + } + + if s.FileJson == nil { + s.FileJson = NewBool(true) + } +} + +type ExperimentalAuditSettings struct { + SysLogEnabled *bool `restricted:"true"` + SysLogIP *string `restricted:"true"` + SysLogPort *int `restricted:"true"` + SysLogTag *string `restricted:"true"` + SysLogCert *string `restricted:"true"` + SysLogInsecure *bool `restricted:"true"` + SysLogMaxQueueSize *int `restricted:"true"` + + FileEnabled *bool `restricted:"true"` + FileName *string `restricted:"true"` + FileMaxSizeMB *int `restricted:"true"` + FileMaxAgeDays *int `restricted:"true"` + FileMaxBackups *int `restricted:"true"` + FileCompress *bool `restricted:"true"` + FileMaxQueueSize *int `restricted:"true"` +} + +func (s *ExperimentalAuditSettings) SetDefaults() { + if s.SysLogEnabled == nil { + s.SysLogEnabled = NewBool(false) + } + + if s.SysLogIP == nil { + s.SysLogIP = NewString("localhost") + } + + if s.SysLogPort == nil { + s.SysLogPort = NewInt(6514) + } + + if s.SysLogTag == nil { + s.SysLogTag = NewString("") + } + + if s.SysLogCert == nil { + s.SysLogCert = NewString("") + } + + if s.SysLogInsecure == nil { + s.SysLogInsecure = NewBool(false) + } + + if s.SysLogMaxQueueSize == nil { + s.SysLogMaxQueueSize = NewInt(1000) + } + + if s.FileEnabled == nil { + s.FileEnabled = NewBool(false) + } + + if s.FileName == nil { + s.FileName = NewString("") + } + + if s.FileMaxSizeMB == nil { + s.FileMaxSizeMB = NewInt(100) + } + + if s.FileMaxAgeDays == nil { + s.FileMaxAgeDays = NewInt(0) // no limit on age + } + + if s.FileMaxBackups == nil { // no limit on number of backups + s.FileMaxBackups = NewInt(0) + } + + if s.FileCompress == nil { + s.FileCompress = NewBool(false) + } + + if s.FileMaxQueueSize == nil { + s.FileMaxQueueSize = NewInt(1000) + } +} + +type NotificationLogSettings struct { + EnableConsole *bool `restricted:"true"` + ConsoleLevel *string `restricted:"true"` + ConsoleJson *bool `restricted:"true"` + EnableFile *bool `restricted:"true"` + FileLevel *string `restricted:"true"` + FileJson *bool `restricted:"true"` + FileLocation *string `restricted:"true"` +} + +func (s *NotificationLogSettings) SetDefaults() { + if s.EnableConsole == nil { + s.EnableConsole = NewBool(true) + } + + if s.ConsoleLevel == nil { + s.ConsoleLevel = NewString("DEBUG") + } + + if s.EnableFile == nil { + s.EnableFile = NewBool(true) + } + + if s.FileLevel == nil { + s.FileLevel = NewString("INFO") + } + + if s.FileLocation == nil { + s.FileLocation = NewString("") + } + + if s.ConsoleJson == nil { + s.ConsoleJson = NewBool(true) + } + + if s.FileJson == nil { + s.FileJson = NewBool(true) + } +} + +type PasswordSettings struct { + MinimumLength *int + Lowercase *bool + Number *bool + Uppercase *bool + Symbol *bool +} + +func (s *PasswordSettings) SetDefaults() { + if s.MinimumLength == nil { + s.MinimumLength = NewInt(10) + } + + if s.Lowercase == nil { + s.Lowercase = NewBool(true) + } + + if s.Number == nil { + s.Number = NewBool(true) + } + + if s.Uppercase == nil { + s.Uppercase = NewBool(true) + } + + if s.Symbol == nil { + s.Symbol = NewBool(true) + } +} + +type FileSettings struct { + EnableFileAttachments *bool + EnableMobileUpload *bool + EnableMobileDownload *bool + MaxFileSize *int64 + DriverName *string `restricted:"true"` + Directory *string `restricted:"true"` + EnablePublicLink *bool + PublicLinkSalt *string + InitialFont *string + AmazonS3AccessKeyId *string `restricted:"true"` + AmazonS3SecretAccessKey *string `restricted:"true"` + AmazonS3Bucket *string `restricted:"true"` + AmazonS3Region *string `restricted:"true"` + AmazonS3Endpoint *string `restricted:"true"` + AmazonS3SSL *bool `restricted:"true"` + AmazonS3SignV2 *bool `restricted:"true"` + AmazonS3SSE *bool `restricted:"true"` + AmazonS3Trace *bool `restricted:"true"` +} + +func (s *FileSettings) SetDefaults(isUpdate bool) { + if s.EnableFileAttachments == nil { + s.EnableFileAttachments = NewBool(true) + } + + if s.EnableMobileUpload == nil { + s.EnableMobileUpload = NewBool(true) + } + + if s.EnableMobileDownload == nil { + s.EnableMobileDownload = NewBool(true) + } + + if s.MaxFileSize == nil { + s.MaxFileSize = NewInt64(52428800) // 50 MB + } + + if s.DriverName == nil { + s.DriverName = NewString(IMAGE_DRIVER_LOCAL) + } + + if s.Directory == nil { + s.Directory = NewString(FILE_SETTINGS_DEFAULT_DIRECTORY) + } + + if s.EnablePublicLink == nil { + s.EnablePublicLink = NewBool(false) + } + + if isUpdate { + // When updating an existing configuration, ensure link salt has been specified. + if s.PublicLinkSalt == nil || len(*s.PublicLinkSalt) == 0 { + s.PublicLinkSalt = NewString(NewRandomString(32)) + } + } else { + // When generating a blank configuration, leave link salt empty to be generated on server start. + s.PublicLinkSalt = NewString("") + } + + if s.InitialFont == nil { + // Defaults to "nunito-bold.ttf" + s.InitialFont = NewString("nunito-bold.ttf") + } + + if s.AmazonS3AccessKeyId == nil { + s.AmazonS3AccessKeyId = NewString("") + } + + if s.AmazonS3SecretAccessKey == nil { + s.AmazonS3SecretAccessKey = NewString("") + } + + if s.AmazonS3Bucket == nil { + s.AmazonS3Bucket = NewString("") + } + + if s.AmazonS3Region == nil { + s.AmazonS3Region = NewString("") + } + + if s.AmazonS3Endpoint == nil || len(*s.AmazonS3Endpoint) == 0 { + // Defaults to "s3.amazonaws.com" + s.AmazonS3Endpoint = NewString("s3.amazonaws.com") + } + + if s.AmazonS3SSL == nil { + s.AmazonS3SSL = NewBool(true) // Secure by default. + } + + if s.AmazonS3SignV2 == nil { + s.AmazonS3SignV2 = new(bool) + // Signature v2 is not enabled by default. + } + + if s.AmazonS3SSE == nil { + s.AmazonS3SSE = NewBool(false) // Not Encrypted by default. + } + + if s.AmazonS3Trace == nil { + s.AmazonS3Trace = NewBool(false) + } +} + +type EmailSettings struct { + EnableSignUpWithEmail *bool + EnableSignInWithEmail *bool + EnableSignInWithUsername *bool + SendEmailNotifications *bool + UseChannelInEmailNotifications *bool + RequireEmailVerification *bool + FeedbackName *string + FeedbackEmail *string + ReplyToAddress *string + FeedbackOrganization *string + EnableSMTPAuth *bool `restricted:"true"` + SMTPUsername *string `restricted:"true"` + SMTPPassword *string `restricted:"true"` + SMTPServer *string `restricted:"true"` + SMTPPort *string `restricted:"true"` + SMTPServerTimeout *int + ConnectionSecurity *string `restricted:"true"` + SendPushNotifications *bool + PushNotificationServer *string + PushNotificationContents *string + EnableEmailBatching *bool + EmailBatchingBufferSize *int + EmailBatchingInterval *int + EnablePreviewModeBanner *bool + SkipServerCertificateVerification *bool `restricted:"true"` + EmailNotificationContentsType *string + LoginButtonColor *string + LoginButtonBorderColor *string + LoginButtonTextColor *string +} + +func (s *EmailSettings) SetDefaults(isUpdate bool) { + if s.EnableSignUpWithEmail == nil { + s.EnableSignUpWithEmail = NewBool(true) + } + + if s.EnableSignInWithEmail == nil { + s.EnableSignInWithEmail = NewBool(*s.EnableSignUpWithEmail) + } + + if s.EnableSignInWithUsername == nil { + s.EnableSignInWithUsername = NewBool(true) + } + + if s.SendEmailNotifications == nil { + s.SendEmailNotifications = NewBool(true) + } + + if s.UseChannelInEmailNotifications == nil { + s.UseChannelInEmailNotifications = NewBool(false) + } + + if s.RequireEmailVerification == nil { + s.RequireEmailVerification = NewBool(false) + } + + if s.FeedbackName == nil { + s.FeedbackName = NewString("") + } + + if s.FeedbackEmail == nil { + s.FeedbackEmail = NewString("test@example.com") + } + + if s.ReplyToAddress == nil { + s.ReplyToAddress = NewString("test@example.com") + } + + if s.FeedbackOrganization == nil { + s.FeedbackOrganization = NewString(EMAIL_SETTINGS_DEFAULT_FEEDBACK_ORGANIZATION) + } + + if s.EnableSMTPAuth == nil { + if s.ConnectionSecurity == nil || *s.ConnectionSecurity == CONN_SECURITY_NONE { + s.EnableSMTPAuth = NewBool(false) + } else { + s.EnableSMTPAuth = NewBool(true) + } + } + + if s.SMTPUsername == nil { + s.SMTPUsername = NewString("") + } + + if s.SMTPPassword == nil { + s.SMTPPassword = NewString("") + } + + if s.SMTPServer == nil || len(*s.SMTPServer) == 0 { + s.SMTPServer = NewString("localhost") + } + + if s.SMTPPort == nil || len(*s.SMTPPort) == 0 { + s.SMTPPort = NewString("10025") + } + + if s.SMTPServerTimeout == nil || *s.SMTPServerTimeout == 0 { + s.SMTPServerTimeout = NewInt(10) + } + + if s.ConnectionSecurity == nil || *s.ConnectionSecurity == CONN_SECURITY_PLAIN { + s.ConnectionSecurity = NewString(CONN_SECURITY_NONE) + } + + if s.SendPushNotifications == nil { + s.SendPushNotifications = NewBool(!isUpdate) + } + + if s.PushNotificationServer == nil { + if isUpdate { + s.PushNotificationServer = NewString("") + } else { + s.PushNotificationServer = NewString(GENERIC_NOTIFICATION_SERVER) + } + } + + if s.PushNotificationContents == nil { + s.PushNotificationContents = NewString(FULL_NOTIFICATION) + } + + if s.EnableEmailBatching == nil { + s.EnableEmailBatching = NewBool(false) + } + + if s.EmailBatchingBufferSize == nil { + s.EmailBatchingBufferSize = NewInt(EMAIL_BATCHING_BUFFER_SIZE) + } + + if s.EmailBatchingInterval == nil { + s.EmailBatchingInterval = NewInt(EMAIL_BATCHING_INTERVAL) + } + + if s.EnablePreviewModeBanner == nil { + s.EnablePreviewModeBanner = NewBool(true) + } + + if s.EnableSMTPAuth == nil { + if *s.ConnectionSecurity == CONN_SECURITY_NONE { + s.EnableSMTPAuth = NewBool(false) + } else { + s.EnableSMTPAuth = NewBool(true) + } + } + + if *s.ConnectionSecurity == CONN_SECURITY_PLAIN { + *s.ConnectionSecurity = CONN_SECURITY_NONE + } + + if s.SkipServerCertificateVerification == nil { + s.SkipServerCertificateVerification = NewBool(false) + } + + if s.EmailNotificationContentsType == nil { + s.EmailNotificationContentsType = NewString(EMAIL_NOTIFICATION_CONTENTS_FULL) + } + + if s.LoginButtonColor == nil { + s.LoginButtonColor = NewString("#0000") + } + + if s.LoginButtonBorderColor == nil { + s.LoginButtonBorderColor = NewString("#2389D7") + } + + if s.LoginButtonTextColor == nil { + s.LoginButtonTextColor = NewString("#2389D7") + } +} + +type RateLimitSettings struct { + Enable *bool `restricted:"true"` + PerSec *int `restricted:"true"` + MaxBurst *int `restricted:"true"` + MemoryStoreSize *int `restricted:"true"` + VaryByRemoteAddr *bool `restricted:"true"` + VaryByUser *bool `restricted:"true"` + VaryByHeader string `restricted:"true"` +} + +func (s *RateLimitSettings) SetDefaults() { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.PerSec == nil { + s.PerSec = NewInt(10) + } + + if s.MaxBurst == nil { + s.MaxBurst = NewInt(100) + } + + if s.MemoryStoreSize == nil { + s.MemoryStoreSize = NewInt(10000) + } + + if s.VaryByRemoteAddr == nil { + s.VaryByRemoteAddr = NewBool(true) + } + + if s.VaryByUser == nil { + s.VaryByUser = NewBool(false) + } +} + +type PrivacySettings struct { + ShowEmailAddress *bool + ShowFullName *bool +} + +func (s *PrivacySettings) setDefaults() { + if s.ShowEmailAddress == nil { + s.ShowEmailAddress = NewBool(true) + } + + if s.ShowFullName == nil { + s.ShowFullName = NewBool(true) + } +} + +type SupportSettings struct { + TermsOfServiceLink *string `restricted:"true"` + PrivacyPolicyLink *string `restricted:"true"` + AboutLink *string `restricted:"true"` + HelpLink *string `restricted:"true"` + ReportAProblemLink *string `restricted:"true"` + SupportEmail *string + CustomTermsOfServiceEnabled *bool + CustomTermsOfServiceReAcceptancePeriod *int +} + +func (s *SupportSettings) SetDefaults() { + if !IsSafeLink(s.TermsOfServiceLink) { + *s.TermsOfServiceLink = SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK + } + + if s.TermsOfServiceLink == nil { + s.TermsOfServiceLink = NewString(SUPPORT_SETTINGS_DEFAULT_TERMS_OF_SERVICE_LINK) + } + + if !IsSafeLink(s.PrivacyPolicyLink) { + *s.PrivacyPolicyLink = "" + } + + if s.PrivacyPolicyLink == nil { + s.PrivacyPolicyLink = NewString(SUPPORT_SETTINGS_DEFAULT_PRIVACY_POLICY_LINK) + } + + if !IsSafeLink(s.AboutLink) { + *s.AboutLink = "" + } + + if s.AboutLink == nil { + s.AboutLink = NewString(SUPPORT_SETTINGS_DEFAULT_ABOUT_LINK) + } + + if !IsSafeLink(s.HelpLink) { + *s.HelpLink = "" + } + + if s.HelpLink == nil { + s.HelpLink = NewString(SUPPORT_SETTINGS_DEFAULT_HELP_LINK) + } + + if !IsSafeLink(s.ReportAProblemLink) { + *s.ReportAProblemLink = "" + } + + if s.ReportAProblemLink == nil { + s.ReportAProblemLink = NewString(SUPPORT_SETTINGS_DEFAULT_REPORT_A_PROBLEM_LINK) + } + + if s.SupportEmail == nil { + s.SupportEmail = NewString(SUPPORT_SETTINGS_DEFAULT_SUPPORT_EMAIL) + } + + if s.CustomTermsOfServiceEnabled == nil { + s.CustomTermsOfServiceEnabled = NewBool(false) + } + + if s.CustomTermsOfServiceReAcceptancePeriod == nil { + s.CustomTermsOfServiceReAcceptancePeriod = NewInt(SUPPORT_SETTINGS_DEFAULT_RE_ACCEPTANCE_PERIOD) + } +} + +type AnnouncementSettings struct { + EnableBanner *bool + BannerText *string + BannerColor *string + BannerTextColor *string + AllowBannerDismissal *bool +} + +func (s *AnnouncementSettings) SetDefaults() { + if s.EnableBanner == nil { + s.EnableBanner = NewBool(false) + } + + if s.BannerText == nil { + s.BannerText = NewString("") + } + + if s.BannerColor == nil { + s.BannerColor = NewString(ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_COLOR) + } + + if s.BannerTextColor == nil { + s.BannerTextColor = NewString(ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_TEXT_COLOR) + } + + if s.AllowBannerDismissal == nil { + s.AllowBannerDismissal = NewBool(true) + } +} + +type ThemeSettings struct { + EnableThemeSelection *bool + DefaultTheme *string + AllowCustomThemes *bool + AllowedThemes []string +} + +func (s *ThemeSettings) SetDefaults() { + if s.EnableThemeSelection == nil { + s.EnableThemeSelection = NewBool(true) + } + + if s.DefaultTheme == nil { + s.DefaultTheme = NewString(TEAM_SETTINGS_DEFAULT_TEAM_TEXT) + } + + if s.AllowCustomThemes == nil { + s.AllowCustomThemes = NewBool(true) + } + + if s.AllowedThemes == nil { + s.AllowedThemes = []string{} + } +} + +type TeamSettings struct { + SiteName *string + MaxUsersPerTeam *int + DEPRECATED_DO_NOT_USE_EnableTeamCreation *bool `json:"EnableTeamCreation" mapstructure:"EnableTeamCreation"` // This field is deprecated and must not be used. + EnableUserCreation *bool + EnableOpenServer *bool + EnableUserDeactivation *bool + RestrictCreationToDomains *string + EnableCustomBrand *bool + CustomBrandText *string + CustomDescriptionText *string + RestrictDirectMessage *string + DEPRECATED_DO_NOT_USE_RestrictTeamInvite *string `json:"RestrictTeamInvite" mapstructure:"RestrictTeamInvite"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_RestrictPublicChannelManagement *string `json:"RestrictPublicChannelManagement" mapstructure:"RestrictPublicChannelManagement"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManagement *string `json:"RestrictPrivateChannelManagement" mapstructure:"RestrictPrivateChannelManagement"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_RestrictPublicChannelCreation *string `json:"RestrictPublicChannelCreation" mapstructure:"RestrictPublicChannelCreation"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_RestrictPrivateChannelCreation *string `json:"RestrictPrivateChannelCreation" mapstructure:"RestrictPrivateChannelCreation"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_RestrictPublicChannelDeletion *string `json:"RestrictPublicChannelDeletion" mapstructure:"RestrictPublicChannelDeletion"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_RestrictPrivateChannelDeletion *string `json:"RestrictPrivateChannelDeletion" mapstructure:"RestrictPrivateChannelDeletion"` // This field is deprecated and must not be used. + DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManageMembers *string `json:"RestrictPrivateChannelManageMembers" mapstructure:"RestrictPrivateChannelManageMembers"` // This field is deprecated and must not be used. + EnableXToLeaveChannelsFromLHS *bool + UserStatusAwayTimeout *int64 + MaxChannelsPerTeam *int64 + MaxNotificationsPerChannel *int64 + EnableConfirmNotificationsToChannel *bool + TeammateNameDisplay *string + ExperimentalViewArchivedChannels *bool + ExperimentalEnableAutomaticReplies *bool + ExperimentalHideTownSquareinLHS *bool + ExperimentalTownSquareIsReadOnly *bool + LockTeammateNameDisplay *bool + ExperimentalPrimaryTeam *string + ExperimentalDefaultChannels []string +} + +func (s *TeamSettings) SetDefaults() { + + if s.SiteName == nil || *s.SiteName == "" { + s.SiteName = NewString(TEAM_SETTINGS_DEFAULT_SITE_NAME) + } + + if s.MaxUsersPerTeam == nil { + s.MaxUsersPerTeam = NewInt(TEAM_SETTINGS_DEFAULT_MAX_USERS_PER_TEAM) + } + + if s.DEPRECATED_DO_NOT_USE_EnableTeamCreation == nil { + s.DEPRECATED_DO_NOT_USE_EnableTeamCreation = NewBool(true) + } + + if s.EnableUserCreation == nil { + s.EnableUserCreation = NewBool(true) + } + + if s.EnableOpenServer == nil { + s.EnableOpenServer = NewBool(false) + } + + if s.RestrictCreationToDomains == nil { + s.RestrictCreationToDomains = NewString("") + } + + if s.EnableCustomBrand == nil { + s.EnableCustomBrand = NewBool(false) + } + + if s.EnableUserDeactivation == nil { + s.EnableUserDeactivation = NewBool(false) + } + + if s.CustomBrandText == nil { + s.CustomBrandText = NewString(TEAM_SETTINGS_DEFAULT_CUSTOM_BRAND_TEXT) + } + + if s.CustomDescriptionText == nil { + s.CustomDescriptionText = NewString(TEAM_SETTINGS_DEFAULT_CUSTOM_DESCRIPTION_TEXT) + } + + if s.RestrictDirectMessage == nil { + s.RestrictDirectMessage = NewString(DIRECT_MESSAGE_ANY) + } + + if s.DEPRECATED_DO_NOT_USE_RestrictTeamInvite == nil { + s.DEPRECATED_DO_NOT_USE_RestrictTeamInvite = NewString(PERMISSIONS_ALL) + } + + if s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelManagement == nil { + s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelManagement = NewString(PERMISSIONS_ALL) + } + + if s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManagement == nil { + s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManagement = NewString(PERMISSIONS_ALL) + } + + if s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelCreation == nil { + s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelCreation = new(string) + // If this setting does not exist, assume migration from <3.6, so use management setting as default. + if *s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelManagement == PERMISSIONS_CHANNEL_ADMIN { + *s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelCreation = PERMISSIONS_TEAM_ADMIN + } else { + *s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelCreation = *s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelManagement + } + } + + if s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelCreation == nil { + // If this setting does not exist, assume migration from <3.6, so use management setting as default. + if *s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManagement == PERMISSIONS_CHANNEL_ADMIN { + s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelCreation = NewString(PERMISSIONS_TEAM_ADMIN) + } else { + s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelCreation = NewString(*s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManagement) + } + } + + if s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelDeletion == nil { + // If this setting does not exist, assume migration from <3.6, so use management setting as default. + s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelDeletion = NewString(*s.DEPRECATED_DO_NOT_USE_RestrictPublicChannelManagement) + } + + if s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelDeletion == nil { + // If this setting does not exist, assume migration from <3.6, so use management setting as default. + s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelDeletion = NewString(*s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManagement) + } + + if s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManageMembers == nil { + s.DEPRECATED_DO_NOT_USE_RestrictPrivateChannelManageMembers = NewString(PERMISSIONS_ALL) + } + + if s.EnableXToLeaveChannelsFromLHS == nil { + s.EnableXToLeaveChannelsFromLHS = NewBool(false) + } + + if s.UserStatusAwayTimeout == nil { + s.UserStatusAwayTimeout = NewInt64(TEAM_SETTINGS_DEFAULT_USER_STATUS_AWAY_TIMEOUT) + } + + if s.MaxChannelsPerTeam == nil { + s.MaxChannelsPerTeam = NewInt64(2000) + } + + if s.MaxNotificationsPerChannel == nil { + s.MaxNotificationsPerChannel = NewInt64(1000) + } + + if s.EnableConfirmNotificationsToChannel == nil { + s.EnableConfirmNotificationsToChannel = NewBool(true) + } + + if s.ExperimentalEnableAutomaticReplies == nil { + s.ExperimentalEnableAutomaticReplies = NewBool(false) + } + + if s.ExperimentalHideTownSquareinLHS == nil { + s.ExperimentalHideTownSquareinLHS = NewBool(false) + } + + if s.ExperimentalTownSquareIsReadOnly == nil { + s.ExperimentalTownSquareIsReadOnly = NewBool(false) + } + + if s.ExperimentalPrimaryTeam == nil { + s.ExperimentalPrimaryTeam = NewString("") + } + + if s.ExperimentalDefaultChannels == nil { + s.ExperimentalDefaultChannels = []string{} + } + + if s.DEPRECATED_DO_NOT_USE_EnableTeamCreation == nil { + s.DEPRECATED_DO_NOT_USE_EnableTeamCreation = NewBool(true) + } + + if s.EnableUserCreation == nil { + s.EnableUserCreation = NewBool(true) + } + + if s.ExperimentalViewArchivedChannels == nil { + s.ExperimentalViewArchivedChannels = NewBool(false) + } + + if s.LockTeammateNameDisplay == nil { + s.LockTeammateNameDisplay = NewBool(false) + } +} + +type ClientRequirements struct { + AndroidLatestVersion string `restricted:"true"` + AndroidMinVersion string `restricted:"true"` + DesktopLatestVersion string `restricted:"true"` + DesktopMinVersion string `restricted:"true"` + IosLatestVersion string `restricted:"true"` + IosMinVersion string `restricted:"true"` +} + +type LdapSettings struct { + // Basic + Enable *bool + EnableSync *bool + LdapServer *string + LdapPort *int + ConnectionSecurity *string + BaseDN *string + BindUsername *string + BindPassword *string + + // Filtering + UserFilter *string + GroupFilter *string + GuestFilter *string + EnableAdminFilter *bool + AdminFilter *string + + // Group Mapping + GroupDisplayNameAttribute *string + GroupIdAttribute *string + + // User Mapping + FirstNameAttribute *string + LastNameAttribute *string + EmailAttribute *string + UsernameAttribute *string + NicknameAttribute *string + IdAttribute *string + PositionAttribute *string + LoginIdAttribute *string + PictureAttribute *string + + // Synchronization + SyncIntervalMinutes *int + + // Advanced + SkipCertificateVerification *bool + QueryTimeout *int + MaxPageSize *int + + // Customization + LoginFieldName *string + + LoginButtonColor *string + LoginButtonBorderColor *string + LoginButtonTextColor *string + + Trace *bool +} + +func (s *LdapSettings) SetDefaults() { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + // When unset should default to LDAP Enabled + if s.EnableSync == nil { + s.EnableSync = NewBool(*s.Enable) + } + + if s.EnableAdminFilter == nil { + s.EnableAdminFilter = NewBool(false) + } + + if s.LdapServer == nil { + s.LdapServer = NewString("") + } + + if s.LdapPort == nil { + s.LdapPort = NewInt(389) + } + + if s.ConnectionSecurity == nil { + s.ConnectionSecurity = NewString("") + } + + if s.BaseDN == nil { + s.BaseDN = NewString("") + } + + if s.BindUsername == nil { + s.BindUsername = NewString("") + } + + if s.BindPassword == nil { + s.BindPassword = NewString("") + } + + if s.UserFilter == nil { + s.UserFilter = NewString("") + } + + if s.GuestFilter == nil { + s.GuestFilter = NewString("") + } + + if s.AdminFilter == nil { + s.AdminFilter = NewString("") + } + + if s.GroupFilter == nil { + s.GroupFilter = NewString("") + } + + if s.GroupDisplayNameAttribute == nil { + s.GroupDisplayNameAttribute = NewString(LDAP_SETTINGS_DEFAULT_GROUP_DISPLAY_NAME_ATTRIBUTE) + } + + if s.GroupIdAttribute == nil { + s.GroupIdAttribute = NewString(LDAP_SETTINGS_DEFAULT_GROUP_ID_ATTRIBUTE) + } + + if s.FirstNameAttribute == nil { + s.FirstNameAttribute = NewString(LDAP_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE) + } + + if s.LastNameAttribute == nil { + s.LastNameAttribute = NewString(LDAP_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE) + } + + if s.EmailAttribute == nil { + s.EmailAttribute = NewString(LDAP_SETTINGS_DEFAULT_EMAIL_ATTRIBUTE) + } + + if s.UsernameAttribute == nil { + s.UsernameAttribute = NewString(LDAP_SETTINGS_DEFAULT_USERNAME_ATTRIBUTE) + } + + if s.NicknameAttribute == nil { + s.NicknameAttribute = NewString(LDAP_SETTINGS_DEFAULT_NICKNAME_ATTRIBUTE) + } + + if s.IdAttribute == nil { + s.IdAttribute = NewString(LDAP_SETTINGS_DEFAULT_ID_ATTRIBUTE) + } + + if s.PositionAttribute == nil { + s.PositionAttribute = NewString(LDAP_SETTINGS_DEFAULT_POSITION_ATTRIBUTE) + } + + if s.PictureAttribute == nil { + s.PictureAttribute = NewString(LDAP_SETTINGS_DEFAULT_PICTURE_ATTRIBUTE) + } + + // For those upgrading to the version when LoginIdAttribute was added + // they need IdAttribute == LoginIdAttribute not to break + if s.LoginIdAttribute == nil { + s.LoginIdAttribute = s.IdAttribute + } + + if s.SyncIntervalMinutes == nil { + s.SyncIntervalMinutes = NewInt(60) + } + + if s.SkipCertificateVerification == nil { + s.SkipCertificateVerification = NewBool(false) + } + + if s.QueryTimeout == nil { + s.QueryTimeout = NewInt(60) + } + + if s.MaxPageSize == nil { + s.MaxPageSize = NewInt(0) + } + + if s.LoginFieldName == nil { + s.LoginFieldName = NewString(LDAP_SETTINGS_DEFAULT_LOGIN_FIELD_NAME) + } + + if s.LoginButtonColor == nil { + s.LoginButtonColor = NewString("#0000") + } + + if s.LoginButtonBorderColor == nil { + s.LoginButtonBorderColor = NewString("#2389D7") + } + + if s.LoginButtonTextColor == nil { + s.LoginButtonTextColor = NewString("#2389D7") + } + + if s.Trace == nil { + s.Trace = NewBool(false) + } +} + +type ComplianceSettings struct { + Enable *bool + Directory *string + EnableDaily *bool +} + +func (s *ComplianceSettings) SetDefaults() { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.Directory == nil { + s.Directory = NewString("./data/") + } + + if s.EnableDaily == nil { + s.EnableDaily = NewBool(false) + } +} + +type LocalizationSettings struct { + DefaultServerLocale *string + DefaultClientLocale *string + AvailableLocales *string +} + +func (s *LocalizationSettings) SetDefaults() { + if s.DefaultServerLocale == nil { + s.DefaultServerLocale = NewString(DEFAULT_LOCALE) + } + + if s.DefaultClientLocale == nil { + s.DefaultClientLocale = NewString(DEFAULT_LOCALE) + } + + if s.AvailableLocales == nil { + s.AvailableLocales = NewString("") + } +} + +type SamlSettings struct { + // Basic + Enable *bool + EnableSyncWithLdap *bool + EnableSyncWithLdapIncludeAuth *bool + + Verify *bool + Encrypt *bool + SignRequest *bool + + IdpUrl *string + IdpDescriptorUrl *string + IdpMetadataUrl *string + ServiceProviderIdentifier *string + AssertionConsumerServiceURL *string + + SignatureAlgorithm *string + CanonicalAlgorithm *string + + ScopingIDPProviderId *string + ScopingIDPName *string + + IdpCertificateFile *string + PublicCertificateFile *string + PrivateKeyFile *string + + // User Mapping + IdAttribute *string + GuestAttribute *string + EnableAdminAttribute *bool + AdminAttribute *string + FirstNameAttribute *string + LastNameAttribute *string + EmailAttribute *string + UsernameAttribute *string + NicknameAttribute *string + LocaleAttribute *string + PositionAttribute *string + + LoginButtonText *string + + LoginButtonColor *string + LoginButtonBorderColor *string + LoginButtonTextColor *string +} + +func (s *SamlSettings) SetDefaults() { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.EnableSyncWithLdap == nil { + s.EnableSyncWithLdap = NewBool(false) + } + + if s.EnableSyncWithLdapIncludeAuth == nil { + s.EnableSyncWithLdapIncludeAuth = NewBool(false) + } + + if s.EnableAdminAttribute == nil { + s.EnableAdminAttribute = NewBool(false) + } + + if s.Verify == nil { + s.Verify = NewBool(true) + } + + if s.Encrypt == nil { + s.Encrypt = NewBool(true) + } + + if s.SignRequest == nil { + s.SignRequest = NewBool(false) + } + + if s.SignatureAlgorithm == nil { + s.SignatureAlgorithm = NewString(SAML_SETTINGS_DEFAULT_SIGNATURE_ALGORITHM) + } + + if s.CanonicalAlgorithm == nil { + s.CanonicalAlgorithm = NewString(SAML_SETTINGS_DEFAULT_CANONICAL_ALGORITHM) + } + + if s.IdpUrl == nil { + s.IdpUrl = NewString("") + } + + if s.IdpDescriptorUrl == nil { + s.IdpDescriptorUrl = NewString("") + } + + if s.ServiceProviderIdentifier == nil { + if s.IdpDescriptorUrl != nil { + s.ServiceProviderIdentifier = NewString(*s.IdpDescriptorUrl) + } else { + s.ServiceProviderIdentifier = NewString("") + } + } + + if s.IdpMetadataUrl == nil { + s.IdpMetadataUrl = NewString("") + } + + if s.IdpCertificateFile == nil { + s.IdpCertificateFile = NewString("") + } + + if s.PublicCertificateFile == nil { + s.PublicCertificateFile = NewString("") + } + + if s.PrivateKeyFile == nil { + s.PrivateKeyFile = NewString("") + } + + if s.AssertionConsumerServiceURL == nil { + s.AssertionConsumerServiceURL = NewString("") + } + + if s.ScopingIDPProviderId == nil { + s.ScopingIDPProviderId = NewString("") + } + + if s.ScopingIDPName == nil { + s.ScopingIDPName = NewString("") + } + + if s.LoginButtonText == nil || *s.LoginButtonText == "" { + s.LoginButtonText = NewString(USER_AUTH_SERVICE_SAML_TEXT) + } + + if s.IdAttribute == nil { + s.IdAttribute = NewString(SAML_SETTINGS_DEFAULT_ID_ATTRIBUTE) + } + + if s.GuestAttribute == nil { + s.GuestAttribute = NewString(SAML_SETTINGS_DEFAULT_GUEST_ATTRIBUTE) + } + if s.AdminAttribute == nil { + s.AdminAttribute = NewString(SAML_SETTINGS_DEFAULT_ADMIN_ATTRIBUTE) + } + if s.FirstNameAttribute == nil { + s.FirstNameAttribute = NewString(SAML_SETTINGS_DEFAULT_FIRST_NAME_ATTRIBUTE) + } + + if s.LastNameAttribute == nil { + s.LastNameAttribute = NewString(SAML_SETTINGS_DEFAULT_LAST_NAME_ATTRIBUTE) + } + + if s.EmailAttribute == nil { + s.EmailAttribute = NewString(SAML_SETTINGS_DEFAULT_EMAIL_ATTRIBUTE) + } + + if s.UsernameAttribute == nil { + s.UsernameAttribute = NewString(SAML_SETTINGS_DEFAULT_USERNAME_ATTRIBUTE) + } + + if s.NicknameAttribute == nil { + s.NicknameAttribute = NewString(SAML_SETTINGS_DEFAULT_NICKNAME_ATTRIBUTE) + } + + if s.PositionAttribute == nil { + s.PositionAttribute = NewString(SAML_SETTINGS_DEFAULT_POSITION_ATTRIBUTE) + } + + if s.LocaleAttribute == nil { + s.LocaleAttribute = NewString(SAML_SETTINGS_DEFAULT_LOCALE_ATTRIBUTE) + } + + if s.LoginButtonColor == nil { + s.LoginButtonColor = NewString("#34a28b") + } + + if s.LoginButtonBorderColor == nil { + s.LoginButtonBorderColor = NewString("#2389D7") + } + + if s.LoginButtonTextColor == nil { + s.LoginButtonTextColor = NewString("#ffffff") + } +} + +type NativeAppSettings struct { + AppDownloadLink *string `restricted:"true"` + AndroidAppDownloadLink *string `restricted:"true"` + IosAppDownloadLink *string `restricted:"true"` +} + +func (s *NativeAppSettings) SetDefaults() { + if s.AppDownloadLink == nil { + s.AppDownloadLink = NewString(NATIVEAPP_SETTINGS_DEFAULT_APP_DOWNLOAD_LINK) + } + + if s.AndroidAppDownloadLink == nil { + s.AndroidAppDownloadLink = NewString(NATIVEAPP_SETTINGS_DEFAULT_ANDROID_APP_DOWNLOAD_LINK) + } + + if s.IosAppDownloadLink == nil { + s.IosAppDownloadLink = NewString(NATIVEAPP_SETTINGS_DEFAULT_IOS_APP_DOWNLOAD_LINK) + } +} + +type ElasticsearchSettings struct { + ConnectionUrl *string `restricted:"true"` + Username *string `restricted:"true"` + Password *string `restricted:"true"` + EnableIndexing *bool `restricted:"true"` + EnableSearching *bool `restricted:"true"` + EnableAutocomplete *bool `restricted:"true"` + Sniff *bool `restricted:"true"` + PostIndexReplicas *int `restricted:"true"` + PostIndexShards *int `restricted:"true"` + ChannelIndexReplicas *int `restricted:"true"` + ChannelIndexShards *int `restricted:"true"` + UserIndexReplicas *int `restricted:"true"` + UserIndexShards *int `restricted:"true"` + AggregatePostsAfterDays *int `restricted:"true"` + PostsAggregatorJobStartTime *string `restricted:"true"` + IndexPrefix *string `restricted:"true"` + LiveIndexingBatchSize *int `restricted:"true"` + BulkIndexingTimeWindowSeconds *int `restricted:"true"` + RequestTimeoutSeconds *int `restricted:"true"` + SkipTLSVerification *bool `restricted:"true"` + Trace *string `restricted:"true"` +} + +func (s *ElasticsearchSettings) SetDefaults() { + if s.ConnectionUrl == nil { + s.ConnectionUrl = NewString(ELASTICSEARCH_SETTINGS_DEFAULT_CONNECTION_URL) + } + + if s.Username == nil { + s.Username = NewString(ELASTICSEARCH_SETTINGS_DEFAULT_USERNAME) + } + + if s.Password == nil { + s.Password = NewString(ELASTICSEARCH_SETTINGS_DEFAULT_PASSWORD) + } + + if s.EnableIndexing == nil { + s.EnableIndexing = NewBool(false) + } + + if s.EnableSearching == nil { + s.EnableSearching = NewBool(false) + } + + if s.EnableAutocomplete == nil { + s.EnableAutocomplete = NewBool(false) + } + + if s.Sniff == nil { + s.Sniff = NewBool(true) + } + + if s.PostIndexReplicas == nil { + s.PostIndexReplicas = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_REPLICAS) + } + + if s.PostIndexShards == nil { + s.PostIndexShards = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_SHARDS) + } + + if s.ChannelIndexReplicas == nil { + s.ChannelIndexReplicas = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_CHANNEL_INDEX_REPLICAS) + } + + if s.ChannelIndexShards == nil { + s.ChannelIndexShards = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_CHANNEL_INDEX_SHARDS) + } + + if s.UserIndexReplicas == nil { + s.UserIndexReplicas = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_USER_INDEX_REPLICAS) + } + + if s.UserIndexShards == nil { + s.UserIndexShards = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_USER_INDEX_SHARDS) + } + + if s.AggregatePostsAfterDays == nil { + s.AggregatePostsAfterDays = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_AGGREGATE_POSTS_AFTER_DAYS) + } + + if s.PostsAggregatorJobStartTime == nil { + s.PostsAggregatorJobStartTime = NewString(ELASTICSEARCH_SETTINGS_DEFAULT_POSTS_AGGREGATOR_JOB_START_TIME) + } + + if s.IndexPrefix == nil { + s.IndexPrefix = NewString(ELASTICSEARCH_SETTINGS_DEFAULT_INDEX_PREFIX) + } + + if s.LiveIndexingBatchSize == nil { + s.LiveIndexingBatchSize = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_LIVE_INDEXING_BATCH_SIZE) + } + + if s.BulkIndexingTimeWindowSeconds == nil { + s.BulkIndexingTimeWindowSeconds = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_BULK_INDEXING_TIME_WINDOW_SECONDS) + } + + if s.RequestTimeoutSeconds == nil { + s.RequestTimeoutSeconds = NewInt(ELASTICSEARCH_SETTINGS_DEFAULT_REQUEST_TIMEOUT_SECONDS) + } + + if s.SkipTLSVerification == nil { + s.SkipTLSVerification = NewBool(false) + } + + if s.Trace == nil { + s.Trace = NewString("") + } +} + +type BleveSettings struct { + IndexDir *string + EnableIndexing *bool + EnableSearching *bool + EnableAutocomplete *bool + BulkIndexingTimeWindowSeconds *int +} + +func (bs *BleveSettings) SetDefaults() { + if bs.IndexDir == nil { + bs.IndexDir = NewString(BLEVE_SETTINGS_DEFAULT_INDEX_DIR) + } + + if bs.EnableIndexing == nil { + bs.EnableIndexing = NewBool(false) + } + + if bs.EnableSearching == nil { + bs.EnableSearching = NewBool(false) + } + + if bs.EnableAutocomplete == nil { + bs.EnableAutocomplete = NewBool(false) + } + + if bs.BulkIndexingTimeWindowSeconds == nil { + bs.BulkIndexingTimeWindowSeconds = NewInt(BLEVE_SETTINGS_DEFAULT_BULK_INDEXING_TIME_WINDOW_SECONDS) + } +} + +type DataRetentionSettings struct { + EnableMessageDeletion *bool + EnableFileDeletion *bool + MessageRetentionDays *int + FileRetentionDays *int + DeletionJobStartTime *string +} + +func (s *DataRetentionSettings) SetDefaults() { + if s.EnableMessageDeletion == nil { + s.EnableMessageDeletion = NewBool(false) + } + + if s.EnableFileDeletion == nil { + s.EnableFileDeletion = NewBool(false) + } + + if s.MessageRetentionDays == nil { + s.MessageRetentionDays = NewInt(DATA_RETENTION_SETTINGS_DEFAULT_MESSAGE_RETENTION_DAYS) + } + + if s.FileRetentionDays == nil { + s.FileRetentionDays = NewInt(DATA_RETENTION_SETTINGS_DEFAULT_FILE_RETENTION_DAYS) + } + + if s.DeletionJobStartTime == nil { + s.DeletionJobStartTime = NewString(DATA_RETENTION_SETTINGS_DEFAULT_DELETION_JOB_START_TIME) + } +} + +type JobSettings struct { + RunJobs *bool `restricted:"true"` + RunScheduler *bool `restricted:"true"` +} + +func (s *JobSettings) SetDefaults() { + if s.RunJobs == nil { + s.RunJobs = NewBool(true) + } + + if s.RunScheduler == nil { + s.RunScheduler = NewBool(true) + } +} + +type PluginState struct { + Enable bool +} + +type PluginSettings struct { + Enable *bool + EnableUploads *bool `restricted:"true"` + AllowInsecureDownloadUrl *bool `restricted:"true"` + EnableHealthCheck *bool `restricted:"true"` + Directory *string `restricted:"true"` + ClientDirectory *string `restricted:"true"` + Plugins map[string]map[string]interface{} + PluginStates map[string]*PluginState + EnableMarketplace *bool + EnableRemoteMarketplace *bool + AutomaticPrepackagedPlugins *bool + RequirePluginSignature *bool + MarketplaceUrl *string + SignaturePublicKeyFiles []string +} + +func (s *PluginSettings) SetDefaults(ls LogSettings) { + if s.Enable == nil { + s.Enable = NewBool(true) + } + + if s.EnableUploads == nil { + s.EnableUploads = NewBool(false) + } + + if s.AllowInsecureDownloadUrl == nil { + s.AllowInsecureDownloadUrl = NewBool(false) + } + + if s.EnableHealthCheck == nil { + s.EnableHealthCheck = NewBool(true) + } + + if s.Directory == nil || *s.Directory == "" { + s.Directory = NewString(PLUGIN_SETTINGS_DEFAULT_DIRECTORY) + } + + if s.ClientDirectory == nil || *s.ClientDirectory == "" { + s.ClientDirectory = NewString(PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY) + } + + if s.Plugins == nil { + s.Plugins = make(map[string]map[string]interface{}) + } + + if s.PluginStates == nil { + s.PluginStates = make(map[string]*PluginState) + } + + if s.PluginStates["com.mattermost.nps"] == nil { + // Enable the NPS plugin by default if diagnostics are enabled + s.PluginStates["com.mattermost.nps"] = &PluginState{Enable: ls.EnableDiagnostics == nil || *ls.EnableDiagnostics} + } + + if s.EnableMarketplace == nil { + s.EnableMarketplace = NewBool(PLUGIN_SETTINGS_DEFAULT_ENABLE_MARKETPLACE) + } + + if s.EnableRemoteMarketplace == nil { + s.EnableRemoteMarketplace = NewBool(true) + } + + if s.AutomaticPrepackagedPlugins == nil { + s.AutomaticPrepackagedPlugins = NewBool(true) + } + + if s.MarketplaceUrl == nil || *s.MarketplaceUrl == "" || *s.MarketplaceUrl == PLUGIN_SETTINGS_OLD_MARKETPLACE_URL { + s.MarketplaceUrl = NewString(PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL) + } + + if s.RequirePluginSignature == nil { + s.RequirePluginSignature = NewBool(false) + } + + if s.SignaturePublicKeyFiles == nil { + s.SignaturePublicKeyFiles = []string{} + } +} + +type GlobalRelayMessageExportSettings struct { + CustomerType *string // must be either A9 or A10, dictates SMTP server url + SmtpUsername *string + SmtpPassword *string + EmailAddress *string // the address to send messages to +} + +func (s *GlobalRelayMessageExportSettings) SetDefaults() { + if s.CustomerType == nil { + s.CustomerType = NewString(GLOBALRELAY_CUSTOMER_TYPE_A9) + } + if s.SmtpUsername == nil { + s.SmtpUsername = NewString("") + } + if s.SmtpPassword == nil { + s.SmtpPassword = NewString("") + } + if s.EmailAddress == nil { + s.EmailAddress = NewString("") + } +} + +type MessageExportSettings struct { + EnableExport *bool + ExportFormat *string + DailyRunTime *string + ExportFromTimestamp *int64 + BatchSize *int + + // formatter-specific settings - these are only expected to be non-nil if ExportFormat is set to the associated format + GlobalRelaySettings *GlobalRelayMessageExportSettings +} + +func (s *MessageExportSettings) SetDefaults() { + if s.EnableExport == nil { + s.EnableExport = NewBool(false) + } + + if s.ExportFormat == nil { + s.ExportFormat = NewString(COMPLIANCE_EXPORT_TYPE_ACTIANCE) + } + + if s.DailyRunTime == nil { + s.DailyRunTime = NewString("01:00") + } + + if s.ExportFromTimestamp == nil { + s.ExportFromTimestamp = NewInt64(0) + } + + if s.BatchSize == nil { + s.BatchSize = NewInt(10000) + } + + if s.GlobalRelaySettings == nil { + s.GlobalRelaySettings = &GlobalRelayMessageExportSettings{} + } + s.GlobalRelaySettings.SetDefaults() +} + +type DisplaySettings struct { + CustomUrlSchemes []string + ExperimentalTimezone *bool +} + +func (s *DisplaySettings) SetDefaults() { + if s.CustomUrlSchemes == nil { + customUrlSchemes := []string{} + s.CustomUrlSchemes = customUrlSchemes + } + + if s.ExperimentalTimezone == nil { + s.ExperimentalTimezone = NewBool(false) + } +} + +type GuestAccountsSettings struct { + Enable *bool + AllowEmailAccounts *bool + EnforceMultifactorAuthentication *bool + RestrictCreationToDomains *string +} + +func (s *GuestAccountsSettings) SetDefaults() { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.AllowEmailAccounts == nil { + s.AllowEmailAccounts = NewBool(true) + } + + if s.EnforceMultifactorAuthentication == nil { + s.EnforceMultifactorAuthentication = NewBool(false) + } + + if s.RestrictCreationToDomains == nil { + s.RestrictCreationToDomains = NewString("") + } +} + +type ImageProxySettings struct { + Enable *bool + ImageProxyType *string + RemoteImageProxyURL *string + RemoteImageProxyOptions *string +} + +func (s *ImageProxySettings) SetDefaults(ss ServiceSettings) { + if s.Enable == nil { + if ss.DEPRECATED_DO_NOT_USE_ImageProxyType == nil || *ss.DEPRECATED_DO_NOT_USE_ImageProxyType == "" { + s.Enable = NewBool(false) + } else { + s.Enable = NewBool(true) + } + } + + if s.ImageProxyType == nil { + if ss.DEPRECATED_DO_NOT_USE_ImageProxyType == nil || *ss.DEPRECATED_DO_NOT_USE_ImageProxyType == "" { + s.ImageProxyType = NewString(IMAGE_PROXY_TYPE_LOCAL) + } else { + s.ImageProxyType = ss.DEPRECATED_DO_NOT_USE_ImageProxyType + } + } + + if s.RemoteImageProxyURL == nil { + if ss.DEPRECATED_DO_NOT_USE_ImageProxyURL == nil { + s.RemoteImageProxyURL = NewString("") + } else { + s.RemoteImageProxyURL = ss.DEPRECATED_DO_NOT_USE_ImageProxyURL + } + } + + if s.RemoteImageProxyOptions == nil { + if ss.DEPRECATED_DO_NOT_USE_ImageProxyOptions == nil { + s.RemoteImageProxyOptions = NewString("") + } else { + s.RemoteImageProxyOptions = ss.DEPRECATED_DO_NOT_USE_ImageProxyOptions + } + } +} + +type ConfigFunc func() *Config + +type Config struct { + ServiceSettings ServiceSettings + TeamSettings TeamSettings + ClientRequirements ClientRequirements + SqlSettings SqlSettings + LogSettings LogSettings + ExperimentalAuditSettings ExperimentalAuditSettings + NotificationLogSettings NotificationLogSettings + PasswordSettings PasswordSettings + FileSettings FileSettings + EmailSettings EmailSettings + RateLimitSettings RateLimitSettings + PrivacySettings PrivacySettings + SupportSettings SupportSettings + AnnouncementSettings AnnouncementSettings + ThemeSettings ThemeSettings + GitLabSettings SSOSettings + GoogleSettings SSOSettings + Office365Settings Office365Settings + LdapSettings LdapSettings + ComplianceSettings ComplianceSettings + LocalizationSettings LocalizationSettings + SamlSettings SamlSettings + NativeAppSettings NativeAppSettings + ClusterSettings ClusterSettings + MetricsSettings MetricsSettings + ExperimentalSettings ExperimentalSettings + AnalyticsSettings AnalyticsSettings + ElasticsearchSettings ElasticsearchSettings + BleveSettings BleveSettings + DataRetentionSettings DataRetentionSettings + MessageExportSettings MessageExportSettings + JobSettings JobSettings + PluginSettings PluginSettings + DisplaySettings DisplaySettings + GuestAccountsSettings GuestAccountsSettings + ImageProxySettings ImageProxySettings +} + +func (o *Config) Clone() *Config { + var ret Config + if err := json.Unmarshal([]byte(o.ToJson()), &ret); err != nil { + panic(err) + } + return &ret +} + +func (o *Config) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func (o *Config) GetSSOService(service string) *SSOSettings { + switch service { + case SERVICE_GITLAB: + return &o.GitLabSettings + case SERVICE_GOOGLE: + return &o.GoogleSettings + case SERVICE_OFFICE365: + return o.Office365Settings.SSOSettings() + } + + return nil +} + +func ConfigFromJson(data io.Reader) *Config { + var o *Config + json.NewDecoder(data).Decode(&o) + return o +} + +// isUpdate detects a pre-existing config based on whether SiteURL has been changed +func (o *Config) isUpdate() bool { + return o.ServiceSettings.SiteURL != nil +} + +func (o *Config) SetDefaults() { + isUpdate := o.isUpdate() + + o.LdapSettings.SetDefaults() + o.SamlSettings.SetDefaults() + + if o.TeamSettings.TeammateNameDisplay == nil { + o.TeamSettings.TeammateNameDisplay = NewString(SHOW_USERNAME) + + if *o.SamlSettings.Enable || *o.LdapSettings.Enable { + *o.TeamSettings.TeammateNameDisplay = SHOW_FULLNAME + } + } + + o.SqlSettings.SetDefaults(isUpdate) + o.FileSettings.SetDefaults(isUpdate) + o.EmailSettings.SetDefaults(isUpdate) + o.PrivacySettings.setDefaults() + o.Office365Settings.setDefaults() + o.GitLabSettings.setDefaults("", "", "", "") + o.GoogleSettings.setDefaults(GOOGLE_SETTINGS_DEFAULT_SCOPE, GOOGLE_SETTINGS_DEFAULT_AUTH_ENDPOINT, GOOGLE_SETTINGS_DEFAULT_TOKEN_ENDPOINT, GOOGLE_SETTINGS_DEFAULT_USER_API_ENDPOINT) + o.ServiceSettings.SetDefaults(isUpdate) + o.PasswordSettings.SetDefaults() + o.TeamSettings.SetDefaults() + o.MetricsSettings.SetDefaults() + o.ExperimentalSettings.SetDefaults() + o.SupportSettings.SetDefaults() + o.AnnouncementSettings.SetDefaults() + o.ThemeSettings.SetDefaults() + o.ClusterSettings.SetDefaults() + o.PluginSettings.SetDefaults(o.LogSettings) + o.AnalyticsSettings.SetDefaults() + o.ComplianceSettings.SetDefaults() + o.LocalizationSettings.SetDefaults() + o.ElasticsearchSettings.SetDefaults() + o.BleveSettings.SetDefaults() + o.NativeAppSettings.SetDefaults() + o.DataRetentionSettings.SetDefaults() + o.RateLimitSettings.SetDefaults() + o.LogSettings.SetDefaults() + o.ExperimentalAuditSettings.SetDefaults() + o.NotificationLogSettings.SetDefaults() + o.JobSettings.SetDefaults() + o.MessageExportSettings.SetDefaults() + o.DisplaySettings.SetDefaults() + o.GuestAccountsSettings.SetDefaults() + o.ImageProxySettings.SetDefaults(o.ServiceSettings) +} + +func (o *Config) IsValid() *AppError { + if len(*o.ServiceSettings.SiteURL) == 0 && *o.EmailSettings.EnableEmailBatching { + return NewAppError("Config.IsValid", "model.config.is_valid.site_url_email_batching.app_error", nil, "", http.StatusBadRequest) + } + + if *o.ClusterSettings.Enable && *o.EmailSettings.EnableEmailBatching { + return NewAppError("Config.IsValid", "model.config.is_valid.cluster_email_batching.app_error", nil, "", http.StatusBadRequest) + } + + if len(*o.ServiceSettings.SiteURL) == 0 && *o.ServiceSettings.AllowCookiesForSubdomains { + return NewAppError("Config.IsValid", "model.config.is_valid.allow_cookies_for_subdomains.app_error", nil, "", http.StatusBadRequest) + } + + if err := o.TeamSettings.isValid(); err != nil { + return err + } + + if err := o.SqlSettings.isValid(); err != nil { + return err + } + + if err := o.FileSettings.isValid(); err != nil { + return err + } + + if err := o.EmailSettings.isValid(); err != nil { + return err + } + + if err := o.LdapSettings.isValid(); err != nil { + return err + } + + if err := o.SamlSettings.isValid(); err != nil { + return err + } + + if *o.PasswordSettings.MinimumLength < PASSWORD_MINIMUM_LENGTH || *o.PasswordSettings.MinimumLength > PASSWORD_MAXIMUM_LENGTH { + return NewAppError("Config.IsValid", "model.config.is_valid.password_length.app_error", map[string]interface{}{"MinLength": PASSWORD_MINIMUM_LENGTH, "MaxLength": PASSWORD_MAXIMUM_LENGTH}, "", http.StatusBadRequest) + } + + if err := o.RateLimitSettings.isValid(); err != nil { + return err + } + + if err := o.ServiceSettings.isValid(); err != nil { + return err + } + + if err := o.ElasticsearchSettings.isValid(); err != nil { + return err + } + + if err := o.BleveSettings.isValid(); err != nil { + return err + } + + if err := o.DataRetentionSettings.isValid(); err != nil { + return err + } + + if err := o.LocalizationSettings.isValid(); err != nil { + return err + } + + if err := o.MessageExportSettings.isValid(o.FileSettings); err != nil { + return err + } + + if err := o.DisplaySettings.isValid(); err != nil { + return err + } + + if err := o.ImageProxySettings.isValid(); err != nil { + return err + } + return nil +} + +func (s *TeamSettings) isValid() *AppError { + if *s.MaxUsersPerTeam <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.max_users.app_error", nil, "", http.StatusBadRequest) + } + + if *s.MaxChannelsPerTeam <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.max_channels.app_error", nil, "", http.StatusBadRequest) + } + + if *s.MaxNotificationsPerChannel <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.max_notify_per_channel.app_error", nil, "", http.StatusBadRequest) + } + + if !(*s.RestrictDirectMessage == DIRECT_MESSAGE_ANY || *s.RestrictDirectMessage == DIRECT_MESSAGE_TEAM) { + return NewAppError("Config.IsValid", "model.config.is_valid.restrict_direct_message.app_error", nil, "", http.StatusBadRequest) + } + + if !(*s.TeammateNameDisplay == SHOW_FULLNAME || *s.TeammateNameDisplay == SHOW_NICKNAME_FULLNAME || *s.TeammateNameDisplay == SHOW_USERNAME) { + return NewAppError("Config.IsValid", "model.config.is_valid.teammate_name_display.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.SiteName) > SITENAME_MAX_LENGTH { + return NewAppError("Config.IsValid", "model.config.is_valid.sitename_length.app_error", map[string]interface{}{"MaxLength": SITENAME_MAX_LENGTH}, "", http.StatusBadRequest) + } + + return nil +} + +func (s *SqlSettings) isValid() *AppError { + if *s.AtRestEncryptKey != "" && len(*s.AtRestEncryptKey) < 32 { + return NewAppError("Config.IsValid", "model.config.is_valid.encrypt_sql.app_error", nil, "", http.StatusBadRequest) + } + + if !(*s.DriverName == DATABASE_DRIVER_MYSQL || *s.DriverName == DATABASE_DRIVER_POSTGRES) { + return NewAppError("Config.IsValid", "model.config.is_valid.sql_driver.app_error", nil, "", http.StatusBadRequest) + } + + if *s.MaxIdleConns <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.sql_idle.app_error", nil, "", http.StatusBadRequest) + } + + if *s.ConnMaxLifetimeMilliseconds < 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.sql_conn_max_lifetime_milliseconds.app_error", nil, "", http.StatusBadRequest) + } + + if *s.QueryTimeout <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.sql_query_timeout.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.DataSource) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.sql_data_src.app_error", nil, "", http.StatusBadRequest) + } + + if *s.MaxOpenConns <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.sql_max_conn.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (s *FileSettings) isValid() *AppError { + if *s.MaxFileSize <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.max_file_size.app_error", nil, "", http.StatusBadRequest) + } + + if !(*s.DriverName == IMAGE_DRIVER_LOCAL || *s.DriverName == IMAGE_DRIVER_S3) { + return NewAppError("Config.IsValid", "model.config.is_valid.file_driver.app_error", nil, "", http.StatusBadRequest) + } + + if *s.PublicLinkSalt != "" && len(*s.PublicLinkSalt) < 32 { + return NewAppError("Config.IsValid", "model.config.is_valid.file_salt.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (s *EmailSettings) isValid() *AppError { + if !(*s.ConnectionSecurity == CONN_SECURITY_NONE || *s.ConnectionSecurity == CONN_SECURITY_TLS || *s.ConnectionSecurity == CONN_SECURITY_STARTTLS || *s.ConnectionSecurity == CONN_SECURITY_PLAIN) { + return NewAppError("Config.IsValid", "model.config.is_valid.email_security.app_error", nil, "", http.StatusBadRequest) + } + + if *s.EmailBatchingBufferSize <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.email_batching_buffer_size.app_error", nil, "", http.StatusBadRequest) + } + + if *s.EmailBatchingInterval < 30 { + return NewAppError("Config.IsValid", "model.config.is_valid.email_batching_interval.app_error", nil, "", http.StatusBadRequest) + } + + if !(*s.EmailNotificationContentsType == EMAIL_NOTIFICATION_CONTENTS_FULL || *s.EmailNotificationContentsType == EMAIL_NOTIFICATION_CONTENTS_GENERIC) { + return NewAppError("Config.IsValid", "model.config.is_valid.email_notification_contents_type.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (s *RateLimitSettings) isValid() *AppError { + if *s.MemoryStoreSize <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.rate_mem.app_error", nil, "", http.StatusBadRequest) + } + + if *s.PerSec <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.rate_sec.app_error", nil, "", http.StatusBadRequest) + } + + if *s.MaxBurst <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.max_burst.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (s *LdapSettings) isValid() *AppError { + if !(*s.ConnectionSecurity == CONN_SECURITY_NONE || *s.ConnectionSecurity == CONN_SECURITY_TLS || *s.ConnectionSecurity == CONN_SECURITY_STARTTLS) { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_security.app_error", nil, "", http.StatusBadRequest) + } + + if *s.SyncIntervalMinutes <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_sync_interval.app_error", nil, "", http.StatusBadRequest) + } + + if *s.MaxPageSize < 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_max_page_size.app_error", nil, "", http.StatusBadRequest) + } + + if *s.Enable { + if *s.LdapServer == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_server", nil, "", http.StatusBadRequest) + } + + if *s.BaseDN == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_basedn", nil, "", http.StatusBadRequest) + } + + if *s.EmailAttribute == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_email", nil, "", http.StatusBadRequest) + } + + if *s.UsernameAttribute == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_username", nil, "", http.StatusBadRequest) + } + + if *s.IdAttribute == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_id", nil, "", http.StatusBadRequest) + } + + if *s.LoginIdAttribute == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.ldap_login_id", nil, "", http.StatusBadRequest) + } + + if *s.UserFilter != "" { + if _, err := ldap.CompileFilter(*s.UserFilter); err != nil { + return NewAppError("ValidateFilter", "ent.ldap.validate_filter.app_error", nil, err.Error(), http.StatusBadRequest) + } + } + + if *s.GuestFilter != "" { + if _, err := ldap.CompileFilter(*s.GuestFilter); err != nil { + return NewAppError("LdapSettings.isValid", "ent.ldap.validate_guest_filter.app_error", nil, err.Error(), http.StatusBadRequest) + } + } + + if *s.AdminFilter != "" { + if _, err := ldap.CompileFilter(*s.AdminFilter); err != nil { + return NewAppError("LdapSettings.isValid", "ent.ldap.validate_admin_filter.app_error", nil, err.Error(), http.StatusBadRequest) + } + } + } + + return nil +} + +func (s *SamlSettings) isValid() *AppError { + if *s.Enable { + if len(*s.IdpUrl) == 0 || !IsValidHttpUrl(*s.IdpUrl) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_idp_url.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.IdpDescriptorUrl) == 0 || !IsValidHttpUrl(*s.IdpDescriptorUrl) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_idp_descriptor_url.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.IdpCertificateFile) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_idp_cert.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.EmailAttribute) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.UsernameAttribute) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_username_attribute.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.ServiceProviderIdentifier) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_spidentifier_attribute.app_error", nil, "", http.StatusBadRequest) + } + + if *s.Verify { + if len(*s.AssertionConsumerServiceURL) == 0 || !IsValidHttpUrl(*s.AssertionConsumerServiceURL) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_assertion_consumer_service_url.app_error", nil, "", http.StatusBadRequest) + } + } + + if *s.Encrypt { + if len(*s.PrivateKeyFile) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_private_key.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.PublicCertificateFile) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_public_cert.app_error", nil, "", http.StatusBadRequest) + } + } + + if len(*s.EmailAttribute) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "", http.StatusBadRequest) + } + + if !(*s.SignatureAlgorithm == SAML_SETTINGS_SIGNATURE_ALGORITHM_SHA1 || *s.SignatureAlgorithm == SAML_SETTINGS_SIGNATURE_ALGORITHM_SHA256 || *s.SignatureAlgorithm == SAML_SETTINGS_SIGNATURE_ALGORITHM_SHA512) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_signature_algorithm.app_error", nil, "", http.StatusBadRequest) + } + if !(*s.CanonicalAlgorithm == SAML_SETTINGS_CANONICAL_ALGORITHM_C14N || *s.CanonicalAlgorithm == SAML_SETTINGS_CANONICAL_ALGORITHM_C14N11) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_canonical_algorithm.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.GuestAttribute) > 0 { + if !(strings.Contains(*s.GuestAttribute, "=")) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_guest_attribute.app_error", nil, "", http.StatusBadRequest) + } + if len(strings.Split(*s.GuestAttribute, "=")) != 2 { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_guest_attribute.app_error", nil, "", http.StatusBadRequest) + } + } + + if len(*s.AdminAttribute) > 0 { + if !(strings.Contains(*s.AdminAttribute, "=")) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_admin_attribute.app_error", nil, "", http.StatusBadRequest) + } + if len(strings.Split(*s.AdminAttribute, "=")) != 2 { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_admin_attribute.app_error", nil, "", http.StatusBadRequest) + } + } + } + + return nil +} + +func (s *ServiceSettings) isValid() *AppError { + if !(*s.ConnectionSecurity == CONN_SECURITY_NONE || *s.ConnectionSecurity == CONN_SECURITY_TLS) { + return NewAppError("Config.IsValid", "model.config.is_valid.webserver_security.app_error", nil, "", http.StatusBadRequest) + } + + if *s.ConnectionSecurity == CONN_SECURITY_TLS && !*s.UseLetsEncrypt { + appErr := NewAppError("Config.IsValid", "model.config.is_valid.tls_cert_file.app_error", nil, "", http.StatusBadRequest) + + if *s.TLSCertFile == "" { + return appErr + } else if _, err := os.Stat(*s.TLSCertFile); os.IsNotExist(err) { + return appErr + } + + appErr = NewAppError("Config.IsValid", "model.config.is_valid.tls_key_file.app_error", nil, "", http.StatusBadRequest) + + if *s.TLSKeyFile == "" { + return appErr + } else if _, err := os.Stat(*s.TLSKeyFile); os.IsNotExist(err) { + return appErr + } + } + + if len(s.TLSOverwriteCiphers) > 0 { + for _, cipher := range s.TLSOverwriteCiphers { + if _, ok := ServerTLSSupportedCiphers[cipher]; !ok { + return NewAppError("Config.IsValid", "model.config.is_valid.tls_overwrite_cipher.app_error", map[string]interface{}{"name": cipher}, "", http.StatusBadRequest) + } + } + } + + if *s.ReadTimeout <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.read_timeout.app_error", nil, "", http.StatusBadRequest) + } + + if *s.WriteTimeout <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.write_timeout.app_error", nil, "", http.StatusBadRequest) + } + + if *s.TimeBetweenUserTypingUpdatesMilliseconds < 1000 { + return NewAppError("Config.IsValid", "model.config.is_valid.time_between_user_typing.app_error", nil, "", http.StatusBadRequest) + } + + if *s.MaximumLoginAttempts <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.login_attempts.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.SiteURL) != 0 { + if _, err := url.ParseRequestURI(*s.SiteURL); err != nil { + return NewAppError("Config.IsValid", "model.config.is_valid.site_url.app_error", nil, "", http.StatusBadRequest) + } + } + + if len(*s.WebsocketURL) != 0 { + if _, err := url.ParseRequestURI(*s.WebsocketURL); err != nil { + return NewAppError("Config.IsValid", "model.config.is_valid.websocket_url.app_error", nil, "", http.StatusBadRequest) + } + } + + host, port, _ := net.SplitHostPort(*s.ListenAddress) + var isValidHost bool + if host == "" { + isValidHost = true + } else { + isValidHost = (net.ParseIP(host) != nil) || IsDomainName(host) + } + portInt, err := strconv.Atoi(port) + if err != nil || !isValidHost || portInt < 0 || portInt > math.MaxUint16 { + return NewAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "", http.StatusBadRequest) + } + + if *s.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DISABLED && + *s.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DEFAULT_ON && + *s.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DEFAULT_OFF { + return NewAppError("Config.IsValid", "model.config.is_valid.group_unread_channels.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (s *ElasticsearchSettings) isValid() *AppError { + if *s.EnableIndexing { + if len(*s.ConnectionUrl) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.connection_url.app_error", nil, "", http.StatusBadRequest) + } + } + + if *s.EnableSearching && !*s.EnableIndexing { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.enable_searching.app_error", nil, "", http.StatusBadRequest) + } + + if *s.EnableAutocomplete && !*s.EnableIndexing { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.enable_autocomplete.app_error", nil, "", http.StatusBadRequest) + } + + if *s.AggregatePostsAfterDays < 1 { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", nil, "", http.StatusBadRequest) + } + + if _, err := time.Parse("15:04", *s.PostsAggregatorJobStartTime); err != nil { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if *s.LiveIndexingBatchSize < 1 { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.live_indexing_batch_size.app_error", nil, "", http.StatusBadRequest) + } + + if *s.BulkIndexingTimeWindowSeconds < 1 { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.bulk_indexing_time_window_seconds.app_error", nil, "", http.StatusBadRequest) + } + + if *s.RequestTimeoutSeconds < 1 { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.request_timeout_seconds.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (bs *BleveSettings) isValid() *AppError { + if *bs.EnableIndexing { + if len(*bs.IndexDir) == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.bleve_search.filename.app_error", nil, "", http.StatusBadRequest) + } + } else { + if *bs.EnableSearching { + return NewAppError("Config.IsValid", "model.config.is_valid.bleve_search.enable_searching.app_error", nil, "", http.StatusBadRequest) + } + if *bs.EnableAutocomplete { + return NewAppError("Config.IsValid", "model.config.is_valid.bleve_search.enable_autocomplete.app_error", nil, "", http.StatusBadRequest) + } + } + if *bs.BulkIndexingTimeWindowSeconds < 1 { + return NewAppError("Config.IsValid", "model.config.is_valid.bleve_search.bulk_indexing_time_window_seconds.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (s *DataRetentionSettings) isValid() *AppError { + if *s.MessageRetentionDays <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", nil, "", http.StatusBadRequest) + } + + if *s.FileRetentionDays <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", nil, "", http.StatusBadRequest) + } + + if _, err := time.Parse("15:04", *s.DeletionJobStartTime); err != nil { + return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.deletion_job_start_time.app_error", nil, err.Error(), http.StatusBadRequest) + } + + return nil +} + +func (s *LocalizationSettings) isValid() *AppError { + if len(*s.AvailableLocales) > 0 { + if !strings.Contains(*s.AvailableLocales, *s.DefaultClientLocale) { + return NewAppError("Config.IsValid", "model.config.is_valid.localization.available_locales.app_error", nil, "", http.StatusBadRequest) + } + } + + return nil +} + +func (s *MessageExportSettings) isValid(fs FileSettings) *AppError { + if s.EnableExport == nil { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.enable.app_error", nil, "", http.StatusBadRequest) + } + if *s.EnableExport { + if s.ExportFromTimestamp == nil || *s.ExportFromTimestamp < 0 || *s.ExportFromTimestamp > GetMillis() { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.export_from.app_error", nil, "", http.StatusBadRequest) + } else if s.DailyRunTime == nil { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.daily_runtime.app_error", nil, "", http.StatusBadRequest) + } else if _, err := time.Parse("15:04", *s.DailyRunTime); err != nil { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.daily_runtime.app_error", nil, err.Error(), http.StatusBadRequest) + } else if s.BatchSize == nil || *s.BatchSize < 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.batch_size.app_error", nil, "", http.StatusBadRequest) + } else if s.ExportFormat == nil || (*s.ExportFormat != COMPLIANCE_EXPORT_TYPE_ACTIANCE && *s.ExportFormat != COMPLIANCE_EXPORT_TYPE_GLOBALRELAY && *s.ExportFormat != COMPLIANCE_EXPORT_TYPE_CSV) { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.export_type.app_error", nil, "", http.StatusBadRequest) + } + + if *s.ExportFormat == COMPLIANCE_EXPORT_TYPE_GLOBALRELAY { + if s.GlobalRelaySettings == nil { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.config_missing.app_error", nil, "", http.StatusBadRequest) + } else if s.GlobalRelaySettings.CustomerType == nil || (*s.GlobalRelaySettings.CustomerType != GLOBALRELAY_CUSTOMER_TYPE_A9 && *s.GlobalRelaySettings.CustomerType != GLOBALRELAY_CUSTOMER_TYPE_A10) { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.customer_type.app_error", nil, "", http.StatusBadRequest) + } else if s.GlobalRelaySettings.EmailAddress == nil || !strings.Contains(*s.GlobalRelaySettings.EmailAddress, "@") { + // validating email addresses is hard - just make sure it contains an '@' sign + // see https://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.email_address.app_error", nil, "", http.StatusBadRequest) + } else if s.GlobalRelaySettings.SmtpUsername == nil || *s.GlobalRelaySettings.SmtpUsername == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.smtp_username.app_error", nil, "", http.StatusBadRequest) + } else if s.GlobalRelaySettings.SmtpPassword == nil || *s.GlobalRelaySettings.SmtpPassword == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay.smtp_password.app_error", nil, "", http.StatusBadRequest) + } + } + } + return nil +} + +func (s *DisplaySettings) isValid() *AppError { + if len(s.CustomUrlSchemes) != 0 { + validProtocolPattern := regexp.MustCompile(`(?i)^\s*[A-Za-z][A-Za-z0-9.+-]*\s*$`) + + for _, scheme := range s.CustomUrlSchemes { + if !validProtocolPattern.MatchString(scheme) { + return NewAppError( + "Config.IsValid", + "model.config.is_valid.display.custom_url_schemes.app_error", + map[string]interface{}{"Scheme": scheme}, + "", + http.StatusBadRequest, + ) + } + } + } + + return nil +} + +func (s *ImageProxySettings) isValid() *AppError { + if *s.Enable { + switch *s.ImageProxyType { + case IMAGE_PROXY_TYPE_LOCAL: + // No other settings to validate + case IMAGE_PROXY_TYPE_ATMOS_CAMO: + if *s.RemoteImageProxyURL == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_url.app_error", nil, "", http.StatusBadRequest) + } + + if *s.RemoteImageProxyOptions == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_options.app_error", nil, "", http.StatusBadRequest) + } + default: + return NewAppError("Config.IsValid", "model.config.is_valid.image_proxy_type.app_error", nil, "", http.StatusBadRequest) + } + } + + return nil +} + +func (o *Config) GetSanitizeOptions() map[string]bool { + options := map[string]bool{} + options["fullname"] = *o.PrivacySettings.ShowFullName + options["email"] = *o.PrivacySettings.ShowEmailAddress + + return options +} + +func (o *Config) Sanitize() { + if o.LdapSettings.BindPassword != nil && len(*o.LdapSettings.BindPassword) > 0 { + *o.LdapSettings.BindPassword = FAKE_SETTING + } + + *o.FileSettings.PublicLinkSalt = FAKE_SETTING + + if len(*o.FileSettings.AmazonS3SecretAccessKey) > 0 { + *o.FileSettings.AmazonS3SecretAccessKey = FAKE_SETTING + } + + if o.EmailSettings.SMTPPassword != nil && len(*o.EmailSettings.SMTPPassword) > 0 { + *o.EmailSettings.SMTPPassword = FAKE_SETTING + } + + if len(*o.GitLabSettings.Secret) > 0 { + *o.GitLabSettings.Secret = FAKE_SETTING + } + + *o.SqlSettings.DataSource = FAKE_SETTING + *o.SqlSettings.AtRestEncryptKey = FAKE_SETTING + + *o.ElasticsearchSettings.Password = FAKE_SETTING + + for i := range o.SqlSettings.DataSourceReplicas { + o.SqlSettings.DataSourceReplicas[i] = FAKE_SETTING + } + + for i := range o.SqlSettings.DataSourceSearchReplicas { + o.SqlSettings.DataSourceSearchReplicas[i] = FAKE_SETTING + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/data_retention_policy.go b/vendor/github.com/mattermost/mattermost-server/v5/model/data_retention_policy.go new file mode 100644 index 00000000..a39ff911 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/data_retention_policy.go @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type DataRetentionPolicy struct { + MessageDeletionEnabled bool `json:"message_deletion_enabled"` + FileDeletionEnabled bool `json:"file_deletion_enabled"` + MessageRetentionCutoff int64 `json:"message_retention_cutoff"` + FileRetentionCutoff int64 `json:"file_retention_cutoff"` +} + +func (me *DataRetentionPolicy) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func DataRetentionPolicyFromJson(data io.Reader) *DataRetentionPolicy { + var me *DataRetentionPolicy + json.NewDecoder(data).Decode(&me) + return me +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/emoji.go b/vendor/github.com/mattermost/mattermost-server/v5/model/emoji.go new file mode 100644 index 00000000..aeee9b38 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/emoji.go @@ -0,0 +1,96 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "regexp" +) + +const ( + EMOJI_NAME_MAX_LENGTH = 64 + EMOJI_SORT_BY_NAME = "name" +) + +var EMOJI_PATTERN = regexp.MustCompile(`:[a-zA-Z0-9_-]+:`) + +type Emoji struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + CreatorId string `json:"creator_id"` + Name string `json:"name"` +} + +func inSystemEmoji(emojiName string) bool { + _, ok := SystemEmojis[emojiName] + return ok +} + +func GetSystemEmojiId(emojiName string) (string, bool) { + id, found := SystemEmojis[emojiName] + return id, found +} + +func (emoji *Emoji) IsValid() *AppError { + if !IsValidId(emoji.Id) { + return NewAppError("Emoji.IsValid", "model.emoji.id.app_error", nil, "", http.StatusBadRequest) + } + + if emoji.CreateAt == 0 { + return NewAppError("Emoji.IsValid", "model.emoji.create_at.app_error", nil, "id="+emoji.Id, http.StatusBadRequest) + } + + if emoji.UpdateAt == 0 { + return NewAppError("Emoji.IsValid", "model.emoji.update_at.app_error", nil, "id="+emoji.Id, http.StatusBadRequest) + } + + if len(emoji.CreatorId) > 26 { + return NewAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "", http.StatusBadRequest) + } + + return IsValidEmojiName(emoji.Name) +} + +func IsValidEmojiName(name string) *AppError { + if len(name) == 0 || len(name) > EMOJI_NAME_MAX_LENGTH || !IsValidAlphaNumHyphenUnderscore(name, false) || inSystemEmoji(name) { + return NewAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (emoji *Emoji) PreSave() { + if emoji.Id == "" { + emoji.Id = NewId() + } + + emoji.CreateAt = GetMillis() + emoji.UpdateAt = emoji.CreateAt +} + +func (emoji *Emoji) ToJson() string { + b, _ := json.Marshal(emoji) + return string(b) +} + +func EmojiFromJson(data io.Reader) *Emoji { + var emoji *Emoji + json.NewDecoder(data).Decode(&emoji) + return emoji +} + +func EmojiListToJson(emojiList []*Emoji) string { + b, _ := json.Marshal(emojiList) + return string(b) +} + +func EmojiListFromJson(data io.Reader) []*Emoji { + var emojiList []*Emoji + json.NewDecoder(data).Decode(&emojiList) + return emojiList +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/emoji_data.go b/vendor/github.com/mattermost/mattermost-server/v5/model/emoji_data.go new file mode 100644 index 00000000..807f6abb --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/emoji_data.go @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +var SystemEmojis = map[string]string{"policewoman": "1f46e-200d-2640-fe0f", "family_man_girl_medium_skin_tone": "1f468-1f3fd", "man_technologist": "1f468-200d-1f4bb", "family_woman_girl_medium_light_skin_tone": "1f469-1f3fc", "massage_woman_medium_light_skin_tone": "1f486-1f3fc-200d-2640-fe0f", "family_woman_woman_boy": "1f469-200d-1f469-200d-1f466", "rice_scene": "1f391", "notes": "1f3b6", "burundi": "1f1e7-1f1ee", "woman_medium_skin_tone": "1f469-1f3fd", "tipping_hand_man_medium_dark_skin_tone": "1f481-1f3fe-200d-2642-fe0f", "new_moon": "1f311", "belize": "1f1e7-1f1ff", "bhutan": "1f1e7-1f1f9", "eu": "1f1ea-1f1fa", "point_up_dark_skin_tone": "261d-1f3ff", "older_man_medium_light_skin_tone": "1f474-1f3fc", "prince": "1f934", "walking_man": "1f6b6", "telephone_receiver": "1f4de", "arrow_upper_right": "2197-fe0f", "taiwan": "1f1f9-1f1fc", "-1_light_skin_tone": "1f44e-1f3fb", "bear": "1f43b", "derelict_house": "1f3da", "blue_book": "1f4d8", "ok": "1f197", "woman_farmer_medium_light_skin_tone": "1f469-1f3fc", "man_shrugging_light_skin_tone": "1f937-1f3fb-200d-2642-fe0f", "dancing_women": "1f46f", "cd": "1f4bf", "tada": "1f389", "virgo": "264d-fe0f", "white_flower": "1f4ae", "guardswoman_medium_dark_skin_tone": "1f482-1f3fe-200d-2640-fe0f", "performing_arts": "1f3ad", "prayer_beads": "1f4ff", "congo_brazzaville": "1f1e8-1f1ec", "point_down_medium_skin_tone": "1f447-1f3fd", "raised_hand_with_fingers_splayed_light_skin_tone": "1f590-1f3fb", "man_playing_water_polo_medium_skin_tone": "1f93d-1f3fd-200d-2642-fe0f", "four_leaf_clover": "1f340", "microphone": "1f3a4", "heartpulse": "1f497", "north_korea": "1f1f0-1f1f5", "neutral_face": "1f610", "volleyball": "1f3d0", "man_playing_water_polo": "1f93d-200d-2642-fe0f", "uk": "1f1ec-1f1e7", "wallis_futuna": "1f1fc-1f1eb", "earth_africa": "1f30d", "droplet": "1f4a7", "construction_worker_man_medium_dark_skin_tone": "1f477-1f3fe-200d-2640-fe0f", "family_woman_woman_girl_boy_medium_light_skin_tone": "1f469-1f3fc", "mountain_biking_man_medium_dark_skin_tone": "1f6b5-1f3fe-200d-2640-fe0f", "vulcan_salute_light_skin_tone": "1f596-1f3fb", "woman_shrugging_dark_skin_tone": "1f937-1f3ff-200d-2640-fe0f", "walking_man_medium_light_skin_tone": "1f6b6-1f3fc-200d-2640-fe0f", "wave": "1f44b", "framed_picture": "1f5bc", "mag": "1f50d", "fist_left": "1f91b", "building_construction": "1f3d7", "clock9": "1f558", "cayman_islands": "1f1f0-1f1fe", "laos": "1f1f1-1f1e6", "woman_playing_handball_dark_skin_tone": "1f93e-1f3ff-200d-2640-fe0f", "man_office_worker": "1f468-200d-1f4bc", "family_man_woman_girl": "1f468-200d-1f469-200d-1f467", "wilted_flower": "1f940", "books": "1f4da", "rage": "1f621", "rice_ball": "1f359", "desert": "1f3dc", "malta": "1f1f2-1f1f9", "haircut_woman_dark_skin_tone": "1f487-1f3ff-200d-2640-fe0f", "symbols": "1f523", "marshall_islands": "1f1f2-1f1ed", "sierra_leone": "1f1f8-1f1f1", "crossed_fingers_medium_dark_skin_tone": "1f91e-1f3fe", "man_judge_medium_skin_tone": "1f468-1f3fd", "bamboo": "1f38d", "keyboard": "2328-fe0f", "clock10": "1f559", "massage_man_medium_skin_tone": "1f486-1f3fd-200d-2642-fe0f", "tipping_hand_man_dark_skin_tone": "1f481-1f3ff-200d-2642-fe0f", "man_facepalming_light_skin_tone": "1f926-1f3fb-200d-2642-fe0f", "train": "1f68b", "traffic_light": "1f6a5", "vietnam": "1f1fb-1f1f3", "boy_medium_light_skin_tone": "1f466-1f3fc", "man_farmer_light_skin_tone": "1f468-1f3fb", "man_singer_medium_dark_skin_tone": "1f468-1f3fe", "woman_cartwheeling_medium_light_skin_tone": "1f938-1f3fc-200d-2640-fe0f", "top": "1f51d", "gb": "1f1ec-1f1e7", "mouse2": "1f401", "do_not_litter": "1f6af", "south_sudan": "1f1f8-1f1f8", "bowing_woman_light_skin_tone": "1f647-1f3fb-200d-2640-fe0f", "family_man_man_girl_girl_medium_light_skin_tone": "1f468-1f3fc", "japanese_goblin": "1f47a", "camel": "1f42b", "taurus": "2649-fe0f", "mute": "1f507", "woman_mechanic_medium_dark_skin_tone": "1f469-1f3fe", "surfer": "1f3c4", "tipping_hand_man": "1f481-200d-2642-fe0f", "family_woman_woman_boy_boy": "1f469-200d-1f469-200d-1f466-200d-1f466", "floppy_disk": "1f4be", "atm": "1f3e7", "clock230": "1f55d", "prince_light_skin_tone": "1f934-1f3fb", "name_badge": "1f4db", "octocat": "octocat", "family_woman_woman_girl_girl_dark_skin_tone": "1f469-1f3ff", "christmas_tree": "1f384", "waxing_gibbous_moon": "1f314", "mountain_cableway": "1f6a0", "woman_scientist_medium_dark_skin_tone": "1f469-1f3fe", "haircut_man_medium_dark_skin_tone": "1f487-1f3fe-200d-2642-fe0f", "basketball_woman_medium_dark_skin_tone": "26f9-1f3fe-200d-2640-fe0f", "family_man_man_boy_medium_light_skin_tone": "1f468-1f3fc", "rowing_woman_medium_dark_skin_tone": "1f6a3-1f3fe-200d-2640-fe0f", "bowling": "1f3b3", "shinto_shrine": "26e9", "round_pushpin": "1f4cd", "cyprus": "1f1e8-1f1fe", "open_hands_dark_skin_tone": "1f450-1f3ff", "clap_medium_skin_tone": "1f44f-1f3fd", "bath_medium_dark_skin_tone": "1f6c0-1f3fe", "briefcase": "1f4bc", "tiger": "1f42f", "morocco": "1f1f2-1f1e6", "open_hands_light_skin_tone": "1f450-1f3fb", "hand_light_skin_tone": "270b-1f3fb", "weight_lifting_man_medium_dark_skin_tone": "1f3cb-1f3fe-200d-2640-fe0f", "mans_shoe": "1f45e", "poland": "1f1f5-1f1f1", "raised_hands_medium_skin_tone": "1f64c-1f3fd", "family_man_woman_girl_boy_light_skin_tone": "1f468-1f3fb", "woman_playing_handball_medium_skin_tone": "1f93e-1f3fd-200d-2640-fe0f", "office": "1f3e2", "woman_singer_medium_dark_skin_tone": "1f469-1f3fe", "family_woman_woman_boy_boy_medium_light_skin_tone": "1f469-1f3fc", "scorpion": "1f982", "tomato": "1f345", "goal_net": "1f945", "chad": "1f1f9-1f1e9", "family_man_woman_girl_boy_medium_skin_tone": "1f468-1f3fd", "mountain_biking_man_light_skin_tone": "1f6b5-1f3fb-200d-2640-fe0f", "weight_lifting_man_dark_skin_tone": "1f3cb-1f3ff-200d-2640-fe0f", "eyeglasses": "1f453", "golfing_woman": "1f3cc-fe0f-200d-2640-fe0f", "dvd": "1f4c0", "clipboard": "1f4cb", "ireland": "1f1ee-1f1ea", "woman_student_dark_skin_tone": "1f469-1f3ff", "angry": "1f620", "baby": "1f476", "women_wrestling": "1f93c-200d-2640-fe0f", "black_square_button": "1f532", "male_detective_medium_light_skin_tone": "1f575-1f3fc-200d-2640-fe0f", "dancer_dark_skin_tone": "1f483-1f3ff", "id": "1f194", "vibration_mode": "1f4f3", "handshake": "1f91d", "tiger2": "1f405", "leaves": "1f343", "baseball": "26be-fe0f", "golf": "26f3-fe0f", "toilet": "1f6bd", "male_detective_dark_skin_tone": "1f575-1f3ff-200d-2640-fe0f", "family_woman_boy": "1f469-200d-1f466", "duck": "1f986", "writing_hand_medium_dark_skin_tone": "270d-1f3fe", "woman_singer_medium_light_skin_tone": "1f469-1f3fc", "man_teacher_medium_skin_tone": "1f468-1f3fd", "lips": "1f444", "octopus": "1f419", "policeman_medium_dark_skin_tone": "1f46e-1f3fe-200d-2640-fe0f", "man_factory_worker_dark_skin_tone": "1f468-1f3ff", "man_astronaut_light_skin_tone": "1f468-1f3fb", "ok_man_dark_skin_tone": "1f646-1f3ff-200d-2642-fe0f", "couple_with_heart": "1f491", "pray_medium_skin_tone": "1f64f-1f3fd", "woman_health_worker_light_skin_tone": "1f469-1f3fb", "tipping_hand_woman_medium_light_skin_tone": "1f481-1f3fc-200d-2640-fe0f", "no_good_woman_dark_skin_tone": "1f645-1f3ff-200d-2640-fe0f", "no_good_woman_medium_dark_skin_tone": "1f645-1f3fe-200d-2640-fe0f", "thumbsdown": "1f44e", "fist": "270a", "camera_flash": "1f4f8", "azerbaijan": "1f1e6-1f1ff", "woman_with_turban_medium_light_skin_tone": "1f473-1f3fc-200d-2640-fe0f", "man_singer_medium_skin_tone": "1f468-1f3fd", "mali": "1f1f2-1f1f1", "blonde_woman_medium_dark_skin_tone": "1f471-1f3fe-200d-2640-fe0f", "family_man_man_girl_light_skin_tone": "1f468-1f3fb", "biking_woman_medium_skin_tone": "1f6b4-1f3fd-200d-2640-fe0f", "crab": "1f980", "green_salad": "1f957", "men_wrestling": "1f93c-200d-2642-fe0f", "-1_medium_dark_skin_tone": "1f44e-1f3fe", "baby_dark_skin_tone": "1f476-1f3ff", "surfing_woman_light_skin_tone": "1f3c4-1f3fb-200d-2640-fe0f", "stuck_out_tongue_winking_eye": "1f61c", "plate_with_cutlery": "1f37d", "swimmer": "1f3ca", "blonde_woman_medium_light_skin_tone": "1f471-1f3fc-200d-2640-fe0f", "curly_loop": "27b0", "india": "1f1ee-1f1f3", "norfolk_island": "1f1f3-1f1eb", "mountain_biking_woman_dark_skin_tone": "1f6b5-1f3ff-200d-2640-fe0f", "ghost": "1f47b", "boar": "1f417", "railway_track": "1f6e4", "100": "1f4af", "metal_medium_dark_skin_tone": "1f918-1f3fe", "woman_playing_handball_light_skin_tone": "1f93e-1f3fb-200d-2640-fe0f", "barber": "1f488", "clock730": "1f562", "equatorial_guinea": "1f1ec-1f1f6", "maldives": "1f1f2-1f1fb", "weight_lifting_woman_medium_dark_skin_tone": "1f3cb-1f3fe-200d-2640-fe0f", "eagle": "1f985", "tea": "1f375", "tanabata_tree": "1f38b", "night_with_stars": "1f303", "balloon": "1f388", "on": "1f51b", "lizard": "1f98e", "beer": "1f37a", "part_alternation_mark": "303d-fe0f", "white_square_button": "1f533", "clock430": "1f55f", "gibraltar": "1f1ec-1f1ee", "massage_woman_medium_skin_tone": "1f486-1f3fd-200d-2640-fe0f", "sweat": "1f613", "athletic_shoe": "1f45f", "joystick": "1f579", "biohazard": "2623-fe0f", "muscle_medium_light_skin_tone": "1f4aa-1f3fc", "bride_with_veil_medium_light_skin_tone": "1f470-1f3fc", "parasol_on_ground": "26f1", "costa_rica": "1f1e8-1f1f7", "woman_student_light_skin_tone": "1f469-1f3fb", "massage_woman_medium_dark_skin_tone": "1f486-1f3fe-200d-2640-fe0f", "surfing_woman_medium_dark_skin_tone": "1f3c4-1f3fe-200d-2640-fe0f", "rowing_woman": "1f6a3-200d-2640-fe0f", "guardsman_light_skin_tone": "1f482-1f3fb-200d-2640-fe0f", "construction_worker_man_light_skin_tone": "1f477-1f3fb-200d-2640-fe0f", "family_woman_boy_boy": "1f469-200d-1f466-200d-1f466", "small_airplane": "1f6e9", "baggage_claim": "1f6c4", "bosnia_herzegovina": "1f1e7-1f1e6", "falkland_islands": "1f1eb-1f1f0", "crossed_fingers_medium_light_skin_tone": "1f91e-1f3fc", "man_scientist_medium_light_skin_tone": "1f468-1f3fc", "family_woman_girl_boy_medium_skin_tone": "1f469-1f3fd", "satisfied": "1f606", "u5408": "1f234", "cn": "1f1e8-1f1f3", "isle_of_man": "1f1ee-1f1f2", "fist_raised_medium_light_skin_tone": "270a-1f3fc", "family_man_woman_girl_dark_skin_tone": "1f468-1f3ff", "family_woman_girl_dark_skin_tone": "1f469-1f3ff", "family_man_boy_medium_skin_tone": "1f468-1f3fd", "money_mouth_face": "1f911", "syringe": "1f489", "hand_medium_light_skin_tone": "270b-1f3fc", "writing_hand_medium_skin_tone": "270d-1f3fd", "man_farmer_medium_light_skin_tone": "1f468-1f3fc", "woman_artist_dark_skin_tone": "1f469-1f3ff", "tickets": "1f39f", "man_cartwheeling_light_skin_tone": "1f938-1f3fb-200d-2642-fe0f", "squid": "1f991", "fish": "1f41f", "memo": "1f4dd", "eye_speech_bubble": "1f441-200d-1f5e8", "+1_light_skin_tone": "1f44d-1f3fb", "tulip": "1f337", "blossom": "1f33c", "family_woman_woman_girl_medium_dark_skin_tone": "1f469-1f3fe", "triumph": "1f624", "rooster": "1f413", "ng": "1f196", "blonde_man_medium_light_skin_tone": "1f471-1f3fc-200d-2640-fe0f", "policeman_light_skin_tone": "1f46e-1f3fb-200d-2640-fe0f", "woman_cook_dark_skin_tone": "1f469-1f3ff", "pray_dark_skin_tone": "1f64f-1f3ff", "point_up_2_medium_skin_tone": "1f446-1f3fd", "busts_in_silhouette": "1f465", "tornado": "1f32a", "woman_juggling": "1f939-200d-2640-fe0f", "cupid": "1f498", "white_check_mark": "2705", "aruba": "1f1e6-1f1fc", "family_man_boy_boy_medium_dark_skin_tone": "1f468-1f3fe", "woman_cartwheeling_dark_skin_tone": "1f938-1f3ff-200d-2640-fe0f", "woman_playing_water_polo_dark_skin_tone": "1f93d-1f3ff-200d-2640-fe0f", "relaxed": "263a-fe0f", "birthday": "1f382", "high_brightness": "1f506", "couple_with_heart_woman_woman_medium_light_skin_tone": "1f469-1f3fc", "biking_man_dark_skin_tone": "1f6b4-1f3ff-200d-2640-fe0f", "disappointed_relieved": "1f625", "canary_islands": "1f1ee-1f1e8", "st_pierre_miquelon": "1f1f5-1f1f2", "trinidad_tobago": "1f1f9-1f1f9", "turkmenistan": "1f1f9-1f1f2", "pouting_woman_medium_skin_tone": "1f64e-1f3fd-200d-2640-fe0f", "man_student_dark_skin_tone": "1f468-1f3ff", "princess_dark_skin_tone": "1f478-1f3ff", "family_woman_boy_boy_medium_skin_tone": "1f469-1f3fd", "old_key": "1f5dd", "muscle_medium_skin_tone": "1f4aa-1f3fd", "ear_medium_dark_skin_tone": "1f442-1f3fe", "girl_medium_dark_skin_tone": "1f467-1f3fe", "man_pilot_dark_skin_tone": "1f468-1f3ff", "wolf": "1f43a", "gem": "1f48e", "arrow_double_up": "23eb", "woman_factory_worker_light_skin_tone": "1f469-1f3fb", "woman_mechanic_medium_skin_tone": "1f469-1f3fd", "woman_firefighter_light_skin_tone": "1f469-1f3fb", "sunglasses": "1f60e", "snake": "1f40d", "pen": "1f58a", "nose_medium_skin_tone": "1f443-1f3fd", "weight_lifting_man": "1f3cb-fe0f", "alarm_clock": "23f0", "golfing_woman_medium_light_skin_tone": "1f3cc-1f3fc-200d-2640-fe0f", "vulcan_salute": "1f596", "earth_asia": "1f30f", "+1_dark_skin_tone": "1f44d-1f3ff", "family_woman_boy_medium_dark_skin_tone": "1f469-1f3fe", "family_woman_girl_girl_light_skin_tone": "1f469-1f3fb", "weight_lifting_man_medium_light_skin_tone": "1f3cb-1f3fc-200d-2640-fe0f", "angel": "1f47c", "peach": "1f351", "truck": "1f69a", "tajikistan": "1f1f9-1f1ef", "tr": "1f1f9-1f1f7", "running_woman_medium_skin_tone": "1f3c3-1f3fd-200d-2640-fe0f", "wrench": "1f527", "black_flag": "1f3f4", "cape_verde": "1f1e8-1f1fb", "man_technologist_medium_light_skin_tone": "1f468-1f3fc", "mountain_biking_woman_medium_light_skin_tone": "1f6b5-1f3fc-200d-2640-fe0f", "man_astronaut_medium_skin_tone": "1f468-1f3fd", "man_in_tuxedo_medium_light_skin_tone": "1f935-1f3fc", "see_no_evil": "1f648", "egg": "1f95a", "1234": "1f522", "lesotho": "1f1f1-1f1f8", "middle_finger_medium_skin_tone": "1f595-1f3fd", "woman_health_worker_medium_light_skin_tone": "1f469-1f3fc", "sneezing_face": "1f927", "man_cook": "1f468-200d-1f373", "mortar_board": "1f393", "candle": "1f56f", "basketball_man_medium_skin_tone": "26f9-1f3fd-200d-2640-fe0f", "ferris_wheel": "1f3a1", "martinique": "1f1f2-1f1f6", "st_vincent_grenadines": "1f1fb-1f1e8", "yemen": "1f1fe-1f1ea", "pray_light_skin_tone": "1f64f-1f3fb", "man_in_tuxedo_medium_skin_tone": "1f935-1f3fd", "woman_pilot_medium_dark_skin_tone": "1f469-1f3fe", "pregnant_woman_dark_skin_tone": "1f930-1f3ff", "fu": "1f595", "haircut": "1f487", "boxing_glove": "1f94a", "page_with_curl": "1f4c3", "muscle_light_skin_tone": "1f4aa-1f3fb", "woman_firefighter_dark_skin_tone": "1f469-1f3ff", "ok_woman_light_skin_tone": "1f646-1f3fb-200d-2640-fe0f", "family_man_woman_boy_boy_medium_light_skin_tone": "1f468-1f3fc", "family_woman_boy_dark_skin_tone": "1f469-1f3ff", "weight_lifting_man_light_skin_tone": "1f3cb-1f3fb-200d-2640-fe0f", "blonde_man": "1f471", "woman_technologist": "1f469-200d-1f4bb", "boom": "1f4a5", "1st_place_medal": "1f947", "nine": "0039-fe0f-20e3", "czech_republic": "1f1e8-1f1ff", "meat_on_bone": "1f356", "hamburger": "1f354", "video_game": "1f3ae", "clock2": "1f551", "woman_facepalming_dark_skin_tone": "1f926-1f3ff-200d-2640-fe0f", "couplekiss_man_man_medium_dark_skin_tone": "1f468-1f3fe", "arrow_lower_right": "2198-fe0f", "haircut_woman_light_skin_tone": "1f487-1f3fb-200d-2640-fe0f", "woman_dark_skin_tone": "1f469-1f3ff", "older_man_light_skin_tone": "1f474-1f3fb", "first_quarter_moon_with_face": "1f31b", "fries": "1f35f", "restroom": "1f6bb", "zero": "0030-fe0f-20e3", "fr": "1f1eb-1f1f7", "kuwait": "1f1f0-1f1fc", "man_health_worker_medium_skin_tone": "1f468-1f3fd", "woman_judge_light_skin_tone": "1f469-1f3fb", "man_judge_dark_skin_tone": "1f468-1f3ff", "ok_woman_medium_skin_tone": "1f646-1f3fd-200d-2640-fe0f", "bike": "1f6b2", "registered": "00ae-fe0f", "blonde_woman_medium_skin_tone": "1f471-1f3fd-200d-2640-fe0f", "stuck_out_tongue_closed_eyes": "1f61d", "collision": "1f4a5", "wheelchair": "267f-fe0f", "black_circle": "26ab-fe0f", "point_up_2_light_skin_tone": "1f446-1f3fb", "older_man": "1f474", "suspension_railway": "1f69f", "libra": "264e-fe0f", "crossed_flags": "1f38c", "man_cartwheeling_medium_dark_skin_tone": "1f938-1f3fe-200d-2642-fe0f", "man_playing_water_polo_dark_skin_tone": "1f93d-1f3ff-200d-2642-fe0f", "scream": "1f631", "no_good_man": "1f645-200d-2642-fe0f", "timer_clock": "23f2", "venezuela": "1f1fb-1f1ea", "raised_back_of_hand_light_skin_tone": "1f91a-1f3fb", "woman_technologist_medium_skin_tone": "1f469-1f3fd", "popcorn": "1f37f", "romania": "1f1f7-1f1f4", "togo": "1f1f9-1f1ec", "writing_hand_dark_skin_tone": "270d-1f3ff", "woman_singer_dark_skin_tone": "1f469-1f3ff", "pouting_woman_medium_dark_skin_tone": "1f64e-1f3fe-200d-2640-fe0f", "man_health_worker_light_skin_tone": "1f468-1f3fb", "dancer_medium_light_skin_tone": "1f483-1f3fc", "phone": "260e-fe0f", "chart": "1f4b9", "repeat": "1f501", "mahjong": "1f004-fe0f", "liberia": "1f1f1-1f1f7", "rage3": "rage3", "person_frowning": "1f64d", "open_hands_medium_skin_tone": "1f450-1f3fd", "man_dark_skin_tone": "1f468-1f3ff", "man_factory_worker_medium_light_skin_tone": "1f468-1f3fc", "man_astronaut_medium_light_skin_tone": "1f468-1f3fc", "ring": "1f48d", "ok_hand_medium_skin_tone": "1f44c-1f3fd", "santa": "1f385", "beach_umbrella": "1f3d6", "finland": "1f1eb-1f1ee", "woman_facepalming_medium_skin_tone": "1f926-1f3fd-200d-2640-fe0f", "woman_shrugging_medium_light_skin_tone": "1f937-1f3fc-200d-2640-fe0f", "sunflower": "1f33b", "ok_hand_dark_skin_tone": "1f44c-1f3ff", "santa_medium_skin_tone": "1f385-1f3fd", "call_me_hand_medium_skin_tone": "1f919-1f3fd", "man_firefighter_light_skin_tone": "1f468-1f3fb", "kiss": "1f48b", "mandarin": "1f34a", "dollar": "1f4b5", "clock3": "1f552", "argentina": "1f1e6-1f1f7", "fist_left_light_skin_tone": "1f91b-1f3fb", "santa_light_skin_tone": "1f385-1f3fb", "family_man_girl_boy_medium_light_skin_tone": "1f468-1f3fc", "sushi": "1f363", "rice": "1f35a", "mailbox_with_mail": "1f4ec", "woman_cook_medium_dark_skin_tone": "1f469-1f3fe", "family_man_woman_girl_girl_medium_dark_skin_tone": "1f468-1f3fe", "straight_ruler": "1f4cf", "blue_heart": "1f499", "slightly_frowning_face": "1f641", "crossed_fingers": "1f91e", "seedling": "1f331", "herb": "1f33f", "medal_military": "1f396", "camping": "1f3d5", "arrow_backward": "25c0-fe0f", "heavy_multiplication_x": "2716-fe0f", "icecream": "1f366", "heavy_dollar_sign": "1f4b2", "frowning_woman_dark_skin_tone": "1f64d-1f3ff-200d-2640-fe0f", "family_man_woman_boy_boy_medium_dark_skin_tone": "1f468-1f3fe", "brazil": "1f1e7-1f1f7", "fist_right_medium_light_skin_tone": "1f91c-1f3fc", "man_scientist_medium_dark_skin_tone": "1f468-1f3fe", "family_woman_girl_light_skin_tone": "1f469-1f3fb", "swimming_man_medium_dark_skin_tone": "1f3ca-1f3fe-200d-2640-fe0f", "man": "1f468", "lemon": "1f34b", "japanese_castle": "1f3ef", "cinema": "1f3a6", "wave_light_skin_tone": "1f44b-1f3fb", "middle_finger_medium_light_skin_tone": "1f595-1f3fc", "five": "0035-fe0f-20e3", "boy_medium_dark_skin_tone": "1f466-1f3fe", "woman_technologist_dark_skin_tone": "1f469-1f3ff", "man_playing_handball_light_skin_tone": "1f93e-1f3fb-200d-2642-fe0f", "construction_worker_man": "1f477", "stadium": "1f3df", "biking_woman_dark_skin_tone": "1f6b4-1f3ff-200d-2640-fe0f", "trophy": "1f3c6", "arrow_left": "2b05-fe0f", "boy_dark_skin_tone": "1f466-1f3ff", "no_good_woman_light_skin_tone": "1f645-1f3fb-200d-2640-fe0f", "skull_and_crossbones": "2620-fe0f", "couple_with_heart_woman_woman": "1f469-200d-2764-fe0f-200d-1f469", "no_bicycles": "1f6b3", "bell": "1f514", "feelsgood": "feelsgood", "bowing_man_dark_skin_tone": "1f647-1f3ff-200d-2640-fe0f", "mouse": "1f42d", "anchor": "2693-fe0f", "cyclone": "1f300", "solomon_islands": "1f1f8-1f1e7", "basketball": "1f3c0", "notebook_with_decorative_cover": "1f4d4", "family_man_girl_girl_dark_skin_tone": "1f468-1f3ff", "singapore": "1f1f8-1f1ec", "golfing_man_medium_dark_skin_tone": "1f3cc-1f3fe-200d-2640-fe0f", "woman": "1f469", "fried_shrimp": "1f364", "construction": "1f6a7", "eight_pointed_black_star": "2734-fe0f", "black_joker": "1f0cf", "cambodia": "1f1f0-1f1ed", "mount_fuji": "1f5fb", "link": "1f517", "womens": "1f6ba", "family_man_man_boy_boy_medium_skin_tone": "1f468-1f3fd", "joy": "1f602", "crying_cat_face": "1f63f", "parking": "1f17f-fe0f", "barbados": "1f1e7-1f1e7", "bowing_woman_dark_skin_tone": "1f647-1f3ff-200d-2640-fe0f", "angel_medium_light_skin_tone": "1f47c-1f3fc", "waning_gibbous_moon": "1f316", "synagogue": "1f54d", "american_samoa": "1f1e6-1f1f8", "basketball_woman_medium_light_skin_tone": "26f9-1f3fc-200d-2640-fe0f", "bug": "1f41b", "woman_farmer_light_skin_tone": "1f469-1f3fb", "door": "1f6aa", "place_of_worship": "1f6d0", "eight_spoked_asterisk": "2733-fe0f", "mrs_claus_dark_skin_tone": "1f936-1f3ff", "u7a7a": "1f233", "man_astronaut_medium_dark_skin_tone": "1f468-1f3fe", "jack_o_lantern": "1f383", "lock_with_ink_pen": "1f50f", "male_detective_medium_skin_tone": "1f575-1f3fd-200d-2640-fe0f", "woman_firefighter_medium_dark_skin_tone": "1f469-1f3fe", "smiling_imp": "1f608", "tv": "1f4fa", "pouting_man": "1f64e-200d-2642-fe0f", "e-mail": "1f4e7", "package": "1f4e6", "clock130": "1f55c", "family_man_boy_light_skin_tone": "1f468-1f3fb", "cat2": "1f408", "mountain_biking_woman_medium_dark_skin_tone": "1f6b5-1f3fe-200d-2640-fe0f", "nauseated_face": "1f922", "fountain": "26f2-fe0f", "middle_finger": "1f595", "dancers": "1f46f", "cactus": "1f335", "man_student_light_skin_tone": "1f468-1f3fb", "family_man_man_girl_dark_skin_tone": "1f468-1f3ff", "family_man_girl_girl_medium_skin_tone": "1f468-1f3fd", "us_virgin_islands": "1f1fb-1f1ee", "woman_astronaut_medium_dark_skin_tone": "1f469-1f3fe", "honeybee": "1f41d", "bouquet": "1f490", "golfing_man": "1f3cc-fe0f", "u7981": "1f232", "french_guiana": "1f1ec-1f1eb", "kenya": "1f1f0-1f1ea", "melon": "1f348", "nicaragua": "1f1f3-1f1ee", "raised_hand_with_fingers_splayed_medium_light_skin_tone": "1f590-1f3fc", "bath_light_skin_tone": "1f6c0-1f3fb", "man_pilot_medium_light_skin_tone": "1f468-1f3fc", "european_post_office": "1f3e4", "mobile_phone_off": "1f4f4", "no_smoking": "1f6ad", "family_woman_girl_girl_medium_light_skin_tone": "1f469-1f3fc", "family_woman_girl_medium_dark_skin_tone": "1f469-1f3fe", "man_juggling_medium_dark_skin_tone": "1f939-1f3fe-200d-2642-fe0f", "expressionless": "1f611", "school_satchel": "1f392", "film_strip": "1f39e", "running_man_light_skin_tone": "1f3c3-1f3fb-200d-2640-fe0f", "family_man_woman_girl_medium_dark_skin_tone": "1f468-1f3fe", "family_woman_woman_boy_boy_light_skin_tone": "1f469-1f3fb", "smiley_cat": "1f63a", "chestnut": "1f330", "girl_dark_skin_tone": "1f467-1f3ff", "bride_with_veil_medium_dark_skin_tone": "1f470-1f3fe", "family_man_man_boy_boy_light_skin_tone": "1f468-1f3fb", "family_man_boy_boy_medium_light_skin_tone": "1f468-1f3fc", "wink": "1f609", "carrot": "1f955", "credit_card": "1f4b3", "triangular_ruler": "1f4d0", "question": "2753", "+1_medium_dark_skin_tone": "1f44d-1f3fe", "man_teacher_medium_dark_skin_tone": "1f468-1f3fe", "family_man_girl_girl_medium_light_skin_tone": "1f468-1f3fc", "kimono": "1f458", "bellhop_bell": "1f6ce", "red_circle": "1f534", "call_me_hand_light_skin_tone": "1f919-1f3fb", "nail_care_medium_skin_tone": "1f485-1f3fd", "woman_teacher_medium_skin_tone": "1f469-1f3fd", "woman_juggling_dark_skin_tone": "1f939-1f3ff-200d-2640-fe0f", "runner": "1f3c3", "heavy_check_mark": "2714-fe0f", "family_man_girl_girl_medium_dark_skin_tone": "1f468-1f3fe", "tent": "26fa-fe0f", "card_index_dividers": "1f5c2", "man_singer_dark_skin_tone": "1f468-1f3ff", "man_firefighter_medium_dark_skin_tone": "1f468-1f3fe", "man_playing_handball_medium_dark_skin_tone": "1f93e-1f3fe-200d-2642-fe0f", "female_detective_medium_dark_skin_tone": "1f575-1f3fe-200d-2640-fe0f", "metal": "1f918", "dark_sunglasses": "1f576", "vertical_traffic_light": "1f6a6", "four": "0034-fe0f-20e3", "wavy_dash": "3030-fe0f", "ear_medium_light_skin_tone": "1f442-1f3fc", "man_juggling_dark_skin_tone": "1f939-1f3ff-200d-2642-fe0f", "kissing_heart": "1f618", "sweet_potato": "1f360", "gift_heart": "1f49d", "man_technologist_light_skin_tone": "1f468-1f3fb", "prince_medium_dark_skin_tone": "1f934-1f3fe", "ok_man_medium_skin_tone": "1f646-1f3fd-200d-2642-fe0f", "womans_clothes": "1f45a", "roller_coaster": "1f3a2", "woman_student_medium_light_skin_tone": "1f469-1f3fc", "zipper_mouth_face": "1f910", "person_with_blond_hair": "1f471", "leftwards_arrow_with_hook": "21a9-fe0f", "white_circle": "26aa-fe0f", "afghanistan": "1f1e6-1f1eb", "face_with_thermometer": "1f912", "bow": "1f647", "kr": "1f1f0-1f1f7", "finnadie": "finnadie", "girl_light_skin_tone": "1f467-1f3fb", "woman_farmer_medium_skin_tone": "1f469-1f3fd", "umbrella": "2614-fe0f", "ice_cream": "1f368", "point_down_medium_light_skin_tone": "1f447-1f3fc", "woman_health_worker_dark_skin_tone": "1f469-1f3ff", "rowing_woman_medium_light_skin_tone": "1f6a3-1f3fc-200d-2640-fe0f", "money_with_wings": "1f4b8", "dolls": "1f38e", "surfing_man_medium_dark_skin_tone": "1f3c4-1f3fe-200d-2640-fe0f", "mrs_claus_medium_skin_tone": "1f936-1f3fd", "basketball_man_dark_skin_tone": "26f9-1f3ff-200d-2640-fe0f", "dragon_face": "1f432", "woman_cartwheeling": "1f938-200d-2640-fe0f", "aquarius": "2652-fe0f", "sos": "1f198", "clock1230": "1f567", "haiti": "1f1ed-1f1f9", "woman_facepalming": "1f926-200d-2640-fe0f", "clinking_glasses": "1f942", "trollface": "trollface", "mrs_claus_light_skin_tone": "1f936-1f3fb", "clap": "1f44f", "couplekiss_man_man": "1f468-200d-2764-fe0f-200d-1f48b-200d-1f468", "page_facing_up": "1f4c4", "belgium": "1f1e7-1f1ea", "curacao": "1f1e8-1f1fc", "family_woman_woman_boy_dark_skin_tone": "1f469-1f3ff", "two_men_holding_hands": "1f46c", "mountain_snow": "1f3d4", "wind_chime": "1f390", "person_with_pouting_face": "1f64e", "cityscape": "1f3d9", "bride_with_veil_dark_skin_tone": "1f470-1f3ff", "frowning_woman_light_skin_tone": "1f64d-1f3fb-200d-2640-fe0f", "bath_medium_light_skin_tone": "1f6c0-1f3fc", "sheep": "1f411", "sparkler": "1f387", "frowning_woman": "1f64d", "rat": "1f400", "custard": "1f36e", "video_camera": "1f4f9", "open_umbrella": "2602-fe0f", "man_with_turban_medium_light_skin_tone": "1f473-1f3fc-200d-2640-fe0f", "woman_student_medium_dark_skin_tone": "1f469-1f3fe", "ok_woman_medium_light_skin_tone": "1f646-1f3fc-200d-2640-fe0f", "swimming_man_dark_skin_tone": "1f3ca-1f3ff-200d-2640-fe0f", "man_cook_light_skin_tone": "1f468-1f3fb", "running_woman_light_skin_tone": "1f3c3-1f3fb-200d-2640-fe0f", "rabbit": "1f430", "ox": "1f402", "corn": "1f33d", "mozambique": "1f1f2-1f1ff", "point_right_light_skin_tone": "1f449-1f3fb", "nail_care_light_skin_tone": "1f485-1f3fb", "smiley": "1f603", "new_moon_with_face": "1f31a", "croatia": "1f1ed-1f1f7", "man_judge_medium_dark_skin_tone": "1f468-1f3fe", "fist_raised": "270a", "man_astronaut": "1f468-200d-1f680", "clock1130": "1f566", "st_lucia": "1f1f1-1f1e8", "princess": "1f478", "fist_left_medium_light_skin_tone": "1f91b-1f3fc", "point_left_medium_dark_skin_tone": "1f448-1f3fe", "woman_factory_worker_medium_skin_tone": "1f469-1f3fd", "angel_dark_skin_tone": "1f47c-1f3ff", "woman_cook": "1f469-200d-1f373", "koala": "1f428", "satellite": "1f4e1", "book": "1f4d6", "large_orange_diamond": "1f536", "monaco": "1f1f2-1f1e8", "spiral_notepad": "1f5d2", "capricorn": "2651-fe0f", "bacon": "1f953", "blonde_man_medium_dark_skin_tone": "1f471-1f3fe-200d-2640-fe0f", "business_suit_levitating_dark_skin_tone": "1f574-1f3ff", "call_me_hand_medium_light_skin_tone": "1f919-1f3fc", "female_detective_medium_light_skin_tone": "1f575-1f3fc-200d-2640-fe0f", "haircut_woman_medium_light_skin_tone": "1f487-1f3fc-200d-2640-fe0f", "alien": "1f47d", "baguette_bread": "1f956", "northern_mariana_islands": "1f1f2-1f1f5", "ukraine": "1f1fa-1f1e6", "flushed": "1f633", "man_scientist": "1f468-200d-1f52c", "trident": "1f531", "family_woman_woman_girl_boy_medium_skin_tone": "1f469-1f3fd", "family_man_boy_boy": "1f468-200d-1f466-200d-1f466", "tennis": "1f3be", "fire_engine": "1f692", "pushpin": "1f4cc", "man_health_worker_medium_dark_skin_tone": "1f468-1f3fe", "boy": "1f466", "headphones": "1f3a7", "fuelpump": "26fd-fe0f", "u6709": "1f236", "man_cook_medium_skin_tone": "1f468-1f3fd", "bride_with_veil_medium_skin_tone": "1f470-1f3fd", "point_up": "261d-fe0f", "necktie": "1f454", "control_knobs": "1f39b", "austria": "1f1e6-1f1f9", "papua_new_guinea": "1f1f5-1f1ec", "alembic": "2697-fe0f", "cook_islands": "1f1e8-1f1f0", "iceland": "1f1ee-1f1f8", "car": "1f697", "potable_water": "1f6b0", "haircut_man_medium_light_skin_tone": "1f487-1f3fc-200d-2642-fe0f", "couplekiss_woman_woman_medium_light_skin_tone": "1f469-1f3fc", "couplekiss_man_man_medium_skin_tone": "1f468-1f3fd", "cookie": "1f36a", "flight_departure": "1f6eb", "muscle_dark_skin_tone": "1f4aa-1f3ff", "construction_worker_man_medium_skin_tone": "1f477-1f3fd-200d-2640-fe0f", "black_medium_small_square": "25fe-fe0f", "guyana": "1f1ec-1f1fe", "file_folder": "1f4c1", "fountain_pen": "1f58b", "construction_worker_woman_medium_skin_tone": "1f477-1f3fd-200d-2640-fe0f", "family_man_woman_girl_boy_medium_light_skin_tone": "1f468-1f3fc", "poultry_leg": "1f357", "ski": "1f3bf", "guardswoman_medium_skin_tone": "1f482-1f3fd-200d-2640-fe0f", "family_man_man_girl_medium_dark_skin_tone": "1f468-1f3fe", "family_woman_boy_light_skin_tone": "1f469-1f3fb", "trumpet": "1f3ba", "no_pedestrians": "1f6b7", "heavy_minus_sign": "2796", "fist_oncoming": "1f44a", "ambulance": "1f691", "man_artist_dark_skin_tone": "1f468-1f3ff", "drum": "1f941", "train2": "1f686", "u7121": "1f21a-fe0f", "burkina_faso": "1f1e7-1f1eb", "nose_light_skin_tone": "1f443-1f3fb", "policewoman_medium_dark_skin_tone": "1f46e-1f3fe-200d-2640-fe0f", "lantern": "1f3ee", "metal_light_skin_tone": "1f918-1f3fb", "male_detective_light_skin_tone": "1f575-1f3fb-200d-2640-fe0f", "woman_juggling_medium_dark_skin_tone": "1f939-1f3fe-200d-2640-fe0f", "rowing_man_light_skin_tone": "1f6a3-1f3fb-200d-2640-fe0f", "lying_face": "1f925", "point_left": "1f448", "rosette": "1f3f5", "houses": "1f3d8", "repeat_one": "1f502", "liechtenstein": "1f1f1-1f1ee", "cloud_with_lightning": "1f329", "man_cartwheeling": "1f938-200d-2642-fe0f", "pause_button": "23f8", "arrows_clockwise": "1f503", "raised_hand_with_fingers_splayed_dark_skin_tone": "1f590-1f3ff", "clap_dark_skin_tone": "1f44f-1f3ff", "raising_hand_man_medium_skin_tone": "1f64b-1f3fd-200d-2642-fe0f", "family_woman_woman_girl_girl_medium_dark_skin_tone": "1f469-1f3fe", "dog": "1f436", "pouting_man_medium_dark_skin_tone": "1f64e-1f3fe-200d-2642-fe0f", "surfing_woman_medium_skin_tone": "1f3c4-1f3fd-200d-2640-fe0f", "confused": "1f615", "detective": "1f575-fe0f", "studio_microphone": "1f399", "fist_oncoming_medium_light_skin_tone": "1f44a-1f3fc", "man_firefighter_medium_skin_tone": "1f468-1f3fd", "tshirt": "1f455", "trolleybus": "1f68e", "norway": "1f1f3-1f1f4", "neckbeard": "neckbeard", "three": "0033-fe0f-20e3", "point_right_medium_skin_tone": "1f449-1f3fd", "man_medium_dark_skin_tone": "1f468-1f3fe", "pouting_man_medium_light_skin_tone": "1f64e-1f3fc-200d-2642-fe0f", "clock630": "1f561", "fist_raised_medium_skin_tone": "270a-1f3fd", "anguished": "1f627", "eye": "1f441", "bride_with_veil": "1f470", "hear_no_evil": "1f649", "wine_glass": "1f377", "soon": "1f51c", "family_man_boy_boy_light_skin_tone": "1f468-1f3fb", "family_woman_boy_boy_medium_light_skin_tone": "1f469-1f3fc", "dromedary_camel": "1f42a", "chipmunk": "1f43f", "soccer": "26bd-fe0f", "man_with_gua_pi_mao_medium_light_skin_tone": "1f472-1f3fc", "business_suit_levitating_medium_light_skin_tone": "1f574-1f3fc", "running_woman_medium_dark_skin_tone": "1f3c3-1f3fe-200d-2640-fe0f", "speaking_head": "1f5e3", "menorah": "1f54e", "non-potable_water": "1f6b1", "woman_with_turban_dark_skin_tone": "1f473-1f3ff-200d-2640-fe0f", "woman_shrugging_medium_skin_tone": "1f937-1f3fd-200d-2640-fe0f", "frowning_woman_medium_light_skin_tone": "1f64d-1f3fc-200d-2640-fe0f", "biking_man_medium_light_skin_tone": "1f6b4-1f3fc-200d-2640-fe0f", "woman_shrugging": "1f937-200d-2640-fe0f", "arrow_upper_left": "2196-fe0f", "metal_medium_skin_tone": "1f918-1f3fd", "woman_factory_worker_dark_skin_tone": "1f469-1f3ff", "hotsprings": "2668-fe0f", "ear_dark_skin_tone": "1f442-1f3ff", "girl_medium_skin_tone": "1f467-1f3fd", "woman_farmer_dark_skin_tone": "1f469-1f3ff", "man_student_medium_skin_tone": "1f468-1f3fd", "biking_man_medium_skin_tone": "1f6b4-1f3fd-200d-2640-fe0f", "woman_scientist": "1f469-200d-1f52c", "vs": "1f19a", "weight_lifting_man_medium_skin_tone": "1f3cb-1f3fd-200d-2640-fe0f", "arrow_right": "27a1-fe0f", "woman_juggling_medium_light_skin_tone": "1f939-1f3fc-200d-2640-fe0f", "racehorse": "1f40e", "sun_behind_large_cloud": "1f325", "convenience_store": "1f3ea", "namibia": "1f1f3-1f1e6", "raised_hands_medium_dark_skin_tone": "1f64c-1f3fe", "man_judge_medium_light_skin_tone": "1f468-1f3fc", "dancer_medium_skin_tone": "1f483-1f3fd", "fearful": "1f628", "frog": "1f438", "shopping_cart": "1f6d2", "family_man_boy_medium_light_skin_tone": "1f468-1f3fc", "basketball_man_medium_dark_skin_tone": "26f9-1f3fe-200d-2640-fe0f", "oman": "1f1f4-1f1f2", "paraguay": "1f1f5-1f1fe", "horse": "1f434", "tram": "1f68a", "wastebasket": "1f5d1", "yen": "1f4b4", "heavy_exclamation_mark": "2757-fe0f", "arrow_double_down": "23ec", "walking_woman_dark_skin_tone": "1f6b6-1f3ff-200d-2640-fe0f", "shoe": "1f45e", "ear_of_rice": "1f33e", "mountain": "26f0", "uzbekistan": "1f1fa-1f1ff", "baby_light_skin_tone": "1f476-1f3fb", "haircut_woman_medium_dark_skin_tone": "1f487-1f3fe-200d-2640-fe0f", "golfing_woman_medium_skin_tone": "1f3cc-1f3fd-200d-2640-fe0f", "earth_americas": "1f30e", "woman_playing_water_polo_medium_light_skin_tone": "1f93d-1f3fc-200d-2640-fe0f", "walking_woman": "1f6b6-200d-2640-fe0f", "fried_egg": "1f373", "rocket": "1f680", "artificial_satellite": "1f6f0", "man_shrugging_medium_skin_tone": "1f937-1f3fd-200d-2642-fe0f", "golfing_man_dark_skin_tone": "1f3cc-1f3ff-200d-2640-fe0f", "older_woman_medium_skin_tone": "1f475-1f3fd", "man_with_gua_pi_mao_medium_dark_skin_tone": "1f472-1f3fe", "persevere": "1f623", "raising_hand_woman": "1f64b", "pig": "1f437", "european_castle": "1f3f0", "department_store": "1f3ec", "fist_right_light_skin_tone": "1f91c-1f3fb", "raising_hand_woman_dark_skin_tone": "1f64b-1f3ff-200d-2640-fe0f", "paw_prints": "1f43e", "moon": "1f314", "man_medium_skin_tone": "1f468-1f3fd", "rowing_man_dark_skin_tone": "1f6a3-1f3ff-200d-2640-fe0f", "sleepy": "1f62a", "light_rail": "1f688", "peace_symbol": "262e-fe0f", "m": "24c2-fe0f", "woman_pilot_medium_skin_tone": "1f469-1f3fd", "dango": "1f361", "minibus": "1f690", "family_man_man_girl_girl_medium_dark_skin_tone": "1f468-1f3fe", "dizzy_face": "1f635", "bowing_woman": "1f647-200d-2640-fe0f", "pig2": "1f416", "factory": "1f3ed", "small_red_triangle": "1f53a", "ok_man_light_skin_tone": "1f646-1f3fb-200d-2642-fe0f", "two_women_holding_hands": "1f46d", "funeral_urn": "26b1-fe0f", "cocos_islands": "1f1e8-1f1e8", "lipstick": "1f484", "fleur_de_lis": "269c-fe0f", "man_with_gua_pi_mao_dark_skin_tone": "1f472-1f3ff", "woman_factory_worker_medium_dark_skin_tone": "1f469-1f3fe", "no_good_man_medium_light_skin_tone": "1f645-1f3fc-200d-2642-fe0f", "horse_racing_medium_dark_skin_tone": "1f3c7-1f3fe", "clock1030": "1f565", "couplekiss_man_man_dark_skin_tone": "1f468-1f3ff", "frowning_man": "1f64d-200d-2642-fe0f", "family_woman_boy_boy_dark_skin_tone": "1f469-1f3ff", "family_man_girl_boy_light_skin_tone": "1f468-1f3fb", "smile": "1f604", "clock7": "1f556", "massage_man": "1f486-200d-2642-fe0f", "guardswoman_dark_skin_tone": "1f482-1f3ff-200d-2640-fe0f", "raising_hand_man_dark_skin_tone": "1f64b-1f3ff-200d-2642-fe0f", "woman_with_turban_medium_dark_skin_tone": "1f473-1f3fe-200d-2640-fe0f", "worried": "1f61f", "no_good": "1f645", "card_index": "1f4c7", "aland_islands": "1f1e6-1f1fd", "lion": "1f981", "hammer": "1f528", "bomb": "1f4a3", "reunion": "1f1f7-1f1ea", "walking_man_light_skin_tone": "1f6b6-1f3fb-200d-2640-fe0f", "family_woman_boy_medium_light_skin_tone": "1f469-1f3fc", "pouting_cat": "1f63e", "cow": "1f42e", "motor_scooter": "1f6f5", "hong_kong": "1f1ed-1f1f0", "family_man_girl_medium_dark_skin_tone": "1f468-1f3fe", "sailboat": "26f5-fe0f", "fiji": "1f1eb-1f1ef", "raised_hands_medium_light_skin_tone": "1f64c-1f3fc", "woman_office_worker_dark_skin_tone": "1f469-1f3ff", "family_man_woman_girl_girl_medium_light_skin_tone": "1f468-1f3fc", "arrow_up": "2b06-fe0f", "walking_woman_medium_light_skin_tone": "1f6b6-1f3fc-200d-2640-fe0f", "nose_medium_light_skin_tone": "1f443-1f3fc", "basketball_woman": "26f9-fe0f-200d-2640-fe0f", "+1_medium_light_skin_tone": "1f44d-1f3fc", "crossed_fingers_medium_skin_tone": "1f91e-1f3fd", "raised_back_of_hand_dark_skin_tone": "1f91a-1f3ff", "swimming_woman_medium_light_skin_tone": "1f3ca-1f3fc-200d-2640-fe0f", "construction_worker_woman": "1f477-200d-2640-fe0f", "rugby_football": "1f3c9", "micronesia": "1f1eb-1f1f2", "point_up_2_medium_light_skin_tone": "1f446-1f3fc", "running_man_dark_skin_tone": "1f3c3-1f3ff-200d-2640-fe0f", "woman_playing_handball_medium_light_skin_tone": "1f93e-1f3fc-200d-2640-fe0f", "speaker": "1f508", "jersey": "1f1ef-1f1ea", "laughing": "1f606", "pregnant_woman": "1f930", "haircut_woman": "1f487", "blue_car": "1f699", "microscope": "1f52c", "postbox": "1f4ee", "man_firefighter_dark_skin_tone": "1f468-1f3ff", "sunny": "2600-fe0f", "beginner": "1f530", "clap_medium_light_skin_tone": "1f44f-1f3fc", "man_with_turban_dark_skin_tone": "1f473-1f3ff-200d-2640-fe0f", "rotating_light": "1f6a8", "saudi_arabia": "1f1f8-1f1e6", "family_woman_woman_girl_girl_medium_skin_tone": "1f469-1f3fd", "family_woman_girl_boy_light_skin_tone": "1f469-1f3fb", "man_with_gua_pi_mao": "1f472", "electric_plug": "1f50c", "panama": "1f1f5-1f1e6", "family_woman_woman_girl_light_skin_tone": "1f469-1f3fb", "thinking": "1f914", "point_down": "1f447", "spider": "1f577", "cloud_with_lightning_and_rain": "26c8", "ice_skate": "26f8", "ok_man_medium_dark_skin_tone": "1f646-1f3fe-200d-2642-fe0f", "netherlands": "1f1f3-1f1f1", "family_man_woman_boy": "1f46a", "orange": "1f34a", "snowboarder": "1f3c2", "passenger_ship": "1f6f3", "arrows_counterclockwise": "1f504", "tractor": "1f69c", "gambia": "1f1ec-1f1f2", "middle_finger_dark_skin_tone": "1f595-1f3ff", "tipping_hand_woman_medium_dark_skin_tone": "1f481-1f3fe-200d-2640-fe0f", "family_man_man_girl_boy_medium_light_skin_tone": "1f468-1f3fc", "thumbsup": "1f44d", "couple": "1f46b", "pouch": "1f45d", "asterisk": "002a-fe0f-20e3", "anguilla": "1f1e6-1f1ee", "woman_cook_light_skin_tone": "1f469-1f3fb", "kissing_cat": "1f63d", "nose": "1f443", "point_left_medium_skin_tone": "1f448-1f3fd", "baby_chick": "1f424", "deciduous_tree": "1f333", "u7533": "1f238", "surfing_woman_dark_skin_tone": "1f3c4-1f3ff-200d-2640-fe0f", "woman_shrugging_medium_dark_skin_tone": "1f937-1f3fe-200d-2640-fe0f", "family_woman_woman_boy_boy_dark_skin_tone": "1f469-1f3ff", "cloud_with_rain": "1f327", "oden": "1f362", "botswana": "1f1e7-1f1fc", "greenland": "1f1ec-1f1f1", "man_office_worker_light_skin_tone": "1f468-1f3fb", "raising_hand_woman_medium_dark_skin_tone": "1f64b-1f3fe-200d-2640-fe0f", "family_man_man_girl_boy_medium_dark_skin_tone": "1f468-1f3fe", "school": "1f3eb", "woman_astronaut_light_skin_tone": "1f469-1f3fb", "woman_judge_medium_skin_tone": "1f469-1f3fd", "dancing_men": "1f46f-200d-2642-fe0f", "paperclips": "1f587", "underage": "1f51e", "ok_woman_dark_skin_tone": "1f646-1f3ff-200d-2640-fe0f", "man_playing_handball_dark_skin_tone": "1f93e-1f3ff-200d-2642-fe0f", "family_man_girl_girl": "1f468-200d-1f467-200d-1f467", "wind_face": "1f32c", "banana": "1f34c", "eight": "0038-fe0f-20e3", "man_technologist_medium_dark_skin_tone": "1f468-1f3fe", "man_office_worker_medium_skin_tone": "1f468-1f3fd", "walking_man_dark_skin_tone": "1f6b6-1f3ff-200d-2640-fe0f", "family_man_man_girl_girl_medium_skin_tone": "1f468-1f3fd", "snowman": "26c4-fe0f", "basketball_man": "26f9-fe0f", "information_source": "2139-fe0f", "cote_divoire": "1f1e8-1f1ee", "man_in_tuxedo_light_skin_tone": "1f935-1f3fb", "walking_woman_light_skin_tone": "1f6b6-1f3fb-200d-2640-fe0f", "woman_playing_water_polo_light_skin_tone": "1f93d-1f3fb-200d-2640-fe0f", "bird": "1f426", "o": "2b55-fe0f", "family_woman_girl_medium_skin_tone": "1f469-1f3fd", "rowing_woman_dark_skin_tone": "1f6a3-1f3ff-200d-2640-fe0f", "facepunch": "1f44a", "railway_car": "1f683", "wave_dark_skin_tone": "1f44b-1f3ff", "man_cook_medium_dark_skin_tone": "1f468-1f3fe", "prince_medium_light_skin_tone": "1f934-1f3fc", "cowboy_hat_face": "1f920", "handbag": "1f45c", "hourglass": "231b-fe0f", "albania": "1f1e6-1f1f1", "chile": "1f1e8-1f1f1", "woman_singer_medium_skin_tone": "1f469-1f3fd", "ear_medium_skin_tone": "1f442-1f3fd", "pouting_man_medium_skin_tone": "1f64e-1f3fd-200d-2642-fe0f", "surfing_man_medium_light_skin_tone": "1f3c4-1f3fc-200d-2640-fe0f", "eggplant": "1f346", "next_track_button": "23ed", "gabon": "1f1ec-1f1e6", "western_sahara": "1f1ea-1f1ed", "raised_hands_light_skin_tone": "1f64c-1f3fb", "older_woman_medium_light_skin_tone": "1f475-1f3fc", "joy_cat": "1f639", "feet": "1f43e", "partly_sunny": "26c5-fe0f", "pig_nose": "1f43d", "wc": "1f6be", "malaysia": "1f1f2-1f1fe", "girl_medium_light_skin_tone": "1f467-1f3fc", "man_office_worker_medium_dark_skin_tone": "1f468-1f3fe", "man_mechanic_medium_light_skin_tone": "1f468-1f3fc", "shamrock": "2618-fe0f", "tumbler_glass": "1f943", "palestinian_territories": "1f1f5-1f1f8", "kissing": "1f617", "city_sunset": "1f306", "pencil2": "270f-fe0f", "cool": "1f192", "australia": "1f1e6-1f1fa", "green_heart": "1f49a", "sparkle": "2747-fe0f", "ng_woman": "1f645", "high_heel": "1f460", "hamster": "1f439", "last_quarter_moon": "1f317", "stopwatch": "23f1", "date": "1f4c5", "nail_care_dark_skin_tone": "1f485-1f3ff", "santa_dark_skin_tone": "1f385-1f3ff", "astonished": "1f632", "mushroom": "1f344", "radio": "1f4fb", "hammer_and_wrench": "1f6e0", "arrow_down": "2b07-fe0f", "speech_balloon": "1f4ac", "couple_with_heart_man_man_medium_skin_tone": "1f468-1f3fd", "euro": "1f4b6", "es": "1f1ea-1f1f8", "woman_factory_worker_medium_light_skin_tone": "1f469-1f3fc", "pouting_woman_dark_skin_tone": "1f64e-1f3ff-200d-2640-fe0f", "massage_woman": "1f486", "spades": "2660-fe0f", "blonde_woman_dark_skin_tone": "1f471-1f3ff-200d-2640-fe0f", "man_farmer_medium_skin_tone": "1f468-1f3fd", "man_mechanic_medium_skin_tone": "1f468-1f3fd", "family_man_boy_dark_skin_tone": "1f468-1f3ff", "man_juggling_medium_light_skin_tone": "1f939-1f3fc-200d-2642-fe0f", "hearts": "2665-fe0f", "clock930": "1f564", "central_african_republic": "1f1e8-1f1eb", "boy_medium_skin_tone": "1f466-1f3fd", "pregnant_woman_medium_skin_tone": "1f930-1f3fd", "woman_facepalming_medium_light_skin_tone": "1f926-1f3fc-200d-2640-fe0f", "palm_tree": "1f334", "rose": "1f339", "beers": "1f37b", "red_car": "1f697", "no_entry": "26d4-fe0f", "candy": "1f36c", "fist_oncoming_medium_skin_tone": "1f44a-1f3fd", "rowing_woman_medium_skin_tone": "1f6a3-1f3fd-200d-2640-fe0f", "sake": "1f376", "oncoming_police_car": "1f694", "woman_teacher_medium_dark_skin_tone": "1f469-1f3fe", "family_man_woman_girl_girl_medium_skin_tone": "1f468-1f3fd", "kissing_closed_eyes": "1f61a", "pager": "1f4df", "pencil": "1f4dd", "copyright": "00a9-fe0f", "wave_medium_skin_tone": "1f44b-1f3fd", "loud_sound": "1f50a", "luxembourg": "1f1f1-1f1fa", "policewoman_dark_skin_tone": "1f46e-1f3ff-200d-2640-fe0f", "woman_cartwheeling_medium_skin_tone": "1f938-1f3fd-200d-2640-fe0f", "swimming_woman_medium_dark_skin_tone": "1f3ca-1f3fe-200d-2640-fe0f", "family_man_man_girl_boy": "1f468-200d-1f468-200d-1f467-200d-1f466", "police_car": "1f693", "mailbox_with_no_mail": "1f4ed", "middle_finger_light_skin_tone": "1f595-1f3fb", "pregnant_woman_medium_light_skin_tone": "1f930-1f3fc", "raising_hand_woman_medium_skin_tone": "1f64b-1f3fd-200d-2640-fe0f", "running": "1f3c3", "sun_with_face": "1f31e", "man_teacher_dark_skin_tone": "1f468-1f3ff", "family_man_woman_girl_girl_dark_skin_tone": "1f468-1f3ff", "izakaya_lantern": "1f3ee", "comoros": "1f1f0-1f1f2", "fist_oncoming_medium_dark_skin_tone": "1f44a-1f3fe", "man_singer": "1f468-200d-1f3a4", "mountain_bicyclist": "1f6b5", "point_down_light_skin_tone": "1f447-1f3fb", "family_man_woman_girl_boy_medium_dark_skin_tone": "1f468-1f3fe", "sob": "1f62d", "ophiuchus": "26ce", "greece": "1f1ec-1f1f7", "raised_back_of_hand_medium_skin_tone": "1f91a-1f3fd", "family_man_man_boy_light_skin_tone": "1f468-1f3fb", "woman_cartwheeling_light_skin_tone": "1f938-1f3fb-200d-2640-fe0f", "massage_woman_light_skin_tone": "1f486-1f3fb-200d-2640-fe0f", "fishing_pole_and_fish": "1f3a3", "two_hearts": "1f495", "armenia": "1f1e6-1f1f2", "south_africa": "1f1ff-1f1e6", "boy_light_skin_tone": "1f466-1f3fb", "man_in_tuxedo_medium_dark_skin_tone": "1f935-1f3fe", "kiribati": "1f1f0-1f1ee", "v_dark_skin_tone": "270c-1f3ff", "frowning_man_medium_light_skin_tone": "1f64d-1f3fc-200d-2642-fe0f", "family_woman_woman_girl_boy": "1f469-200d-1f469-200d-1f467-200d-1f466", "family_woman_girl_boy_medium_dark_skin_tone": "1f469-1f3fe", "leopard": "1f406", "fireworks": "1f386", "clock6": "1f555", "bowing_man_medium_light_skin_tone": "1f647-1f3fc-200d-2640-fe0f", "raising_hand": "1f64b", "family_man_woman_girl_girl_light_skin_tone": "1f468-1f3fb", "vulcan_salute_medium_light_skin_tone": "1f596-1f3fc", "guardswoman_medium_light_skin_tone": "1f482-1f3fc-200d-2640-fe0f", "muscle": "1f4aa", "full_moon": "1f315", "pisces": "2653-fe0f", "kosovo": "1f1fd-1f1f0", "fist_left_dark_skin_tone": "1f91b-1f3ff", "point_up_2_dark_skin_tone": "1f446-1f3ff", "man_technologist_dark_skin_tone": "1f468-1f3ff", "spoon": "1f944", "nigeria": "1f1f3-1f1ec", "raised_back_of_hand_medium_light_skin_tone": "1f91a-1f3fc", "blonde_woman_light_skin_tone": "1f471-1f3fb-200d-2640-fe0f", "man_dancing_light_skin_tone": "1f57a-1f3fb", "shrimp": "1f990", "mountain_biking_man": "1f6b5", "boat": "26f5-fe0f", "egypt": "1f1ea-1f1ec", "family_woman_woman_boy_light_skin_tone": "1f469-1f3fb", "man_playing_water_polo_light_skin_tone": "1f93d-1f3fb-200d-2642-fe0f", "family_man_man_boy_boy": "1f468-200d-1f468-200d-1f466-200d-1f466", "foggy": "1f301", "construction_worker_woman_medium_light_skin_tone": "1f477-1f3fc-200d-2640-fe0f", "princess_medium_skin_tone": "1f478-1f3fd", "man_dancing_medium_dark_skin_tone": "1f57a-1f3fe", "couple_with_heart_man_man_dark_skin_tone": "1f468-1f3ff", "carousel_horse": "1f3a0", "crayon": "1f58d", "niue": "1f1f3-1f1fa", "woman_office_worker_medium_skin_tone": "1f469-1f3fd", "swimming_man_medium_skin_tone": "1f3ca-1f3fd-200d-2640-fe0f", "pensive": "1f614", "fire": "1f525", "monorail": "1f69d", "guam": "1f1ec-1f1fa", "older_woman_light_skin_tone": "1f475-1f3fb", "man_facepalming_medium_light_skin_tone": "1f926-1f3fc-200d-2642-fe0f", "family_man_man_girl": "1f468-200d-1f468-200d-1f467", "hammer_and_pick": "2692", "space_invader": "1f47e", "waning_crescent_moon": "1f318", "love_letter": "1f48c", "star_and_crescent": "262a-fe0f", "man_with_turban_light_skin_tone": "1f473-1f3fb-200d-2640-fe0f", "tipping_hand_woman_light_skin_tone": "1f481-1f3fb-200d-2640-fe0f", "dress": "1f457", "rainbow": "1f308", "cheese": "1f9c0", "bento": "1f371", "gear": "2699-fe0f", "-1_medium_skin_tone": "1f44e-1f3fd", "family_man_girl_boy_dark_skin_tone": "1f468-1f3ff", "fish_cake": "1f365", "desert_island": "1f3dd", "crystal_ball": "1f52e", "lock": "1f512", "no_good_man_medium_skin_tone": "1f645-1f3fd-200d-2642-fe0f", "small_blue_diamond": "1f539", "fist_raised_medium_dark_skin_tone": "270a-1f3fe", "man_health_worker_medium_light_skin_tone": "1f468-1f3fc", "ok_man_medium_light_skin_tone": "1f646-1f3fc-200d-2642-fe0f", "man_cartwheeling_dark_skin_tone": "1f938-1f3ff-200d-2642-fe0f", "policeman": "1f46e", "closed_lock_with_key": "1f510", "koko": "1f201", "guardswoman": "1f482-200d-2640-fe0f", "mailbox": "1f4eb", "weight_lifting_woman_light_skin_tone": "1f3cb-1f3fb-200d-2640-fe0f", "drooling_face": "1f924", "motorway": "1f6e3", "orthodox_cross": "2626-fe0f", "peru": "1f1f5-1f1ea", "woman_firefighter_medium_light_skin_tone": "1f469-1f3fc", "atom_symbol": "269b-fe0f", "benin": "1f1e7-1f1ef", "montenegro": "1f1f2-1f1ea", "tonga": "1f1f9-1f1f4", "family_man_boy_boy_medium_skin_tone": "1f468-1f3fd", "man_mechanic_light_skin_tone": "1f468-1f3fb", "female_detective": "1f575-fe0f-200d-2640-fe0f", "closed_umbrella": "1f302", "cow2": "1f404", "ballot_box": "1f5f3", "construction_worker_man_dark_skin_tone": "1f477-1f3ff-200d-2640-fe0f", "woman_technologist_medium_dark_skin_tone": "1f469-1f3fe", "indonesia": "1f1ee-1f1e9", "woman_pilot_medium_light_skin_tone": "1f469-1f3fc", "family_man_man_boy_boy_medium_light_skin_tone": "1f468-1f3fc", "call_me_hand": "1f919", "sun_behind_small_cloud": "1f324", "national_park": "1f3de", "radio_button": "1f518", "selfie_medium_light_skin_tone": "1f933-1f3fc", "woman_firefighter": "1f469-200d-1f692", "metal_dark_skin_tone": "1f918-1f3ff", "older_woman": "1f475", "man_factory_worker_medium_skin_tone": "1f468-1f3fd", "pick": "26cf", "woman_student_medium_skin_tone": "1f469-1f3fd", "mountain_biking_woman_light_skin_tone": "1f6b5-1f3fb-200d-2640-fe0f", "flags": "1f38f", "black_nib": "2712-fe0f", "rwanda": "1f1f7-1f1fc", "surfing_man_light_skin_tone": "1f3c4-1f3fb-200d-2640-fe0f", "first_quarter_moon": "1f313", "oil_drum": "1f6e2", "heart_decoration": "1f49f", "jp": "1f1ef-1f1f5", "woman_pilot": "1f469-200d-2708-fe0f", "city_sunrise": "1f307", "leo": "264c-fe0f", "arrow_up_down": "2195-fe0f", "selfie_medium_skin_tone": "1f933-1f3fd", "surfing_man_medium_skin_tone": "1f3c4-1f3fd-200d-2640-fe0f", "ramen": "1f35c", "up": "1f199", "woman_medium_light_skin_tone": "1f469-1f3fc", "woman_artist": "1f469-200d-1f3a8", "football": "1f3c8", "shopping": "1f6cd", "small_red_triangle_down": "1f53b", "crossed_fingers_light_skin_tone": "1f91e-1f3fb", "woman_artist_medium_dark_skin_tone": "1f469-1f3fe", "milk_glass": "1f95b", "clapper": "1f3ac", "star_of_david": "2721-fe0f", "dominican_republic": "1f1e9-1f1f4", "woman_teacher_light_skin_tone": "1f469-1f3fb", "man_juggling_medium_skin_tone": "1f939-1f3fd-200d-2642-fe0f", "-1": "1f44e", "wedding": "1f492", "faroe_islands": "1f1eb-1f1f4", "raising_hand_man_medium_dark_skin_tone": "1f64b-1f3fe-200d-2642-fe0f", "gemini": "264a-fe0f", "st_helena": "1f1f8-1f1ed", "running_woman_medium_light_skin_tone": "1f3c3-1f3fc-200d-2640-fe0f", "biking_woman_light_skin_tone": "1f6b4-1f3fb-200d-2640-fe0f", "paperclip": "1f4ce", "wave_medium_light_skin_tone": "1f44b-1f3fc", "man_factory_worker_medium_dark_skin_tone": "1f468-1f3fe", "woman_cartwheeling_medium_dark_skin_tone": "1f938-1f3fe-200d-2640-fe0f", "clock12": "1f55b", "ru": "1f1f7-1f1fa", "clown_face": "1f921", "pizza": "1f355", "hole": "1f573", "incoming_envelope": "1f4e8", "yin_yang": "262f-fe0f", "warning": "26a0-fe0f", "family_man_man_girl_boy_dark_skin_tone": "1f468-1f3ff", "man_cartwheeling_medium_skin_tone": "1f938-1f3fd-200d-2642-fe0f", "ram": "1f40f", "cucumber": "1f952", "heartbeat": "1f493", "swaziland": "1f1f8-1f1ff", "nail_care_medium_dark_skin_tone": "1f485-1f3fe", "bath_medium_skin_tone": "1f6c0-1f3fd", "strawberry": "1f353", "peanuts": "1f95c", "field_hockey": "1f3d1", "cricket": "1f3cf", "woman_farmer_medium_dark_skin_tone": "1f469-1f3fe", "family_man_man_girl_girl_light_skin_tone": "1f468-1f3fb", "penguin": "1f427", "star": "2b50-fe0f", "woman_shrugging_light_skin_tone": "1f937-1f3fb-200d-2640-fe0f", "golfing_man_light_skin_tone": "1f3cc-1f3fb-200d-2640-fe0f", "innocent": "1f607", "mosque": "1f54c", "calendar": "1f4c6", "canada": "1f1e8-1f1e6", "rage4": "rage4", "woman_office_worker_medium_dark_skin_tone": "1f469-1f3fe", "poodle": "1f429", "grapes": "1f347", "love_hotel": "1f3e9", "vulcan_salute_medium_skin_tone": "1f596-1f3fd", "guardsman_medium_dark_skin_tone": "1f482-1f3fe-200d-2640-fe0f", "raising_hand_man_light_skin_tone": "1f64b-1f3fb-200d-2642-fe0f", "sleeping": "1f634", "nail_care": "1f485", "monkey": "1f412", "sao_tome_principe": "1f1f8-1f1f9", "dancer_medium_dark_skin_tone": "1f483-1f3fe", "classical_building": "1f3db", "swimming_woman_medium_skin_tone": "1f3ca-1f3fd-200d-2640-fe0f", "ok_hand": "1f44c", "rice_cracker": "1f358", "moyai": "1f5ff", "rage2": "rage2", "angel_light_skin_tone": "1f47c-1f3fb", "family_man_man_boy_boy_dark_skin_tone": "1f468-1f3ff", "smile_cat": "1f638", "angola": "1f1e6-1f1f4", "cameroon": "1f1e8-1f1f2", "man_student_medium_dark_skin_tone": "1f468-1f3fe", "weight_lifting_woman_medium_light_skin_tone": "1f3cb-1f3fc-200d-2640-fe0f", "waxing_crescent_moon": "1f312", "articulated_lorry": "1f69b", "pouting_woman_light_skin_tone": "1f64e-1f3fb-200d-2640-fe0f", "running_man_medium_dark_skin_tone": "1f3c3-1f3fe-200d-2640-fe0f", "couple_with_heart_woman_woman_light_skin_tone": "1f469-1f3fb", "horse_racing_light_skin_tone": "1f3c7-1f3fb", "raised_back_of_hand": "1f91a", "saxophone": "1f3b7", "right_anger_bubble": "1f5ef", "tokelau": "1f1f9-1f1f0", "no_good_woman_medium_light_skin_tone": "1f645-1f3fc-200d-2640-fe0f", "walking_woman_medium_skin_tone": "1f6b6-1f3fd-200d-2640-fe0f", "family_woman_girl_girl": "1f469-200d-1f467-200d-1f467", "cake": "1f370", "abcd": "1f521", "tuvalu": "1f1f9-1f1fb", "suspect": "suspect", "mattermost": "mattermost", "swimming_woman_light_skin_tone": "1f3ca-1f3fb-200d-2640-fe0f", "white_medium_square": "25fb-fe0f", "haircut_woman_medium_skin_tone": "1f487-1f3fd-200d-2640-fe0f", "massage_woman_dark_skin_tone": "1f486-1f3ff-200d-2640-fe0f", "family_man_woman_girl_light_skin_tone": "1f468-1f3fb", "turks_caicos_islands": "1f1f9-1f1e8", "point_left_dark_skin_tone": "1f448-1f3ff", "family_man_man_boy_medium_dark_skin_tone": "1f468-1f3fe", "hand": "270b", "coffee": "2615-fe0f", "somalia": "1f1f8-1f1f4", "mountain_biking_man_dark_skin_tone": "1f6b5-1f3ff-200d-2640-fe0f", "hatching_chick": "1f423", "pear": "1f350", "baby_bottle": "1f37c", "ribbon": "1f380", "st_kitts_nevis": "1f1f0-1f1f3", "radioactive": "2622-fe0f", "end": "1f51a", "hand_medium_skin_tone": "270b-1f3fd", "family_woman_woman_girl_medium_light_skin_tone": "1f469-1f3fc", "3rd_place_medal": "1f949", "fist_left_medium_dark_skin_tone": "1f91b-1f3fe", "bolivia": "1f1e7-1f1f4", "point_up_light_skin_tone": "261d-1f3fb", "cherries": "1f352", "inbox_tray": "1f4e5", "pitcairn_islands": "1f1f5-1f1f3", "rage1": "rage1", "man_farmer_medium_dark_skin_tone": "1f468-1f3fe", "woman_with_turban": "1f473-200d-2640-fe0f", "unicorn": "1f984", "butterfly": "1f98b", "watch": "231a-fe0f", "arrow_up_small": "1f53c", "triangular_flag_on_post": "1f6a9", "heart_eyes": "1f60d", "shallow_pan_of_food": "1f958", "broken_heart": "1f494", "family_man_boy_boy_dark_skin_tone": "1f468-1f3ff", "golfing_woman_dark_skin_tone": "1f3cc-1f3ff-200d-2640-fe0f", "bath_dark_skin_tone": "1f6c0-1f3ff", "selfie": "1f933", "congratulations": "3297-fe0f", "baby_medium_light_skin_tone": "1f476-1f3fc", "woman_health_worker_medium_skin_tone": "1f469-1f3fd", "man_juggling": "1f939-200d-2642-fe0f", "arrow_down_small": "1f53d", "writing_hand_medium_light_skin_tone": "270d-1f3fc", "blonde_woman": "1f471-200d-2640-fe0f", "massage": "1f486", "metro": "1f687", "bath": "1f6c0", "female_detective_light_skin_tone": "1f575-1f3fb-200d-2640-fe0f", "haircut_man_light_skin_tone": "1f487-1f3fb-200d-2642-fe0f", "bowing_woman_medium_dark_skin_tone": "1f647-1f3fe-200d-2640-fe0f", "family_woman_woman_boy_medium_dark_skin_tone": "1f469-1f3fe", "shell": "1f41a", "seychelles": "1f1f8-1f1e8", "tipping_hand_man_medium_skin_tone": "1f481-1f3fd-200d-2642-fe0f", "panda_face": "1f43c", "sint_maarten": "1f1f8-1f1fd", "face_with_head_bandage": "1f915", "checkered_flag": "1f3c1", "samoa": "1f1fc-1f1f8", "v_medium_skin_tone": "270c-1f3fd", "couple_with_heart_man_man": "1f468-200d-2764-fe0f-200d-1f468", "shaved_ice": "1f367", "badminton": "1f3f8", "clock530": "1f560", "man_playing_water_polo_medium_dark_skin_tone": "1f93d-1f3fe-200d-2642-fe0f", "bulgaria": "1f1e7-1f1ec", "hurtrealbad": "hurtrealbad", "fist_oncoming_dark_skin_tone": "1f44a-1f3ff", "bat": "1f987", "signal_strength": "1f4f6", "iran": "1f1ee-1f1f7", "construction_worker_woman_medium_dark_skin_tone": "1f477-1f3fe-200d-2640-fe0f", "kiwi_fruit": "1f95d", "2nd_place_medal": "1f948", "kaaba": "1f54b", "knife": "1f52a", "ok_hand_light_skin_tone": "1f44c-1f3fb", "angel_medium_dark_skin_tone": "1f47c-1f3fe", "spider_web": "1f578", "oncoming_taxi": "1f696", "bookmark": "1f516", "u6307": "1f22f-fe0f", "za": "1f1ff-1f1e6", "fist_raised_light_skin_tone": "270a-1f3fb", "mag_right": "1f50e", "guinea": "1f1ec-1f1f3", "family_woman_woman_girl_dark_skin_tone": "1f469-1f3ff", "man_playing_handball_medium_light_skin_tone": "1f93e-1f3fc-200d-2642-fe0f", "game_die": "1f3b2", "bullettrain_front": "1f685", "speedboat": "1f6a4", "hand_dark_skin_tone": "270b-1f3ff", "selfie_light_skin_tone": "1f933-1f3fb", "family_man_woman_boy_boy_dark_skin_tone": "1f468-1f3ff", "running_man": "1f3c3", "couplekiss_woman_woman": "1f469-200d-2764-fe0f-200d-1f48b-200d-1f469", "woman_teacher": "1f469-200d-1f3eb", "running_shirt_with_sash": "1f3bd", "bowing_man_medium_skin_tone": "1f647-1f3fd-200d-2640-fe0f", "point_right_medium_dark_skin_tone": "1f449-1f3fe", "man_cartwheeling_medium_light_skin_tone": "1f938-1f3fc-200d-2642-fe0f", "pouting_man_light_skin_tone": "1f64e-1f3fb-200d-2642-fe0f", "biking_man_light_skin_tone": "1f6b4-1f3fb-200d-2640-fe0f", "oncoming_automobile": "1f698", "steam_locomotive": "1f682", "newspaper": "1f4f0", "antigua_barbuda": "1f1e6-1f1ec", "macau": "1f1f2-1f1f4", "niger": "1f1f3-1f1ea", "chicken": "1f414", "flashlight": "1f526", "family_man_woman_boy_boy_medium_skin_tone": "1f468-1f3fd", "mens": "1f6b9", "it": "1f1ee-1f1f9", "new_caledonia": "1f1f3-1f1e8", "pray_medium_light_skin_tone": "1f64f-1f3fc", "nose_medium_dark_skin_tone": "1f443-1f3fe", "man_facepalming_medium_dark_skin_tone": "1f926-1f3fe-200d-2642-fe0f", "poop": "1f4a9", "clap_light_skin_tone": "1f44f-1f3fb", "guardsman_medium_skin_tone": "1f482-1f3fd-200d-2640-fe0f", "woman_teacher_dark_skin_tone": "1f469-1f3ff", "grinning": "1f600", "aries": "2648-fe0f", "mrs_claus": "1f936", "green_book": "1f4d7", "middle_finger_medium_dark_skin_tone": "1f595-1f3fe", "rowing_woman_light_skin_tone": "1f6a3-1f3fb-200d-2640-fe0f", "tokyo_tower": "1f5fc", "printer": "1f5a8", "put_litter_in_its_place": "1f6ae", "suriname": "1f1f8-1f1f7", "woman_light_skin_tone": "1f469-1f3fb", "man_playing_water_polo_medium_light_skin_tone": "1f93d-1f3fc-200d-2642-fe0f", "dove": "1f54a", "latin_cross": "271d-fe0f", "exclamation": "2757-fe0f", "man_health_worker_dark_skin_tone": "1f468-1f3ff", "bride_with_veil_light_skin_tone": "1f470-1f3fb", "rowboat": "1f6a3", "world_map": "1f5fa", "sleeping_bed": "1f6cc", "haircut_man_dark_skin_tone": "1f487-1f3ff-200d-2642-fe0f", "surfing_man_dark_skin_tone": "1f3c4-1f3ff-200d-2640-fe0f", "couple_with_heart_woman_man": "1f491", "chart_with_upwards_trend": "1f4c8", "fist_right_dark_skin_tone": "1f91c-1f3ff", "raised_hand_with_fingers_splayed_medium_skin_tone": "1f590-1f3fd", "older_woman_medium_dark_skin_tone": "1f475-1f3fe", "couplekiss_woman_woman_medium_dark_skin_tone": "1f469-1f3fe", "hatched_chick": "1f425", "running_man_medium_skin_tone": "1f3c3-1f3fd-200d-2640-fe0f", "chocolate_bar": "1f36b", "grenada": "1f1ec-1f1e9", "man_farmer_dark_skin_tone": "1f468-1f3ff", "milky_way": "1f30c", "slovakia": "1f1f8-1f1f0", "selfie_dark_skin_tone": "1f933-1f3ff", "prince_medium_skin_tone": "1f934-1f3fd", "family_man_boy": "1f468-200d-1f466", "chains": "26d3", "british_virgin_islands": "1f1fb-1f1ec", "bow_and_arrow": "1f3f9", "ferry": "26f4", "o2": "1f17e-fe0f", "st_barthelemy": "1f1e7-1f1f1", "policewoman_medium_skin_tone": "1f46e-1f3fd-200d-2640-fe0f", "imp": "1f47f", "bathtub": "1f6c1", "anger": "1f4a2", "previous_track_button": "23ee", "pouting_woman_medium_light_skin_tone": "1f64e-1f3fc-200d-2640-fe0f", "rabbit2": "1f407", "newspaper_roll": "1f5de", "one": "0031-fe0f-20e3", "family_man_man_girl_medium_skin_tone": "1f468-1f3fd", "pouting_woman": "1f64e", "moneybag": "1f4b0", "guardsman_dark_skin_tone": "1f482-1f3ff-200d-2640-fe0f", "man_shrugging_medium_light_skin_tone": "1f937-1f3fc-200d-2642-fe0f", "smirk": "1f60f", "woman_farmer": "1f469-200d-1f33e", "ocean": "1f30a", "sweat_drops": "1f4a6", "x": "274c", "woman_judge_medium_dark_skin_tone": "1f469-1f3fe", "tropical_drink": "1f379", "brunei": "1f1e7-1f1f3", "woman_artist_medium_light_skin_tone": "1f469-1f3fc", "pregnant_woman_medium_dark_skin_tone": "1f930-1f3fe", "basketball_woman_light_skin_tone": "26f9-1f3fb-200d-2640-fe0f", "evergreen_tree": "1f332", "fax": "1f4e0", "woman_medium_dark_skin_tone": "1f469-1f3fe", "walking_woman_medium_dark_skin_tone": "1f6b6-1f3fe-200d-2640-fe0f", "lollipop": "1f36d", "bicyclist": "1f6b4", "bulb": "1f4a1", "computer": "1f4bb", "frowning_man_dark_skin_tone": "1f64d-1f3ff-200d-2642-fe0f", "guardsman_medium_light_skin_tone": "1f482-1f3fc-200d-2640-fe0f", "dancer_light_skin_tone": "1f483-1f3fb", "no_good_woman": "1f645", "cherry_blossom": "1f338", "woman_playing_water_polo": "1f93d-200d-2640-fe0f", "heavy_division_sign": "2797", "sri_lanka": "1f1f1-1f1f0", "-1_medium_light_skin_tone": "1f44e-1f3fc", "family_woman_girl_girl_dark_skin_tone": "1f469-1f3ff", "raised_hands": "1f64c", "sandal": "1f461", "rhinoceros": "1f98f", "swimming_man": "1f3ca", "scissors": "2702-fe0f", "horse_racing_dark_skin_tone": "1f3c7-1f3ff", "coffin": "26b0-fe0f", "clock1": "1f550", "eritrea": "1f1ea-1f1f7", "qatar": "1f1f6-1f1e6", "tanzania": "1f1f9-1f1ff", "pregnant_woman_light_skin_tone": "1f930-1f3fb", "cop": "1f46e", "tipping_hand_woman": "1f481", "estonia": "1f1ea-1f1ea", "man_singer_light_skin_tone": "1f468-1f3fb", "woman_judge_dark_skin_tone": "1f469-1f3ff", "business_suit_levitating_medium_skin_tone": "1f574-1f3fd", "blowfish": "1f421", "mountain_railway": "1f69e", "fast_forward": "23e9", "+1_medium_skin_tone": "1f44d-1f3fd", "goat": "1f410", "congo_kinshasa": "1f1e8-1f1e9", "point_down_dark_skin_tone": "1f447-1f3ff", "basketball_man_light_skin_tone": "26f9-1f3fb-200d-2640-fe0f", "woman_playing_handball_medium_dark_skin_tone": "1f93e-1f3fe-200d-2640-fe0f", "doughnut": "1f369", "musical_keyboard": "1f3b9", "couplekiss_woman_woman_medium_skin_tone": "1f469-1f3fd", "heavy_heart_exclamation": "2763-fe0f", "u6e80": "1f235", "woman_with_turban_medium_skin_tone": "1f473-1f3fd-200d-2640-fe0f", "horse_racing_medium_skin_tone": "1f3c7-1f3fd", "ear": "1f442", "canoe": "1f6f6", "andorra": "1f1e6-1f1e9", "ca": "1f1e8-1f1e6", "family_man_man_boy": "1f468-200d-1f468-200d-1f466", "ticket": "1f3ab", "station": "1f689", "large_blue_circle": "1f535", "palau": "1f1f5-1f1fc", "blush": "1f60a", "man_student": "1f468-200d-1f393", "woman_singer": "1f469-200d-1f3a4", "house_with_garden": "1f3e1", "smoking": "1f6ac", "b": "1f171-fe0f", "golfing_woman_light_skin_tone": "1f3cc-1f3fb-200d-2640-fe0f", "man_artist_light_skin_tone": "1f468-1f3fb", "unamused": "1f612", "japanese_ogre": "1f479", "film_projector": "1f4fd", "ballot_box_with_check": "2611-fe0f", "goberserk": "goberserk", "metal_medium_light_skin_tone": "1f918-1f3fc", "latvia": "1f1f1-1f1fb", "moldova": "1f1f2-1f1e9", "mask": "1f637", "bowing_man": "1f647", "man_shrugging": "1f937-200d-2642-fe0f", "ping_pong": "1f3d3", "trackball": "1f5b2", "six": "0036-fe0f-20e3", "point_up_medium_skin_tone": "261d-1f3fd", "hotel": "1f3e8", "bookmark_tabs": "1f4d1", "chart_with_downwards_trend": "1f4c9", "v_light_skin_tone": "270c-1f3fb", "tipping_hand_woman_medium_skin_tone": "1f481-1f3fd-200d-2640-fe0f", "heart_eyes_cat": "1f63b", "dancer": "1f483", "movie_camera": "1f3a5", "two": "0032-fe0f-20e3", "clap_medium_dark_skin_tone": "1f44f-1f3fe", "woman_astronaut_medium_skin_tone": "1f469-1f3fd", "frowning": "1f626", "cry": "1f622", "no_bell": "1f515", "hand_medium_dark_skin_tone": "270b-1f3fe", "ear_light_skin_tone": "1f442-1f3fb", "family_man_girl_boy": "1f468-200d-1f467-200d-1f466", "swimming_woman": "1f3ca-200d-2640-fe0f", "mountain_biking_woman": "1f6b5-200d-2640-fe0f", "mantelpiece_clock": "1f570", "bermuda": "1f1e7-1f1f2", "new_zealand": "1f1f3-1f1ff", "massage_man_medium_dark_skin_tone": "1f486-1f3fe-200d-2642-fe0f", "crown": "1f451", "biking_man": "1f6b4", "woman_playing_water_polo_medium_skin_tone": "1f93d-1f3fd-200d-2640-fe0f", "man_in_tuxedo_dark_skin_tone": "1f935-1f3ff", "no_entry_sign": "1f6ab", "hash": "0023-fe0f-20e3", "white_small_square": "25ab-fe0f", "iraq": "1f1ee-1f1f6", "switzerland": "1f1e8-1f1ed", "woman_mechanic_light_skin_tone": "1f469-1f3fb", "squirrel": "shipit", "woman_cook_medium_light_skin_tone": "1f469-1f3fc", "confounded": "1f616", "+1": "1f44d", "rowing_man": "1f6a3", "mailbox_closed": "1f4ea", "customs": "1f6c3", "mayotte": "1f1fe-1f1f9", "man_mechanic_medium_dark_skin_tone": "1f468-1f3fe", "man_artist_medium_skin_tone": "1f468-1f3fd", "man_playing_handball_medium_skin_tone": "1f93e-1f3fd-200d-2642-fe0f", "grimacing": "1f62c", "dart": "1f3af", "wave_medium_dark_skin_tone": "1f44b-1f3fe", "slightly_smiling_face": "1f642", "medal_sports": "1f3c5", "bank": "1f3e6", "man_student_medium_light_skin_tone": "1f468-1f3fc", "man_pilot_light_skin_tone": "1f468-1f3fb", "weight_lifting_woman_medium_skin_tone": "1f3cb-1f3fd-200d-2640-fe0f", "dash": "1f4a8", "volcano": "1f30b", "antarctica": "1f1e6-1f1f6", "woman_facepalming_light_skin_tone": "1f926-1f3fb-200d-2640-fe0f", "man_dancing_medium_light_skin_tone": "1f57a-1f3fc", "scream_cat": "1f640", "fog": "1f32b", "fist_oncoming_light_skin_tone": "1f44a-1f3fb", "man_dancing_medium_skin_tone": "1f57a-1f3fd", "burrito": "1f32f", "thought_balloon": "1f4ad", "massage_man_medium_light_skin_tone": "1f486-1f3fc-200d-2642-fe0f", "couple_with_heart_woman_woman_dark_skin_tone": "1f469-1f3ff", "writing_hand": "270d-fe0f", "zap": "26a1-fe0f", "recycle": "267b-fe0f", "policewoman_medium_light_skin_tone": "1f46e-1f3fc-200d-2640-fe0f", "frowning_woman_medium_skin_tone": "1f64d-1f3fd-200d-2640-fe0f", "massage_man_light_skin_tone": "1f486-1f3fb-200d-2642-fe0f", "woman_student": "1f469-200d-1f393", "surfing_woman": "1f3c4-200d-2640-fe0f", "sunrise": "1f305", "open_file_folder": "1f4c2", "diamonds": "2666-fe0f", "family_man_woman_girl_girl": "1f468-200d-1f469-200d-1f467-200d-1f467", "airplane": "2708-fe0f", "arrow_heading_down": "2935-fe0f", "uruguay": "1f1fa-1f1fe", "point_down_medium_dark_skin_tone": "1f447-1f3fe", "family_man_man_boy_dark_skin_tone": "1f468-1f3ff", "family_man_woman_girl_boy": "1f468-200d-1f469-200d-1f467-200d-1f466", "confetti_ball": "1f38a", "flower_playing_cards": "1f3b4", "algeria": "1f1e9-1f1ff", "man_teacher_medium_light_skin_tone": "1f468-1f3fc", "woman_artist_light_skin_tone": "1f469-1f3fb", "family_man_woman_girl_medium_skin_tone": "1f468-1f3fd", "nerd_face": "1f913", "eyes": "1f440", "boot": "1f462", "unlock": "1f513", "zzz": "1f4a4", "vatican_city": "1f1fb-1f1e6", "hot_pepper": "1f336", "slot_machine": "1f3b0", "sunrise_over_mountains": "1f304", "haircut_man_medium_skin_tone": "1f487-1f3fd-200d-2642-fe0f", "stuck_out_tongue": "1f61b", "point_up_medium_dark_skin_tone": "261d-1f3fe", "vulcan_salute_medium_dark_skin_tone": "1f596-1f3fe", "family": "1f46a", "key": "1f511", "myanmar": "1f1f2-1f1f2", "policeman_medium_light_skin_tone": "1f46e-1f3fc-200d-2640-fe0f", "man_shrugging_medium_dark_skin_tone": "1f937-1f3fe-200d-2642-fe0f", "woman_health_worker": "1f469-200d-2695-fe0f", "woman_judge": "1f469-200d-2696-fe0f", "japan": "1f5fe", "dominica": "1f1e9-1f1f2", "dragon": "1f409", "open_book": "1f4d6", "raising_hand_man": "1f64b-200d-2642-fe0f", "bikini": "1f459", "loudspeaker": "1f4e2", "woman_astronaut_medium_light_skin_tone": "1f469-1f3fc", "envelope_with_arrow": "1f4e9", "thailand": "1f1f9-1f1ed", "point_up_medium_light_skin_tone": "261d-1f3fc", "baby_medium_dark_skin_tone": "1f476-1f3fe", "man_scientist_medium_skin_tone": "1f468-1f3fd", "bowing_woman_medium_light_skin_tone": "1f647-1f3fc-200d-2640-fe0f", "construction_worker": "1f477", "nut_and_bolt": "1f529", "sparkling_heart": "1f496", "couplekiss_woman_woman_dark_skin_tone": "1f469-1f3ff", "elephant": "1f418", "bar_chart": "1f4ca", "nose_dark_skin_tone": "1f443-1f3ff", "stop_button": "23f9", "family_man_woman_boy_boy_light_skin_tone": "1f468-1f3fb", "family_man_girl_medium_light_skin_tone": "1f468-1f3fc", "relieved": "1f60c", "man_in_tuxedo": "1f935", "kick_scooter": "1f6f4", "statue_of_liberty": "1f5fd", "information_desk_person": "1f481", "sa": "1f202-fe0f", "abc": "1f524", "robot": "1f916", "cat": "1f431", "accept": "1f251", "upside_down_face": "1f643", "cloud": "2601-fe0f", "frowning_man_light_skin_tone": "1f64d-1f3fb-200d-2642-fe0f", "walking_man_medium_skin_tone": "1f6b6-1f3fd-200d-2640-fe0f", "sparkles": "2728", "u5272": "1f239", "globe_with_meridians": "1f310", "frowning_woman_medium_dark_skin_tone": "1f64d-1f3fe-200d-2640-fe0f", "grey_exclamation": "2755", "tm": "2122-fe0f", "massage_man_dark_skin_tone": "1f486-1f3ff-200d-2642-fe0f", "family_woman_woman_girl_boy_dark_skin_tone": "1f469-1f3ff", "paintbrush": "1f58c", "arrow_right_hook": "21aa-fe0f", "mauritania": "1f1f2-1f1f7", "man_scientist_light_skin_tone": "1f468-1f3fb", "woman_juggling_light_skin_tone": "1f939-1f3fb-200d-2640-fe0f", "ok_woman": "1f646", "snail": "1f40c", "hocho": "1f52a", "arrow_forward": "25b6-fe0f", "french_southern_territories": "1f1f9-1f1eb", "iphone": "1f4f1", "princess_medium_light_skin_tone": "1f478-1f3fc", "maple_leaf": "1f341", "open_hands": "1f450", "racing_car": "1f3ce", "pill": "1f48a", "cuba": "1f1e8-1f1fa", "fist_raised_dark_skin_tone": "270a-1f3ff", "blonde_man_dark_skin_tone": "1f471-1f3ff-200d-2640-fe0f", "family_woman_girl_boy_dark_skin_tone": "1f469-1f3ff", "fox_face": "1f98a", "man_playing_handball": "1f93e-200d-2642-fe0f", "bullettrain_side": "1f684", "black_small_square": "25aa-fe0f", "kazakhstan": "1f1f0-1f1ff", "vanuatu": "1f1fb-1f1fa", "older_man_medium_skin_tone": "1f474-1f3fd", "man_teacher": "1f468-200d-1f3eb", "family_man_man_boy_boy_medium_dark_skin_tone": "1f468-1f3fe", "back": "1f519", "point_up_2_medium_dark_skin_tone": "1f446-1f3fe", "woman_teacher_medium_light_skin_tone": "1f469-1f3fc", "family_woman_boy_boy_medium_dark_skin_tone": "1f469-1f3fe", "surfing_woman_medium_light_skin_tone": "1f3c4-1f3fc-200d-2640-fe0f", "portugal": "1f1f5-1f1f9", "construction_worker_woman_dark_skin_tone": "1f477-1f3ff-200d-2640-fe0f", "family_man_man_boy_medium_skin_tone": "1f468-1f3fd", "family_man_girl_dark_skin_tone": "1f468-1f3ff", "woman_mechanic": "1f469-200d-1f527", "arrow_heading_up": "2934-fe0f", "clock330": "1f55e", "malawi": "1f1f2-1f1fc", "ok_hand_medium_dark_skin_tone": "1f44c-1f3fe", "prince_dark_skin_tone": "1f934-1f3ff", "ice_hockey": "1f3d2", "pk": "1f1f5-1f1f0", "san_marino": "1f1f8-1f1f2", "point_left_light_skin_tone": "1f448-1f3fb", "woman_office_worker_medium_light_skin_tone": "1f469-1f3fc", "swimming_man_medium_light_skin_tone": "1f3ca-1f3fc-200d-2640-fe0f", "stuffed_flatbread": "1f959", "aerial_tramway": "1f6a1", "family_man_man_girl_girl_dark_skin_tone": "1f468-1f3ff", "family_woman_girl_girl_medium_dark_skin_tone": "1f469-1f3fe", "closed_book": "1f4d5", "family_woman_girl_boy_medium_light_skin_tone": "1f469-1f3fc", "family_man_man_girl_boy_medium_skin_tone": "1f468-1f3fd", "v": "270c-fe0f", "play_or_pause_button": "23ef", "el_salvador": "1f1f8-1f1fb", "woman_judge_medium_light_skin_tone": "1f469-1f3fc", "santa_medium_light_skin_tone": "1f385-1f3fc", "couplekiss_man_man_light_skin_tone": "1f468-1f3fb", "blonde_man_light_skin_tone": "1f471-1f3fb-200d-2640-fe0f", "fist_right": "1f91c", "man_with_turban": "1f473", "cancer": "264b-fe0f", "tunisia": "1f1f9-1f1f3", "open_hands_medium_light_skin_tone": "1f450-1f3fc", "call_me_hand_medium_dark_skin_tone": "1f919-1f3fe", "tired_face": "1f62b", "tongue": "1f445", "shower": "1f6bf", "british_indian_ocean_territory": "1f1ee-1f1f4", "man_firefighter_medium_light_skin_tone": "1f468-1f3fc", "couple_with_heart_woman_woman_medium_dark_skin_tone": "1f469-1f3fe", "crescent_moon": "1f319", "ecuador": "1f1ea-1f1e8", "french_polynesia": "1f1f5-1f1eb", "man_light_skin_tone": "1f468-1f3fb", "mountain_biking_woman_medium_skin_tone": "1f6b5-1f3fd-200d-2640-fe0f", "pakistan": "1f1f5-1f1f0", "open_hands_medium_dark_skin_tone": "1f450-1f3fe", "telephone": "260e-fe0f", "envelope": "2709-fe0f", "revolving_hearts": "1f49e", "mega": "1f4e3", "montserrat": "1f1f2-1f1f8", "uganda": "1f1fa-1f1ec", "tropical_fish": "1f420", "hibiscus": "1f33a", "rainbow_flag": "1f3f3-fe0f-200d-1f308", "bangladesh": "1f1e7-1f1e9", "shipit": "shipit", "no_good_man_dark_skin_tone": "1f645-1f3ff-200d-2642-fe0f", "no_mouth": "1f636", "man_farmer": "1f468-200d-1f33e", "speak_no_evil": "1f64a", "level_slider": "1f39a", "guatemala": "1f1ec-1f1f9", "woman_factory_worker": "1f469-200d-1f3ed", "fork_and_knife": "1f374", "belarus": "1f1e7-1f1fe", "family_woman_woman_girl_boy_medium_dark_skin_tone": "1f469-1f3fe", "yum": "1f60b", "helicopter": "1f681", "busstop": "1f68f", "policewoman_light_skin_tone": "1f46e-1f3fb-200d-2640-fe0f", "man_technologist_medium_skin_tone": "1f468-1f3fd", "man_with_gua_pi_mao_light_skin_tone": "1f472-1f3fb", "man_astronaut_dark_skin_tone": "1f468-1f3ff", "skull": "1f480", "smirk_cat": "1f63c", "jeans": "1f456", "flipper": "1f42c", "dizzy": "1f4ab", "cocktail": "1f378", "basketball_woman_medium_skin_tone": "26f9-1f3fd-200d-2640-fe0f", "v_medium_light_skin_tone": "270c-1f3fc", "secret": "3299-fe0f", "seven": "0037-fe0f-20e3", "ghana": "1f1ec-1f1ed", "guernsey": "1f1ec-1f1ec", "kyrgyzstan": "1f1f0-1f1ec", "godmode": "godmode", "female_detective_dark_skin_tone": "1f575-1f3ff-200d-2640-fe0f", "fallen_leaf": "1f342", "snowflake": "2744-fe0f", "raised_hand_with_fingers_splayed_medium_dark_skin_tone": "1f590-1f3fe", "woman_health_worker_medium_dark_skin_tone": "1f469-1f3fe", "man_shrugging_dark_skin_tone": "1f937-1f3ff-200d-2642-fe0f", "pout": "1f621", "stars": "1f320", "family_woman_girl_boy": "1f469-200d-1f467-200d-1f466", "gun": "1f52b", "woman_scientist_dark_skin_tone": "1f469-1f3ff", "basketball_woman_dark_skin_tone": "26f9-1f3ff-200d-2640-fe0f", "biking_woman_medium_light_skin_tone": "1f6b4-1f3fc-200d-2640-fe0f", "family_man_girl_boy_medium_dark_skin_tone": "1f468-1f3fe", "oncoming_bus": "1f68d", "seat": "1f4ba", "vhs": "1f4fc", "lithuania": "1f1f1-1f1f9", "v_medium_dark_skin_tone": "270c-1f3fe", "man_with_gua_pi_mao_medium_skin_tone": "1f472-1f3fd", "frowning_face": "2639-fe0f", "shit": "1f4a9", "ab": "1f18e", "couple_with_heart_woman_woman_medium_skin_tone": "1f469-1f3fd", "family_woman_woman_girl_girl": "1f469-200d-1f469-200d-1f467-200d-1f467", "potato": "1f954", "minidisc": "1f4bd", "libya": "1f1f1-1f1fe", "point_right_dark_skin_tone": "1f449-1f3ff", "man_artist": "1f468-200d-1f3a8", "pineapple": "1f34d", "spaghetti": "1f35d", "couch_and_lamp": "1f6cb", "free": "1f193", "jamaica": "1f1ef-1f1f2", "woman_astronaut_dark_skin_tone": "1f469-1f3ff", "man_mechanic": "1f468-200d-1f527", "curry": "1f35b", "small_orange_diamond": "1f538", "pray": "1f64f", "hotdog": "1f32d", "currency_exchange": "1f4b1", "-1_dark_skin_tone": "1f44e-1f3ff", "man_office_worker_dark_skin_tone": "1f468-1f3ff", "clock830": "1f563", "policeman_medium_skin_tone": "1f46e-1f3fd-200d-2640-fe0f", "grin": "1f601", "water_buffalo": "1f403", "older_man_dark_skin_tone": "1f474-1f3ff", "business_suit_levitating_medium_dark_skin_tone": "1f574-1f3fe", "couple_with_heart_man_man_medium_light_skin_tone": "1f468-1f3fc", "rowing_man_medium_light_skin_tone": "1f6a3-1f3fc-200d-2640-fe0f", "purse": "1f45b", "slovenia": "1f1f8-1f1ee", "tipping_hand_man_medium_light_skin_tone": "1f481-1f3fc-200d-2642-fe0f", "madagascar": "1f1f2-1f1ec", "south_georgia_south_sandwich_islands": "1f1ec-1f1f8", "punch": "1f44a", "man_pilot": "1f468-200d-2708-fe0f", "owl": "1f989", "croissant": "1f950", "email": "2709-fe0f", "outbox_tray": "1f4e4", "construction_worker_man_medium_light_skin_tone": "1f477-1f3fc-200d-2640-fe0f", "mrs_claus_medium_dark_skin_tone": "1f936-1f3fe", "family_man_woman_girl_boy_dark_skin_tone": "1f468-1f3ff", "file_cabinet": "1f5c4", "hungary": "1f1ed-1f1fa", "pray_medium_dark_skin_tone": "1f64f-1f3fe", "woman_mechanic_dark_skin_tone": "1f469-1f3ff", "angel_medium_skin_tone": "1f47c-1f3fd", "man_dancing": "1f57a", "pound": "1f4b7", "macedonia": "1f1f2-1f1f0", "man_facepalming_medium_skin_tone": "1f926-1f3fd-200d-2642-fe0f", "scroll": "1f4dc", "rescue_worker_helmet": "26d1", "desktop_computer": "1f5a5", "heavy_plus_sign": "2795", "man_with_turban_medium_skin_tone": "1f473-1f3fd-200d-2640-fe0f", "horse_racing": "1f3c7", "low_brightness": "1f505", "loop": "27bf", "man_with_turban_medium_dark_skin_tone": "1f473-1f3fe-200d-2640-fe0f", "champagne": "1f37e", "construction_worker_woman_light_skin_tone": "1f477-1f3fb-200d-2640-fe0f", "man_teacher_light_skin_tone": "1f468-1f3fb", "family_woman_woman_girl_girl_medium_light_skin_tone": "1f469-1f3fc", "footprints": "1f463", "cloud_with_snow": "1f328", "man_cook_medium_light_skin_tone": "1f468-1f3fc", "woman_mechanic_medium_light_skin_tone": "1f469-1f3fc", "point_up_2": "1f446", "circus_tent": "1f3aa", "serbia": "1f1f7-1f1f8", "fist_right_medium_dark_skin_tone": "1f91c-1f3fe", "weight_lifting_woman_dark_skin_tone": "1f3cb-1f3ff-200d-2640-fe0f", "musical_score": "1f3bc", "violin": "1f3bb", "card_file_box": "1f5c3", "tipping_hand_woman_dark_skin_tone": "1f481-1f3ff-200d-2640-fe0f", "man_facepalming_dark_skin_tone": "1f926-1f3ff-200d-2642-fe0f", "open_mouth": "1f62e", "left_right_arrow": "2194-fe0f", "no_good_man_light_skin_tone": "1f645-1f3fb-200d-2642-fe0f", "man_factory_worker": "1f468-200d-1f3ed", "man_judge": "1f468-200d-2696-fe0f", "negative_squared_cross_mark": "274e", "bowing_woman_medium_skin_tone": "1f647-1f3fd-200d-2640-fe0f", "family_woman_boy_boy_light_skin_tone": "1f469-1f3fb", "battery": "1f50b", "couplekiss_man_man_medium_light_skin_tone": "1f468-1f3fc", "clock5": "1f554", "white_flag": "1f3f3-fe0f", "guadeloupe": "1f1ec-1f1f5", "muscle_medium_dark_skin_tone": "1f4aa-1f3fe", "man_scientist_dark_skin_tone": "1f468-1f3ff", "business_suit_levitating_light_skin_tone": "1f574-1f3fb", "woman_office_worker": "1f469-200d-1f4bc", "gift": "1f381", "sound": "1f509", "clubs": "2663-fe0f", "woman_scientist_medium_light_skin_tone": "1f469-1f3fc", "female_detective_medium_skin_tone": "1f575-1f3fd-200d-2640-fe0f", "man_singer_medium_light_skin_tone": "1f468-1f3fc", "family_man_girl": "1f468-200d-1f467", "bee": "1f41d", "full_moon_with_face": "1f31d", "black_medium_square": "25fc-fe0f", "zambia": "1f1ff-1f1f2", "raised_hands_dark_skin_tone": "1f64c-1f3ff", "family_woman_woman_boy_boy_medium_skin_tone": "1f469-1f3fd", "bread": "1f35e", "clock11": "1f55a", "man_office_worker_medium_light_skin_tone": "1f468-1f3fc", "woman_firefighter_medium_skin_tone": "1f469-1f3fd", "man_dancing_dark_skin_tone": "1f57a-1f3ff", "family_man_boy_medium_dark_skin_tone": "1f468-1f3fe", "hugs": "1f917", "roll_eyes": "1f644", "raised_hand": "270b", "tangerine": "1f34a", "grey_question": "2754", "princess_light_skin_tone": "1f478-1f3fb", "motor_boat": "1f6e5", "passport_control": "1f6c2", "man_artist_medium_dark_skin_tone": "1f468-1f3fe", "golfing_man_medium_skin_tone": "1f3cc-1f3fd-200d-2640-fe0f", "shirt": "1f455", "whale": "1f433", "apple": "1f34e", "ethiopia": "1f1ea-1f1f9", "jordan": "1f1ef-1f1f4", "biking_woman_medium_dark_skin_tone": "1f6b4-1f3fe-200d-2640-fe0f", "family_woman_girl_girl_medium_skin_tone": "1f469-1f3fd", "turkey": "1f983", "snowman_with_snow": "2603-fe0f", "fist_left_medium_skin_tone": "1f91b-1f3fd", "woman_with_turban_light_skin_tone": "1f473-1f3fb-200d-2640-fe0f", "woman_pilot_dark_skin_tone": "1f469-1f3ff", "family_woman_woman_boy_medium_skin_tone": "1f469-1f3fd", "purple_heart": "1f49c", "black_heart": "1f5a4", "haircut_man": "1f487-200d-2642-fe0f", "arrow_lower_left": "2199-fe0f", "guinea_bissau": "1f1ec-1f1fc", "sudan": "1f1f8-1f1e9", "woman_scientist_light_skin_tone": "1f469-1f3fb", "bust_in_silhouette": "1f464", "walking": "1f6b6", "european_union": "1f1ea-1f1fa", "running_woman_dark_skin_tone": "1f3c3-1f3ff-200d-2640-fe0f", "om": "1f549", "rowing_man_medium_skin_tone": "1f6a3-1f3fd-200d-2640-fe0f", "ideograph_advantage": "1f250", "nepal": "1f1f3-1f1f5", "syria": "1f1f8-1f1fe", "man_pilot_medium_dark_skin_tone": "1f468-1f3fe", "princess_medium_dark_skin_tone": "1f478-1f3fe", "watermelon": "1f349", "left_luggage": "1f6c5", "us": "1f1fa-1f1f8", "point_left_medium_light_skin_tone": "1f448-1f3fc", "family_man_girl_boy_medium_skin_tone": "1f468-1f3fd", "biking_man_medium_dark_skin_tone": "1f6b4-1f3fe-200d-2640-fe0f", "keycap_ten": "1f51f", "man_medium_light_skin_tone": "1f468-1f3fc", "couple_with_heart_man_man_light_skin_tone": "1f468-1f3fb", "family_man_man_girl_boy_light_skin_tone": "1f468-1f3fb", "dog2": "1f415", "art": "1f3a8", "taxi": "1f695", "motorcycle": "1f3cd", "diamond_shape_with_a_dot_inside": "1f4a0", "writing_hand_light_skin_tone": "270d-1f3fb", "woman_playing_water_polo_medium_dark_skin_tone": "1f93d-1f3fe-200d-2640-fe0f", "martial_arts_uniform": "1f94b", "spiral_calendar": "1f5d3", "older_man_medium_dark_skin_tone": "1f474-1f3fe", "woman_artist_medium_skin_tone": "1f469-1f3fd", "no_good_man_medium_dark_skin_tone": "1f645-1f3fe-200d-2642-fe0f", "family_woman_woman_boy_medium_light_skin_tone": "1f469-1f3fc", "ship": "1f6a2", "bangbang": "203c-fe0f", "israel": "1f1ee-1f1f1", "rowing_man_medium_dark_skin_tone": "1f6a3-1f3fe-200d-2640-fe0f", "calling": "1f4f2", "scorpius": "264f-fe0f", "vulcan_salute_dark_skin_tone": "1f596-1f3ff", "woman_office_worker_light_skin_tone": "1f469-1f3fb", "man_judge_light_skin_tone": "1f468-1f3fb", "family_woman_woman_boy_boy_medium_dark_skin_tone": "1f469-1f3fe", "woman_playing_handball": "1f93e-200d-2640-fe0f", "bridge_at_night": "1f309", "stop_sign": "1f6d1", "8ball": "1f3b1", "orange_book": "1f4d9", "couplekiss_man_woman": "1f48f", "no_mobile_phones": "1f4f5", "pouting_man_dark_skin_tone": "1f64e-1f3ff-200d-2642-fe0f", "man_juggling_light_skin_tone": "1f939-1f3fb-200d-2642-fe0f", "cold_sweat": "1f630", "star2": "1f31f", "taco": "1f32e", "point_right_medium_light_skin_tone": "1f449-1f3fc", "selfie_medium_dark_skin_tone": "1f933-1f3fe", "family_woman_woman_girl_boy_light_skin_tone": "1f469-1f3fb", "hankey": "1f4a9", "monkey_face": "1f435", "sweden": "1f1f8-1f1ea", "crocodile": "1f40a", "last_quarter_moon_with_face": "1f31c", "comet": "2604-fe0f", "caribbean_netherlands": "1f1e7-1f1f6", "walking_man_medium_dark_skin_tone": "1f6b6-1f3fe-200d-2640-fe0f", "basketball_man_medium_light_skin_tone": "26f9-1f3fc-200d-2640-fe0f", "deer": "1f98c", "clock4": "1f553", "christmas_island": "1f1e8-1f1fd", "fist_right_medium_skin_tone": "1f91c-1f3fd", "man_cook_dark_skin_tone": "1f468-1f3ff", "family_man_man_girl_medium_light_skin_tone": "1f468-1f3fc", "whale2": "1f40b", "sagittarius": "2650-fe0f", "children_crossing": "1f6b8", "call_me_hand_dark_skin_tone": "1f919-1f3ff", "ok_woman_medium_dark_skin_tone": "1f646-1f3fe-200d-2640-fe0f", "man_firefighter": "1f468-200d-1f692", "rewind": "23ea", "guardswoman_light_skin_tone": "1f482-1f3fb-200d-2640-fe0f", "woman_technologist_light_skin_tone": "1f469-1f3fb", "woman_pilot_light_skin_tone": "1f469-1f3fb", "raising_hand_woman_light_skin_tone": "1f64b-1f3fb-200d-2640-fe0f", "bowing_man_light_skin_tone": "1f647-1f3fb-200d-2640-fe0f", "frowning_man_medium_skin_tone": "1f64d-1f3fd-200d-2642-fe0f", "shark": "1f988", "sun_behind_rain_cloud": "1f326", "dagger": "1f5e1", "musical_note": "1f3b5", "crossed_fingers_dark_skin_tone": "1f91e-1f3ff", "man_pilot_medium_skin_tone": "1f468-1f3fd", "family_woman_boy_medium_skin_tone": "1f469-1f3fd", "golfing_man_medium_light_skin_tone": "1f3cc-1f3fc-200d-2640-fe0f", "girl": "1f467", "family_man_woman_boy_boy": "1f468-200d-1f469-200d-1f466-200d-1f466", "biking_woman": "1f6b4-200d-2640-fe0f", "cl": "1f191", "raised_back_of_hand_medium_dark_skin_tone": "1f91a-1f3fe", "raising_hand_woman_medium_light_skin_tone": "1f64b-1f3fc-200d-2640-fe0f", "baby_medium_skin_tone": "1f476-1f3fd", "guardsman": "1f482", "woman_astronaut": "1f469-200d-1f680", "tophat": "1f3a9", "honduras": "1f1ed-1f1f3", "mexico": "1f1f2-1f1fd", "nauru": "1f1f3-1f1f7", "mrs_claus_medium_light_skin_tone": "1f936-1f3fc", "weary": "1f629", "womans_hat": "1f452", "person_fencing": "1f93a", "u6708": "1f237-fe0f", "a": "1f170-fe0f", "de": "1f1e9-1f1ea", "lebanon": "1f1f1-1f1e7", "puerto_rico": "1f1f5-1f1f7", "man_mechanic_dark_skin_tone": "1f468-1f3ff", "policeman_dark_skin_tone": "1f46e-1f3ff-200d-2640-fe0f", "kissing_smiling_eyes": "1f619", "avocado": "1f951", "six_pointed_star": "1f52f", "record_button": "23fa", "family_woman_woman_girl_girl_light_skin_tone": "1f469-1f3fb", "mountain_biking_man_medium_light_skin_tone": "1f6b5-1f3fc-200d-2640-fe0f", "couple_with_heart_man_man_medium_dark_skin_tone": "1f468-1f3fe", "family_man_girl_girl_light_skin_tone": "1f468-1f3fb", "post_office": "1f3e3", "telescope": "1f52d", "baby_symbol": "1f6bc", "capital_abcd": "1f520", "woman_singer_light_skin_tone": "1f469-1f3fb", "woman_facepalming_medium_dark_skin_tone": "1f926-1f3fe-200d-2640-fe0f", "ant": "1f41c", "house": "1f3e0", "shield": "1f6e1", "yellow_heart": "1f49b", "u55b6": "1f23a", "senegal": "1f1f8-1f1f3", "united_arab_emirates": "1f1e6-1f1ea", "no_good_woman_medium_skin_tone": "1f645-1f3fd-200d-2640-fe0f", "running_man_medium_light_skin_tone": "1f3c3-1f3fc-200d-2640-fe0f", "beetle": "1f41e", "bus": "1f68c", "flight_arrival": "1f6ec", "black_large_square": "2b1b-fe0f", "white_large_square": "2b1c-fe0f", "woman_technologist_medium_light_skin_tone": "1f469-1f3fc", "skier": "26f7", "ok_hand_medium_light_skin_tone": "1f44c-1f3fc", "rofl": "1f923", "hushed": "1f62f", "ng_man": "1f645-200d-2642-fe0f", "running_woman": "1f3c3-200d-2640-fe0f", "family_man_man_girl_girl": "1f468-200d-1f468-200d-1f467-200d-1f467", "gorilla": "1f98d", "horse_racing_medium_light_skin_tone": "1f3c7-1f3fc", "mountain_biking_man_medium_skin_tone": "1f6b5-1f3fd-200d-2640-fe0f", "disappointed": "1f61e", "dolphin": "1f42c", "green_apple": "1f34f", "honey_pot": "1f36f", "georgia": "1f1ec-1f1ea", "business_suit_levitating": "1f574", "camera": "1f4f7", "ledger": "1f4d2", "woman_cook_medium_skin_tone": "1f469-1f3fd", "bahamas": "1f1e7-1f1f8", "family_woman_woman_girl": "1f469-200d-1f469-200d-1f467", "man_factory_worker_light_skin_tone": "1f468-1f3fb", "golfing_woman_medium_dark_skin_tone": "1f3cc-1f3fe-200d-2640-fe0f", "family_woman_girl": "1f469-200d-1f467", "turtle": "1f422", "mauritius": "1f1f2-1f1fa", "family_man_girl_light_skin_tone": "1f468-1f3fb", "hospital": "1f3e5", "church": "26ea-fe0f", "wheel_of_dharma": "2638-fe0f", "mongolia": "1f1f2-1f1f3", "man_facepalming": "1f926-200d-2642-fe0f", "bed": "1f6cf", "man_artist_medium_light_skin_tone": "1f468-1f3fc", "santa_medium_dark_skin_tone": "1f385-1f3fe", "family_man_woman_girl_medium_light_skin_tone": "1f468-1f3fc", "large_blue_diamond": "1f537", "colombia": "1f1e8-1f1f4", "philippines": "1f1f5-1f1ed", "older_woman_dark_skin_tone": "1f475-1f3ff", "woman_scientist_medium_skin_tone": "1f469-1f3fd", "couplekiss_woman_woman_light_skin_tone": "1f469-1f3fb", "male_detective": "1f575-fe0f", "crossed_swords": "2694-fe0f", "notebook": "1f4d3", "nail_care_medium_light_skin_tone": "1f485-1f3fc", "blonde_man_medium_skin_tone": "1f471-1f3fd-200d-2640-fe0f", "tipping_hand_man_light_skin_tone": "1f481-1f3fb-200d-2642-fe0f", "sweat_smile": "1f605", "white_medium_small_square": "25fd-fe0f", "bowtie": "bowtie", "reminder_ribbon": "1f397", "clamp": "1f5dc", "balance_scale": "2696-fe0f", "postal_horn": "1f4ef", "swimming_woman_dark_skin_tone": "1f3ca-1f3ff-200d-2640-fe0f", "raised_hand_with_fingers_splayed": "1f590", "ok_man": "1f646-200d-2642-fe0f", "new": "1f195", "male_detective_medium_dark_skin_tone": "1f575-1f3fe-200d-2640-fe0f", "computer_mouse": "1f5b1", "hourglass_flowing_sand": "23f3", "bahrain": "1f1e7-1f1ed", "djibouti": "1f1e9-1f1ef", "zimbabwe": "1f1ff-1f1fc", "swimming_man_light_skin_tone": "1f3ca-1f3fb-200d-2640-fe0f", "interrobang": "2049-fe0f", "clock8": "1f557", "pancakes": "1f95e", "thermometer": "1f321", "label": "1f3f7", "denmark": "1f1e9-1f1f0", "raising_hand_man_medium_light_skin_tone": "1f64b-1f3fc-200d-2642-fe0f", "frowning_man_medium_dark_skin_tone": "1f64d-1f3fe-200d-2642-fe0f", "point_right": "1f449", "guitar": "1f3b8", "family_woman_woman_girl_medium_skin_tone": "1f469-1f3fd", "woman_juggling_medium_skin_tone": "1f939-1f3fd-200d-2640-fe0f", "man_health_worker": "1f468-200d-2695-fe0f", "stew": "1f372", "surfing_man": "1f3c4", "twisted_rightwards_arrows": "1f500", "timor_leste": "1f1f9-1f1f1", "weight_lifting_woman": "1f3cb-fe0f-200d-2640-fe0f", "amphora": "1f3fa", "heart": "2764-fe0f", "bowing_man_medium_dark_skin_tone": "1f647-1f3fe-200d-2640-fe0f"} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/emoji_search.go b/vendor/github.com/mattermost/mattermost-server/v5/model/emoji_search.go new file mode 100644 index 00000000..71e2671c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/emoji_search.go @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type EmojiSearch struct { + Term string `json:"term"` + PrefixOnly bool `json:"prefix_only"` +} + +func (es *EmojiSearch) ToJson() string { + b, _ := json.Marshal(es) + return string(b) +} + +func EmojiSearchFromJson(data io.Reader) *EmojiSearch { + var es *EmojiSearch + json.NewDecoder(data).Decode(&es) + return es +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/file.go b/vendor/github.com/mattermost/mattermost-server/v5/model/file.go new file mode 100644 index 00000000..9f76bac1 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/file.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + MaxImageSize = int64(6048 * 4032) // 24 megapixels, roughly 36MB as a raw image +) + +var ( + IMAGE_EXTENSIONS = [7]string{".jpg", ".jpeg", ".gif", ".bmp", ".png", ".tiff", "tif"} + IMAGE_MIME_TYPES = map[string]string{".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff", ".tif": "image/tif"} +) + +type FileUploadResponse struct { + FileInfos []*FileInfo `json:"file_infos"` + ClientIds []string `json:"client_ids"` +} + +func FileUploadResponseFromJson(data io.Reader) *FileUploadResponse { + var o *FileUploadResponse + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *FileUploadResponse) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/file_info.go b/vendor/github.com/mattermost/mattermost-server/v5/model/file_info.go new file mode 100644 index 00000000..8a3a5cc0 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/file_info.go @@ -0,0 +1,209 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "bytes" + "encoding/json" + "image" + "image/gif" + "io" + "mime" + "net/http" + "path/filepath" + "strings" +) + +const ( + FILEINFO_SORT_BY_CREATED = "CreateAt" + FILEINFO_SORT_BY_SIZE = "Size" +) + +// GetFileInfosOptions contains options for getting FileInfos +type GetFileInfosOptions struct { + // UserIds optionally limits the FileInfos to those created by the given users. + UserIds []string `json:"user_ids"` + // ChannelIds optionally limits the FileInfos to those created in the given channels. + ChannelIds []string `json:"channel_ids"` + // Since optionally limits FileInfos to those created at or after the given time, specified as Unix time in milliseconds. + Since int64 `json:"since"` + // IncludeDeleted if set includes deleted FileInfos. + IncludeDeleted bool `json:"include_deleted"` + // SortBy sorts the FileInfos by this field. The default is to sort by date created. + SortBy string `json:"sort_by"` + // SortDescending changes the sort direction to descending order when true. + SortDescending bool `json:"sort_descending"` +} + +type FileInfo struct { + Id string `json:"id"` + CreatorId string `json:"user_id"` + PostId string `json:"post_id,omitempty"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + Path string `json:"-"` // not sent back to the client + ThumbnailPath string `json:"-"` // not sent back to the client + PreviewPath string `json:"-"` // not sent back to the client + Name string `json:"name"` + Extension string `json:"extension"` + Size int64 `json:"size"` + MimeType string `json:"mime_type"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + HasPreviewImage bool `json:"has_preview_image,omitempty"` +} + +func (fi *FileInfo) ToJson() string { + b, _ := json.Marshal(fi) + return string(b) +} + +func FileInfoFromJson(data io.Reader) *FileInfo { + decoder := json.NewDecoder(data) + + var fi FileInfo + if err := decoder.Decode(&fi); err != nil { + return nil + } else { + return &fi + } +} + +func FileInfosToJson(infos []*FileInfo) string { + b, _ := json.Marshal(infos) + return string(b) +} + +func FileInfosFromJson(data io.Reader) []*FileInfo { + decoder := json.NewDecoder(data) + + var infos []*FileInfo + if err := decoder.Decode(&infos); err != nil { + return nil + } else { + return infos + } +} + +func (fi *FileInfo) PreSave() { + if fi.Id == "" { + fi.Id = NewId() + } + + if fi.CreateAt == 0 { + fi.CreateAt = GetMillis() + } + + if fi.UpdateAt < fi.CreateAt { + fi.UpdateAt = fi.CreateAt + } +} + +func (fi *FileInfo) IsValid() *AppError { + if !IsValidId(fi.Id) { + return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(fi.CreatorId) && fi.CreatorId != "nouser" { + return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+fi.Id, http.StatusBadRequest) + } + + if len(fi.PostId) != 0 && !IsValidId(fi.PostId) { + return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.post_id.app_error", nil, "id="+fi.Id, http.StatusBadRequest) + } + + if fi.CreateAt == 0 { + return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.create_at.app_error", nil, "id="+fi.Id, http.StatusBadRequest) + } + + if fi.UpdateAt == 0 { + return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.update_at.app_error", nil, "id="+fi.Id, http.StatusBadRequest) + } + + if fi.Path == "" { + return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.path.app_error", nil, "id="+fi.Id, http.StatusBadRequest) + } + + return nil +} + +func (fi *FileInfo) IsImage() bool { + return strings.HasPrefix(fi.MimeType, "image") +} + +func NewInfo(name string) *FileInfo { + info := &FileInfo{ + Name: name, + } + + extension := strings.ToLower(filepath.Ext(name)) + info.MimeType = mime.TypeByExtension(extension) + + if extension != "" && extension[0] == '.' { + // The client expects a file extension without the leading period + info.Extension = extension[1:] + } else { + info.Extension = extension + } + + return info +} + +func GetInfoForBytes(name string, data []byte) (*FileInfo, *AppError) { + info := &FileInfo{ + Name: name, + Size: int64(len(data)), + } + var err *AppError + + extension := strings.ToLower(filepath.Ext(name)) + info.MimeType = mime.TypeByExtension(extension) + + if extension != "" && extension[0] == '.' { + // The client expects a file extension without the leading period + info.Extension = extension[1:] + } else { + info.Extension = extension + } + + if info.IsImage() { + // Only set the width and height if it's actually an image that we can understand + if config, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil { + info.Width = config.Width + info.Height = config.Height + + if info.MimeType == "image/gif" { + // Just show the gif itself instead of a preview image for animated gifs + if gifConfig, err := gif.DecodeAll(bytes.NewReader(data)); err != nil { + // Still return the rest of the info even though it doesn't appear to be an actual gif + info.HasPreviewImage = true + return info, NewAppError("GetInfoForBytes", "model.file_info.get.gif.app_error", nil, "name="+name, http.StatusBadRequest) + } else { + info.HasPreviewImage = len(gifConfig.Image) == 1 + } + } else { + info.HasPreviewImage = true + } + } + } + + return info, err +} + +func GetEtagForFileInfos(infos []*FileInfo) string { + if len(infos) == 0 { + return Etag() + } + + var maxUpdateAt int64 + + for _, info := range infos { + if info.UpdateAt > maxUpdateAt { + maxUpdateAt = info.UpdateAt + } + } + + return Etag(infos[0].PostId, maxUpdateAt) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/gitlab.go b/vendor/github.com/mattermost/mattermost-server/v5/model/gitlab.go new file mode 100644 index 00000000..0b069cd6 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/gitlab.go @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + USER_AUTH_SERVICE_GITLAB = "gitlab" +) diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/group.go b/vendor/github.com/mattermost/mattermost-server/v5/model/group.go new file mode 100644 index 00000000..4de0dcc4 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/group.go @@ -0,0 +1,210 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "regexp" +) + +const ( + GroupSourceLdap GroupSource = "ldap" + + GroupNameMaxLength = 64 + GroupSourceMaxLength = 64 + GroupDisplayNameMaxLength = 128 + GroupDescriptionMaxLength = 1024 + GroupRemoteIDMaxLength = 48 +) + +type GroupSource string + +var allGroupSources = []GroupSource{ + GroupSourceLdap, +} + +var groupSourcesRequiringRemoteID = []GroupSource{ + GroupSourceLdap, +} + +type Group struct { + Id string `json:"id"` + Name *string `json:"name,omitempty"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + Source GroupSource `json:"source"` + RemoteId string `json:"remote_id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + HasSyncables bool `db:"-" json:"has_syncables"` + MemberCount *int `db:"-" json:"member_count,omitempty"` + AllowReference bool `json:"allow_reference"` +} + +type GroupWithSchemeAdmin struct { + Group + SchemeAdmin *bool `db:"SyncableSchemeAdmin" json:"scheme_admin,omitempty"` +} + +type GroupsAssociatedToChannelWithSchemeAdmin struct { + ChannelId string `json:"channel_id"` + Group + SchemeAdmin *bool `db:"SyncableSchemeAdmin" json:"scheme_admin,omitempty"` +} +type GroupsAssociatedToChannel struct { + ChannelId string `json:"channel_id"` + Groups []*GroupWithSchemeAdmin `json:"groups"` +} + +type GroupPatch struct { + Name *string `json:"name"` + DisplayName *string `json:"display_name"` + Description *string `json:"description"` + AllowReference *bool `json:"allow_reference"` +} + +type LdapGroupSearchOpts struct { + Q string + IsLinked *bool + IsConfigured *bool +} + +type GroupSearchOpts struct { + Q string + NotAssociatedToTeam string + NotAssociatedToChannel string + IncludeMemberCount bool + FilterAllowReference bool + PageOpts *PageOpts + Since int64 + + // FilterParentTeamPermitted filters the groups to the intersect of the + // set associated to the parent team and those returned by the query. + // If the parent team is not group-constrained or if NotAssociatedToChannel + // is not set then this option is ignored. + FilterParentTeamPermitted bool +} + +type PageOpts struct { + Page int + PerPage int +} + +func (group *Group) Patch(patch *GroupPatch) { + if patch.Name != nil { + group.Name = patch.Name + } + if patch.DisplayName != nil { + group.DisplayName = *patch.DisplayName + } + if patch.Description != nil { + group.Description = *patch.Description + } + if patch.AllowReference != nil { + group.AllowReference = *patch.AllowReference + } +} + +func (group *Group) IsValidForCreate() *AppError { + err := group.IsValidName() + if err != nil { + return err + } + + if l := len(group.DisplayName); l == 0 || l > GroupDisplayNameMaxLength { + return NewAppError("Group.IsValidForCreate", "model.group.display_name.app_error", map[string]interface{}{"GroupDisplayNameMaxLength": GroupDisplayNameMaxLength}, "", http.StatusBadRequest) + } + + if len(group.Description) > GroupDescriptionMaxLength { + return NewAppError("Group.IsValidForCreate", "model.group.description.app_error", map[string]interface{}{"GroupDescriptionMaxLength": GroupDescriptionMaxLength}, "", http.StatusBadRequest) + } + + isValidSource := false + for _, groupSource := range allGroupSources { + if group.Source == groupSource { + isValidSource = true + break + } + } + if !isValidSource { + return NewAppError("Group.IsValidForCreate", "model.group.source.app_error", nil, "", http.StatusBadRequest) + } + + if len(group.RemoteId) > GroupRemoteIDMaxLength || (len(group.RemoteId) == 0 && group.requiresRemoteId()) { + return NewAppError("Group.IsValidForCreate", "model.group.remote_id.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (group *Group) requiresRemoteId() bool { + for _, groupSource := range groupSourcesRequiringRemoteID { + if groupSource == group.Source { + return true + } + } + return false +} + +func (group *Group) IsValidForUpdate() *AppError { + if !IsValidId(group.Id) { + return NewAppError("Group.IsValidForUpdate", "model.group.id.app_error", nil, "", http.StatusBadRequest) + } + if group.CreateAt == 0 { + return NewAppError("Group.IsValidForUpdate", "model.group.create_at.app_error", nil, "", http.StatusBadRequest) + } + if group.UpdateAt == 0 { + return NewAppError("Group.IsValidForUpdate", "model.group.update_at.app_error", nil, "", http.StatusBadRequest) + } + if err := group.IsValidForCreate(); err != nil { + return err + } + return nil +} + +func (group *Group) ToJson() string { + b, _ := json.Marshal(group) + return string(b) +} + +var validGroupnameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`) + +func (group *Group) IsValidName() *AppError { + + if group.Name == nil { + if group.AllowReference { + return NewAppError("Group.IsValidName", "model.group.name.app_error", map[string]interface{}{"GroupNameMaxLength": GroupNameMaxLength}, "", http.StatusBadRequest) + } + } else { + if l := len(*group.Name); l == 0 || l > GroupNameMaxLength { + return NewAppError("Group.IsValidName", "model.group.name.invalid_length.app_error", map[string]interface{}{"GroupNameMaxLength": GroupNameMaxLength}, "", http.StatusBadRequest) + } + + if !validGroupnameChars.MatchString(*group.Name) { + return NewAppError("Group.IsValidName", "model.group.name.invalid_chars.app_error", nil, "", http.StatusBadRequest) + } + } + return nil +} + +func GroupFromJson(data io.Reader) *Group { + var group *Group + json.NewDecoder(data).Decode(&group) + return group +} + +func GroupsFromJson(data io.Reader) []*Group { + var groups []*Group + json.NewDecoder(data).Decode(&groups) + return groups +} + +func GroupPatchFromJson(data io.Reader) *GroupPatch { + var groupPatch *GroupPatch + json.NewDecoder(data).Decode(&groupPatch) + return groupPatch +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/group_member.go b/vendor/github.com/mattermost/mattermost-server/v5/model/group_member.go new file mode 100644 index 00000000..d18d7849 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/group_member.go @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import "net/http" + +type GroupMember struct { + GroupId string `json:"group_id"` + UserId string `json:"user_id"` + CreateAt int64 `json:"create_at"` + DeleteAt int64 `json:"delete_at"` +} + +func (gm *GroupMember) IsValid() *AppError { + if !IsValidId(gm.GroupId) { + return NewAppError("GroupMember.IsValid", "model.group_member.group_id.app_error", nil, "", http.StatusBadRequest) + } + if !IsValidId(gm.UserId) { + return NewAppError("GroupMember.IsValid", "model.group_member.user_id.app_error", nil, "", http.StatusBadRequest) + } + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/group_syncable.go b/vendor/github.com/mattermost/mattermost-server/v5/model/group_syncable.go new file mode 100644 index 00000000..6a4d4023 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/group_syncable.go @@ -0,0 +1,180 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" +) + +type GroupSyncableType string + +const ( + GroupSyncableTypeTeam GroupSyncableType = "Team" + GroupSyncableTypeChannel GroupSyncableType = "Channel" +) + +func (gst GroupSyncableType) String() string { + return string(gst) +} + +type GroupSyncable struct { + GroupId string `json:"group_id"` + + // SyncableId represents the Id of the model that is being synced with the group, for example a ChannelId or + // TeamId. + SyncableId string `db:"-" json:"-"` + + AutoAdd bool `json:"auto_add"` + SchemeAdmin bool `json:"scheme_admin"` + CreateAt int64 `json:"create_at"` + DeleteAt int64 `json:"delete_at"` + UpdateAt int64 `json:"update_at"` + Type GroupSyncableType `db:"-" json:"-"` + + // Values joined in from the associated team and/or channel + ChannelDisplayName string `db:"-" json:"-"` + TeamDisplayName string `db:"-" json:"-"` + TeamType string `db:"-" json:"-"` + ChannelType string `db:"-" json:"-"` + TeamID string `db:"-" json:"-"` +} + +func (syncable *GroupSyncable) IsValid() *AppError { + if !IsValidId(syncable.GroupId) { + return NewAppError("GroupSyncable.SyncableIsValid", "model.group_syncable.group_id.app_error", nil, "", http.StatusBadRequest) + } + if !IsValidId(syncable.SyncableId) { + return NewAppError("GroupSyncable.SyncableIsValid", "model.group_syncable.syncable_id.app_error", nil, "", http.StatusBadRequest) + } + return nil +} + +func (syncable *GroupSyncable) UnmarshalJSON(b []byte) error { + var kvp map[string]interface{} + err := json.Unmarshal(b, &kvp) + if err != nil { + return err + } + for key, value := range kvp { + switch key { + case "team_id": + syncable.SyncableId = value.(string) + syncable.Type = GroupSyncableTypeTeam + case "channel_id": + syncable.SyncableId = value.(string) + syncable.Type = GroupSyncableTypeChannel + case "group_id": + syncable.GroupId = value.(string) + case "auto_add": + syncable.AutoAdd = value.(bool) + default: + } + } + return nil +} + +func (syncable *GroupSyncable) MarshalJSON() ([]byte, error) { + type Alias GroupSyncable + + switch syncable.Type { + case GroupSyncableTypeTeam: + return json.Marshal(&struct { + TeamID string `json:"team_id"` + TeamDisplayName string `json:"team_display_name,omitempty"` + TeamType string `json:"team_type,omitempty"` + *Alias + }{ + TeamDisplayName: syncable.TeamDisplayName, + TeamType: syncable.TeamType, + TeamID: syncable.SyncableId, + Alias: (*Alias)(syncable), + }) + case GroupSyncableTypeChannel: + return json.Marshal(&struct { + ChannelID string `json:"channel_id"` + ChannelDisplayName string `json:"channel_display_name,omitempty"` + ChannelType string `json:"channel_type,omitempty"` + + TeamID string `json:"team_id,omitempty"` + TeamDisplayName string `json:"team_display_name,omitempty"` + TeamType string `json:"team_type,omitempty"` + + *Alias + }{ + ChannelID: syncable.SyncableId, + ChannelDisplayName: syncable.ChannelDisplayName, + ChannelType: syncable.ChannelType, + + TeamID: syncable.TeamID, + TeamDisplayName: syncable.TeamDisplayName, + TeamType: syncable.TeamType, + + Alias: (*Alias)(syncable), + }) + default: + return nil, &json.MarshalerError{ + Err: fmt.Errorf("unknown syncable type: %s", syncable.Type), + } + } +} + +type GroupSyncablePatch struct { + AutoAdd *bool `json:"auto_add"` + SchemeAdmin *bool `json:"scheme_admin"` +} + +func (syncable *GroupSyncable) Patch(patch *GroupSyncablePatch) { + if patch.AutoAdd != nil { + syncable.AutoAdd = *patch.AutoAdd + } + if patch.SchemeAdmin != nil { + syncable.SchemeAdmin = *patch.SchemeAdmin + } +} + +type UserTeamIDPair struct { + UserID string + TeamID string +} + +type UserChannelIDPair struct { + UserID string + ChannelID string +} + +func GroupSyncableFromJson(data io.Reader) *GroupSyncable { + groupSyncable := &GroupSyncable{} + bodyBytes, _ := ioutil.ReadAll(data) + json.Unmarshal(bodyBytes, groupSyncable) + return groupSyncable +} + +func GroupSyncablesFromJson(data io.Reader) []*GroupSyncable { + groupSyncables := []*GroupSyncable{} + bodyBytes, _ := ioutil.ReadAll(data) + json.Unmarshal(bodyBytes, &groupSyncables) + return groupSyncables +} + +func NewGroupTeam(groupID, teamID string, autoAdd bool) *GroupSyncable { + return &GroupSyncable{ + GroupId: groupID, + SyncableId: teamID, + Type: GroupSyncableTypeTeam, + AutoAdd: autoAdd, + } +} + +func NewGroupChannel(groupID, channelID string, autoAdd bool) *GroupSyncable { + return &GroupSyncable{ + GroupId: groupID, + SyncableId: channelID, + Type: GroupSyncableTypeChannel, + AutoAdd: autoAdd, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/guest_invite.go b/vendor/github.com/mattermost/mattermost-server/v5/model/guest_invite.go new file mode 100644 index 00000000..3cdd4893 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/guest_invite.go @@ -0,0 +1,53 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" +) + +type GuestsInvite struct { + Emails []string `json:"emails"` + Channels []string `json:"channels"` + Message string `json:"message"` +} + +// IsValid validates the user and returns an error if it isn't configured +// correctly. +func (i *GuestsInvite) IsValid() *AppError { + if len(i.Emails) == 0 { + return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.emails.app_error", nil, "", http.StatusBadRequest) + } + + for _, email := range i.Emails { + if len(email) > USER_EMAIL_MAX_LENGTH || len(email) == 0 || !IsValidEmail(email) { + return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.email.app_error", nil, "email="+email, http.StatusBadRequest) + } + } + + if len(i.Channels) == 0 { + return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.channels.app_error", nil, "", http.StatusBadRequest) + } + + for _, channel := range i.Channels { + if len(channel) != 26 { + return NewAppError("GuestsInvite.IsValid", "model.guest.is_valid.channel.app_error", nil, "channel="+channel, http.StatusBadRequest) + } + } + return nil +} + +// GuestsInviteFromJson will decode the input and return a GuestsInvite +func GuestsInviteFromJson(data io.Reader) *GuestsInvite { + var i *GuestsInvite + json.NewDecoder(data).Decode(&i) + return i +} + +func (i *GuestsInvite) ToJson() string { + b, _ := json.Marshal(i) + return string(b) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/incoming_webhook.go b/vendor/github.com/mattermost/mattermost-server/v5/model/incoming_webhook.go new file mode 100644 index 00000000..78f1e4e8 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/incoming_webhook.go @@ -0,0 +1,217 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "regexp" +) + +const ( + DEFAULT_WEBHOOK_USERNAME = "webhook" +) + +type IncomingWebhook struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + TeamId string `json:"team_id"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + ChannelLocked bool `json:"channel_locked"` +} + +type IncomingWebhookRequest struct { + Text string `json:"text"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + ChannelName string `json:"channel"` + Props StringInterface `json:"props"` + Attachments []*SlackAttachment `json:"attachments"` + Type string `json:"type"` + IconEmoji string `json:"icon_emoji"` +} + +func (o *IncomingWebhook) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func IncomingWebhookFromJson(data io.Reader) *IncomingWebhook { + var o *IncomingWebhook + json.NewDecoder(data).Decode(&o) + return o +} + +func IncomingWebhookListToJson(l []*IncomingWebhook) string { + b, _ := json.Marshal(l) + return string(b) +} + +func IncomingWebhookListFromJson(data io.Reader) []*IncomingWebhook { + var o []*IncomingWebhook + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *IncomingWebhook) IsValid() *AppError { + + if !IsValidId(o.Id) { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "", http.StatusBadRequest) + + } + + if o.CreateAt == 0 { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if o.UpdateAt == 0 { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if !IsValidId(o.UserId) { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.ChannelId) { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.channel_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.TeamId) { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.team_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.DisplayName) > 64 { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.display_name.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Description) > 500 { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.description.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Username) > 64 { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.username.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.IconURL) > 1024 { + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.icon_url.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (o *IncomingWebhook) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt +} + +func (o *IncomingWebhook) PreUpdate() { + o.UpdateAt = GetMillis() +} + +// escapeControlCharsFromPayload escapes control chars (\n, \t) from a byte slice. +// Context: +// JSON strings are not supposed to contain control characters such as \n, \t, +// ... but some incoming webhooks might still send invalid JSON and we want to +// try to handle that. An example invalid JSON string from an incoming webhook +// might look like this (strings for both "text" and "fallback" attributes are +// invalid JSON strings because they contain unescaped newlines and tabs): +// `{ +// "text": "this is a test +// that contains a newline and tabs", +// "attachments": [ +// { +// "fallback": "Required plain-text summary of the attachment +// that contains a newline and tabs", +// "color": "#36a64f", +// ... +// "text": "Optional text that appears within the attachment +// that contains a newline and tabs", +// ... +// "thumb_url": "http://example.com/path/to/thumb.png" +// } +// ] +// }` +// This function will search for `"key": "value"` pairs, and escape \n, \t +// from the value. +func escapeControlCharsFromPayload(by []byte) []byte { + // we'll search for `"text": "..."` or `"fallback": "..."`, ... + keys := "text|fallback|pretext|author_name|title|value" + + // the regexp reads like this: + // (?s): this flag let . match \n (default is false) + // "(keys)": we search for the keys defined above + // \s*:\s*: followed by 0..n spaces/tabs, a colon then 0..n spaces/tabs + // ": a double-quote + // (\\"|[^"])*: any number of times the `\"` string or any char but a double-quote + // ": a double-quote + r := `(?s)"(` + keys + `)"\s*:\s*"(\\"|[^"])*"` + re := regexp.MustCompile(r) + + // the function that will escape \n and \t on the regexp matches + repl := func(b []byte) []byte { + if bytes.Contains(b, []byte("\n")) { + b = bytes.Replace(b, []byte("\n"), []byte("\\n"), -1) + } + if bytes.Contains(b, []byte("\t")) { + b = bytes.Replace(b, []byte("\t"), []byte("\\t"), -1) + } + + return b + } + + return re.ReplaceAllFunc(by, repl) +} + +func decodeIncomingWebhookRequest(by []byte) (*IncomingWebhookRequest, error) { + decoder := json.NewDecoder(bytes.NewReader(by)) + var o IncomingWebhookRequest + err := decoder.Decode(&o) + if err == nil { + return &o, nil + } else { + return nil, err + } +} + +func IncomingWebhookRequestFromJson(data io.Reader) (*IncomingWebhookRequest, *AppError) { + buf := new(bytes.Buffer) + buf.ReadFrom(data) + by := buf.Bytes() + + // Try to decode the JSON data. Only if it fails, try to escape control + // characters from the strings contained in the JSON data. + o, err := decodeIncomingWebhookRequest(by) + if err != nil { + o, err = decodeIncomingWebhookRequest(escapeControlCharsFromPayload(by)) + if err != nil { + return nil, NewAppError("IncomingWebhookRequestFromJson", "model.incoming_hook.parse_data.app_error", nil, err.Error(), http.StatusBadRequest) + } + } + + o.Attachments = StringifySlackFieldValue(o.Attachments) + + return o, nil +} + +func (o *IncomingWebhookRequest) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/initial_load.go b/vendor/github.com/mattermost/mattermost-server/v5/model/initial_load.go new file mode 100644 index 00000000..9368f371 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/initial_load.go @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type InitialLoad struct { + User *User `json:"user"` + TeamMembers []*TeamMember `json:"team_members"` + Teams []*Team `json:"teams"` + Preferences Preferences `json:"preferences"` + ClientCfg map[string]string `json:"client_cfg"` + LicenseCfg map[string]string `json:"license_cfg"` + NoAccounts bool `json:"no_accounts"` +} + +func (me *InitialLoad) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func InitialLoadFromJson(data io.Reader) *InitialLoad { + var o *InitialLoad + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/integration_action.go b/vendor/github.com/mattermost/mattermost-server/v5/model/integration_action.go new file mode 100644 index 00000000..3f362d64 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/integration_action.go @@ -0,0 +1,525 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/rand" + "encoding/asn1" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "strconv" + "strings" +) + +const ( + POST_ACTION_TYPE_BUTTON = "button" + POST_ACTION_TYPE_SELECT = "select" + INTERACTIVE_DIALOG_TRIGGER_TIMEOUT_MILLISECONDS = 3000 +) + +var PostActionRetainPropKeys = []string{"from_webhook", "override_username", "override_icon_url"} + +type DoPostActionRequest struct { + SelectedOption string `json:"selected_option,omitempty"` + Cookie string `json:"cookie,omitempty"` +} + +type PostAction struct { + // A unique Action ID. If not set, generated automatically. + Id string `json:"id,omitempty"` + + // The type of the interactive element. Currently supported are + // "select" and "button". + Type string `json:"type,omitempty"` + + // The text on the button, or in the select placeholder. + Name string `json:"name,omitempty"` + + // If the action is disabled. + Disabled bool `json:"disabled,omitempty"` + + // Style defines a text and border style. + // Supported values are "default", "primary", "success", "good", "warning", "danger" + // and any hex color. + Style string `json:"style,omitempty"` + + // DataSource indicates the data source for the select action. If left + // empty, the select is populated from Options. Other supported values + // are "users" and "channels". + DataSource string `json:"data_source,omitempty"` + + // Options contains the values listed in a select dropdown on the post. + Options []*PostActionOptions `json:"options,omitempty"` + + // DefaultOption contains the option, if any, that will appear as the + // default selection in a select box. It has no effect when used with + // other types of actions. + DefaultOption string `json:"default_option,omitempty"` + + // Defines the interaction with the backend upon a user action. + // Integration contains Context, which is private plugin data; + // Integrations are stripped from Posts when they are sent to the + // client, or are encrypted in a Cookie. + Integration *PostActionIntegration `json:"integration,omitempty"` + Cookie string `json:"cookie,omitempty" db:"-"` +} + +func (p *PostAction) Equals(input *PostAction) bool { + if p.Id != input.Id { + return false + } + + if p.Type != input.Type { + return false + } + + if p.Name != input.Name { + return false + } + + if p.DataSource != input.DataSource { + return false + } + + if p.DefaultOption != input.DefaultOption { + return false + } + + if p.Cookie != input.Cookie { + return false + } + + // Compare PostActionOptions + if len(p.Options) != len(input.Options) { + return false + } + + for k := range p.Options { + if p.Options[k].Text != input.Options[k].Text { + return false + } + + if p.Options[k].Value != input.Options[k].Value { + return false + } + } + + // Compare PostActionIntegration + if p.Integration.URL != input.Integration.URL { + return false + } + + if len(p.Integration.Context) != len(input.Integration.Context) { + return false + } + + for key, value := range p.Integration.Context { + inputValue, ok := input.Integration.Context[key] + + if !ok { + return false + } + + if value != inputValue { + return false + } + } + + return true +} + +// PostActionCookie is set by the server, serialized and encrypted into +// PostAction.Cookie. The clients should hold on to it, and include it with +// subsequent DoPostAction requests. This allows the server to access the +// action metadata even when it's not available in the database, for ephemeral +// posts. +type PostActionCookie struct { + Type string `json:"type,omitempty"` + PostId string `json:"post_id,omitempty"` + RootPostId string `json:"root_post_id,omitempty"` + ChannelId string `json:"channel_id,omitempty"` + DataSource string `json:"data_source,omitempty"` + Integration *PostActionIntegration `json:"integration,omitempty"` + RetainProps map[string]interface{} `json:"retain_props,omitempty"` + RemoveProps []string `json:"remove_props,omitempty"` +} + +type PostActionOptions struct { + Text string `json:"text"` + Value string `json:"value"` +} + +type PostActionIntegration struct { + URL string `json:"url,omitempty"` + Context map[string]interface{} `json:"context,omitempty"` +} + +type PostActionIntegrationRequest struct { + UserId string `json:"user_id"` + UserName string `json:"user_name"` + ChannelId string `json:"channel_id"` + ChannelName string `json:"channel_name"` + TeamId string `json:"team_id"` + TeamName string `json:"team_domain"` + PostId string `json:"post_id"` + TriggerId string `json:"trigger_id"` + Type string `json:"type"` + DataSource string `json:"data_source"` + Context map[string]interface{} `json:"context,omitempty"` +} + +type PostActionIntegrationResponse struct { + Update *Post `json:"update"` + EphemeralText string `json:"ephemeral_text"` + SkipSlackParsing bool `json:"skip_slack_parsing"` // Set to `true` to skip the Slack-compatibility handling of Text. +} + +type PostActionAPIResponse struct { + Status string `json:"status"` // needed to maintain backwards compatibility + TriggerId string `json:"trigger_id"` +} + +type Dialog struct { + CallbackId string `json:"callback_id"` + Title string `json:"title"` + IntroductionText string `json:"introduction_text"` + IconURL string `json:"icon_url"` + Elements []DialogElement `json:"elements"` + SubmitLabel string `json:"submit_label"` + NotifyOnCancel bool `json:"notify_on_cancel"` + State string `json:"state"` +} + +type DialogElement struct { + DisplayName string `json:"display_name"` + Name string `json:"name"` + Type string `json:"type"` + SubType string `json:"subtype"` + Default string `json:"default"` + Placeholder string `json:"placeholder"` + HelpText string `json:"help_text"` + Optional bool `json:"optional"` + MinLength int `json:"min_length"` + MaxLength int `json:"max_length"` + DataSource string `json:"data_source"` + Options []*PostActionOptions `json:"options"` +} + +type OpenDialogRequest struct { + TriggerId string `json:"trigger_id"` + URL string `json:"url"` + Dialog Dialog `json:"dialog"` +} + +type SubmitDialogRequest struct { + Type string `json:"type"` + URL string `json:"url,omitempty"` + CallbackId string `json:"callback_id"` + State string `json:"state"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + TeamId string `json:"team_id"` + Submission map[string]interface{} `json:"submission"` + Cancelled bool `json:"cancelled"` +} + +type SubmitDialogResponse struct { + Error string `json:"error,omitempty"` + Errors map[string]string `json:"errors,omitempty"` +} + +func GenerateTriggerId(userId string, s crypto.Signer) (string, string, *AppError) { + clientTriggerId := NewId() + triggerData := strings.Join([]string{clientTriggerId, userId, strconv.FormatInt(GetMillis(), 10)}, ":") + ":" + + h := crypto.SHA256 + sum := h.New() + sum.Write([]byte(triggerData)) + signature, err := s.Sign(rand.Reader, sum.Sum(nil), h) + if err != nil { + return "", "", NewAppError("GenerateTriggerId", "interactive_message.generate_trigger_id.signing_failed", nil, err.Error(), http.StatusInternalServerError) + } + + base64Sig := base64.StdEncoding.EncodeToString(signature) + + triggerId := base64.StdEncoding.EncodeToString([]byte(triggerData + base64Sig)) + return clientTriggerId, triggerId, nil +} + +func (r *PostActionIntegrationRequest) GenerateTriggerId(s crypto.Signer) (string, string, *AppError) { + clientTriggerId, triggerId, err := GenerateTriggerId(r.UserId, s) + if err != nil { + return "", "", err + } + + r.TriggerId = triggerId + return clientTriggerId, triggerId, nil +} + +func DecodeAndVerifyTriggerId(triggerId string, s *ecdsa.PrivateKey) (string, string, *AppError) { + triggerIdBytes, err := base64.StdEncoding.DecodeString(triggerId) + if err != nil { + return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.base64_decode_failed", nil, err.Error(), http.StatusBadRequest) + } + + split := strings.Split(string(triggerIdBytes), ":") + if len(split) != 4 { + return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.missing_data", nil, "", http.StatusBadRequest) + } + + clientTriggerId := split[0] + userId := split[1] + timestampStr := split[2] + timestamp, _ := strconv.ParseInt(timestampStr, 10, 64) + + now := GetMillis() + if now-timestamp > INTERACTIVE_DIALOG_TRIGGER_TIMEOUT_MILLISECONDS { + return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.expired", map[string]interface{}{"Seconds": INTERACTIVE_DIALOG_TRIGGER_TIMEOUT_MILLISECONDS / 1000}, "", http.StatusBadRequest) + } + + signature, err := base64.StdEncoding.DecodeString(split[3]) + if err != nil { + return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.base64_decode_failed_signature", nil, err.Error(), http.StatusBadRequest) + } + + var esig struct { + R, S *big.Int + } + + if _, err := asn1.Unmarshal(signature, &esig); err != nil { + return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.signature_decode_failed", nil, err.Error(), http.StatusBadRequest) + } + + triggerData := strings.Join([]string{clientTriggerId, userId, timestampStr}, ":") + ":" + + h := crypto.SHA256 + sum := h.New() + sum.Write([]byte(triggerData)) + + if !ecdsa.Verify(&s.PublicKey, sum.Sum(nil), esig.R, esig.S) { + return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.verify_signature_failed", nil, "", http.StatusBadRequest) + } + + return clientTriggerId, userId, nil +} + +func (r *OpenDialogRequest) DecodeAndVerifyTriggerId(s *ecdsa.PrivateKey) (string, string, *AppError) { + return DecodeAndVerifyTriggerId(r.TriggerId, s) +} + +func (r *PostActionIntegrationRequest) ToJson() []byte { + b, _ := json.Marshal(r) + return b +} + +func PostActionIntegrationRequestFromJson(data io.Reader) *PostActionIntegrationRequest { + var o *PostActionIntegrationRequest + err := json.NewDecoder(data).Decode(&o) + if err != nil { + return nil + } + return o +} + +func (r *PostActionIntegrationResponse) ToJson() []byte { + b, _ := json.Marshal(r) + return b +} + +func PostActionIntegrationResponseFromJson(data io.Reader) *PostActionIntegrationResponse { + var o *PostActionIntegrationResponse + err := json.NewDecoder(data).Decode(&o) + if err != nil { + return nil + } + return o +} + +func SubmitDialogRequestFromJson(data io.Reader) *SubmitDialogRequest { + var o *SubmitDialogRequest + err := json.NewDecoder(data).Decode(&o) + if err != nil { + return nil + } + return o +} + +func (r *SubmitDialogRequest) ToJson() []byte { + b, _ := json.Marshal(r) + return b +} + +func SubmitDialogResponseFromJson(data io.Reader) *SubmitDialogResponse { + var o *SubmitDialogResponse + err := json.NewDecoder(data).Decode(&o) + if err != nil { + return nil + } + return o +} + +func (r *SubmitDialogResponse) ToJson() []byte { + b, _ := json.Marshal(r) + return b +} + +func (o *Post) StripActionIntegrations() { + attachments := o.Attachments() + if o.GetProp("attachments") != nil { + o.AddProp("attachments", attachments) + } + for _, attachment := range attachments { + for _, action := range attachment.Actions { + action.Integration = nil + } + } +} + +func (o *Post) GetAction(id string) *PostAction { + for _, attachment := range o.Attachments() { + for _, action := range attachment.Actions { + if action.Id == id { + return action + } + } + } + return nil +} + +func (o *Post) GenerateActionIds() { + if o.GetProp("attachments") != nil { + o.AddProp("attachments", o.Attachments()) + } + if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok { + for _, attachment := range attachments { + for _, action := range attachment.Actions { + if action.Id == "" { + action.Id = NewId() + } + } + } + } +} + +func AddPostActionCookies(o *Post, secret []byte) *Post { + p := o.Clone() + + // retainedProps carry over their value from the old post, including no value + retainProps := map[string]interface{}{} + removeProps := []string{} + for _, key := range PostActionRetainPropKeys { + value, ok := p.GetProps()[key] + if ok { + retainProps[key] = value + } else { + removeProps = append(removeProps, key) + } + } + + attachments := p.Attachments() + for _, attachment := range attachments { + for _, action := range attachment.Actions { + c := &PostActionCookie{ + Type: action.Type, + ChannelId: p.ChannelId, + DataSource: action.DataSource, + Integration: action.Integration, + RetainProps: retainProps, + RemoveProps: removeProps, + } + + c.PostId = p.Id + if p.RootId == "" { + c.RootPostId = p.Id + } else { + c.RootPostId = p.RootId + } + + b, _ := json.Marshal(c) + action.Cookie, _ = encryptPostActionCookie(string(b), secret) + } + } + + return p +} + +func encryptPostActionCookie(plain string, secret []byte) (string, error) { + if len(secret) == 0 { + return plain, nil + } + + block, err := aes.NewCipher(secret) + if err != nil { + return "", err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, aesgcm.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return "", err + } + + sealed := aesgcm.Seal(nil, nonce, []byte(plain), nil) + + combined := append(nonce, sealed...) + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(combined))) + base64.StdEncoding.Encode(encoded, combined) + + return string(encoded), nil +} + +func DecryptPostActionCookie(encoded string, secret []byte) (string, error) { + if len(secret) == 0 { + return encoded, nil + } + + block, err := aes.NewCipher(secret) + if err != nil { + return "", err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + decoded := make([]byte, base64.StdEncoding.DecodedLen(len(encoded))) + n, err := base64.StdEncoding.Decode(decoded, []byte(encoded)) + if err != nil { + return "", err + } + decoded = decoded[:n] + + nonceSize := aesgcm.NonceSize() + if len(decoded) < nonceSize { + return "", fmt.Errorf("cookie too short") + } + + nonce, decoded := decoded[:nonceSize], decoded[nonceSize:] + plain, err := aesgcm.Open(nil, nonce, decoded, nil) + if err != nil { + return "", err + } + + return string(plain), nil +} + +func DoPostActionRequestFromJson(data io.Reader) *DoPostActionRequest { + var o *DoPostActionRequest + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/job.go b/vendor/github.com/mattermost/mattermost-server/v5/model/job.go new file mode 100644 index 00000000..e6e1d689 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/job.go @@ -0,0 +1,125 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "time" +) + +const ( + JOB_TYPE_DATA_RETENTION = "data_retention" + JOB_TYPE_MESSAGE_EXPORT = "message_export" + JOB_TYPE_ELASTICSEARCH_POST_INDEXING = "elasticsearch_post_indexing" + JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION = "elasticsearch_post_aggregation" + JOB_TYPE_BLEVE_POST_INDEXING = "bleve_post_indexing" + JOB_TYPE_LDAP_SYNC = "ldap_sync" + JOB_TYPE_MIGRATIONS = "migrations" + JOB_TYPE_PLUGINS = "plugins" + + JOB_STATUS_PENDING = "pending" + JOB_STATUS_IN_PROGRESS = "in_progress" + JOB_STATUS_SUCCESS = "success" + JOB_STATUS_ERROR = "error" + JOB_STATUS_CANCEL_REQUESTED = "cancel_requested" + JOB_STATUS_CANCELED = "canceled" + JOB_STATUS_WARNING = "warning" +) + +type Job struct { + Id string `json:"id"` + Type string `json:"type"` + Priority int64 `json:"priority"` + CreateAt int64 `json:"create_at"` + StartAt int64 `json:"start_at"` + LastActivityAt int64 `json:"last_activity_at"` + Status string `json:"status"` + Progress int64 `json:"progress"` + Data map[string]string `json:"data"` +} + +func (j *Job) IsValid() *AppError { + if !IsValidId(j.Id) { + return NewAppError("Job.IsValid", "model.job.is_valid.id.app_error", nil, "id="+j.Id, http.StatusBadRequest) + } + + if j.CreateAt == 0 { + return NewAppError("Job.IsValid", "model.job.is_valid.create_at.app_error", nil, "id="+j.Id, http.StatusBadRequest) + } + + switch j.Type { + case JOB_TYPE_DATA_RETENTION: + case JOB_TYPE_ELASTICSEARCH_POST_INDEXING: + case JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION: + case JOB_TYPE_BLEVE_POST_INDEXING: + case JOB_TYPE_LDAP_SYNC: + case JOB_TYPE_MESSAGE_EXPORT: + case JOB_TYPE_MIGRATIONS: + case JOB_TYPE_PLUGINS: + default: + return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest) + } + + switch j.Status { + case JOB_STATUS_PENDING: + case JOB_STATUS_IN_PROGRESS: + case JOB_STATUS_SUCCESS: + case JOB_STATUS_ERROR: + case JOB_STATUS_CANCEL_REQUESTED: + case JOB_STATUS_CANCELED: + default: + return NewAppError("Job.IsValid", "model.job.is_valid.status.app_error", nil, "id="+j.Id, http.StatusBadRequest) + } + + return nil +} + +func (j *Job) ToJson() string { + b, _ := json.Marshal(j) + return string(b) +} + +func JobFromJson(data io.Reader) *Job { + var job Job + if err := json.NewDecoder(data).Decode(&job); err == nil { + return &job + } else { + return nil + } +} + +func JobsToJson(jobs []*Job) string { + b, _ := json.Marshal(jobs) + return string(b) +} + +func JobsFromJson(data io.Reader) []*Job { + var jobs []*Job + if err := json.NewDecoder(data).Decode(&jobs); err == nil { + return jobs + } else { + return nil + } +} + +func (j *Job) DataToJson() string { + b, _ := json.Marshal(j.Data) + return string(b) +} + +type Worker interface { + Run() + Stop() + JobChannel() chan<- Job +} + +type Scheduler interface { + Name() string + JobType() string + Enabled(cfg *Config) bool + NextScheduleTime(cfg *Config, now time.Time, pendingJobs bool, lastSuccessfulJob *Job) *time.Time + ScheduleJob(cfg *Config, pendingJobs bool, lastSuccessfulJob *Job) (*Job, *AppError) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/ldap.go b/vendor/github.com/mattermost/mattermost-server/v5/model/ldap.go new file mode 100644 index 00000000..d5f98f1a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/ldap.go @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + USER_AUTH_SERVICE_LDAP = "ldap" +) diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/license.go b/vendor/github.com/mattermost/mattermost-server/v5/model/license.go new file mode 100644 index 00000000..0504edc0 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/license.go @@ -0,0 +1,278 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" +) + +const ( + EXPIRED_LICENSE_ERROR = "api.license.add_license.expired.app_error" + INVALID_LICENSE_ERROR = "api.license.add_license.invalid.app_error" + LICENSE_GRACE_PERIOD = 1000 * 60 * 60 * 24 * 10 //10 days + LICENSE_RENEWAL_LINK = "https://licensing.mattermost.com/renew" +) + +type LicenseRecord struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + Bytes string `json:"-"` +} + +type License struct { + Id string `json:"id"` + IssuedAt int64 `json:"issued_at"` + StartsAt int64 `json:"starts_at"` + ExpiresAt int64 `json:"expires_at"` + Customer *Customer `json:"customer"` + Features *Features `json:"features"` + SkuName string `json:"sku_name"` + SkuShortName string `json:"sku_short_name"` +} + +type Customer struct { + Id string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Company string `json:"company"` +} + +type TrialLicenseRequest struct { + ServerID string `json:"server_id"` + Email string `json:"email"` + Name string `json:"name"` + SiteURL string `json:"site_url"` + SiteName string `json:"site_name"` + Users int `json:"users"` + TermsAccepted bool `json:"terms_accepted"` + ReceiveEmailsAccepted bool `json:"receive_emails_accepted"` +} + +func (tlr *TrialLicenseRequest) ToJson() string { + b, _ := json.Marshal(tlr) + return string(b) +} + +type Features struct { + Users *int `json:"users"` + LDAP *bool `json:"ldap"` + LDAPGroups *bool `json:"ldap_groups"` + MFA *bool `json:"mfa"` + GoogleOAuth *bool `json:"google_oauth"` + Office365OAuth *bool `json:"office365_oauth"` + Compliance *bool `json:"compliance"` + Cluster *bool `json:"cluster"` + Metrics *bool `json:"metrics"` + MHPNS *bool `json:"mhpns"` + SAML *bool `json:"saml"` + Elasticsearch *bool `json:"elastic_search"` + Announcement *bool `json:"announcement"` + ThemeManagement *bool `json:"theme_management"` + EmailNotificationContents *bool `json:"email_notification_contents"` + DataRetention *bool `json:"data_retention"` + MessageExport *bool `json:"message_export"` + CustomPermissionsSchemes *bool `json:"custom_permissions_schemes"` + CustomTermsOfService *bool `json:"custom_terms_of_service"` + GuestAccounts *bool `json:"guest_accounts"` + GuestAccountsPermissions *bool `json:"guest_accounts_permissions"` + IDLoadedPushNotifications *bool `json:"id_loaded"` + LockTeammateNameDisplay *bool `json:"lock_teammate_name_display"` + EnterprisePlugins *bool `json:"enterprise_plugins"` + + // after we enabled more features we'll need to control them with this + FutureFeatures *bool `json:"future_features"` +} + +func (f *Features) ToMap() map[string]interface{} { + return map[string]interface{}{ + "ldap": *f.LDAP, + "ldap_groups": *f.LDAPGroups, + "mfa": *f.MFA, + "google": *f.GoogleOAuth, + "office365": *f.Office365OAuth, + "compliance": *f.Compliance, + "cluster": *f.Cluster, + "metrics": *f.Metrics, + "mhpns": *f.MHPNS, + "saml": *f.SAML, + "elastic_search": *f.Elasticsearch, + "email_notification_contents": *f.EmailNotificationContents, + "data_retention": *f.DataRetention, + "message_export": *f.MessageExport, + "custom_permissions_schemes": *f.CustomPermissionsSchemes, + "guest_accounts": *f.GuestAccounts, + "guest_accounts_permissions": *f.GuestAccountsPermissions, + "id_loaded": *f.IDLoadedPushNotifications, + "lock_teammate_name_display": *f.LockTeammateNameDisplay, + "enterprise_plugins": *f.EnterprisePlugins, + "future": *f.FutureFeatures, + } +} + +func (f *Features) SetDefaults() { + if f.FutureFeatures == nil { + f.FutureFeatures = NewBool(true) + } + + if f.Users == nil { + f.Users = NewInt(0) + } + + if f.LDAP == nil { + f.LDAP = NewBool(*f.FutureFeatures) + } + + if f.LDAPGroups == nil { + f.LDAPGroups = NewBool(*f.FutureFeatures) + } + + if f.MFA == nil { + f.MFA = NewBool(*f.FutureFeatures) + } + + if f.GoogleOAuth == nil { + f.GoogleOAuth = NewBool(*f.FutureFeatures) + } + + if f.Office365OAuth == nil { + f.Office365OAuth = NewBool(*f.FutureFeatures) + } + + if f.Compliance == nil { + f.Compliance = NewBool(*f.FutureFeatures) + } + + if f.Cluster == nil { + f.Cluster = NewBool(*f.FutureFeatures) + } + + if f.Metrics == nil { + f.Metrics = NewBool(*f.FutureFeatures) + } + + if f.MHPNS == nil { + f.MHPNS = NewBool(*f.FutureFeatures) + } + + if f.SAML == nil { + f.SAML = NewBool(*f.FutureFeatures) + } + + if f.Elasticsearch == nil { + f.Elasticsearch = NewBool(*f.FutureFeatures) + } + + if f.Announcement == nil { + f.Announcement = NewBool(true) + } + + if f.ThemeManagement == nil { + f.ThemeManagement = NewBool(true) + } + + if f.EmailNotificationContents == nil { + f.EmailNotificationContents = NewBool(*f.FutureFeatures) + } + + if f.DataRetention == nil { + f.DataRetention = NewBool(*f.FutureFeatures) + } + + if f.MessageExport == nil { + f.MessageExport = NewBool(*f.FutureFeatures) + } + + if f.CustomPermissionsSchemes == nil { + f.CustomPermissionsSchemes = NewBool(*f.FutureFeatures) + } + + if f.GuestAccounts == nil { + f.GuestAccounts = NewBool(*f.FutureFeatures) + } + + if f.GuestAccountsPermissions == nil { + f.GuestAccountsPermissions = NewBool(*f.FutureFeatures) + } + + if f.CustomTermsOfService == nil { + f.CustomTermsOfService = NewBool(*f.FutureFeatures) + } + + if f.IDLoadedPushNotifications == nil { + f.IDLoadedPushNotifications = NewBool(*f.FutureFeatures) + } + + if f.LockTeammateNameDisplay == nil { + f.LockTeammateNameDisplay = NewBool(*f.FutureFeatures) + } + + if f.EnterprisePlugins == nil { + f.EnterprisePlugins = NewBool(*f.FutureFeatures) + } +} + +func (l *License) IsExpired() bool { + return l.ExpiresAt < GetMillis() +} + +func (l *License) IsPastGracePeriod() bool { + timeDiff := GetMillis() - l.ExpiresAt + return timeDiff > LICENSE_GRACE_PERIOD +} + +func (l *License) IsStarted() bool { + return l.StartsAt < GetMillis() +} + +func (l *License) ToJson() string { + b, _ := json.Marshal(l) + return string(b) +} + +// NewTestLicense returns a license that expires in the future and has the given features. +func NewTestLicense(features ...string) *License { + ret := &License{ + ExpiresAt: GetMillis() + 90*24*60*60*1000, + Customer: &Customer{}, + Features: &Features{}, + } + ret.Features.SetDefaults() + + featureMap := map[string]bool{} + for _, feature := range features { + featureMap[feature] = true + } + featureJson, _ := json.Marshal(featureMap) + json.Unmarshal(featureJson, &ret.Features) + + return ret +} + +func LicenseFromJson(data io.Reader) *License { + var o *License + json.NewDecoder(data).Decode(&o) + return o +} + +func (lr *LicenseRecord) IsValid() *AppError { + if !IsValidId(lr.Id) { + return NewAppError("LicenseRecord.IsValid", "model.license_record.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if lr.CreateAt == 0 { + return NewAppError("LicenseRecord.IsValid", "model.license_record.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) + } + + if len(lr.Bytes) == 0 || len(lr.Bytes) > 10000 { + return NewAppError("LicenseRecord.IsValid", "model.license_record.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (lr *LicenseRecord) PreSave() { + lr.CreateAt = GetMillis() +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/link_metadata.go b/vendor/github.com/mattermost/mattermost-server/v5/model/link_metadata.go new file mode 100644 index 00000000..d20be2cb --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/link_metadata.go @@ -0,0 +1,193 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "hash/fnv" + "net/http" + "time" + "unicode/utf8" + + "github.com/dyatlov/go-opengraph/opengraph" +) + +const ( + LINK_METADATA_TYPE_IMAGE LinkMetadataType = "image" + LINK_METADATA_TYPE_NONE LinkMetadataType = "none" + LINK_METADATA_TYPE_OPENGRAPH LinkMetadataType = "opengraph" + MAX_IMAGES int = 5 +) + +type LinkMetadataType string + +// LinkMetadata stores arbitrary data about a link posted in a message. This includes dimensions of linked images +// and OpenGraph metadata. +type LinkMetadata struct { + // Hash is a value computed from the URL and Timestamp for use as a primary key in the database. + Hash int64 + + URL string + Timestamp int64 + Type LinkMetadataType + + // Data is the actual metadata for the link. It should contain data of one of the following types: + // - *model.PostImage if the linked content is an image + // - *opengraph.OpenGraph if the linked content is an HTML document + // - nil if the linked content has no metadata + Data interface{} +} + +// truncateText ensure string is 300 chars, truncate and add ellipsis +// if it was bigger. +func truncateText(original string) string { + if utf8.RuneCountInString(original) > 300 { + return fmt.Sprintf("%.300s[...]", original) + } + return original +} + +func firstNImages(images []*opengraph.Image, maxImages int) []*opengraph.Image { + if maxImages < 0 { // dont break stuff, if it's weird, go for sane defaults + maxImages = MAX_IMAGES + } + numImages := len(images) + if numImages > maxImages { + return images[0:maxImages] + } + return images +} + +// TruncateOpenGraph ensure OpenGraph metadata doesn't grow too big by +// shortening strings, trimming fields and reducing the number of +// images. +func TruncateOpenGraph(ogdata *opengraph.OpenGraph) *opengraph.OpenGraph { + if ogdata != nil { + empty := &opengraph.OpenGraph{} + ogdata.Title = truncateText(ogdata.Title) + ogdata.Description = truncateText(ogdata.Description) + ogdata.SiteName = truncateText(ogdata.SiteName) + ogdata.Article = empty.Article + ogdata.Book = empty.Book + ogdata.Profile = empty.Profile + ogdata.Determiner = empty.Determiner + ogdata.Locale = empty.Locale + ogdata.LocalesAlternate = empty.LocalesAlternate + ogdata.Images = firstNImages(ogdata.Images, MAX_IMAGES) + ogdata.Audios = empty.Audios + ogdata.Videos = empty.Videos + } + return ogdata +} + +func (o *LinkMetadata) PreSave() { + o.Hash = GenerateLinkMetadataHash(o.URL, o.Timestamp) +} + +func (o *LinkMetadata) IsValid() *AppError { + if o.URL == "" { + return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.url.app_error", nil, "", http.StatusBadRequest) + } + + if o.Timestamp == 0 || !isRoundedToNearestHour(o.Timestamp) { + return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.timestamp.app_error", nil, "", http.StatusBadRequest) + } + + switch o.Type { + case LINK_METADATA_TYPE_IMAGE: + if o.Data == nil { + return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data.app_error", nil, "", http.StatusBadRequest) + } + + if _, ok := o.Data.(*PostImage); !ok { + return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest) + } + case LINK_METADATA_TYPE_NONE: + if o.Data != nil { + return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest) + } + case LINK_METADATA_TYPE_OPENGRAPH: + if o.Data == nil { + return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data.app_error", nil, "", http.StatusBadRequest) + } + + if _, ok := o.Data.(*opengraph.OpenGraph); !ok { + return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest) + } + default: + return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.type.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +// DeserializeDataToConcreteType converts o.Data from JSON into properly structured data. This is intended to be used +// after getting a LinkMetadata object that has been stored in the database. +func (o *LinkMetadata) DeserializeDataToConcreteType() error { + var b []byte + switch t := o.Data.(type) { + case []byte: + // MySQL uses a byte slice for JSON + b = t + case string: + // Postgres uses a string for JSON + b = []byte(t) + } + + if b == nil { + // Data doesn't need to be fixed + return nil + } + + var data interface{} + var err error + + switch o.Type { + case LINK_METADATA_TYPE_IMAGE: + image := &PostImage{} + + err = json.Unmarshal(b, &image) + + data = image + case LINK_METADATA_TYPE_OPENGRAPH: + og := &opengraph.OpenGraph{} + + json.Unmarshal(b, &og) + + data = og + } + + if err != nil { + return err + } + + o.Data = data + + return nil +} + +// FloorToNearestHour takes a timestamp (in milliseconds) and returns it rounded to the previous hour in UTC. +func FloorToNearestHour(ms int64) int64 { + t := time.Unix(0, ms*int64(1000*1000)) + + return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()).UnixNano() / int64(time.Millisecond) +} + +// isRoundedToNearestHour returns true if the given timestamp (in milliseconds) has been rounded to the nearest hour in UTC. +func isRoundedToNearestHour(ms int64) bool { + return FloorToNearestHour(ms) == ms +} + +// GenerateLinkMetadataHash generates a unique hash for a given URL and timestamp for use as a database key. +func GenerateLinkMetadataHash(url string, timestamp int64) int64 { + hash := fnv.New32() + + // Note that we ignore write errors here because the Hash interface says that its Write will never return an error + binary.Write(hash, binary.LittleEndian, timestamp) + hash.Write([]byte(url)) + + return int64(hash.Sum32()) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/manifest.go b/vendor/github.com/mattermost/mattermost-server/v5/model/manifest.go new file mode 100644 index 00000000..7c09830a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/manifest.go @@ -0,0 +1,486 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/blang/semver" + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +type PluginOption struct { + // The display name for the option. + DisplayName string `json:"display_name" yaml:"display_name"` + + // The string value for the option. + Value string `json:"value" yaml:"value"` +} + +type PluginSettingType int + +const ( + Bool PluginSettingType = iota + Dropdown + Generated + Radio + Text + LongText + Number + Username + Custom +) + +type PluginSetting struct { + // The key that the setting will be assigned to in the configuration file. + Key string `json:"key" yaml:"key"` + + // The display name for the setting. + DisplayName string `json:"display_name" yaml:"display_name"` + + // The type of the setting. + // + // "bool" will result in a boolean true or false setting. + // + // "dropdown" will result in a string setting that allows the user to select from a list of + // pre-defined options. + // + // "generated" will result in a string setting that is set to a random, cryptographically secure + // string. + // + // "radio" will result in a string setting that allows the user to select from a short selection + // of pre-defined options. + // + // "text" will result in a string setting that can be typed in manually. + // + // "longtext" will result in a multi line string that can be typed in manually. + // + // "number" will result in in integer setting that can be typed in manually. + // + // "username" will result in a text setting that will autocomplete to a username. + // + // "custom" will result in a custom defined setting and will load the custom component registered for the Web App System Console. + Type string `json:"type" yaml:"type"` + + // The help text to display to the user. Supports Markdown formatting. + HelpText string `json:"help_text" yaml:"help_text"` + + // The help text to display alongside the "Regenerate" button for settings of the "generated" type. + RegenerateHelpText string `json:"regenerate_help_text,omitempty" yaml:"regenerate_help_text,omitempty"` + + // The placeholder to display for "generated", "text", "longtext", "number" and "username" types when blank. + Placeholder string `json:"placeholder" yaml:"placeholder"` + + // The default value of the setting. + Default interface{} `json:"default" yaml:"default"` + + // For "radio" or "dropdown" settings, this is the list of pre-defined options that the user can choose + // from. + Options []*PluginOption `json:"options,omitempty" yaml:"options,omitempty"` +} + +type PluginSettingsSchema struct { + // Optional text to display above the settings. Supports Markdown formatting. + Header string `json:"header" yaml:"header"` + + // Optional text to display below the settings. Supports Markdown formatting. + Footer string `json:"footer" yaml:"footer"` + + // A list of setting definitions. + Settings []*PluginSetting `json:"settings" yaml:"settings"` +} + +// The plugin manifest defines the metadata required to load and present your plugin. The manifest +// file should be named plugin.json or plugin.yaml and placed in the top of your +// plugin bundle. +// +// Example plugin.json: +// +// +// { +// "id": "com.mycompany.myplugin", +// "name": "My Plugin", +// "description": "This is my plugin", +// "homepage_url": "https://example.com", +// "support_url": "https://example.com/support", +// "release_notes_url": "https://example.com/releases/v0.0.1", +// "icon_path": "assets/logo.svg", +// "version": "0.1.0", +// "min_server_version": "5.6.0", +// "server": { +// "executables": { +// "linux-amd64": "server/dist/plugin-linux-amd64", +// "darwin-amd64": "server/dist/plugin-darwin-amd64", +// "windows-amd64": "server/dist/plugin-windows-amd64.exe" +// } +// }, +// "webapp": { +// "bundle_path": "webapp/dist/main.js" +// }, +// "settings_schema": { +// "header": "Some header text", +// "footer": "Some footer text", +// "settings": [{ +// "key": "someKey", +// "display_name": "Enable Extra Feature", +// "type": "bool", +// "help_text": "When true, an extra feature will be enabled!", +// "default": "false" +// }] +// }, +// "props": { +// "someKey": "someData" +// } +// } +type Manifest struct { + // The id is a globally unique identifier that represents your plugin. Ids must be at least + // 3 characters, at most 190 characters and must match ^[a-zA-Z0-9-_\.]+$. + // Reverse-DNS notation using a name you control is a good option, e.g. "com.mycompany.myplugin". + Id string `json:"id" yaml:"id"` + + // The name to be displayed for the plugin. + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // A description of what your plugin is and does. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + + // HomepageURL is an optional link to learn more about the plugin. + HomepageURL string `json:"homepage_url,omitempty" yaml:"homepage_url,omitempty"` + + // SupportURL is an optional URL where plugin issues can be reported. + SupportURL string `json:"support_url,omitempty" yaml:"support_url,omitempty"` + + // ReleaseNotesURL is an optional URL where a changelog for the release can be found. + ReleaseNotesURL string `json:"release_notes_url,omitempty" yaml:"release_notes_url,omitempty"` + + // A relative file path in the bundle that points to the plugins svg icon for use with the Plugin Marketplace. + // This should be relative to the root of your bundle and the location of the manifest file. Bitmap image formats are not supported. + IconPath string `json:"icon_path,omitempty" yaml:"icon_path,omitempty"` + + // A version number for your plugin. Semantic versioning is recommended: http://semver.org + Version string `json:"version" yaml:"version"` + + // The minimum Mattermost server version required for your plugin. + // + // Minimum server version: 5.6 + MinServerVersion string `json:"min_server_version,omitempty" yaml:"min_server_version,omitempty"` + + // Server defines the server-side portion of your plugin. + Server *ManifestServer `json:"server,omitempty" yaml:"server,omitempty"` + + // Backend is a deprecated flag for defining the server-side portion of your plugin. Going forward, use Server instead. + Backend *ManifestServer `json:"backend,omitempty" yaml:"backend,omitempty"` + + // If your plugin extends the web app, you'll need to define webapp. + Webapp *ManifestWebapp `json:"webapp,omitempty" yaml:"webapp,omitempty"` + + // To allow administrators to configure your plugin via the Mattermost system console, you can + // provide your settings schema. + SettingsSchema *PluginSettingsSchema `json:"settings_schema,omitempty" yaml:"settings_schema,omitempty"` + + // Plugins can store any kind of data in Props to allow other plugins to use it. + Props map[string]interface{} `json:"props,omitempty" yaml:"props,omitempty"` + + // RequiredConfig defines any required server configuration fields for the plugin to function properly. + // + // Use the plugin helpers CheckRequiredServerConfiguration method to enforce this. + RequiredConfig *Config `json:"required_configuration,omitempty" yaml:"required_configuration,omitempty"` +} + +type ManifestServer struct { + // Executables are the paths to your executable binaries, specifying multiple entry points + // for different platforms when bundled together in a single plugin. + Executables *ManifestExecutables `json:"executables,omitempty" yaml:"executables,omitempty"` + + // Executable is the path to your executable binary. This should be relative to the root + // of your bundle and the location of the manifest file. + // + // On Windows, this file must have a ".exe" extension. + // + // If your plugin is compiled for multiple platforms, consider bundling them together + // and using the Executables field instead. + Executable string `json:"executable" yaml:"executable"` +} + +type ManifestExecutables struct { + // LinuxAmd64 is the path to your executable binary for the corresponding platform + LinuxAmd64 string `json:"linux-amd64,omitempty" yaml:"linux-amd64,omitempty"` + // DarwinAmd64 is the path to your executable binary for the corresponding platform + DarwinAmd64 string `json:"darwin-amd64,omitempty" yaml:"darwin-amd64,omitempty"` + // WindowsAmd64 is the path to your executable binary for the corresponding platform + // This file must have a ".exe" extension + WindowsAmd64 string `json:"windows-amd64,omitempty" yaml:"windows-amd64,omitempty"` +} + +type ManifestWebapp struct { + // The path to your webapp bundle. This should be relative to the root of your bundle and the + // location of the manifest file. + BundlePath string `json:"bundle_path" yaml:"bundle_path"` + + // BundleHash is the 64-bit FNV-1a hash of the webapp bundle, computed when the plugin is loaded + BundleHash []byte `json:"-"` +} + +func (m *Manifest) ToJson() string { + b, _ := json.Marshal(m) + return string(b) +} + +func ManifestListToJson(m []*Manifest) string { + b, _ := json.Marshal(m) + return string(b) +} + +func ManifestFromJson(data io.Reader) *Manifest { + var m *Manifest + json.NewDecoder(data).Decode(&m) + return m +} + +func ManifestListFromJson(data io.Reader) []*Manifest { + var manifests []*Manifest + json.NewDecoder(data).Decode(&manifests) + return manifests +} + +func (m *Manifest) HasClient() bool { + return m.Webapp != nil +} + +func (m *Manifest) ClientManifest() *Manifest { + cm := new(Manifest) + *cm = *m + cm.Name = "" + cm.Description = "" + cm.Server = nil + if cm.Webapp != nil { + cm.Webapp = new(ManifestWebapp) + *cm.Webapp = *m.Webapp + cm.Webapp.BundlePath = "/static/" + m.Id + "/" + fmt.Sprintf("%s_%x_bundle.js", m.Id, m.Webapp.BundleHash) + } + return cm +} + +// GetExecutableForRuntime returns the path to the executable for the given runtime architecture. +// +// If the manifest defines multiple executables, but none match, or if only a single executable +// is defined, the Executable field will be returned. This method does not guarantee that the +// resulting binary can actually execute on the given platform. +func (m *Manifest) GetExecutableForRuntime(goOs, goArch string) string { + server := m.Server + + // Support the deprecated backend parameter. + if server == nil { + server = m.Backend + } + + if server == nil { + return "" + } + + var executable string + if server.Executables != nil { + if goOs == "linux" && goArch == "amd64" { + executable = server.Executables.LinuxAmd64 + } else if goOs == "darwin" && goArch == "amd64" { + executable = server.Executables.DarwinAmd64 + } else if goOs == "windows" && goArch == "amd64" { + executable = server.Executables.WindowsAmd64 + } + } + + if executable == "" { + executable = server.Executable + } + + return executable +} + +func (m *Manifest) HasServer() bool { + return m.Server != nil || m.Backend != nil +} + +func (m *Manifest) HasWebapp() bool { + return m.Webapp != nil +} + +func (m *Manifest) MeetMinServerVersion(serverVersion string) (bool, error) { + minServerVersion, err := semver.Parse(m.MinServerVersion) + if err != nil { + return false, errors.New("failed to parse MinServerVersion") + } + sv := semver.MustParse(serverVersion) + if sv.LT(minServerVersion) { + return false, nil + } + return true, nil +} + +func (m *Manifest) IsValid() error { + if !IsValidPluginId(m.Id) { + return errors.New("invalid plugin ID") + } + + if m.HomepageURL != "" && !IsValidHttpUrl(m.HomepageURL) { + return errors.New("invalid HomepageURL") + } + + if m.SupportURL != "" && !IsValidHttpUrl(m.SupportURL) { + return errors.New("invalid SupportURL") + } + + if m.ReleaseNotesURL != "" && !IsValidHttpUrl(m.ReleaseNotesURL) { + return errors.New("invalid ReleaseNotesURL") + } + + if m.Version != "" { + _, err := semver.Parse(m.Version) + if err != nil { + return errors.Wrap(err, "failed to parse Version") + } + } + + if m.MinServerVersion != "" { + _, err := semver.Parse(m.MinServerVersion) + if err != nil { + return errors.Wrap(err, "failed to parse MinServerVersion") + } + } + + if m.SettingsSchema != nil { + err := m.SettingsSchema.isValid() + if err != nil { + return errors.Wrap(err, "invalid settings schema") + } + } + + return nil +} + +func (s *PluginSettingsSchema) isValid() error { + for _, setting := range s.Settings { + err := setting.isValid() + if err != nil { + return err + } + } + + return nil +} + +func (s *PluginSetting) isValid() error { + pluginSettingType, err := convertTypeToPluginSettingType(s.Type) + if err != nil { + return err + } + + if s.RegenerateHelpText != "" && pluginSettingType != Generated { + return errors.New("should not set RegenerateHelpText for setting type that is not generated") + } + + if s.Placeholder != "" && !(pluginSettingType == Generated || + pluginSettingType == Text || + pluginSettingType == LongText || + pluginSettingType == Number || + pluginSettingType == Username) { + return errors.New("should not set Placeholder for setting type not in text, generated or username") + } + + if s.Options != nil { + if pluginSettingType != Radio && pluginSettingType != Dropdown { + return errors.New("should not set Options for setting type not in radio or dropdown") + } + + for _, option := range s.Options { + if option.DisplayName == "" || option.Value == "" { + return errors.New("should not have empty Displayname or Value for any option") + } + } + } + + return nil +} + +func convertTypeToPluginSettingType(t string) (PluginSettingType, error) { + var settingType PluginSettingType + switch t { + case "bool": + return Bool, nil + case "dropdown": + return Dropdown, nil + case "generated": + return Generated, nil + case "radio": + return Radio, nil + case "text": + return Text, nil + case "number": + return Number, nil + case "longtext": + return LongText, nil + case "username": + return Username, nil + case "custom": + return Custom, nil + default: + return settingType, errors.New("invalid setting type: " + t) + } +} + +// FindManifest will find and parse the manifest in a given directory. +// +// In all cases other than a does-not-exist error, path is set to the path of the manifest file that was +// found. +// +// Manifests are JSON or YAML files named plugin.json, plugin.yaml, or plugin.yml. +func FindManifest(dir string) (manifest *Manifest, path string, err error) { + for _, name := range []string{"plugin.yml", "plugin.yaml"} { + path = filepath.Join(dir, name) + f, ferr := os.Open(path) + if ferr != nil { + if !os.IsNotExist(ferr) { + return nil, "", ferr + } + continue + } + b, ioerr := ioutil.ReadAll(f) + f.Close() + if ioerr != nil { + return nil, path, ioerr + } + var parsed Manifest + err = yaml.Unmarshal(b, &parsed) + if err != nil { + return nil, path, err + } + manifest = &parsed + manifest.Id = strings.ToLower(manifest.Id) + return manifest, path, nil + } + + path = filepath.Join(dir, "plugin.json") + f, ferr := os.Open(path) + if ferr != nil { + if os.IsNotExist(ferr) { + path = "" + } + return nil, path, ferr + } + defer f.Close() + var parsed Manifest + err = json.NewDecoder(f).Decode(&parsed) + if err != nil { + return nil, path, err + } + manifest = &parsed + manifest.Id = strings.ToLower(manifest.Id) + return manifest, path, nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/marketplace_plugin.go b/vendor/github.com/mattermost/mattermost-server/v5/model/marketplace_plugin.go new file mode 100644 index 00000000..47644513 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/marketplace_plugin.go @@ -0,0 +1,124 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "io" + "net/url" + "strconv" + + "github.com/pkg/errors" +) + +// BaseMarketplacePlugin is a Mattermost plugin received from the Marketplace server. +type BaseMarketplacePlugin struct { + HomepageURL string `json:"homepage_url"` + IconData string `json:"icon_data"` + DownloadURL string `json:"download_url"` + ReleaseNotesURL string `json:"release_notes_url"` + Labels []MarketplaceLabel `json:"labels"` + Signature string `json:"signature"` // Signature represents a signature of a plugin saved in base64 encoding. + Manifest *Manifest `json:"manifest"` +} + +// MarketplaceLabel represents a label shown in the Marketplace UI. +type MarketplaceLabel struct { + Name string `json:"name"` + Description string `json:"description"` + URL string `json:"url"` + Color string `json:"color"` +} + +// MarketplacePlugin is a state aware Marketplace plugin. +type MarketplacePlugin struct { + *BaseMarketplacePlugin + InstalledVersion string `json:"installed_version"` +} + +// BaseMarketplacePluginsFromReader decodes a json-encoded list of plugins from the given io.Reader. +func BaseMarketplacePluginsFromReader(reader io.Reader) ([]*BaseMarketplacePlugin, error) { + plugins := []*BaseMarketplacePlugin{} + decoder := json.NewDecoder(reader) + + if err := decoder.Decode(&plugins); err != nil && err != io.EOF { + return nil, err + } + + return plugins, nil +} + +// MarketplacePluginsFromReader decodes a json-encoded list of plugins from the given io.Reader. +func MarketplacePluginsFromReader(reader io.Reader) ([]*MarketplacePlugin, error) { + plugins := []*MarketplacePlugin{} + decoder := json.NewDecoder(reader) + + if err := decoder.Decode(&plugins); err != nil && err != io.EOF { + return nil, err + } + + return plugins, nil +} + +// DecodeSignature Decodes signature and returns ReadSeeker. +func (plugin *BaseMarketplacePlugin) DecodeSignature() (io.ReadSeeker, error) { + signatureBytes, err := base64.StdEncoding.DecodeString(plugin.Signature) + if err != nil { + return nil, errors.Wrap(err, "Unable to decode base64 signature.") + } + return bytes.NewReader(signatureBytes), nil +} + +// MarketplacePluginFilter describes the parameters to request a list of plugins. +type MarketplacePluginFilter struct { + Page int + PerPage int + Filter string + ServerVersion string + BuildEnterpriseReady bool + EnterprisePlugins bool + LocalOnly bool +} + +// ApplyToURL modifies the given url to include query string parameters for the request. +func (filter *MarketplacePluginFilter) ApplyToURL(u *url.URL) { + q := u.Query() + q.Add("page", strconv.Itoa(filter.Page)) + if filter.PerPage > 0 { + q.Add("per_page", strconv.Itoa(filter.PerPage)) + } + q.Add("filter", filter.Filter) + q.Add("server_version", filter.ServerVersion) + q.Add("build_enterprise_ready", strconv.FormatBool(filter.BuildEnterpriseReady)) + q.Add("enterprise_plugins", strconv.FormatBool(filter.EnterprisePlugins)) + q.Add("local_only", strconv.FormatBool(filter.LocalOnly)) + u.RawQuery = q.Encode() +} + +// InstallMarketplacePluginRequest struct describes parameters of the requested plugin. +type InstallMarketplacePluginRequest struct { + Id string `json:"id"` + Version string `json:"version"` +} + +// PluginRequestFromReader decodes a json-encoded plugin request from the given io.Reader. +func PluginRequestFromReader(reader io.Reader) (*InstallMarketplacePluginRequest, error) { + var r *InstallMarketplacePluginRequest + err := json.NewDecoder(reader).Decode(&r) + if err != nil { + return nil, err + } + return r, nil +} + +// ToJson method will return json from plugin request. +func (r *InstallMarketplacePluginRequest) ToJson() (string, error) { + b, err := json.Marshal(r) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/mention_map.go b/vendor/github.com/mattermost/mattermost-server/v5/model/mention_map.go new file mode 100644 index 00000000..2f3444dd --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/mention_map.go @@ -0,0 +1,80 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "net/url" +) + +type UserMentionMap map[string]string +type ChannelMentionMap map[string]string + +const ( + userMentionsKey = "user_mentions" + userMentionsIdsKey = "user_mentions_ids" + channelMentionsKey = "channel_mentions" + channelMentionsIdsKey = "channel_mentions_ids" +) + +func UserMentionMapFromURLValues(values url.Values) (UserMentionMap, error) { + return mentionsFromURLValues(values, userMentionsKey, userMentionsIdsKey) +} + +func (m UserMentionMap) ToURLValues() url.Values { + return mentionsToURLValues(m, userMentionsKey, userMentionsIdsKey) +} + +func ChannelMentionMapFromURLValues(values url.Values) (ChannelMentionMap, error) { + return mentionsFromURLValues(values, channelMentionsKey, channelMentionsIdsKey) +} + +func (m ChannelMentionMap) ToURLValues() url.Values { + return mentionsToURLValues(m, channelMentionsKey, channelMentionsIdsKey) +} + +func mentionsFromURLValues(values url.Values, mentionKey, idKey string) (map[string]string, error) { + mentions, mentionsOk := values[mentionKey] + ids, idsOk := values[idKey] + + if !mentionsOk && !idsOk { + return map[string]string{}, nil + } + + if !mentionsOk { + return nil, fmt.Errorf("%s key not found", mentionKey) + } + + if !idsOk { + return nil, fmt.Errorf("%s key not found", idKey) + } + + if len(mentions) != len(ids) { + return nil, fmt.Errorf("keys %s and %s have different length", mentionKey, idKey) + } + + mentionsMap := make(map[string]string) + for i, mention := range mentions { + id := ids[i] + + if oldId, ok := mentionsMap[mention]; ok && oldId != id { + return nil, fmt.Errorf("key %s has two different values: %s and %s", mention, oldId, id) + } + + mentionsMap[mention] = id + } + + return mentionsMap, nil +} + +func mentionsToURLValues(mentions map[string]string, mentionKey, idKey string) url.Values { + values := url.Values{} + + for mention, id := range mentions { + values.Add(mentionKey, mention) + values.Add(idKey, id) + } + + return values +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/message_export.go b/vendor/github.com/mattermost/mattermost-server/v5/model/message_export.go new file mode 100644 index 00000000..88108e2e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/message_export.go @@ -0,0 +1,31 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type MessageExport struct { + TeamId *string + TeamName *string + TeamDisplayName *string + + ChannelId *string + ChannelName *string + ChannelDisplayName *string + ChannelType *string + + UserId *string + UserEmail *string + Username *string + IsBot bool + + PostId *string + PostCreateAt *int64 + PostUpdateAt *int64 + PostDeleteAt *int64 + PostMessage *string + PostType *string + PostRootId *string + PostProps *string + PostOriginalId *string + PostFileIds StringArray +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/mfa_secret.go b/vendor/github.com/mattermost/mattermost-server/v5/model/mfa_secret.go new file mode 100644 index 00000000..979ff342 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/mfa_secret.go @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type MfaSecret struct { + Secret string `json:"secret"` + QRCode string `json:"qr_code"` +} + +func (me *MfaSecret) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func MfaSecretFromJson(data io.Reader) *MfaSecret { + var me *MfaSecret + json.NewDecoder(data).Decode(&me) + return me +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/migration.go b/vendor/github.com/mattermost/mattermost-server/v5/model/migration.go new file mode 100644 index 00000000..7dd08bef --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/migration.go @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2 = "migration_advanced_permissions_phase_2" + + MIGRATION_KEY_EMOJI_PERMISSIONS_SPLIT = "emoji_permissions_split" + MIGRATION_KEY_WEBHOOK_PERMISSIONS_SPLIT = "webhook_permissions_split" + MIGRATION_KEY_LIST_JOIN_PUBLIC_PRIVATE_TEAMS = "list_join_public_private_teams" + MIGRATION_KEY_REMOVE_PERMANENT_DELETE_USER = "remove_permanent_delete_user" + MIGRATION_KEY_ADD_BOT_PERMISSIONS = "add_bot_permissions" + MIGRATION_KEY_APPLY_CHANNEL_MANAGE_DELETE_TO_CHANNEL_USER = "apply_channel_manage_delete_to_channel_user" + MIGRATION_KEY_REMOVE_CHANNEL_MANAGE_DELETE_FROM_TEAM_USER = "remove_channel_manage_delete_from_team_user" + MIGRATION_KEY_VIEW_MEMBERS_NEW_PERMISSION = "view_members_new_permission" + MIGRATION_KEY_ADD_MANAGE_GUESTS_PERMISSIONS = "add_manage_guests_permissions" + MIGRATION_KEY_CHANNEL_MODERATIONS_PERMISSIONS = "channel_moderations_permissions" + MIGRATION_KEY_ADD_USE_GROUP_MENTIONS_PERMISSION = "add_use_group_mentions_permission" +) diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/oauth.go b/vendor/github.com/mattermost/mattermost-server/v5/model/oauth.go new file mode 100644 index 00000000..4a345a6e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/oauth.go @@ -0,0 +1,151 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "unicode/utf8" +) + +const ( + OAUTH_ACTION_SIGNUP = "signup" + OAUTH_ACTION_LOGIN = "login" + OAUTH_ACTION_EMAIL_TO_SSO = "email_to_sso" + OAUTH_ACTION_SSO_TO_EMAIL = "sso_to_email" + OAUTH_ACTION_MOBILE = "mobile" +) + +type OAuthApp struct { + Id string `json:"id"` + CreatorId string `json:"creator_id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + ClientSecret string `json:"client_secret"` + Name string `json:"name"` + Description string `json:"description"` + IconURL string `json:"icon_url"` + CallbackUrls StringArray `json:"callback_urls"` + Homepage string `json:"homepage"` + IsTrusted bool `json:"is_trusted"` +} + +// IsValid validates the app and returns an error if it isn't configured +// correctly. +func (a *OAuthApp) IsValid() *AppError { + + if !IsValidId(a.Id) { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.app_id.app_error", nil, "", http.StatusBadRequest) + } + + if a.CreateAt == 0 { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.create_at.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + + if a.UpdateAt == 0 { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.update_at.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + + if !IsValidId(a.CreatorId) { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.creator_id.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + + if len(a.ClientSecret) == 0 || len(a.ClientSecret) > 128 { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.client_secret.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + + if len(a.Name) == 0 || len(a.Name) > 64 { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.name.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + + if len(a.CallbackUrls) == 0 || len(fmt.Sprintf("%s", a.CallbackUrls)) > 1024 { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + + for _, callback := range a.CallbackUrls { + if !IsValidHttpUrl(callback) { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "", http.StatusBadRequest) + } + } + + if len(a.Homepage) == 0 || len(a.Homepage) > 256 || !IsValidHttpUrl(a.Homepage) { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.homepage.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(a.Description) > 512 { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.description.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + + if len(a.IconURL) > 0 { + if len(a.IconURL) > 512 || !IsValidHttpUrl(a.IconURL) { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.icon_url.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + } + + return nil +} + +// PreSave will set the Id and ClientSecret if missing. It will also fill +// in the CreateAt, UpdateAt times. It should be run before saving the app to the db. +func (a *OAuthApp) PreSave() { + if a.Id == "" { + a.Id = NewId() + } + + if a.ClientSecret == "" { + a.ClientSecret = NewId() + } + + a.CreateAt = GetMillis() + a.UpdateAt = a.CreateAt +} + +// PreUpdate should be run before updating the app in the db. +func (a *OAuthApp) PreUpdate() { + a.UpdateAt = GetMillis() +} + +func (a *OAuthApp) ToJson() string { + b, _ := json.Marshal(a) + return string(b) +} + +// Generate a valid strong etag so the browser can cache the results +func (a *OAuthApp) Etag() string { + return Etag(a.Id, a.UpdateAt) +} + +// Remove any private data from the app object +func (a *OAuthApp) Sanitize() { + a.ClientSecret = "" +} + +func (a *OAuthApp) IsValidRedirectURL(url string) bool { + for _, u := range a.CallbackUrls { + if u == url { + return true + } + } + + return false +} + +func OAuthAppFromJson(data io.Reader) *OAuthApp { + var app *OAuthApp + json.NewDecoder(data).Decode(&app) + return app +} + +func OAuthAppListToJson(l []*OAuthApp) string { + b, _ := json.Marshal(l) + return string(b) +} + +func OAuthAppListFromJson(data io.Reader) []*OAuthApp { + var o []*OAuthApp + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/outgoing_webhook.go b/vendor/github.com/mattermost/mattermost-server/v5/model/outgoing_webhook.go new file mode 100644 index 00000000..f4278de0 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/outgoing_webhook.go @@ -0,0 +1,264 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" +) + +type OutgoingWebhook struct { + Id string `json:"id"` + Token string `json:"token"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + CreatorId string `json:"creator_id"` + ChannelId string `json:"channel_id"` + TeamId string `json:"team_id"` + TriggerWords StringArray `json:"trigger_words"` + TriggerWhen int `json:"trigger_when"` + CallbackURLs StringArray `json:"callback_urls"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + ContentType string `json:"content_type"` + Username string `json:"username"` + IconURL string `json:"icon_url"` +} + +type OutgoingWebhookPayload struct { + Token string `json:"token"` + TeamId string `json:"team_id"` + TeamDomain string `json:"team_domain"` + ChannelId string `json:"channel_id"` + ChannelName string `json:"channel_name"` + Timestamp int64 `json:"timestamp"` + UserId string `json:"user_id"` + UserName string `json:"user_name"` + PostId string `json:"post_id"` + Text string `json:"text"` + TriggerWord string `json:"trigger_word"` + FileIds string `json:"file_ids"` +} + +type OutgoingWebhookResponse struct { + Text *string `json:"text"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + Props StringInterface `json:"props"` + Attachments []*SlackAttachment `json:"attachments"` + Type string `json:"type"` + ResponseType string `json:"response_type"` +} + +const OUTGOING_HOOK_RESPONSE_TYPE_COMMENT = "comment" + +func (o *OutgoingWebhookPayload) ToJSON() string { + b, _ := json.Marshal(o) + return string(b) +} + +func (o *OutgoingWebhookPayload) ToFormValues() string { + v := url.Values{} + v.Set("token", o.Token) + v.Set("team_id", o.TeamId) + v.Set("team_domain", o.TeamDomain) + v.Set("channel_id", o.ChannelId) + v.Set("channel_name", o.ChannelName) + v.Set("timestamp", strconv.FormatInt(o.Timestamp/1000, 10)) + v.Set("user_id", o.UserId) + v.Set("user_name", o.UserName) + v.Set("post_id", o.PostId) + v.Set("text", o.Text) + v.Set("trigger_word", o.TriggerWord) + v.Set("file_ids", o.FileIds) + + return v.Encode() +} + +func (o *OutgoingWebhook) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func OutgoingWebhookFromJson(data io.Reader) *OutgoingWebhook { + var o *OutgoingWebhook + json.NewDecoder(data).Decode(&o) + return o +} + +func OutgoingWebhookListToJson(l []*OutgoingWebhook) string { + b, _ := json.Marshal(l) + return string(b) +} + +func OutgoingWebhookListFromJson(data io.Reader) []*OutgoingWebhook { + var o []*OutgoingWebhook + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *OutgoingWebhookResponse) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func OutgoingWebhookResponseFromJson(data io.Reader) (*OutgoingWebhookResponse, error) { + var o *OutgoingWebhookResponse + err := json.NewDecoder(data).Decode(&o) + return o, err +} + +func (o *OutgoingWebhook) IsValid() *AppError { + + if !IsValidId(o.Id) { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Token) != 26 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.token.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if o.UpdateAt == 0 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if !IsValidId(o.CreatorId) { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.ChannelId) != 0 && !IsValidId(o.ChannelId) { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.TeamId) { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.team_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(fmt.Sprintf("%s", o.TriggerWords)) > 1024 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.words.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.TriggerWords) != 0 { + for _, triggerWord := range o.TriggerWords { + if len(triggerWord) == 0 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.trigger_words.app_error", nil, "", http.StatusBadRequest) + } + } + } + + if len(o.CallbackURLs) == 0 || len(fmt.Sprintf("%s", o.CallbackURLs)) > 1024 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.callback.app_error", nil, "", http.StatusBadRequest) + } + + for _, callback := range o.CallbackURLs { + if !IsValidHttpUrl(callback) { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.url.app_error", nil, "", http.StatusBadRequest) + } + } + + if len(o.DisplayName) > 64 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.display_name.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Description) > 500 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.description.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.ContentType) > 128 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "", http.StatusBadRequest) + } + + if o.TriggerWhen > 1 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.Username) > 64 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.username.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.IconURL) > 1024 { + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.icon_url.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (o *OutgoingWebhook) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + if o.Token == "" { + o.Token = NewId() + } + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt +} + +func (o *OutgoingWebhook) PreUpdate() { + o.UpdateAt = GetMillis() +} + +func (o *OutgoingWebhook) TriggerWordExactMatch(word string) bool { + if len(word) == 0 { + return false + } + + for _, trigger := range o.TriggerWords { + if trigger == word { + return true + } + } + + return false +} + +func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool { + if len(word) == 0 { + return false + } + + for _, trigger := range o.TriggerWords { + if strings.HasPrefix(word, trigger) { + return true + } + } + + return false +} + +func (o *OutgoingWebhook) GetTriggerWord(word string, isExactMatch bool) (triggerWord string) { + if len(word) == 0 { + return + } + + if isExactMatch { + for _, trigger := range o.TriggerWords { + if trigger == word { + triggerWord = trigger + break + } + } + } else { + for _, trigger := range o.TriggerWords { + if strings.HasPrefix(word, trigger) { + triggerWord = trigger + break + } + } + } + + return triggerWord +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/permission.go b/vendor/github.com/mattermost/mattermost-server/v5/model/permission.go new file mode 100644 index 00000000..cc3c5a70 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/permission.go @@ -0,0 +1,676 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + PERMISSION_SCOPE_SYSTEM = "system_scope" + PERMISSION_SCOPE_TEAM = "team_scope" + PERMISSION_SCOPE_CHANNEL = "channel_scope" +) + +type Permission struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Scope string `json:"scope"` +} + +var PERMISSION_INVITE_USER *Permission +var PERMISSION_ADD_USER_TO_TEAM *Permission +var PERMISSION_USE_SLASH_COMMANDS *Permission +var PERMISSION_MANAGE_SLASH_COMMANDS *Permission +var PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS *Permission +var PERMISSION_CREATE_PUBLIC_CHANNEL *Permission +var PERMISSION_CREATE_PRIVATE_CHANNEL *Permission +var PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS *Permission +var PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS *Permission +var PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE *Permission +var PERMISSION_MANAGE_ROLES *Permission +var PERMISSION_MANAGE_TEAM_ROLES *Permission +var PERMISSION_MANAGE_CHANNEL_ROLES *Permission +var PERMISSION_CREATE_DIRECT_CHANNEL *Permission +var PERMISSION_CREATE_GROUP_CHANNEL *Permission +var PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES *Permission +var PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES *Permission +var PERMISSION_LIST_PUBLIC_TEAMS *Permission +var PERMISSION_JOIN_PUBLIC_TEAMS *Permission +var PERMISSION_LIST_PRIVATE_TEAMS *Permission +var PERMISSION_JOIN_PRIVATE_TEAMS *Permission +var PERMISSION_LIST_TEAM_CHANNELS *Permission +var PERMISSION_JOIN_PUBLIC_CHANNELS *Permission +var PERMISSION_DELETE_PUBLIC_CHANNEL *Permission +var PERMISSION_DELETE_PRIVATE_CHANNEL *Permission +var PERMISSION_EDIT_OTHER_USERS *Permission +var PERMISSION_READ_CHANNEL *Permission +var PERMISSION_READ_PUBLIC_CHANNEL *Permission +var PERMISSION_ADD_REACTION *Permission +var PERMISSION_REMOVE_REACTION *Permission +var PERMISSION_REMOVE_OTHERS_REACTIONS *Permission +var PERMISSION_PERMANENT_DELETE_USER *Permission +var PERMISSION_UPLOAD_FILE *Permission +var PERMISSION_GET_PUBLIC_LINK *Permission +var PERMISSION_MANAGE_WEBHOOKS *Permission +var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission +var PERMISSION_MANAGE_INCOMING_WEBHOOKS *Permission +var PERMISSION_MANAGE_OUTGOING_WEBHOOKS *Permission +var PERMISSION_MANAGE_OTHERS_INCOMING_WEBHOOKS *Permission +var PERMISSION_MANAGE_OTHERS_OUTGOING_WEBHOOKS *Permission +var PERMISSION_MANAGE_OAUTH *Permission +var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission +var PERMISSION_MANAGE_EMOJIS *Permission +var PERMISSION_MANAGE_OTHERS_EMOJIS *Permission +var PERMISSION_CREATE_EMOJIS *Permission +var PERMISSION_DELETE_EMOJIS *Permission +var PERMISSION_DELETE_OTHERS_EMOJIS *Permission +var PERMISSION_CREATE_POST *Permission +var PERMISSION_CREATE_POST_PUBLIC *Permission +var PERMISSION_CREATE_POST_EPHEMERAL *Permission +var PERMISSION_EDIT_POST *Permission +var PERMISSION_EDIT_OTHERS_POSTS *Permission +var PERMISSION_DELETE_POST *Permission +var PERMISSION_DELETE_OTHERS_POSTS *Permission +var PERMISSION_REMOVE_USER_FROM_TEAM *Permission +var PERMISSION_CREATE_TEAM *Permission +var PERMISSION_MANAGE_TEAM *Permission +var PERMISSION_IMPORT_TEAM *Permission +var PERMISSION_VIEW_TEAM *Permission +var PERMISSION_LIST_USERS_WITHOUT_TEAM *Permission +var PERMISSION_MANAGE_JOBS *Permission +var PERMISSION_CREATE_USER_ACCESS_TOKEN *Permission +var PERMISSION_READ_USER_ACCESS_TOKEN *Permission +var PERMISSION_REVOKE_USER_ACCESS_TOKEN *Permission +var PERMISSION_CREATE_BOT *Permission +var PERMISSION_ASSIGN_BOT *Permission +var PERMISSION_READ_BOTS *Permission +var PERMISSION_READ_OTHERS_BOTS *Permission +var PERMISSION_MANAGE_BOTS *Permission +var PERMISSION_MANAGE_OTHERS_BOTS *Permission +var PERMISSION_VIEW_MEMBERS *Permission +var PERMISSION_INVITE_GUEST *Permission +var PERMISSION_PROMOTE_GUEST *Permission +var PERMISSION_DEMOTE_TO_GUEST *Permission +var PERMISSION_USE_CHANNEL_MENTIONS *Permission +var PERMISSION_USE_GROUP_MENTIONS *Permission + +// General permission that encompasses all system admin functions +// in the future this could be broken up to allow access to some +// admin functions but not others +var PERMISSION_MANAGE_SYSTEM *Permission + +var ALL_PERMISSIONS []*Permission + +var CHANNEL_MODERATED_PERMISSIONS []string +var CHANNEL_MODERATED_PERMISSIONS_MAP map[string]string + +func initializePermissions() { + PERMISSION_INVITE_USER = &Permission{ + "invite_user", + "authentication.permissions.team_invite_user.name", + "authentication.permissions.team_invite_user.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_ADD_USER_TO_TEAM = &Permission{ + "add_user_to_team", + "authentication.permissions.add_user_to_team.name", + "authentication.permissions.add_user_to_team.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_USE_SLASH_COMMANDS = &Permission{ + "use_slash_commands", + "authentication.permissions.team_use_slash_commands.name", + "authentication.permissions.team_use_slash_commands.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_MANAGE_SLASH_COMMANDS = &Permission{ + "manage_slash_commands", + "authentication.permissions.manage_slash_commands.name", + "authentication.permissions.manage_slash_commands.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS = &Permission{ + "manage_others_slash_commands", + "authentication.permissions.manage_others_slash_commands.name", + "authentication.permissions.manage_others_slash_commands.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_CREATE_PUBLIC_CHANNEL = &Permission{ + "create_public_channel", + "authentication.permissions.create_public_channel.name", + "authentication.permissions.create_public_channel.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_CREATE_PRIVATE_CHANNEL = &Permission{ + "create_private_channel", + "authentication.permissions.create_private_channel.name", + "authentication.permissions.create_private_channel.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS = &Permission{ + "manage_public_channel_members", + "authentication.permissions.manage_public_channel_members.name", + "authentication.permissions.manage_public_channel_members.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS = &Permission{ + "manage_private_channel_members", + "authentication.permissions.manage_private_channel_members.name", + "authentication.permissions.manage_private_channel_members.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE = &Permission{ + "assign_system_admin_role", + "authentication.permissions.assign_system_admin_role.name", + "authentication.permissions.assign_system_admin_role.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_ROLES = &Permission{ + "manage_roles", + "authentication.permissions.manage_roles.name", + "authentication.permissions.manage_roles.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_TEAM_ROLES = &Permission{ + "manage_team_roles", + "authentication.permissions.manage_team_roles.name", + "authentication.permissions.manage_team_roles.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_CHANNEL_ROLES = &Permission{ + "manage_channel_roles", + "authentication.permissions.manage_channel_roles.name", + "authentication.permissions.manage_channel_roles.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_MANAGE_SYSTEM = &Permission{ + "manage_system", + "authentication.permissions.manage_system.name", + "authentication.permissions.manage_system.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_CREATE_DIRECT_CHANNEL = &Permission{ + "create_direct_channel", + "authentication.permissions.create_direct_channel.name", + "authentication.permissions.create_direct_channel.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_CREATE_GROUP_CHANNEL = &Permission{ + "create_group_channel", + "authentication.permissions.create_group_channel.name", + "authentication.permissions.create_group_channel.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES = &Permission{ + "manage_public_channel_properties", + "authentication.permissions.manage_public_channel_properties.name", + "authentication.permissions.manage_public_channel_properties.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES = &Permission{ + "manage_private_channel_properties", + "authentication.permissions.manage_private_channel_properties.name", + "authentication.permissions.manage_private_channel_properties.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_LIST_PUBLIC_TEAMS = &Permission{ + "list_public_teams", + "authentication.permissions.list_public_teams.name", + "authentication.permissions.list_public_teams.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_JOIN_PUBLIC_TEAMS = &Permission{ + "join_public_teams", + "authentication.permissions.join_public_teams.name", + "authentication.permissions.join_public_teams.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_LIST_PRIVATE_TEAMS = &Permission{ + "list_private_teams", + "authentication.permissions.list_private_teams.name", + "authentication.permissions.list_private_teams.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_JOIN_PRIVATE_TEAMS = &Permission{ + "join_private_teams", + "authentication.permissions.join_private_teams.name", + "authentication.permissions.join_private_teams.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_LIST_TEAM_CHANNELS = &Permission{ + "list_team_channels", + "authentication.permissions.list_team_channels.name", + "authentication.permissions.list_team_channels.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_JOIN_PUBLIC_CHANNELS = &Permission{ + "join_public_channels", + "authentication.permissions.join_public_channels.name", + "authentication.permissions.join_public_channels.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_DELETE_PUBLIC_CHANNEL = &Permission{ + "delete_public_channel", + "authentication.permissions.delete_public_channel.name", + "authentication.permissions.delete_public_channel.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_DELETE_PRIVATE_CHANNEL = &Permission{ + "delete_private_channel", + "authentication.permissions.delete_private_channel.name", + "authentication.permissions.delete_private_channel.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_EDIT_OTHER_USERS = &Permission{ + "edit_other_users", + "authentication.permissions.edit_other_users.name", + "authentication.permissions.edit_other_users.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_READ_CHANNEL = &Permission{ + "read_channel", + "authentication.permissions.read_channel.name", + "authentication.permissions.read_channel.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_READ_PUBLIC_CHANNEL = &Permission{ + "read_public_channel", + "authentication.permissions.read_public_channel.name", + "authentication.permissions.read_public_channel.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_ADD_REACTION = &Permission{ + "add_reaction", + "authentication.permissions.add_reaction.name", + "authentication.permissions.add_reaction.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_REMOVE_REACTION = &Permission{ + "remove_reaction", + "authentication.permissions.remove_reaction.name", + "authentication.permissions.remove_reaction.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_REMOVE_OTHERS_REACTIONS = &Permission{ + "remove_others_reactions", + "authentication.permissions.remove_others_reactions.name", + "authentication.permissions.remove_others_reactions.description", + PERMISSION_SCOPE_CHANNEL, + } + // DEPRECATED + PERMISSION_PERMANENT_DELETE_USER = &Permission{ + "permanent_delete_user", + "authentication.permissions.permanent_delete_user.name", + "authentication.permissions.permanent_delete_user.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_UPLOAD_FILE = &Permission{ + "upload_file", + "authentication.permissions.upload_file.name", + "authentication.permissions.upload_file.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_GET_PUBLIC_LINK = &Permission{ + "get_public_link", + "authentication.permissions.get_public_link.name", + "authentication.permissions.get_public_link.description", + PERMISSION_SCOPE_SYSTEM, + } + // DEPRECATED + PERMISSION_MANAGE_WEBHOOKS = &Permission{ + "manage_webhooks", + "authentication.permissions.manage_webhooks.name", + "authentication.permissions.manage_webhooks.description", + PERMISSION_SCOPE_TEAM, + } + // DEPRECATED + PERMISSION_MANAGE_OTHERS_WEBHOOKS = &Permission{ + "manage_others_webhooks", + "authentication.permissions.manage_others_webhooks.name", + "authentication.permissions.manage_others_webhooks.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_INCOMING_WEBHOOKS = &Permission{ + "manage_incoming_webhooks", + "authentication.permissions.manage_incoming_webhooks.name", + "authentication.permissions.manage_incoming_webhooks.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_OUTGOING_WEBHOOKS = &Permission{ + "manage_outgoing_webhooks", + "authentication.permissions.manage_outgoing_webhooks.name", + "authentication.permissions.manage_outgoing_webhooks.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_OTHERS_INCOMING_WEBHOOKS = &Permission{ + "manage_others_incoming_webhooks", + "authentication.permissions.manage_others_incoming_webhooks.name", + "authentication.permissions.manage_others_incoming_webhooks.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_OTHERS_OUTGOING_WEBHOOKS = &Permission{ + "manage_others_outgoing_webhooks", + "authentication.permissions.manage_others_outgoing_webhooks.name", + "authentication.permissions.manage_others_outgoing_webhooks.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_MANAGE_OAUTH = &Permission{ + "manage_oauth", + "authentication.permissions.manage_oauth.name", + "authentication.permissions.manage_oauth.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH = &Permission{ + "manage_system_wide_oauth", + "authentication.permissions.manage_system_wide_oauth.name", + "authentication.permissions.manage_system_wide_oauth.description", + PERMISSION_SCOPE_SYSTEM, + } + // DEPRECATED + PERMISSION_MANAGE_EMOJIS = &Permission{ + "manage_emojis", + "authentication.permissions.manage_emojis.name", + "authentication.permissions.manage_emojis.description", + PERMISSION_SCOPE_TEAM, + } + // DEPRECATED + PERMISSION_MANAGE_OTHERS_EMOJIS = &Permission{ + "manage_others_emojis", + "authentication.permissions.manage_others_emojis.name", + "authentication.permissions.manage_others_emojis.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_CREATE_EMOJIS = &Permission{ + "create_emojis", + "authentication.permissions.create_emojis.name", + "authentication.permissions.create_emojis.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_DELETE_EMOJIS = &Permission{ + "delete_emojis", + "authentication.permissions.delete_emojis.name", + "authentication.permissions.delete_emojis.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_DELETE_OTHERS_EMOJIS = &Permission{ + "delete_others_emojis", + "authentication.permissions.delete_others_emojis.name", + "authentication.permissions.delete_others_emojis.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_CREATE_POST = &Permission{ + "create_post", + "authentication.permissions.create_post.name", + "authentication.permissions.create_post.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_CREATE_POST_PUBLIC = &Permission{ + "create_post_public", + "authentication.permissions.create_post_public.name", + "authentication.permissions.create_post_public.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_CREATE_POST_EPHEMERAL = &Permission{ + "create_post_ephemeral", + "authentication.permissions.create_post_ephemeral.name", + "authentication.permissions.create_post_ephemeral.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_EDIT_POST = &Permission{ + "edit_post", + "authentication.permissions.edit_post.name", + "authentication.permissions.edit_post.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_EDIT_OTHERS_POSTS = &Permission{ + "edit_others_posts", + "authentication.permissions.edit_others_posts.name", + "authentication.permissions.edit_others_posts.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_DELETE_POST = &Permission{ + "delete_post", + "authentication.permissions.delete_post.name", + "authentication.permissions.delete_post.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_DELETE_OTHERS_POSTS = &Permission{ + "delete_others_posts", + "authentication.permissions.delete_others_posts.name", + "authentication.permissions.delete_others_posts.description", + PERMISSION_SCOPE_CHANNEL, + } + PERMISSION_REMOVE_USER_FROM_TEAM = &Permission{ + "remove_user_from_team", + "authentication.permissions.remove_user_from_team.name", + "authentication.permissions.remove_user_from_team.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_CREATE_TEAM = &Permission{ + "create_team", + "authentication.permissions.create_team.name", + "authentication.permissions.create_team.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_TEAM = &Permission{ + "manage_team", + "authentication.permissions.manage_team.name", + "authentication.permissions.manage_team.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_IMPORT_TEAM = &Permission{ + "import_team", + "authentication.permissions.import_team.name", + "authentication.permissions.import_team.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_VIEW_TEAM = &Permission{ + "view_team", + "authentication.permissions.view_team.name", + "authentication.permissions.view_team.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{ + "list_users_without_team", + "authentication.permissions.list_users_without_team.name", + "authentication.permissions.list_users_without_team.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_CREATE_USER_ACCESS_TOKEN = &Permission{ + "create_user_access_token", + "authentication.permissions.create_user_access_token.name", + "authentication.permissions.create_user_access_token.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_READ_USER_ACCESS_TOKEN = &Permission{ + "read_user_access_token", + "authentication.permissions.read_user_access_token.name", + "authentication.permissions.read_user_access_token.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_REVOKE_USER_ACCESS_TOKEN = &Permission{ + "revoke_user_access_token", + "authentication.permissions.revoke_user_access_token.name", + "authentication.permissions.revoke_user_access_token.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_CREATE_BOT = &Permission{ + "create_bot", + "authentication.permissions.create_bot.name", + "authentication.permissions.create_bot.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_ASSIGN_BOT = &Permission{ + "assign_bot", + "authentication.permissions.assign_bot.name", + "authentication.permissions.assign_bot.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_READ_BOTS = &Permission{ + "read_bots", + "authentication.permissions.read_bots.name", + "authentication.permissions.read_bots.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_READ_OTHERS_BOTS = &Permission{ + "read_others_bots", + "authentication.permissions.read_others_bots.name", + "authentication.permissions.read_others_bots.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_BOTS = &Permission{ + "manage_bots", + "authentication.permissions.manage_bots.name", + "authentication.permissions.manage_bots.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_OTHERS_BOTS = &Permission{ + "manage_others_bots", + "authentication.permissions.manage_others_bots.name", + "authentication.permissions.manage_others_bots.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_MANAGE_JOBS = &Permission{ + "manage_jobs", + "authentication.permisssions.manage_jobs.name", + "authentication.permisssions.manage_jobs.description", + PERMISSION_SCOPE_SYSTEM, + } + PERMISSION_VIEW_MEMBERS = &Permission{ + "view_members", + "authentication.permisssions.view_members.name", + "authentication.permisssions.view_members.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_INVITE_GUEST = &Permission{ + "invite_guest", + "authentication.permissions.invite_guest.name", + "authentication.permissions.invite_guest.description", + PERMISSION_SCOPE_TEAM, + } + PERMISSION_PROMOTE_GUEST = &Permission{ + "promote_guest", + "authentication.permissions.promote_guest.name", + "authentication.permissions.promote_guest.description", + PERMISSION_SCOPE_SYSTEM, + } + + PERMISSION_DEMOTE_TO_GUEST = &Permission{ + "demote_to_guest", + "authentication.permissions.demote_to_guest.name", + "authentication.permissions.demote_to_guest.description", + PERMISSION_SCOPE_SYSTEM, + } + + PERMISSION_USE_CHANNEL_MENTIONS = &Permission{ + "use_channel_mentions", + "authentication.permissions.use_channel_mentions.name", + "authentication.permissions.use_channel_mentions.description", + PERMISSION_SCOPE_CHANNEL, + } + + PERMISSION_USE_GROUP_MENTIONS = &Permission{ + "use_group_mentions", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PERMISSION_SCOPE_CHANNEL, + } + + ALL_PERMISSIONS = []*Permission{ + PERMISSION_INVITE_USER, + PERMISSION_ADD_USER_TO_TEAM, + PERMISSION_USE_SLASH_COMMANDS, + PERMISSION_MANAGE_SLASH_COMMANDS, + PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS, + PERMISSION_CREATE_PUBLIC_CHANNEL, + PERMISSION_CREATE_PRIVATE_CHANNEL, + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS, + PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS, + PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE, + PERMISSION_MANAGE_ROLES, + PERMISSION_MANAGE_TEAM_ROLES, + PERMISSION_MANAGE_CHANNEL_ROLES, + PERMISSION_CREATE_DIRECT_CHANNEL, + PERMISSION_CREATE_GROUP_CHANNEL, + PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES, + PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES, + PERMISSION_LIST_PUBLIC_TEAMS, + PERMISSION_JOIN_PUBLIC_TEAMS, + PERMISSION_LIST_PRIVATE_TEAMS, + PERMISSION_JOIN_PRIVATE_TEAMS, + PERMISSION_LIST_TEAM_CHANNELS, + PERMISSION_JOIN_PUBLIC_CHANNELS, + PERMISSION_DELETE_PUBLIC_CHANNEL, + PERMISSION_DELETE_PRIVATE_CHANNEL, + PERMISSION_EDIT_OTHER_USERS, + PERMISSION_READ_CHANNEL, + PERMISSION_READ_PUBLIC_CHANNEL, + PERMISSION_ADD_REACTION, + PERMISSION_REMOVE_REACTION, + PERMISSION_REMOVE_OTHERS_REACTIONS, + PERMISSION_PERMANENT_DELETE_USER, + PERMISSION_UPLOAD_FILE, + PERMISSION_GET_PUBLIC_LINK, + PERMISSION_MANAGE_WEBHOOKS, + PERMISSION_MANAGE_OTHERS_WEBHOOKS, + PERMISSION_MANAGE_INCOMING_WEBHOOKS, + PERMISSION_MANAGE_OUTGOING_WEBHOOKS, + PERMISSION_MANAGE_OTHERS_INCOMING_WEBHOOKS, + PERMISSION_MANAGE_OTHERS_OUTGOING_WEBHOOKS, + PERMISSION_MANAGE_OAUTH, + PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH, + PERMISSION_MANAGE_EMOJIS, + PERMISSION_MANAGE_OTHERS_EMOJIS, + PERMISSION_CREATE_EMOJIS, + PERMISSION_DELETE_EMOJIS, + PERMISSION_DELETE_OTHERS_EMOJIS, + PERMISSION_CREATE_POST, + PERMISSION_CREATE_POST_PUBLIC, + PERMISSION_CREATE_POST_EPHEMERAL, + PERMISSION_EDIT_POST, + PERMISSION_EDIT_OTHERS_POSTS, + PERMISSION_DELETE_POST, + PERMISSION_DELETE_OTHERS_POSTS, + PERMISSION_REMOVE_USER_FROM_TEAM, + PERMISSION_CREATE_TEAM, + PERMISSION_MANAGE_TEAM, + PERMISSION_IMPORT_TEAM, + PERMISSION_VIEW_TEAM, + PERMISSION_LIST_USERS_WITHOUT_TEAM, + PERMISSION_MANAGE_JOBS, + PERMISSION_CREATE_USER_ACCESS_TOKEN, + PERMISSION_READ_USER_ACCESS_TOKEN, + PERMISSION_REVOKE_USER_ACCESS_TOKEN, + PERMISSION_CREATE_BOT, + PERMISSION_READ_BOTS, + PERMISSION_READ_OTHERS_BOTS, + PERMISSION_MANAGE_BOTS, + PERMISSION_MANAGE_OTHERS_BOTS, + PERMISSION_MANAGE_SYSTEM, + PERMISSION_VIEW_MEMBERS, + PERMISSION_INVITE_GUEST, + PERMISSION_PROMOTE_GUEST, + PERMISSION_DEMOTE_TO_GUEST, + PERMISSION_USE_CHANNEL_MENTIONS, + PERMISSION_USE_GROUP_MENTIONS, + } + + CHANNEL_MODERATED_PERMISSIONS = []string{ + PERMISSION_CREATE_POST.Id, + "create_reactions", + "manage_members", + PERMISSION_USE_CHANNEL_MENTIONS.Id, + } + + CHANNEL_MODERATED_PERMISSIONS_MAP = map[string]string{ + PERMISSION_CREATE_POST.Id: CHANNEL_MODERATED_PERMISSIONS[0], + PERMISSION_ADD_REACTION.Id: CHANNEL_MODERATED_PERMISSIONS[1], + PERMISSION_REMOVE_REACTION.Id: CHANNEL_MODERATED_PERMISSIONS[1], + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id: CHANNEL_MODERATED_PERMISSIONS[2], + PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id: CHANNEL_MODERATED_PERMISSIONS[2], + PERMISSION_USE_CHANNEL_MENTIONS.Id: CHANNEL_MODERATED_PERMISSIONS[3], + } +} + +func init() { + initializePermissions() +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_event_data.go b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_event_data.go new file mode 100644 index 00000000..c704c993 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_event_data.go @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +// PluginEventData used to notify peers about plugin changes. +type PluginEventData struct { + Id string `json:"id"` +} + +func (p *PluginEventData) ToJson() string { + b, _ := json.Marshal(p) + return string(b) +} + +func PluginEventDataFromJson(data io.Reader) PluginEventData { + var m PluginEventData + json.NewDecoder(data).Decode(&m) + return m +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_key_value.go b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_key_value.go new file mode 100644 index 00000000..cd5406ea --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_key_value.go @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "unicode/utf8" +) + +const ( + KEY_VALUE_PLUGIN_ID_MAX_RUNES = 190 + KEY_VALUE_KEY_MAX_RUNES = 50 +) + +type PluginKeyValue struct { + PluginId string `json:"plugin_id"` + Key string `json:"key" db:"PKey"` + Value []byte `json:"value" db:"PValue"` + ExpireAt int64 `json:"expire_at"` +} + +func (kv *PluginKeyValue) IsValid() *AppError { + if len(kv.PluginId) == 0 || utf8.RuneCountInString(kv.PluginId) > KEY_VALUE_PLUGIN_ID_MAX_RUNES { + return NewAppError("PluginKeyValue.IsValid", "model.plugin_key_value.is_valid.plugin_id.app_error", map[string]interface{}{"Max": KEY_VALUE_KEY_MAX_RUNES, "Min": 0}, "key="+kv.Key, http.StatusBadRequest) + } + + if len(kv.Key) == 0 || utf8.RuneCountInString(kv.Key) > KEY_VALUE_KEY_MAX_RUNES { + return NewAppError("PluginKeyValue.IsValid", "model.plugin_key_value.is_valid.key.app_error", map[string]interface{}{"Max": KEY_VALUE_KEY_MAX_RUNES, "Min": 0}, "key="+kv.Key, http.StatusBadRequest) + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_kvset_options.go b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_kvset_options.go new file mode 100644 index 00000000..1d374c80 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_kvset_options.go @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" +) + +// PluginKVSetOptions contains information on how to store a value in the plugin KV store. +type PluginKVSetOptions struct { + Atomic bool // Only store the value if the current value matches the oldValue + OldValue []byte // The value to compare with the current value. Only used when Atomic is true + ExpireInSeconds int64 // Set an expire counter +} + +// IsValid returns nil if the chosen options are valid. +func (opt *PluginKVSetOptions) IsValid() *AppError { + if !opt.Atomic && opt.OldValue != nil { + return NewAppError( + "PluginKVSetOptions.IsValid", + "model.plugin_kvset_options.is_valid.old_value.app_error", + nil, + "", + http.StatusBadRequest, + ) + } + + return nil +} + +// NewPluginKeyValueFromOptions return a PluginKeyValue given a pluginID, a KV pair and options. +func NewPluginKeyValueFromOptions(pluginId, key string, value []byte, opt PluginKVSetOptions) (*PluginKeyValue, *AppError) { + expireAt := int64(0) + if opt.ExpireInSeconds != 0 { + expireAt = GetMillis() + (opt.ExpireInSeconds * 1000) + } + + kv := &PluginKeyValue{ + PluginId: pluginId, + Key: key, + Value: value, + ExpireAt: expireAt, + } + + return kv, nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_status.go b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_status.go new file mode 100644 index 00000000..b4ba2e73 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_status.go @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + PluginStateNotRunning = 0 + PluginStateStarting = 1 // unused by server + PluginStateRunning = 2 + PluginStateFailedToStart = 3 + PluginStateFailedToStayRunning = 4 + PluginStateStopping = 5 // unused by server +) + +// PluginStatus provides a cluster-aware view of installed plugins. +type PluginStatus struct { + PluginId string `json:"plugin_id"` + ClusterId string `json:"cluster_id"` + PluginPath string `json:"plugin_path"` + State int `json:"state"` + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` +} + +type PluginStatuses []*PluginStatus + +func (m *PluginStatuses) ToJson() string { + b, _ := json.Marshal(m) + return string(b) +} + +func PluginStatusesFromJson(data io.Reader) PluginStatuses { + var m PluginStatuses + json.NewDecoder(data).Decode(&m) + return m +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_valid.go b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_valid.go new file mode 100644 index 00000000..b6144513 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/plugin_valid.go @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "regexp" + "unicode/utf8" +) + +const ( + MinIdLength = 3 + MaxIdLength = 190 + ValidIdRegex = `^[a-zA-Z0-9-_\.]+$` +) + +// ValidId constrains the set of valid plugin identifiers: +// ^[a-zA-Z0-9-_\.]+ +var validId *regexp.Regexp + +func init() { + validId = regexp.MustCompile(ValidIdRegex) +} + +// IsValidPluginId verifies that the plugin id has a minimum length of 3, maximum length of 190, and +// contains only alphanumeric characters, dashes, underscores and periods. +// +// These constraints are necessary since the plugin id is used as part of a filesystem path. +func IsValidPluginId(id string) bool { + if utf8.RuneCountInString(id) < MinIdLength { + return false + } + + if utf8.RuneCountInString(id) > MaxIdLength { + return false + } + + return validId.MatchString(id) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/plugins_response.go b/vendor/github.com/mattermost/mattermost-server/v5/model/plugins_response.go new file mode 100644 index 00000000..421ee2f5 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/plugins_response.go @@ -0,0 +1,29 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type PluginInfo struct { + Manifest +} + +type PluginsResponse struct { + Active []*PluginInfo `json:"active"` + Inactive []*PluginInfo `json:"inactive"` +} + +func (m *PluginsResponse) ToJson() string { + b, _ := json.Marshal(m) + return string(b) +} + +func PluginsResponseFromJson(data io.Reader) *PluginsResponse { + var m *PluginsResponse + json.NewDecoder(data).Decode(&m) + return m +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/post.go b/vendor/github.com/mattermost/mattermost-server/v5/model/post.go new file mode 100644 index 00000000..817ca08a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/post.go @@ -0,0 +1,663 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "regexp" + "sort" + "strings" + "sync" + "unicode/utf8" + + "github.com/mattermost/mattermost-server/v5/utils/markdown" +) + +const ( + POST_SYSTEM_MESSAGE_PREFIX = "system_" + POST_DEFAULT = "" + POST_SLACK_ATTACHMENT = "slack_attachment" + POST_SYSTEM_GENERIC = "system_generic" + POST_JOIN_LEAVE = "system_join_leave" // Deprecated, use POST_JOIN_CHANNEL or POST_LEAVE_CHANNEL instead + POST_JOIN_CHANNEL = "system_join_channel" + POST_GUEST_JOIN_CHANNEL = "system_guest_join_channel" + POST_LEAVE_CHANNEL = "system_leave_channel" + POST_JOIN_TEAM = "system_join_team" + POST_LEAVE_TEAM = "system_leave_team" + POST_AUTO_RESPONDER = "system_auto_responder" + POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead + POST_ADD_TO_CHANNEL = "system_add_to_channel" + POST_ADD_GUEST_TO_CHANNEL = "system_add_guest_to_chan" + POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel" + POST_MOVE_CHANNEL = "system_move_channel" + POST_ADD_TO_TEAM = "system_add_to_team" + POST_REMOVE_FROM_TEAM = "system_remove_from_team" + POST_HEADER_CHANGE = "system_header_change" + POST_DISPLAYNAME_CHANGE = "system_displayname_change" + POST_CONVERT_CHANNEL = "system_convert_channel" + POST_PURPOSE_CHANGE = "system_purpose_change" + POST_CHANNEL_DELETED = "system_channel_deleted" + POST_CHANNEL_RESTORED = "system_channel_restored" + POST_EPHEMERAL = "system_ephemeral" + POST_CHANGE_CHANNEL_PRIVACY = "system_change_chan_privacy" + POST_ADD_BOT_TEAMS_CHANNELS = "add_bot_teams_channels" + POST_FILEIDS_MAX_RUNES = 150 + POST_FILENAMES_MAX_RUNES = 4000 + POST_HASHTAGS_MAX_RUNES = 1000 + POST_MESSAGE_MAX_RUNES_V1 = 4000 + POST_MESSAGE_MAX_BYTES_V2 = 65535 // Maximum size of a TEXT column in MySQL + POST_MESSAGE_MAX_RUNES_V2 = POST_MESSAGE_MAX_BYTES_V2 / 4 // Assume a worst-case representation + POST_PROPS_MAX_RUNES = 8000 + POST_PROPS_MAX_USER_RUNES = POST_PROPS_MAX_RUNES - 400 // Leave some room for system / pre-save modifications + POST_CUSTOM_TYPE_PREFIX = "custom_" + POST_ME = "me" + PROPS_ADD_CHANNEL_MEMBER = "add_channel_member" + + POST_PROPS_ADDED_USER_ID = "addedUserId" + POST_PROPS_DELETE_BY = "deleteBy" + POST_PROPS_OVERRIDE_ICON_URL = "override_icon_url" + POST_PROPS_OVERRIDE_ICON_EMOJI = "override_icon_emoji" + + POST_PROPS_MENTION_HIGHLIGHT_DISABLED = "mentionHighlightDisabled" + POST_PROPS_GROUP_HIGHLIGHT_DISABLED = "disable_group_highlight" +) + +var AT_MENTION_PATTEN = regexp.MustCompile(`\B@`) + +type Post struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + EditAt int64 `json:"edit_at"` + DeleteAt int64 `json:"delete_at"` + IsPinned bool `json:"is_pinned"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + RootId string `json:"root_id"` + ParentId string `json:"parent_id"` + OriginalId string `json:"original_id"` + + Message string `json:"message"` + // MessageSource will contain the message as submitted by the user if Message has been modified + // by Mattermost for presentation (e.g if an image proxy is being used). It should be used to + // populate edit boxes if present. + MessageSource string `json:"message_source,omitempty" db:"-"` + + Type string `json:"type"` + propsMu sync.RWMutex `db:"-"` // Unexported mutex used to guard Post.Props. + Props StringInterface `json:"props"` // Deprecated: use GetProps() + Hashtags string `json:"hashtags"` + Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more + FileIds StringArray `json:"file_ids,omitempty"` + PendingPostId string `json:"pending_post_id" db:"-"` + HasReactions bool `json:"has_reactions,omitempty"` + + // Transient data populated before sending a post to the client + ReplyCount int64 `json:"reply_count" db:"-"` + Metadata *PostMetadata `json:"metadata,omitempty" db:"-"` +} + +type PostEphemeral struct { + UserID string `json:"user_id"` + Post *Post `json:"post"` +} + +type PostPatch struct { + IsPinned *bool `json:"is_pinned"` + Message *string `json:"message"` + Props *StringInterface `json:"props"` + FileIds *StringArray `json:"file_ids"` + HasReactions *bool `json:"has_reactions"` +} + +type SearchParameter struct { + Terms *string `json:"terms"` + IsOrSearch *bool `json:"is_or_search"` + TimeZoneOffset *int `json:"time_zone_offset"` + Page *int `json:"page"` + PerPage *int `json:"per_page"` + IncludeDeletedChannels *bool `json:"include_deleted_channels"` +} + +type AnalyticsPostCountsOptions struct { + TeamId string + BotsOnly bool + YesterdayOnly bool +} + +func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch { + copy := *o + if copy.Message != nil { + *copy.Message = RewriteImageURLs(*o.Message, f) + } + return © +} + +type PostForExport struct { + Post + TeamName string + ChannelName string + Username string + ReplyCount int +} + +type DirectPostForExport struct { + Post + User string + ChannelMembers *[]string +} + +type ReplyForExport struct { + Post + Username string +} + +type PostForIndexing struct { + Post + TeamId string `json:"team_id"` + ParentCreateAt *int64 `json:"parent_create_at"` +} + +// ShallowCopy is an utility function to shallow copy a Post to the given +// destination without touching the internal RWMutex. +func (o *Post) ShallowCopy(dst *Post) error { + if dst == nil { + return errors.New("dst cannot be nil") + } + o.propsMu.RLock() + defer o.propsMu.RUnlock() + dst.propsMu.Lock() + defer dst.propsMu.Unlock() + dst.Id = o.Id + dst.CreateAt = o.CreateAt + dst.UpdateAt = o.UpdateAt + dst.EditAt = o.EditAt + dst.DeleteAt = o.DeleteAt + dst.IsPinned = o.IsPinned + dst.UserId = o.UserId + dst.ChannelId = o.ChannelId + dst.RootId = o.RootId + dst.ParentId = o.ParentId + dst.OriginalId = o.OriginalId + dst.Message = o.Message + dst.MessageSource = o.MessageSource + dst.Type = o.Type + dst.Props = o.Props + dst.Hashtags = o.Hashtags + dst.Filenames = o.Filenames + dst.FileIds = o.FileIds + dst.PendingPostId = o.PendingPostId + dst.HasReactions = o.HasReactions + dst.ReplyCount = o.ReplyCount + dst.Metadata = o.Metadata + return nil +} + +// Clone shallowly copies the post and returns the copy. +func (o *Post) Clone() *Post { + copy := &Post{} + o.ShallowCopy(copy) + return copy +} + +func (o *Post) ToJson() string { + copy := o.Clone() + copy.StripActionIntegrations() + b, _ := json.Marshal(copy) + return string(b) +} + +func (o *Post) ToUnsanitizedJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +type GetPostsSinceOptions struct { + ChannelId string + Time int64 + SkipFetchThreads bool +} + +type GetPostsOptions struct { + ChannelId string + PostId string + Page int + PerPage int + SkipFetchThreads bool +} + +func PostFromJson(data io.Reader) *Post { + var o *Post + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *Post) Etag() string { + return Etag(o.Id, o.UpdateAt) +} + +func (o *Post) IsValid(maxPostSize int) *AppError { + if !IsValidId(o.Id) { + return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if o.UpdateAt == 0 { + return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if !IsValidId(o.UserId) { + return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.ChannelId) { + return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) + } + + if !(IsValidId(o.RootId) || len(o.RootId) == 0) { + return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest) + } + + if !(IsValidId(o.ParentId) || len(o.ParentId) == 0) { + return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.ParentId) == 26 && len(o.RootId) == 0 { + return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest) + } + + if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) { + return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.Message) > maxPostSize { + return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES { + return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + switch o.Type { + case + POST_DEFAULT, + POST_SYSTEM_GENERIC, + POST_JOIN_LEAVE, + POST_AUTO_RESPONDER, + POST_ADD_REMOVE, + POST_JOIN_CHANNEL, + POST_GUEST_JOIN_CHANNEL, + POST_LEAVE_CHANNEL, + POST_JOIN_TEAM, + POST_LEAVE_TEAM, + POST_ADD_TO_CHANNEL, + POST_ADD_GUEST_TO_CHANNEL, + POST_REMOVE_FROM_CHANNEL, + POST_MOVE_CHANNEL, + POST_ADD_TO_TEAM, + POST_REMOVE_FROM_TEAM, + POST_SLACK_ATTACHMENT, + POST_HEADER_CHANGE, + POST_PURPOSE_CHANGE, + POST_DISPLAYNAME_CHANGE, + POST_CONVERT_CHANNEL, + POST_CHANNEL_DELETED, + POST_CHANNEL_RESTORED, + POST_CHANGE_CHANNEL_PRIVACY, + POST_ME, + POST_ADD_BOT_TEAMS_CHANNELS: + default: + if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) { + return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) + } + } + + if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES { + return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES { + return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(StringInterfaceToJson(o.GetProps())) > POST_PROPS_MAX_RUNES { + return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + return nil +} + +func (o *Post) SanitizeProps() { + membersToSanitize := []string{ + PROPS_ADD_CHANNEL_MEMBER, + } + + for _, member := range membersToSanitize { + if _, ok := o.GetProps()[member]; ok { + o.DelProp(member) + } + } +} + +func (o *Post) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + o.OriginalId = "" + + if o.CreateAt == 0 { + o.CreateAt = GetMillis() + } + + o.UpdateAt = o.CreateAt + o.PreCommit() +} + +func (o *Post) PreCommit() { + if o.GetProps() == nil { + o.SetProps(make(map[string]interface{})) + } + + if o.Filenames == nil { + o.Filenames = []string{} + } + + if o.FileIds == nil { + o.FileIds = []string{} + } + + o.GenerateActionIds() + + // There's a rare bug where the client sends up duplicate FileIds so protect against that + o.FileIds = RemoveDuplicateStrings(o.FileIds) +} + +func (o *Post) MakeNonNil() { + if o.GetProps() == nil { + o.SetProps(make(map[string]interface{})) + } +} + +func (o *Post) DelProp(key string) { + o.propsMu.Lock() + defer o.propsMu.Unlock() + propsCopy := make(map[string]interface{}, len(o.Props)-1) + for k, v := range o.Props { + propsCopy[k] = v + } + delete(propsCopy, key) + o.Props = propsCopy +} + +func (o *Post) AddProp(key string, value interface{}) { + o.propsMu.Lock() + defer o.propsMu.Unlock() + propsCopy := make(map[string]interface{}, len(o.Props)+1) + for k, v := range o.Props { + propsCopy[k] = v + } + propsCopy[key] = value + o.Props = propsCopy +} + +func (o *Post) GetProps() StringInterface { + o.propsMu.RLock() + defer o.propsMu.RUnlock() + return o.Props +} + +func (o *Post) SetProps(props StringInterface) { + o.propsMu.Lock() + defer o.propsMu.Unlock() + o.Props = props +} + +func (o *Post) GetProp(key string) interface{} { + o.propsMu.RLock() + defer o.propsMu.RUnlock() + return o.Props[key] +} + +func (o *Post) IsSystemMessage() bool { + return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX +} + +func (o *Post) IsJoinLeaveMessage() bool { + return o.Type == POST_JOIN_LEAVE || + o.Type == POST_ADD_REMOVE || + o.Type == POST_JOIN_CHANNEL || + o.Type == POST_LEAVE_CHANNEL || + o.Type == POST_JOIN_TEAM || + o.Type == POST_LEAVE_TEAM || + o.Type == POST_ADD_TO_CHANNEL || + o.Type == POST_REMOVE_FROM_CHANNEL || + o.Type == POST_ADD_TO_TEAM || + o.Type == POST_REMOVE_FROM_TEAM +} + +func (o *Post) Patch(patch *PostPatch) { + if patch.IsPinned != nil { + o.IsPinned = *patch.IsPinned + } + + if patch.Message != nil { + o.Message = *patch.Message + } + + if patch.Props != nil { + newProps := *patch.Props + o.SetProps(newProps) + } + + if patch.FileIds != nil { + o.FileIds = *patch.FileIds + } + + if patch.HasReactions != nil { + o.HasReactions = *patch.HasReactions + } +} + +func (o *PostPatch) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } + + return string(b) +} + +func PostPatchFromJson(data io.Reader) *PostPatch { + decoder := json.NewDecoder(data) + var post PostPatch + err := decoder.Decode(&post) + if err != nil { + return nil + } + + return &post +} + +func (o *SearchParameter) SearchParameterToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } + + return string(b) +} + +func SearchParameterFromJson(data io.Reader) *SearchParameter { + decoder := json.NewDecoder(data) + var searchParam SearchParameter + err := decoder.Decode(&searchParam) + if err != nil { + return nil + } + + return &searchParam +} + +func (o *Post) ChannelMentions() []string { + return ChannelMentions(o.Message) +} + +// DisableMentionHighlights disables a posts mention highlighting and returns the first channel mention that was present in the message. +func (o *Post) DisableMentionHighlights() string { + mention, hasMentions := findAtChannelMention(o.Message) + if hasMentions { + o.AddProp(POST_PROPS_MENTION_HIGHLIGHT_DISABLED, true) + } + return mention +} + +// DisableMentionHighlights disables mention highlighting for a post patch if required. +func (o *PostPatch) DisableMentionHighlights() { + if _, hasMentions := findAtChannelMention(*o.Message); hasMentions { + if o.Props == nil { + o.Props = &StringInterface{} + } + (*o.Props)[POST_PROPS_MENTION_HIGHLIGHT_DISABLED] = true + } +} + +func findAtChannelMention(message string) (mention string, found bool) { + re := regexp.MustCompile(`(?i)\B@(channel|all|here)\b`) + matched := re.FindStringSubmatch(message) + if found = (len(matched) > 0); found { + mention = strings.ToLower(matched[0]) + } + return +} + +func (o *Post) Attachments() []*SlackAttachment { + if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok { + return attachments + } + var ret []*SlackAttachment + if attachments, ok := o.GetProp("attachments").([]interface{}); ok { + for _, attachment := range attachments { + if enc, err := json.Marshal(attachment); err == nil { + var decoded SlackAttachment + if json.Unmarshal(enc, &decoded) == nil { + ret = append(ret, &decoded) + } + } + } + } + return ret +} + +func (o *Post) AttachmentsEqual(input *Post) bool { + attachments := o.Attachments() + inputAttachments := input.Attachments() + + if len(attachments) != len(inputAttachments) { + return false + } + + for i := range attachments { + if !attachments[i].Equals(inputAttachments[i]) { + return false + } + } + + return true +} + +var markdownDestinationEscaper = strings.NewReplacer( + `\`, `\\`, + `<`, `\<`, + `>`, `\>`, + `(`, `\(`, + `)`, `\)`, +) + +// WithRewrittenImageURLs returns a new shallow copy of the post where the message has been +// rewritten via RewriteImageURLs. +func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post { + copy := o.Clone() + copy.Message = RewriteImageURLs(o.Message, f) + if copy.MessageSource == "" && copy.Message != o.Message { + copy.MessageSource = o.Message + } + return copy +} + +func (o *PostEphemeral) ToUnsanitizedJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +// RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced +// according to the function f. For each image URL, f will be invoked, and the resulting markdown +// will contain the URL returned by that invocation instead. +// +// Image URLs are destination URLs used in inline images or reference definitions that are used +// anywhere in the input markdown as an image. +func RewriteImageURLs(message string, f func(string) string) string { + if !strings.Contains(message, "![") { + return message + } + + var ranges []markdown.Range + + markdown.Inspect(message, func(blockOrInline interface{}) bool { + switch v := blockOrInline.(type) { + case *markdown.ReferenceImage: + ranges = append(ranges, v.ReferenceDefinition.RawDestination) + case *markdown.InlineImage: + ranges = append(ranges, v.RawDestination) + default: + return true + } + return true + }) + + if ranges == nil { + return message + } + + sort.Slice(ranges, func(i, j int) bool { + return ranges[i].Position < ranges[j].Position + }) + + copyRanges := make([]markdown.Range, 0, len(ranges)) + urls := make([]string, 0, len(ranges)) + resultLength := len(message) + + start := 0 + for i, r := range ranges { + switch { + case i == 0: + case r.Position != ranges[i-1].Position: + start = ranges[i-1].End + default: + continue + } + original := message[r.Position:r.End] + replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original))) + resultLength += len(replacement) - len(original) + copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position}) + urls = append(urls, replacement) + } + + result := make([]byte, resultLength) + + offset := 0 + for i, r := range copyRanges { + offset += copy(result[offset:], message[r.Position:r.End]) + offset += copy(result[offset:], urls[i]) + } + copy(result[offset:], message[ranges[len(ranges)-1].End:]) + + return string(result) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/post_embed.go b/vendor/github.com/mattermost/mattermost-server/v5/model/post_embed.go new file mode 100644 index 00000000..5c6efec1 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/post_embed.go @@ -0,0 +1,23 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + POST_EMBED_IMAGE PostEmbedType = "image" + POST_EMBED_MESSAGE_ATTACHMENT PostEmbedType = "message_attachment" + POST_EMBED_OPENGRAPH PostEmbedType = "opengraph" + POST_EMBED_LINK PostEmbedType = "link" +) + +type PostEmbedType string + +type PostEmbed struct { + Type PostEmbedType `json:"type"` + + // The URL of the embedded content. Used for image and OpenGraph embeds. + URL string `json:"url,omitempty"` + + // Any additional data for the embedded content. Only used for OpenGraph embeds. + Data interface{} `json:"data,omitempty"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/post_list.go b/vendor/github.com/mattermost/mattermost-server/v5/model/post_list.go new file mode 100644 index 00000000..d00b68b5 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/post_list.go @@ -0,0 +1,166 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "sort" +) + +type PostList struct { + Order []string `json:"order"` + Posts map[string]*Post `json:"posts"` + NextPostId string `json:"next_post_id"` + PrevPostId string `json:"prev_post_id"` +} + +func NewPostList() *PostList { + return &PostList{ + Order: make([]string, 0), + Posts: make(map[string]*Post), + NextPostId: "", + PrevPostId: "", + } +} + +func (o *PostList) ToSlice() []*Post { + var posts []*Post + for _, id := range o.Order { + posts = append(posts, o.Posts[id]) + } + return posts +} + +func (o *PostList) WithRewrittenImageURLs(f func(string) string) *PostList { + copy := *o + copy.Posts = make(map[string]*Post) + for id, post := range o.Posts { + copy.Posts[id] = post.WithRewrittenImageURLs(f) + } + return © +} + +func (o *PostList) StripActionIntegrations() { + posts := o.Posts + o.Posts = make(map[string]*Post) + for id, post := range posts { + pcopy := post.Clone() + pcopy.StripActionIntegrations() + o.Posts[id] = pcopy + } +} + +func (o *PostList) ToJson() string { + copy := *o + copy.StripActionIntegrations() + b, err := json.Marshal(©) + if err != nil { + return "" + } else { + return string(b) + } +} + +func (o *PostList) MakeNonNil() { + if o.Order == nil { + o.Order = make([]string, 0) + } + + if o.Posts == nil { + o.Posts = make(map[string]*Post) + } + + for _, v := range o.Posts { + v.MakeNonNil() + } +} + +func (o *PostList) AddOrder(id string) { + + if o.Order == nil { + o.Order = make([]string, 0, 128) + } + + o.Order = append(o.Order, id) +} + +func (o *PostList) AddPost(post *Post) { + + if o.Posts == nil { + o.Posts = make(map[string]*Post) + } + + o.Posts[post.Id] = post +} + +func (o *PostList) UniqueOrder() { + keys := make(map[string]bool) + order := []string{} + for _, postId := range o.Order { + if _, value := keys[postId]; !value { + keys[postId] = true + order = append(order, postId) + } + } + + o.Order = order +} + +func (o *PostList) Extend(other *PostList) { + for postId := range other.Posts { + o.AddPost(other.Posts[postId]) + } + + for _, postId := range other.Order { + o.AddOrder(postId) + } + + o.UniqueOrder() +} + +func (o *PostList) SortByCreateAt() { + sort.Slice(o.Order, func(i, j int) bool { + return o.Posts[o.Order[i]].CreateAt > o.Posts[o.Order[j]].CreateAt + }) +} + +func (o *PostList) Etag() string { + + id := "0" + var t int64 = 0 + + for _, v := range o.Posts { + if v.UpdateAt > t { + t = v.UpdateAt + id = v.Id + } else if v.UpdateAt == t && v.Id > id { + t = v.UpdateAt + id = v.Id + } + } + + orderId := "" + if len(o.Order) > 0 { + orderId = o.Order[0] + } + + return Etag(orderId, id, t) +} + +func (o *PostList) IsChannelId(channelId string) bool { + for _, v := range o.Posts { + if v.ChannelId != channelId { + return false + } + } + + return true +} + +func PostListFromJson(data io.Reader) *PostList { + var o *PostList + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/post_metadata.go b/vendor/github.com/mattermost/mattermost-server/v5/model/post_metadata.go new file mode 100644 index 00000000..7b0687ca --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/post_metadata.go @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" +) + +type PostMetadata struct { + // Embeds holds information required to render content embedded in the post. This includes the OpenGraph metadata + // for links in the post. + Embeds []*PostEmbed `json:"embeds,omitempty"` + + // Emojis holds all custom emojis used in the post or used in reaction to the post. + Emojis []*Emoji `json:"emojis,omitempty"` + + // Files holds information about the file attachments on the post. + Files []*FileInfo `json:"files,omitempty"` + + // Images holds the dimensions of all external images in the post as a map of the image URL to its diemsnions. + // This includes image embeds (when the message contains a plaintext link to an image), Markdown images, images + // contained in the OpenGraph metadata, and images contained in message attachments. It does not contain + // the dimensions of any file attachments as those are stored in FileInfos. + Images map[string]*PostImage `json:"images,omitempty"` + + // Reactions holds reactions made to the post. + Reactions []*Reaction `json:"reactions,omitempty"` +} + +type PostImage struct { + Width int `json:"width"` + Height int `json:"height"` + + // Format is the name of the image format as used by image/go such as "png", "gif", or "jpeg". + Format string `json:"format"` + + // FrameCount stores the number of frames in this image, if it is an animated gif. It will be 0 for other formats. + FrameCount int `json:"frame_count"` +} + +func (o *PostImage) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/post_search_results.go b/vendor/github.com/mattermost/mattermost-server/v5/model/post_search_results.go new file mode 100644 index 00000000..74ef4b52 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/post_search_results.go @@ -0,0 +1,40 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type PostSearchMatches map[string][]string + +type PostSearchResults struct { + *PostList + Matches PostSearchMatches `json:"matches"` +} + +func MakePostSearchResults(posts *PostList, matches PostSearchMatches) *PostSearchResults { + return &PostSearchResults{ + posts, + matches, + } +} + +func (o *PostSearchResults) ToJson() string { + copy := *o + copy.PostList.StripActionIntegrations() + b, err := json.Marshal(©) + if err != nil { + return "" + } else { + return string(b) + } +} + +func PostSearchResultsFromJson(data io.Reader) *PostSearchResults { + var o *PostSearchResults + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/preference.go b/vendor/github.com/mattermost/mattermost-server/v5/model/preference.go new file mode 100644 index 00000000..346f88f8 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/preference.go @@ -0,0 +1,123 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "regexp" + "strings" + "unicode/utf8" +) + +const ( + PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show" + PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step" + PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings" + PREFERENCE_CATEGORY_FLAGGED_POST = "flagged_post" + PREFERENCE_CATEGORY_FAVORITE_CHANNEL = "favorite_channel" + PREFERENCE_CATEGORY_SIDEBAR_SETTINGS = "sidebar_settings" + + PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings" + PREFERENCE_NAME_CHANNEL_DISPLAY_MODE = "channel_display_mode" + PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews" + PREFERENCE_NAME_MESSAGE_DISPLAY = "message_display" + PREFERENCE_NAME_NAME_FORMAT = "name_format" + PREFERENCE_NAME_USE_MILITARY_TIME = "use_military_time" + + PREFERENCE_CATEGORY_THEME = "theme" + // the name for theme props is the team id + + PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP = "oauth_app" + // the name for oauth_app is the client_id and value is the current scope + + PREFERENCE_CATEGORY_LAST = "last" + PREFERENCE_NAME_LAST_CHANNEL = "channel" + PREFERENCE_NAME_LAST_TEAM = "team" + + PREFERENCE_CATEGORY_NOTIFICATIONS = "notifications" + PREFERENCE_NAME_EMAIL_INTERVAL = "email_interval" + + PREFERENCE_EMAIL_INTERVAL_NO_BATCHING_SECONDS = "30" // the "immediate" setting is actually 30s + PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS = "900" // fifteen minutes is 900 seconds + PREFERENCE_EMAIL_INTERVAL_IMMEDIATELY = "immediately" + PREFERENCE_EMAIL_INTERVAL_FIFTEEN = "fifteen" + PREFERENCE_EMAIL_INTERVAL_FIFTEEN_AS_SECONDS = "900" + PREFERENCE_EMAIL_INTERVAL_HOUR = "hour" + PREFERENCE_EMAIL_INTERVAL_HOUR_AS_SECONDS = "3600" +) + +type Preference struct { + UserId string `json:"user_id"` + Category string `json:"category"` + Name string `json:"name"` + Value string `json:"value"` +} + +func (o *Preference) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func PreferenceFromJson(data io.Reader) *Preference { + var o *Preference + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *Preference) IsValid() *AppError { + if !IsValidId(o.UserId) { + return NewAppError("Preference.IsValid", "model.preference.is_valid.id.app_error", nil, "user_id="+o.UserId, http.StatusBadRequest) + } + + if len(o.Category) == 0 || len(o.Category) > 32 { + return NewAppError("Preference.IsValid", "model.preference.is_valid.category.app_error", nil, "category="+o.Category, http.StatusBadRequest) + } + + if len(o.Name) > 32 { + return NewAppError("Preference.IsValid", "model.preference.is_valid.name.app_error", nil, "name="+o.Name, http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.Value) > 2000 { + return NewAppError("Preference.IsValid", "model.preference.is_valid.value.app_error", nil, "value="+o.Value, http.StatusBadRequest) + } + + if o.Category == PREFERENCE_CATEGORY_THEME { + var unused map[string]string + if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&unused); err != nil { + return NewAppError("Preference.IsValid", "model.preference.is_valid.theme.app_error", nil, "value="+o.Value, http.StatusBadRequest) + } + } + + return nil +} + +func (o *Preference) PreUpdate() { + if o.Category == PREFERENCE_CATEGORY_THEME { + // decode the value of theme (a map of strings to string) and eliminate any invalid values + var props map[string]string + if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&props); err != nil { + // just continue, the invalid preference value should get caught by IsValid before saving + return + } + + colorPattern := regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`) + + // blank out any invalid theme values + for name, value := range props { + if name == "image" || name == "type" || name == "codeTheme" { + continue + } + + if !colorPattern.MatchString(value) { + props[name] = "#ffffff" + } + } + + if b, err := json.Marshal(props); err == nil { + o.Value = string(b) + } + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/preferences.go b/vendor/github.com/mattermost/mattermost-server/v5/model/preferences.go new file mode 100644 index 00000000..6ed845b6 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/preferences.go @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type Preferences []Preference + +func (o *Preferences) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func PreferencesFromJson(data io.Reader) (Preferences, error) { + decoder := json.NewDecoder(data) + var o Preferences + err := decoder.Decode(&o) + if err == nil { + return o, nil + } else { + return nil, err + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/push_notification.go b/vendor/github.com/mattermost/mattermost-server/v5/model/push_notification.go new file mode 100644 index 00000000..5b0118ce --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/push_notification.go @@ -0,0 +1,117 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "errors" + "io" + "strings" +) + +const ( + PUSH_NOTIFY_APPLE = "apple" + PUSH_NOTIFY_ANDROID = "android" + PUSH_NOTIFY_APPLE_REACT_NATIVE = "apple_rn" + PUSH_NOTIFY_ANDROID_REACT_NATIVE = "android_rn" + + PUSH_TYPE_MESSAGE = "message" + PUSH_TYPE_CLEAR = "clear" + PUSH_TYPE_UPDATE_BADGE = "update_badge" + PUSH_MESSAGE_V2 = "v2" + + PUSH_SOUND_NONE = "none" + + // The category is set to handle a set of interactive Actions + // with the push notifications + CATEGORY_CAN_REPLY = "CAN_REPLY" + + MHPNS = "https://push.mattermost.com" + + PUSH_SEND_PREPARE = "Prepared to send" + PUSH_SEND_SUCCESS = "Successful" + PUSH_NOT_SENT = "Not Sent due to preferences" + PUSH_RECEIVED = "Received by device" +) + +type PushNotificationAck struct { + Id string `json:"id"` + ClientReceivedAt int64 `json:"received_at"` + ClientPlatform string `json:"platform"` + NotificationType string `json:"type"` + PostId string `json:"post_id,omitempty"` + IsIdLoaded bool `json:"is_id_loaded"` +} + +type PushNotification struct { + AckId string `json:"ack_id"` + Platform string `json:"platform"` + ServerId string `json:"server_id"` + DeviceId string `json:"device_id"` + PostId string `json:"post_id"` + Category string `json:"category,omitempty"` + Sound string `json:"sound,omitempty"` + Message string `json:"message,omitempty"` + Badge int `json:"badge,omitempty"` + ContentAvailable int `json:"cont_ava,omitempty"` + TeamId string `json:"team_id,omitempty"` + ChannelId string `json:"channel_id,omitempty"` + RootId string `json:"root_id,omitempty"` + ChannelName string `json:"channel_name,omitempty"` + Type string `json:"type,omitempty"` + SenderId string `json:"sender_id,omitempty"` + SenderName string `json:"sender_name,omitempty"` + OverrideUsername string `json:"override_username,omitempty"` + OverrideIconUrl string `json:"override_icon_url,omitempty"` + FromWebhook string `json:"from_webhook,omitempty"` + Version string `json:"version,omitempty"` + IsIdLoaded bool `json:"is_id_loaded"` +} + +func (me *PushNotification) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func (me *PushNotification) DeepCopy() *PushNotification { + copy := *me + return © +} + +func (me *PushNotification) SetDeviceIdAndPlatform(deviceId string) { + + index := strings.Index(deviceId, ":") + + if index > -1 { + me.Platform = deviceId[:index] + me.DeviceId = deviceId[index+1:] + } +} + +func PushNotificationFromJson(data io.Reader) (*PushNotification, error) { + if data == nil { + return nil, errors.New("push notification data can't be nil") + } + var me *PushNotification + if err := json.NewDecoder(data).Decode(&me); err != nil { + return nil, err + } + return me, nil +} + +func PushNotificationAckFromJson(data io.Reader) (*PushNotificationAck, error) { + if data == nil { + return nil, errors.New("push notification data can't be nil") + } + var ack *PushNotificationAck + if err := json.NewDecoder(data).Decode(&ack); err != nil { + return nil, err + } + return ack, nil +} + +func (ack *PushNotificationAck) ToJson() string { + b, _ := json.Marshal(ack) + return string(b) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/push_response.go b/vendor/github.com/mattermost/mattermost-server/v5/model/push_response.go new file mode 100644 index 00000000..e6e8059b --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/push_response.go @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + PUSH_STATUS = "status" + PUSH_STATUS_OK = "OK" + PUSH_STATUS_FAIL = "FAIL" + PUSH_STATUS_REMOVE = "REMOVE" + PUSH_STATUS_ERROR_MSG = "error" +) + +type PushResponse map[string]string + +func NewOkPushResponse() PushResponse { + m := make(map[string]string) + m[PUSH_STATUS] = PUSH_STATUS_OK + return m +} + +func NewRemovePushResponse() PushResponse { + m := make(map[string]string) + m[PUSH_STATUS] = PUSH_STATUS_REMOVE + return m +} + +func NewErrorPushResponse(message string) PushResponse { + m := make(map[string]string) + m[PUSH_STATUS] = PUSH_STATUS_FAIL + m[PUSH_STATUS_ERROR_MSG] = message + return m +} + +func (me *PushResponse) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func PushResponseFromJson(data io.Reader) PushResponse { + decoder := json.NewDecoder(data) + + var objmap PushResponse + if err := decoder.Decode(&objmap); err != nil { + return make(map[string]string) + } else { + return objmap + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/reaction.go b/vendor/github.com/mattermost/mattermost-server/v5/model/reaction.go new file mode 100644 index 00000000..50879c67 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/reaction.go @@ -0,0 +1,92 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" + "regexp" +) + +type Reaction struct { + UserId string `json:"user_id"` + PostId string `json:"post_id"` + EmojiName string `json:"emoji_name"` + CreateAt int64 `json:"create_at"` +} + +func (o *Reaction) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func ReactionFromJson(data io.Reader) *Reaction { + var o Reaction + + if err := json.NewDecoder(data).Decode(&o); err != nil { + return nil + } else { + return &o + } +} + +func ReactionsToJson(o []*Reaction) string { + b, _ := json.Marshal(o) + return string(b) +} + +func MapPostIdToReactionsToJson(o map[string][]*Reaction) string { + b, _ := json.Marshal(o) + return string(b) +} + +func MapPostIdToReactionsFromJson(data io.Reader) map[string][]*Reaction { + decoder := json.NewDecoder(data) + + var objmap map[string][]*Reaction + if err := decoder.Decode(&objmap); err != nil { + return make(map[string][]*Reaction) + } else { + return objmap + } +} + +func ReactionsFromJson(data io.Reader) []*Reaction { + var o []*Reaction + + if err := json.NewDecoder(data).Decode(&o); err != nil { + return nil + } else { + return o + } +} + +func (o *Reaction) IsValid() *AppError { + if !IsValidId(o.UserId) { + return NewAppError("Reaction.IsValid", "model.reaction.is_valid.user_id.app_error", nil, "user_id="+o.UserId, http.StatusBadRequest) + } + + if !IsValidId(o.PostId) { + return NewAppError("Reaction.IsValid", "model.reaction.is_valid.post_id.app_error", nil, "post_id="+o.PostId, http.StatusBadRequest) + } + + validName := regexp.MustCompile(`^[a-zA-Z0-9\-\+_]+$`) + + if len(o.EmojiName) == 0 || len(o.EmojiName) > EMOJI_NAME_MAX_LENGTH || !validName.MatchString(o.EmojiName) { + return NewAppError("Reaction.IsValid", "model.reaction.is_valid.emoji_name.app_error", nil, "emoji_name="+o.EmojiName, http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("Reaction.IsValid", "model.reaction.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (o *Reaction) PreSave() { + if o.CreateAt == 0 { + o.CreateAt = GetMillis() + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/role.go b/vendor/github.com/mattermost/mattermost-server/v5/model/role.go new file mode 100644 index 00000000..38ac1ef7 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/role.go @@ -0,0 +1,632 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "strings" +) + +var BuiltInSchemeManagedRoleIDs []string + +func init() { + BuiltInSchemeManagedRoleIDs = []string{ + SYSTEM_GUEST_ROLE_ID, + SYSTEM_USER_ROLE_ID, + SYSTEM_ADMIN_ROLE_ID, + SYSTEM_POST_ALL_ROLE_ID, + SYSTEM_POST_ALL_PUBLIC_ROLE_ID, + SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, + + TEAM_GUEST_ROLE_ID, + TEAM_USER_ROLE_ID, + TEAM_ADMIN_ROLE_ID, + TEAM_POST_ALL_ROLE_ID, + TEAM_POST_ALL_PUBLIC_ROLE_ID, + + CHANNEL_GUEST_ROLE_ID, + CHANNEL_USER_ROLE_ID, + CHANNEL_ADMIN_ROLE_ID, + } +} + +type RoleType string +type RoleScope string + +const ( + SYSTEM_GUEST_ROLE_ID = "system_guest" + SYSTEM_USER_ROLE_ID = "system_user" + SYSTEM_ADMIN_ROLE_ID = "system_admin" + SYSTEM_POST_ALL_ROLE_ID = "system_post_all" + SYSTEM_POST_ALL_PUBLIC_ROLE_ID = "system_post_all_public" + SYSTEM_USER_ACCESS_TOKEN_ROLE_ID = "system_user_access_token" + + TEAM_GUEST_ROLE_ID = "team_guest" + TEAM_USER_ROLE_ID = "team_user" + TEAM_ADMIN_ROLE_ID = "team_admin" + TEAM_POST_ALL_ROLE_ID = "team_post_all" + TEAM_POST_ALL_PUBLIC_ROLE_ID = "team_post_all_public" + + CHANNEL_GUEST_ROLE_ID = "channel_guest" + CHANNEL_USER_ROLE_ID = "channel_user" + CHANNEL_ADMIN_ROLE_ID = "channel_admin" + + ROLE_NAME_MAX_LENGTH = 64 + ROLE_DISPLAY_NAME_MAX_LENGTH = 128 + ROLE_DESCRIPTION_MAX_LENGTH = 1024 + + RoleScopeSystem RoleScope = "System" + RoleScopeTeam RoleScope = "Team" + RoleScopeChannel RoleScope = "Channel" + + RoleTypeGuest RoleType = "Guest" + RoleTypeUser RoleType = "User" + RoleTypeAdmin RoleType = "Admin" +) + +type Role struct { + Id string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + Permissions []string `json:"permissions"` + SchemeManaged bool `json:"scheme_managed"` + BuiltIn bool `json:"built_in"` +} + +type RolePatch struct { + Permissions *[]string `json:"permissions"` +} + +type RolePermissions struct { + RoleID string + Permissions []string +} + +func (r *Role) ToJson() string { + b, _ := json.Marshal(r) + return string(b) +} + +func RoleFromJson(data io.Reader) *Role { + var r *Role + json.NewDecoder(data).Decode(&r) + return r +} + +func RoleListToJson(r []*Role) string { + b, _ := json.Marshal(r) + return string(b) +} + +func RoleListFromJson(data io.Reader) []*Role { + var roles []*Role + json.NewDecoder(data).Decode(&roles) + return roles +} + +func (r *RolePatch) ToJson() string { + b, _ := json.Marshal(r) + return string(b) +} + +func RolePatchFromJson(data io.Reader) *RolePatch { + var rolePatch *RolePatch + json.NewDecoder(data).Decode(&rolePatch) + return rolePatch +} + +func (r *Role) Patch(patch *RolePatch) { + if patch.Permissions != nil { + r.Permissions = *patch.Permissions + } +} + +// MergeChannelHigherScopedPermissions is meant to be invoked on a channel scheme's role and merges the higher-scoped +// channel role's permissions. +func (r *Role) MergeChannelHigherScopedPermissions(higherScopedPermissions *RolePermissions) { + mergedPermissions := []string{} + + higherScopedPermissionsMap := AsStringBoolMap(higherScopedPermissions.Permissions) + rolePermissionsMap := AsStringBoolMap(r.Permissions) + + for _, cp := range ALL_PERMISSIONS { + if cp.Scope != PERMISSION_SCOPE_CHANNEL { + continue + } + + _, presentOnHigherScope := higherScopedPermissionsMap[cp.Id] + + // For the channel admin role always look to the higher scope to determine if the role has ther permission. + // The channel admin is a special case because they're not part of the UI to be "channel moderated", only + // channel members and channel guests are. + if higherScopedPermissions.RoleID == CHANNEL_ADMIN_ROLE_ID && presentOnHigherScope { + mergedPermissions = append(mergedPermissions, cp.Id) + continue + } + + _, permissionIsModerated := CHANNEL_MODERATED_PERMISSIONS_MAP[cp.Id] + if permissionIsModerated { + _, presentOnRole := rolePermissionsMap[cp.Id] + if presentOnRole && presentOnHigherScope { + mergedPermissions = append(mergedPermissions, cp.Id) + } + } else { + if presentOnHigherScope { + mergedPermissions = append(mergedPermissions, cp.Id) + } + } + } + + r.Permissions = mergedPermissions +} + +// Returns an array of permissions that are in either role.Permissions +// or patch.Permissions, but not both. +func PermissionsChangedByPatch(role *Role, patch *RolePatch) []string { + var result []string + + if patch.Permissions == nil { + return result + } + + roleMap := make(map[string]bool) + patchMap := make(map[string]bool) + + for _, permission := range role.Permissions { + roleMap[permission] = true + } + + for _, permission := range *patch.Permissions { + patchMap[permission] = true + } + + for _, permission := range role.Permissions { + if !patchMap[permission] { + result = append(result, permission) + } + } + + for _, permission := range *patch.Permissions { + if !roleMap[permission] { + result = append(result, permission) + } + } + + return result +} + +func ChannelModeratedPermissionsChangedByPatch(role *Role, patch *RolePatch) []string { + var result []string + + if role == nil { + return result + } + + if patch.Permissions == nil { + return result + } + + roleMap := make(map[string]bool) + patchMap := make(map[string]bool) + + for _, permission := range role.Permissions { + if channelModeratedPermissionName, found := CHANNEL_MODERATED_PERMISSIONS_MAP[permission]; found { + roleMap[channelModeratedPermissionName] = true + } + } + + for _, permission := range *patch.Permissions { + if channelModeratedPermissionName, found := CHANNEL_MODERATED_PERMISSIONS_MAP[permission]; found { + patchMap[channelModeratedPermissionName] = true + } + } + + for permissionKey := range roleMap { + if !patchMap[permissionKey] { + result = append(result, permissionKey) + } + } + + for permissionKey := range patchMap { + if !roleMap[permissionKey] { + result = append(result, permissionKey) + } + } + + return result +} + +// GetChannelModeratedPermissions returns a map of channel moderated permissions that the role has access to +func (r *Role) GetChannelModeratedPermissions(channelType string) map[string]bool { + moderatedPermissions := make(map[string]bool) + for _, permission := range r.Permissions { + if _, found := CHANNEL_MODERATED_PERMISSIONS_MAP[permission]; !found { + continue + } + + for moderated, moderatedPermissionValue := range CHANNEL_MODERATED_PERMISSIONS_MAP { + // the moderated permission has already been found to be true so skip this iteration + if moderatedPermissions[moderatedPermissionValue] { + continue + } + + if moderated == permission { + // Special case where the channel moderated permission for `manage_members` is different depending on whether the channel is private or public + if moderated == PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id || moderated == PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id { + canManagePublic := channelType == CHANNEL_OPEN && moderated == PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id + canManagePrivate := channelType == CHANNEL_PRIVATE && moderated == PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id + moderatedPermissions[moderatedPermissionValue] = canManagePublic || canManagePrivate + } else { + moderatedPermissions[moderatedPermissionValue] = true + } + } + } + } + + return moderatedPermissions +} + +// RolePatchFromChannelModerationsPatch Creates and returns a RolePatch based on a slice of ChannelModerationPatchs, roleName is expected to be either "members" or "guests". +func (r *Role) RolePatchFromChannelModerationsPatch(channelModerationsPatch []*ChannelModerationPatch, roleName string) *RolePatch { + permissionsToAddToPatch := make(map[string]bool) + + // Iterate through the list of existing permissions on the role and append permissions that we want to keep. + for _, permission := range r.Permissions { + // Permission is not moderated so dont add it to the patch and skip the channelModerationsPatch + if _, isModerated := CHANNEL_MODERATED_PERMISSIONS_MAP[permission]; !isModerated { + continue + } + + permissionEnabled := true + // Check if permission has a matching moderated permission name inside the channel moderation patch + for _, channelModerationPatch := range channelModerationsPatch { + if *channelModerationPatch.Name == CHANNEL_MODERATED_PERMISSIONS_MAP[permission] { + // Permission key exists in patch with a value of false so skip over it + if roleName == "members" { + if channelModerationPatch.Roles.Members != nil && !*channelModerationPatch.Roles.Members { + permissionEnabled = false + } + } else if roleName == "guests" { + if channelModerationPatch.Roles.Guests != nil && !*channelModerationPatch.Roles.Guests { + permissionEnabled = false + } + } + } + } + + if permissionEnabled { + permissionsToAddToPatch[permission] = true + } + } + + // Iterate through the patch and add any permissions that dont already exist on the role + for _, channelModerationPatch := range channelModerationsPatch { + for permission, moderatedPermissionName := range CHANNEL_MODERATED_PERMISSIONS_MAP { + if roleName == "members" && channelModerationPatch.Roles.Members != nil && *channelModerationPatch.Roles.Members && *channelModerationPatch.Name == moderatedPermissionName { + permissionsToAddToPatch[permission] = true + } + + if roleName == "guests" && channelModerationPatch.Roles.Guests != nil && *channelModerationPatch.Roles.Guests && *channelModerationPatch.Name == moderatedPermissionName { + permissionsToAddToPatch[permission] = true + } + } + } + + patchPermissions := make([]string, 0, len(permissionsToAddToPatch)) + for permission := range permissionsToAddToPatch { + patchPermissions = append(patchPermissions, permission) + } + + return &RolePatch{Permissions: &patchPermissions} +} + +func (r *Role) IsValid() bool { + if !IsValidId(r.Id) { + return false + } + + return r.IsValidWithoutId() +} + +func (r *Role) IsValidWithoutId() bool { + if !IsValidRoleName(r.Name) { + return false + } + + if len(r.DisplayName) == 0 || len(r.DisplayName) > ROLE_DISPLAY_NAME_MAX_LENGTH { + return false + } + + if len(r.Description) > ROLE_DESCRIPTION_MAX_LENGTH { + return false + } + + for _, permission := range r.Permissions { + permissionValidated := false + for _, p := range ALL_PERMISSIONS { + if permission == p.Id { + permissionValidated = true + break + } + } + + if !permissionValidated { + return false + } + } + + return true +} + +func IsValidRoleName(roleName string) bool { + if len(roleName) <= 0 || len(roleName) > ROLE_NAME_MAX_LENGTH { + return false + } + + if strings.TrimLeft(roleName, "abcdefghijklmnopqrstuvwxyz0123456789_") != "" { + return false + } + + return true +} + +func MakeDefaultRoles() map[string]*Role { + roles := make(map[string]*Role) + + roles[CHANNEL_GUEST_ROLE_ID] = &Role{ + Name: "channel_guest", + DisplayName: "authentication.roles.channel_guest.name", + Description: "authentication.roles.channel_guest.description", + Permissions: []string{ + PERMISSION_READ_CHANNEL.Id, + PERMISSION_ADD_REACTION.Id, + PERMISSION_REMOVE_REACTION.Id, + PERMISSION_UPLOAD_FILE.Id, + PERMISSION_EDIT_POST.Id, + PERMISSION_CREATE_POST.Id, + PERMISSION_USE_CHANNEL_MENTIONS.Id, + PERMISSION_USE_SLASH_COMMANDS.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[CHANNEL_USER_ROLE_ID] = &Role{ + Name: "channel_user", + DisplayName: "authentication.roles.channel_user.name", + Description: "authentication.roles.channel_user.description", + Permissions: []string{ + PERMISSION_READ_CHANNEL.Id, + PERMISSION_ADD_REACTION.Id, + PERMISSION_REMOVE_REACTION.Id, + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + PERMISSION_UPLOAD_FILE.Id, + PERMISSION_GET_PUBLIC_LINK.Id, + PERMISSION_CREATE_POST.Id, + PERMISSION_USE_CHANNEL_MENTIONS.Id, + PERMISSION_USE_SLASH_COMMANDS.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[CHANNEL_ADMIN_ROLE_ID] = &Role{ + Name: "channel_admin", + DisplayName: "authentication.roles.channel_admin.name", + Description: "authentication.roles.channel_admin.description", + Permissions: []string{ + PERMISSION_MANAGE_CHANNEL_ROLES.Id, + PERMISSION_USE_GROUP_MENTIONS.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[TEAM_GUEST_ROLE_ID] = &Role{ + Name: "team_guest", + DisplayName: "authentication.roles.team_guest.name", + Description: "authentication.roles.team_guest.description", + Permissions: []string{ + PERMISSION_VIEW_TEAM.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[TEAM_USER_ROLE_ID] = &Role{ + Name: "team_user", + DisplayName: "authentication.roles.team_user.name", + Description: "authentication.roles.team_user.description", + Permissions: []string{ + PERMISSION_LIST_TEAM_CHANNELS.Id, + PERMISSION_JOIN_PUBLIC_CHANNELS.Id, + PERMISSION_READ_PUBLIC_CHANNEL.Id, + PERMISSION_VIEW_TEAM.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[TEAM_POST_ALL_ROLE_ID] = &Role{ + Name: "team_post_all", + DisplayName: "authentication.roles.team_post_all.name", + Description: "authentication.roles.team_post_all.description", + Permissions: []string{ + PERMISSION_CREATE_POST.Id, + PERMISSION_USE_CHANNEL_MENTIONS.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[TEAM_POST_ALL_PUBLIC_ROLE_ID] = &Role{ + Name: "team_post_all_public", + DisplayName: "authentication.roles.team_post_all_public.name", + Description: "authentication.roles.team_post_all_public.description", + Permissions: []string{ + PERMISSION_CREATE_POST_PUBLIC.Id, + PERMISSION_USE_CHANNEL_MENTIONS.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[TEAM_ADMIN_ROLE_ID] = &Role{ + Name: "team_admin", + DisplayName: "authentication.roles.team_admin.name", + Description: "authentication.roles.team_admin.description", + Permissions: []string{ + PERMISSION_REMOVE_USER_FROM_TEAM.Id, + PERMISSION_MANAGE_TEAM.Id, + PERMISSION_IMPORT_TEAM.Id, + PERMISSION_MANAGE_TEAM_ROLES.Id, + PERMISSION_MANAGE_CHANNEL_ROLES.Id, + PERMISSION_MANAGE_OTHERS_INCOMING_WEBHOOKS.Id, + PERMISSION_MANAGE_OTHERS_OUTGOING_WEBHOOKS.Id, + PERMISSION_MANAGE_SLASH_COMMANDS.Id, + PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id, + PERMISSION_MANAGE_INCOMING_WEBHOOKS.Id, + PERMISSION_MANAGE_OUTGOING_WEBHOOKS.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[SYSTEM_GUEST_ROLE_ID] = &Role{ + Name: "system_guest", + DisplayName: "authentication.roles.global_guest.name", + Description: "authentication.roles.global_guest.description", + Permissions: []string{ + PERMISSION_CREATE_DIRECT_CHANNEL.Id, + PERMISSION_CREATE_GROUP_CHANNEL.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[SYSTEM_USER_ROLE_ID] = &Role{ + Name: "system_user", + DisplayName: "authentication.roles.global_user.name", + Description: "authentication.roles.global_user.description", + Permissions: []string{ + PERMISSION_LIST_PUBLIC_TEAMS.Id, + PERMISSION_JOIN_PUBLIC_TEAMS.Id, + PERMISSION_CREATE_DIRECT_CHANNEL.Id, + PERMISSION_CREATE_GROUP_CHANNEL.Id, + PERMISSION_VIEW_MEMBERS.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[SYSTEM_POST_ALL_ROLE_ID] = &Role{ + Name: "system_post_all", + DisplayName: "authentication.roles.system_post_all.name", + Description: "authentication.roles.system_post_all.description", + Permissions: []string{ + PERMISSION_CREATE_POST.Id, + PERMISSION_USE_CHANNEL_MENTIONS.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SYSTEM_POST_ALL_PUBLIC_ROLE_ID] = &Role{ + Name: "system_post_all_public", + DisplayName: "authentication.roles.system_post_all_public.name", + Description: "authentication.roles.system_post_all_public.description", + Permissions: []string{ + PERMISSION_CREATE_POST_PUBLIC.Id, + PERMISSION_USE_CHANNEL_MENTIONS.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SYSTEM_USER_ACCESS_TOKEN_ROLE_ID] = &Role{ + Name: "system_user_access_token", + DisplayName: "authentication.roles.system_user_access_token.name", + Description: "authentication.roles.system_user_access_token.description", + Permissions: []string{ + PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, + PERMISSION_READ_USER_ACCESS_TOKEN.Id, + PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SYSTEM_ADMIN_ROLE_ID] = &Role{ + Name: "system_admin", + DisplayName: "authentication.roles.global_admin.name", + Description: "authentication.roles.global_admin.description", + // System admins can do anything channel and team admins can do + // plus everything members of teams and channels can do to all teams + // and channels on the system + Permissions: append( + append( + append( + append( + []string{ + PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE.Id, + PERMISSION_MANAGE_SYSTEM.Id, + PERMISSION_MANAGE_ROLES.Id, + PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id, + PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, + PERMISSION_DELETE_PUBLIC_CHANNEL.Id, + PERMISSION_CREATE_PUBLIC_CHANNEL.Id, + PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES.Id, + PERMISSION_DELETE_PRIVATE_CHANNEL.Id, + PERMISSION_CREATE_PRIVATE_CHANNEL.Id, + PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH.Id, + PERMISSION_MANAGE_OTHERS_INCOMING_WEBHOOKS.Id, + PERMISSION_MANAGE_OTHERS_OUTGOING_WEBHOOKS.Id, + PERMISSION_EDIT_OTHER_USERS.Id, + PERMISSION_EDIT_OTHERS_POSTS.Id, + PERMISSION_MANAGE_OAUTH.Id, + PERMISSION_INVITE_USER.Id, + PERMISSION_INVITE_GUEST.Id, + PERMISSION_PROMOTE_GUEST.Id, + PERMISSION_DEMOTE_TO_GUEST.Id, + PERMISSION_DELETE_POST.Id, + PERMISSION_DELETE_OTHERS_POSTS.Id, + PERMISSION_CREATE_TEAM.Id, + PERMISSION_ADD_USER_TO_TEAM.Id, + PERMISSION_LIST_USERS_WITHOUT_TEAM.Id, + PERMISSION_MANAGE_JOBS.Id, + PERMISSION_CREATE_POST_PUBLIC.Id, + PERMISSION_CREATE_POST_EPHEMERAL.Id, + PERMISSION_CREATE_USER_ACCESS_TOKEN.Id, + PERMISSION_READ_USER_ACCESS_TOKEN.Id, + PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id, + PERMISSION_CREATE_BOT.Id, + PERMISSION_READ_BOTS.Id, + PERMISSION_READ_OTHERS_BOTS.Id, + PERMISSION_MANAGE_BOTS.Id, + PERMISSION_MANAGE_OTHERS_BOTS.Id, + PERMISSION_REMOVE_OTHERS_REACTIONS.Id, + PERMISSION_LIST_PRIVATE_TEAMS.Id, + PERMISSION_JOIN_PRIVATE_TEAMS.Id, + PERMISSION_VIEW_MEMBERS.Id, + }, + roles[TEAM_USER_ROLE_ID].Permissions..., + ), + roles[CHANNEL_USER_ROLE_ID].Permissions..., + ), + roles[TEAM_ADMIN_ROLE_ID].Permissions..., + ), + roles[CHANNEL_ADMIN_ROLE_ID].Permissions..., + ), + SchemeManaged: true, + BuiltIn: true, + } + + return roles +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/saml.go b/vendor/github.com/mattermost/mattermost-server/v5/model/saml.go new file mode 100644 index 00000000..59ac2acc --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/saml.go @@ -0,0 +1,199 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "encoding/xml" + "io" + "time" +) + +const ( + USER_AUTH_SERVICE_SAML = "saml" + USER_AUTH_SERVICE_SAML_TEXT = "SAML" + USER_AUTH_SERVICE_IS_SAML = "isSaml" + USER_AUTH_SERVICE_IS_MOBILE = "isMobile" +) + +type SamlAuthRequest struct { + Base64AuthRequest string + URL string + RelayState string +} + +type SamlCertificateStatus struct { + IdpCertificateFile bool `json:"idp_certificate_file"` + PrivateKeyFile bool `json:"private_key_file"` + PublicCertificateFile bool `json:"public_certificate_file"` +} + +type SamlMetadataResponse struct { + IdpDescriptorUrl string `json:"idp_descriptor_url"` + IdpUrl string `json:"idp_url"` + IdpPublicCertificate string `json:"idp_public_certificate"` +} + +type NameIDFormat struct { + XMLName xml.Name + Format string `xml:",attr,omitempty"` + Value string `xml:",innerxml"` +} + +type NameID struct { + NameQualifier string `xml:",attr"` + SPNameQualifier string `xml:",attr"` + Format string `xml:",attr,omitempty"` + SPProvidedID string `xml:",attr"` + Value string `xml:",chardata"` +} + +type AttributeValue struct { + Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"` + Value string `xml:",chardata"` + NameID *NameID +} + +type Attribute struct { + XMLName xml.Name + FriendlyName string `xml:",attr"` + Name string `xml:",attr"` + NameFormat string `xml:",attr"` + Values []AttributeValue `xml:"AttributeValue"` +} + +type Endpoint struct { + XMLName xml.Name + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + ResponseLocation string `xml:"ResponseLocation,attr,omitempty"` +} + +type IndexedEndpoint struct { + XMLName xml.Name + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + ResponseLocation *string `xml:"ResponseLocation,attr,omitempty"` + Index int `xml:"index,attr"` + IsDefault *bool `xml:"isDefault,attr"` +} + +type IDPSSODescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"` + SSODescriptor + WantAuthnRequestsSigned *bool `xml:",attr"` + + SingleSignOnServices []Endpoint `xml:"SingleSignOnService"` + NameIDMappingServices []Endpoint `xml:"NameIDMappingService"` + AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"` + AttributeProfiles []string `xml:"AttributeProfile"` + Attributes []Attribute `xml:"Attribute"` +} + +type SSODescriptor struct { + XMLName xml.Name + RoleDescriptor + ArtifactResolutionServices []IndexedEndpoint `xml:"ArtifactResolutionService"` + SingleLogoutServices []Endpoint `xml:"SingleLogoutService"` + ManageNameIDServices []Endpoint `xml:"ManageNameIDService"` + NameIDFormats []NameIDFormat `xml:"NameIDFormat"` +} + +type X509Certificate struct { + XMLName xml.Name + Cert string `xml:",innerxml"` +} + +type X509Data struct { + XMLName xml.Name + X509Certificate X509Certificate `xml:"X509Certificate"` +} + +type KeyInfo struct { + XMLName xml.Name + DS string `xml:"xmlns:ds,attr"` + X509Data X509Data `xml:"X509Data"` +} +type EncryptionMethod struct { + Algorithm string `xml:"Algorithm,attr"` +} + +type KeyDescriptor struct { + XMLName xml.Name + Use string `xml:"use,attr,omitempty"` + KeyInfo KeyInfo `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo,omitempty"` +} + +type RoleDescriptor struct { + XMLName xml.Name + ID string `xml:",attr,omitempty"` + ValidUntil time.Time `xml:"validUntil,attr,omitempty"` + CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"` + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + ErrorURL string `xml:"errorURL,attr,omitempty"` + KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor,omitempty"` + Organization *Organization `xml:"Organization,omitempty"` + ContactPersons []ContactPerson `xml:"ContactPerson,omitempty"` +} + +type ContactPerson struct { + XMLName xml.Name + ContactType string `xml:"contactType,attr"` + Company string + GivenName string + SurName string + EmailAddresses []string `xml:"EmailAddress"` + TelephoneNumbers []string `xml:"TelephoneNumber"` +} + +type LocalizedName struct { + Lang string `xml:"xml lang,attr"` + Value string `xml:",chardata"` +} + +type LocalizedURI struct { + Lang string `xml:"xml lang,attr"` + Value string `xml:",chardata"` +} + +type Organization struct { + XMLName xml.Name + OrganizationNames []LocalizedName `xml:"OrganizationName"` + OrganizationDisplayNames []LocalizedName `xml:"OrganizationDisplayName"` + OrganizationURLs []LocalizedURI `xml:"OrganizationURL"` +} + +type EntityDescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"` + EntityID string `xml:"entityID,attr"` + ID string `xml:",attr,omitempty"` + ValidUntil time.Time `xml:"validUntil,attr,omitempty"` + CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"` + RoleDescriptors []RoleDescriptor `xml:"RoleDescriptor"` + IDPSSODescriptors []IDPSSODescriptor `xml:"IDPSSODescriptor"` + Organization Organization `xml:"Organization"` + ContactPerson ContactPerson `xml:"ContactPerson"` +} + +func (s *SamlCertificateStatus) ToJson() string { + b, _ := json.Marshal(s) + return string(b) +} + +func SamlCertificateStatusFromJson(data io.Reader) *SamlCertificateStatus { + var status *SamlCertificateStatus + json.NewDecoder(data).Decode(&status) + return status +} + +func (s *SamlMetadataResponse) ToJson() string { + b, _ := json.Marshal(s) + return string(b) +} + +func SamlMetadataResponseFromJson(data io.Reader) *SamlMetadataResponse { + var status *SamlMetadataResponse + json.NewDecoder(data).Decode(&status) + return status +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/scheduled_task.go b/vendor/github.com/mattermost/mattermost-server/v5/model/scheduled_task.go new file mode 100644 index 00000000..657cc749 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/scheduled_task.go @@ -0,0 +1,77 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "time" +) + +type TaskFunc func() + +type ScheduledTask struct { + Name string `json:"name"` + Interval time.Duration `json:"interval"` + Recurring bool `json:"recurring"` + function func() + cancel chan struct{} + cancelled chan struct{} +} + +func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask { + return createTask(name, function, timeToExecution, false) +} + +func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask { + return createTask(name, function, interval, true) +} + +func createTask(name string, function TaskFunc, interval time.Duration, recurring bool) *ScheduledTask { + task := &ScheduledTask{ + Name: name, + Interval: interval, + Recurring: recurring, + function: function, + cancel: make(chan struct{}), + cancelled: make(chan struct{}), + } + + go func() { + defer close(task.cancelled) + + ticker := time.NewTicker(interval) + defer func() { + ticker.Stop() + }() + + for { + select { + case <-ticker.C: + function() + case <-task.cancel: + return + } + + if !task.Recurring { + break + } + } + }() + + return task +} + +func (task *ScheduledTask) Cancel() { + close(task.cancel) + <-task.cancelled +} + +func (task *ScheduledTask) String() string { + return fmt.Sprintf( + "%s\nInterval: %s\nRecurring: %t\n", + task.Name, + task.Interval.String(), + task.Recurring, + ) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/scheme.go b/vendor/github.com/mattermost/mattermost-server/v5/model/scheme.go new file mode 100644 index 00000000..630f14a6 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/scheme.go @@ -0,0 +1,227 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "regexp" +) + +const ( + SCHEME_DISPLAY_NAME_MAX_LENGTH = 128 + SCHEME_NAME_MAX_LENGTH = 64 + SCHEME_DESCRIPTION_MAX_LENGTH = 1024 + SCHEME_SCOPE_TEAM = "team" + SCHEME_SCOPE_CHANNEL = "channel" +) + +type Scheme struct { + Id string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + Scope string `json:"scope"` + DefaultTeamAdminRole string `json:"default_team_admin_role"` + DefaultTeamUserRole string `json:"default_team_user_role"` + DefaultChannelAdminRole string `json:"default_channel_admin_role"` + DefaultChannelUserRole string `json:"default_channel_user_role"` + DefaultTeamGuestRole string `json:"default_team_guest_role"` + DefaultChannelGuestRole string `json:"default_channel_guest_role"` +} + +type SchemePatch struct { + Name *string `json:"name"` + DisplayName *string `json:"display_name"` + Description *string `json:"description"` +} + +type SchemeIDPatch struct { + SchemeID *string `json:"scheme_id"` +} + +// SchemeConveyor is used for importing and exporting a Scheme and its associated Roles. +type SchemeConveyor struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + Scope string `json:"scope"` + TeamAdmin string `json:"default_team_admin_role"` + TeamUser string `json:"default_team_user_role"` + TeamGuest string `json:"default_team_guest_role"` + ChannelAdmin string `json:"default_channel_admin_role"` + ChannelUser string `json:"default_channel_user_role"` + ChannelGuest string `json:"default_channel_guest_role"` + Roles []*Role `json:"roles"` +} + +func (sc *SchemeConveyor) Scheme() *Scheme { + return &Scheme{ + DisplayName: sc.DisplayName, + Name: sc.Name, + Description: sc.Description, + Scope: sc.Scope, + DefaultTeamAdminRole: sc.TeamAdmin, + DefaultTeamUserRole: sc.TeamUser, + DefaultTeamGuestRole: sc.TeamGuest, + DefaultChannelAdminRole: sc.ChannelAdmin, + DefaultChannelUserRole: sc.ChannelUser, + DefaultChannelGuestRole: sc.ChannelGuest, + } +} + +type SchemeRoles struct { + SchemeAdmin bool `json:"scheme_admin"` + SchemeUser bool `json:"scheme_user"` + SchemeGuest bool `json:"scheme_guest"` +} + +func (scheme *Scheme) ToJson() string { + b, _ := json.Marshal(scheme) + return string(b) +} + +func SchemeFromJson(data io.Reader) *Scheme { + var scheme *Scheme + json.NewDecoder(data).Decode(&scheme) + return scheme +} + +func SchemesToJson(schemes []*Scheme) string { + b, _ := json.Marshal(schemes) + return string(b) +} + +func SchemesFromJson(data io.Reader) []*Scheme { + var schemes []*Scheme + if err := json.NewDecoder(data).Decode(&schemes); err == nil { + return schemes + } else { + return nil + } +} + +func (scheme *Scheme) IsValid() bool { + if !IsValidId(scheme.Id) { + return false + } + + return scheme.IsValidForCreate() +} + +func (scheme *Scheme) IsValidForCreate() bool { + if len(scheme.DisplayName) == 0 || len(scheme.DisplayName) > SCHEME_DISPLAY_NAME_MAX_LENGTH { + return false + } + + if !IsValidSchemeName(scheme.Name) { + return false + } + + if len(scheme.Description) > SCHEME_DESCRIPTION_MAX_LENGTH { + return false + } + + switch scheme.Scope { + case SCHEME_SCOPE_TEAM, SCHEME_SCOPE_CHANNEL: + default: + return false + } + + if !IsValidRoleName(scheme.DefaultChannelAdminRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultChannelUserRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultChannelGuestRole) { + return false + } + + if scheme.Scope == SCHEME_SCOPE_TEAM { + if !IsValidRoleName(scheme.DefaultTeamAdminRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultTeamUserRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultTeamGuestRole) { + return false + } + } + + if scheme.Scope == SCHEME_SCOPE_CHANNEL { + if len(scheme.DefaultTeamAdminRole) != 0 { + return false + } + + if len(scheme.DefaultTeamUserRole) != 0 { + return false + } + + if len(scheme.DefaultTeamGuestRole) != 0 { + return false + } + } + + return true +} + +func (scheme *Scheme) Patch(patch *SchemePatch) { + if patch.DisplayName != nil { + scheme.DisplayName = *patch.DisplayName + } + if patch.Name != nil { + scheme.Name = *patch.Name + } + if patch.Description != nil { + scheme.Description = *patch.Description + } +} + +func (patch *SchemePatch) ToJson() string { + b, _ := json.Marshal(patch) + return string(b) +} + +func SchemePatchFromJson(data io.Reader) *SchemePatch { + var patch *SchemePatch + json.NewDecoder(data).Decode(&patch) + return patch +} + +func SchemeIDFromJson(data io.Reader) *string { + var p *SchemeIDPatch + json.NewDecoder(data).Decode(&p) + return p.SchemeID +} + +func (p *SchemeIDPatch) ToJson() string { + b, _ := json.Marshal(p) + return string(b) +} + +func IsValidSchemeName(name string) bool { + re := regexp.MustCompile(fmt.Sprintf("^[a-z0-9_]{2,%d}$", SCHEME_NAME_MAX_LENGTH)) + return re.MatchString(name) +} + +func (schemeRoles *SchemeRoles) ToJson() string { + b, _ := json.Marshal(schemeRoles) + return string(b) +} + +func SchemeRolesFromJson(data io.Reader) *SchemeRoles { + var schemeRoles *SchemeRoles + json.NewDecoder(data).Decode(&schemeRoles) + return schemeRoles +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/search_params.go b/vendor/github.com/mattermost/mattermost-server/v5/model/search_params.go new file mode 100644 index 00000000..e6dce73c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/search_params.go @@ -0,0 +1,369 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "regexp" + "strings" + "time" +) + +var searchTermPuncStart = regexp.MustCompile(`^[^\pL\d\s#"]+`) +var searchTermPuncEnd = regexp.MustCompile(`[^\pL\d\s*"]+$`) + +type SearchParams struct { + Terms string + ExcludedTerms string + IsHashtag bool + InChannels []string + ExcludedChannels []string + FromUsers []string + ExcludedUsers []string + AfterDate string + ExcludedAfterDate string + BeforeDate string + ExcludedBeforeDate string + OnDate string + ExcludedDate string + OrTerms bool + IncludeDeletedChannels bool + TimeZoneOffset int + // True if this search doesn't originate from a "current user". + SearchWithoutUserId bool +} + +// Returns the epoch timestamp of the start of the day specified by SearchParams.AfterDate +func (p *SearchParams) GetAfterDateMillis() int64 { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.AfterDate)) + if err != nil { + date = time.Now() + } + + // travel forward 1 day + oneDay := time.Hour * 24 + afterDate := date.Add(oneDay) + return GetStartOfDayMillis(afterDate, p.TimeZoneOffset) +} + +// Returns the epoch timestamp of the start of the day specified by SearchParams.ExcludedAfterDate +func (p *SearchParams) GetExcludedAfterDateMillis() int64 { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedAfterDate)) + if err != nil { + date = time.Now() + } + + // travel forward 1 day + oneDay := time.Hour * 24 + afterDate := date.Add(oneDay) + return GetStartOfDayMillis(afterDate, p.TimeZoneOffset) +} + +// Returns the epoch timestamp of the end of the day specified by SearchParams.BeforeDate +func (p *SearchParams) GetBeforeDateMillis() int64 { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.BeforeDate)) + if err != nil { + return 0 + } + + // travel back 1 day + oneDay := time.Hour * -24 + beforeDate := date.Add(oneDay) + return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset) +} + +// Returns the epoch timestamp of the end of the day specified by SearchParams.ExcludedBeforeDate +func (p *SearchParams) GetExcludedBeforeDateMillis() int64 { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedBeforeDate)) + if err != nil { + return 0 + } + + // travel back 1 day + oneDay := time.Hour * -24 + beforeDate := date.Add(oneDay) + return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset) +} + +// Returns the epoch timestamps of the start and end of the day specified by SearchParams.OnDate +func (p *SearchParams) GetOnDateMillis() (int64, int64) { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.OnDate)) + if err != nil { + return 0, 0 + } + + return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset) +} + +// Returns the epoch timestamps of the start and end of the day specified by SearchParams.ExcludedDate +func (p *SearchParams) GetExcludedDateMillis() (int64, int64) { + date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedDate)) + if err != nil { + return 0, 0 + } + + return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset) +} + +var searchFlags = [...]string{"from", "channel", "in", "before", "after", "on"} + +type flag struct { + name string + value string + exclude bool +} + +type searchWord struct { + value string + exclude bool +} + +func splitWords(text string) []string { + words := []string{} + + foundQuote := false + location := 0 + for i, char := range text { + if char == '"' { + if foundQuote { + // Grab the quoted section + word := text[location : i+1] + words = append(words, word) + foundQuote = false + location = i + 1 + } else { + nextStart := i + if i > 0 && text[i-1] == '-' { + nextStart = i - 1 + } + words = append(words, strings.Fields(text[location:nextStart])...) + foundQuote = true + location = nextStart + } + } + } + + words = append(words, strings.Fields(text[location:])...) + + return words +} + +func parseSearchFlags(input []string) ([]searchWord, []flag) { + words := []searchWord{} + flags := []flag{} + + skipNextWord := false + for i, word := range input { + if skipNextWord { + skipNextWord = false + continue + } + + isFlag := false + + if colon := strings.Index(word, ":"); colon != -1 { + var flagName string + var exclude bool + if strings.HasPrefix(word, "-") { + flagName = word[1:colon] + exclude = true + } else { + flagName = word[:colon] + exclude = false + } + + value := word[colon+1:] + + for _, searchFlag := range searchFlags { + // check for case insensitive equality + if strings.EqualFold(flagName, searchFlag) { + if value != "" { + flags = append(flags, flag{ + searchFlag, + value, + exclude, + }) + isFlag = true + } else if i < len(input)-1 { + flags = append(flags, flag{ + searchFlag, + input[i+1], + exclude, + }) + skipNextWord = true + isFlag = true + } + + if isFlag { + break + } + } + } + } + + if !isFlag { + exclude := false + if strings.HasPrefix(word, "-") { + exclude = true + } + // trim off surrounding punctuation (note that we leave trailing asterisks to allow wildcards) + word = searchTermPuncStart.ReplaceAllString(word, "") + word = searchTermPuncEnd.ReplaceAllString(word, "") + + // and remove extra pound #s + word = hashtagStart.ReplaceAllString(word, "#") + + if len(word) != 0 { + words = append(words, searchWord{ + word, + exclude, + }) + } + } + } + + return words, flags +} + +func ParseSearchParams(text string, timeZoneOffset int) []*SearchParams { + words, flags := parseSearchFlags(splitWords(text)) + + hashtagTermList := []string{} + excludedHashtagTermList := []string{} + plainTermList := []string{} + excludedPlainTermList := []string{} + + for _, word := range words { + if validHashtag.MatchString(word.value) { + if word.exclude { + excludedHashtagTermList = append(excludedHashtagTermList, word.value) + } else { + hashtagTermList = append(hashtagTermList, word.value) + } + } else { + if word.exclude { + excludedPlainTermList = append(excludedPlainTermList, word.value) + } else { + plainTermList = append(plainTermList, word.value) + } + } + } + + hashtagTerms := strings.Join(hashtagTermList, " ") + excludedHashtagTerms := strings.Join(excludedHashtagTermList, " ") + plainTerms := strings.Join(plainTermList, " ") + excludedPlainTerms := strings.Join(excludedPlainTermList, " ") + + inChannels := []string{} + excludedChannels := []string{} + fromUsers := []string{} + excludedUsers := []string{} + afterDate := "" + excludedAfterDate := "" + beforeDate := "" + excludedBeforeDate := "" + onDate := "" + excludedDate := "" + + for _, flag := range flags { + if flag.name == "in" || flag.name == "channel" { + if flag.exclude { + excludedChannels = append(excludedChannels, flag.value) + } else { + inChannels = append(inChannels, flag.value) + } + } else if flag.name == "from" { + if flag.exclude { + excludedUsers = append(excludedUsers, flag.value) + } else { + fromUsers = append(fromUsers, flag.value) + } + } else if flag.name == "after" { + if flag.exclude { + excludedAfterDate = flag.value + } else { + afterDate = flag.value + } + } else if flag.name == "before" { + if flag.exclude { + excludedBeforeDate = flag.value + } else { + beforeDate = flag.value + } + } else if flag.name == "on" { + if flag.exclude { + excludedDate = flag.value + } else { + onDate = flag.value + } + } + } + + paramsList := []*SearchParams{} + + if len(plainTerms) > 0 || len(excludedPlainTerms) > 0 { + paramsList = append(paramsList, &SearchParams{ + Terms: plainTerms, + ExcludedTerms: excludedPlainTerms, + IsHashtag: false, + InChannels: inChannels, + ExcludedChannels: excludedChannels, + FromUsers: fromUsers, + ExcludedUsers: excludedUsers, + AfterDate: afterDate, + ExcludedAfterDate: excludedAfterDate, + BeforeDate: beforeDate, + ExcludedBeforeDate: excludedBeforeDate, + OnDate: onDate, + ExcludedDate: excludedDate, + TimeZoneOffset: timeZoneOffset, + }) + } + + if len(hashtagTerms) > 0 || len(excludedHashtagTerms) > 0 { + paramsList = append(paramsList, &SearchParams{ + Terms: hashtagTerms, + ExcludedTerms: excludedHashtagTerms, + IsHashtag: true, + InChannels: inChannels, + ExcludedChannels: excludedChannels, + FromUsers: fromUsers, + ExcludedUsers: excludedUsers, + AfterDate: afterDate, + ExcludedAfterDate: excludedAfterDate, + BeforeDate: beforeDate, + ExcludedBeforeDate: excludedBeforeDate, + OnDate: onDate, + ExcludedDate: excludedDate, + TimeZoneOffset: timeZoneOffset, + }) + } + + // special case for when no terms are specified but we still have a filter + if len(plainTerms) == 0 && len(hashtagTerms) == 0 && + len(excludedPlainTerms) == 0 && len(excludedHashtagTerms) == 0 && + (len(inChannels) != 0 || len(fromUsers) != 0 || + len(excludedChannels) != 0 || len(excludedUsers) != 0 || + len(afterDate) != 0 || len(excludedAfterDate) != 0 || + len(beforeDate) != 0 || len(excludedBeforeDate) != 0 || + len(onDate) != 0 || len(excludedDate) != 0) { + paramsList = append(paramsList, &SearchParams{ + Terms: "", + ExcludedTerms: "", + IsHashtag: false, + InChannels: inChannels, + ExcludedChannels: excludedChannels, + FromUsers: fromUsers, + ExcludedUsers: excludedUsers, + AfterDate: afterDate, + ExcludedAfterDate: excludedAfterDate, + BeforeDate: beforeDate, + ExcludedBeforeDate: excludedBeforeDate, + OnDate: onDate, + ExcludedDate: excludedDate, + TimeZoneOffset: timeZoneOffset, + }) + } + + return paramsList +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/security_bulletin.go b/vendor/github.com/mattermost/mattermost-server/v5/model/security_bulletin.go new file mode 100644 index 00000000..ae66cf30 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/security_bulletin.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type SecurityBulletin struct { + Id string `json:"id"` + AppliesToVersion string `json:"applies_to_version"` +} + +type SecurityBulletins []SecurityBulletin + +func (me *SecurityBulletin) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func SecurityBulletinFromJson(data io.Reader) *SecurityBulletin { + var o *SecurityBulletin + json.NewDecoder(data).Decode(&o) + return o +} + +func (me SecurityBulletins) ToJson() string { + if b, err := json.Marshal(me); err != nil { + return "[]" + } else { + return string(b) + } +} + +func SecurityBulletinsFromJson(data io.Reader) SecurityBulletins { + var o SecurityBulletins + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/session.go b/vendor/github.com/mattermost/mattermost-server/v5/model/session.go new file mode 100644 index 00000000..b5567a65 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/session.go @@ -0,0 +1,208 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "strconv" + "strings" + + "github.com/mattermost/mattermost-server/v5/mlog" +) + +const ( + SESSION_COOKIE_TOKEN = "MMAUTHTOKEN" + SESSION_COOKIE_USER = "MMUSERID" + SESSION_COOKIE_CSRF = "MMCSRF" + SESSION_CACHE_SIZE = 35000 + SESSION_PROP_PLATFORM = "platform" + SESSION_PROP_OS = "os" + SESSION_PROP_BROWSER = "browser" + SESSION_PROP_TYPE = "type" + SESSION_PROP_USER_ACCESS_TOKEN_ID = "user_access_token_id" + SESSION_PROP_IS_BOT = "is_bot" + SESSION_PROP_IS_BOT_VALUE = "true" + SESSION_TYPE_USER_ACCESS_TOKEN = "UserAccessToken" + SESSION_PROP_IS_GUEST = "is_guest" + SESSION_ACTIVITY_TIMEOUT = 1000 * 60 * 5 // 5 minutes + SESSION_USER_ACCESS_TOKEN_EXPIRY = 100 * 365 // 100 years +) + +type Session struct { + Id string `json:"id"` + Token string `json:"token"` + CreateAt int64 `json:"create_at"` + ExpiresAt int64 `json:"expires_at"` + LastActivityAt int64 `json:"last_activity_at"` + UserId string `json:"user_id"` + DeviceId string `json:"device_id"` + Roles string `json:"roles"` + IsOAuth bool `json:"is_oauth"` + Props StringMap `json:"props"` + TeamMembers []*TeamMember `json:"team_members" db:"-"` + Local bool `json:"local" db:"-"` +} + +// Returns true if the session is unrestricted, which should grant it +// with all permissions. This is used for local mode sessions +func (me *Session) IsUnrestricted() bool { + return me.Local +} + +func (me *Session) DeepCopy() *Session { + copySession := *me + + if me.Props != nil { + copySession.Props = CopyStringMap(me.Props) + } + + if me.TeamMembers != nil { + copySession.TeamMembers = make([]*TeamMember, len(me.TeamMembers)) + for index, tm := range me.TeamMembers { + copySession.TeamMembers[index] = new(TeamMember) + *copySession.TeamMembers[index] = *tm + } + } + + return ©Session +} + +func (me *Session) ToJson() string { + b, _ := json.Marshal(me) + return string(b) +} + +func SessionFromJson(data io.Reader) *Session { + var me *Session + json.NewDecoder(data).Decode(&me) + return me +} + +func (me *Session) PreSave() { + if me.Id == "" { + me.Id = NewId() + } + + if me.Token == "" { + me.Token = NewId() + } + + me.CreateAt = GetMillis() + me.LastActivityAt = me.CreateAt + + if me.Props == nil { + me.Props = make(map[string]string) + } +} + +func (me *Session) Sanitize() { + me.Token = "" +} + +func (me *Session) IsExpired() bool { + + if me.ExpiresAt <= 0 { + return false + } + + if GetMillis() > me.ExpiresAt { + return true + } + + return false +} + +func (me *Session) SetExpireInDays(days int) { + if me.CreateAt == 0 { + me.ExpiresAt = GetMillis() + (1000 * 60 * 60 * 24 * int64(days)) + } else { + me.ExpiresAt = me.CreateAt + (1000 * 60 * 60 * 24 * int64(days)) + } +} + +func (me *Session) AddProp(key string, value string) { + + if me.Props == nil { + me.Props = make(map[string]string) + } + + me.Props[key] = value +} + +func (me *Session) GetTeamByTeamId(teamId string) *TeamMember { + for _, team := range me.TeamMembers { + if team.TeamId == teamId { + return team + } + } + + return nil +} + +func (me *Session) IsMobileApp() bool { + return len(me.DeviceId) > 0 || me.IsMobile() +} + +func (me *Session) IsMobile() bool { + val, ok := me.Props[USER_AUTH_SERVICE_IS_MOBILE] + if !ok { + return false + } + isMobile, err := strconv.ParseBool(val) + if err != nil { + mlog.Error("Error parsing boolean property from Session", mlog.Err(err)) + return false + } + return isMobile +} + +func (me *Session) IsSaml() bool { + val, ok := me.Props[USER_AUTH_SERVICE_IS_SAML] + if !ok { + return false + } + isSaml, err := strconv.ParseBool(val) + if err != nil { + mlog.Error("Error parsing boolean property from Session", mlog.Err(err)) + return false + } + return isSaml +} + +func (me *Session) IsSSOLogin() bool { + return me.IsOAuth || me.IsSaml() +} + +func (me *Session) GetUserRoles() []string { + return strings.Fields(me.Roles) +} + +func (me *Session) GenerateCSRF() string { + token := NewId() + me.AddProp("csrf", token) + return token +} + +func (me *Session) GetCSRF() string { + if me.Props == nil { + return "" + } + + return me.Props["csrf"] +} + +func SessionsToJson(o []*Session) string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func SessionsFromJson(data io.Reader) []*Session { + var o []*Session + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/slack_attachment.go b/vendor/github.com/mattermost/mattermost-server/v5/model/slack_attachment.go new file mode 100644 index 00000000..a85c6be2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/slack_attachment.go @@ -0,0 +1,193 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "regexp" +) + +var linkWithTextRegex = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) + +type SlackAttachment struct { + Id int64 `json:"id"` + Fallback string `json:"fallback"` + Color string `json:"color"` + Pretext string `json:"pretext"` + AuthorName string `json:"author_name"` + AuthorLink string `json:"author_link"` + AuthorIcon string `json:"author_icon"` + Title string `json:"title"` + TitleLink string `json:"title_link"` + Text string `json:"text"` + Fields []*SlackAttachmentField `json:"fields"` + ImageURL string `json:"image_url"` + ThumbURL string `json:"thumb_url"` + Footer string `json:"footer"` + FooterIcon string `json:"footer_icon"` + Timestamp interface{} `json:"ts"` // This is either a string or an int64 + Actions []*PostAction `json:"actions,omitempty"` +} + +func (s *SlackAttachment) Equals(input *SlackAttachment) bool { + // Direct comparison of simple types + + if s.Id != input.Id { + return false + } + + if s.Fallback != input.Fallback { + return false + } + + if s.Color != input.Color { + return false + } + + if s.Pretext != input.Pretext { + return false + } + + if s.AuthorName != input.AuthorName { + return false + } + + if s.AuthorLink != input.AuthorLink { + return false + } + + if s.AuthorIcon != input.AuthorIcon { + return false + } + + if s.Title != input.Title { + return false + } + + if s.TitleLink != input.TitleLink { + return false + } + + if s.Text != input.Text { + return false + } + + if s.ImageURL != input.ImageURL { + return false + } + + if s.ThumbURL != input.ThumbURL { + return false + } + + if s.Footer != input.Footer { + return false + } + + if s.FooterIcon != input.FooterIcon { + return false + } + + // Compare length & slice values of fields + if len(s.Fields) != len(input.Fields) { + return false + } + + for j := range s.Fields { + if !s.Fields[j].Equals(input.Fields[j]) { + return false + } + } + + // Compare length & slice values of actions + if len(s.Actions) != len(input.Actions) { + return false + } + + for j := range s.Actions { + if !s.Actions[j].Equals(input.Actions[j]) { + return false + } + } + + return s.Timestamp == input.Timestamp +} + +type SlackAttachmentField struct { + Title string `json:"title"` + Value interface{} `json:"value"` + Short SlackCompatibleBool `json:"short"` +} + +func (s *SlackAttachmentField) Equals(input *SlackAttachmentField) bool { + if s.Title != input.Title { + return false + } + + if s.Value != input.Value { + return false + } + + if s.Short != input.Short { + return false + } + + return true +} + +func StringifySlackFieldValue(a []*SlackAttachment) []*SlackAttachment { + var nonNilAttachments []*SlackAttachment + for _, attachment := range a { + if attachment == nil { + continue + } + nonNilAttachments = append(nonNilAttachments, attachment) + + var nonNilFields []*SlackAttachmentField + for _, field := range attachment.Fields { + if field == nil { + continue + } + nonNilFields = append(nonNilFields, field) + + if field.Value != nil { + // Ensure the value is set to a string if it is set + field.Value = fmt.Sprintf("%v", field.Value) + } + } + attachment.Fields = nonNilFields + } + return nonNilAttachments +} + +// This method only parses and processes the attachments, +// all else should be set in the post which is passed +func ParseSlackAttachment(post *Post, attachments []*SlackAttachment) { + if post.Type == "" { + post.Type = POST_SLACK_ATTACHMENT + } + + postAttachments := []*SlackAttachment{} + + for _, attachment := range attachments { + if attachment == nil { + continue + } + + attachment.Text = ParseSlackLinksToMarkdown(attachment.Text) + attachment.Pretext = ParseSlackLinksToMarkdown(attachment.Pretext) + + for _, field := range attachment.Fields { + if value, ok := field.Value.(string); ok { + field.Value = ParseSlackLinksToMarkdown(value) + } + } + postAttachments = append(postAttachments, attachment) + } + post.AddProp("attachments", postAttachments) +} + +func ParseSlackLinksToMarkdown(text string) string { + return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/slack_compatibility.go b/vendor/github.com/mattermost/mattermost-server/v5/model/slack_compatibility.go new file mode 100644 index 00000000..2d3e2878 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/slack_compatibility.go @@ -0,0 +1,30 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "strings" +) + +// SlackCompatibleBool is an alias for bool that implements json.Unmarshaler +type SlackCompatibleBool bool + +// UnmarshalJSON implements json.Unmarshaler +// +// Slack allows bool values to be represented as strings ("true"/"false") or +// literals (true/false). To maintain compatibility, we define an Unmarshaler +// that supports both. +func (b *SlackCompatibleBool) UnmarshalJSON(data []byte) error { + value := strings.ToLower(string(data)) + if value == "true" || value == `"true"` { + *b = true + } else if value == "false" || value == `"false"` { + *b = false + } else { + return fmt.Errorf("unmarshal: unable to convert %s to bool", data) + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/status.go b/vendor/github.com/mattermost/mattermost-server/v5/model/status.go new file mode 100644 index 00000000..741fa1ed --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/status.go @@ -0,0 +1,75 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + STATUS_OUT_OF_OFFICE = "ooo" + STATUS_OFFLINE = "offline" + STATUS_AWAY = "away" + STATUS_DND = "dnd" + STATUS_ONLINE = "online" + STATUS_CACHE_SIZE = SESSION_CACHE_SIZE + STATUS_CHANNEL_TIMEOUT = 20000 // 20 seconds + STATUS_MIN_UPDATE_TIME = 120000 // 2 minutes +) + +type Status struct { + UserId string `json:"user_id"` + Status string `json:"status"` + Manual bool `json:"manual"` + LastActivityAt int64 `json:"last_activity_at"` + ActiveChannel string `json:"active_channel,omitempty" db:"-"` +} + +func (o *Status) ToJson() string { + oCopy := *o + oCopy.ActiveChannel = "" + b, _ := json.Marshal(oCopy) + return string(b) +} + +func (o *Status) ToClusterJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func StatusFromJson(data io.Reader) *Status { + var o *Status + json.NewDecoder(data).Decode(&o) + return o +} + +func StatusListToJson(u []*Status) string { + uCopy := make([]Status, len(u)) + for i, s := range u { + sCopy := *s + sCopy.ActiveChannel = "" + uCopy[i] = sCopy + } + + b, _ := json.Marshal(uCopy) + return string(b) +} + +func StatusListFromJson(data io.Reader) []*Status { + var statuses []*Status + json.NewDecoder(data).Decode(&statuses) + return statuses +} + +func StatusMapToInterfaceMap(statusMap map[string]*Status) map[string]interface{} { + interfaceMap := map[string]interface{}{} + for _, s := range statusMap { + // Omitted statues mean offline + if s.Status != STATUS_OFFLINE { + interfaceMap[s.UserId] = s.Status + } + } + return interfaceMap +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/suggest_command.go b/vendor/github.com/mattermost/mattermost-server/v5/model/suggest_command.go new file mode 100644 index 00000000..45a7af38 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/suggest_command.go @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type SuggestCommand struct { + Suggestion string `json:"suggestion"` + Description string `json:"description"` +} + +func (o *SuggestCommand) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func SuggestCommandFromJson(data io.Reader) *SuggestCommand { + var o *SuggestCommand + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/switch_request.go b/vendor/github.com/mattermost/mattermost-server/v5/model/switch_request.go new file mode 100644 index 00000000..0ec4db7d --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/switch_request.go @@ -0,0 +1,53 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type SwitchRequest struct { + CurrentService string `json:"current_service"` + NewService string `json:"new_service"` + Email string `json:"email"` + Password string `json:"password"` + NewPassword string `json:"new_password"` + MfaCode string `json:"mfa_code"` + LdapLoginId string `json:"ldap_id"` +} + +func (o *SwitchRequest) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func SwitchRequestFromJson(data io.Reader) *SwitchRequest { + var o *SwitchRequest + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *SwitchRequest) EmailToOAuth() bool { + return o.CurrentService == USER_AUTH_SERVICE_EMAIL && + (o.NewService == USER_AUTH_SERVICE_SAML || + o.NewService == USER_AUTH_SERVICE_GITLAB || + o.NewService == SERVICE_GOOGLE || + o.NewService == SERVICE_OFFICE365) +} + +func (o *SwitchRequest) OAuthToEmail() bool { + return (o.CurrentService == USER_AUTH_SERVICE_SAML || + o.CurrentService == USER_AUTH_SERVICE_GITLAB || + o.CurrentService == SERVICE_GOOGLE || + o.CurrentService == SERVICE_OFFICE365) && o.NewService == USER_AUTH_SERVICE_EMAIL +} + +func (o *SwitchRequest) EmailToLdap() bool { + return o.CurrentService == USER_AUTH_SERVICE_EMAIL && o.NewService == USER_AUTH_SERVICE_LDAP +} + +func (o *SwitchRequest) LdapToEmail() bool { + return o.CurrentService == USER_AUTH_SERVICE_LDAP && o.NewService == USER_AUTH_SERVICE_EMAIL +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/system.go b/vendor/github.com/mattermost/mattermost-server/v5/model/system.go new file mode 100644 index 00000000..4c3132e2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/system.go @@ -0,0 +1,71 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "math/big" +) + +const ( + SYSTEM_DIAGNOSTIC_ID = "DiagnosticId" + SYSTEM_RAN_UNIT_TESTS = "RanUnitTests" + SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime" + SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId" + SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime" + SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey" + SYSTEM_POST_ACTION_COOKIE_SECRET = "PostActionCookieSecret" + SYSTEM_INSTALLATION_DATE_KEY = "InstallationDate" + SYSTEM_FIRST_SERVER_RUN_TIMESTAMP_KEY = "FirstServerRunTimestamp" +) + +type System struct { + Name string `json:"name"` + Value string `json:"value"` +} + +func (o *System) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func SystemFromJson(data io.Reader) *System { + var o *System + json.NewDecoder(data).Decode(&o) + return o +} + +type SystemPostActionCookieSecret struct { + Secret []byte `json:"key,omitempty"` +} + +type SystemAsymmetricSigningKey struct { + ECDSAKey *SystemECDSAKey `json:"ecdsa_key,omitempty"` +} + +type SystemECDSAKey struct { + Curve string `json:"curve"` + X *big.Int `json:"x"` + Y *big.Int `json:"y"` + D *big.Int `json:"d,omitempty"` +} + +// ServerBusyState provides serialization for app.Busy. +type ServerBusyState struct { + Busy bool `json:"busy"` + Expires int64 `json:"expires"` + Expires_ts string `json:"expires_ts,omitempty"` +} + +func (sbs *ServerBusyState) ToJson() string { + b, _ := json.Marshal(sbs) + return string(b) +} + +func ServerBusyStateFromJson(r io.Reader) *ServerBusyState { + var sbs *ServerBusyState + json.NewDecoder(r).Decode(&sbs) + return sbs +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/team.go b/vendor/github.com/mattermost/mattermost-server/v5/model/team.go new file mode 100644 index 00000000..381eb8bb --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/team.go @@ -0,0 +1,330 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "strings" + "unicode/utf8" +) + +const ( + TEAM_OPEN = "O" + TEAM_INVITE = "I" + TEAM_ALLOWED_DOMAINS_MAX_LENGTH = 500 + TEAM_COMPANY_NAME_MAX_LENGTH = 64 + TEAM_DESCRIPTION_MAX_LENGTH = 255 + TEAM_DISPLAY_NAME_MAX_RUNES = 64 + TEAM_EMAIL_MAX_LENGTH = 128 + TEAM_NAME_MAX_LENGTH = 64 + TEAM_NAME_MIN_LENGTH = 2 +) + +type Team struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Description string `json:"description"` + Email string `json:"email"` + Type string `json:"type"` + CompanyName string `json:"company_name"` + AllowedDomains string `json:"allowed_domains"` + InviteId string `json:"invite_id"` + AllowOpenInvite bool `json:"allow_open_invite"` + LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"` + SchemeId *string `json:"scheme_id"` + GroupConstrained *bool `json:"group_constrained"` +} + +type TeamPatch struct { + DisplayName *string `json:"display_name"` + Description *string `json:"description"` + CompanyName *string `json:"company_name"` + AllowedDomains *string `json:"allowed_domains"` + AllowOpenInvite *bool `json:"allow_open_invite"` + GroupConstrained *bool `json:"group_constrained"` +} + +type TeamForExport struct { + Team + SchemeName *string +} + +type Invites struct { + Invites []map[string]string `json:"invites"` +} + +type TeamsWithCount struct { + Teams []*Team `json:"teams"` + TotalCount int64 `json:"total_count"` +} + +func InvitesFromJson(data io.Reader) *Invites { + var o *Invites + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *Invites) ToEmailList() []string { + emailList := make([]string, len(o.Invites)) + for _, invite := range o.Invites { + emailList = append(emailList, invite["email"]) + } + return emailList +} + +func (o *Invites) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func (o *Team) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func TeamFromJson(data io.Reader) *Team { + var o *Team + json.NewDecoder(data).Decode(&o) + return o +} + +func TeamMapToJson(u map[string]*Team) string { + b, _ := json.Marshal(u) + return string(b) +} + +func TeamMapFromJson(data io.Reader) map[string]*Team { + var teams map[string]*Team + json.NewDecoder(data).Decode(&teams) + return teams +} + +func TeamListToJson(t []*Team) string { + b, _ := json.Marshal(t) + return string(b) +} + +func TeamsWithCountToJson(tlc *TeamsWithCount) []byte { + b, _ := json.Marshal(tlc) + return b +} + +func TeamsWithCountFromJson(data io.Reader) *TeamsWithCount { + var twc *TeamsWithCount + json.NewDecoder(data).Decode(&twc) + return twc +} + +func TeamListFromJson(data io.Reader) []*Team { + var teams []*Team + json.NewDecoder(data).Decode(&teams) + return teams +} + +func (o *Team) Etag() string { + return Etag(o.Id, o.UpdateAt) +} + +func (o *Team) IsValid() *AppError { + + if !IsValidId(o.Id) { + return NewAppError("Team.IsValid", "model.team.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("Team.IsValid", "model.team.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if o.UpdateAt == 0 { + return NewAppError("Team.IsValid", "model.team.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.Email) > TEAM_EMAIL_MAX_LENGTH { + return NewAppError("Team.IsValid", "model.team.is_valid.email.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.Email) > 0 && !IsValidEmail(o.Email) { + return NewAppError("Team.IsValid", "model.team.is_valid.email.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.DisplayName) == 0 || utf8.RuneCountInString(o.DisplayName) > TEAM_DISPLAY_NAME_MAX_RUNES { + return NewAppError("Team.IsValid", "model.team.is_valid.name.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.Name) > TEAM_NAME_MAX_LENGTH { + return NewAppError("Team.IsValid", "model.team.is_valid.url.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.Description) > TEAM_DESCRIPTION_MAX_LENGTH { + return NewAppError("Team.IsValid", "model.team.is_valid.description.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.InviteId) == 0 { + return NewAppError("Team.IsValid", "model.team.is_valid.invite_id.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if IsReservedTeamName(o.Name) { + return NewAppError("Team.IsValid", "model.team.is_valid.reserved.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if !IsValidTeamName(o.Name) { + return NewAppError("Team.IsValid", "model.team.is_valid.characters.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if !(o.Type == TEAM_OPEN || o.Type == TEAM_INVITE) { + return NewAppError("Team.IsValid", "model.team.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.CompanyName) > TEAM_COMPANY_NAME_MAX_LENGTH { + return NewAppError("Team.IsValid", "model.team.is_valid.company.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.AllowedDomains) > TEAM_ALLOWED_DOMAINS_MAX_LENGTH { + return NewAppError("Team.IsValid", "model.team.is_valid.domains.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + return nil +} + +func (o *Team) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt + + o.Name = SanitizeUnicode(o.Name) + o.DisplayName = SanitizeUnicode(o.DisplayName) + o.Description = SanitizeUnicode(o.Description) + o.CompanyName = SanitizeUnicode(o.CompanyName) + + if len(o.InviteId) == 0 { + o.InviteId = NewId() + } +} + +func (o *Team) PreUpdate() { + o.UpdateAt = GetMillis() + o.Name = SanitizeUnicode(o.Name) + o.DisplayName = SanitizeUnicode(o.DisplayName) + o.Description = SanitizeUnicode(o.Description) + o.CompanyName = SanitizeUnicode(o.CompanyName) +} + +func IsReservedTeamName(s string) bool { + s = strings.ToLower(s) + + for _, value := range reservedName { + if strings.Index(s, value) == 0 { + return true + } + } + + return false +} + +func IsValidTeamName(s string) bool { + if !IsValidAlphaNum(s) { + return false + } + + if len(s) < TEAM_NAME_MIN_LENGTH { + return false + } + + return true +} + +var validTeamNameCharacter = regexp.MustCompile(`^[a-z0-9-]$`) + +func CleanTeamName(s string) string { + s = strings.ToLower(strings.Replace(s, " ", "-", -1)) + + for _, value := range reservedName { + if strings.Index(s, value) == 0 { + s = strings.Replace(s, value, "", -1) + } + } + + s = strings.TrimSpace(s) + + for _, c := range s { + char := fmt.Sprintf("%c", c) + if !validTeamNameCharacter.MatchString(char) { + s = strings.Replace(s, char, "", -1) + } + } + + s = strings.Trim(s, "-") + + if !IsValidTeamName(s) { + s = NewId() + } + + return s +} + +func (o *Team) Sanitize() { + o.Email = "" + o.InviteId = "" +} + +func (o *Team) Patch(patch *TeamPatch) { + if patch.DisplayName != nil { + o.DisplayName = *patch.DisplayName + } + + if patch.Description != nil { + o.Description = *patch.Description + } + + if patch.CompanyName != nil { + o.CompanyName = *patch.CompanyName + } + + if patch.AllowedDomains != nil { + o.AllowedDomains = *patch.AllowedDomains + } + + if patch.AllowOpenInvite != nil { + o.AllowOpenInvite = *patch.AllowOpenInvite + } + + if patch.GroupConstrained != nil { + o.GroupConstrained = patch.GroupConstrained + } +} + +func (o *Team) IsGroupConstrained() bool { + return o.GroupConstrained != nil && *o.GroupConstrained +} + +func (t *TeamPatch) ToJson() string { + b, err := json.Marshal(t) + if err != nil { + return "" + } + + return string(b) +} + +func TeamPatchFromJson(data io.Reader) *TeamPatch { + decoder := json.NewDecoder(data) + var team TeamPatch + err := decoder.Decode(&team) + if err != nil { + return nil + } + + return &team +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/team_member.go b/vendor/github.com/mattermost/mattermost-server/v5/model/team_member.go new file mode 100644 index 00000000..b747f17c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/team_member.go @@ -0,0 +1,186 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +const ( + USERNAME = "Username" +) + +type TeamMember struct { + TeamId string `json:"team_id"` + UserId string `json:"user_id"` + Roles string `json:"roles"` + DeleteAt int64 `json:"delete_at"` + SchemeGuest bool `json:"scheme_guest"` + SchemeUser bool `json:"scheme_user"` + SchemeAdmin bool `json:"scheme_admin"` + ExplicitRoles string `json:"explicit_roles"` +} + +type TeamUnread struct { + TeamId string `json:"team_id"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` +} + +type TeamMemberForExport struct { + TeamMember + TeamName string +} + +type TeamMemberWithError struct { + UserId string `json:"user_id"` + Member *TeamMember `json:"member"` + Error *AppError `json:"error"` +} + +type EmailInviteWithError struct { + Email string `json:"email"` + Error *AppError `json:"error"` +} + +type TeamMembersGetOptions struct { + // Sort the team members. Accepts "Username", but defaults to "Id". + Sort string + + // If true, exclude team members whose corresponding user is deleted. + ExcludeDeletedUsers bool + + // Restrict to search in a list of teams and channels + ViewRestrictions *ViewUsersRestrictions +} + +func (o *TeamMember) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func (o *TeamUnread) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func TeamMemberFromJson(data io.Reader) *TeamMember { + var o *TeamMember + json.NewDecoder(data).Decode(&o) + return o +} + +func TeamUnreadFromJson(data io.Reader) *TeamUnread { + var o *TeamUnread + json.NewDecoder(data).Decode(&o) + return o +} + +func EmailInviteWithErrorFromJson(data io.Reader) []*EmailInviteWithError { + var o []*EmailInviteWithError + json.NewDecoder(data).Decode(&o) + return o +} + +func EmailInviteWithErrorToEmails(o []*EmailInviteWithError) []string { + var ret []string + for _, o := range o { + if o.Error == nil { + ret = append(ret, o.Email) + } + } + return ret +} + +func EmailInviteWithErrorToJson(o []*EmailInviteWithError) string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func EmailInviteWithErrorToString(o *EmailInviteWithError) string { + return fmt.Sprintf("%s:%s", o.Email, o.Error.Error()) +} + +func TeamMembersWithErrorToTeamMembers(o []*TeamMemberWithError) []*TeamMember { + var ret []*TeamMember + for _, o := range o { + if o.Error == nil { + ret = append(ret, o.Member) + } + } + return ret +} + +func TeamMembersWithErrorToJson(o []*TeamMemberWithError) string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func TeamMemberWithErrorToString(o *TeamMemberWithError) string { + return fmt.Sprintf("%s:%s", o.UserId, o.Error.Error()) +} + +func TeamMembersWithErrorFromJson(data io.Reader) []*TeamMemberWithError { + var o []*TeamMemberWithError + json.NewDecoder(data).Decode(&o) + return o +} + +func TeamMembersToJson(o []*TeamMember) string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func TeamMembersFromJson(data io.Reader) []*TeamMember { + var o []*TeamMember + json.NewDecoder(data).Decode(&o) + return o +} + +func TeamsUnreadToJson(o []*TeamUnread) string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func TeamsUnreadFromJson(data io.Reader) []*TeamUnread { + var o []*TeamUnread + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *TeamMember) IsValid() *AppError { + + if !IsValidId(o.TeamId) { + return NewAppError("TeamMember.IsValid", "model.team_member.is_valid.team_id.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(o.UserId) { + return NewAppError("TeamMember.IsValid", "model.team_member.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (o *TeamMember) PreUpdate() { +} + +func (o *TeamMember) GetRoles() []string { + return strings.Fields(o.Roles) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/team_search.go b/vendor/github.com/mattermost/mattermost-server/v5/model/team_search.go new file mode 100644 index 00000000..b8b1fe30 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/team_search.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type TeamSearch struct { + Term string `json:"term"` + Page *int `json:"page,omitempty"` + PerPage *int `json:"per_page,omitempty"` +} + +func (t *TeamSearch) IsPaginated() bool { + return t.Page != nil && t.PerPage != nil +} + +// ToJson convert a TeamSearch to json string +func (t *TeamSearch) ToJson() string { + b, err := json.Marshal(t) + if err != nil { + return "" + } + + return string(b) +} + +// TeamSearchFromJson decodes the input and returns a TeamSearch +func TeamSearchFromJson(data io.Reader) *TeamSearch { + decoder := json.NewDecoder(data) + var cs TeamSearch + err := decoder.Decode(&cs) + if err == nil { + return &cs + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/team_stats.go b/vendor/github.com/mattermost/mattermost-server/v5/model/team_stats.go new file mode 100644 index 00000000..9209a0cf --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/team_stats.go @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type TeamStats struct { + TeamId string `json:"team_id"` + TotalMemberCount int64 `json:"total_member_count"` + ActiveMemberCount int64 `json:"active_member_count"` +} + +func (o *TeamStats) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func TeamStatsFromJson(data io.Reader) *TeamStats { + var o *TeamStats + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/terms_of_service.go b/vendor/github.com/mattermost/mattermost-server/v5/model/terms_of_service.go new file mode 100644 index 00000000..8ce5d350 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/terms_of_service.go @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "unicode/utf8" +) + +const TERMS_OF_SERVICE_CACHE_SIZE = 1 + +type TermsOfService struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UserId string `json:"user_id"` + Text string `json:"text"` +} + +func (t *TermsOfService) IsValid() *AppError { + if !IsValidId(t.Id) { + return InvalidTermsOfServiceError("id", "") + } + + if t.CreateAt == 0 { + return InvalidTermsOfServiceError("create_at", t.Id) + } + + if !IsValidId(t.UserId) { + return InvalidTermsOfServiceError("user_id", t.Id) + } + + if utf8.RuneCountInString(t.Text) > POST_MESSAGE_MAX_RUNES_V2 { + return InvalidTermsOfServiceError("text", t.Id) + } + + return nil +} + +func (t *TermsOfService) ToJson() string { + b, _ := json.Marshal(t) + return string(b) +} + +func TermsOfServiceFromJson(data io.Reader) *TermsOfService { + var termsOfService *TermsOfService + json.NewDecoder(data).Decode(&termsOfService) + return termsOfService +} + +func InvalidTermsOfServiceError(fieldName string, termsOfServiceId string) *AppError { + id := fmt.Sprintf("model.terms_of_service.is_valid.%s.app_error", fieldName) + details := "" + if termsOfServiceId != "" { + details = "terms_of_service_id=" + termsOfServiceId + } + return NewAppError("TermsOfService.IsValid", id, map[string]interface{}{"MaxLength": POST_MESSAGE_MAX_RUNES_V2}, details, http.StatusBadRequest) +} + +func (t *TermsOfService) PreSave() { + if t.Id == "" { + t.Id = NewId() + } + + t.CreateAt = GetMillis() +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/token.go b/vendor/github.com/mattermost/mattermost-server/v5/model/token.go new file mode 100644 index 00000000..0730778c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/token.go @@ -0,0 +1,40 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import "net/http" + +const ( + TOKEN_SIZE = 64 + MAX_TOKEN_EXIPRY_TIME = 1000 * 60 * 60 * 48 // 48 hour + TOKEN_TYPE_OAUTH = "oauth" +) + +type Token struct { + Token string + CreateAt int64 + Type string + Extra string +} + +func NewToken(tokentype, extra string) *Token { + return &Token{ + Token: NewRandomString(TOKEN_SIZE), + CreateAt: GetMillis(), + Type: tokentype, + Extra: extra, + } +} + +func (t *Token) IsValid() *AppError { + if len(t.Token) != TOKEN_SIZE { + return NewAppError("Token.IsValid", "model.token.is_valid.size", nil, "", http.StatusInternalServerError) + } + + if t.CreateAt == 0 { + return NewAppError("Token.IsValid", "model.token.is_valid.expiry", nil, "", http.StatusInternalServerError) + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/user.go b/vendor/github.com/mattermost/mattermost-server/v5/model/user.go new file mode 100644 index 00000000..168605ad --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/user.go @@ -0,0 +1,903 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "math/rand" + "net/http" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "github.com/mattermost/mattermost-server/v5/services/timezones" + "golang.org/x/crypto/bcrypt" + "golang.org/x/text/language" +) + +const ( + ME = "me" + USER_NOTIFY_ALL = "all" + USER_NOTIFY_HERE = "here" + USER_NOTIFY_MENTION = "mention" + USER_NOTIFY_NONE = "none" + DESKTOP_NOTIFY_PROP = "desktop" + DESKTOP_SOUND_NOTIFY_PROP = "desktop_sound" + MARK_UNREAD_NOTIFY_PROP = "mark_unread" + PUSH_NOTIFY_PROP = "push" + PUSH_STATUS_NOTIFY_PROP = "push_status" + EMAIL_NOTIFY_PROP = "email" + CHANNEL_MENTIONS_NOTIFY_PROP = "channel" + COMMENTS_NOTIFY_PROP = "comments" + MENTION_KEYS_NOTIFY_PROP = "mention_keys" + COMMENTS_NOTIFY_NEVER = "never" + COMMENTS_NOTIFY_ROOT = "root" + COMMENTS_NOTIFY_ANY = "any" + FIRST_NAME_NOTIFY_PROP = "first_name" + AUTO_RESPONDER_ACTIVE_NOTIFY_PROP = "auto_responder_active" + AUTO_RESPONDER_MESSAGE_NOTIFY_PROP = "auto_responder_message" + + DEFAULT_LOCALE = "en" + USER_AUTH_SERVICE_EMAIL = "email" + + USER_EMAIL_MAX_LENGTH = 128 + USER_NICKNAME_MAX_RUNES = 64 + USER_POSITION_MAX_RUNES = 128 + USER_FIRST_NAME_MAX_RUNES = 64 + USER_LAST_NAME_MAX_RUNES = 64 + USER_AUTH_DATA_MAX_LENGTH = 128 + USER_NAME_MAX_LENGTH = 64 + USER_NAME_MIN_LENGTH = 1 + USER_PASSWORD_MAX_LENGTH = 72 + USER_LOCALE_MAX_LENGTH = 5 +) + +type User struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at,omitempty"` + UpdateAt int64 `json:"update_at,omitempty"` + DeleteAt int64 `json:"delete_at"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + AuthData *string `json:"auth_data,omitempty"` + AuthService string `json:"auth_service"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified,omitempty"` + Nickname string `json:"nickname"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Position string `json:"position"` + Roles string `json:"roles"` + AllowMarketing bool `json:"allow_marketing,omitempty"` + Props StringMap `json:"props,omitempty"` + NotifyProps StringMap `json:"notify_props,omitempty"` + LastPasswordUpdate int64 `json:"last_password_update,omitempty"` + LastPictureUpdate int64 `json:"last_picture_update,omitempty"` + FailedAttempts int `json:"failed_attempts,omitempty"` + Locale string `json:"locale"` + Timezone StringMap `json:"timezone"` + MfaActive bool `json:"mfa_active,omitempty"` + MfaSecret string `json:"mfa_secret,omitempty"` + LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"` + IsBot bool `db:"-" json:"is_bot,omitempty"` + BotDescription string `db:"-" json:"bot_description,omitempty"` + BotLastIconUpdate int64 `db:"-" json:"bot_last_icon_update,omitempty"` + TermsOfServiceId string `db:"-" json:"terms_of_service_id,omitempty"` + TermsOfServiceCreateAt int64 `db:"-" json:"terms_of_service_create_at,omitempty"` +} + +type UserUpdate struct { + Old *User + New *User +} + +type UserPatch struct { + Username *string `json:"username"` + Password *string `json:"password,omitempty"` + Nickname *string `json:"nickname"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + Position *string `json:"position"` + Email *string `json:"email"` + Props StringMap `json:"props,omitempty"` + NotifyProps StringMap `json:"notify_props,omitempty"` + Locale *string `json:"locale"` + Timezone StringMap `json:"timezone"` +} + +type UserAuth struct { + Password string `json:"password,omitempty"` + AuthData *string `json:"auth_data,omitempty"` + AuthService string `json:"auth_service,omitempty"` +} + +type UserForIndexing struct { + Id string `json:"id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + CreateAt int64 `json:"create_at"` + DeleteAt int64 `json:"delete_at"` + TeamsIds []string `json:"team_id"` + ChannelsIds []string `json:"channel_id"` +} + +type ViewUsersRestrictions struct { + Teams []string + Channels []string +} + +func (r *ViewUsersRestrictions) Hash() string { + if r == nil { + return "" + } + ids := append(r.Teams, r.Channels...) + sort.Strings(ids) + hash := sha256.New() + hash.Write([]byte(strings.Join(ids, ""))) + return fmt.Sprintf("%x", hash.Sum(nil)) +} + +type UserSlice []*User + +func (u UserSlice) Usernames() []string { + usernames := []string{} + for _, user := range u { + usernames = append(usernames, user.Username) + } + sort.Strings(usernames) + return usernames +} + +func (u UserSlice) IDs() []string { + ids := []string{} + for _, user := range u { + ids = append(ids, user.Id) + } + return ids +} + +func (u UserSlice) FilterWithoutBots() UserSlice { + var matches []*User + + for _, user := range u { + if !user.IsBot { + matches = append(matches, user) + } + } + return UserSlice(matches) +} + +func (u UserSlice) FilterByActive(active bool) UserSlice { + var matches []*User + + for _, user := range u { + if user.DeleteAt == 0 && active { + matches = append(matches, user) + } else if user.DeleteAt != 0 && !active { + matches = append(matches, user) + } + } + return UserSlice(matches) +} + +func (u UserSlice) FilterByID(ids []string) UserSlice { + var matches []*User + for _, user := range u { + for _, id := range ids { + if id == user.Id { + matches = append(matches, user) + } + } + } + return UserSlice(matches) +} + +func (u UserSlice) FilterWithoutID(ids []string) UserSlice { + var keep []*User + for _, user := range u { + present := false + for _, id := range ids { + if id == user.Id { + present = true + } + } + if !present { + keep = append(keep, user) + } + } + return UserSlice(keep) +} + +func (u *User) DeepCopy() *User { + copyUser := *u + if u.AuthData != nil { + copyUser.AuthData = NewString(*u.AuthData) + } + if u.Props != nil { + copyUser.Props = CopyStringMap(u.Props) + } + if u.NotifyProps != nil { + copyUser.NotifyProps = CopyStringMap(u.NotifyProps) + } + if u.Timezone != nil { + copyUser.Timezone = CopyStringMap(u.Timezone) + } + return ©User +} + +// IsValid validates the user and returns an error if it isn't configured +// correctly. +func (u *User) IsValid() *AppError { + + if !IsValidId(u.Id) { + return InvalidUserError("id", "") + } + + if u.CreateAt == 0 { + return InvalidUserError("create_at", u.Id) + } + + if u.UpdateAt == 0 { + return InvalidUserError("update_at", u.Id) + } + + if !IsValidUsername(u.Username) { + return InvalidUserError("username", u.Id) + } + + if len(u.Email) > USER_EMAIL_MAX_LENGTH || len(u.Email) == 0 || !IsValidEmail(u.Email) { + return InvalidUserError("email", u.Id) + } + + if utf8.RuneCountInString(u.Nickname) > USER_NICKNAME_MAX_RUNES { + return InvalidUserError("nickname", u.Id) + } + + if utf8.RuneCountInString(u.Position) > USER_POSITION_MAX_RUNES { + return InvalidUserError("position", u.Id) + } + + if utf8.RuneCountInString(u.FirstName) > USER_FIRST_NAME_MAX_RUNES { + return InvalidUserError("first_name", u.Id) + } + + if utf8.RuneCountInString(u.LastName) > USER_LAST_NAME_MAX_RUNES { + return InvalidUserError("last_name", u.Id) + } + + if u.AuthData != nil && len(*u.AuthData) > USER_AUTH_DATA_MAX_LENGTH { + return InvalidUserError("auth_data", u.Id) + } + + if u.AuthData != nil && len(*u.AuthData) > 0 && len(u.AuthService) == 0 { + return InvalidUserError("auth_data_type", u.Id) + } + + if len(u.Password) > 0 && u.AuthData != nil && len(*u.AuthData) > 0 { + return InvalidUserError("auth_data_pwd", u.Id) + } + + if len(u.Password) > USER_PASSWORD_MAX_LENGTH { + return InvalidUserError("password_limit", u.Id) + } + + if !IsValidLocale(u.Locale) { + return InvalidUserError("locale", u.Id) + } + + return nil +} + +func InvalidUserError(fieldName string, userId string) *AppError { + id := fmt.Sprintf("model.user.is_valid.%s.app_error", fieldName) + details := "" + if userId != "" { + details = "user_id=" + userId + } + return NewAppError("User.IsValid", id, nil, details, http.StatusBadRequest) +} + +func NormalizeUsername(username string) string { + return strings.ToLower(username) +} + +func NormalizeEmail(email string) string { + return strings.ToLower(email) +} + +// PreSave will set the Id and Username if missing. It will also fill +// in the CreateAt, UpdateAt times. It will also hash the password. It should +// be run before saving the user to the db. +func (u *User) PreSave() { + if u.Id == "" { + u.Id = NewId() + } + + if u.Username == "" { + u.Username = NewId() + } + + if u.AuthData != nil && *u.AuthData == "" { + u.AuthData = nil + } + + u.Username = SanitizeUnicode(u.Username) + u.FirstName = SanitizeUnicode(u.FirstName) + u.LastName = SanitizeUnicode(u.LastName) + u.Nickname = SanitizeUnicode(u.Nickname) + + u.Username = NormalizeUsername(u.Username) + u.Email = NormalizeEmail(u.Email) + + u.CreateAt = GetMillis() + u.UpdateAt = u.CreateAt + + u.LastPasswordUpdate = u.CreateAt + + u.MfaActive = false + + if u.Locale == "" { + u.Locale = DEFAULT_LOCALE + } + + if u.Props == nil { + u.Props = make(map[string]string) + } + + if u.NotifyProps == nil || len(u.NotifyProps) == 0 { + u.SetDefaultNotifications() + } + + if u.Timezone == nil { + u.Timezone = timezones.DefaultUserTimezone() + } + + if len(u.Password) > 0 { + u.Password = HashPassword(u.Password) + } +} + +// PreUpdate should be run before updating the user in the db. +func (u *User) PreUpdate() { + u.Username = SanitizeUnicode(u.Username) + u.FirstName = SanitizeUnicode(u.FirstName) + u.LastName = SanitizeUnicode(u.LastName) + u.Nickname = SanitizeUnicode(u.Nickname) + u.BotDescription = SanitizeUnicode(u.BotDescription) + + u.Username = NormalizeUsername(u.Username) + u.Email = NormalizeEmail(u.Email) + u.UpdateAt = GetMillis() + + u.FirstName = SanitizeUnicode(u.FirstName) + u.LastName = SanitizeUnicode(u.LastName) + u.Nickname = SanitizeUnicode(u.Nickname) + u.BotDescription = SanitizeUnicode(u.BotDescription) + + if u.AuthData != nil && *u.AuthData == "" { + u.AuthData = nil + } + + if u.NotifyProps == nil || len(u.NotifyProps) == 0 { + u.SetDefaultNotifications() + } else if _, ok := u.NotifyProps[MENTION_KEYS_NOTIFY_PROP]; ok { + // Remove any blank mention keys + splitKeys := strings.Split(u.NotifyProps[MENTION_KEYS_NOTIFY_PROP], ",") + goodKeys := []string{} + for _, key := range splitKeys { + if len(key) > 0 { + goodKeys = append(goodKeys, strings.ToLower(key)) + } + } + u.NotifyProps[MENTION_KEYS_NOTIFY_PROP] = strings.Join(goodKeys, ",") + } +} + +func (u *User) SetDefaultNotifications() { + u.NotifyProps = make(map[string]string) + u.NotifyProps[EMAIL_NOTIFY_PROP] = "true" + u.NotifyProps[PUSH_NOTIFY_PROP] = USER_NOTIFY_MENTION + u.NotifyProps[DESKTOP_NOTIFY_PROP] = USER_NOTIFY_MENTION + u.NotifyProps[DESKTOP_SOUND_NOTIFY_PROP] = "true" + u.NotifyProps[MENTION_KEYS_NOTIFY_PROP] = "" + u.NotifyProps[CHANNEL_MENTIONS_NOTIFY_PROP] = "true" + u.NotifyProps[PUSH_STATUS_NOTIFY_PROP] = STATUS_AWAY + u.NotifyProps[COMMENTS_NOTIFY_PROP] = COMMENTS_NOTIFY_NEVER + u.NotifyProps[FIRST_NAME_NOTIFY_PROP] = "false" +} + +func (u *User) UpdateMentionKeysFromUsername(oldUsername string) { + nonUsernameKeys := []string{} + for _, key := range u.GetMentionKeys() { + if key != oldUsername && key != "@"+oldUsername { + nonUsernameKeys = append(nonUsernameKeys, key) + } + } + + u.NotifyProps[MENTION_KEYS_NOTIFY_PROP] = "" + if len(nonUsernameKeys) > 0 { + u.NotifyProps[MENTION_KEYS_NOTIFY_PROP] += "," + strings.Join(nonUsernameKeys, ",") + } +} + +func (u *User) GetMentionKeys() []string { + var keys []string + + for _, key := range strings.Split(u.NotifyProps[MENTION_KEYS_NOTIFY_PROP], ",") { + trimmedKey := strings.TrimSpace(key) + + if trimmedKey == "" { + continue + } + + keys = append(keys, trimmedKey) + } + + return keys +} + +func (u *User) Patch(patch *UserPatch) { + if patch.Username != nil { + u.Username = *patch.Username + } + + if patch.Nickname != nil { + u.Nickname = *patch.Nickname + } + + if patch.FirstName != nil { + u.FirstName = *patch.FirstName + } + + if patch.LastName != nil { + u.LastName = *patch.LastName + } + + if patch.Position != nil { + u.Position = *patch.Position + } + + if patch.Email != nil { + u.Email = *patch.Email + } + + if patch.Props != nil { + u.Props = patch.Props + } + + if patch.NotifyProps != nil { + u.NotifyProps = patch.NotifyProps + } + + if patch.Locale != nil { + u.Locale = *patch.Locale + } + + if patch.Timezone != nil { + u.Timezone = patch.Timezone + } +} + +// ToJson convert a User to a json string +func (u *User) ToJson() string { + b, _ := json.Marshal(u) + return string(b) +} + +func (u *UserPatch) ToJson() string { + b, _ := json.Marshal(u) + return string(b) +} + +func (u *UserAuth) ToJson() string { + b, _ := json.Marshal(u) + return string(b) +} + +// Generate a valid strong etag so the browser can cache the results +func (u *User) Etag(showFullName, showEmail bool) string { + return Etag(u.Id, u.UpdateAt, u.TermsOfServiceId, u.TermsOfServiceCreateAt, showFullName, showEmail, u.BotLastIconUpdate) +} + +// Remove any private data from the user object +func (u *User) Sanitize(options map[string]bool) { + u.Password = "" + u.AuthData = NewString("") + u.MfaSecret = "" + + if len(options) != 0 && !options["email"] { + u.Email = "" + } + if len(options) != 0 && !options["fullname"] { + u.FirstName = "" + u.LastName = "" + } + if len(options) != 0 && !options["passwordupdate"] { + u.LastPasswordUpdate = 0 + } + if len(options) != 0 && !options["authservice"] { + u.AuthService = "" + } +} + +// Remove any input data from the user object that is not user controlled +func (u *User) SanitizeInput(isAdmin bool) { + if !isAdmin { + u.AuthData = NewString("") + u.AuthService = "" + } + u.LastPasswordUpdate = 0 + u.LastPictureUpdate = 0 + u.FailedAttempts = 0 + u.EmailVerified = false + u.MfaActive = false + u.MfaSecret = "" +} + +func (u *User) ClearNonProfileFields() { + u.Password = "" + u.AuthData = NewString("") + u.MfaSecret = "" + u.EmailVerified = false + u.AllowMarketing = false + u.NotifyProps = StringMap{} + u.LastPasswordUpdate = 0 + u.FailedAttempts = 0 +} + +func (u *User) SanitizeProfile(options map[string]bool) { + u.ClearNonProfileFields() + + u.Sanitize(options) +} + +func (u *User) MakeNonNil() { + if u.Props == nil { + u.Props = make(map[string]string) + } + + if u.NotifyProps == nil { + u.NotifyProps = make(map[string]string) + } +} + +func (u *User) AddNotifyProp(key string, value string) { + u.MakeNonNil() + + u.NotifyProps[key] = value +} + +func (u *User) GetFullName() string { + if len(u.FirstName) > 0 && len(u.LastName) > 0 { + return u.FirstName + " " + u.LastName + } else if len(u.FirstName) > 0 { + return u.FirstName + } else if len(u.LastName) > 0 { + return u.LastName + } else { + return "" + } +} + +func (u *User) getDisplayName(baseName, nameFormat string) string { + displayName := baseName + + if nameFormat == SHOW_NICKNAME_FULLNAME { + if len(u.Nickname) > 0 { + displayName = u.Nickname + } else if fullName := u.GetFullName(); len(fullName) > 0 { + displayName = fullName + } + } else if nameFormat == SHOW_FULLNAME { + if fullName := u.GetFullName(); len(fullName) > 0 { + displayName = fullName + } + } + + return displayName +} + +func (u *User) GetDisplayName(nameFormat string) string { + displayName := u.Username + + return u.getDisplayName(displayName, nameFormat) +} + +func (u *User) GetDisplayNameWithPrefix(nameFormat, prefix string) string { + displayName := prefix + u.Username + + return u.getDisplayName(displayName, nameFormat) +} + +func (u *User) GetRoles() []string { + return strings.Fields(u.Roles) +} + +func (u *User) GetRawRoles() string { + return u.Roles +} + +func IsValidUserRoles(userRoles string) bool { + + roles := strings.Fields(userRoles) + + for _, r := range roles { + if !IsValidRoleName(r) { + return false + } + } + + // Exclude just the system_admin role explicitly to prevent mistakes + if len(roles) == 1 && roles[0] == "system_admin" { + return false + } + + return true +} + +// Make sure you acually want to use this function. In context.go there are functions to check permissions +// This function should not be used to check permissions. +func (u *User) IsGuest() bool { + return IsInRole(u.Roles, SYSTEM_GUEST_ROLE_ID) +} + +func (u *User) IsSystemAdmin() bool { + return IsInRole(u.Roles, SYSTEM_ADMIN_ROLE_ID) +} + +// Make sure you acually want to use this function. In context.go there are functions to check permissions +// This function should not be used to check permissions. +func (u *User) IsInRole(inRole string) bool { + return IsInRole(u.Roles, inRole) +} + +// Make sure you acually want to use this function. In context.go there are functions to check permissions +// This function should not be used to check permissions. +func IsInRole(userRoles string, inRole string) bool { + roles := strings.Split(userRoles, " ") + + for _, r := range roles { + if r == inRole { + return true + } + } + + return false +} + +func (u *User) IsSSOUser() bool { + return u.AuthService != "" && u.AuthService != USER_AUTH_SERVICE_EMAIL +} + +func (u *User) IsOAuthUser() bool { + return u.AuthService == USER_AUTH_SERVICE_GITLAB +} + +func (u *User) IsLDAPUser() bool { + return u.AuthService == USER_AUTH_SERVICE_LDAP +} + +func (u *User) IsSAMLUser() bool { + return u.AuthService == USER_AUTH_SERVICE_SAML +} + +func (u *User) GetPreferredTimezone() string { + return GetPreferredTimezone(u.Timezone) +} + +// UserFromJson will decode the input and return a User +func UserFromJson(data io.Reader) *User { + var user *User + json.NewDecoder(data).Decode(&user) + return user +} + +func UserPatchFromJson(data io.Reader) *UserPatch { + var user *UserPatch + json.NewDecoder(data).Decode(&user) + return user +} + +func UserAuthFromJson(data io.Reader) *UserAuth { + var user *UserAuth + json.NewDecoder(data).Decode(&user) + return user +} + +func UserMapToJson(u map[string]*User) string { + b, _ := json.Marshal(u) + return string(b) +} + +func UserMapFromJson(data io.Reader) map[string]*User { + var users map[string]*User + json.NewDecoder(data).Decode(&users) + return users +} + +func UserListToJson(u []*User) string { + b, _ := json.Marshal(u) + return string(b) +} + +func UserListFromJson(data io.Reader) []*User { + var users []*User + json.NewDecoder(data).Decode(&users) + return users +} + +// HashPassword generates a hash using the bcrypt.GenerateFromPassword +func HashPassword(password string) string { + hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) + if err != nil { + panic(err) + } + + return string(hash) +} + +// ComparePassword compares the hash +func ComparePassword(hash string, password string) bool { + + if len(password) == 0 || len(hash) == 0 { + return false + } + + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`) + +var restrictedUsernames = []string{ + "all", + "channel", + "matterbot", + "system", +} + +func IsValidUsername(s string) bool { + if len(s) < USER_NAME_MIN_LENGTH || len(s) > USER_NAME_MAX_LENGTH { + return false + } + + if !validUsernameChars.MatchString(s) { + return false + } + + for _, restrictedUsername := range restrictedUsernames { + if s == restrictedUsername { + return false + } + } + + return true +} + +func CleanUsername(s string) string { + s = NormalizeUsername(strings.Replace(s, " ", "-", -1)) + + for _, value := range reservedName { + if s == value { + s = strings.Replace(s, value, "", -1) + } + } + + s = strings.TrimSpace(s) + + for _, c := range s { + char := fmt.Sprintf("%c", c) + if !validUsernameChars.MatchString(char) { + s = strings.Replace(s, char, "-", -1) + } + } + + s = strings.Trim(s, "-") + + if !IsValidUsername(s) { + s = "a" + NewId() + } + + return s +} + +func IsValidUserNotifyLevel(notifyLevel string) bool { + return notifyLevel == CHANNEL_NOTIFY_ALL || + notifyLevel == CHANNEL_NOTIFY_MENTION || + notifyLevel == CHANNEL_NOTIFY_NONE +} + +func IsValidPushStatusNotifyLevel(notifyLevel string) bool { + return notifyLevel == STATUS_ONLINE || + notifyLevel == STATUS_AWAY || + notifyLevel == STATUS_OFFLINE +} + +func IsValidCommentsNotifyLevel(notifyLevel string) bool { + return notifyLevel == COMMENTS_NOTIFY_ANY || + notifyLevel == COMMENTS_NOTIFY_ROOT || + notifyLevel == COMMENTS_NOTIFY_NEVER +} + +func IsValidEmailBatchingInterval(emailInterval string) bool { + return emailInterval == PREFERENCE_EMAIL_INTERVAL_IMMEDIATELY || + emailInterval == PREFERENCE_EMAIL_INTERVAL_FIFTEEN || + emailInterval == PREFERENCE_EMAIL_INTERVAL_HOUR +} + +func IsValidLocale(locale string) bool { + if locale != "" { + if len(locale) > USER_LOCALE_MAX_LENGTH { + return false + } else if _, err := language.Parse(locale); err != nil { + return false + } + } + + return true +} + +type UserWithGroups struct { + User + GroupIDs *string `json:"-"` + Groups []*Group `json:"groups"` + SchemeGuest bool `json:"scheme_guest"` + SchemeUser bool `json:"scheme_user"` + SchemeAdmin bool `json:"scheme_admin"` +} + +func (u *UserWithGroups) GetGroupIDs() []string { + if u.GroupIDs == nil { + return nil + } + trimmed := strings.TrimSpace(*u.GroupIDs) + if len(trimmed) == 0 { + return nil + } + return strings.Split(trimmed, ",") +} + +type UsersWithGroupsAndCount struct { + Users []*UserWithGroups `json:"users"` + Count int64 `json:"total_count"` +} + +func UsersWithGroupsAndCountFromJson(data io.Reader) *UsersWithGroupsAndCount { + uwg := &UsersWithGroupsAndCount{} + bodyBytes, _ := ioutil.ReadAll(data) + json.Unmarshal(bodyBytes, uwg) + return uwg +} + +var passwordRandomSource = rand.NewSource(time.Now().Unix()) +var passwordSpecialChars = "!$%^&*(),." +var passwordNumbers = "0123456789" +var passwordUpperCaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +var passwordLowerCaseLetters = "abcdefghijklmnopqrstuvwxyz" +var passwordAllChars = passwordSpecialChars + passwordNumbers + passwordUpperCaseLetters + passwordLowerCaseLetters + +func GeneratePassword(minimumLength int) string { + r := rand.New(passwordRandomSource) + + // Make sure we are guaranteed at least one of each type to meet any possible password complexity requirements. + password := string([]rune(passwordUpperCaseLetters)[r.Intn(len(passwordUpperCaseLetters))]) + + string([]rune(passwordNumbers)[r.Intn(len(passwordNumbers))]) + + string([]rune(passwordLowerCaseLetters)[r.Intn(len(passwordLowerCaseLetters))]) + + string([]rune(passwordSpecialChars)[r.Intn(len(passwordSpecialChars))]) + + for len(password) < minimumLength { + i := r.Intn(len(passwordAllChars)) + password = password + string([]rune(passwordAllChars)[i]) + } + + return password +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/user_access_token.go b/vendor/github.com/mattermost/mattermost-server/v5/model/user_access_token.go new file mode 100644 index 00000000..f458a6d9 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/user_access_token.go @@ -0,0 +1,65 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "net/http" +) + +type UserAccessToken struct { + Id string `json:"id"` + Token string `json:"token,omitempty"` + UserId string `json:"user_id"` + Description string `json:"description"` + IsActive bool `json:"is_active"` +} + +func (t *UserAccessToken) IsValid() *AppError { + if !IsValidId(t.Id) { + return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if len(t.Token) != 26 { + return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.token.app_error", nil, "", http.StatusBadRequest) + } + + if !IsValidId(t.UserId) { + return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(t.Description) > 255 { + return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.description.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (t *UserAccessToken) PreSave() { + t.Id = NewId() + t.IsActive = true +} + +func (t *UserAccessToken) ToJson() string { + b, _ := json.Marshal(t) + return string(b) +} + +func UserAccessTokenFromJson(data io.Reader) *UserAccessToken { + var t *UserAccessToken + json.NewDecoder(data).Decode(&t) + return t +} + +func UserAccessTokenListToJson(t []*UserAccessToken) string { + b, _ := json.Marshal(t) + return string(b) +} + +func UserAccessTokenListFromJson(data io.Reader) []*UserAccessToken { + var t []*UserAccessToken + json.NewDecoder(data).Decode(&t) + return t +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/user_access_token_search.go b/vendor/github.com/mattermost/mattermost-server/v5/model/user_access_token_search.go new file mode 100644 index 00000000..a692f692 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/user_access_token_search.go @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type UserAccessTokenSearch struct { + Term string `json:"term"` +} + +// ToJson convert a UserAccessTokenSearch to json string +func (c *UserAccessTokenSearch) ToJson() string { + b, err := json.Marshal(c) + if err != nil { + return "" + } + + return string(b) +} + +// UserAccessTokenSearchJson decodes the input and returns a UserAccessTokenSearch +func UserAccessTokenSearchFromJson(data io.Reader) *UserAccessTokenSearch { + decoder := json.NewDecoder(data) + var cs UserAccessTokenSearch + err := decoder.Decode(&cs) + if err == nil { + return &cs + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/user_autocomplete.go b/vendor/github.com/mattermost/mattermost-server/v5/model/user_autocomplete.go new file mode 100644 index 00000000..48a892e2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/user_autocomplete.go @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type UserAutocompleteInChannel struct { + InChannel []*User `json:"in_channel"` + OutOfChannel []*User `json:"out_of_channel"` +} + +type UserAutocompleteInTeam struct { + InTeam []*User `json:"in_team"` +} + +type UserAutocomplete struct { + Users []*User `json:"users"` + OutOfChannel []*User `json:"out_of_channel,omitempty"` +} + +func (o *UserAutocomplete) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func UserAutocompleteFromJson(data io.Reader) *UserAutocomplete { + decoder := json.NewDecoder(data) + autocomplete := new(UserAutocomplete) + err := decoder.Decode(&autocomplete) + if err == nil { + return autocomplete + } else { + return nil + } +} + +func (o *UserAutocompleteInChannel) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func UserAutocompleteInChannelFromJson(data io.Reader) *UserAutocompleteInChannel { + var o *UserAutocompleteInChannel + json.NewDecoder(data).Decode(&o) + return o +} + +func (o *UserAutocompleteInTeam) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func UserAutocompleteInTeamFromJson(data io.Reader) *UserAutocompleteInTeam { + var o *UserAutocompleteInTeam + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/user_count.go b/vendor/github.com/mattermost/mattermost-server/v5/model/user_count.go new file mode 100644 index 00000000..3c20b23a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/user_count.go @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +// Options for counting users +type UserCountOptions struct { + // Should include users that are bots + IncludeBotAccounts bool + // Should include deleted users (of any type) + IncludeDeleted bool + // Exclude regular users + ExcludeRegularUsers bool + // Only include users on a specific team. "" for any team. + TeamId string + // Restrict to search in a list of teams and channels + ViewRestrictions *ViewUsersRestrictions +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/user_get.go b/vendor/github.com/mattermost/mattermost-server/v5/model/user_get.go new file mode 100644 index 00000000..f865d53c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/user_get.go @@ -0,0 +1,38 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type UserGetOptions struct { + // Filters the users in the team + InTeamId string + // Filters the users not in the team + NotInTeamId string + // Filters the users in the channel + InChannelId string + // Filters the users not in the channel + NotInChannelId string + // Filters the users group constrained + GroupConstrained bool + // Filters the users without a team + WithoutTeam bool + // Filters the inactive users + Inactive bool + // Filters the active users + Active bool + // Filters for the given role + Role string + // Sorting option + Sort string + // Restrict to search in a list of teams and channels + ViewRestrictions *ViewUsersRestrictions + // Page + Page int + // Page size + PerPage int +} + +type UserGetByIdsOptions struct { + // Since filters the users based on their UpdateAt timestamp. + Since int64 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/user_search.go b/vendor/github.com/mattermost/mattermost-server/v5/model/user_search.go new file mode 100644 index 00000000..fa9fa8a2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/user_search.go @@ -0,0 +1,67 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const USER_SEARCH_MAX_LIMIT = 1000 +const USER_SEARCH_DEFAULT_LIMIT = 100 + +// UserSearch captures the parameters provided by a client for initiating a user search. +type UserSearch struct { + Term string `json:"term"` + TeamId string `json:"team_id"` + NotInTeamId string `json:"not_in_team_id"` + InChannelId string `json:"in_channel_id"` + NotInChannelId string `json:"not_in_channel_id"` + GroupConstrained bool `json:"group_constrained"` + AllowInactive bool `json:"allow_inactive"` + WithoutTeam bool `json:"without_team"` + Limit int `json:"limit"` + Role string `json:"role"` +} + +// ToJson convert a User to a json string +func (u *UserSearch) ToJson() []byte { + b, _ := json.Marshal(u) + return b +} + +// UserSearchFromJson will decode the input and return a User +func UserSearchFromJson(data io.Reader) *UserSearch { + us := UserSearch{} + json.NewDecoder(data).Decode(&us) + + if us.Limit == 0 { + us.Limit = USER_SEARCH_DEFAULT_LIMIT + } + + return &us +} + +// UserSearchOptions captures internal parameters derived from the user's permissions and a +// UserSearch request. +type UserSearchOptions struct { + // IsAdmin tracks whether or not the search is being conducted by an administrator. + IsAdmin bool + // AllowEmails allows search to examine the emails of users. + AllowEmails bool + // AllowFullNames allows search to examine the full names of users, vs. just usernames and nicknames. + AllowFullNames bool + // AllowInactive configures whether or not to return inactive users in the search results. + AllowInactive bool + // Narrows the search to the group constrained users + GroupConstrained bool + // Limit limits the total number of results returned. + Limit int + // Filters for the given role + Role string + // Restrict to search in a list of teams and channels + ViewRestrictions *ViewUsersRestrictions + // List of allowed channels + ListOfAllowedChannels []string +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/user_terms_of_service.go b/vendor/github.com/mattermost/mattermost-server/v5/model/user_terms_of_service.go new file mode 100644 index 00000000..9a0f4f18 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/user_terms_of_service.go @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +type UserTermsOfService struct { + UserId string `json:"user_id"` + TermsOfServiceId string `json:"terms_of_service_id"` + CreateAt int64 `json:"create_at"` +} + +func (ut *UserTermsOfService) IsValid() *AppError { + if !IsValidId(ut.UserId) { + return InvalidUserTermsOfServiceError("user_id", ut.UserId) + } + + if !IsValidId(ut.TermsOfServiceId) { + return InvalidUserTermsOfServiceError("terms_of_service_id", ut.UserId) + } + + if ut.CreateAt == 0 { + return InvalidUserTermsOfServiceError("create_at", ut.UserId) + } + + return nil +} + +func (ut *UserTermsOfService) ToJson() string { + b, _ := json.Marshal(ut) + return string(b) +} + +func (ut *UserTermsOfService) PreSave() { + if ut.UserId == "" { + ut.UserId = NewId() + } + + ut.CreateAt = GetMillis() +} + +func UserTermsOfServiceFromJson(data io.Reader) *UserTermsOfService { + var userTermsOfService *UserTermsOfService + json.NewDecoder(data).Decode(&userTermsOfService) + return userTermsOfService +} + +func InvalidUserTermsOfServiceError(fieldName string, userTermsOfServiceId string) *AppError { + id := fmt.Sprintf("model.user_terms_of_service.is_valid.%s.app_error", fieldName) + details := "" + if userTermsOfServiceId != "" { + details = "user_terms_of_service_user_id=" + userTermsOfServiceId + } + return NewAppError("UserTermsOfService.IsValid", id, nil, details, http.StatusBadRequest) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/users_stats.go b/vendor/github.com/mattermost/mattermost-server/v5/model/users_stats.go new file mode 100644 index 00000000..3e18296e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/users_stats.go @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type UsersStats struct { + TotalUsersCount int64 `json:"total_users_count"` +} + +func (o *UsersStats) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func UsersStatsFromJson(data io.Reader) *UsersStats { + var o *UsersStats + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/utils.go b/vendor/github.com/mattermost/mattermost-server/v5/model/utils.go new file mode 100644 index 00000000..e75fb022 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/utils.go @@ -0,0 +1,703 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "bytes" + "crypto/rand" + "encoding/base32" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/mail" + "net/url" + "regexp" + "strconv" + "strings" + "time" + "unicode" + + goi18n "github.com/mattermost/go-i18n/i18n" + "github.com/pborman/uuid" +) + +const ( + LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz" + UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + NUMBERS = "0123456789" + SYMBOLS = " !\"\\#$%&'()*+,-./:;<=>?@[]^_`|~" +) + +type StringInterface map[string]interface{} +type StringMap map[string]string +type StringArray []string + +func (sa StringArray) Equals(input StringArray) bool { + + if len(sa) != len(input) { + return false + } + + for index := range sa { + + if sa[index] != input[index] { + return false + } + } + + return true +} + +var translateFunc goi18n.TranslateFunc = nil + +func AppErrorInit(t goi18n.TranslateFunc) { + translateFunc = t +} + +type AppError struct { + Id string `json:"id"` + Message string `json:"message"` // Message to be display to the end user without debugging information + DetailedError string `json:"detailed_error"` // Internal error string to help the developer + RequestId string `json:"request_id,omitempty"` // The RequestId that's also set in the header + StatusCode int `json:"status_code,omitempty"` // The http status code + Where string `json:"-"` // The function where it happened in the form of Struct.Func + IsOAuth bool `json:"is_oauth,omitempty"` // Whether the error is OAuth specific + params map[string]interface{} +} + +func (er *AppError) Error() string { + return er.Where + ": " + er.Message + ", " + er.DetailedError +} + +func (er *AppError) Translate(T goi18n.TranslateFunc) { + if T == nil { + er.Message = er.Id + return + } + + if er.params == nil { + er.Message = T(er.Id) + } else { + er.Message = T(er.Id, er.params) + } +} + +func (er *AppError) SystemMessage(T goi18n.TranslateFunc) string { + if er.params == nil { + return T(er.Id) + } else { + return T(er.Id, er.params) + } +} + +func (er *AppError) ToJson() string { + b, _ := json.Marshal(er) + return string(b) +} + +// AppErrorFromJson will decode the input and return an AppError +func AppErrorFromJson(data io.Reader) *AppError { + str := "" + bytes, rerr := ioutil.ReadAll(data) + if rerr != nil { + str = rerr.Error() + } else { + str = string(bytes) + } + + decoder := json.NewDecoder(strings.NewReader(str)) + var er AppError + err := decoder.Decode(&er) + if err == nil { + return &er + } else { + return NewAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, "body: "+str, http.StatusInternalServerError) + } +} + +func NewAppError(where string, id string, params map[string]interface{}, details string, status int) *AppError { + ap := &AppError{} + ap.Id = id + ap.params = params + ap.Message = id + ap.Where = where + ap.DetailedError = details + ap.StatusCode = status + ap.IsOAuth = false + ap.Translate(translateFunc) + return ap +} + +var encoding = base32.NewEncoding("ybndrfg8ejkmcpqxot1uwisza345h769") + +// NewId is a globally unique identifier. It is a [A-Z0-9] string 26 +// characters long. It is a UUID version 4 Guid that is zbased32 encoded +// with the padding stripped off. +func NewId() string { + var b bytes.Buffer + encoder := base32.NewEncoder(encoding, &b) + encoder.Write(uuid.NewRandom()) + encoder.Close() + b.Truncate(26) // removes the '==' padding + return b.String() +} + +// NewRandomTeamName is a NewId that will be a valid team name. +func NewRandomTeamName() string { + teamName := NewId() + for IsReservedTeamName(teamName) { + teamName = NewId() + } + return teamName +} + +// NewRandomString returns a random string of the given length. +// The resulting entropy will be (5 * length) bits. +func NewRandomString(length int) string { + data := make([]byte, 1+(length*5/8)) + rand.Read(data) + return encoding.EncodeToString(data)[:length] +} + +// NewRandomBase32String returns a base32 encoded string of a random slice +// of bytes of the given size. The resulting entropy will be (8 * size) bits. +func NewRandomBase32String(size int) string { + data := make([]byte, size) + rand.Read(data) + return base32.StdEncoding.EncodeToString(data) +} + +// GetMillis is a convenience method to get milliseconds since epoch. +func GetMillis() int64 { + return time.Now().UnixNano() / int64(time.Millisecond) +} + +// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time. +func GetMillisForTime(thisTime time.Time) int64 { + return thisTime.UnixNano() / int64(time.Millisecond) +} + +// PadDateStringZeros is a convenience method to pad 2 digit date parts with zeros to meet ISO 8601 format +func PadDateStringZeros(dateString string) string { + parts := strings.Split(dateString, "-") + for index, part := range parts { + if len(part) == 1 { + parts[index] = "0" + part + } + } + dateString = strings.Join(parts[:], "-") + return dateString +} + +// GetStartOfDayMillis is a convenience method to get milliseconds since epoch for provided date's start of day +func GetStartOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { + localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) + resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 0, 0, 0, 0, localSearchTimeZone) + return GetMillisForTime(resultTime) +} + +// GetEndOfDayMillis is a convenience method to get milliseconds since epoch for provided date's end of day +func GetEndOfDayMillis(thisTime time.Time, timeZoneOffset int) int64 { + localSearchTimeZone := time.FixedZone("Local Search Time Zone", timeZoneOffset) + resultTime := time.Date(thisTime.Year(), thisTime.Month(), thisTime.Day(), 23, 59, 59, 999999999, localSearchTimeZone) + return GetMillisForTime(resultTime) +} + +func CopyStringMap(originalMap map[string]string) map[string]string { + copyMap := make(map[string]string) + for k, v := range originalMap { + copyMap[k] = v + } + return copyMap +} + +// MapToJson converts a map to a json string +func MapToJson(objmap map[string]string) string { + b, _ := json.Marshal(objmap) + return string(b) +} + +// MapBoolToJson converts a map to a json string +func MapBoolToJson(objmap map[string]bool) string { + b, _ := json.Marshal(objmap) + return string(b) +} + +// MapFromJson will decode the key/value pair map +func MapFromJson(data io.Reader) map[string]string { + decoder := json.NewDecoder(data) + + var objmap map[string]string + if err := decoder.Decode(&objmap); err != nil { + return make(map[string]string) + } else { + return objmap + } +} + +// MapFromJson will decode the key/value pair map +func MapBoolFromJson(data io.Reader) map[string]bool { + decoder := json.NewDecoder(data) + + var objmap map[string]bool + if err := decoder.Decode(&objmap); err != nil { + return make(map[string]bool) + } else { + return objmap + } +} + +func ArrayToJson(objmap []string) string { + b, _ := json.Marshal(objmap) + return string(b) +} + +func ArrayFromJson(data io.Reader) []string { + decoder := json.NewDecoder(data) + + var objmap []string + if err := decoder.Decode(&objmap); err != nil { + return make([]string, 0) + } else { + return objmap + } +} + +func ArrayFromInterface(data interface{}) []string { + stringArray := []string{} + + dataArray, ok := data.([]interface{}) + if !ok { + return stringArray + } + + for _, v := range dataArray { + if str, ok := v.(string); ok { + stringArray = append(stringArray, str) + } + } + + return stringArray +} + +func StringInterfaceToJson(objmap map[string]interface{}) string { + b, _ := json.Marshal(objmap) + return string(b) +} + +func StringInterfaceFromJson(data io.Reader) map[string]interface{} { + decoder := json.NewDecoder(data) + + var objmap map[string]interface{} + if err := decoder.Decode(&objmap); err != nil { + return make(map[string]interface{}) + } else { + return objmap + } +} + +func StringToJson(s string) string { + b, _ := json.Marshal(s) + return string(b) +} + +func StringFromJson(data io.Reader) string { + decoder := json.NewDecoder(data) + + var s string + if err := decoder.Decode(&s); err != nil { + return "" + } else { + return s + } +} + +func GetServerIpAddress(iface string) string { + var addrs []net.Addr + if len(iface) == 0 { + var err error + addrs, err = net.InterfaceAddrs() + if err != nil { + return "" + } + } else { + interfaces, err := net.Interfaces() + if err != nil { + return "" + } + for _, i := range interfaces { + if i.Name == iface { + addrs, err = i.Addrs() + if err != nil { + return "" + } + break + } + } + } + + for _, addr := range addrs { + + if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() && !ip.IP.IsLinkLocalMulticast() { + if ip.IP.To4() != nil { + return ip.IP.String() + } + } + } + + return "" +} + +func IsLower(s string) bool { + return strings.ToLower(s) == s +} + +func IsValidEmail(email string) bool { + if !IsLower(email) { + return false + } + + if addr, err := mail.ParseAddress(email); err != nil { + return false + } else if addr.Name != "" { + // mail.ParseAddress accepts input of the form "Billy Bob <billy@example.com>" which we don't allow + return false + } + + return true +} + +var reservedName = []string{ + "admin", + "api", + "channel", + "claim", + "error", + "help", + "landing", + "login", + "mfa", + "oauth", + "plug", + "plugins", + "post", + "signup", +} + +func IsValidChannelIdentifier(s string) bool { + + if !IsValidAlphaNumHyphenUnderscore(s, true) { + return false + } + + if len(s) < CHANNEL_NAME_MIN_LENGTH { + return false + } + + return true +} + +func IsValidAlphaNum(s string) bool { + validAlphaNum := regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`) + + return validAlphaNum.MatchString(s) +} + +func IsValidAlphaNumHyphenUnderscore(s string, withFormat bool) bool { + if withFormat { + validAlphaNumHyphenUnderscore := regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`) + return validAlphaNumHyphenUnderscore.MatchString(s) + } + + validSimpleAlphaNumHyphenUnderscore := regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`) + return validSimpleAlphaNumHyphenUnderscore.MatchString(s) +} + +func Etag(parts ...interface{}) string { + + etag := CurrentVersion + + for _, part := range parts { + etag += fmt.Sprintf(".%v", part) + } + + return etag +} + +var validHashtag = regexp.MustCompile(`^(#\pL[\pL\d\-_.]*[\pL\d])$`) +var puncStart = regexp.MustCompile(`^[^\pL\d\s#]+`) +var hashtagStart = regexp.MustCompile(`^#{2,}`) +var puncEnd = regexp.MustCompile(`[^\pL\d\s]+$`) + +func ParseHashtags(text string) (string, string) { + words := strings.Fields(text) + + hashtagString := "" + plainString := "" + for _, word := range words { + // trim off surrounding punctuation + word = puncStart.ReplaceAllString(word, "") + word = puncEnd.ReplaceAllString(word, "") + + // and remove extra pound #s + word = hashtagStart.ReplaceAllString(word, "#") + + if validHashtag.MatchString(word) { + hashtagString += " " + word + } else { + plainString += " " + word + } + } + + if len(hashtagString) > 1000 { + hashtagString = hashtagString[:999] + lastSpace := strings.LastIndex(hashtagString, " ") + if lastSpace > -1 { + hashtagString = hashtagString[:lastSpace] + } else { + hashtagString = "" + } + } + + return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString) +} + +func IsFileExtImage(ext string) bool { + ext = strings.ToLower(ext) + for _, imgExt := range IMAGE_EXTENSIONS { + if ext == imgExt { + return true + } + } + return false +} + +func GetImageMimeType(ext string) string { + ext = strings.ToLower(ext) + if len(IMAGE_MIME_TYPES[ext]) == 0 { + return "image" + } else { + return IMAGE_MIME_TYPES[ext] + } +} + +func ClearMentionTags(post string) string { + post = strings.Replace(post, "<mention>", "", -1) + post = strings.Replace(post, "</mention>", "", -1) + return post +} + +func IsValidHttpUrl(rawUrl string) bool { + if strings.Index(rawUrl, "http://") != 0 && strings.Index(rawUrl, "https://") != 0 { + return false + } + + if _, err := url.ParseRequestURI(rawUrl); err != nil { + return false + } + + return true +} + +func IsValidTurnOrStunServer(rawUri string) bool { + if strings.Index(rawUri, "turn:") != 0 && strings.Index(rawUri, "stun:") != 0 { + return false + } + + if _, err := url.ParseRequestURI(rawUri); err != nil { + return false + } + + return true +} + +func IsSafeLink(link *string) bool { + if link != nil { + if IsValidHttpUrl(*link) { + return true + } else if strings.HasPrefix(*link, "/") { + return true + } else { + return false + } + } + + return true +} + +func IsValidWebsocketUrl(rawUrl string) bool { + if strings.Index(rawUrl, "ws://") != 0 && strings.Index(rawUrl, "wss://") != 0 { + return false + } + + if _, err := url.ParseRequestURI(rawUrl); err != nil { + return false + } + + return true +} + +func IsValidTrueOrFalseString(value string) bool { + return value == "true" || value == "false" +} + +func IsValidNumberString(value string) bool { + if _, err := strconv.Atoi(value); err != nil { + return false + } + + return true +} + +func IsValidId(value string) bool { + if len(value) != 26 { + return false + } + + for _, r := range value { + if !unicode.IsLetter(r) && !unicode.IsNumber(r) { + return false + } + } + + return true +} + +// Copied from https://golang.org/src/net/dnsclient.go#L119 +func IsDomainName(s string) bool { + // See RFC 1035, RFC 3696. + // Presentation format has dots before every label except the first, and the + // terminal empty label is optional here because we assume fully-qualified + // (absolute) input. We must therefore reserve space for the first and last + // labels' length octets in wire format, where they are necessary and the + // maximum total length is 255. + // So our _effective_ maximum is 253, but 254 is not rejected if the last + // character is a dot. + l := len(s) + if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { + return false + } + + last := byte('.') + ok := false // Ok once we've seen a letter. + partlen := 0 + for i := 0; i < len(s); i++ { + c := s[i] + switch { + default: + return false + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': + ok = true + partlen++ + case '0' <= c && c <= '9': + // fine + partlen++ + case c == '-': + // Byte before dash cannot be dot. + if last == '.' { + return false + } + partlen++ + case c == '.': + // Byte before dot cannot be dot, dash. + if last == '.' || last == '-' { + return false + } + if partlen > 63 || partlen == 0 { + return false + } + partlen = 0 + } + last = c + } + if last == '-' || partlen > 63 { + return false + } + + return ok +} + +func RemoveDuplicateStrings(in []string) []string { + out := []string{} + seen := make(map[string]bool, len(in)) + + for _, item := range in { + if !seen[item] { + out = append(out, item) + + seen[item] = true + } + } + + return out +} + +func GetPreferredTimezone(timezone StringMap) string { + if timezone["useAutomaticTimezone"] == "true" { + return timezone["automaticTimezone"] + } + + return timezone["manualTimezone"] +} + +// IsSamlFile checks if filename is a SAML file. +func IsSamlFile(saml *SamlSettings, filename string) bool { + return filename == *saml.PublicCertificateFile || filename == *saml.PrivateKeyFile || filename == *saml.IdpCertificateFile +} + +func AsStringBoolMap(list []string) map[string]bool { + listMap := map[string]bool{} + for _, p := range list { + listMap[p] = true + } + return listMap +} + +// SanitizeUnicode will remove undesirable Unicode characters from a string. +func SanitizeUnicode(s string) string { + return strings.Map(filterBlacklist, s) +} + +// filterBlacklist returns `r` if it is not in the blacklist, otherwise drop (-1). +// Blacklist is taken from https://www.w3.org/TR/unicode-xml/#Charlist +func filterBlacklist(r rune) rune { + const drop = -1 + switch r { + case '\u0340', '\u0341': // clones of grave and acute; deprecated in Unicode + return drop + case '\u17A3', '\u17D3': // obsolete characters for Khmer; deprecated in Unicode + return drop + case '\u2028', '\u2029': // line and paragraph separator + return drop + case '\u202A', '\u202B', '\u202C', '\u202D', '\u202E': // BIDI embedding controls + return drop + case '\u206A', '\u206B': // activate/inhibit symmetric swapping; deprecated in Unicode + return drop + case '\u206C', '\u206D': // activate/inhibit Arabic form shaping; deprecated in Unicode + return drop + case '\u206E', '\u206F': // activate/inhibit national digit shapes; deprecated in Unicode + return drop + case '\uFFF9', '\uFFFA', '\uFFFB': // interlinear annotation characters + return drop + case '\uFEFF': // byte order mark + return drop + case '\uFFFC': // object replacement character + return drop + } + + // Scoping for musical notation + if r >= 0x0001D173 && r <= 0x0001D17A { + return drop + } + + // Language tag code points + if r >= 0x000E0000 && r <= 0x000E007F { + return drop + } + + return r +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/version.go b/vendor/github.com/mattermost/mattermost-server/v5/model/version.go new file mode 100644 index 00000000..3cefff8c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/version.go @@ -0,0 +1,176 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "strconv" + "strings" +) + +// This is a list of all the current versions including any patches. +// It should be maintained in chronological order with most current +// release at the front of the list. +var versions = []string{ + "5.25.0", + "5.24.0", + "5.23.0", + "5.22.0", + "5.21.0", + "5.20.0", + "5.19.0", + "5.18.0", + "5.17.0", + "5.16.0", + "5.15.0", + "5.14.0", + "5.13.0", + "5.12.0", + "5.11.0", + "5.10.0", + "5.9.0", + "5.8.0", + "5.7.0", + "5.6.0", + "5.5.0", + "5.4.0", + "5.3.0", + "5.2.0", + "5.1.0", + "5.0.0", + "4.10.0", + "4.9.0", + "4.8.1", + "4.8.0", + "4.7.2", + "4.7.1", + "4.7.0", + "4.6.0", + "4.5.0", + "4.4.0", + "4.3.0", + "4.2.0", + "4.1.0", + "4.0.0", + "3.10.0", + "3.9.0", + "3.8.0", + "3.7.0", + "3.6.0", + "3.5.0", + "3.4.0", + "3.3.0", + "3.2.0", + "3.1.0", + "3.0.0", + "2.2.0", + "2.1.0", + "2.0.0", + "1.4.0", + "1.3.0", + "1.2.1", + "1.2.0", + "1.1.0", + "1.0.0", + "0.7.1", + "0.7.0", + "0.6.0", + "0.5.0", +} + +var CurrentVersion string = versions[0] +var BuildNumber string +var BuildDate string +var BuildHash string +var BuildHashEnterprise string +var BuildEnterpriseReady string +var versionsWithoutHotFixes []string + +func init() { + versionsWithoutHotFixes = make([]string, 0, len(versions)) + seen := make(map[string]string) + + for _, version := range versions { + maj, min, _ := SplitVersion(version) + verStr := fmt.Sprintf("%v.%v.0", maj, min) + + if seen[verStr] == "" { + versionsWithoutHotFixes = append(versionsWithoutHotFixes, verStr) + seen[verStr] = verStr + } + } +} + +func SplitVersion(version string) (int64, int64, int64) { + parts := strings.Split(version, ".") + + major := int64(0) + minor := int64(0) + patch := int64(0) + + if len(parts) > 0 { + major, _ = strconv.ParseInt(parts[0], 10, 64) + } + + if len(parts) > 1 { + minor, _ = strconv.ParseInt(parts[1], 10, 64) + } + + if len(parts) > 2 { + patch, _ = strconv.ParseInt(parts[2], 10, 64) + } + + return major, minor, patch +} + +func GetPreviousVersion(version string) string { + verMajor, verMinor, _ := SplitVersion(version) + verStr := fmt.Sprintf("%v.%v.0", verMajor, verMinor) + + for index, v := range versionsWithoutHotFixes { + if v == verStr && len(versionsWithoutHotFixes) > index+1 { + return versionsWithoutHotFixes[index+1] + } + } + + return "" +} + +func IsCurrentVersion(versionToCheck string) bool { + currentMajor, currentMinor, _ := SplitVersion(CurrentVersion) + toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck) + + if toCheckMajor == currentMajor && toCheckMinor == currentMinor { + return true + } else { + return false + } +} + +func IsPreviousVersionsSupported(versionToCheck string) bool { + toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck) + versionToCheckStr := fmt.Sprintf("%v.%v.0", toCheckMajor, toCheckMinor) + + // Current Supported + if versionsWithoutHotFixes[0] == versionToCheckStr { + return true + } + + // Current - 1 Supported + if versionsWithoutHotFixes[1] == versionToCheckStr { + return true + } + + // Current - 2 Supported + if versionsWithoutHotFixes[2] == versionToCheckStr { + return true + } + + // Current - 3 Supported + if versionsWithoutHotFixes[3] == versionToCheckStr { + return true + } + + return false +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/websocket_client.go b/vendor/github.com/mattermost/mattermost-server/v5/model/websocket_client.go new file mode 100644 index 00000000..72ca4a8f --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/websocket_client.go @@ -0,0 +1,321 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "bytes" + "encoding/json" + "net/http" + "sync/atomic" + "time" + + "github.com/gorilla/websocket" +) + +const ( + SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB + PING_TIMEOUT_BUFFER_SECONDS = 5 +) + +type msgType int + +const ( + msgTypeJSON msgType = iota + 1 + msgTypePong +) + +type writeMessage struct { + msgType msgType + data interface{} +} + +const avgReadMsgSizeBytes = 1024 + +// WebSocketClient stores the necessary information required to +// communicate with a WebSocket endpoint. +// A client must read from PingTimeoutChannel, EventChannel and ResponseChannel to prevent +// deadlocks from occuring in the program. +type WebSocketClient struct { + Url string // The location of the server like "ws://localhost:8065" + ApiUrl string // The API location of the server like "ws://localhost:8065/api/v3" + ConnectUrl string // The WebSocket URL to connect to like "ws://localhost:8065/api/v3/path/to/websocket" + Conn *websocket.Conn // The WebSocket connection + AuthToken string // The token used to open the WebSocket connection + Sequence int64 // The ever-incrementing sequence attached to each WebSocket action + PingTimeoutChannel chan bool // The channel used to signal ping timeouts + EventChannel chan *WebSocketEvent // The channel used to receive various events pushed from the server. For example: typing, posted + ResponseChannel chan *WebSocketResponse // The channel used to receive responses for requests made to the server + ListenError *AppError // A field that is set if there was an abnormal closure of the WebSocket connection + writeChan chan writeMessage + + pingTimeoutTimer *time.Timer + quitPingWatchdog chan struct{} + + quitWriterChan chan struct{} + resetTimerChan chan struct{} + closed int32 +} + +// NewWebSocketClient constructs a new WebSocket client with convenience +// methods for talking to the server. +func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) { + return NewWebSocketClientWithDialer(websocket.DefaultDialer, url, authToken) +} + +// NewWebSocketClientWithDialer constructs a new WebSocket client with convenience +// methods for talking to the server using a custom dialer. +func NewWebSocketClientWithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) { + conn, _, err := dialer.Dial(url+API_URL_SUFFIX+"/websocket", nil) + if err != nil { + return nil, NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + client := &WebSocketClient{ + Url: url, + ApiUrl: url + API_URL_SUFFIX, + ConnectUrl: url + API_URL_SUFFIX + "/websocket", + Conn: conn, + AuthToken: authToken, + Sequence: 1, + PingTimeoutChannel: make(chan bool, 1), + EventChannel: make(chan *WebSocketEvent, 100), + ResponseChannel: make(chan *WebSocketResponse, 100), + writeChan: make(chan writeMessage), + quitPingWatchdog: make(chan struct{}), + quitWriterChan: make(chan struct{}), + resetTimerChan: make(chan struct{}), + } + + client.configurePingHandling() + go client.writer() + + client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken}) + + return client, nil +} + +// NewWebSocketClient4 constructs a new WebSocket client with convenience +// methods for talking to the server. Uses the v4 endpoint. +func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) { + return NewWebSocketClient4WithDialer(websocket.DefaultDialer, url, authToken) +} + +// NewWebSocketClient4WithDialer constructs a new WebSocket client with convenience +// methods for talking to the server using a custom dialer. Uses the v4 endpoint. +func NewWebSocketClient4WithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) { + return NewWebSocketClientWithDialer(dialer, url, authToken) +} + +// Connect creates a websocket connection with the given ConnectUrl. +// This is racy and error-prone should not be used. Use any of the New* functions to create a websocket. +func (wsc *WebSocketClient) Connect() *AppError { + return wsc.ConnectWithDialer(websocket.DefaultDialer) +} + +// ConnectWithDialer creates a websocket connection with the given ConnectUrl using the dialer. +// This is racy and error-prone and should not be used. Use any of the New* functions to create a websocket. +func (wsc *WebSocketClient) ConnectWithDialer(dialer *websocket.Dialer) *AppError { + var err error + wsc.Conn, _, err = dialer.Dial(wsc.ConnectUrl, nil) + if err != nil { + return NewAppError("Connect", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) + } + // Super racy and should not be done anyways. + // All of this needs to be redesigned for v6. + wsc.configurePingHandling() + // If it has been closed before, we just restart the writer. + if atomic.CompareAndSwapInt32(&wsc.closed, 1, 0) { + wsc.writeChan = make(chan writeMessage) + wsc.quitWriterChan = make(chan struct{}) + go wsc.writer() + wsc.resetTimerChan = make(chan struct{}) + wsc.quitPingWatchdog = make(chan struct{}) + } + + wsc.EventChannel = make(chan *WebSocketEvent, 100) + wsc.ResponseChannel = make(chan *WebSocketResponse, 100) + + wsc.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": wsc.AuthToken}) + + return nil +} + +// Close closes the websocket client. It is recommended that a closed client should not be +// reused again. Rather a new client should be created anew. +func (wsc *WebSocketClient) Close() { + // CAS to 1 and proceed. Return if already 1. + if !atomic.CompareAndSwapInt32(&wsc.closed, 0, 1) { + return + } + wsc.quitWriterChan <- struct{}{} + close(wsc.writeChan) + // We close the connection, which breaks the reader loop. + // Then we let the defer block in the reader do further cleanup. + wsc.Conn.Close() +} + +// TODO: un-export the Conn so that Write methods go through the writer +func (wsc *WebSocketClient) writer() { + for { + select { + case msg := <-wsc.writeChan: + switch msg.msgType { + case msgTypeJSON: + wsc.Conn.WriteJSON(msg.data) + case msgTypePong: + wsc.Conn.WriteMessage(websocket.PongMessage, []byte{}) + } + case <-wsc.quitWriterChan: + return + } + } +} + +// Listen starts the read loop of the websocket client. +func (wsc *WebSocketClient) Listen() { + // This loop can exit in 2 conditions: + // 1. Either the connection breaks naturally. + // 2. Close was explicitly called, which closes the connection manually. + // + // Due to the way the API is written, there is a requirement that a client may NOT + // call Listen at all and can still call Close and Connect. + // Therefore, we let the cleanup of the reader stuff rely on closing the connection + // and then we do the cleanup in the defer block. + // + // First, we close some channels and then CAS to 1 and proceed to close the writer chan also. + // This is needed because then the defer clause does not double-close the writer when (2) happens. + // But if (1) happens, we set the closed bit, and close the rest of the stuff. + go func() { + defer func() { + close(wsc.EventChannel) + close(wsc.ResponseChannel) + close(wsc.quitPingWatchdog) + close(wsc.resetTimerChan) + // We CAS to 1 and proceed. + if !atomic.CompareAndSwapInt32(&wsc.closed, 0, 1) { + return + } + wsc.quitWriterChan <- struct{}{} + close(wsc.writeChan) + wsc.Conn.Close() // This can most likely be removed. Needs to be checked. + }() + + var buf bytes.Buffer + buf.Grow(avgReadMsgSizeBytes) + + for { + // Reset buffer. + buf.Reset() + _, r, err := wsc.Conn.NextReader() + if err != nil { + if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { + wsc.ListenError = NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) + } + return + } + // Use pre-allocated buffer. + _, err = buf.ReadFrom(r) + if err != nil { + // This should use a different error ID, but en.json is not imported anyways. + // It's a different bug altogether but we let it be for now. + // See MM-24520. + wsc.ListenError = NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + event := WebSocketEventFromJson(bytes.NewReader(buf.Bytes())) + if event == nil { + continue + } + if event.IsValid() { + wsc.EventChannel <- event + continue + } + + var response WebSocketResponse + if err := json.Unmarshal(buf.Bytes(), &response); err == nil && response.IsValid() { + wsc.ResponseChannel <- &response + continue + } + } + }() +} + +func (wsc *WebSocketClient) SendMessage(action string, data map[string]interface{}) { + req := &WebSocketRequest{} + req.Seq = wsc.Sequence + req.Action = action + req.Data = data + + wsc.Sequence++ + wsc.writeChan <- writeMessage{ + msgType: msgTypeJSON, + data: req, + } +} + +// UserTyping will push a user_typing event out to all connected users +// who are in the specified channel +func (wsc *WebSocketClient) UserTyping(channelId, parentId string) { + data := map[string]interface{}{ + "channel_id": channelId, + "parent_id": parentId, + } + + wsc.SendMessage("user_typing", data) +} + +// GetStatuses will return a map of string statuses using user id as the key +func (wsc *WebSocketClient) GetStatuses() { + wsc.SendMessage("get_statuses", nil) +} + +// GetStatusesByIds will fetch certain user statuses based on ids and return +// a map of string statuses using user id as the key +func (wsc *WebSocketClient) GetStatusesByIds(userIds []string) { + data := map[string]interface{}{ + "user_ids": userIds, + } + wsc.SendMessage("get_statuses_by_ids", data) +} + +func (wsc *WebSocketClient) configurePingHandling() { + wsc.Conn.SetPingHandler(wsc.pingHandler) + wsc.pingTimeoutTimer = time.NewTimer(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS)) + go wsc.pingWatchdog() +} + +func (wsc *WebSocketClient) pingHandler(appData string) error { + if atomic.LoadInt32(&wsc.closed) == 1 { + return nil + } + wsc.resetTimerChan <- struct{}{} + wsc.writeChan <- writeMessage{ + msgType: msgTypePong, + } + return nil +} + +// pingWatchdog is used to send values to the PingTimeoutChannel whenever a timeout occurs. +// We use the resetTimerChan from the pingHandler to pass the signal, and then reset the timer +// after draining it. And if the timer naturally expires, we also extend it to prevent it from +// being deadlocked when the resetTimerChan case runs. Because timer.Stop would return false, +// and the code would be forever stuck trying to read from C. +func (wsc *WebSocketClient) pingWatchdog() { + for { + select { + case <-wsc.resetTimerChan: + if !wsc.pingTimeoutTimer.Stop() { + <-wsc.pingTimeoutTimer.C + } + wsc.pingTimeoutTimer.Reset(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS)) + + case <-wsc.pingTimeoutTimer.C: + wsc.PingTimeoutChannel <- true + wsc.pingTimeoutTimer.Reset(time.Second * (60 + PING_TIMEOUT_BUFFER_SECONDS)) + case <-wsc.quitPingWatchdog: + return + } + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/websocket_message.go b/vendor/github.com/mattermost/mattermost-server/v5/model/websocket_message.go new file mode 100644 index 00000000..b3e4b186 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/websocket_message.go @@ -0,0 +1,259 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "bytes" + "encoding/json" + "fmt" + "io" +) + +const ( + WEBSOCKET_EVENT_TYPING = "typing" + WEBSOCKET_EVENT_POSTED = "posted" + WEBSOCKET_EVENT_POST_EDITED = "post_edited" + WEBSOCKET_EVENT_POST_DELETED = "post_deleted" + WEBSOCKET_EVENT_POST_UNREAD = "post_unread" + WEBSOCKET_EVENT_CHANNEL_CONVERTED = "channel_converted" + WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created" + WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted" + WEBSOCKET_EVENT_CHANNEL_RESTORED = "channel_restored" + WEBSOCKET_EVENT_CHANNEL_UPDATED = "channel_updated" + WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED = "channel_member_updated" + WEBSOCKET_EVENT_CHANNEL_SCHEME_UPDATED = "channel_scheme_updated" + WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added" + WEBSOCKET_EVENT_GROUP_ADDED = "group_added" + WEBSOCKET_EVENT_NEW_USER = "new_user" + WEBSOCKET_EVENT_ADDED_TO_TEAM = "added_to_team" + WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team" + WEBSOCKET_EVENT_UPDATE_TEAM = "update_team" + WEBSOCKET_EVENT_DELETE_TEAM = "delete_team" + WEBSOCKET_EVENT_RESTORE_TEAM = "restore_team" + WEBSOCKET_EVENT_UPDATE_TEAM_SCHEME = "update_team_scheme" + WEBSOCKET_EVENT_USER_ADDED = "user_added" + WEBSOCKET_EVENT_USER_UPDATED = "user_updated" + WEBSOCKET_EVENT_USER_ROLE_UPDATED = "user_role_updated" + WEBSOCKET_EVENT_MEMBERROLE_UPDATED = "memberrole_updated" + WEBSOCKET_EVENT_USER_REMOVED = "user_removed" + WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed" + WEBSOCKET_EVENT_PREFERENCES_CHANGED = "preferences_changed" + WEBSOCKET_EVENT_PREFERENCES_DELETED = "preferences_deleted" + WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message" + WEBSOCKET_EVENT_STATUS_CHANGE = "status_change" + WEBSOCKET_EVENT_HELLO = "hello" + WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge" + WEBSOCKET_EVENT_REACTION_ADDED = "reaction_added" + WEBSOCKET_EVENT_REACTION_REMOVED = "reaction_removed" + WEBSOCKET_EVENT_RESPONSE = "response" + WEBSOCKET_EVENT_EMOJI_ADDED = "emoji_added" + WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed" + WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED = "plugin_statuses_changed" + WEBSOCKET_EVENT_PLUGIN_ENABLED = "plugin_enabled" + WEBSOCKET_EVENT_PLUGIN_DISABLED = "plugin_disabled" + WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated" + WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed" + WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed" + WEBSOCKET_EVENT_OPEN_DIALOG = "open_dialog" + WEBSOCKET_EVENT_GUESTS_DEACTIVATED = "guests_deactivated" + WEBSOCKET_EVENT_RECEIVED_GROUP = "received_group" + WEBSOCKET_EVENT_RECEIVED_GROUP_ASSOCIATED_TO_TEAM = "received_group_associated_to_team" + WEBSOCKET_EVENT_RECEIVED_GROUP_NOT_ASSOCIATED_TO_TEAM = "received_group_not_associated_to_team" + WEBSOCKET_EVENT_RECEIVED_GROUP_ASSOCIATED_TO_CHANNEL = "received_group_associated_to_channel" + WEBSOCKET_EVENT_RECEIVED_GROUP_NOT_ASSOCIATED_TO_CHANNEL = "received_group_not_associated_to_channel" +) + +type WebSocketMessage interface { + ToJson() string + IsValid() bool + EventType() string +} + +type WebsocketBroadcast struct { + OmitUsers map[string]bool `json:"omit_users"` // broadcast is omitted for users listed here + UserId string `json:"user_id"` // broadcast only occurs for this user + ChannelId string `json:"channel_id"` // broadcast only occurs for users in this channel + TeamId string `json:"team_id"` // broadcast only occurs for users in this team + ContainsSanitizedData bool `json:"-"` + ContainsSensitiveData bool `json:"-"` +} + +type precomputedWebSocketEventJSON struct { + Event json.RawMessage + Data json.RawMessage + Broadcast json.RawMessage +} + +// webSocketEventJSON mirrors WebSocketEvent to make some of its unexported fields serializable +type webSocketEventJSON struct { + Event string `json:"event"` + Data map[string]interface{} `json:"data"` + Broadcast *WebsocketBroadcast `json:"broadcast"` + Sequence int64 `json:"seq"` +} + +// **NOTE**: Direct access to WebSocketEvent fields is deprecated. They will be +// made unexported in next major version release. Provided getter functions should be used instead. +type WebSocketEvent struct { + Event string // Deprecated: use EventType() + Data map[string]interface{} // Deprecated: use GetData() + Broadcast *WebsocketBroadcast // Deprecated: use GetBroadcast() + Sequence int64 // Deprecated: use GetSequence() + precomputedJSON *precomputedWebSocketEventJSON +} + +// PrecomputeJSON precomputes and stores the serialized JSON for all fields other than Sequence. +// This makes ToJson much more efficient when sending the same event to multiple connections. +func (ev *WebSocketEvent) PrecomputeJSON() *WebSocketEvent { + copy := ev.Copy() + event, _ := json.Marshal(copy.Event) + data, _ := json.Marshal(copy.Data) + broadcast, _ := json.Marshal(copy.Broadcast) + copy.precomputedJSON = &precomputedWebSocketEventJSON{ + Event: json.RawMessage(event), + Data: json.RawMessage(data), + Broadcast: json.RawMessage(broadcast), + } + return copy +} + +func (ev *WebSocketEvent) Add(key string, value interface{}) { + ev.Data[key] = value +} + +func NewWebSocketEvent(event, teamId, channelId, userId string, omitUsers map[string]bool) *WebSocketEvent { + return &WebSocketEvent{Event: event, Data: make(map[string]interface{}), + Broadcast: &WebsocketBroadcast{TeamId: teamId, ChannelId: channelId, UserId: userId, OmitUsers: omitUsers}} +} + +func (ev *WebSocketEvent) Copy() *WebSocketEvent { + copy := &WebSocketEvent{ + Event: ev.Event, + Data: ev.Data, + Broadcast: ev.Broadcast, + Sequence: ev.Sequence, + precomputedJSON: ev.precomputedJSON, + } + return copy +} + +func (ev *WebSocketEvent) GetData() map[string]interface{} { + return ev.Data +} + +func (ev *WebSocketEvent) GetBroadcast() *WebsocketBroadcast { + return ev.Broadcast +} + +func (ev *WebSocketEvent) GetSequence() int64 { + return ev.Sequence +} + +func (ev *WebSocketEvent) SetEvent(event string) *WebSocketEvent { + copy := ev.Copy() + copy.Event = event + return copy +} + +func (ev *WebSocketEvent) SetData(data map[string]interface{}) *WebSocketEvent { + copy := ev.Copy() + copy.Data = data + return copy +} + +func (ev *WebSocketEvent) SetBroadcast(broadcast *WebsocketBroadcast) *WebSocketEvent { + copy := ev.Copy() + copy.Broadcast = broadcast + return copy +} + +func (ev *WebSocketEvent) SetSequence(seq int64) *WebSocketEvent { + copy := ev.Copy() + copy.Sequence = seq + return copy +} + +func (ev *WebSocketEvent) IsValid() bool { + return ev.Event != "" +} + +func (ev *WebSocketEvent) EventType() string { + return ev.Event +} + +func (ev *WebSocketEvent) ToJson() string { + if ev.precomputedJSON != nil { + return fmt.Sprintf(`{"event": %s, "data": %s, "broadcast": %s, "seq": %d}`, ev.precomputedJSON.Event, ev.precomputedJSON.Data, ev.precomputedJSON.Broadcast, ev.Sequence) + } + b, _ := json.Marshal(webSocketEventJSON{ + ev.Event, + ev.Data, + ev.Broadcast, + ev.Sequence, + }) + return string(b) +} + +func WebSocketEventFromJson(data io.Reader) *WebSocketEvent { + var ev WebSocketEvent + var o webSocketEventJSON + if err := json.NewDecoder(data).Decode(&o); err != nil { + return nil + } + ev.Event = o.Event + if u, ok := o.Data["user"]; ok { + // We need to convert to and from JSON again + // because the user is in the form of a map[string]interface{}. + buf, err := json.Marshal(u) + if err != nil { + return nil + } + o.Data["user"] = UserFromJson(bytes.NewReader(buf)) + } + ev.Data = o.Data + ev.Broadcast = o.Broadcast + ev.Sequence = o.Sequence + return &ev +} + +// WebSocketResponse represents a response received through the WebSocket +// for a request made to the server. This is available through the ResponseChannel +// channel in WebSocketClient. +type WebSocketResponse struct { + Status string `json:"status"` // The status of the response. For example: OK, FAIL. + SeqReply int64 `json:"seq_reply,omitempty"` // A counter which is incremented for every response sent. + Data map[string]interface{} `json:"data,omitempty"` // The data contained in the response. + Error *AppError `json:"error,omitempty"` // A field that is set if any error has occurred. +} + +func (m *WebSocketResponse) Add(key string, value interface{}) { + m.Data[key] = value +} + +func NewWebSocketResponse(status string, seqReply int64, data map[string]interface{}) *WebSocketResponse { + return &WebSocketResponse{Status: status, SeqReply: seqReply, Data: data} +} + +func NewWebSocketError(seqReply int64, err *AppError) *WebSocketResponse { + return &WebSocketResponse{Status: STATUS_FAIL, SeqReply: seqReply, Error: err} +} + +func (m *WebSocketResponse) IsValid() bool { + return m.Status != "" +} + +func (m *WebSocketResponse) EventType() string { + return WEBSOCKET_EVENT_RESPONSE +} + +func (m *WebSocketResponse) ToJson() string { + b, _ := json.Marshal(m) + return string(b) +} + +func WebSocketResponseFromJson(data io.Reader) *WebSocketResponse { + var o *WebSocketResponse + json.NewDecoder(data).Decode(&o) + return o +} diff --git a/vendor/github.com/mattermost/mattermost-server/v5/model/websocket_request.go b/vendor/github.com/mattermost/mattermost-server/v5/model/websocket_request.go new file mode 100644 index 00000000..6628a5c9 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v5/model/websocket_request.go @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + + goi18n "github.com/mattermost/go-i18n/i18n" +) + +// WebSocketRequest represents a request made to the server through a websocket. +type WebSocketRequest struct { + // Client-provided fields + Seq int64 `json:"seq"` // A counter which is incremented for every request made. + Action string `json:"action"` // The action to perform for a request. For example: get_statuses, user_typing. + Data map[string]interface{} `json:"data"` // The metadata for an action. + + // Server-provided fields + Session Session `json:"-"` + T goi18n.TranslateFunc `json:"-"` + Locale string `json:"-"` +} + +func (o *WebSocketRequest) ToJson() string { + b, _ := json.Marshal(o) + return string(b) +} + +func WebSocketRequestFromJson(data io.Reader) *WebSocketRequest { + var o *WebSocketRequest + json.NewDecoder(data).Decode(&o) + return o +} |