diff options
author | Wim <wim@42.be> | 2021-10-16 23:11:32 +0200 |
---|---|---|
committer | Wim <wim@42.be> | 2021-10-16 23:23:24 +0200 |
commit | 20f6c05ec50739d31f4dbe9fde0d223f2c43f6e8 (patch) | |
tree | 230edca06449a8d1755f08aabf45a03e07e6f17c /vendor/github.com/mattermost/mattermost-server/v6/model | |
parent | 57fce93af7f64f025cec6f3ed6088163086bc9fe (diff) | |
download | matterbridge-msglm-20f6c05ec50739d31f4dbe9fde0d223f2c43f6e8.tar.gz matterbridge-msglm-20f6c05ec50739d31f4dbe9fde0d223f2c43f6e8.tar.bz2 matterbridge-msglm-20f6c05ec50739d31f4dbe9fde0d223f2c43f6e8.zip |
Update vendor
Diffstat (limited to 'vendor/github.com/mattermost/mattermost-server/v6/model')
127 files changed, 30488 insertions, 0 deletions
diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/access.go b/vendor/github.com/mattermost/mattermost-server/v6/model/access.go new file mode 100644 index 00000000..f17c8fbe --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/access.go @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" +) + +const ( + AccessTokenGrantType = "authorization_code" + AccessTokenType = "bearer" + RefreshTokenGrantType = "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"` + IdToken string `json:"id_token"` +} + +// IsValid validates the AccessData and returns an error if it isn't configured +// correctly. +func (ad *AccessData) IsValid() *AppError { + if ad.ClientId == "" || len(ad.ClientId) > 26 { + return NewAppError("AccessData.IsValid", "model.access.is_valid.client_id.app_error", nil, "", http.StatusBadRequest) + } + + if ad.UserId == "" || 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 ad.RedirectUri == "" || 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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/analytics_row.go b/vendor/github.com/mattermost/mattermost-server/v6/model/analytics_row.go new file mode 100644 index 00000000..72ba3a09 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/analytics_row.go @@ -0,0 +1,11 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type AnalyticsRow struct { + Name string `json:"name"` + Value float64 `json:"value"` +} + +type AnalyticsRows []*AnalyticsRow diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/audit.go b/vendor/github.com/mattermost/mattermost-server/v6/model/audit.go new file mode 100644 index 00000000..3e8345c7 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/audit.go @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +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"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/auditconv.go b/vendor/github.com/mattermost/mattermost-server/v6/model/auditconv.go new file mode 100644 index 00000000..38b4381e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/auditconv.go @@ -0,0 +1,713 @@ +// 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 + case *RemoteCluster: + return newRemoteCluster(v), true + } + return val, false +} + +type auditChannel struct { + ID string + Name string + Type ChannelType +} + +// 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", string(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 +} + +type auditRemoteCluster struct { + RemoteId string + RemoteTeamId string + Name string + DisplayName string + SiteURL string + CreateAt int64 + LastPingAt int64 + CreatorId string +} + +// newRemoteCluster creates a simplified representation of RemoteCluster for output to audit log. +func newRemoteCluster(r *RemoteCluster) auditRemoteCluster { + var rc auditRemoteCluster + if r != nil { + rc.RemoteId = r.RemoteId + rc.RemoteTeamId = r.RemoteTeamId + rc.Name = r.Name + rc.DisplayName = r.DisplayName + rc.SiteURL = r.SiteURL + rc.CreateAt = r.CreateAt + rc.LastPingAt = r.LastPingAt + rc.CreatorId = r.CreatorId + } + return rc +} + +func (r auditRemoteCluster) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("remote_id", r.RemoteId) + enc.StringKey("remote_team_id", r.RemoteTeamId) + enc.StringKey("name", r.Name) + enc.StringKey("display_name", r.DisplayName) + enc.StringKey("site_url", r.SiteURL) + enc.Int64Key("create_at", r.CreateAt) + enc.Int64Key("last_ping_at", r.LastPingAt) + enc.StringKey("creator_id", r.CreatorId) +} + +func (r auditRemoteCluster) IsNil() bool { + return false +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/audits.go b/vendor/github.com/mattermost/mattermost-server/v6/model/audits.go new file mode 100644 index 00000000..1c547c89 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/audits.go @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +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) + } + return "" +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/authorize.go b/vendor/github.com/mattermost/mattermost-server/v6/model/authorize.go new file mode 100644 index 00000000..1a767e3c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/authorize.go @@ -0,0 +1,118 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" +) + +const ( + AuthCodeExpireTime = 60 * 10 // 10 minutes + AuthCodeResponseType = "code" + ImplicitResponseType = "token" + DefaultScope = "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 ad.Code == "" || 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 ar.ResponseType == "" { + return NewAppError("AuthData.IsValid", "model.authorize.is_valid.response_type.app_error", nil, "", http.StatusBadRequest) + } + + if ar.RedirectURI == "" || 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 = AuthCodeExpireTime + } + + if ad.CreateAt == 0 { + ad.CreateAt = GetMillis() + } + + if ad.Scope == "" { + ad.Scope = DefaultScope + } +} + +func (ad *AuthData) IsExpired() bool { + return GetMillis() > ad.CreateAt+int64(ad.ExpiresIn*1000) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/bot.go b/vendor/github.com/mattermost/mattermost-server/v6/model/bot.go new file mode 100644 index 00000000..fe9b9078 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/bot.go @@ -0,0 +1,204 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "net/http" + "strings" + "unicode/utf8" +) + +const ( + BotDisplayNameMaxRunes = UserFirstNameMaxRunes + BotDescriptionMaxRunes = 1024 + BotCreatorIdMaxRunes = KeyValuePluginIdMaxRunes // UserId or PluginId + BotWarnMetricBotUsername = "mattermost-advisor" + BotSystemBotUsername = "system-bot" +) + +// 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) > BotDisplayNameMaxRunes { + return NewAppError("Bot.IsValid", "model.bot.is_valid.user_id.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if utf8.RuneCountInString(b.Description) > BotDescriptionMaxRunes { + return NewAppError("Bot.IsValid", "model.bot.is_valid.description.app_error", b.Trace(), "", http.StatusBadRequest) + } + + if b.OwnerId == "" || utf8.RuneCountInString(b.OwnerId) > BotCreatorIdMaxRunes { + 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) +} + +// Patch modifies an existing bot with optional fields from the given patch. +// TODO 6.0: consider returning a boolean to indicate whether or not the patch +// applied any changes. +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 + } +} + +// WouldPatch returns whether or not the given patch would be applied or not. +func (b *Bot) WouldPatch(patch *BotPatch) bool { + if patch == nil { + return false + } + if patch.Username != nil && *patch.Username != b.Username { + return true + } + if patch.DisplayName != nil && *patch.DisplayName != b.DisplayName { + return true + } + if patch.Description != nil && *patch.Description != b.Description { + return true + } + return false +} + +// 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: SystemUserRoleId, + } +} + +// 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(ShowUsername), + } +} + +// 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 != ChannelTypeDirect { + 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/v6/model/builtin.go b/vendor/github.com/mattermost/mattermost-server/v6/model/builtin.go new file mode 100644 index 00000000..38e01f8b --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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/v6/model/bundle_info.go b/vendor/github.com/mattermost/mattermost-server/v6/model/bundle_info.go new file mode 100644 index 00000000..63969de7 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/bundle_info.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "github.com/mattermost/mattermost-server/v6/shared/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/v6/model/channel.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel.go new file mode 100644 index 00000000..75fc2680 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel.go @@ -0,0 +1,338 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto/sha1" + "encoding/hex" + "io" + "net/http" + "sort" + "strings" + "unicode/utf8" +) + +type ChannelType string + +const ( + ChannelTypeOpen ChannelType = "O" + ChannelTypePrivate ChannelType = "P" + ChannelTypeDirect ChannelType = "D" + ChannelTypeGroup ChannelType = "G" + + ChannelGroupMaxUsers = 8 + ChannelGroupMinUsers = 3 + DefaultChannelName = "town-square" + ChannelDisplayNameMaxRunes = 64 + ChannelNameMinLength = 2 + ChannelNameMaxLength = 64 + ChannelHeaderMaxRunes = 1024 + ChannelPurposeMaxRunes = 250 + ChannelCacheSize = 25000 + + ChannelSortByUsername = "username" + ChannelSortByStatus = "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 ChannelType `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"` + Shared *bool `json:"shared"` + TotalMsgCountRoot int64 `json:"total_msg_count_root"` + PolicyID *string `json:"policy_id" db:"-"` +} + +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 + Deleted bool + ExcludeChannelNames []string + TeamIds []string + GroupConstrained bool + ExcludeGroupConstrained bool + PolicyID string + ExcludePolicyConstrained bool + IncludePolicyID bool + Public bool + Private bool + 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"` +} + +type ChannelOption func(channel *Channel) + +func WithID(ID string) ChannelOption { + return func(channel *Channel) { + channel.Id = ID + } +} + +func (o *Channel) DeepCopy() *Channel { + copy := *o + if copy.SchemeId != nil { + copy.SchemeId = NewString(*o.SchemeId) + } + return © +} + +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) > ChannelDisplayNameMaxRunes { + 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 == ChannelTypeOpen || o.Type == ChannelTypePrivate || o.Type == ChannelTypeDirect || o.Type == ChannelTypeGroup) { + return NewAppError("Channel.IsValid", "model.channel.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.Header) > ChannelHeaderMaxRunes { + return NewAppError("Channel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(o.Purpose) > ChannelPurposeMaxRunes { + 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 != ChannelTypeDirect && 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 == ChannelTypeDirect || o.Type == ChannelTypeGroup +} + +func (o *Channel) IsOpen() bool { + return o.Type == ChannelTypeOpen +} + +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) IsShared() bool { + return o.Shared != nil && *o.Shared +} + +func (o *Channel) GetOtherUserIdForDM(userId string) string { + if o.Type != ChannelTypeDirect { + 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 + } + 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) > ChannelNameMaxLength { + name = name[:ChannelNameMaxLength] + } + + 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/v6/model/channel_count.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_count.go new file mode 100644 index 00000000..25329a15 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_count.go @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto/md5" + "fmt" + "sort" + "strconv" +) + +type ChannelCounts struct { + Counts map[string]int64 `json:"counts"` + CountsRoot map[string]int64 `json:"counts_root"` + UpdateTimes map[string]int64 `json:"update_times"` +} + +func (o *ChannelCounts) Etag() string { + // we don't include CountsRoot in ETag calculation, since it's a deriviative + 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) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/channel_data.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_data.go new file mode 100644 index 00000000..083a3f44 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_data.go @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +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) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/channel_list.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_list.go new file mode 100644 index 00000000..7b86b560 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_list.go @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type ChannelList []*Channel + +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)) +} + +type ChannelListWithTeamData []*ChannelWithTeamData + +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)) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/channel_member.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_member.go new file mode 100644 index 00000000..82bc84ae --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_member.go @@ -0,0 +1,163 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "strings" +) + +const ( + ChannelNotifyDefault = "default" + ChannelNotifyAll = "all" + ChannelNotifyMention = "mention" + ChannelNotifyNone = "none" + ChannelMarkUnreadAll = "all" + ChannelMarkUnreadMention = "mention" + IgnoreChannelMentionsDefault = "default" + IgnoreChannelMentionsOff = "off" + IgnoreChannelMentionsOn = "on" + IgnoreChannelMentionsNotifyProp = "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"` + MentionCountRoot int64 `json:"mention_count_root"` + MsgCountRoot int64 `json:"msg_count_root"` + 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"` + MentionCountRoot int64 `json:"mention_count_root"` + MsgCountRoot int64 `json:"msg_count_root"` + 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"` + MentionCountRoot int64 `json:"mention_count_root"` + MsgCountRoot int64 `json:"msg_count_root"` + 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 *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[DesktopNotifyProp] + 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[MarkUnreadNotifyProp] + 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[PushNotifyProp]; 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[EmailNotifyProp]; 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[IgnoreChannelMentionsNotifyProp]; 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 (o *ChannelMember) SetChannelMuted(muted bool) { + if o.IsChannelMuted() { + o.NotifyProps[MarkUnreadNotifyProp] = ChannelMarkUnreadAll + } else { + o.NotifyProps[MarkUnreadNotifyProp] = ChannelMarkUnreadMention + } +} + +func (o *ChannelMember) IsChannelMuted() bool { + return o.NotifyProps[MarkUnreadNotifyProp] == ChannelMarkUnreadMention +} + +func IsChannelNotifyLevelValid(notifyLevel string) bool { + return notifyLevel == ChannelNotifyDefault || + notifyLevel == ChannelNotifyAll || + notifyLevel == ChannelNotifyMention || + notifyLevel == ChannelNotifyNone +} + +func IsChannelMarkUnreadLevelValid(markUnreadLevel string) bool { + return markUnreadLevel == ChannelMarkUnreadAll || markUnreadLevel == ChannelMarkUnreadMention +} + +func IsSendEmailValid(sendEmail string) bool { + return sendEmail == ChannelNotifyDefault || sendEmail == "true" || sendEmail == "false" +} + +func IsIgnoreChannelMentionsValid(ignoreChannelMentions string) bool { + return ignoreChannelMentions == IgnoreChannelMentionsOn || ignoreChannelMentions == IgnoreChannelMentionsOff || ignoreChannelMentions == IgnoreChannelMentionsDefault +} + +func GetDefaultChannelNotifyProps() StringMap { + return StringMap{ + DesktopNotifyProp: ChannelNotifyDefault, + MarkUnreadNotifyProp: ChannelMarkUnreadAll, + PushNotifyProp: ChannelNotifyDefault, + EmailNotifyProp: ChannelNotifyDefault, + IgnoreChannelMentionsNotifyProp: IgnoreChannelMentionsDefault, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/channel_member_history.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_member_history.go new file mode 100644 index 00000000..b77e0ff9 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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/v6/model/channel_member_history_result.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_member_history_result.go new file mode 100644 index 00000000..8f43ca4e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_member_history_result.go @@ -0,0 +1,17 @@ +// 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 + UserDeleteAt int64 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/channel_mentions.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_mentions.go new file mode 100644 index 00000000..eb14e8ed --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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/v6/model/channel_search.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_search.go new file mode 100644 index 00000000..530deb20 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_search.go @@ -0,0 +1,22 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ChannelSearchDefaultLimit = 50 + +type ChannelSearch struct { + Term string `json:"term"` + ExcludeDefaultChannels bool `json:"exclude_default_channels"` + NotAssociatedToGroup string `json:"not_associated_to_group"` + TeamIds []string `json:"team_ids"` + GroupConstrained bool `json:"group_constrained"` + ExcludeGroupConstrained bool `json:"exclude_group_constrained"` + ExcludePolicyConstrained bool `json:"exclude_policy_constrained"` + Public bool `json:"public"` + Private bool `json:"private"` + IncludeDeleted bool `json:"include_deleted"` + Deleted bool `json:"deleted"` + Page *int `json:"page,omitempty"` + PerPage *int `json:"per_page,omitempty"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/channel_sidebar.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_sidebar.go new file mode 100644 index 00000000..fd4d44ca --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_sidebar.go @@ -0,0 +1,85 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "regexp" +) + +type SidebarCategoryType string +type SidebarCategorySorting string + +const ( + // Each sidebar category has a 'type'. System categories are Channels, Favorites and DMs + // All user-created categories will have type Custom + SidebarCategoryChannels SidebarCategoryType = "channels" + SidebarCategoryDirectMessages SidebarCategoryType = "direct_messages" + SidebarCategoryFavorites SidebarCategoryType = "favorites" + SidebarCategoryCustom SidebarCategoryType = "custom" + // Increment to use when adding/reordering things in the sidebar + MinimalSidebarSortDistance = 10 + // Default Sort Orders for categories + DefaultSidebarSortOrderFavorites = 0 + DefaultSidebarSortOrderChannels = DefaultSidebarSortOrderFavorites + MinimalSidebarSortDistance + DefaultSidebarSortOrderDMs = DefaultSidebarSortOrderChannels + MinimalSidebarSortDistance + // Sorting modes + // default for all categories except DMs (behaves like manual) + SidebarCategorySortDefault SidebarCategorySorting = "" + // sort manually + SidebarCategorySortManual SidebarCategorySorting = "manual" + // sort by recency (default for DMs) + SidebarCategorySortRecent SidebarCategorySorting = "recent" + // sort by display name alphabetically + SidebarCategorySortAlphabetical SidebarCategorySorting = "alpha" +) + +// SidebarCategory represents the corresponding DB table +// SortOrder is never returned to the user and only used for queries +type SidebarCategory struct { + Id string `json:"id"` + UserId string `json:"user_id"` + TeamId string `json:"team_id"` + SortOrder int64 `json:"-"` + Sorting SidebarCategorySorting `json:"sorting"` + Type SidebarCategoryType `json:"type"` + DisplayName string `json:"display_name"` + Muted bool `json:"muted"` + Collapsed bool `json:"collapsed"` +} + +// SidebarCategoryWithChannels combines data from SidebarCategory table with the Channel IDs that belong to that category +type SidebarCategoryWithChannels struct { + SidebarCategory + Channels []string `json:"channel_ids"` +} + +type SidebarCategoryOrder []string + +// OrderedSidebarCategories combines categories, their channel IDs and an array of Category IDs, sorted +type OrderedSidebarCategories struct { + Categories SidebarCategoriesWithChannels `json:"categories"` + Order SidebarCategoryOrder `json:"order"` +} + +type SidebarChannel struct { + ChannelId string `json:"channel_id"` + UserId string `json:"user_id"` + CategoryId string `json:"category_id"` + SortOrder int64 `json:"-"` +} + +type SidebarChannels []*SidebarChannel +type SidebarCategoriesWithChannels []*SidebarCategoryWithChannels + +var categoryIdPattern = regexp.MustCompile("(favorites|channels|direct_messages)_[a-z0-9]{26}_[a-z0-9]{26}") + +func IsValidCategoryId(s string) bool { + // Category IDs can either be regular IDs + if IsValidId(s) { + return true + } + + // Or default categories can follow the pattern {type}_{userID}_{teamID} + return categoryIdPattern.MatchString(s) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/channel_stats.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_stats.go new file mode 100644 index 00000000..cf44d541 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_stats.go @@ -0,0 +1,11 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type ChannelStats struct { + ChannelId string `json:"channel_id"` + MemberCount int64 `json:"member_count"` + GuestCount int64 `json:"guest_count"` + PinnedPostCount int64 `json:"pinnedpost_count"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/channel_view.go b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_view.go new file mode 100644 index 00000000..c34c438d --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/channel_view.go @@ -0,0 +1,15 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type ChannelView struct { + ChannelId string `json:"channel_id"` + PrevChannelId string `json:"prev_channel_id"` + CollapsedThreadsSupported bool `json:"collapsed_threads_supported"` +} + +type ChannelViewResponse struct { + Status string `json:"status"` + LastViewedAtTimes map[string]int64 `json:"last_viewed_at_times"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/client4.go b/vendor/github.com/mattermost/mattermost-server/v6/model/client4.go new file mode 100644 index 00000000..615b1264 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/client4.go @@ -0,0 +1,7690 @@ +// 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" + "net/http" + "net/url" + "strconv" + "strings" +) + +const ( + HeaderRequestId = "X-Request-ID" + HeaderVersionId = "X-Version-ID" + HeaderClusterId = "X-Cluster-ID" + HeaderEtagServer = "ETag" + HeaderEtagClient = "If-None-Match" + HeaderForwarded = "X-Forwarded-For" + HeaderRealIP = "X-Real-IP" + HeaderForwardedProto = "X-Forwarded-Proto" + HeaderToken = "token" + HeaderCsrfToken = "X-CSRF-Token" + HeaderBearer = "BEARER" + HeaderAuth = "Authorization" + HeaderCloudToken = "X-Cloud-Token" + HeaderRemoteclusterToken = "X-RemoteCluster-Token" + HeaderRemoteclusterId = "X-RemoteCluster-Id" + HeaderRequestedWith = "X-Requested-With" + HeaderRequestedWithXML = "XMLHttpRequest" + HeaderRange = "Range" + STATUS = "status" + StatusOk = "OK" + StatusFail = "FAIL" + StatusUnhealthy = "UNHEALTHY" + StatusRemove = "REMOVE" + + ClientDir = "client" + + APIURLSuffixV1 = "/api/v1" + APIURLSuffixV4 = "/api/v4" + APIURLSuffix = APIURLSuffixV4 +) + +type Response struct { + StatusCode int + 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 + + // TrueString is the string value sent to the server for true boolean query parameters. + trueString string + + // FalseString is the string value sent to the server for false boolean query parameters. + falseString string +} + +// SetBoolString is a helper method for overriding how true and false query string parameters are +// sent to the server. +// +// This method is only exposed for testing. It is never necessary to configure these values +// in production. +func (c *Client4) SetBoolString(value bool, valueStr string) { + if value { + c.trueString = valueStr + } else { + c.falseString = valueStr + } +} + +// boolString builds the query string parameter for boolean values. +func (c *Client4) boolString(value bool) string { + if value && c.trueString != "" { + return c.trueString + } else if !value && c.falseString != "" { + return c.falseString + } + + if value { + return "true" + } + return "false" +} + +func closeBody(r *http.Response) { + if r.Body != nil { + _, _ = io.Copy(ioutil.Discard, r.Body) + _ = r.Body.Close() + } +} + +func NewAPIv4Client(url string) *Client4 { + url = strings.TrimRight(url, "/") + return &Client4{url, url + APIURLSuffix, &http.Client{}, "", "", map[string]string{}, "", ""} +} + +func NewAPIv4SocketClient(socketPath string) *Client4 { + tr := &http.Transport{ + Dial: func(network, addr string) (net.Conn, error) { + return net.Dial("unix", socketPath) + }, + } + + client := NewAPIv4Client("http://_") + client.HTTPClient = &http.Client{Transport: tr} + + return client +} + +func BuildResponse(r *http.Response) *Response { + if r == nil { + return nil + } + + return &Response{ + StatusCode: r.StatusCode, + RequestId: r.Header.Get(HeaderRequestId), + Etag: r.Header.Get(HeaderEtagServer), + ServerVersion: r.Header.Get(HeaderVersionId), + Header: r.Header, + } +} + +func (c *Client4) SetToken(token string) { + c.AuthToken = token + c.AuthType = HeaderBearer +} + +// 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 = HeaderToken +} + +func (c *Client4) ClearOAuthToken() { + c.AuthToken = "" + c.AuthType = HeaderBearer +} + +func (c *Client4) usersRoute() string { + return "/users" +} + +func (c *Client4) userRoute(userId string) string { + return fmt.Sprintf(c.usersRoute()+"/%v", userId) +} + +func (c *Client4) userThreadsRoute(userID, teamID string) string { + return c.userRoute(userID) + c.teamRoute(teamID) + "/threads" +} + +func (c *Client4) userThreadRoute(userId, teamId, threadId string) string { + return c.userThreadsRoute(userId, teamId) + "/" + threadId +} + +func (c *Client4) userCategoryRoute(userID, teamID string) string { + return c.userRoute(userID) + c.teamRoute(teamID) + "/channels/categories" +} + +func (c *Client4) userAccessTokensRoute() string { + return fmt.Sprintf(c.usersRoute() + "/tokens") +} + +func (c *Client4) userAccessTokenRoute(tokenId string) string { + return fmt.Sprintf(c.usersRoute()+"/tokens/%v", tokenId) +} + +func (c *Client4) userByUsernameRoute(userName string) string { + return fmt.Sprintf(c.usersRoute()+"/username/%v", userName) +} + +func (c *Client4) userByEmailRoute(email string) string { + return fmt.Sprintf(c.usersRoute()+"/email/%v", email) +} + +func (c *Client4) botsRoute() string { + return "/bots" +} + +func (c *Client4) botRoute(botUserId string) string { + return fmt.Sprintf("%s/%s", c.botsRoute(), botUserId) +} + +func (c *Client4) teamsRoute() string { + return "/teams" +} + +func (c *Client4) teamRoute(teamId string) string { + return fmt.Sprintf(c.teamsRoute()+"/%v", teamId) +} + +func (c *Client4) teamAutoCompleteCommandsRoute(teamId string) string { + return fmt.Sprintf(c.teamsRoute()+"/%v/commands/autocomplete", teamId) +} + +func (c *Client4) teamByNameRoute(teamName string) string { + return fmt.Sprintf(c.teamsRoute()+"/name/%v", teamName) +} + +func (c *Client4) teamMemberRoute(teamId, userId string) string { + return fmt.Sprintf(c.teamRoute(teamId)+"/members/%v", userId) +} + +func (c *Client4) teamMembersRoute(teamId string) string { + return fmt.Sprintf(c.teamRoute(teamId) + "/members") +} + +func (c *Client4) teamStatsRoute(teamId string) string { + return fmt.Sprintf(c.teamRoute(teamId) + "/stats") +} + +func (c *Client4) teamImportRoute(teamId string) string { + return fmt.Sprintf(c.teamRoute(teamId) + "/import") +} + +func (c *Client4) channelsRoute() string { + return "/channels" +} + +func (c *Client4) channelsForTeamRoute(teamId string) string { + return fmt.Sprintf(c.teamRoute(teamId) + "/channels") +} + +func (c *Client4) channelRoute(channelId string) string { + return fmt.Sprintf(c.channelsRoute()+"/%v", channelId) +} + +func (c *Client4) channelByNameRoute(channelName, teamId string) string { + return fmt.Sprintf(c.teamRoute(teamId)+"/channels/name/%v", channelName) +} + +func (c *Client4) channelsForTeamForUserRoute(teamId, userId string, includeDeleted bool) string { + route := fmt.Sprintf(c.userRoute(userId) + c.teamRoute(teamId) + "/channels") + if includeDeleted { + query := fmt.Sprintf("?include_deleted=%v", includeDeleted) + return route + query + } + return route +} + +func (c *Client4) channelByNameForTeamNameRoute(channelName, teamName string) string { + return fmt.Sprintf(c.teamByNameRoute(teamName)+"/channels/name/%v", channelName) +} + +func (c *Client4) channelMembersRoute(channelId string) string { + return fmt.Sprintf(c.channelRoute(channelId) + "/members") +} + +func (c *Client4) channelMemberRoute(channelId, userId string) string { + return fmt.Sprintf(c.channelMembersRoute(channelId)+"/%v", userId) +} + +func (c *Client4) postsRoute() string { + return "/posts" +} + +func (c *Client4) postsEphemeralRoute() string { + return "/posts/ephemeral" +} + +func (c *Client4) configRoute() string { + return "/config" +} + +func (c *Client4) licenseRoute() string { + return "/license" +} + +func (c *Client4) postRoute(postId string) string { + return fmt.Sprintf(c.postsRoute()+"/%v", postId) +} + +func (c *Client4) filesRoute() string { + return "/files" +} + +func (c *Client4) fileRoute(fileId string) string { + return fmt.Sprintf(c.filesRoute()+"/%v", fileId) +} + +func (c *Client4) uploadsRoute() string { + return "/uploads" +} + +func (c *Client4) uploadRoute(uploadId string) string { + return fmt.Sprintf("%s/%s", c.uploadsRoute(), uploadId) +} + +func (c *Client4) pluginsRoute() string { + return "/plugins" +} + +func (c *Client4) pluginRoute(pluginId string) string { + return fmt.Sprintf(c.pluginsRoute()+"/%v", pluginId) +} + +func (c *Client4) systemRoute() string { + return "/system" +} + +func (c *Client4) cloudRoute() string { + return "/cloud" +} + +func (c *Client4) testEmailRoute() string { + return "/email/test" +} + +func (c *Client4) testSiteURLRoute() string { + return "/site_url/test" +} + +func (c *Client4) testS3Route() string { + return "/file/s3_test" +} + +func (c *Client4) databaseRoute() string { + return "/database" +} + +func (c *Client4) cacheRoute() string { + return "/caches" +} + +func (c *Client4) clusterRoute() string { + return "/cluster" +} + +func (c *Client4) incomingWebhooksRoute() string { + return "/hooks/incoming" +} + +func (c *Client4) incomingWebhookRoute(hookID string) string { + return fmt.Sprintf(c.incomingWebhooksRoute()+"/%v", hookID) +} + +func (c *Client4) complianceReportsRoute() string { + return "/compliance/reports" +} + +func (c *Client4) complianceReportRoute(reportId string) string { + return fmt.Sprintf("%s/%s", c.complianceReportsRoute(), reportId) +} + +func (c *Client4) complianceReportDownloadRoute(reportId string) string { + return fmt.Sprintf("%s/%s/download", c.complianceReportsRoute(), reportId) +} + +func (c *Client4) outgoingWebhooksRoute() string { + return "/hooks/outgoing" +} + +func (c *Client4) outgoingWebhookRoute(hookID string) string { + return fmt.Sprintf(c.outgoingWebhooksRoute()+"/%v", hookID) +} + +func (c *Client4) preferencesRoute(userId string) string { + return fmt.Sprintf(c.userRoute(userId) + "/preferences") +} + +func (c *Client4) userStatusRoute(userId string) string { + return fmt.Sprintf(c.userRoute(userId) + "/status") +} + +func (c *Client4) userStatusesRoute() string { + return fmt.Sprintf(c.usersRoute() + "/status") +} + +func (c *Client4) samlRoute() string { + return "/saml" +} + +func (c *Client4) ldapRoute() string { + return "/ldap" +} + +func (c *Client4) brandRoute() string { + return "/brand" +} + +func (c *Client4) dataRetentionRoute() string { + return "/data_retention" +} + +func (c *Client4) dataRetentionPolicyRoute(policyID string) string { + return fmt.Sprintf(c.dataRetentionRoute()+"/policies/%v", policyID) +} + +func (c *Client4) elasticsearchRoute() string { + return "/elasticsearch" +} + +func (c *Client4) bleveRoute() string { + return "/bleve" +} + +func (c *Client4) commandsRoute() string { + return "/commands" +} + +func (c *Client4) commandRoute(commandId string) string { + return fmt.Sprintf(c.commandsRoute()+"/%v", commandId) +} + +func (c *Client4) commandMoveRoute(commandId string) string { + return fmt.Sprintf(c.commandsRoute()+"/%v/move", commandId) +} + +func (c *Client4) emojisRoute() string { + return "/emoji" +} + +func (c *Client4) emojiRoute(emojiId string) string { + return fmt.Sprintf(c.emojisRoute()+"/%v", emojiId) +} + +func (c *Client4) emojiByNameRoute(name string) string { + return fmt.Sprintf(c.emojisRoute()+"/name/%v", name) +} + +func (c *Client4) reactionsRoute() string { + return "/reactions" +} + +func (c *Client4) oAuthAppsRoute() string { + return "/oauth/apps" +} + +func (c *Client4) oAuthAppRoute(appId string) string { + return fmt.Sprintf("/oauth/apps/%v", appId) +} + +func (c *Client4) openGraphRoute() string { + return "/opengraph" +} + +func (c *Client4) jobsRoute() string { + return "/jobs" +} + +func (c *Client4) rolesRoute() string { + return "/roles" +} + +func (c *Client4) schemesRoute() string { + return "/schemes" +} + +func (c *Client4) schemeRoute(id string) string { + return c.schemesRoute() + fmt.Sprintf("/%v", id) +} + +func (c *Client4) analyticsRoute() string { + return "/analytics" +} + +func (c *Client4) timezonesRoute() string { + return fmt.Sprintf(c.systemRoute() + "/timezones") +} + +func (c *Client4) channelSchemeRoute(channelId string) string { + return fmt.Sprintf(c.channelsRoute()+"/%v/scheme", channelId) +} + +func (c *Client4) teamSchemeRoute(teamId string) string { + return fmt.Sprintf(c.teamsRoute()+"/%v/scheme", teamId) +} + +func (c *Client4) totalUsersStatsRoute() string { + return fmt.Sprintf(c.usersRoute() + "/stats") +} + +func (c *Client4) redirectLocationRoute() string { + return "/redirect_location" +} + +func (c *Client4) serverBusyRoute() string { + return "/server_busy" +} + +func (c *Client4) userTermsOfServiceRoute(userId string) string { + return c.userRoute(userId) + "/terms_of_service" +} + +func (c *Client4) termsOfServiceRoute() string { + return "/terms_of_service" +} + +func (c *Client4) groupsRoute() string { + return "/groups" +} + +func (c *Client4) publishUserTypingRoute(userId string) string { + return c.userRoute(userId) + "/typing" +} + +func (c *Client4) groupRoute(groupID string) string { + return fmt.Sprintf("%s/%s", c.groupsRoute(), groupID) +} + +func (c *Client4) groupSyncableRoute(groupID, syncableID string, syncableType GroupSyncableType) string { + return fmt.Sprintf("%s/%ss/%s", c.groupRoute(groupID), strings.ToLower(syncableType.String()), syncableID) +} + +func (c *Client4) groupSyncablesRoute(groupID string, syncableType GroupSyncableType) string { + return fmt.Sprintf("%s/%ss", c.groupRoute(groupID), strings.ToLower(syncableType.String())) +} + +func (c *Client4) importsRoute() string { + return "/imports" +} + +func (c *Client4) exportsRoute() string { + return "/exports" +} + +func (c *Client4) exportRoute(name string) string { + return fmt.Sprintf(c.exportsRoute()+"/%v", name) +} + +func (c *Client4) sharedChannelsRoute() string { + return "/sharedchannels" +} + +func (c *Client4) permissionsRoute() string { + return "/permissions" +} + +func (c *Client4) DoAPIGet(url string, etag string) (*http.Response, error) { + return c.DoAPIRequest(http.MethodGet, c.APIURL+url, "", etag) +} + +func (c *Client4) DoAPIPost(url string, data string) (*http.Response, error) { + return c.DoAPIRequest(http.MethodPost, c.APIURL+url, data, "") +} + +func (c *Client4) DoAPIDeleteBytes(url string, data []byte) (*http.Response, error) { + return c.DoAPIRequestBytes(http.MethodDelete, c.APIURL+url, data, "") +} + +func (c *Client4) DoAPIPatchBytes(url string, data []byte) (*http.Response, error) { + return c.DoAPIRequestBytes(http.MethodPatch, c.APIURL+url, data, "") +} + +func (c *Client4) DoAPIPostBytes(url string, data []byte) (*http.Response, error) { + return c.DoAPIRequestBytes(http.MethodPost, c.APIURL+url, data, "") +} + +func (c *Client4) DoAPIPut(url string, data string) (*http.Response, error) { + return c.DoAPIRequest(http.MethodPut, c.APIURL+url, data, "") +} + +func (c *Client4) DoAPIPutBytes(url string, data []byte) (*http.Response, error) { + return c.DoAPIRequestBytes(http.MethodPut, c.APIURL+url, data, "") +} + +func (c *Client4) DoAPIDelete(url string) (*http.Response, error) { + return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, "", "") +} + +func (c *Client4) DoAPIRequest(method, url, data, etag string) (*http.Response, error) { + return c.DoAPIRequestReader(method, url, strings.NewReader(data), map[string]string{HeaderEtagClient: etag}) +} + +func (c *Client4) DoAPIRequestWithHeaders(method, url, data string, headers map[string]string) (*http.Response, error) { + return c.DoAPIRequestReader(method, url, strings.NewReader(data), headers) +} + +func (c *Client4) DoAPIRequestBytes(method, url string, data []byte, etag string) (*http.Response, error) { + return c.DoAPIRequestReader(method, url, bytes.NewReader(data), map[string]string{HeaderEtagClient: etag}) +} + +func (c *Client4) DoAPIRequestReader(method, url string, data io.Reader, headers map[string]string) (*http.Response, error) { + rq, err := http.NewRequest(method, url, data) + if err != nil { + return nil, err + } + + for k, v := range headers { + rq.Header.Set(k, v) + } + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, 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 { + return rp, err + } + + 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, error) { + return c.doUploadFile(url, bytes.NewReader(data), contentType, 0) +} + +func (c *Client4) doUploadFile(url string, body io.Reader, contentType string, contentLength int64) (*FileUploadResponse, *Response, error) { + rq, err := http.NewRequest("POST", c.APIURL+url, body) + if err != nil { + return nil, nil, err + } + if contentLength != 0 { + rq.ContentLength = contentLength + } + rq.Header.Set("Content-Type", contentType) + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return nil, BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + var res FileUploadResponse + if jsonErr := json.NewDecoder(rp.Body).Decode(&res); jsonErr != nil { + return nil, nil, NewAppError("doUploadFile", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &res, BuildResponse(rp), nil +} + +func (c *Client4) DoEmojiUploadFile(url string, data []byte, contentType string) (*Emoji, *Response, error) { + rq, err := http.NewRequest("POST", c.APIURL+url, bytes.NewReader(data)) + if err != nil { + return nil, nil, err + } + rq.Header.Set("Content-Type", contentType) + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return nil, BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + var e Emoji + if jsonErr := json.NewDecoder(rp.Body).Decode(&e); jsonErr != nil { + return nil, nil, NewAppError("DoEmojiUploadFile", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &e, BuildResponse(rp), nil +} + +func (c *Client4) DoUploadImportTeam(url string, data []byte, contentType string) (map[string]string, *Response, error) { + rq, err := http.NewRequest("POST", c.APIURL+url, bytes.NewReader(data)) + if err != nil { + return nil, nil, err + } + rq.Header.Set("Content-Type", contentType) + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return nil, BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + return MapFromJSON(rp.Body), BuildResponse(rp), nil +} + +// Authentication Section + +// LoginById authenticates a user by user id and password. +func (c *Client4) LoginById(id string, password string) (*User, *Response, error) { + 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, error) { + 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, error) { + m := make(map[string]string) + m["login_id"] = loginId + m["password"] = password + m["ldap_only"] = c.boolString(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, error) { + 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, error) { + 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, error) { + r, err := c.DoAPIPost("/users/login", MapToJSON(m)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + c.AuthToken = r.Header.Get(HeaderToken) + c.AuthType = HeaderBearer + + var user User + if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil { + return nil, nil, NewAppError("login", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &user, BuildResponse(r), nil +} + +// Logout terminates the current user's session. +func (c *Client4) Logout() (*Response, error) { + r, err := c.DoAPIPost("/users/logout", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + c.AuthToken = "" + c.AuthType = HeaderBearer + return BuildResponse(r), nil +} + +// SwitchAccountType changes a user's login type from one type to another. +func (c *Client4) SwitchAccountType(switchRequest *SwitchRequest) (string, *Response, error) { + buf, err := json.Marshal(switchRequest) + if err != nil { + return "", BuildResponse(nil), NewAppError("SwitchAccountType", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.usersRoute()+"/login/switch", buf) + if err != nil { + return "", BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body)["follow_link"], BuildResponse(r), nil +} + +// User Section + +// CreateUser creates a user in the system based on the provided user struct. +func (c *Client4) CreateUser(user *User) (*User, *Response, error) { + userJSON, jsonErr := json.Marshal(user) + if jsonErr != nil { + return nil, nil, NewAppError("CreateUser", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + + r, err := c.DoAPIPost(c.usersRoute(), string(userJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u User + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("CreateUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// CreateUserWithToken creates a user in the system based on the provided tokenId. +func (c *Client4) CreateUserWithToken(user *User, tokenId string) (*User, *Response, error) { + if tokenId == "" { + return nil, nil, NewAppError("MissingHashOrData", "api.user.create_user.missing_token.app_error", nil, "", http.StatusBadRequest) + } + + query := fmt.Sprintf("?t=%v", tokenId) + buf, err := json.Marshal(user) + if err != nil { + return nil, nil, NewAppError("CreateUserWithToken", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.usersRoute()+query, buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var u User + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("CreateUserWithToken", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// CreateUserWithInviteId creates a user in the system based on the provided invited id. +func (c *Client4) CreateUserWithInviteId(user *User, inviteId string) (*User, *Response, error) { + if inviteId == "" { + return nil, nil, NewAppError("MissingInviteId", "api.user.create_user.missing_invite_id.app_error", nil, "", http.StatusBadRequest) + } + + query := fmt.Sprintf("?iid=%v", url.QueryEscape(inviteId)) + buf, err := json.Marshal(user) + if err != nil { + return nil, nil, NewAppError("CreateUserWithInviteId", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.usersRoute()+query, buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var u User + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("CreateUserWithInviteId", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// GetMe returns the logged in user. +func (c *Client4) GetMe(etag string) (*User, *Response, error) { + r, err := c.DoAPIGet(c.userRoute(Me), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u User + if r.StatusCode == http.StatusNotModified { + return &u, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("GetMe", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// GetUser returns a user based on the provided user id string. +func (c *Client4) GetUser(userId, etag string) (*User, *Response, error) { + r, err := c.DoAPIGet(c.userRoute(userId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u User + if r.StatusCode == http.StatusNotModified { + return &u, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("GetUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// GetUserByUsername returns a user based on the provided user name string. +func (c *Client4) GetUserByUsername(userName, etag string) (*User, *Response, error) { + r, err := c.DoAPIGet(c.userByUsernameRoute(userName), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u User + if r.StatusCode == http.StatusNotModified { + return &u, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("GetUserByUsername", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// GetUserByEmail returns a user based on the provided user email string. +func (c *Client4) GetUserByEmail(email, etag string) (*User, *Response, error) { + r, err := c.DoAPIGet(c.userByEmailRoute(email), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u User + if r.StatusCode == http.StatusNotModified { + return &u, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("GetUserByEmail", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?in_team=%v&name=%v&limit=%d", teamId, username, limit) + r, err := c.DoAPIGet(c.usersRoute()+"/autocomplete"+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u UserAutocomplete + if r.StatusCode == http.StatusNotModified { + return &u, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("AutocompleteUsersInTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?in_team=%v&in_channel=%v&name=%v&limit=%d", teamId, channelId, username, limit) + r, err := c.DoAPIGet(c.usersRoute()+"/autocomplete"+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u UserAutocomplete + if r.StatusCode == http.StatusNotModified { + return &u, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("AutocompleteUsersInChannel", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// AutocompleteUsers returns the users in the system based on search term. +func (c *Client4) AutocompleteUsers(username string, limit int, etag string) (*UserAutocomplete, *Response, error) { + query := fmt.Sprintf("?name=%v&limit=%d", username, limit) + r, err := c.DoAPIGet(c.usersRoute()+"/autocomplete"+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u UserAutocomplete + if r.StatusCode == http.StatusNotModified { + return &u, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("AutocompleteUsers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// GetDefaultProfileImage gets the default user's profile image. Must be logged in. +func (c *Client4) GetDefaultProfileImage(userId string) ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.userRoute(userId)+"/image/default", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetDefaultProfileImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + + return data, BuildResponse(r), nil +} + +// GetProfileImage gets user's profile image. Must be logged in. +func (c *Client4) GetProfileImage(userId, etag string) ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.userRoute(userId)+"/image", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetProfileImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersInTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?sort=create_at&in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetNewUsersInTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?sort=last_activity_at&in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetRecentlyActiveUsersInTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?active=true&in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetActiveUsersInTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?not_in_team=%v&page=%v&per_page=%v", teamId, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersNotInTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v", channelId, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersInChannel", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v&sort=status", channelId, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersInChannelByStatus", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?in_team=%v¬_in_channel=%v&page=%v&per_page=%v", teamId, channelId, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersNotInChannel", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?without_team=1&page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersWithoutTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetUsersInGroup returns a page of users in a group. Page counting starts at 0. +func (c *Client4) GetUsersInGroup(groupID string, page int, perPage int, etag string) ([]*User, *Response, error) { + query := fmt.Sprintf("?in_group=%v&page=%v&per_page=%v", groupID, page, perPage) + r, err := c.DoAPIGet(c.usersRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersInGroup", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetUsersByIds returns a list of users based on the provided user ids. +func (c *Client4) GetUsersByIds(userIds []string) ([]*User, *Response, error) { + r, err := c.DoAPIPost(c.usersRoute()+"/ids", ArrayToJSON(userIds)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersByIds", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetUsersByIds returns a list of users based on the provided user ids. +func (c *Client4) GetUsersByIdsWithOptions(userIds []string, options *UserGetByIdsOptions) ([]*User, *Response, error) { + v := url.Values{} + if options.Since != 0 { + v.Set("since", fmt.Sprintf("%d", options.Since)) + } + + url := c.usersRoute() + "/ids" + if len(v) > 0 { + url += "?" + v.Encode() + } + + r, err := c.DoAPIPost(url, ArrayToJSON(userIds)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersByIdsWithOptions", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetUsersByUsernames returns a list of users based on the provided usernames. +func (c *Client4) GetUsersByUsernames(usernames []string) ([]*User, *Response, error) { + r, err := c.DoAPIPost(c.usersRoute()+"/usernames", ArrayToJSON(usernames)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersByUsernames", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIPost(c.usersRoute()+"/group_channels", ArrayToJSON(groupChannelIds)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + usersByChannelId := map[string][]*User{} + json.NewDecoder(r.Body).Decode(&usersByChannelId) + return usersByChannelId, BuildResponse(r), nil +} + +// SearchUsers returns a list of users based on some search criteria. +func (c *Client4) SearchUsers(search *UserSearch) ([]*User, *Response, error) { + buf, err := json.Marshal(search) + if err != nil { + return nil, nil, NewAppError("SearchUsers", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.usersRoute()+"/search", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*User + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("SearchUsers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// UpdateUser updates a user in the system based on the provided user struct. +func (c *Client4) UpdateUser(user *User) (*User, *Response, error) { + buf, err := json.Marshal(user) + if err != nil { + return nil, nil, NewAppError("UpdateUser", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.userRoute(user.Id), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u User + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("UpdateUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// PatchUser partially updates a user in the system. Any missing fields are not updated. +func (c *Client4) PatchUser(userId string, patch *UserPatch) (*User, *Response, error) { + buf, err := json.Marshal(patch) + if err != nil { + return nil, nil, NewAppError("PatchUser", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.userRoute(userId)+"/patch", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u User + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("PatchUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// UpdateUserAuth updates a user AuthData (uthData, authService and password) in the system. +func (c *Client4) UpdateUserAuth(userId string, userAuth *UserAuth) (*UserAuth, *Response, error) { + buf, err := json.Marshal(userAuth) + if err != nil { + return nil, nil, NewAppError("UpdateUserAuth", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.userRoute(userId)+"/auth", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var ua UserAuth + if jsonErr := json.NewDecoder(r.Body).Decode(&ua); jsonErr != nil { + return nil, nil, NewAppError("UpdateUserAuth", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &ua, BuildResponse(r), nil +} + +// 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) (*Response, error) { + requestBody := make(map[string]interface{}) + requestBody["activate"] = activate + requestBody["code"] = code + + r, err := c.DoAPIPut(c.userRoute(userId)+"/mfa", StringInterfaceToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIPost(c.userRoute(userId)+"/mfa/generate", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var secret MfaSecret + if jsonErr := json.NewDecoder(r.Body).Decode(&secret); jsonErr != nil { + return nil, nil, NewAppError("GenerateMfaSecret", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &secret, BuildResponse(r), nil +} + +// 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) (*Response, error) { + requestBody := map[string]string{"current_password": currentPassword, "new_password": newPassword} + r, err := c.DoAPIPut(c.userRoute(userId)+"/password", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UpdateUserHashedPassword updates a user's password with an already-hashed password. Must be a system administrator. +func (c *Client4) UpdateUserHashedPassword(userId, newHashedPassword string) (*Response, error) { + requestBody := map[string]string{"already_hashed": "true", "new_password": newHashedPassword} + r, err := c.DoAPIPut(c.userRoute(userId)+"/password", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// PromoteGuestToUser convert a guest into a regular user +func (c *Client4) PromoteGuestToUser(guestId string) (*Response, error) { + r, err := c.DoAPIPost(c.userRoute(guestId)+"/promote", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// DemoteUserToGuest convert a regular user into a guest +func (c *Client4) DemoteUserToGuest(guestId string) (*Response, error) { + r, err := c.DoAPIPost(c.userRoute(guestId)+"/demote", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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) (*Response, error) { + requestBody := map[string]string{"roles": roles} + r, err := c.DoAPIPut(c.userRoute(userId)+"/roles", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UpdateUserActive updates status of a user whether active or not. +func (c *Client4) UpdateUserActive(userId string, active bool) (*Response, error) { + requestBody := make(map[string]interface{}) + requestBody["active"] = active + r, err := c.DoAPIPut(c.userRoute(userId)+"/active", StringInterfaceToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + + return BuildResponse(r), nil +} + +// DeleteUser deactivates a user in the system based on the provided user id string. +func (c *Client4) DeleteUser(userId string) (*Response, error) { + r, err := c.DoAPIDelete(c.userRoute(userId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// PermanentDeleteUser deletes a user in the system based on the provided user id string. +func (c *Client4) PermanentDeleteUser(userId string) (*Response, error) { + r, err := c.DoAPIDelete(c.userRoute(userId) + "?permanent=" + c.boolString(true)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// ConvertUserToBot converts a user to a bot user. +func (c *Client4) ConvertUserToBot(userId string) (*Bot, *Response, error) { + r, err := c.DoAPIPost(c.userRoute(userId)+"/convert_to_bot", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var bot *Bot + err = json.NewDecoder(r.Body).Decode(&bot) + if err != nil { + return nil, BuildResponse(r), NewAppError("ConvertUserToBot", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return bot, BuildResponse(r), nil +} + +// ConvertBotToUser converts a bot user to a user. +func (c *Client4) ConvertBotToUser(userId string, userPatch *UserPatch, setSystemAdmin bool) (*User, *Response, error) { + var query string + if setSystemAdmin { + query = "?set_system_admin=true" + } + buf, err := json.Marshal(userPatch) + if err != nil { + return nil, nil, NewAppError("ConvertBotToUser", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.botRoute(userId)+"/convert_to_user"+query, buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u User + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("ConvertBotToUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// PermanentDeleteAll permanently deletes all users in the system. This is a local only endpoint +func (c *Client4) PermanentDeleteAllUsers() (*Response, error) { + r, err := c.DoAPIDelete(c.usersRoute()) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// SendPasswordResetEmail will send a link for password resetting to a user with the +// provided email. +func (c *Client4) SendPasswordResetEmail(email string) (*Response, error) { + requestBody := map[string]string{"email": email} + r, err := c.DoAPIPost(c.usersRoute()+"/password/reset/send", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// ResetPassword uses a recovery code to update reset a user's password. +func (c *Client4) ResetPassword(token, newPassword string) (*Response, error) { + requestBody := map[string]string{"token": token, "new_password": newPassword} + r, err := c.DoAPIPost(c.usersRoute()+"/password/reset", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetSessions returns a list of sessions based on the provided user id string. +func (c *Client4) GetSessions(userId, etag string) ([]*Session, *Response, error) { + r, err := c.DoAPIGet(c.userRoute(userId)+"/sessions", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Session + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetSessions", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// RevokeSession revokes a user session based on the provided user id and session id strings. +func (c *Client4) RevokeSession(userId, sessionId string) (*Response, error) { + requestBody := map[string]string{"session_id": sessionId} + r, err := c.DoAPIPost(c.userRoute(userId)+"/sessions/revoke", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// RevokeAllSessions revokes all sessions for the provided user id string. +func (c *Client4) RevokeAllSessions(userId string) (*Response, error) { + r, err := c.DoAPIPost(c.userRoute(userId)+"/sessions/revoke/all", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// RevokeAllSessions revokes all sessions for all the users. +func (c *Client4) RevokeSessionsFromAllUsers() (*Response, error) { + r, err := c.DoAPIPost(c.usersRoute()+"/sessions/revoke/all", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// AttachDeviceId attaches a mobile device ID to the current session. +func (c *Client4) AttachDeviceId(deviceId string) (*Response, error) { + requestBody := map[string]string{"device_id": deviceId} + r, err := c.DoAPIPut(c.usersRoute()+"/sessions/device", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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. +// An optional boolean can be set to include collapsed thread unreads. Must be authenticated. +func (c *Client4) GetTeamsUnreadForUser(userId, teamIdToExclude string, includeCollapsedThreads bool) ([]*TeamUnread, *Response, error) { + query := url.Values{} + + if teamIdToExclude != "" { + query.Set("exclude_team", teamIdToExclude) + } + + if includeCollapsedThreads { + query.Set("include_collapsed_threads", "true") + } + + r, err := c.DoAPIGet(c.userRoute(userId)+"/teams/unread?"+query.Encode(), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var list []*TeamUnread + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetTeamsUnreadForUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.userRoute(userId)+"/audits"+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var audits Audits + err = json.NewDecoder(r.Body).Decode(&audits) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetUserAudits", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return audits, BuildResponse(r), nil +} + +// VerifyUserEmail will verify a user's email using the supplied token. +func (c *Client4) VerifyUserEmail(token string) (*Response, error) { + requestBody := map[string]string{"token": token} + r, err := c.DoAPIPost(c.usersRoute()+"/email/verify", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// VerifyUserEmailWithoutToken will verify a user's email by its Id. (Requires manage system role) +func (c *Client4) VerifyUserEmailWithoutToken(userId string) (*User, *Response, error) { + r, err := c.DoAPIPost(c.userRoute(userId)+"/email/verify/member", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u User + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("VerifyUserEmailWithoutToken", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// 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) (*Response, error) { + requestBody := map[string]string{"email": email} + r, err := c.DoAPIPost(c.usersRoute()+"/email/verify/send", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// SetDefaultProfileImage resets the profile image to a default generated one. +func (c *Client4) SetDefaultProfileImage(userId string) (*Response, error) { + r, err := c.DoAPIDelete(c.userRoute(userId) + "/image") + if err != nil { + return BuildResponse(r), err + } + return BuildResponse(r), nil +} + +// SetProfileImage sets profile image of the user. +func (c *Client4) SetProfileImage(userId string, data []byte) (*Response, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("image", "profile.png") + if err != nil { + return nil, 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 nil, NewAppError("SetProfileImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if err = writer.Close(); err != nil { + return nil, NewAppError("SetProfileImage", "model.client.set_profile_user.writer.app_error", nil, err.Error(), http.StatusBadRequest) + } + + rq, err := http.NewRequest("POST", c.APIURL+c.userRoute(userId)+"/image", bytes.NewReader(body.Bytes())) + if err != nil { + return nil, err + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + return BuildResponse(rp), nil +} + +// 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, error) { + requestBody := map[string]string{"description": description} + r, err := c.DoAPIPost(c.userRoute(userId)+"/tokens", MapToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var uat UserAccessToken + if jsonErr := json.NewDecoder(r.Body).Decode(&uat); jsonErr != nil { + return nil, nil, NewAppError("CreateUserAccessToken", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &uat, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.userAccessTokensRoute()+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*UserAccessToken + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUserAccessTokens", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.userAccessTokenRoute(tokenId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var uat UserAccessToken + if jsonErr := json.NewDecoder(r.Body).Decode(&uat); jsonErr != nil { + return nil, nil, NewAppError("GetUserAccessToken", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &uat, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.userRoute(userId)+"/tokens"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*UserAccessToken + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUserAccessTokensForUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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) (*Response, error) { + requestBody := map[string]string{"token_id": tokenId} + r, err := c.DoAPIPost(c.usersRoute()+"/tokens/revoke", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// SearchUserAccessTokens returns user access tokens matching the provided search term. +func (c *Client4) SearchUserAccessTokens(search *UserAccessTokenSearch) ([]*UserAccessToken, *Response, error) { + buf, err := json.Marshal(search) + if err != nil { + return nil, nil, NewAppError("SearchUserAccessTokens", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.usersRoute()+"/tokens/search", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*UserAccessToken + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("SearchUserAccessTokens", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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) (*Response, error) { + requestBody := map[string]string{"token_id": tokenId} + r, err := c.DoAPIPost(c.usersRoute()+"/tokens/disable", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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) (*Response, error) { + requestBody := map[string]string{"token_id": tokenId} + r, err := c.DoAPIPost(c.usersRoute()+"/tokens/enable", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// Bots section + +// CreateBot creates a bot in the system based on the provided bot struct. +func (c *Client4) CreateBot(bot *Bot) (*Bot, *Response, error) { + buf, err := json.Marshal(bot) + if err != nil { + return nil, nil, NewAppError("CreateBot", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.botsRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var resp *Bot + err = json.NewDecoder(r.Body).Decode(&resp) + if err != nil { + return nil, BuildResponse(r), NewAppError("CreateBot", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return resp, BuildResponse(r), nil +} + +// PatchBot partially updates a bot. Any missing fields are not updated. +func (c *Client4) PatchBot(userId string, patch *BotPatch) (*Bot, *Response, error) { + buf, err := json.Marshal(patch) + if err != nil { + return nil, nil, NewAppError("PatchBot", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.botRoute(userId), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var bot *Bot + err = json.NewDecoder(r.Body).Decode(&bot) + if err != nil { + return nil, BuildResponse(r), NewAppError("PatchBot", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return bot, BuildResponse(r), nil +} + +// GetBot fetches the given, undeleted bot. +func (c *Client4) GetBot(userId string, etag string) (*Bot, *Response, error) { + r, err := c.DoAPIGet(c.botRoute(userId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var bot *Bot + err = json.NewDecoder(r.Body).Decode(&bot) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetBot", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return bot, BuildResponse(r), nil +} + +// GetBotIncludeDeleted fetches the given bot, even if it is deleted. +func (c *Client4) GetBotIncludeDeleted(userId string, etag string) (*Bot, *Response, error) { + r, err := c.DoAPIGet(c.botRoute(userId)+"?include_deleted="+c.boolString(true), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var bot *Bot + err = json.NewDecoder(r.Body).Decode(&bot) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetBotIncludeDeleted", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return bot, BuildResponse(r), nil +} + +// GetBots fetches the given page of bots, excluding deleted. +func (c *Client4) GetBots(page, perPage int, etag string) ([]*Bot, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.botsRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var bots BotList + err = json.NewDecoder(r.Body).Decode(&bots) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetBots", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return bots, BuildResponse(r), nil +} + +// GetBotsIncludeDeleted fetches the given page of bots, including deleted. +func (c *Client4) GetBotsIncludeDeleted(page, perPage int, etag string) ([]*Bot, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&include_deleted="+c.boolString(true), page, perPage) + r, err := c.DoAPIGet(c.botsRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var bots BotList + err = json.NewDecoder(r.Body).Decode(&bots) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetBotsIncludeDeleted", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return bots, BuildResponse(r), nil +} + +// GetBotsOrphaned fetches the given page of bots, only including orphanded bots. +func (c *Client4) GetBotsOrphaned(page, perPage int, etag string) ([]*Bot, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&only_orphaned="+c.boolString(true), page, perPage) + r, err := c.DoAPIGet(c.botsRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var bots BotList + err = json.NewDecoder(r.Body).Decode(&bots) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetBotsOrphaned", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return bots, BuildResponse(r), nil +} + +// DisableBot disables the given bot in the system. +func (c *Client4) DisableBot(botUserId string) (*Bot, *Response, error) { + r, err := c.DoAPIPostBytes(c.botRoute(botUserId)+"/disable", nil) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var bot *Bot + err = json.NewDecoder(r.Body).Decode(&bot) + if err != nil { + return nil, BuildResponse(r), NewAppError("DisableBot", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return bot, BuildResponse(r), nil +} + +// EnableBot disables the given bot in the system. +func (c *Client4) EnableBot(botUserId string) (*Bot, *Response, error) { + r, err := c.DoAPIPostBytes(c.botRoute(botUserId)+"/enable", nil) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var bot *Bot + err = json.NewDecoder(r.Body).Decode(&bot) + if err != nil { + return nil, BuildResponse(r), NewAppError("EnableBot", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return bot, BuildResponse(r), nil +} + +// AssignBot assigns the given bot to the given user +func (c *Client4) AssignBot(botUserId, newOwnerId string) (*Bot, *Response, error) { + r, err := c.DoAPIPostBytes(c.botRoute(botUserId)+"/assign/"+newOwnerId, nil) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var bot *Bot + err = json.NewDecoder(r.Body).Decode(&bot) + if err != nil { + return nil, BuildResponse(r), NewAppError("AssignBot", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return bot, BuildResponse(r), nil +} + +// Team Section + +// CreateTeam creates a team in the system based on the provided team struct. +func (c *Client4) CreateTeam(team *Team) (*Team, *Response, error) { + buf, err := json.Marshal(team) + if err != nil { + return nil, nil, NewAppError("CreateTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.teamsRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var t Team + if jsonErr := json.NewDecoder(r.Body).Decode(&t); jsonErr != nil { + return nil, nil, NewAppError("CreateTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &t, BuildResponse(r), nil +} + +// GetTeam returns a team based on the provided team id string. +func (c *Client4) GetTeam(teamId, etag string) (*Team, *Response, error) { + r, err := c.DoAPIGet(c.teamRoute(teamId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var t Team + if jsonErr := json.NewDecoder(r.Body).Decode(&t); jsonErr != nil { + return nil, nil, NewAppError("GetTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &t, BuildResponse(r), nil +} + +// GetAllTeams returns all teams based on permissions. +func (c *Client4) GetAllTeams(etag string, page int, perPage int) ([]*Team, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.teamsRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Team + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetAllTeams", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetAllTeamsWithTotalCount returns all teams based on permissions. +func (c *Client4) GetAllTeamsWithTotalCount(etag string, page int, perPage int) ([]*Team, int64, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&include_total_count="+c.boolString(true), page, perPage) + r, err := c.DoAPIGet(c.teamsRoute()+query, etag) + if err != nil { + return nil, 0, BuildResponse(r), err + } + defer closeBody(r) + var listWithCount TeamsWithCount + if jsonErr := json.NewDecoder(r.Body).Decode(&listWithCount); jsonErr != nil { + return nil, 0, nil, NewAppError("GetAllTeamsWithTotalCount", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return listWithCount.Teams, listWithCount.TotalCount, BuildResponse(r), nil +} + +// GetAllTeamsExcludePolicyConstrained returns all teams which are not part of a data retention policy. +// Must be a system administrator. +func (c *Client4) GetAllTeamsExcludePolicyConstrained(etag string, page int, perPage int) ([]*Team, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&exclude_policy_constrained=%v", page, perPage, true) + r, err := c.DoAPIGet(c.teamsRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Team + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetAllTeamsExcludePolicyConstrained", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetTeamByName returns a team based on the provided team name string. +func (c *Client4) GetTeamByName(name, etag string) (*Team, *Response, error) { + r, err := c.DoAPIGet(c.teamByNameRoute(name), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var t Team + if jsonErr := json.NewDecoder(r.Body).Decode(&t); jsonErr != nil { + return nil, nil, NewAppError("GetTeamByName", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &t, BuildResponse(r), nil +} + +// SearchTeams returns teams matching the provided search term. +func (c *Client4) SearchTeams(search *TeamSearch) ([]*Team, *Response, error) { + buf, err := json.Marshal(search) + if err != nil { + return nil, nil, NewAppError("SearchTeams", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.teamsRoute()+"/search", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Team + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("SearchTeams", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// SearchTeamsPaged returns a page of teams and the total count matching the provided search term. +func (c *Client4) SearchTeamsPaged(search *TeamSearch) ([]*Team, int64, *Response, error) { + if search.Page == nil { + search.Page = NewInt(0) + } + if search.PerPage == nil { + search.PerPage = NewInt(100) + } + buf, err := json.Marshal(search) + if err != nil { + return nil, 0, BuildResponse(nil), NewAppError("SearchTeamsPaged", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.teamsRoute()+"/search", buf) + if err != nil { + return nil, 0, BuildResponse(r), err + } + defer closeBody(r) + var listWithCount TeamsWithCount + if jsonErr := json.NewDecoder(r.Body).Decode(&listWithCount); jsonErr != nil { + return nil, 0, nil, NewAppError("GetAllTeamsWithTotalCount", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return listWithCount.Teams, listWithCount.TotalCount, BuildResponse(r), nil +} + +// TeamExists returns true or false if the team exist or not. +func (c *Client4) TeamExists(name, etag string) (bool, *Response, error) { + r, err := c.DoAPIGet(c.teamByNameRoute(name)+"/exists", etag) + if err != nil { + return false, BuildResponse(r), err + } + defer closeBody(r) + return MapBoolFromJSON(r.Body)["exists"], BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.userRoute(userId)+"/teams", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Team + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetTeamsForUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetTeamMember returns a team member based on the provided team and user id strings. +func (c *Client4) GetTeamMember(teamId, userId, etag string) (*TeamMember, *Response, error) { + r, err := c.DoAPIGet(c.teamMemberRoute(teamId, userId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tm TeamMember + if r.StatusCode == http.StatusNotModified { + return &tm, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&tm); jsonErr != nil { + return nil, nil, NewAppError("GetTeamMember", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &tm, BuildResponse(r), nil +} + +// UpdateTeamMemberRoles will update the roles on a team for a user. +func (c *Client4) UpdateTeamMemberRoles(teamId, userId, newRoles string) (*Response, error) { + requestBody := map[string]string{"roles": newRoles} + r, err := c.DoAPIPut(c.teamMemberRoute(teamId, userId)+"/roles", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UpdateTeamMemberSchemeRoles will update the scheme-derived roles on a team for a user. +func (c *Client4) UpdateTeamMemberSchemeRoles(teamId string, userId string, schemeRoles *SchemeRoles) (*Response, error) { + buf, err := json.Marshal(schemeRoles) + if err != nil { + return nil, NewAppError("UpdateTeamMemberSchemeRoles", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.teamMemberRoute(teamId, userId)+"/schemeRoles", buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UpdateTeam will update a team. +func (c *Client4) UpdateTeam(team *Team) (*Team, *Response, error) { + buf, err := json.Marshal(team) + if err != nil { + return nil, nil, NewAppError("UpdateTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.teamRoute(team.Id), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var t Team + if jsonErr := json.NewDecoder(r.Body).Decode(&t); jsonErr != nil { + return nil, nil, NewAppError("UpdateTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &t, BuildResponse(r), nil +} + +// PatchTeam partially updates a team. Any missing fields are not updated. +func (c *Client4) PatchTeam(teamId string, patch *TeamPatch) (*Team, *Response, error) { + buf, err := json.Marshal(patch) + if err != nil { + return nil, nil, NewAppError("PatchTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.teamRoute(teamId)+"/patch", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var t Team + if jsonErr := json.NewDecoder(r.Body).Decode(&t); jsonErr != nil { + return nil, nil, NewAppError("PatchTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &t, BuildResponse(r), nil +} + +// RestoreTeam restores a previously deleted team. +func (c *Client4) RestoreTeam(teamId string) (*Team, *Response, error) { + r, err := c.DoAPIPost(c.teamRoute(teamId)+"/restore", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var t Team + if jsonErr := json.NewDecoder(r.Body).Decode(&t); jsonErr != nil { + return nil, nil, NewAppError("RestoreTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &t, BuildResponse(r), nil +} + +// RegenerateTeamInviteId requests a new invite ID to be generated. +func (c *Client4) RegenerateTeamInviteId(teamId string) (*Team, *Response, error) { + r, err := c.DoAPIPost(c.teamRoute(teamId)+"/regenerate_invite_id", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var t Team + if jsonErr := json.NewDecoder(r.Body).Decode(&t); jsonErr != nil { + return nil, nil, NewAppError("RegenerateTeamInviteId", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &t, BuildResponse(r), nil +} + +// SoftDeleteTeam deletes the team softly (archive only, not permanent delete). +func (c *Client4) SoftDeleteTeam(teamId string) (*Response, error) { + r, err := c.DoAPIDelete(c.teamRoute(teamId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// PermanentDeleteTeam deletes the team, should only be used when needed for +// compliance and the like. +func (c *Client4) PermanentDeleteTeam(teamId string) (*Response, error) { + r, err := c.DoAPIDelete(c.teamRoute(teamId) + "?permanent=" + c.boolString(true)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UpdateTeamPrivacy modifies the team type (model.TeamOpen <--> model.TeamInvite) and sets +// the corresponding AllowOpenInvite appropriately. +func (c *Client4) UpdateTeamPrivacy(teamId string, privacy string) (*Team, *Response, error) { + requestBody := map[string]string{"privacy": privacy} + r, err := c.DoAPIPut(c.teamRoute(teamId)+"/privacy", MapToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var t Team + if jsonErr := json.NewDecoder(r.Body).Decode(&t); jsonErr != nil { + return nil, nil, NewAppError("UpdateTeamPrivacy", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &t, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.teamMembersRoute(teamId)+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tms []*TeamMember + if r.StatusCode == http.StatusNotModified { + return tms, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&tms); jsonErr != nil { + return nil, nil, NewAppError("GetTeamMembers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return tms, BuildResponse(r), nil +} + +// 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, excludeDeletedUsers bool, etag string) ([]*TeamMember, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&sort=%v&exclude_deleted_users=%v", page, perPage, sort, excludeDeletedUsers) + r, err := c.DoAPIGet(c.teamMembersRoute(teamId)+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tms []*TeamMember + if r.StatusCode == http.StatusNotModified { + return tms, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&tms); jsonErr != nil { + return nil, nil, NewAppError("GetTeamMembersSortAndWithoutDeletedUsers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return tms, BuildResponse(r), nil +} + +// GetTeamMembersForUser returns the team members for a user. +func (c *Client4) GetTeamMembersForUser(userId string, etag string) ([]*TeamMember, *Response, error) { + r, err := c.DoAPIGet(c.userRoute(userId)+"/teams/members", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tms []*TeamMember + if r.StatusCode == http.StatusNotModified { + return tms, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&tms); jsonErr != nil { + return nil, nil, NewAppError("GetTeamMembersForUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return tms, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIPost(fmt.Sprintf("/teams/%v/members/ids", teamId), ArrayToJSON(userIds)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tms []*TeamMember + if jsonErr := json.NewDecoder(r.Body).Decode(&tms); jsonErr != nil { + return nil, nil, NewAppError("GetTeamMembersByIds", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return tms, BuildResponse(r), nil +} + +// AddTeamMember adds user to a team and return a team member. +func (c *Client4) AddTeamMember(teamId, userId string) (*TeamMember, *Response, error) { + member := &TeamMember{TeamId: teamId, UserId: userId} + buf, err := json.Marshal(member) + if err != nil { + return nil, nil, NewAppError("AddTeamMember", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.teamMembersRoute(teamId), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tm TeamMember + if jsonErr := json.NewDecoder(r.Body).Decode(&tm); jsonErr != nil { + return nil, nil, NewAppError("AddTeamMember", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &tm, BuildResponse(r), nil +} + +// 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, error) { + 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.teamsRoute()+"/members/invite"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tm TeamMember + if jsonErr := json.NewDecoder(r.Body).Decode(&tm); jsonErr != nil { + return nil, nil, NewAppError("AddTeamMemberFromInvite", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &tm, BuildResponse(r), nil +} + +// AddTeamMembers adds a number of users to a team and returns the team members. +func (c *Client4) AddTeamMembers(teamId string, userIds []string) ([]*TeamMember, *Response, error) { + var members []*TeamMember + for _, userId := range userIds { + member := &TeamMember{TeamId: teamId, UserId: userId} + members = append(members, member) + } + js, jsonErr := json.Marshal(members) + if jsonErr != nil { + return nil, nil, NewAppError("AddTeamMembers", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.teamMembersRoute(teamId)+"/batch", string(js)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tms []*TeamMember + if jsonErr := json.NewDecoder(r.Body).Decode(&tms); jsonErr != nil { + return nil, nil, NewAppError("AddTeamMembers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return tms, BuildResponse(r), nil +} + +// AddTeamMembers adds a number of users to a team and returns the team members. +func (c *Client4) AddTeamMembersGracefully(teamId string, userIds []string) ([]*TeamMemberWithError, *Response, error) { + var members []*TeamMember + for _, userId := range userIds { + member := &TeamMember{TeamId: teamId, UserId: userId} + members = append(members, member) + } + js, jsonErr := json.Marshal(members) + if jsonErr != nil { + return nil, nil, NewAppError("AddTeamMembersGracefully", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + + r, err := c.DoAPIPost(c.teamMembersRoute(teamId)+"/batch?graceful="+c.boolString(true), string(js)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tms []*TeamMemberWithError + if jsonErr := json.NewDecoder(r.Body).Decode(&tms); jsonErr != nil { + return nil, nil, NewAppError("AddTeamMembersGracefully", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return tms, BuildResponse(r), nil +} + +// RemoveTeamMember will remove a user from a team. +func (c *Client4) RemoveTeamMember(teamId, userId string) (*Response, error) { + r, err := c.DoAPIDelete(c.teamMemberRoute(teamId, userId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetTeamStats returns a team stats based on the team id string. +// Must be authenticated. +func (c *Client4) GetTeamStats(teamId, etag string) (*TeamStats, *Response, error) { + r, err := c.DoAPIGet(c.teamStatsRoute(teamId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var ts TeamStats + if jsonErr := json.NewDecoder(r.Body).Decode(&ts); jsonErr != nil { + return nil, nil, NewAppError("GetTeamStats", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &ts, BuildResponse(r), nil +} + +// GetTotalUsersStats returns a total system user stats. +// Must be authenticated. +func (c *Client4) GetTotalUsersStats(etag string) (*UsersStats, *Response, error) { + r, err := c.DoAPIGet(c.totalUsersStatsRoute(), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var stats UsersStats + if jsonErr := json.NewDecoder(r.Body).Decode(&stats); jsonErr != nil { + return nil, nil, NewAppError("GetTotalUsersStats", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &stats, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.userRoute(userId)+c.teamRoute(teamId)+"/unread", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tu TeamUnread + if jsonErr := json.NewDecoder(r.Body).Decode(&tu); jsonErr != nil { + return nil, nil, NewAppError("GetTeamUnread", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &tu, BuildResponse(r), nil +} + +// 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, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("file", filename) + if err != nil { + return nil, nil, err + } + + if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return nil, nil, err + } + + part, err = writer.CreateFormField("filesize") + if err != nil { + return nil, nil, err + } + + if _, err = io.Copy(part, strings.NewReader(strconv.Itoa(filesize))); err != nil { + return nil, nil, err + } + + part, err = writer.CreateFormField("importFrom") + if err != nil { + return nil, nil, err + } + + if _, err := io.Copy(part, strings.NewReader(importFrom)); err != nil { + return nil, nil, err + } + + if err := writer.Close(); err != nil { + return nil, nil, err + } + + return c.DoUploadImportTeam(c.teamImportRoute(teamId), body.Bytes(), writer.FormDataContentType()) +} + +// InviteUsersToTeam invite users by email to the team. +func (c *Client4) InviteUsersToTeam(teamId string, userEmails []string) (*Response, error) { + r, err := c.DoAPIPost(c.teamRoute(teamId)+"/invite/email", ArrayToJSON(userEmails)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// InviteGuestsToTeam invite guest by email to some channels in a team. +func (c *Client4) InviteGuestsToTeam(teamId string, userEmails []string, channels []string, message string) (*Response, error) { + guestsInvite := GuestsInvite{ + Emails: userEmails, + Channels: channels, + Message: message, + } + buf, err := json.Marshal(guestsInvite) + if err != nil { + return nil, NewAppError("InviteGuestsToTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.teamRoute(teamId)+"/invite-guests/email", buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// InviteUsersToTeam invite users by email to the team. +func (c *Client4) InviteUsersToTeamGracefully(teamId string, userEmails []string) ([]*EmailInviteWithError, *Response, error) { + r, err := c.DoAPIPost(c.teamRoute(teamId)+"/invite/email?graceful="+c.boolString(true), ArrayToJSON(userEmails)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*EmailInviteWithError + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("InviteUsersToTeamGracefully", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + guestsInvite := GuestsInvite{ + Emails: userEmails, + Channels: channels, + Message: message, + } + buf, err := json.Marshal(guestsInvite) + if err != nil { + return nil, nil, NewAppError("InviteGuestsToTeamGracefully", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.teamRoute(teamId)+"/invite-guests/email?graceful="+c.boolString(true), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*EmailInviteWithError + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("InviteGuestsToTeamGracefully", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// InvalidateEmailInvites will invalidate active email invitations that have not been accepted by the user. +func (c *Client4) InvalidateEmailInvites() (*Response, error) { + r, err := c.DoAPIDelete(c.teamsRoute() + "/invites/email") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetTeamInviteInfo returns a team object from an invite id containing sanitized information. +func (c *Client4) GetTeamInviteInfo(inviteId string) (*Team, *Response, error) { + r, err := c.DoAPIGet(c.teamsRoute()+"/invite/"+inviteId, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var t Team + if jsonErr := json.NewDecoder(r.Body).Decode(&t); jsonErr != nil { + return nil, nil, NewAppError("GetTeamInviteInfo", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &t, BuildResponse(r), nil +} + +// SetTeamIcon sets team icon of the team. +func (c *Client4) SetTeamIcon(teamId string, data []byte) (*Response, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("image", "teamIcon.png") + if err != nil { + return nil, 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 nil, NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if err = writer.Close(); err != nil { + return nil, NewAppError("SetTeamIcon", "model.client.set_team_icon.writer.app_error", nil, err.Error(), http.StatusBadRequest) + } + + rq, err := http.NewRequest("POST", c.APIURL+c.teamRoute(teamId)+"/image", bytes.NewReader(body.Bytes())) + if err != nil { + return nil, err + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + return BuildResponse(rp), nil +} + +// GetTeamIcon gets the team icon of the team. +func (c *Client4) GetTeamIcon(teamId, etag string) ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.teamRoute(teamId)+"/image", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetTeamIcon", "model.client.get_team_icon.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// RemoveTeamIcon updates LastTeamIconUpdate to 0 which indicates team icon is removed. +func (c *Client4) RemoveTeamIcon(teamId string) (*Response, error) { + r, err := c.DoAPIDelete(c.teamRoute(teamId) + "/image") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// Channel Section + +// GetAllChannels get all the channels. Must be a system administrator. +func (c *Client4) GetAllChannels(page int, perPage int, etag string) (ChannelListWithTeamData, *Response, error) { + return c.getAllChannels(page, perPage, etag, ChannelSearchOpts{}) +} + +// GetAllChannelsIncludeDeleted get all the channels. Must be a system administrator. +func (c *Client4) GetAllChannelsIncludeDeleted(page int, perPage int, etag string) (ChannelListWithTeamData, *Response, error) { + return c.getAllChannels(page, perPage, etag, ChannelSearchOpts{IncludeDeleted: true}) +} + +// GetAllChannelsExcludePolicyConstrained gets all channels which are not part of a data retention policy. +// Must be a system administrator. +func (c *Client4) GetAllChannelsExcludePolicyConstrained(page, perPage int, etag string) (ChannelListWithTeamData, *Response, error) { + return c.getAllChannels(page, perPage, etag, ChannelSearchOpts{ExcludePolicyConstrained: true}) +} + +func (c *Client4) getAllChannels(page int, perPage int, etag string, opts ChannelSearchOpts) (ChannelListWithTeamData, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&include_deleted=%v&exclude_policy_constrained=%v", + page, perPage, opts.IncludeDeleted, opts.ExcludePolicyConstrained) + r, err := c.DoAPIGet(c.channelsRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch ChannelListWithTeamData + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("getAllChannels", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&include_total_count="+c.boolString(true), page, perPage) + r, err := c.DoAPIGet(c.channelsRoute()+query, etag) + if err != nil { + return nil, 0, BuildResponse(r), err + } + defer closeBody(r) + + var cwc *ChannelsWithCount + err = json.NewDecoder(r.Body).Decode(&cwc) + if err != nil { + return nil, 0, BuildResponse(r), NewAppError("GetAllChannelsWithCount", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return cwc.Channels, cwc.TotalCount, BuildResponse(r), nil +} + +// CreateChannel creates a channel based on the provided channel struct. +func (c *Client4) CreateChannel(channel *Channel) (*Channel, *Response, error) { + channelJSON, jsonErr := json.Marshal(channel) + if jsonErr != nil { + return nil, nil, NewAppError("CreateChannel", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.channelsRoute(), string(channelJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("CreateChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// UpdateChannel updates a channel based on the provided channel struct. +func (c *Client4) UpdateChannel(channel *Channel) (*Channel, *Response, error) { + channelJSON, jsonErr := json.Marshal(channel) + if jsonErr != nil { + return nil, nil, NewAppError("UpdateChannel", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPut(c.channelRoute(channel.Id), string(channelJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("UpdateChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// PatchChannel partially updates a channel. Any missing fields are not updated. +func (c *Client4) PatchChannel(channelId string, patch *ChannelPatch) (*Channel, *Response, error) { + buf, err := json.Marshal(patch) + if err != nil { + return nil, nil, NewAppError("PatchChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.channelRoute(channelId)+"/patch", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("PatchChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// UpdateChannelPrivacy updates channel privacy +func (c *Client4) UpdateChannelPrivacy(channelId string, privacy ChannelType) (*Channel, *Response, error) { + requestBody := map[string]string{"privacy": string(privacy)} + r, err := c.DoAPIPut(c.channelRoute(channelId)+"/privacy", MapToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("UpdateChannelPrivacy", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// RestoreChannel restores a previously deleted channel. Any missing fields are not updated. +func (c *Client4) RestoreChannel(channelId string) (*Channel, *Response, error) { + r, err := c.DoAPIPost(c.channelRoute(channelId)+"/restore", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("RestoreChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// CreateDirectChannel creates a direct message channel based on the two user +// ids provided. +func (c *Client4) CreateDirectChannel(userId1, userId2 string) (*Channel, *Response, error) { + requestBody := []string{userId1, userId2} + r, err := c.DoAPIPost(c.channelsRoute()+"/direct", ArrayToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("CreateDirectChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// CreateGroupChannel creates a group message channel based on userIds provided. +func (c *Client4) CreateGroupChannel(userIds []string) (*Channel, *Response, error) { + r, err := c.DoAPIPost(c.channelsRoute()+"/group", ArrayToJSON(userIds)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("CreateGroupChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// GetChannel returns a channel based on the provided channel id string. +func (c *Client4) GetChannel(channelId, etag string) (*Channel, *Response, error) { + r, err := c.DoAPIGet(c.channelRoute(channelId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// GetChannelStats returns statistics for a channel. +func (c *Client4) GetChannelStats(channelId string, etag string) (*ChannelStats, *Response, error) { + r, err := c.DoAPIGet(c.channelRoute(channelId)+"/stats", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var stats ChannelStats + if jsonErr := json.NewDecoder(r.Body).Decode(&stats); jsonErr != nil { + return nil, nil, NewAppError("GetChannelStats", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &stats, BuildResponse(r), nil +} + +// GetChannelMembersTimezones gets a list of timezones for a channel. +func (c *Client4) GetChannelMembersTimezones(channelId string) ([]string, *Response, error) { + r, err := c.DoAPIGet(c.channelRoute(channelId)+"/timezones", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return ArrayFromJSON(r.Body), BuildResponse(r), nil +} + +// GetPinnedPosts gets a list of pinned posts. +func (c *Client4) GetPinnedPosts(channelId string, etag string) (*PostList, *Response, error) { + r, err := c.DoAPIGet(c.channelRoute(channelId)+"/pinned", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetPinnedPosts", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// GetPrivateChannelsForTeam returns a list of private channels based on the provided team id string. +func (c *Client4) GetPrivateChannelsForTeam(teamId string, page int, perPage int, etag string) ([]*Channel, *Response, error) { + query := fmt.Sprintf("/private?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetPrivateChannelsForTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetPublicChannelsForTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("/deleted?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetDeletedChannelsForTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// GetPublicChannelsByIdsForTeam returns a list of public channels based on provided team id string. +func (c *Client4) GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) ([]*Channel, *Response, error) { + r, err := c.DoAPIPost(c.channelsForTeamRoute(teamId)+"/ids", ArrayToJSON(channelIds)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetPublicChannelsByIdsForTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.channelsForTeamForUserRoute(teamId, userId, includeDeleted), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelsForTeamForUser", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// GetChannelsForTeamAndUserWithLastDeleteAt returns a list channels of a team for a user, additionally filtered with lastDeleteAt. This does not have any effect if includeDeleted is set to false. +func (c *Client4) GetChannelsForTeamAndUserWithLastDeleteAt(teamId, userId string, includeDeleted bool, lastDeleteAt int, etag string) ([]*Channel, *Response, error) { + route := fmt.Sprintf(c.userRoute(userId) + c.teamRoute(teamId) + "/channels") + route += fmt.Sprintf("?include_deleted=%v&last_delete_at=%d", includeDeleted, lastDeleteAt) + r, err := c.DoAPIGet(route, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelsForTeamAndUserWithLastDeleteAt", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// SearchChannels returns the channels on a team matching the provided search term. +func (c *Client4) SearchChannels(teamId string, search *ChannelSearch) ([]*Channel, *Response, error) { + searchJSON, jsonErr := json.Marshal(search) + if jsonErr != nil { + return nil, nil, NewAppError("SearchChannels", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.channelsForTeamRoute(teamId)+"/search", string(searchJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("SearchChannels", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// SearchArchivedChannels returns the archived channels on a team matching the provided search term. +func (c *Client4) SearchArchivedChannels(teamId string, search *ChannelSearch) ([]*Channel, *Response, error) { + searchJSON, jsonErr := json.Marshal(search) + if jsonErr != nil { + return nil, nil, NewAppError("SearchArchivedChannels", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.channelsForTeamRoute(teamId)+"/search_archived", string(searchJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("SearchArchivedChannels", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// SearchAllChannels search in all the channels. Must be a system administrator. +func (c *Client4) SearchAllChannels(search *ChannelSearch) (ChannelListWithTeamData, *Response, error) { + searchJSON, jsonErr := json.Marshal(search) + if jsonErr != nil { + return nil, nil, NewAppError("SearchAllChannels", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.channelsRoute()+"/search", string(searchJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch ChannelListWithTeamData + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("SearchAllChannels", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// SearchAllChannelsPaged searches all the channels and returns the results paged with the total count. +func (c *Client4) SearchAllChannelsPaged(search *ChannelSearch) (*ChannelsWithCount, *Response, error) { + searchJSON, jsonErr := json.Marshal(search) + if jsonErr != nil { + return nil, nil, NewAppError("SearchAllChannelsPaged", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.channelsRoute()+"/search", string(searchJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var cwc *ChannelsWithCount + err = json.NewDecoder(r.Body).Decode(&cwc) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetAllChannelsWithCount", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return cwc, BuildResponse(r), nil +} + +// SearchGroupChannels returns the group channels of the user whose members' usernames match the search term. +func (c *Client4) SearchGroupChannels(search *ChannelSearch) ([]*Channel, *Response, error) { + searchJSON, jsonErr := json.Marshal(search) + if jsonErr != nil { + return nil, nil, NewAppError("SearchGroupChannels", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.channelsRoute()+"/group/search", string(searchJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("SearchGroupChannels", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// DeleteChannel deletes channel based on the provided channel id string. +func (c *Client4) DeleteChannel(channelId string) (*Response, error) { + r, err := c.DoAPIDelete(c.channelRoute(channelId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// PermanentDeleteChannel deletes a channel based on the provided channel id string. +func (c *Client4) PermanentDeleteChannel(channelId string) (*Response, error) { + r, err := c.DoAPIDelete(c.channelRoute(channelId) + "?permanent=" + c.boolString(true)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// MoveChannel moves the channel to the destination team. +func (c *Client4) MoveChannel(channelId, teamId string, force bool) (*Channel, *Response, error) { + requestBody := map[string]interface{}{ + "team_id": teamId, + "force": force, + } + r, err := c.DoAPIPost(c.channelRoute(channelId)+"/move", StringInterfaceToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("MoveChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.channelByNameRoute(channelName, teamId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelByName", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.channelByNameRoute(channelName, teamId)+"?include_deleted="+c.boolString(true), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelByNameIncludeDeleted", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.channelByNameForTeamNameRoute(channelName, teamName), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelByNameForTeamName", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.channelByNameForTeamNameRoute(channelName, teamName)+"?include_deleted="+c.boolString(true), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *Channel + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelByNameForTeamNameIncludeDeleted", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// GetChannelMembers gets a page of channel members. +func (c *Client4) GetChannelMembers(channelId string, page, perPage int, etag string) (ChannelMembers, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.channelMembersRoute(channelId)+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch ChannelMembers + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelMembers", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// GetChannelMembersByIds gets the channel members in a channel for a list of user ids. +func (c *Client4) GetChannelMembersByIds(channelId string, userIds []string) (ChannelMembers, *Response, error) { + r, err := c.DoAPIPost(c.channelMembersRoute(channelId)+"/ids", ArrayToJSON(userIds)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch ChannelMembers + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelMembersByIds", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// GetChannelMember gets a channel member. +func (c *Client4) GetChannelMember(channelId, userId, etag string) (*ChannelMember, *Response, error) { + r, err := c.DoAPIGet(c.channelMemberRoute(channelId, userId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *ChannelMember + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelMember", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// GetChannelMembersForUser gets all the channel members for a user on a team. +func (c *Client4) GetChannelMembersForUser(userId, teamId, etag string) (ChannelMembers, *Response, error) { + r, err := c.DoAPIGet(fmt.Sprintf(c.userRoute(userId)+"/teams/%v/channels/members", teamId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch ChannelMembers + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelMembersForUser", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + url := fmt.Sprintf(c.channelsRoute()+"/members/%v/view", userId) + buf, err := json.Marshal(view) + if err != nil { + return nil, nil, NewAppError("ViewChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(url, buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *ChannelViewResponse + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("ViewChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.userRoute(userId)+c.channelRoute(channelId)+"/unread", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *ChannelUnread + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelUnread", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// UpdateChannelRoles will update the roles on a channel for a user. +func (c *Client4) UpdateChannelRoles(channelId, userId, roles string) (*Response, error) { + requestBody := map[string]string{"roles": roles} + r, err := c.DoAPIPut(c.channelMemberRoute(channelId, userId)+"/roles", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UpdateChannelMemberSchemeRoles will update the scheme-derived roles on a channel for a user. +func (c *Client4) UpdateChannelMemberSchemeRoles(channelId string, userId string, schemeRoles *SchemeRoles) (*Response, error) { + buf, err := json.Marshal(schemeRoles) + if err != nil { + return nil, NewAppError("UpdateChannelMemberSchemeRoles", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.channelMemberRoute(channelId, userId)+"/schemeRoles", buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UpdateChannelNotifyProps will update the notification properties on a channel for a user. +func (c *Client4) UpdateChannelNotifyProps(channelId, userId string, props map[string]string) (*Response, error) { + r, err := c.DoAPIPut(c.channelMemberRoute(channelId, userId)+"/notify_props", MapToJSON(props)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// AddChannelMember adds user to channel and return a channel member. +func (c *Client4) AddChannelMember(channelId, userId string) (*ChannelMember, *Response, error) { + requestBody := map[string]string{"user_id": userId} + r, err := c.DoAPIPost(c.channelMembersRoute(channelId)+"", MapToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *ChannelMember + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("AddChannelMember", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// 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, error) { + requestBody := map[string]string{"user_id": userId, "post_root_id": postRootId} + r, err := c.DoAPIPost(c.channelMembersRoute(channelId)+"", MapToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch *ChannelMember + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("AddChannelMemberWithRootId", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// RemoveUserFromChannel will delete the channel member object for a user, effectively removing the user from a channel. +func (c *Client4) RemoveUserFromChannel(channelId, userId string) (*Response, error) { + r, err := c.DoAPIDelete(c.channelMemberRoute(channelId, userId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// AutocompleteChannelsForTeam will return an ordered list of channels autocomplete suggestions. +func (c *Client4) AutocompleteChannelsForTeam(teamId, name string) (ChannelList, *Response, error) { + query := fmt.Sprintf("?name=%v", name) + r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+"/autocomplete"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch ChannelList + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("AutocompleteChannelsForTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// AutocompleteChannelsForTeamForSearch will return an ordered list of your channels autocomplete suggestions. +func (c *Client4) AutocompleteChannelsForTeamForSearch(teamId, name string) (ChannelList, *Response, error) { + query := fmt.Sprintf("?name=%v", name) + r, err := c.DoAPIGet(c.channelsForTeamRoute(teamId)+"/search_autocomplete"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch ChannelList + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("AutocompleteChannelsForTeamForSearch", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// Post Section + +// CreatePost creates a post based on the provided post struct. +func (c *Client4) CreatePost(post *Post) (*Post, *Response, error) { + postJSON, jsonErr := json.Marshal(post) + if jsonErr != nil { + return nil, nil, NewAppError("CreatePost", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.postsRoute(), string(postJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var p Post + if r.StatusCode == http.StatusNotModified { + return &p, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("CreatePost", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +// 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, error) { + postJSON, jsonErr := json.Marshal(post) + if jsonErr != nil { + return nil, nil, NewAppError("CreatePostEphemeral", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.postsEphemeralRoute(), string(postJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var p Post + if r.StatusCode == http.StatusNotModified { + return &p, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("CreatePostEphemeral", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +// UpdatePost updates a post based on the provided post struct. +func (c *Client4) UpdatePost(postId string, post *Post) (*Post, *Response, error) { + postJSON, jsonErr := json.Marshal(post) + if jsonErr != nil { + return nil, nil, NewAppError("UpdatePost", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPut(c.postRoute(postId), string(postJSON)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var p Post + if r.StatusCode == http.StatusNotModified { + return &p, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("UpdatePost", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +// PatchPost partially updates a post. Any missing fields are not updated. +func (c *Client4) PatchPost(postId string, patch *PostPatch) (*Post, *Response, error) { + buf, err := json.Marshal(patch) + if err != nil { + return nil, nil, NewAppError("PatchPost", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.postRoute(postId)+"/patch", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var p Post + if r.StatusCode == http.StatusNotModified { + return &p, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("PatchPost", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +// SetPostUnread marks channel where post belongs as unread on the time of the provided post. +func (c *Client4) SetPostUnread(userId string, postId string, collapsedThreadsSupported bool) (*Response, error) { + b, err := json.Marshal(map[string]bool{"collapsed_threads_supported": collapsedThreadsSupported}) + if err != nil { + return nil, NewAppError("SetPostUnread", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.userRoute(userId)+c.postRoute(postId)+"/set_unread", b) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// PinPost pin a post based on provided post id string. +func (c *Client4) PinPost(postId string) (*Response, error) { + r, err := c.DoAPIPost(c.postRoute(postId)+"/pin", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UnpinPost unpin a post based on provided post id string. +func (c *Client4) UnpinPost(postId string) (*Response, error) { + r, err := c.DoAPIPost(c.postRoute(postId)+"/unpin", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetPost gets a single post. +func (c *Client4) GetPost(postId string, etag string) (*Post, *Response, error) { + r, err := c.DoAPIGet(c.postRoute(postId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var post Post + if r.StatusCode == http.StatusNotModified { + return &post, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil { + return nil, nil, NewAppError("GetPost", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &post, BuildResponse(r), nil +} + +// DeletePost deletes a post from the provided post id string. +func (c *Client4) DeletePost(postId string) (*Response, error) { + r, err := c.DoAPIDelete(c.postRoute(postId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetPostThread gets a post with all the other posts in the same thread. +func (c *Client4) GetPostThread(postId string, etag string, collapsedThreads bool) (*PostList, *Response, error) { + url := c.postRoute(postId) + "/thread" + if collapsedThreads { + url += "?collapsedThreads=true" + } + r, err := c.DoAPIGet(url, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetPostThread", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// 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, collapsedThreads bool) (*PostList, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + if collapsedThreads { + query += "&collapsedThreads=true" + } + r, err := c.DoAPIGet(c.channelRoute(channelId)+"/posts"+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetPostsForChannel", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// GetFlaggedPostsForUser returns flagged posts of a user based on user id string. +func (c *Client4) GetFlaggedPostsForUser(userId string, page int, perPage int) (*PostList, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.userRoute(userId)+"/posts/flagged"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetFlaggedPostsForUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// 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, error) { + if !IsValidId(teamId) { + return nil, nil, 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.userRoute(userId)+"/posts/flagged"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetFlaggedPostsForUserInTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// 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, error) { + if !IsValidId(channelId) { + return nil, nil, 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.userRoute(userId)+"/posts/flagged"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetFlaggedPostsForUserInChannel", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// GetPostsSince gets posts created after a specified time as Unix time in milliseconds. +func (c *Client4) GetPostsSince(channelId string, time int64, collapsedThreads bool) (*PostList, *Response, error) { + query := fmt.Sprintf("?since=%v", time) + if collapsedThreads { + query += "&collapsedThreads=true" + } + r, err := c.DoAPIGet(c.channelRoute(channelId)+"/posts"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetPostsSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// 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, collapsedThreads bool) (*PostList, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&after=%v", page, perPage, postId) + if collapsedThreads { + query += "&collapsedThreads=true" + } + r, err := c.DoAPIGet(c.channelRoute(channelId)+"/posts"+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetPostsAfter", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// 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, collapsedThreads bool) (*PostList, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&before=%v", page, perPage, postId) + if collapsedThreads { + query += "&collapsedThreads=true" + } + r, err := c.DoAPIGet(c.channelRoute(channelId)+"/posts"+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetPostsBefore", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// 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, collapsedThreads bool) (*PostList, *Response, error) { + query := fmt.Sprintf("?limit_before=%v&limit_after=%v", limitBefore, limitAfter) + if collapsedThreads { + query += "&collapsedThreads=true" + } + r, err := c.DoAPIGet(c.userRoute(userId)+c.channelRoute(channelId)+"/posts/unread"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetPostsAroundLastUnread", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// SearchFiles returns any posts with matching terms string. +func (c *Client4) SearchFiles(teamId string, terms string, isOrSearch bool) (*FileInfoList, *Response, error) { + params := SearchParameter{ + Terms: &terms, + IsOrSearch: &isOrSearch, + } + return c.SearchFilesWithParams(teamId, ¶ms) +} + +// SearchFilesWithParams returns any posts with matching terms string. +func (c *Client4) SearchFilesWithParams(teamId string, params *SearchParameter) (*FileInfoList, *Response, error) { + js, jsonErr := json.Marshal(params) + if jsonErr != nil { + return nil, nil, NewAppError("SearchFilesWithParams", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.teamRoute(teamId)+"/files/search", string(js)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var list FileInfoList + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("SearchFilesWithParams", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// SearchPosts returns any posts with matching terms string. +func (c *Client4) SearchPosts(teamId string, terms string, isOrSearch bool) (*PostList, *Response, error) { + 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, error) { + js, jsonErr := json.Marshal(params) + if jsonErr != nil { + return nil, nil, NewAppError("SearchFilesWithParams", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.teamRoute(teamId)+"/posts/search", string(js)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PostList + if r.StatusCode == http.StatusNotModified { + return &list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("SearchFilesWithParams", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &list, BuildResponse(r), nil +} + +// SearchPostsWithMatches returns any posts with matching terms string, including. +func (c *Client4) SearchPostsWithMatches(teamId string, terms string, isOrSearch bool) (*PostSearchResults, *Response, error) { + requestBody := map[string]interface{}{"terms": terms, "is_or_search": isOrSearch} + r, err := c.DoAPIPost(c.teamRoute(teamId)+"/posts/search", StringInterfaceToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var psr PostSearchResults + if jsonErr := json.NewDecoder(r.Body).Decode(&psr); jsonErr != nil { + return nil, nil, NewAppError("SearchPostsWithMatches", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &psr, BuildResponse(r), nil +} + +// DoPostAction performs a post action. +func (c *Client4) DoPostAction(postId, actionId string) (*Response, error) { + r, err := c.DoAPIPost(c.postRoute(postId)+"/actions/"+actionId, "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// DoPostActionWithCookie performs a post action with extra arguments +func (c *Client4) DoPostActionWithCookie(postId, actionId, selected, cookieStr string) (*Response, error) { + var body []byte + if selected != "" || cookieStr != "" { + body, _ = json.Marshal(DoPostActionRequest{ + SelectedOption: selected, + Cookie: cookieStr, + }) + } + r, err := c.DoAPIPost(c.postRoute(postId)+"/actions/"+actionId, string(body)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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) (*Response, error) { + b, _ := json.Marshal(request) + r, err := c.DoAPIPost("/actions/dialogs/open", string(b)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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, error) { + b, _ := json.Marshal(request) + r, err := c.DoAPIPost("/actions/dialogs/submit", string(b)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var resp SubmitDialogResponse + json.NewDecoder(r.Body).Decode(&resp) + return &resp, BuildResponse(r), nil +} + +// 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, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormField("channel_id") + if err != nil { + return nil, nil, err + } + + _, err = io.Copy(part, strings.NewReader(channelId)) + if err != nil { + return nil, nil, err + } + + part, err = writer.CreateFormFile("files", filename) + if err != nil { + return nil, nil, err + } + _, err = io.Copy(part, bytes.NewBuffer(data)) + if err != nil { + return nil, nil, err + } + + err = writer.Close() + if err != nil { + return nil, nil, err + } + + return c.DoUploadFile(c.filesRoute(), 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, error) { + return c.DoUploadFile(c.filesRoute()+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, error) { + r, err := c.DoAPIGet(c.fileRoute(fileId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetFile", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.fileRoute(fileId)+fmt.Sprintf("?download=%v", download), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("DownloadFile", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// GetFileThumbnail gets the bytes for a file by id. +func (c *Client4) GetFileThumbnail(fileId string) ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.fileRoute(fileId)+"/thumbnail", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetFileThumbnail", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.fileRoute(fileId)+fmt.Sprintf("/thumbnail?download=%v", download), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("DownloadFileThumbnail", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// GetFileLink gets the public link of a file by id. +func (c *Client4) GetFileLink(fileId string) (string, *Response, error) { + r, err := c.DoAPIGet(c.fileRoute(fileId)+"/link", "") + if err != nil { + return "", BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body)["link"], BuildResponse(r), nil +} + +// GetFilePreview gets the bytes for a file by id. +func (c *Client4) GetFilePreview(fileId string) ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.fileRoute(fileId)+"/preview", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetFilePreview", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// DownloadFilePreview gets the bytes for a file by id. +func (c *Client4) DownloadFilePreview(fileId string, download bool) ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.fileRoute(fileId)+fmt.Sprintf("/preview?download=%v", download), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("DownloadFilePreview", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// GetFileInfo gets all the file info objects. +func (c *Client4) GetFileInfo(fileId string) (*FileInfo, *Response, error) { + r, err := c.DoAPIGet(c.fileRoute(fileId)+"/info", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var fi FileInfo + if jsonErr := json.NewDecoder(r.Body).Decode(&fi); jsonErr != nil { + return nil, nil, NewAppError("GetFileInfo", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &fi, BuildResponse(r), nil +} + +// GetFileInfosForPost gets all the file info objects attached to a post. +func (c *Client4) GetFileInfosForPost(postId string, etag string) ([]*FileInfo, *Response, error) { + r, err := c.DoAPIGet(c.postRoute(postId)+"/files/info", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var list []*FileInfo + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetFileInfosForPost", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// General/System Section + +// GenerateSupportPacket downloads the generated support packet +func (c *Client4) GenerateSupportPacket() ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.systemRoute()+"/support_packet", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetFile", "model.client.read_job_result_file.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// GetPing will return ok if the running goRoutines are below the threshold and unhealthy for above. +func (c *Client4) GetPing() (string, *Response, error) { + r, err := c.DoAPIGet(c.systemRoute()+"/ping", "") + if r != nil && r.StatusCode == 500 { + defer r.Body.Close() + return StatusUnhealthy, BuildResponse(r), err + } + if err != nil { + return "", BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body)["status"], BuildResponse(r), nil +} + +// GetPingWithServerStatus will return ok if several basic server health checks +// all pass successfully. +func (c *Client4) GetPingWithServerStatus() (string, *Response, error) { + r, err := c.DoAPIGet(c.systemRoute()+"/ping?get_server_status="+c.boolString(true), "") + if r != nil && r.StatusCode == 500 { + defer r.Body.Close() + return StatusUnhealthy, BuildResponse(r), err + } + if err != nil { + return "", BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body)["status"], BuildResponse(r), nil +} + +// GetPingWithFullServerStatus will return the full status if several basic server +// health checks all pass successfully. +func (c *Client4) GetPingWithFullServerStatus() (map[string]string, *Response, error) { + r, err := c.DoAPIGet(c.systemRoute()+"/ping?get_server_status="+c.boolString(true), "") + if r != nil && r.StatusCode == 500 { + defer r.Body.Close() + return map[string]string{"status": StatusUnhealthy}, BuildResponse(r), err + } + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body), BuildResponse(r), nil +} + +// TestEmail will attempt to connect to the configured SMTP server. +func (c *Client4) TestEmail(config *Config) (*Response, error) { + buf, err := json.Marshal(config) + if err != nil { + return nil, NewAppError("TestEmail", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.testEmailRoute(), buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// TestSiteURL will test the validity of a site URL. +func (c *Client4) TestSiteURL(siteURL string) (*Response, error) { + requestBody := make(map[string]string) + requestBody["site_url"] = siteURL + r, err := c.DoAPIPost(c.testSiteURLRoute(), MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// TestS3Connection will attempt to connect to the AWS S3. +func (c *Client4) TestS3Connection(config *Config) (*Response, error) { + buf, err := json.Marshal(config) + if err != nil { + return nil, NewAppError("TestS3Connection", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.testS3Route(), buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetConfig will retrieve the server config with some sanitized items. +func (c *Client4) GetConfig() (*Config, *Response, error) { + r, err := c.DoAPIGet(c.configRoute(), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return ConfigFromJSON(r.Body), BuildResponse(r), nil +} + +// ReloadConfig will reload the server configuration. +func (c *Client4) ReloadConfig() (*Response, error) { + r, err := c.DoAPIPost(c.configRoute()+"/reload", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.configRoute()+"/client?format=old", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body), BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.configRoute()+"/environment", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return StringInterfaceFromJSON(r.Body), BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.licenseRoute()+"/client?format=old", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body), BuildResponse(r), nil +} + +// DatabaseRecycle will recycle the connections. Discard current connection and get new one. +func (c *Client4) DatabaseRecycle() (*Response, error) { + r, err := c.DoAPIPost(c.databaseRoute()+"/recycle", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// InvalidateCaches will purge the cache and can affect the performance while is cleaning. +func (c *Client4) InvalidateCaches() (*Response, error) { + r, err := c.DoAPIPost(c.cacheRoute()+"/invalidate", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UpdateConfig will update the server configuration. +func (c *Client4) UpdateConfig(config *Config) (*Config, *Response, error) { + buf, err := json.Marshal(config) + if err != nil { + return nil, nil, NewAppError("UpdateConfig", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.configRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return ConfigFromJSON(r.Body), BuildResponse(r), nil +} + +// MigrateConfig will migrate existing config to the new one. +func (c *Client4) MigrateConfig(from, to string) (*Response, error) { + m := make(map[string]string, 2) + m["from"] = from + m["to"] = to + r, err := c.DoAPIPost(c.configRoute()+"/migrate", MapToJSON(m)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UploadLicenseFile will add a license file to the system. +func (c *Client4) UploadLicenseFile(data []byte) (*Response, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("license", "test-license.mattermost-license") + if err != nil { + return nil, 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 nil, NewAppError("UploadLicenseFile", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if err = writer.Close(); err != nil { + return nil, NewAppError("UploadLicenseFile", "model.client.set_profile_user.writer.app_error", nil, err.Error(), http.StatusBadRequest) + } + + rq, err := http.NewRequest("POST", c.APIURL+c.licenseRoute(), bytes.NewReader(body.Bytes())) + if err != nil { + return nil, err + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + return BuildResponse(rp), nil +} + +// RemoveLicenseFile will remove the server license it exists. Note that this will +// disable all enterprise features. +func (c *Client4) RemoveLicenseFile() (*Response, error) { + r, err := c.DoAPIDelete(c.licenseRoute()) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?name=%v&team_id=%v", name, teamId) + r, err := c.DoAPIGet(c.analyticsRoute()+"/old"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var rows AnalyticsRows + err = json.NewDecoder(r.Body).Decode(&rows) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetAnalyticsOld", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return rows, BuildResponse(r), nil +} + +// Webhooks Section + +// CreateIncomingWebhook creates an incoming webhook for a channel. +func (c *Client4) CreateIncomingWebhook(hook *IncomingWebhook) (*IncomingWebhook, *Response, error) { + buf, err := json.Marshal(hook) + if err != nil { + return nil, nil, NewAppError("CreateIncomingWebhook", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.incomingWebhooksRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var iw IncomingWebhook + if jsonErr := json.NewDecoder(r.Body).Decode(&iw); jsonErr != nil { + return nil, nil, NewAppError("CreateIncomingWebhook", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &iw, BuildResponse(r), nil +} + +// UpdateIncomingWebhook updates an incoming webhook for a channel. +func (c *Client4) UpdateIncomingWebhook(hook *IncomingWebhook) (*IncomingWebhook, *Response, error) { + buf, err := json.Marshal(hook) + if err != nil { + return nil, nil, NewAppError("UpdateIncomingWebhook", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.incomingWebhookRoute(hook.Id), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var iw IncomingWebhook + if jsonErr := json.NewDecoder(r.Body).Decode(&iw); jsonErr != nil { + return nil, nil, NewAppError("UpdateIncomingWebhook", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &iw, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.incomingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var iwl []*IncomingWebhook + if r.StatusCode == http.StatusNotModified { + return iwl, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&iwl); jsonErr != nil { + return nil, nil, NewAppError("GetIncomingWebhooks", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return iwl, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId) + r, err := c.DoAPIGet(c.incomingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var iwl []*IncomingWebhook + if r.StatusCode == http.StatusNotModified { + return iwl, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&iwl); jsonErr != nil { + return nil, nil, NewAppError("GetIncomingWebhooksForTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return iwl, BuildResponse(r), nil +} + +// GetIncomingWebhook returns an Incoming webhook given the hook ID. +func (c *Client4) GetIncomingWebhook(hookID string, etag string) (*IncomingWebhook, *Response, error) { + r, err := c.DoAPIGet(c.incomingWebhookRoute(hookID), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var iw IncomingWebhook + if r.StatusCode == http.StatusNotModified { + return &iw, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&iw); jsonErr != nil { + return nil, nil, NewAppError("GetIncomingWebhook", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &iw, BuildResponse(r), nil +} + +// DeleteIncomingWebhook deletes and Incoming Webhook given the hook ID. +func (c *Client4) DeleteIncomingWebhook(hookID string) (*Response, error) { + r, err := c.DoAPIDelete(c.incomingWebhookRoute(hookID)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// CreateOutgoingWebhook creates an outgoing webhook for a team or channel. +func (c *Client4) CreateOutgoingWebhook(hook *OutgoingWebhook) (*OutgoingWebhook, *Response, error) { + buf, err := json.Marshal(hook) + if err != nil { + return nil, nil, NewAppError("CreateOutgoingWebhook", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.outgoingWebhooksRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var ow OutgoingWebhook + if jsonErr := json.NewDecoder(r.Body).Decode(&ow); jsonErr != nil { + return nil, nil, NewAppError("CreateOutgoingWebhook", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &ow, BuildResponse(r), nil +} + +// UpdateOutgoingWebhook creates an outgoing webhook for a team or channel. +func (c *Client4) UpdateOutgoingWebhook(hook *OutgoingWebhook) (*OutgoingWebhook, *Response, error) { + buf, err := json.Marshal(hook) + if err != nil { + return nil, nil, NewAppError("UpdateOutgoingWebhook", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.outgoingWebhookRoute(hook.Id), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var ow OutgoingWebhook + if jsonErr := json.NewDecoder(r.Body).Decode(&ow); jsonErr != nil { + return nil, nil, NewAppError("UpdateOutgoingWebhook", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &ow, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.outgoingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var owl []*OutgoingWebhook + if r.StatusCode == http.StatusNotModified { + return owl, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&owl); jsonErr != nil { + return nil, nil, NewAppError("GetOutgoingWebhooks", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return owl, BuildResponse(r), nil +} + +// GetOutgoingWebhook outgoing webhooks on the system requested by Hook Id. +func (c *Client4) GetOutgoingWebhook(hookId string) (*OutgoingWebhook, *Response, error) { + r, err := c.DoAPIGet(c.outgoingWebhookRoute(hookId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var ow OutgoingWebhook + if jsonErr := json.NewDecoder(r.Body).Decode(&ow); jsonErr != nil { + return nil, nil, NewAppError("GetOutgoingWebhook", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &ow, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&channel_id=%v", page, perPage, channelId) + r, err := c.DoAPIGet(c.outgoingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var owl []*OutgoingWebhook + if r.StatusCode == http.StatusNotModified { + return owl, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&owl); jsonErr != nil { + return nil, nil, NewAppError("GetOutgoingWebhooksForChannel", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return owl, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId) + r, err := c.DoAPIGet(c.outgoingWebhooksRoute()+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var owl []*OutgoingWebhook + if r.StatusCode == http.StatusNotModified { + return owl, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&owl); jsonErr != nil { + return nil, nil, NewAppError("GetOutgoingWebhooksForTeam", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return owl, BuildResponse(r), nil +} + +// RegenOutgoingHookToken regenerate the outgoing webhook token. +func (c *Client4) RegenOutgoingHookToken(hookId string) (*OutgoingWebhook, *Response, error) { + r, err := c.DoAPIPost(c.outgoingWebhookRoute(hookId)+"/regen_token", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var ow OutgoingWebhook + if jsonErr := json.NewDecoder(r.Body).Decode(&ow); jsonErr != nil { + return nil, nil, NewAppError("RegenOutgoingHookToken", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &ow, BuildResponse(r), nil +} + +// DeleteOutgoingWebhook delete the outgoing webhook on the system requested by Hook Id. +func (c *Client4) DeleteOutgoingWebhook(hookId string) (*Response, error) { + r, err := c.DoAPIDelete(c.outgoingWebhookRoute(hookId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// Preferences Section + +// GetPreferences returns the user's preferences. +func (c *Client4) GetPreferences(userId string) (Preferences, *Response, error) { + r, err := c.DoAPIGet(c.preferencesRoute(userId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var prefs Preferences + if jsonErr := json.NewDecoder(r.Body).Decode(&prefs); jsonErr != nil { + return nil, nil, NewAppError("GetPreferences", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return prefs, BuildResponse(r), nil +} + +// UpdatePreferences saves the user's preferences. +func (c *Client4) UpdatePreferences(userId string, preferences Preferences) (*Response, error) { + buf, err := json.Marshal(preferences) + if err != nil { + return nil, NewAppError("UpdatePreferences", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.preferencesRoute(userId), buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// DeletePreferences deletes the user's preferences. +func (c *Client4) DeletePreferences(userId string, preferences Preferences) (*Response, error) { + buf, err := json.Marshal(preferences) + if err != nil { + return nil, NewAppError("DeletePreferences", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.preferencesRoute(userId)+"/delete", buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetPreferencesByCategory returns the user's preferences from the provided category string. +func (c *Client4) GetPreferencesByCategory(userId string, category string) (Preferences, *Response, error) { + url := fmt.Sprintf(c.preferencesRoute(userId)+"/%s", category) + r, err := c.DoAPIGet(url, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var prefs Preferences + if jsonErr := json.NewDecoder(r.Body).Decode(&prefs); jsonErr != nil { + return nil, nil, NewAppError("GetPreferencesByCategory", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return prefs, BuildResponse(r), nil +} + +// 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, error) { + url := fmt.Sprintf(c.preferencesRoute(userId)+"/%s/name/%v", category, preferenceName) + r, err := c.DoAPIGet(url, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var pref Preference + if jsonErr := json.NewDecoder(r.Body).Decode(&pref); jsonErr != nil { + return nil, nil, NewAppError("GetPreferenceByCategoryAndName", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &pref, BuildResponse(r), nil +} + +// SAML Section + +// GetSamlMetadata returns metadata for the SAML configuration. +func (c *Client4) GetSamlMetadata() (string, *Response, error) { + r, err := c.DoAPIGet(c.samlRoute()+"/metadata", "") + if err != nil { + return "", BuildResponse(r), err + } + defer closeBody(r) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(r.Body) + if err != nil { + return "", BuildResponse(r), err + } + + return buf.String(), BuildResponse(r), nil +} + +func fileToMultipart(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) (*Response, error) { + body, writer, err := fileToMultipart(data, filename) + if err != nil { + return nil, NewAppError("UploadSamlIdpCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest) + } + + _, resp, err := c.DoUploadFile(c.samlRoute()+"/certificate/idp", body, writer.FormDataContentType()) + return resp, err +} + +// 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) (*Response, error) { + body, writer, err := fileToMultipart(data, filename) + if err != nil { + return nil, NewAppError("UploadSamlPublicCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest) + } + + _, resp, err := c.DoUploadFile(c.samlRoute()+"/certificate/public", body, writer.FormDataContentType()) + return resp, err +} + +// 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) (*Response, error) { + body, writer, err := fileToMultipart(data, filename) + if err != nil { + return nil, NewAppError("UploadSamlPrivateCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest) + } + + _, resp, err := c.DoUploadFile(c.samlRoute()+"/certificate/private", body, writer.FormDataContentType()) + return resp, err +} + +// DeleteSamlIdpCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML. +func (c *Client4) DeleteSamlIdpCertificate() (*Response, error) { + r, err := c.DoAPIDelete(c.samlRoute() + "/certificate/idp") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// DeleteSamlPublicCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML. +func (c *Client4) DeleteSamlPublicCertificate() (*Response, error) { + r, err := c.DoAPIDelete(c.samlRoute() + "/certificate/public") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// DeleteSamlPrivateCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML. +func (c *Client4) DeleteSamlPrivateCertificate() (*Response, error) { + r, err := c.DoAPIDelete(c.samlRoute() + "/certificate/private") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetSamlCertificateStatus returns metadata for the SAML configuration. +func (c *Client4) GetSamlCertificateStatus() (*SamlCertificateStatus, *Response, error) { + r, err := c.DoAPIGet(c.samlRoute()+"/certificate/status", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var status SamlCertificateStatus + if jsonErr := json.NewDecoder(r.Body).Decode(&status); jsonErr != nil { + return nil, nil, NewAppError("GetSamlCertificateStatus", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &status, BuildResponse(r), nil +} + +func (c *Client4) GetSamlMetadataFromIdp(samlMetadataURL string) (*SamlMetadataResponse, *Response, error) { + requestBody := make(map[string]string) + requestBody["saml_metadata_url"] = samlMetadataURL + r, err := c.DoAPIPost(c.samlRoute()+"/metadatafromidp", MapToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + + defer closeBody(r) + var resp SamlMetadataResponse + if jsonErr := json.NewDecoder(r.Body).Decode(&resp); jsonErr != nil { + return nil, nil, NewAppError("GetSamlMetadataFromIdp", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &resp, BuildResponse(r), nil +} + +// ResetSamlAuthDataToEmail resets the AuthData field of SAML users to their Email. +func (c *Client4) ResetSamlAuthDataToEmail(includeDeleted bool, dryRun bool, userIDs []string) (int64, *Response, error) { + params := map[string]interface{}{ + "include_deleted": includeDeleted, + "dry_run": dryRun, + "user_ids": userIDs, + } + b, _ := json.Marshal(params) + r, err := c.DoAPIPostBytes(c.samlRoute()+"/reset_auth_data", b) + if err != nil { + return 0, BuildResponse(r), err + } + defer closeBody(r) + respBody := map[string]int64{} + err = json.NewDecoder(r.Body).Decode(&respBody) + if err != nil { + return 0, BuildResponse(r), NewAppError("Api4.ResetSamlAuthDataToEmail", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return respBody["num_affected"], BuildResponse(r), nil +} + +// Compliance Section + +// CreateComplianceReport creates an incoming webhook for a channel. +func (c *Client4) CreateComplianceReport(report *Compliance) (*Compliance, *Response, error) { + buf, err := json.Marshal(report) + if err != nil { + return nil, nil, NewAppError("CreateComplianceReport", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.complianceReportsRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var comp Compliance + if jsonErr := json.NewDecoder(r.Body).Decode(&comp); jsonErr != nil { + return nil, nil, NewAppError("CreateComplianceReport", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &comp, BuildResponse(r), nil +} + +// GetComplianceReports returns list of compliance reports. +func (c *Client4) GetComplianceReports(page, perPage int) (Compliances, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.complianceReportsRoute()+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var comp Compliances + if jsonErr := json.NewDecoder(r.Body).Decode(&comp); jsonErr != nil { + return nil, nil, NewAppError("GetComplianceReports", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return comp, BuildResponse(r), nil +} + +// GetComplianceReport returns a compliance report. +func (c *Client4) GetComplianceReport(reportId string) (*Compliance, *Response, error) { + r, err := c.DoAPIGet(c.complianceReportRoute(reportId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var comp Compliance + if jsonErr := json.NewDecoder(r.Body).Decode(&comp); jsonErr != nil { + return nil, nil, NewAppError("GetComplianceReport", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &comp, BuildResponse(r), nil +} + +// DownloadComplianceReport returns a full compliance report as a file. +func (c *Client4) DownloadComplianceReport(reportId string) ([]byte, *Response, error) { + rq, err := http.NewRequest("GET", c.APIURL+c.complianceReportDownloadRoute(reportId), nil) + if err != nil { + return nil, nil, err + } + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, "BEARER "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return nil, BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + data, err := ioutil.ReadAll(rp.Body) + if err != nil { + return nil, BuildResponse(rp), NewAppError("DownloadComplianceReport", "model.client.read_file.app_error", nil, err.Error(), rp.StatusCode) + } + + return data, BuildResponse(rp), nil +} + +// Cluster Section + +// GetClusterStatus returns the status of all the configured cluster nodes. +func (c *Client4) GetClusterStatus() ([]*ClusterInfo, *Response, error) { + r, err := c.DoAPIGet(c.clusterRoute()+"/status", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*ClusterInfo + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetClusterStatus", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// LDAP Section + +// SyncLdap will force a sync with the configured LDAP server. +// If includeRemovedMembers is true, then group members who left or were removed from a +// synced team/channel will be re-joined; otherwise, they will be excluded. +func (c *Client4) SyncLdap(includeRemovedMembers bool) (*Response, error) { + reqBody, _ := json.Marshal(map[string]interface{}{ + "include_removed_members": includeRemovedMembers, + }) + r, err := c.DoAPIPostBytes(c.ldapRoute()+"/sync", reqBody) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// TestLdap will attempt to connect to the configured LDAP server and return OK if configured +// correctly. +func (c *Client4) TestLdap() (*Response, error) { + r, err := c.DoAPIPost(c.ldapRoute()+"/test", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetLdapGroups retrieves the immediate child groups of the given parent group. +func (c *Client4) GetLdapGroups() ([]*Group, *Response, error) { + path := fmt.Sprintf("%s/groups", c.ldapRoute()) + + r, err := c.DoAPIGet(path, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + responseData := struct { + Count int `json:"count"` + Groups []*Group `json:"groups"` + }{} + if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil { + return nil, BuildResponse(r), NewAppError("Api4.GetLdapGroups", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + for i := range responseData.Groups { + responseData.Groups[i].DisplayName = *responseData.Groups[i].Name + } + + return responseData.Groups, BuildResponse(r), nil +} + +// LinkLdapGroup creates or undeletes a Mattermost group and associates it to the given LDAP group DN. +func (c *Client4) LinkLdapGroup(dn string) (*Group, *Response, error) { + path := fmt.Sprintf("%s/groups/%s/link", c.ldapRoute(), dn) + + r, err := c.DoAPIPost(path, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var g Group + if jsonErr := json.NewDecoder(r.Body).Decode(&g); jsonErr != nil { + return nil, nil, NewAppError("LinkLdapGroup", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &g, BuildResponse(r), nil +} + +// UnlinkLdapGroup deletes the Mattermost group associated with the given LDAP group DN. +func (c *Client4) UnlinkLdapGroup(dn string) (*Group, *Response, error) { + path := fmt.Sprintf("%s/groups/%s/link", c.ldapRoute(), dn) + + r, err := c.DoAPIDelete(path) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var g Group + if jsonErr := json.NewDecoder(r.Body).Decode(&g); jsonErr != nil { + return nil, nil, NewAppError("UnlinkLdapGroup", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &g, BuildResponse(r), nil +} + +// MigrateIdLdap migrates the LDAP enabled users to given attribute +func (c *Client4) MigrateIdLdap(toAttribute string) (*Response, error) { + r, err := c.DoAPIPost(c.ldapRoute()+"/migrateid", MapToJSON(map[string]string{ + "toAttribute": toAttribute, + })) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetGroupsByChannel retrieves the Mattermost Groups associated with a given channel +func (c *Client4) GetGroupsByChannel(channelId string, opts GroupSearchOpts) ([]*GroupWithSchemeAdmin, int, *Response, error) { + path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v&filter_allow_reference=%v", c.channelRoute(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, err := c.DoAPIGet(path, "") + if err != nil { + return nil, 0, BuildResponse(r), err + } + 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 { + return nil, 0, BuildResponse(r), NewAppError("Api4.GetGroupsByChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return responseData.Groups, responseData.Count, BuildResponse(r), nil +} + +// GetGroupsByTeam retrieves the Mattermost Groups associated with a given team +func (c *Client4) GetGroupsByTeam(teamId string, opts GroupSearchOpts) ([]*GroupWithSchemeAdmin, int, *Response, error) { + path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v&filter_allow_reference=%v", c.teamRoute(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, err := c.DoAPIGet(path, "") + if err != nil { + return nil, 0, BuildResponse(r), err + } + 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 { + return nil, 0, BuildResponse(r), NewAppError("Api4.GetGroupsByTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return responseData.Groups, responseData.Count, BuildResponse(r), nil +} + +// GetGroupsAssociatedToChannelsByTeam retrieves the Mattermost Groups associated with channels in a given team +func (c *Client4) GetGroupsAssociatedToChannelsByTeam(teamId string, opts GroupSearchOpts) (map[string][]*GroupWithSchemeAdmin, *Response, error) { + path := fmt.Sprintf("%s/groups_by_channels?q=%v&filter_allow_reference=%v", c.teamRoute(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, err := c.DoAPIGet(path, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + responseData := struct { + GroupsAssociatedToChannels map[string][]*GroupWithSchemeAdmin `json:"groups"` + }{} + if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil { + return nil, BuildResponse(r), NewAppError("Api4.GetGroupsAssociatedToChannelsByTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return responseData.GroupsAssociatedToChannels, BuildResponse(r), nil +} + +// GetGroups retrieves Mattermost Groups +func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response, error) { + 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.groupsRoute(), + 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, err := c.DoAPIGet(path, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var list []*Group + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetGroups", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetGroupsByUserId retrieves Mattermost Groups for a user +func (c *Client4) GetGroupsByUserId(userId string) ([]*Group, *Response, error) { + path := fmt.Sprintf( + "%s/%v/groups", + c.usersRoute(), + userId, + ) + + r, err := c.DoAPIGet(path, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Group + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetGroupsByUserId", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +func (c *Client4) MigrateAuthToLdap(fromAuthService string, matchField string, force bool) (*Response, error) { + r, err := c.DoAPIPost(c.usersRoute()+"/migrate_auth/ldap", StringInterfaceToJSON(map[string]interface{}{ + "from": fromAuthService, + "force": force, + "match_field": matchField, + })) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +func (c *Client4) MigrateAuthToSaml(fromAuthService string, usersMap map[string]string, auto bool) (*Response, error) { + r, err := c.DoAPIPost(c.usersRoute()+"/migrate_auth/saml", StringInterfaceToJSON(map[string]interface{}{ + "from": fromAuthService, + "auto": auto, + "matches": usersMap, + })) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UploadLdapPublicCertificate will upload a public certificate for LDAP and set the config to use it. +func (c *Client4) UploadLdapPublicCertificate(data []byte) (*Response, error) { + body, writer, err := fileToMultipart(data, LdapPublicCertificateName) + if err != nil { + return nil, NewAppError("UploadLdapPublicCertificate", "model.client.upload_ldap_cert.app_error", nil, err.Error(), http.StatusBadRequest) + } + + _, resp, err := c.DoUploadFile(c.ldapRoute()+"/certificate/public", body, writer.FormDataContentType()) + return resp, err +} + +// UploadLdapPrivateCertificate will upload a private key for LDAP and set the config to use it. +func (c *Client4) UploadLdapPrivateCertificate(data []byte) (*Response, error) { + body, writer, err := fileToMultipart(data, LdapPrivateKeyName) + if err != nil { + return nil, NewAppError("UploadLdapPrivateCertificate", "model.client.upload_Ldap_cert.app_error", nil, err.Error(), http.StatusBadRequest) + } + + _, resp, err := c.DoUploadFile(c.ldapRoute()+"/certificate/private", body, writer.FormDataContentType()) + return resp, err +} + +// DeleteLdapPublicCertificate deletes the LDAP IDP certificate from the server and updates the config to not use it and disable LDAP. +func (c *Client4) DeleteLdapPublicCertificate() (*Response, error) { + r, err := c.DoAPIDelete(c.ldapRoute() + "/certificate/public") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// DeleteLDAPPrivateCertificate deletes the LDAP IDP certificate from the server and updates the config to not use it and disable LDAP. +func (c *Client4) DeleteLdapPrivateCertificate() (*Response, error) { + r, err := c.DoAPIDelete(c.ldapRoute() + "/certificate/private") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// Audits Section + +// GetAudits returns a list of audits for the whole system. +func (c *Client4) GetAudits(page int, perPage int, etag string) (Audits, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet("/audits"+query, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var audits Audits + err = json.NewDecoder(r.Body).Decode(&audits) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetAudits", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return audits, BuildResponse(r), nil +} + +// Brand Section + +// GetBrandImage retrieves the previously uploaded brand image. +func (c *Client4) GetBrandImage() ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.brandRoute()+"/image", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + if r.StatusCode >= 300 { + return nil, BuildResponse(r), AppErrorFromJSON(r.Body) + } + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetBrandImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + + return data, BuildResponse(r), nil +} + +// DeleteBrandImage deletes the brand image for the system. +func (c *Client4) DeleteBrandImage() (*Response, error) { + r, err := c.DoAPIDelete(c.brandRoute() + "/image") + if err != nil { + return BuildResponse(r), err + } + return BuildResponse(r), nil +} + +// UploadBrandImage sets the brand image for the system. +func (c *Client4) UploadBrandImage(data []byte) (*Response, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("image", "brand.png") + if err != nil { + return nil, 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 nil, NewAppError("UploadBrandImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if err = writer.Close(); err != nil { + return nil, NewAppError("UploadBrandImage", "model.client.set_profile_user.writer.app_error", nil, err.Error(), http.StatusBadRequest) + } + + rq, err := http.NewRequest("POST", c.APIURL+c.brandRoute()+"/image", bytes.NewReader(body.Bytes())) + if err != nil { + return nil, err + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + return BuildResponse(rp), nil +} + +// Logs Section + +// GetLogs page of logs as a string array. +func (c *Client4) GetLogs(page, perPage int) ([]string, *Response, error) { + query := fmt.Sprintf("?page=%v&logs_per_page=%v", page, perPage) + r, err := c.DoAPIGet("/logs"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return ArrayFromJSON(r.Body), BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIPost("/logs", MapToJSON(message)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body), BuildResponse(r), nil +} + +// 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, error) { + buf, err := json.Marshal(app) + if err != nil { + return nil, nil, NewAppError("CreateOAuthApp", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.oAuthAppsRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var oapp OAuthApp + if jsonErr := json.NewDecoder(r.Body).Decode(&oapp); jsonErr != nil { + return nil, nil, NewAppError("CreateOAuthApp", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &oapp, BuildResponse(r), nil +} + +// 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, error) { + buf, err := json.Marshal(app) + if err != nil { + return nil, nil, NewAppError("UpdateOAuthApp", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.oAuthAppRoute(app.Id), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var oapp OAuthApp + if jsonErr := json.NewDecoder(r.Body).Decode(&oapp); jsonErr != nil { + return nil, nil, NewAppError("UpdateOAuthApp", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &oapp, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.oAuthAppsRoute()+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*OAuthApp + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetOAuthApps", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.oAuthAppRoute(appId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var oapp OAuthApp + if jsonErr := json.NewDecoder(r.Body).Decode(&oapp); jsonErr != nil { + return nil, nil, NewAppError("GetOAuthApp", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &oapp, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.oAuthAppRoute(appId)+"/info", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var oapp OAuthApp + if jsonErr := json.NewDecoder(r.Body).Decode(&oapp); jsonErr != nil { + return nil, nil, NewAppError("GetOAuthAppInfo", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &oapp, BuildResponse(r), nil +} + +// DeleteOAuthApp deletes a registered OAuth 2.0 client application. +func (c *Client4) DeleteOAuthApp(appId string) (*Response, error) { + r, err := c.DoAPIDelete(c.oAuthAppRoute(appId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// RegenerateOAuthAppSecret regenerates the client secret for a registered OAuth 2.0 client application. +func (c *Client4) RegenerateOAuthAppSecret(appId string) (*OAuthApp, *Response, error) { + r, err := c.DoAPIPost(c.oAuthAppRoute(appId)+"/regen_secret", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var oapp OAuthApp + if jsonErr := json.NewDecoder(r.Body).Decode(&oapp); jsonErr != nil { + return nil, nil, NewAppError("RegenerateOAuthAppSecret", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &oapp, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.userRoute(userId)+"/oauth/apps/authorized"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*OAuthApp + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetAuthorizedOAuthAppsForUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + buf, err := json.Marshal(authRequest) + if err != nil { + return "", BuildResponse(nil), NewAppError("AuthorizeOAuthApp", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIRequestBytes(http.MethodPost, c.URL+"/oauth/authorize", buf, "") + if err != nil { + return "", BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body)["redirect"], BuildResponse(r), nil +} + +// DeauthorizeOAuthApp will deauthorize an OAuth 2.0 client application from accessing a user's account. +func (c *Client4) DeauthorizeOAuthApp(appId string) (*Response, error) { + requestData := map[string]string{"client_id": appId} + r, err := c.DoAPIRequest(http.MethodPost, c.URL+"/oauth/deauthorize", MapToJSON(requestData), "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetOAuthAccessToken is a test helper function for the OAuth access token endpoint. +func (c *Client4) GetOAuthAccessToken(data url.Values) (*AccessResponse, *Response, error) { + url := c.URL + "/oauth/access_token" + rq, err := http.NewRequest(http.MethodPost, url, strings.NewReader(data.Encode())) + if err != nil { + return nil, nil, err + } + rq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return nil, BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + var ar *AccessResponse + err = json.NewDecoder(rp.Body).Decode(&ar) + if err != nil { + return nil, BuildResponse(rp), NewAppError(url, "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + return ar, BuildResponse(rp), nil +} + +// Elasticsearch Section + +// TestElasticsearch will attempt to connect to the configured Elasticsearch server and return OK if configured. +// correctly. +func (c *Client4) TestElasticsearch() (*Response, error) { + r, err := c.DoAPIPost(c.elasticsearchRoute()+"/test", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// PurgeElasticsearchIndexes immediately deletes all Elasticsearch indexes. +func (c *Client4) PurgeElasticsearchIndexes() (*Response, error) { + r, err := c.DoAPIPost(c.elasticsearchRoute()+"/purge_indexes", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// Bleve Section + +// PurgeBleveIndexes immediately deletes all Bleve indexes. +func (c *Client4) PurgeBleveIndexes() (*Response, error) { + r, err := c.DoAPIPost(c.bleveRoute()+"/purge_indexes", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// Data Retention Section + +// GetDataRetentionPolicy will get the current global data retention policy details. +func (c *Client4) GetDataRetentionPolicy() (*GlobalRetentionPolicy, *Response, error) { + r, err := c.DoAPIGet(c.dataRetentionRoute()+"/policy", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var p GlobalRetentionPolicy + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("GetDataRetentionPolicy", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +// GetDataRetentionPolicyByID will get the details for the granular data retention policy with the specified ID. +func (c *Client4) GetDataRetentionPolicyByID(policyID string) (*RetentionPolicyWithTeamAndChannelCounts, *Response, error) { + r, err := c.DoAPIGet(c.dataRetentionPolicyRoute(policyID), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var p RetentionPolicyWithTeamAndChannelCounts + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("GetDataRetentionPolicyByID", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +// GetDataRetentionPoliciesCount will get the total number of granular data retention policies. +func (c *Client4) GetDataRetentionPoliciesCount() (int64, *Response, error) { + type CountBody struct { + TotalCount int64 `json:"total_count"` + } + r, err := c.DoAPIGet(c.dataRetentionRoute()+"/policies_count", "") + if err != nil { + return 0, BuildResponse(r), err + } + var countObj CountBody + err = json.NewDecoder(r.Body).Decode(&countObj) + if err != nil { + return 0, nil, NewAppError("Client4.GetDataRetentionPoliciesCount", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + return countObj.TotalCount, BuildResponse(r), nil +} + +// GetDataRetentionPolicies will get the current granular data retention policies' details. +func (c *Client4) GetDataRetentionPolicies(page, perPage int) (*RetentionPolicyWithTeamAndChannelCountsList, *Response, error) { + query := fmt.Sprintf("?page=%d&per_page=%d", page, perPage) + r, err := c.DoAPIGet(c.dataRetentionRoute()+"/policies"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var p RetentionPolicyWithTeamAndChannelCountsList + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("GetDataRetentionPolicies", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +// CreateDataRetentionPolicy will create a new granular data retention policy which will be applied to +// the specified teams and channels. The Id field of `policy` must be empty. +func (c *Client4) CreateDataRetentionPolicy(policy *RetentionPolicyWithTeamAndChannelIDs) (*RetentionPolicyWithTeamAndChannelCounts, *Response, error) { + policyJSON, jsonErr := json.Marshal(policy) + if jsonErr != nil { + return nil, nil, NewAppError("CreateDataRetentionPolicy", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.dataRetentionRoute()+"/policies", policyJSON) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var p RetentionPolicyWithTeamAndChannelCounts + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("CreateDataRetentionPolicy", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +// DeleteDataRetentionPolicy will delete the granular data retention policy with the specified ID. +func (c *Client4) DeleteDataRetentionPolicy(policyID string) (*Response, error) { + r, err := c.DoAPIDelete(c.dataRetentionPolicyRoute(policyID)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// PatchDataRetentionPolicy will patch the granular data retention policy with the specified ID. +// The Id field of `patch` must be non-empty. +func (c *Client4) PatchDataRetentionPolicy(patch *RetentionPolicyWithTeamAndChannelIDs) (*RetentionPolicyWithTeamAndChannelCounts, *Response, error) { + patchJSON, jsonErr := json.Marshal(patch) + if jsonErr != nil { + return nil, nil, NewAppError("PatchDataRetentionPolicy", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPatchBytes(c.dataRetentionPolicyRoute(patch.ID), patchJSON) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var p RetentionPolicyWithTeamAndChannelCounts + if jsonErr := json.NewDecoder(r.Body).Decode(&p); jsonErr != nil { + return nil, nil, NewAppError("PatchDataRetentionPolicy", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &p, BuildResponse(r), nil +} + +// GetTeamsForRetentionPolicy will get the teams to which the specified policy is currently applied. +func (c *Client4) GetTeamsForRetentionPolicy(policyID string, page, perPage int) (*TeamsWithCount, *Response, error) { + query := fmt.Sprintf("?page=%d&per_page=%d", page, perPage) + r, err := c.DoAPIGet(c.dataRetentionPolicyRoute(policyID)+"/teams"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + var teams *TeamsWithCount + err = json.NewDecoder(r.Body).Decode(&teams) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.GetTeamsForRetentionPolicy", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + return teams, BuildResponse(r), nil +} + +// SearchTeamsForRetentionPolicy will search the teams to which the specified policy is currently applied. +func (c *Client4) SearchTeamsForRetentionPolicy(policyID string, term string) ([]*Team, *Response, error) { + body, _ := json.Marshal(map[string]interface{}{"term": term}) + r, err := c.DoAPIPostBytes(c.dataRetentionPolicyRoute(policyID)+"/teams/search", body) + if err != nil { + return nil, BuildResponse(r), err + } + var teams []*Team + err = json.NewDecoder(r.Body).Decode(&teams) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.SearchTeamsForRetentionPolicy", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + return teams, BuildResponse(r), nil +} + +// AddTeamsToRetentionPolicy will add the specified teams to the granular data retention policy +// with the specified ID. +func (c *Client4) AddTeamsToRetentionPolicy(policyID string, teamIDs []string) (*Response, error) { + body, _ := json.Marshal(teamIDs) + r, err := c.DoAPIPostBytes(c.dataRetentionPolicyRoute(policyID)+"/teams", body) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// RemoveTeamsFromRetentionPolicy will remove the specified teams from the granular data retention policy +// with the specified ID. +func (c *Client4) RemoveTeamsFromRetentionPolicy(policyID string, teamIDs []string) (*Response, error) { + body, _ := json.Marshal(teamIDs) + r, err := c.DoAPIDeleteBytes(c.dataRetentionPolicyRoute(policyID)+"/teams", body) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetChannelsForRetentionPolicy will get the channels to which the specified policy is currently applied. +func (c *Client4) GetChannelsForRetentionPolicy(policyID string, page, perPage int) (*ChannelsWithCount, *Response, error) { + query := fmt.Sprintf("?page=%d&per_page=%d", page, perPage) + r, err := c.DoAPIGet(c.dataRetentionPolicyRoute(policyID)+"/channels"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + var channels *ChannelsWithCount + err = json.NewDecoder(r.Body).Decode(&channels) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.GetChannelsForRetentionPolicy", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + return channels, BuildResponse(r), nil +} + +// SearchChannelsForRetentionPolicy will search the channels to which the specified policy is currently applied. +func (c *Client4) SearchChannelsForRetentionPolicy(policyID string, term string) (ChannelListWithTeamData, *Response, error) { + body, _ := json.Marshal(map[string]interface{}{"term": term}) + r, err := c.DoAPIPostBytes(c.dataRetentionPolicyRoute(policyID)+"/channels/search", body) + if err != nil { + return nil, BuildResponse(r), err + } + var channels ChannelListWithTeamData + err = json.NewDecoder(r.Body).Decode(&channels) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.SearchChannelsForRetentionPolicy", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + return channels, BuildResponse(r), nil +} + +// AddChannelsToRetentionPolicy will add the specified channels to the granular data retention policy +// with the specified ID. +func (c *Client4) AddChannelsToRetentionPolicy(policyID string, channelIDs []string) (*Response, error) { + body, _ := json.Marshal(channelIDs) + r, err := c.DoAPIPostBytes(c.dataRetentionPolicyRoute(policyID)+"/channels", body) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// RemoveChannelsFromRetentionPolicy will remove the specified channels from the granular data retention policy +// with the specified ID. +func (c *Client4) RemoveChannelsFromRetentionPolicy(policyID string, channelIDs []string) (*Response, error) { + body, _ := json.Marshal(channelIDs) + r, err := c.DoAPIDeleteBytes(c.dataRetentionPolicyRoute(policyID)+"/channels", body) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetTeamPoliciesForUser will get the data retention policies for the teams to which a user belongs. +func (c *Client4) GetTeamPoliciesForUser(userID string, offset, limit int) (*RetentionPolicyForTeamList, *Response, error) { + r, err := c.DoAPIGet(c.userRoute(userID)+"/data_retention/team_policies", "") + if err != nil { + return nil, BuildResponse(r), err + } + var teams RetentionPolicyForTeamList + err = json.NewDecoder(r.Body).Decode(&teams) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.GetTeamPoliciesForUser", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + return &teams, BuildResponse(r), nil +} + +// GetChannelPoliciesForUser will get the data retention policies for the channels to which a user belongs. +func (c *Client4) GetChannelPoliciesForUser(userID string, offset, limit int) (*RetentionPolicyForChannelList, *Response, error) { + r, err := c.DoAPIGet(c.userRoute(userID)+"/data_retention/channel_policies", "") + if err != nil { + return nil, BuildResponse(r), err + } + var channels RetentionPolicyForChannelList + err = json.NewDecoder(r.Body).Decode(&channels) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.GetChannelPoliciesForUser", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + return &channels, BuildResponse(r), nil +} + +// Commands Section + +// CreateCommand will create a new command if the user have the right permissions. +func (c *Client4) CreateCommand(cmd *Command) (*Command, *Response, error) { + buf, err := json.Marshal(cmd) + if err != nil { + return nil, nil, NewAppError("CreateCommand", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.commandsRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var command Command + if jsonErr := json.NewDecoder(r.Body).Decode(&command); jsonErr != nil { + return nil, nil, NewAppError("CreateCommand", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &command, BuildResponse(r), nil +} + +// UpdateCommand updates a command based on the provided Command struct. +func (c *Client4) UpdateCommand(cmd *Command) (*Command, *Response, error) { + buf, err := json.Marshal(cmd) + if err != nil { + return nil, nil, NewAppError("UpdateCommand", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.commandRoute(cmd.Id), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var command Command + if jsonErr := json.NewDecoder(r.Body).Decode(&command); jsonErr != nil { + return nil, nil, NewAppError("UpdateCommand", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &command, BuildResponse(r), nil +} + +// MoveCommand moves a command to a different team. +func (c *Client4) MoveCommand(teamId string, commandId string) (*Response, error) { + cmr := CommandMoveRequest{TeamId: teamId} + buf, err := json.Marshal(cmr) + if err != nil { + return nil, NewAppError("MoveCommand", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.commandMoveRoute(commandId), buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// DeleteCommand deletes a command based on the provided command id string. +func (c *Client4) DeleteCommand(commandId string) (*Response, error) { + r, err := c.DoAPIDelete(c.commandRoute(commandId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// ListCommands will retrieve a list of commands available in the team. +func (c *Client4) ListCommands(teamId string, customOnly bool) ([]*Command, *Response, error) { + query := fmt.Sprintf("?team_id=%v&custom_only=%v", teamId, customOnly) + r, err := c.DoAPIGet(c.commandsRoute()+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var list []*Command + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("ListCommands", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// ListCommandAutocompleteSuggestions will retrieve a list of suggestions for a userInput. +func (c *Client4) ListCommandAutocompleteSuggestions(userInput, teamId string) ([]AutocompleteSuggestion, *Response, error) { + query := fmt.Sprintf("/commands/autocomplete_suggestions?user_input=%v", userInput) + r, err := c.DoAPIGet(c.teamRoute(teamId)+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []AutocompleteSuggestion + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("ListCommandAutocompleteSuggestions", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetCommandById will retrieve a command by id. +func (c *Client4) GetCommandById(cmdId string) (*Command, *Response, error) { + url := fmt.Sprintf("%s/%s", c.commandsRoute(), cmdId) + r, err := c.DoAPIGet(url, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var command Command + if jsonErr := json.NewDecoder(r.Body).Decode(&command); jsonErr != nil { + return nil, nil, NewAppError("GetCommandById", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &command, BuildResponse(r), nil +} + +// ExecuteCommand executes a given slash command. +func (c *Client4) ExecuteCommand(channelId, command string) (*CommandResponse, *Response, error) { + commandArgs := &CommandArgs{ + ChannelId: channelId, + Command: command, + } + buf, err := json.Marshal(commandArgs) + if err != nil { + return nil, nil, NewAppError("ExecuteCommand", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.commandsRoute()+"/execute", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + response, err := CommandResponseFromJSON(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("ExecuteCommand", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return response, BuildResponse(r), nil +} + +// 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, error) { + commandArgs := &CommandArgs{ + ChannelId: channelId, + TeamId: teamId, + Command: command, + } + buf, err := json.Marshal(commandArgs) + if err != nil { + return nil, nil, NewAppError("ExecuteCommandWithTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.commandsRoute()+"/execute", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + response, err := CommandResponseFromJSON(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("ExecuteCommandWithTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return response, BuildResponse(r), nil +} + +// ListAutocompleteCommands will retrieve a list of commands available in the team. +func (c *Client4) ListAutocompleteCommands(teamId string) ([]*Command, *Response, error) { + r, err := c.DoAPIGet(c.teamAutoCompleteCommandsRoute(teamId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Command + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("ListAutocompleteCommands", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// RegenCommandToken will create a new token if the user have the right permissions. +func (c *Client4) RegenCommandToken(commandId string) (string, *Response, error) { + r, err := c.DoAPIPut(c.commandRoute(commandId)+"/regen_token", "") + if err != nil { + return "", BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body)["token"], BuildResponse(r), nil +} + +// Status Section + +// GetUserStatus returns a user based on the provided user id string. +func (c *Client4) GetUserStatus(userId, etag string) (*Status, *Response, error) { + r, err := c.DoAPIGet(c.userStatusRoute(userId), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var s Status + if r.StatusCode == http.StatusNotModified { + return &s, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&s); jsonErr != nil { + return nil, nil, NewAppError("GetUserStatus", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &s, BuildResponse(r), nil +} + +// GetUsersStatusesByIds returns a list of users status based on the provided user ids. +func (c *Client4) GetUsersStatusesByIds(userIds []string) ([]*Status, *Response, error) { + r, err := c.DoAPIPost(c.userStatusesRoute()+"/ids", ArrayToJSON(userIds)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Status + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUsersStatusesByIds", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// UpdateUserStatus sets a user's status based on the provided user id string. +func (c *Client4) UpdateUserStatus(userId string, userStatus *Status) (*Status, *Response, error) { + buf, err := json.Marshal(userStatus) + if err != nil { + return nil, nil, NewAppError("UpdateUserStatus", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.userStatusRoute(userId), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var s Status + if jsonErr := json.NewDecoder(r.Body).Decode(&s); jsonErr != nil { + return nil, nil, NewAppError("UpdateUserStatus", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &s, BuildResponse(r), nil +} + +// 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, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("image", filename) + if err != nil { + return nil, nil, err + } + + if _, err := io.Copy(part, bytes.NewBuffer(image)); err != nil { + return nil, nil, err + } + + emojiJSON, jsonErr := json.Marshal(emoji) + if jsonErr != nil { + return nil, nil, NewAppError("CreateEmoji", "api.marshal_error", nil, jsonErr.Error(), 0) + } + + if err := writer.WriteField("emoji", string(emojiJSON)); err != nil { + return nil, nil, err + } + + if err := writer.Close(); err != nil { + return nil, nil, err + } + + return c.DoEmojiUploadFile(c.emojisRoute(), body.Bytes(), writer.FormDataContentType()) +} + +// GetEmojiList returns a page of custom emoji on the system. +func (c *Client4) GetEmojiList(page, perPage int) ([]*Emoji, *Response, error) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + r, err := c.DoAPIGet(c.emojisRoute()+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var list []*Emoji + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetEmojiList", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + query := fmt.Sprintf("?page=%v&per_page=%v&sort=%v", page, perPage, sort) + r, err := c.DoAPIGet(c.emojisRoute()+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Emoji + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetSortedEmojiList", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// DeleteEmoji delete an custom emoji on the provided emoji id string. +func (c *Client4) DeleteEmoji(emojiId string) (*Response, error) { + r, err := c.DoAPIDelete(c.emojiRoute(emojiId)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetEmoji returns a custom emoji based on the emojiId string. +func (c *Client4) GetEmoji(emojiId string) (*Emoji, *Response, error) { + r, err := c.DoAPIGet(c.emojiRoute(emojiId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var e Emoji + if jsonErr := json.NewDecoder(r.Body).Decode(&e); jsonErr != nil { + return nil, nil, NewAppError("GetEmoji", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &e, BuildResponse(r), nil +} + +// GetEmojiByName returns a custom emoji based on the name string. +func (c *Client4) GetEmojiByName(name string) (*Emoji, *Response, error) { + r, err := c.DoAPIGet(c.emojiByNameRoute(name), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var e Emoji + if jsonErr := json.NewDecoder(r.Body).Decode(&e); jsonErr != nil { + return nil, nil, NewAppError("GetEmojiByName", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &e, BuildResponse(r), nil +} + +// GetEmojiImage returns the emoji image. +func (c *Client4) GetEmojiImage(emojiId string) ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.emojiRoute(emojiId)+"/image", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetEmojiImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode) + } + + return data, BuildResponse(r), nil +} + +// SearchEmoji returns a list of emoji matching some search criteria. +func (c *Client4) SearchEmoji(search *EmojiSearch) ([]*Emoji, *Response, error) { + buf, err := json.Marshal(search) + if err != nil { + return nil, nil, NewAppError("SearchEmoji", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.emojisRoute()+"/search", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Emoji + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("SearchEmoji", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// AutocompleteEmoji returns a list of emoji starting with or matching name. +func (c *Client4) AutocompleteEmoji(name string, etag string) ([]*Emoji, *Response, error) { + query := fmt.Sprintf("?name=%v", name) + r, err := c.DoAPIGet(c.emojisRoute()+"/autocomplete"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Emoji + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("AutocompleteEmoji", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + buf, err := json.Marshal(reaction) + if err != nil { + return nil, nil, NewAppError("SaveReaction", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.reactionsRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var re Reaction + if jsonErr := json.NewDecoder(r.Body).Decode(&re); jsonErr != nil { + return nil, nil, NewAppError("SaveReaction", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &re, BuildResponse(r), nil +} + +// GetReactions returns a list of reactions to a post. +func (c *Client4) GetReactions(postId string) ([]*Reaction, *Response, error) { + r, err := c.DoAPIGet(c.postRoute(postId)+"/reactions", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Reaction + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetReactions", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// DeleteReaction deletes reaction of a user in a post. +func (c *Client4) DeleteReaction(reaction *Reaction) (*Response, error) { + r, err := c.DoAPIDelete(c.userRoute(reaction.UserId) + c.postRoute(reaction.PostId) + fmt.Sprintf("/reactions/%v", reaction.EmojiName)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// FetchBulkReactions returns a map of postIds and corresponding reactions +func (c *Client4) GetBulkReactions(postIds []string) (map[string][]*Reaction, *Response, error) { + r, err := c.DoAPIPost(c.postsRoute()+"/ids/reactions", ArrayToJSON(postIds)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + reactions := map[string][]*Reaction{} + if jsonErr := json.NewDecoder(r.Body).Decode(&reactions); jsonErr != nil { + return nil, nil, NewAppError("GetBulkReactions", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return reactions, BuildResponse(r), nil +} + +// Timezone Section + +// GetSupportedTimezone returns a page of supported timezones on the system. +func (c *Client4) GetSupportedTimezone() ([]string, *Response, error) { + r, err := c.DoAPIGet(c.timezonesRoute(), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var timezones []string + json.NewDecoder(r.Body).Decode(&timezones) + return timezones, BuildResponse(r), nil +} + +// 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, error) { + requestBody := make(map[string]string) + requestBody["url"] = url + + r, err := c.DoAPIPost(c.openGraphRoute(), MapToJSON(requestBody)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body), BuildResponse(r), nil +} + +// Jobs Section + +// GetJob gets a single job. +func (c *Client4) GetJob(id string) (*Job, *Response, error) { + r, err := c.DoAPIGet(c.jobsRoute()+fmt.Sprintf("/%v", id), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var j Job + if jsonErr := json.NewDecoder(r.Body).Decode(&j); jsonErr != nil { + return nil, nil, NewAppError("GetJob", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &j, BuildResponse(r), nil +} + +// GetJobs gets all jobs, sorted with the job that was created most recently first. +func (c *Client4) GetJobs(page int, perPage int) ([]*Job, *Response, error) { + r, err := c.DoAPIGet(c.jobsRoute()+fmt.Sprintf("?page=%v&per_page=%v", page, perPage), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Job + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetJobs", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.jobsRoute()+fmt.Sprintf("/type/%v?page=%v&per_page=%v", jobType, page, perPage), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Job + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetJobsByType", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// CreateJob creates a job based on the provided job struct. +func (c *Client4) CreateJob(job *Job) (*Job, *Response, error) { + buf, err := json.Marshal(job) + if err != nil { + return nil, nil, NewAppError("CreateJob", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.jobsRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var j Job + if jsonErr := json.NewDecoder(r.Body).Decode(&j); jsonErr != nil { + return nil, nil, NewAppError("CreateJob", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &j, BuildResponse(r), nil +} + +// CancelJob requests the cancellation of the job with the provided Id. +func (c *Client4) CancelJob(jobId string) (*Response, error) { + r, err := c.DoAPIPost(c.jobsRoute()+fmt.Sprintf("/%v/cancel", jobId), "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// DownloadJob downloads the results of the job +func (c *Client4) DownloadJob(jobId string) ([]byte, *Response, error) { + r, err := c.DoAPIGet(c.jobsRoute()+fmt.Sprintf("/%v/download", jobId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetFile", "model.client.read_job_result_file.app_error", nil, err.Error(), r.StatusCode) + } + return data, BuildResponse(r), nil +} + +// Roles Section + +// GetRole gets a single role by ID. +func (c *Client4) GetRole(id string) (*Role, *Response, error) { + r, err := c.DoAPIGet(c.rolesRoute()+fmt.Sprintf("/%v", id), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var role Role + if jsonErr := json.NewDecoder(r.Body).Decode(&role); jsonErr != nil { + return nil, nil, NewAppError("GetRole", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &role, BuildResponse(r), nil +} + +// GetRoleByName gets a single role by Name. +func (c *Client4) GetRoleByName(name string) (*Role, *Response, error) { + r, err := c.DoAPIGet(c.rolesRoute()+fmt.Sprintf("/name/%v", name), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var role Role + if jsonErr := json.NewDecoder(r.Body).Decode(&role); jsonErr != nil { + return nil, nil, NewAppError("GetRoleByName", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &role, BuildResponse(r), nil +} + +// GetRolesByNames returns a list of roles based on the provided role names. +func (c *Client4) GetRolesByNames(roleNames []string) ([]*Role, *Response, error) { + r, err := c.DoAPIPost(c.rolesRoute()+"/names", ArrayToJSON(roleNames)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Role + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetRolesByNames", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// PatchRole partially updates a role in the system. Any missing fields are not updated. +func (c *Client4) PatchRole(roleId string, patch *RolePatch) (*Role, *Response, error) { + buf, err := json.Marshal(patch) + if err != nil { + return nil, nil, NewAppError("PatchRole", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.rolesRoute()+fmt.Sprintf("/%v/patch", roleId), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var role Role + if jsonErr := json.NewDecoder(r.Body).Decode(&role); jsonErr != nil { + return nil, nil, NewAppError("PatchRole", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &role, BuildResponse(r), nil +} + +// Schemes Section + +// CreateScheme creates a new Scheme. +func (c *Client4) CreateScheme(scheme *Scheme) (*Scheme, *Response, error) { + buf, err := json.Marshal(scheme) + if err != nil { + return nil, nil, NewAppError("CreateScheme", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.schemesRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var s Scheme + if jsonErr := json.NewDecoder(r.Body).Decode(&s); jsonErr != nil { + return nil, nil, NewAppError("CreateScheme", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &s, BuildResponse(r), nil +} + +// GetScheme gets a single scheme by ID. +func (c *Client4) GetScheme(id string) (*Scheme, *Response, error) { + r, err := c.DoAPIGet(c.schemeRoute(id), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var s Scheme + if jsonErr := json.NewDecoder(r.Body).Decode(&s); jsonErr != nil { + return nil, nil, NewAppError("GetScheme", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &s, BuildResponse(r), nil +} + +// GetSchemes ets 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, error) { + r, err := c.DoAPIGet(c.schemesRoute()+fmt.Sprintf("?scope=%v&page=%v&per_page=%v", scope, page, perPage), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Scheme + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetSchemes", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// DeleteScheme deletes a single scheme by ID. +func (c *Client4) DeleteScheme(id string) (*Response, error) { + r, err := c.DoAPIDelete(c.schemeRoute(id)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// PatchScheme partially updates a scheme in the system. Any missing fields are not updated. +func (c *Client4) PatchScheme(id string, patch *SchemePatch) (*Scheme, *Response, error) { + buf, err := json.Marshal(patch) + if err != nil { + return nil, nil, NewAppError("PatchScheme", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.schemeRoute(id)+"/patch", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var s Scheme + if jsonErr := json.NewDecoder(r.Body).Decode(&s); jsonErr != nil { + return nil, nil, NewAppError("PatchScheme", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &s, BuildResponse(r), nil +} + +// GetTeamsForScheme gets the teams using this scheme, sorted alphabetically by display name. +func (c *Client4) GetTeamsForScheme(schemeId string, page int, perPage int) ([]*Team, *Response, error) { + r, err := c.DoAPIGet(c.schemeRoute(schemeId)+fmt.Sprintf("/teams?page=%v&per_page=%v", page, perPage), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Team + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetTeamsForScheme", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// GetChannelsForScheme gets the channels using this scheme, sorted alphabetically by display name. +func (c *Client4) GetChannelsForScheme(schemeId string, page int, perPage int) (ChannelList, *Response, error) { + r, err := c.DoAPIGet(c.schemeRoute(schemeId)+fmt.Sprintf("/channels?page=%v&per_page=%v", page, perPage), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch ChannelList + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelsForScheme", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// Plugin Section + +// UploadPlugin takes an io.Reader stream pointing to the contents of a .tar.gz plugin. +func (c *Client4) UploadPlugin(file io.Reader) (*Manifest, *Response, error) { + return c.uploadPlugin(file, false) +} + +func (c *Client4) UploadPluginForced(file io.Reader) (*Manifest, *Response, error) { + return c.uploadPlugin(file, true) +} + +func (c *Client4) uploadPlugin(file io.Reader, force bool) (*Manifest, *Response, error) { + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + + if force { + err := writer.WriteField("force", c.boolString(true)) + if err != nil { + return nil, nil, err + } + } + + part, err := writer.CreateFormFile("plugin", "plugin.tar.gz") + if err != nil { + return nil, nil, err + } + + if _, err = io.Copy(part, file); err != nil { + return nil, nil, err + } + + if err = writer.Close(); err != nil { + return nil, nil, err + } + + rq, err := http.NewRequest("POST", c.APIURL+c.pluginsRoute(), body) + if err != nil { + return nil, nil, err + } + rq.Header.Set("Content-Type", writer.FormDataContentType()) + + if c.AuthToken != "" { + rq.Header.Set(HeaderAuth, c.AuthType+" "+c.AuthToken) + } + + rp, err := c.HTTPClient.Do(rq) + if err != nil { + return nil, BuildResponse(rp), err + } + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildResponse(rp), AppErrorFromJSON(rp.Body) + } + + var m Manifest + if jsonErr := json.NewDecoder(rp.Body).Decode(&m); jsonErr != nil { + return nil, nil, NewAppError("uploadPlugin", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &m, BuildResponse(rp), nil +} + +func (c *Client4) InstallPluginFromURL(downloadURL string, force bool) (*Manifest, *Response, error) { + forceStr := c.boolString(force) + + url := fmt.Sprintf("%s?plugin_download_url=%s&force=%s", c.pluginsRoute()+"/install_from_url", url.QueryEscape(downloadURL), forceStr) + r, err := c.DoAPIPost(url, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var m Manifest + if jsonErr := json.NewDecoder(r.Body).Decode(&m); jsonErr != nil { + return nil, nil, NewAppError("InstallPluginFromUrl", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &m, BuildResponse(r), nil +} + +// InstallMarketplacePlugin will install marketplace plugin. +func (c *Client4) InstallMarketplacePlugin(request *InstallMarketplacePluginRequest) (*Manifest, *Response, error) { + buf, err := json.Marshal(request) + if err != nil { + return nil, nil, NewAppError("InstallMarketplacePlugin", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.pluginsRoute()+"/marketplace", string(buf)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var m Manifest + if jsonErr := json.NewDecoder(r.Body).Decode(&m); jsonErr != nil { + return nil, nil, NewAppError("InstallMarketplacePlugin", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &m, BuildResponse(r), nil +} + +// GetPlugins will return a list of plugin manifests for currently active plugins. +func (c *Client4) GetPlugins() (*PluginsResponse, *Response, error) { + r, err := c.DoAPIGet(c.pluginsRoute(), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var resp PluginsResponse + if jsonErr := json.NewDecoder(r.Body).Decode(&resp); jsonErr != nil { + return nil, nil, NewAppError("GetPlugins", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &resp, BuildResponse(r), nil +} + +// GetPluginStatuses will return the plugins installed on any server in the cluster, for reporting +// to the administrator via the system console. +func (c *Client4) GetPluginStatuses() (PluginStatuses, *Response, error) { + r, err := c.DoAPIGet(c.pluginsRoute()+"/statuses", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list PluginStatuses + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetPluginStatuses", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// RemovePlugin will disable and delete a plugin. +func (c *Client4) RemovePlugin(id string) (*Response, error) { + r, err := c.DoAPIDelete(c.pluginRoute(id)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetWebappPlugins will return a list of plugins that the webapp should download. +func (c *Client4) GetWebappPlugins() ([]*Manifest, *Response, error) { + r, err := c.DoAPIGet(c.pluginsRoute()+"/webapp", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var list []*Manifest + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetWebappPlugins", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// EnablePlugin will enable an plugin installed. +func (c *Client4) EnablePlugin(id string) (*Response, error) { + r, err := c.DoAPIPost(c.pluginRoute(id)+"/enable", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// DisablePlugin will disable an enabled plugin. +func (c *Client4) DisablePlugin(id string) (*Response, error) { + r, err := c.DoAPIPost(c.pluginRoute(id)+"/disable", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetMarketplacePlugins will return a list of plugins that an admin can install. +func (c *Client4) GetMarketplacePlugins(filter *MarketplacePluginFilter) ([]*MarketplacePlugin, *Response, error) { + route := c.pluginsRoute() + "/marketplace" + u, err := url.Parse(route) + if err != nil { + return nil, nil, err + } + + filter.ApplyToURL(u) + + r, err := c.DoAPIGet(u.String(), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + plugins, err := MarketplacePluginsFromReader(r.Body) + if err != nil { + return nil, BuildResponse(r), NewAppError(route, "model.client.parse_plugins.app_error", nil, err.Error(), http.StatusBadRequest) + } + + return plugins, BuildResponse(r), nil +} + +// UpdateChannelScheme will update a channel's scheme. +func (c *Client4) UpdateChannelScheme(channelId, schemeId string) (*Response, error) { + sip := &SchemeIDPatch{SchemeID: &schemeId} + buf, err := json.Marshal(sip) + if err != nil { + return nil, NewAppError("UpdateChannelScheme", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.channelSchemeRoute(channelId), buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// UpdateTeamScheme will update a team's scheme. +func (c *Client4) UpdateTeamScheme(teamId, schemeId string) (*Response, error) { + sip := &SchemeIDPatch{SchemeID: &schemeId} + buf, err := json.Marshal(sip) + if err != nil { + return nil, NewAppError("UpdateTeamScheme", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.teamSchemeRoute(teamId), buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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, error) { + url := fmt.Sprintf("%s?url=%s", c.redirectLocationRoute(), url.QueryEscape(urlParam)) + r, err := c.DoAPIGet(url, etag) + if err != nil { + return "", BuildResponse(r), err + } + defer closeBody(r) + return MapFromJSON(r.Body)["location"], BuildResponse(r), nil +} + +// SetServerBusy will mark the server as busy, which disables non-critical services for `secs` seconds. +func (c *Client4) SetServerBusy(secs int) (*Response, error) { + url := fmt.Sprintf("%s?seconds=%d", c.serverBusyRoute(), secs) + r, err := c.DoAPIPost(url, "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// ClearServerBusy will mark the server as not busy. +func (c *Client4) ClearServerBusy() (*Response, error) { + r, err := c.DoAPIDelete(c.serverBusyRoute()) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// 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, error) { + r, err := c.DoAPIGet(c.serverBusyRoute(), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var sbs ServerBusyState + if jsonErr := json.NewDecoder(r.Body).Decode(&sbs); jsonErr != nil { + return nil, nil, NewAppError("GetServerBusy", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &sbs, BuildResponse(r), nil +} + +// RegisterTermsOfServiceAction saves action performed by a user against a specific terms of service. +func (c *Client4) RegisterTermsOfServiceAction(userId, termsOfServiceId string, accepted bool) (*Response, error) { + url := c.userTermsOfServiceRoute(userId) + data := map[string]interface{}{"termsOfServiceId": termsOfServiceId, "accepted": accepted} + r, err := c.DoAPIPost(url, StringInterfaceToJSON(data)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetTermsOfService fetches the latest terms of service +func (c *Client4) GetTermsOfService(etag string) (*TermsOfService, *Response, error) { + url := c.termsOfServiceRoute() + r, err := c.DoAPIGet(url, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tos TermsOfService + if jsonErr := json.NewDecoder(r.Body).Decode(&tos); jsonErr != nil { + return nil, nil, NewAppError("GetTermsOfService", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &tos, BuildResponse(r), nil +} + +// 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, error) { + url := c.userTermsOfServiceRoute(userId) + r, err := c.DoAPIGet(url, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var u UserTermsOfService + if jsonErr := json.NewDecoder(r.Body).Decode(&u); jsonErr != nil { + return nil, nil, NewAppError("GetUserTermsOfService", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &u, BuildResponse(r), nil +} + +// CreateTermsOfService creates new terms of service. +func (c *Client4) CreateTermsOfService(text, userId string) (*TermsOfService, *Response, error) { + url := c.termsOfServiceRoute() + data := map[string]interface{}{"text": text} + r, err := c.DoAPIPost(url, StringInterfaceToJSON(data)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var tos TermsOfService + if jsonErr := json.NewDecoder(r.Body).Decode(&tos); jsonErr != nil { + return nil, nil, NewAppError("CreateTermsOfService", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &tos, BuildResponse(r), nil +} + +func (c *Client4) GetGroup(groupID, etag string) (*Group, *Response, error) { + r, err := c.DoAPIGet(c.groupRoute(groupID), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var g Group + if jsonErr := json.NewDecoder(r.Body).Decode(&g); jsonErr != nil { + return nil, nil, NewAppError("GetGroup", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &g, BuildResponse(r), nil +} + +func (c *Client4) PatchGroup(groupID string, patch *GroupPatch) (*Group, *Response, error) { + payload, _ := json.Marshal(patch) + r, err := c.DoAPIPut(c.groupRoute(groupID)+"/patch", string(payload)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var g Group + if jsonErr := json.NewDecoder(r.Body).Decode(&g); jsonErr != nil { + return nil, nil, NewAppError("PatchGroup", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &g, BuildResponse(r), nil +} + +func (c *Client4) LinkGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, patch *GroupSyncablePatch) (*GroupSyncable, *Response, error) { + payload, _ := json.Marshal(patch) + url := fmt.Sprintf("%s/link", c.groupSyncableRoute(groupID, syncableID, syncableType)) + r, err := c.DoAPIPost(url, string(payload)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var gs GroupSyncable + if jsonErr := json.NewDecoder(r.Body).Decode(&gs); jsonErr != nil { + return nil, nil, NewAppError("LinkGroupSyncable", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &gs, BuildResponse(r), nil +} + +func (c *Client4) UnlinkGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType) (*Response, error) { + url := fmt.Sprintf("%s/link", c.groupSyncableRoute(groupID, syncableID, syncableType)) + r, err := c.DoAPIDelete(url) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +func (c *Client4) GetGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, etag string) (*GroupSyncable, *Response, error) { + r, err := c.DoAPIGet(c.groupSyncableRoute(groupID, syncableID, syncableType), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var gs GroupSyncable + if jsonErr := json.NewDecoder(r.Body).Decode(&gs); jsonErr != nil { + return nil, nil, NewAppError("GetGroupSyncable", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &gs, BuildResponse(r), nil +} + +func (c *Client4) GetGroupSyncables(groupID string, syncableType GroupSyncableType, etag string) ([]*GroupSyncable, *Response, error) { + r, err := c.DoAPIGet(c.groupSyncablesRoute(groupID, syncableType), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*GroupSyncable + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetGroupSyncables", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +func (c *Client4) PatchGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, patch *GroupSyncablePatch) (*GroupSyncable, *Response, error) { + payload, _ := json.Marshal(patch) + r, err := c.DoAPIPut(c.groupSyncableRoute(groupID, syncableID, syncableType)+"/patch", string(payload)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var gs GroupSyncable + if jsonErr := json.NewDecoder(r.Body).Decode(&gs); jsonErr != nil { + return nil, nil, NewAppError("PatchGroupSyncable", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &gs, BuildResponse(r), nil +} + +func (c *Client4) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int, etag string) ([]*UserWithGroups, int64, *Response, error) { + groupIDStr := strings.Join(groupIDs, ",") + query := fmt.Sprintf("?group_ids=%s&page=%d&per_page=%d", groupIDStr, page, perPage) + r, err := c.DoAPIGet(c.teamRoute(teamID)+"/members_minus_group_members"+query, etag) + if err != nil { + return nil, 0, BuildResponse(r), err + } + defer closeBody(r) + + var ugc UsersWithGroupsAndCount + if jsonErr := json.NewDecoder(r.Body).Decode(&ugc); jsonErr != nil { + return nil, 0, nil, NewAppError("TeamMembersMinusGroupMembers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return ugc.Users, ugc.Count, BuildResponse(r), nil +} + +func (c *Client4) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int, etag string) ([]*UserWithGroups, int64, *Response, error) { + groupIDStr := strings.Join(groupIDs, ",") + query := fmt.Sprintf("?group_ids=%s&page=%d&per_page=%d", groupIDStr, page, perPage) + r, err := c.DoAPIGet(c.channelRoute(channelID)+"/members_minus_group_members"+query, etag) + if err != nil { + return nil, 0, BuildResponse(r), err + } + defer closeBody(r) + var ugc UsersWithGroupsAndCount + if jsonErr := json.NewDecoder(r.Body).Decode(&ugc); jsonErr != nil { + return nil, 0, nil, NewAppError("ChannelMembersMinusGroupMembers", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return ugc.Users, ugc.Count, BuildResponse(r), nil +} + +func (c *Client4) PatchConfig(config *Config) (*Config, *Response, error) { + buf, err := json.Marshal(config) + if err != nil { + return nil, nil, NewAppError("PatchConfig", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPutBytes(c.configRoute()+"/patch", buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return ConfigFromJSON(r.Body), BuildResponse(r), nil +} + +func (c *Client4) GetChannelModerations(channelID string, etag string) ([]*ChannelModeration, *Response, error) { + r, err := c.DoAPIGet(c.channelRoute(channelID)+"/moderations", etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*ChannelModeration + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelModerations", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +func (c *Client4) PatchChannelModerations(channelID string, patch []*ChannelModerationPatch) ([]*ChannelModeration, *Response, error) { + payload, err := json.Marshal(patch) + if err != nil { + return nil, nil, NewAppError("PatchChannelModerations", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + + r, err := c.DoAPIPut(c.channelRoute(channelID)+"/moderations/patch", string(payload)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*ChannelModeration + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("PatchChannelModerations", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +func (c *Client4) GetKnownUsers() ([]string, *Response, error) { + r, err := c.DoAPIGet(c.usersRoute()+"/known", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var userIds []string + json.NewDecoder(r.Body).Decode(&userIds) + return userIds, BuildResponse(r), nil +} + +// PublishUserTyping publishes a user is typing websocket event based on the provided TypingRequest. +func (c *Client4) PublishUserTyping(userID string, typingRequest TypingRequest) (*Response, error) { + buf, err := json.Marshal(typingRequest) + if err != nil { + return nil, NewAppError("PublishUserTyping", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.publishUserTypingRoute(userID), buf) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +func (c *Client4) GetChannelMemberCountsByGroup(channelID string, includeTimezones bool, etag string) ([]*ChannelMemberCountByGroup, *Response, error) { + r, err := c.DoAPIGet(c.channelRoute(channelID)+"/member_counts_by_group?include_timezones="+strconv.FormatBool(includeTimezones), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var ch []*ChannelMemberCountByGroup + err = json.NewDecoder(r.Body).Decode(&ch) + if err != nil { + return nil, BuildResponse(r), NewAppError("GetChannelMemberCountsByGroup", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return ch, BuildResponse(r), nil +} + +// RequestTrialLicense will request a trial license and install it in the server +func (c *Client4) RequestTrialLicense(users int) (*Response, error) { + b, _ := json.Marshal(map[string]interface{}{"users": users, "terms_accepted": true}) + r, err := c.DoAPIPost("/trial-license", string(b)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// GetGroupStats retrieves stats for a Mattermost Group +func (c *Client4) GetGroupStats(groupID string) (*GroupStats, *Response, error) { + r, err := c.DoAPIGet(c.groupRoute(groupID)+"/stats", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var gs GroupStats + if jsonErr := json.NewDecoder(r.Body).Decode(&gs); jsonErr != nil { + return nil, nil, NewAppError("GetGroupStats", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &gs, BuildResponse(r), nil +} + +func (c *Client4) GetSidebarCategoriesForTeamForUser(userID, teamID, etag string) (*OrderedSidebarCategories, *Response, error) { + route := c.userCategoryRoute(userID, teamID) + r, err := c.DoAPIGet(route, etag) + if err != nil { + return nil, BuildResponse(r), err + } + + var cat *OrderedSidebarCategories + err = json.NewDecoder(r.Body).Decode(&cat) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.GetSidebarCategoriesForTeamForUser", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + return cat, BuildResponse(r), nil +} + +func (c *Client4) CreateSidebarCategoryForTeamForUser(userID, teamID string, category *SidebarCategoryWithChannels) (*SidebarCategoryWithChannels, *Response, error) { + payload, _ := json.Marshal(category) + route := c.userCategoryRoute(userID, teamID) + r, err := c.DoAPIPostBytes(route, payload) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var cat *SidebarCategoryWithChannels + err = json.NewDecoder(r.Body).Decode(&cat) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.CreateSidebarCategoryForTeamForUser", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + return cat, BuildResponse(r), nil +} + +func (c *Client4) UpdateSidebarCategoriesForTeamForUser(userID, teamID string, categories []*SidebarCategoryWithChannels) ([]*SidebarCategoryWithChannels, *Response, error) { + payload, _ := json.Marshal(categories) + route := c.userCategoryRoute(userID, teamID) + + r, err := c.DoAPIPutBytes(route, payload) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var cat []*SidebarCategoryWithChannels + err = json.NewDecoder(r.Body).Decode(&cat) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.UpdateSidebarCategoriesForTeamForUser", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + + return cat, BuildResponse(r), nil +} + +func (c *Client4) GetSidebarCategoryOrderForTeamForUser(userID, teamID, etag string) ([]string, *Response, error) { + route := c.userCategoryRoute(userID, teamID) + "/order" + r, err := c.DoAPIGet(route, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return ArrayFromJSON(r.Body), BuildResponse(r), nil +} + +func (c *Client4) UpdateSidebarCategoryOrderForTeamForUser(userID, teamID string, order []string) ([]string, *Response, error) { + payload, _ := json.Marshal(order) + route := c.userCategoryRoute(userID, teamID) + "/order" + r, err := c.DoAPIPutBytes(route, payload) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return ArrayFromJSON(r.Body), BuildResponse(r), nil +} + +func (c *Client4) GetSidebarCategoryForTeamForUser(userID, teamID, categoryID, etag string) (*SidebarCategoryWithChannels, *Response, error) { + route := c.userCategoryRoute(userID, teamID) + "/" + categoryID + r, err := c.DoAPIGet(route, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var cat *SidebarCategoryWithChannels + err = json.NewDecoder(r.Body).Decode(&cat) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.UpdateSidebarCategoriesForTeamForUser", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + + return cat, BuildResponse(r), nil +} + +func (c *Client4) UpdateSidebarCategoryForTeamForUser(userID, teamID, categoryID string, category *SidebarCategoryWithChannels) (*SidebarCategoryWithChannels, *Response, error) { + payload, _ := json.Marshal(category) + route := c.userCategoryRoute(userID, teamID) + "/" + categoryID + r, err := c.DoAPIPutBytes(route, payload) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var cat *SidebarCategoryWithChannels + err = json.NewDecoder(r.Body).Decode(&cat) + if err != nil { + return nil, BuildResponse(r), NewAppError("Client4.UpdateSidebarCategoriesForTeamForUser", "model.utils.decode_json.app_error", nil, err.Error(), r.StatusCode) + } + + return cat, BuildResponse(r), nil +} + +// CheckIntegrity performs a database integrity check. +func (c *Client4) CheckIntegrity() ([]IntegrityCheckResult, *Response, error) { + r, err := c.DoAPIPost("/integrity", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var results []IntegrityCheckResult + if err := json.NewDecoder(r.Body).Decode(&results); err != nil { + return nil, BuildResponse(r), NewAppError("Api4.CheckIntegrity", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + return results, BuildResponse(r), nil +} + +func (c *Client4) GetNotices(lastViewed int64, teamId string, client NoticeClientType, clientVersion, locale, etag string) (NoticeMessages, *Response, error) { + url := fmt.Sprintf("/system/notices/%s?lastViewed=%d&client=%s&clientVersion=%s&locale=%s", teamId, lastViewed, client, clientVersion, locale) + r, err := c.DoAPIGet(url, etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + notices, err := UnmarshalProductNoticeMessages(r.Body) + if err != nil { + return nil, BuildResponse(r), err + } + return notices, BuildResponse(r), nil +} + +func (c *Client4) MarkNoticesViewed(ids []string) (*Response, error) { + r, err := c.DoAPIPut("/system/notices/view", ArrayToJSON(ids)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// CreateUpload creates a new upload session. +func (c *Client4) CreateUpload(us *UploadSession) (*UploadSession, *Response, error) { + buf, err := json.Marshal(us) + if err != nil { + return nil, nil, NewAppError("CreateUpload", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPostBytes(c.uploadsRoute(), buf) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var s UploadSession + if jsonErr := json.NewDecoder(r.Body).Decode(&s); jsonErr != nil { + return nil, nil, NewAppError("CreateUpload", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &s, BuildResponse(r), nil +} + +// GetUpload returns the upload session for the specified uploadId. +func (c *Client4) GetUpload(uploadId string) (*UploadSession, *Response, error) { + r, err := c.DoAPIGet(c.uploadRoute(uploadId), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var s UploadSession + if jsonErr := json.NewDecoder(r.Body).Decode(&s); jsonErr != nil { + return nil, nil, NewAppError("GetUpload", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &s, BuildResponse(r), nil +} + +// GetUploadsForUser returns the upload sessions created by the specified +// userId. +func (c *Client4) GetUploadsForUser(userId string) ([]*UploadSession, *Response, error) { + r, err := c.DoAPIGet(c.userRoute(userId)+"/uploads", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*UploadSession + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetUploadsForUser", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + +// UploadData performs an upload. On success it returns +// a FileInfo object. +func (c *Client4) UploadData(uploadId string, data io.Reader) (*FileInfo, *Response, error) { + url := c.uploadRoute(uploadId) + r, err := c.DoAPIRequestReader("POST", c.APIURL+url, data, nil) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var fi FileInfo + if r.StatusCode == http.StatusNoContent { + return nil, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&fi); jsonErr != nil { + return nil, nil, NewAppError("UploadData", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &fi, BuildResponse(r), nil +} + +func (c *Client4) UpdatePassword(userId, currentPassword, newPassword string) (*Response, error) { + requestBody := map[string]string{"current_password": currentPassword, "new_password": newPassword} + r, err := c.DoAPIPut(c.userRoute(userId)+"/password", MapToJSON(requestBody)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +// Cloud Section + +func (c *Client4) GetCloudProducts() ([]*Product, *Response, error) { + r, err := c.DoAPIGet(c.cloudRoute()+"/products", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var cloudProducts []*Product + json.NewDecoder(r.Body).Decode(&cloudProducts) + + return cloudProducts, BuildResponse(r), nil +} + +func (c *Client4) CreateCustomerPayment() (*StripeSetupIntent, *Response, error) { + r, err := c.DoAPIPost(c.cloudRoute()+"/payment", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var setupIntent *StripeSetupIntent + json.NewDecoder(r.Body).Decode(&setupIntent) + + return setupIntent, BuildResponse(r), nil +} + +func (c *Client4) ConfirmCustomerPayment(confirmRequest *ConfirmPaymentMethodRequest) (*Response, error) { + json, _ := json.Marshal(confirmRequest) + + r, err := c.DoAPIPostBytes(c.cloudRoute()+"/payment/confirm", json) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + + return BuildResponse(r), nil +} + +func (c *Client4) GetCloudCustomer() (*CloudCustomer, *Response, error) { + r, err := c.DoAPIGet(c.cloudRoute()+"/customer", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var cloudCustomer *CloudCustomer + json.NewDecoder(r.Body).Decode(&cloudCustomer) + + return cloudCustomer, BuildResponse(r), nil +} + +func (c *Client4) GetSubscription() (*Subscription, *Response, error) { + r, err := c.DoAPIGet(c.cloudRoute()+"/subscription", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var subscription *Subscription + json.NewDecoder(r.Body).Decode(&subscription) + + return subscription, BuildResponse(r), nil +} + +func (c *Client4) GetSubscriptionStats() (*SubscriptionStats, *Response, error) { + r, err := c.DoAPIGet(c.cloudRoute()+"/subscription/stats", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var stats *SubscriptionStats + json.NewDecoder(r.Body).Decode(&stats) + return stats, BuildResponse(r), nil +} + +func (c *Client4) GetInvoicesForSubscription() ([]*Invoice, *Response, error) { + r, err := c.DoAPIGet(c.cloudRoute()+"/subscription/invoices", "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var invoices []*Invoice + json.NewDecoder(r.Body).Decode(&invoices) + + return invoices, BuildResponse(r), nil +} + +func (c *Client4) UpdateCloudCustomer(customerInfo *CloudCustomerInfo) (*CloudCustomer, *Response, error) { + customerBytes, _ := json.Marshal(customerInfo) + + r, err := c.DoAPIPutBytes(c.cloudRoute()+"/customer", customerBytes) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var customer *CloudCustomer + json.NewDecoder(r.Body).Decode(&customer) + + return customer, BuildResponse(r), nil +} + +func (c *Client4) UpdateCloudCustomerAddress(address *Address) (*CloudCustomer, *Response, error) { + addressBytes, _ := json.Marshal(address) + + r, err := c.DoAPIPutBytes(c.cloudRoute()+"/customer/address", addressBytes) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var customer *CloudCustomer + json.NewDecoder(r.Body).Decode(&customer) + + return customer, BuildResponse(r), nil +} + +func (c *Client4) ListImports() ([]string, *Response, error) { + r, err := c.DoAPIGet(c.importsRoute(), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return ArrayFromJSON(r.Body), BuildResponse(r), nil +} + +func (c *Client4) ListExports() ([]string, *Response, error) { + r, err := c.DoAPIGet(c.exportsRoute(), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + return ArrayFromJSON(r.Body), BuildResponse(r), nil +} + +func (c *Client4) DeleteExport(name string) (*Response, error) { + r, err := c.DoAPIDelete(c.exportRoute(name)) + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + return BuildResponse(r), nil +} + +func (c *Client4) DownloadExport(name string, wr io.Writer, offset int64) (int64, *Response, error) { + var headers map[string]string + if offset > 0 { + headers = map[string]string{ + HeaderRange: fmt.Sprintf("bytes=%d-", offset), + } + } + r, err := c.DoAPIRequestWithHeaders(http.MethodGet, c.APIURL+c.exportRoute(name), "", headers) + if err != nil { + return 0, BuildResponse(r), err + } + defer closeBody(r) + n, err := io.Copy(wr, r.Body) + if err != nil { + return n, BuildResponse(r), NewAppError("DownloadExport", "model.client.copy.app_error", nil, err.Error(), r.StatusCode) + } + return n, BuildResponse(r), nil +} + +func (c *Client4) GetUserThreads(userId, teamId string, options GetUserThreadsOpts) (*Threads, *Response, error) { + v := url.Values{} + if options.Since != 0 { + v.Set("since", fmt.Sprintf("%d", options.Since)) + } + if options.Before != "" { + v.Set("before", options.Before) + } + if options.After != "" { + v.Set("after", options.After) + } + if options.PageSize != 0 { + v.Set("per_page", fmt.Sprintf("%d", options.PageSize)) + } + if options.Extended { + v.Set("extended", "true") + } + if options.Deleted { + v.Set("deleted", "true") + } + if options.Unread { + v.Set("unread", "true") + } + url := c.userThreadsRoute(userId, teamId) + if len(v) > 0 { + url += "?" + v.Encode() + } + + r, err := c.DoAPIGet(url, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var threads Threads + json.NewDecoder(r.Body).Decode(&threads) + + return &threads, BuildResponse(r), nil +} + +func (c *Client4) GetUserThread(userId, teamId, threadId string, extended bool) (*ThreadResponse, *Response, error) { + url := c.userThreadRoute(userId, teamId, threadId) + if extended { + url += "?extended=true" + } + r, err := c.DoAPIGet(url, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var thread ThreadResponse + json.NewDecoder(r.Body).Decode(&thread) + + return &thread, BuildResponse(r), nil +} + +func (c *Client4) UpdateThreadsReadForUser(userId, teamId string) (*Response, error) { + r, err := c.DoAPIPut(fmt.Sprintf("%s/read", c.userThreadsRoute(userId, teamId)), "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + + return BuildResponse(r), nil +} + +func (c *Client4) UpdateThreadReadForUser(userId, teamId, threadId string, timestamp int64) (*ThreadResponse, *Response, error) { + r, err := c.DoAPIPut(fmt.Sprintf("%s/read/%d", c.userThreadRoute(userId, teamId, threadId), timestamp), "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var thread ThreadResponse + json.NewDecoder(r.Body).Decode(&thread) + + return &thread, BuildResponse(r), nil +} + +func (c *Client4) UpdateThreadFollowForUser(userId, teamId, threadId string, state bool) (*Response, error) { + var err error + var r *http.Response + if state { + r, err = c.DoAPIPut(c.userThreadRoute(userId, teamId, threadId)+"/following", "") + } else { + r, err = c.DoAPIDelete(c.userThreadRoute(userId, teamId, threadId) + "/following") + } + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + + return BuildResponse(r), nil +} + +func (c *Client4) SendAdminUpgradeRequestEmail() (*Response, error) { + r, err := c.DoAPIPost(c.cloudRoute()+"/subscription/limitreached/invite", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + + return BuildResponse(r), nil +} + +func (c *Client4) SendAdminUpgradeRequestEmailOnJoin() (*Response, error) { + r, err := c.DoAPIPost(c.cloudRoute()+"/subscription/limitreached/join", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + + return BuildResponse(r), nil +} + +func (c *Client4) GetAllSharedChannels(teamID string, page, perPage int) ([]*SharedChannel, *Response, error) { + url := fmt.Sprintf("%s/%s?page=%d&per_page=%d", c.sharedChannelsRoute(), teamID, page, perPage) + r, err := c.DoAPIGet(url, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var channels []*SharedChannel + json.NewDecoder(r.Body).Decode(&channels) + + return channels, BuildResponse(r), nil +} + +func (c *Client4) GetRemoteClusterInfo(remoteID string) (RemoteClusterInfo, *Response, error) { + url := fmt.Sprintf("%s/remote_info/%s", c.sharedChannelsRoute(), remoteID) + r, err := c.DoAPIGet(url, "") + if err != nil { + return RemoteClusterInfo{}, BuildResponse(r), err + } + defer closeBody(r) + + var rci RemoteClusterInfo + json.NewDecoder(r.Body).Decode(&rci) + + return rci, BuildResponse(r), nil +} + +func (c *Client4) GetAncillaryPermissions(subsectionPermissions []string) ([]string, *Response, error) { + var returnedPermissions []string + url := fmt.Sprintf("%s/ancillary?subsection_permissions=%s", c.permissionsRoute(), strings.Join(subsectionPermissions, ",")) + r, err := c.DoAPIGet(url, "") + if err != nil { + return returnedPermissions, BuildResponse(r), err + } + defer closeBody(r) + + json.NewDecoder(r.Body).Decode(&returnedPermissions) + return returnedPermissions, BuildResponse(r), nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/cloud.go b/vendor/github.com/mattermost/mattermost-server/v6/model/cloud.go new file mode 100644 index 00000000..ffd85a2a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/cloud.go @@ -0,0 +1,188 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import "strings" + +const ( + EventTypeFailedPayment = "failed-payment" + EventTypeFailedPaymentNoCard = "failed-payment-no-card" + EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email" + EventTypeTrialWillEnd = "trial-will-end" + EventTypeTrialEnded = "trial-ended" + JoinLimitation = "join" + InviteLimitation = "invite" +) + +var MockCWS string + +type BillingScheme string + +const ( + BillingSchemePerSeat = BillingScheme("per_seat") + BillingSchemeFlatFee = BillingScheme("flat_fee") +) + +type RecurringInterval string + +const ( + RecurringIntervalYearly = RecurringInterval("year") + RecurringIntervalMonthly = RecurringInterval("month") +) + +type SubscriptionFamily string + +const ( + SubscriptionFamilyCloud = SubscriptionFamily("cloud") + SubscriptionFamilyOnPrem = SubscriptionFamily("on-prem") +) + +// Product model represents a product on the cloud system. +type Product struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + PricePerSeat float64 `json:"price_per_seat"` + AddOns []*AddOn `json:"add_ons"` + SKU string `json:"sku"` + PriceID string `json:"price_id"` + Family SubscriptionFamily `json:"product_family"` + RecurringInterval RecurringInterval `json:"recurring_interval"` + BillingScheme BillingScheme `json:"billing_scheme"` +} + +// AddOn represents an addon to a product. +type AddOn struct { + ID string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + PricePerSeat float64 `json:"price_per_seat"` +} + +// StripeSetupIntent represents the SetupIntent model from Stripe for updating payment methods. +type StripeSetupIntent struct { + ID string `json:"id"` + ClientSecret string `json:"client_secret"` +} + +// ConfirmPaymentMethodRequest contains the fields for the customer payment update API. +type ConfirmPaymentMethodRequest struct { + StripeSetupIntentID string `json:"stripe_setup_intent_id"` +} + +// Customer model represents a customer on the system. +type CloudCustomer struct { + CloudCustomerInfo + ID string `json:"id"` + CreatorID string `json:"creator_id"` + CreateAt int64 `json:"create_at"` + BillingAddress *Address `json:"billing_address"` + CompanyAddress *Address `json:"company_address"` + PaymentMethod *PaymentMethod `json:"payment_method"` +} + +// CloudCustomerInfo represents editable info of a customer. +type CloudCustomerInfo struct { + Name string `json:"name"` + Email string `json:"email,omitempty"` + ContactFirstName string `json:"contact_first_name,omitempty"` + ContactLastName string `json:"contact_last_name,omitempty"` + NumEmployees int `json:"num_employees"` +} + +// Address model represents a customer's address. +type Address struct { + City string `json:"city"` + Country string `json:"country"` + Line1 string `json:"line1"` + Line2 string `json:"line2"` + PostalCode string `json:"postal_code"` + State string `json:"state"` +} + +// PaymentMethod represents methods of payment for a customer. +type PaymentMethod struct { + Type string `json:"type"` + LastFour int `json:"last_four"` + ExpMonth int `json:"exp_month"` + ExpYear int `json:"exp_year"` + CardBrand string `json:"card_brand"` + Name string `json:"name"` +} + +// Subscription model represents a subscription on the system. +type Subscription struct { + ID string `json:"id"` + CustomerID string `json:"customer_id"` + ProductID string `json:"product_id"` + AddOns []string `json:"add_ons"` + StartAt int64 `json:"start_at"` + EndAt int64 `json:"end_at"` + CreateAt int64 `json:"create_at"` + Seats int `json:"seats"` + Status string `json:"status"` + DNS string `json:"dns"` + IsPaidTier string `json:"is_paid_tier"` + LastInvoice *Invoice `json:"last_invoice"` + IsFreeTrial string `json:"is_free_trial"` + TrialEndAt int64 `json:"trial_end_at"` +} + +// GetWorkSpaceNameFromDNS returns the work space name. For example from test.mattermost.cloud.com, it returns test +func (s *Subscription) GetWorkSpaceNameFromDNS() string { + return strings.Split(s.DNS, ".")[0] +} + +// Invoice model represents a cloud invoice +type Invoice struct { + ID string `json:"id"` + Number string `json:"number"` + CreateAt int64 `json:"create_at"` + Total int64 `json:"total"` + Tax int64 `json:"tax"` + Status string `json:"status"` + Description string `json:"description"` + PeriodStart int64 `json:"period_start"` + PeriodEnd int64 `json:"period_end"` + SubscriptionID string `json:"subscription_id"` + Items []*InvoiceLineItem `json:"line_items"` +} + +// InvoiceLineItem model represents a cloud invoice lineitem tied to an invoice. +type InvoiceLineItem struct { + PriceID string `json:"price_id"` + Total int64 `json:"total"` + Quantity int64 `json:"quantity"` + PricePerUnit int64 `json:"price_per_unit"` + Description string `json:"description"` + Type string `json:"type"` + Metadata map[string]interface{} `json:"metadata"` +} + +type CWSWebhookPayload struct { + Event string `json:"event"` + FailedPayment *FailedPayment `json:"failed_payment"` + CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"` + SubscriptionTrialEndUnixTimeStamp int64 `json:"trial_end_time_stamp"` +} + +type FailedPayment struct { + CardBrand string `json:"card_brand"` + LastFour int `json:"last_four"` + FailureMessage string `json:"failure_message"` +} + +// CloudWorkspaceOwner is part of the CWS Webhook payload that contains information about the user that created the workspace from the CWS +type CloudWorkspaceOwner struct { + UserName string `json:"username"` +} +type SubscriptionStats struct { + RemainingSeats int `json:"remaining_seats"` + IsPaidTier string `json:"is_paid_tier"` + IsFreeTrial string `json:"is_free_trial"` +} + +type SubscriptionChange struct { + ProductID string `json:"product_id"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_discovery.go b/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_discovery.go new file mode 100644 index 00000000..160a5917 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_discovery.go @@ -0,0 +1,115 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "os" +) + +const ( + CDSOfflineAfterMillis = 1000 * 60 * 30 // 30 minutes + CDSTypeApp = "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 o.Hostname == "" { + 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 o.Hostname == "" { + if ipAddress != "" { + 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 o.ClusterName == "" { + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.name.app_error", nil, "", http.StatusBadRequest) + } + + if o.Type == "" { + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.type.app_error", nil, "", http.StatusBadRequest) + } + + if o.Hostname == "" { + 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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_info.go b/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_info.go new file mode 100644 index 00000000..48d11d2f --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_info.go @@ -0,0 +1,12 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type ClusterInfo struct { + Id string `json:"id"` + Version string `json:"version"` + ConfigHash string `json:"config_hash"` + IPAddress string `json:"ipaddress"` + Hostname string `json:"hostname"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_message.go b/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_message.go new file mode 100644 index 00000000..9db02ffc --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_message.go @@ -0,0 +1,62 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type ClusterEvent string + +const ( + ClusterEventPublish ClusterEvent = "publish" + ClusterEventUpdateStatus ClusterEvent = "update_status" + ClusterEventInvalidateAllCaches ClusterEvent = "inv_all_caches" + ClusterEventInvalidateCacheForReactions ClusterEvent = "inv_reactions" + ClusterEventInvalidateCacheForChannelMembersNotifyProps ClusterEvent = "inv_channel_members_notify_props" + ClusterEventInvalidateCacheForChannelByName ClusterEvent = "inv_channel_name" + ClusterEventInvalidateCacheForChannel ClusterEvent = "inv_channel" + ClusterEventInvalidateCacheForChannelGuestCount ClusterEvent = "inv_channel_guest_count" + ClusterEventInvalidateCacheForUser ClusterEvent = "inv_user" + ClusterEventInvalidateCacheForUserTeams ClusterEvent = "inv_user_teams" + ClusterEventClearSessionCacheForUser ClusterEvent = "clear_session_user" + ClusterEventInvalidateCacheForRoles ClusterEvent = "inv_roles" + ClusterEventInvalidateCacheForRolePermissions ClusterEvent = "inv_role_permissions" + ClusterEventInvalidateCacheForProfileByIds ClusterEvent = "inv_profile_ids" + ClusterEventInvalidateCacheForProfileInChannel ClusterEvent = "inv_profile_in_channel" + ClusterEventInvalidateCacheForSchemes ClusterEvent = "inv_schemes" + ClusterEventInvalidateCacheForFileInfos ClusterEvent = "inv_file_infos" + ClusterEventInvalidateCacheForWebhooks ClusterEvent = "inv_webhooks" + ClusterEventInvalidateCacheForEmojisById ClusterEvent = "inv_emojis_by_id" + ClusterEventInvalidateCacheForEmojisIdByName ClusterEvent = "inv_emojis_id_by_name" + ClusterEventInvalidateCacheForChannelPinnedpostsCounts ClusterEvent = "inv_channel_pinnedposts_counts" + ClusterEventInvalidateCacheForChannelMemberCounts ClusterEvent = "inv_channel_member_counts" + ClusterEventInvalidateCacheForLastPosts ClusterEvent = "inv_last_posts" + ClusterEventInvalidateCacheForLastPostTime ClusterEvent = "inv_last_post_time" + ClusterEventInvalidateCacheForTeams ClusterEvent = "inv_teams" + ClusterEventClearSessionCacheForAllUsers ClusterEvent = "inv_all_user_sessions" + ClusterEventInstallPlugin ClusterEvent = "install_plugin" + ClusterEventRemovePlugin ClusterEvent = "remove_plugin" + ClusterEventPluginEvent ClusterEvent = "plugin_event" + ClusterEventInvalidateCacheForTermsOfService ClusterEvent = "inv_terms_of_service" + ClusterEventBusyStateChanged ClusterEvent = "busy_state_change" + + // Gossip communication + ClusterGossipEventRequestGetLogs = "gossip_request_get_logs" + ClusterGossipEventResponseGetLogs = "gossip_response_get_logs" + ClusterGossipEventRequestGetClusterStats = "gossip_request_cluster_stats" + ClusterGossipEventResponseGetClusterStats = "gossip_response_cluster_stats" + ClusterGossipEventRequestGetPluginStatuses = "gossip_request_plugin_statuses" + ClusterGossipEventResponseGetPluginStatuses = "gossip_response_plugin_statuses" + ClusterGossipEventRequestSaveConfig = "gossip_request_save_config" + ClusterGossipEventResponseSaveConfig = "gossip_response_save_config" + + // SendTypes for ClusterMessage. + ClusterSendBestEffort = "best_effort" + ClusterSendReliable = "reliable" +) + +type ClusterMessage struct { + Event ClusterEvent `json:"event"` + SendType string `json:"-"` + WaitForAllToSend bool `json:"-"` + Data []byte `json:"data,omitempty"` + Props map[string]string `json:"props,omitempty"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_stats.go b/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_stats.go new file mode 100644 index 00000000..3b41cb6e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/cluster_stats.go @@ -0,0 +1,11 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +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"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/command.go b/vendor/github.com/mattermost/mattermost-server/v6/model/command.go new file mode 100644 index 00000000..4bb95298 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/command.go @@ -0,0 +1,136 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "strings" +) + +const ( + CommandMethodPost = "P" + CommandMethodGet = "G" + MinTriggerLength = 1 + MaxTriggerLength = 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"` + // PluginId records the id of the plugin that created this Command. If it is blank, the Command + // was not created by a plugin. + PluginId string `json:"plugin_id"` + AutocompleteData *AutocompleteData `db:"-" json:"autocomplete_data,omitempty"` + // AutocompleteIconData is a base64 encoded svg + AutocompleteIconData string `db:"-" json:"autocomplete_icon_data,omitempty"` +} + +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 the CreatorId is blank, this should be a command created by a plugin. + if o.CreatorId == "" && !IsValidPluginId(o.PluginId) { + return NewAppError("Command.IsValid", "model.command.is_valid.plugin_id.app_error", nil, "", http.StatusBadRequest) + } + + // If the PluginId is blank, this should be a command associated with a userId. + if o.PluginId == "" && !IsValidId(o.CreatorId) { + return NewAppError("Command.IsValid", "model.command.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreatorId != "" && o.PluginId != "" { + return NewAppError("Command.IsValid", "model.command.is_valid.plugin_id.app_error", nil, "command cannot have both a CreatorId and a PluginId", 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) < MinTriggerLength || len(o.Trigger) > MaxTriggerLength || strings.Index(o.Trigger, "/") == 0 || strings.Contains(o.Trigger, " ") { + return NewAppError("Command.IsValid", "model.command.is_valid.trigger.app_error", nil, "", http.StatusBadRequest) + } + + if o.URL == "" || 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 == CommandMethodGet || o.Method == CommandMethodPost) { + 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/v6/model/command_args.go b/vendor/github.com/mattermost/mattermost-server/v6/model/command_args.go new file mode 100644 index 00000000..c8333acb --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/command_args.go @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "github.com/mattermost/mattermost-server/v6/shared/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 i18n.TranslateFunc `json:"-"` + UserMentions UserMentionMap `json:"-"` + ChannelMentions ChannelMentionMap `json:"-"` + + // DO NOT USE Session field is deprecated. MM-26398 + Session Session `json:"-"` +} + +// 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/v6/model/command_autocomplete.go b/vendor/github.com/mattermost/mattermost-server/v6/model/command_autocomplete.go new file mode 100644 index 00000000..a71a08c6 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/command_autocomplete.go @@ -0,0 +1,410 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "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 determines 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: SystemUserRoleId, + 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{SystemAdminRoleId, SystemUserRoleId, ""} + 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 +} + +// 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 +} + +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/v6/model/command_request.go b/vendor/github.com/mattermost/mattermost-server/v6/model/command_request.go new file mode 100644 index 00000000..331394cb --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/command_request.go @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type CommandMoveRequest struct { + TeamId string `json:"team_id"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/command_response.go b/vendor/github.com/mattermost/mattermost-server/v6/model/command_response.go new file mode 100644 index 00000000..b2521f8e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/command_response.go @@ -0,0 +1,72 @@ +// 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/v6/utils/jsonutils" +) + +const ( + CommandResponseTypeInChannel = "in_channel" + CommandResponseTypeEphemeral = "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 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/v6/model/command_webhook.go b/vendor/github.com/mattermost/mattermost-server/v6/model/command_webhook.go new file mode 100644 index 00000000..8093c1b7 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/command_webhook.go @@ -0,0 +1,60 @@ +// 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 + UseCount int +} + +const ( + CommandWebhookLifetime = 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 o.RootId != "" && !IsValidId(o.RootId) { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.root_id.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/compliance.go b/vendor/github.com/mattermost/mattermost-server/v6/model/compliance.go new file mode 100644 index 00000000..b46a0d10 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/compliance.go @@ -0,0 +1,109 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "strings" +) + +const ( + ComplianceStatusCreated = "created" + ComplianceStatusRunning = "running" + ComplianceStatusFinished = "finished" + ComplianceStatusFailed = "failed" + ComplianceStatusRemoved = "removed" + + ComplianceTypeDaily = "daily" + ComplianceTypeAdhoc = "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 + +// ComplianceExportCursor is used for paginated iteration of posts +// for compliance export. +// We need to keep track of the last post ID in addition to the last post +// CreateAt to break ties when two posts have the same CreateAt. +type ComplianceExportCursor struct { + LastChannelsQueryPostCreateAt int64 + LastChannelsQueryPostID string + ChannelsQueryCompleted bool + LastDirectMessagesQueryPostCreateAt int64 + LastDirectMessagesQueryPostID string + DirectMessagesQueryCompleted bool +} + +func (c *Compliance) PreSave() { + if c.Id == "" { + c.Id = NewId() + } + + if c.Status == "" { + c.Status = ComplianceStatusCreated + } + + c.Count = 0 + c.Emails = NormalizeEmail(c.Emails) + c.Keywords = strings.ToLower(c.Keywords) + + c.CreateAt = GetMillis() +} + +func (c *Compliance) DeepCopy() *Compliance { + copy := *c + return © +} + +func (c *Compliance) JobName() string { + jobName := c.Type + if c.Type == ComplianceTypeDaily { + 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 || c.Desc == "" { + 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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/compliance_post.go b/vendor/github.com/mattermost/mattermost-server/v6/model/compliance_post.go new file mode 100644 index 00000000..5c859ffe --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/compliance_post.go @@ -0,0 +1,121 @@ +// 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 + 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", + "UserType", + + "PostId", + "PostCreateAt", + "PostUpdateAt", + "PostDeleteAt", + "PostRootId", + "PostOriginalId", + "PostMessage", + "PostType", + "PostProps", + "PostHashtags", + "PostFileIds", + } +} + +func cleanComplianceStrings(in string) string { + if matched, _ := regexp.MatchString("^\\s*(=|\\+|\\-)", in); matched { + return "'" + in + } + return in +} + +func (cp *CompliancePost) Row() []string { + + postDeleteAt := "" + if cp.PostDeleteAt > 0 { + postDeleteAt = time.Unix(0, cp.PostDeleteAt*int64(1000*1000)).Format(time.RFC3339) + } + + postUpdateAt := "" + if cp.PostUpdateAt != cp.PostCreateAt { + postUpdateAt = time.Unix(0, cp.PostUpdateAt*int64(1000*1000)).Format(time.RFC3339) + } + + userType := "user" + if cp.IsBot { + userType = "bot" + } + + return []string{ + cleanComplianceStrings(cp.TeamName), + cleanComplianceStrings(cp.TeamDisplayName), + + cleanComplianceStrings(cp.ChannelName), + cleanComplianceStrings(cp.ChannelDisplayName), + cleanComplianceStrings(cp.ChannelType), + + cleanComplianceStrings(cp.UserUsername), + cleanComplianceStrings(cp.UserEmail), + cleanComplianceStrings(cp.UserNickname), + userType, + + cp.PostId, + time.Unix(0, cp.PostCreateAt*int64(1000*1000)).Format(time.RFC3339), + postUpdateAt, + postDeleteAt, + + cp.PostRootId, + cp.PostOriginalId, + cleanComplianceStrings(cp.PostMessage), + cp.PostType, + cp.PostProps, + cp.PostHashtags, + cp.PostFileIds, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/config.go b/vendor/github.com/mattermost/mattermost-server/v6/model/config.go new file mode 100644 index 00000000..39cdb893 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/config.go @@ -0,0 +1,3915 @@ +// 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" + "reflect" + "regexp" + "strconv" + "strings" + "time" + + "github.com/mattermost/ldap" + + "github.com/mattermost/mattermost-server/v6/shared/filestore" + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +const ( + ConnSecurityNone = "" + ConnSecurityPlain = "PLAIN" + ConnSecurityTLS = "TLS" + ConnSecurityStarttls = "STARTTLS" + + ImageDriverLocal = "local" + ImageDriverS3 = "amazons3" + + DatabaseDriverMysql = "mysql" + DatabaseDriverPostgres = "postgres" + + SearchengineElasticsearch = "elasticsearch" + + MinioAccessKey = "minioaccesskey" + MinioSecretKey = "miniosecretkey" + MinioBucket = "mattermost-test" + + PasswordMaximumLength = 64 + PasswordMinimumLength = 5 + + ServiceGitlab = "gitlab" + ServiceGoogle = "google" + ServiceOffice365 = "office365" + ServiceOpenid = "openid" + + GenericNoChannelNotification = "generic_no_channel" + GenericNotification = "generic" + GenericNotificationServer = "https://push-test.mattermost.com" + MmSupportAdvisorAddress = "support-advisor@mattermost.com" + FullNotification = "full" + IdLoadedNotification = "id_loaded" + + DirectMessageAny = "any" + DirectMessageTeam = "team" + + ShowUsername = "username" + ShowNicknameFullName = "nickname_full_name" + ShowFullName = "full_name" + + PermissionsAll = "all" + PermissionsChannelAdmin = "channel_admin" + PermissionsTeamAdmin = "team_admin" + PermissionsSystemAdmin = "system_admin" + + FakeSetting = "********************************" + + RestrictEmojiCreationAll = "all" + RestrictEmojiCreationAdmin = "admin" + RestrictEmojiCreationSystemAdmin = "system_admin" + + PermissionsDeletePostAll = "all" + PermissionsDeletePostTeamAdmin = "team_admin" + PermissionsDeletePostSystemAdmin = "system_admin" + + GroupUnreadChannelsDisabled = "disabled" + GroupUnreadChannelsDefaultOn = "default_on" + GroupUnreadChannelsDefaultOff = "default_off" + + CollapsedThreadsDisabled = "disabled" + CollapsedThreadsDefaultOn = "default_on" + CollapsedThreadsDefaultOff = "default_off" + + EmailBatchingBufferSize = 256 + EmailBatchingInterval = 30 + + EmailNotificationContentsFull = "full" + EmailNotificationContentsGeneric = "generic" + + SitenameMaxLength = 30 + + ServiceSettingsDefaultSiteURL = "http://localhost:8065" + ServiceSettingsDefaultTLSCertFile = "" + ServiceSettingsDefaultTLSKeyFile = "" + ServiceSettingsDefaultReadTimeout = 300 + ServiceSettingsDefaultWriteTimeout = 300 + ServiceSettingsDefaultIdleTimeout = 60 + ServiceSettingsDefaultMaxLoginAttempts = 10 + ServiceSettingsDefaultAllowCorsFrom = "" + ServiceSettingsDefaultListenAndAddress = ":8065" + ServiceSettingsDefaultGfycatAPIKey = "2_KtH_W5" + ServiceSettingsDefaultGfycatAPISecret = "3wLVZPiswc3DnaiaFoLkDvB4X0IV6CpMkj4tf2inJRsBY6-FnkT08zGmppWFgeof" + + TeamSettingsDefaultSiteName = "Mattermost" + TeamSettingsDefaultMaxUsersPerTeam = 50 + TeamSettingsDefaultCustomBrandText = "" + TeamSettingsDefaultCustomDescriptionText = "" + TeamSettingsDefaultUserStatusAwayTimeout = 300 + + SqlSettingsDefaultDataSource = "postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable&connect_timeout=10" + + FileSettingsDefaultDirectory = "./data/" + + ImportSettingsDefaultDirectory = "./import" + ImportSettingsDefaultRetentionDays = 30 + + ExportSettingsDefaultDirectory = "./export" + ExportSettingsDefaultRetentionDays = 30 + + EmailSettingsDefaultFeedbackOrganization = "" + + SupportSettingsDefaultTermsOfServiceLink = "https://mattermost.com/terms-of-service/" + SupportSettingsDefaultPrivacyPolicyLink = "https://mattermost.com/privacy-policy/" + SupportSettingsDefaultAboutLink = "https://about.mattermost.com/default-about/" + SupportSettingsDefaultHelpLink = "https://about.mattermost.com/default-help/" + SupportSettingsDefaultReportAProblemLink = "https://about.mattermost.com/default-report-a-problem/" + SupportSettingsDefaultSupportEmail = "" + SupportSettingsDefaultReAcceptancePeriod = 365 + + LdapSettingsDefaultFirstNameAttribute = "" + LdapSettingsDefaultLastNameAttribute = "" + LdapSettingsDefaultEmailAttribute = "" + LdapSettingsDefaultUsernameAttribute = "" + LdapSettingsDefaultNicknameAttribute = "" + LdapSettingsDefaultIdAttribute = "" + LdapSettingsDefaultPositionAttribute = "" + LdapSettingsDefaultLoginFieldName = "" + LdapSettingsDefaultGroupDisplayNameAttribute = "" + LdapSettingsDefaultGroupIdAttribute = "" + LdapSettingsDefaultPictureAttribute = "" + + SamlSettingsDefaultIdAttribute = "" + SamlSettingsDefaultGuestAttribute = "" + SamlSettingsDefaultAdminAttribute = "" + SamlSettingsDefaultFirstNameAttribute = "" + SamlSettingsDefaultLastNameAttribute = "" + SamlSettingsDefaultEmailAttribute = "" + SamlSettingsDefaultUsernameAttribute = "" + SamlSettingsDefaultNicknameAttribute = "" + SamlSettingsDefaultLocaleAttribute = "" + SamlSettingsDefaultPositionAttribute = "" + + SamlSettingsSignatureAlgorithmSha1 = "RSAwithSHA1" + SamlSettingsSignatureAlgorithmSha256 = "RSAwithSHA256" + SamlSettingsSignatureAlgorithmSha512 = "RSAwithSHA512" + SamlSettingsDefaultSignatureAlgorithm = SamlSettingsSignatureAlgorithmSha1 + + SamlSettingsCanonicalAlgorithmC14n = "Canonical1.0" + SamlSettingsCanonicalAlgorithmC14n11 = "Canonical1.1" + SamlSettingsDefaultCanonicalAlgorithm = SamlSettingsCanonicalAlgorithmC14n + + NativeappSettingsDefaultAppDownloadLink = "https://mattermost.com/download/#mattermostApps" + NativeappSettingsDefaultAndroidAppDownloadLink = "https://about.mattermost.com/mattermost-android-app/" + NativeappSettingsDefaultIosAppDownloadLink = "https://about.mattermost.com/mattermost-ios-app/" + + ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds = 5000 + + AnalyticsSettingsDefaultMaxUsersForStatistics = 2500 + + AnnouncementSettingsDefaultBannerColor = "#f2a93b" + AnnouncementSettingsDefaultBannerTextColor = "#333333" + AnnouncementSettingsDefaultNoticesJsonURL = "https://notices.mattermost.com/" + AnnouncementSettingsDefaultNoticesFetchFrequencySeconds = 3600 + + TeamSettingsDefaultTeamText = "default" + + ElasticsearchSettingsDefaultConnectionURL = "http://localhost:9200" + ElasticsearchSettingsDefaultUsername = "elastic" + ElasticsearchSettingsDefaultPassword = "changeme" + ElasticsearchSettingsDefaultPostIndexReplicas = 1 + ElasticsearchSettingsDefaultPostIndexShards = 1 + ElasticsearchSettingsDefaultChannelIndexReplicas = 1 + ElasticsearchSettingsDefaultChannelIndexShards = 1 + ElasticsearchSettingsDefaultUserIndexReplicas = 1 + ElasticsearchSettingsDefaultUserIndexShards = 1 + ElasticsearchSettingsDefaultAggregatePostsAfterDays = 365 + ElasticsearchSettingsDefaultPostsAggregatorJobStartTime = "03:00" + ElasticsearchSettingsDefaultIndexPrefix = "" + ElasticsearchSettingsDefaultLiveIndexingBatchSize = 1 + ElasticsearchSettingsDefaultBulkIndexingTimeWindowSeconds = 3600 + ElasticsearchSettingsDefaultRequestTimeoutSeconds = 30 + + BleveSettingsDefaultIndexDir = "" + BleveSettingsDefaultBulkIndexingTimeWindowSeconds = 3600 + + DataRetentionSettingsDefaultMessageRetentionDays = 365 + DataRetentionSettingsDefaultFileRetentionDays = 365 + DataRetentionSettingsDefaultDeletionJobStartTime = "02:00" + DataRetentionSettingsDefaultBatchSize = 3000 + + PluginSettingsDefaultDirectory = "./plugins" + PluginSettingsDefaultClientDirectory = "./client/plugins" + PluginSettingsDefaultEnableMarketplace = true + PluginSettingsDefaultMarketplaceURL = "https://api.integrations.mattermost.com" + PluginSettingsOldMarketplaceURL = "https://marketplace.integrations.mattermost.com" + + ComplianceExportTypeCsv = "csv" + ComplianceExportTypeActiance = "actiance" + ComplianceExportTypeGlobalrelay = "globalrelay" + ComplianceExportTypeGlobalrelayZip = "globalrelay-zip" + GlobalrelayCustomerTypeA9 = "A9" + GlobalrelayCustomerTypeA10 = "A10" + + ClientSideCertCheckPrimaryAuth = "primary" + ClientSideCertCheckSecondaryAuth = "secondary" + + ImageProxyTypeLocal = "local" + ImageProxyTypeAtmosCamo = "atmos/camo" + + GoogleSettingsDefaultScope = "profile email" + GoogleSettingsDefaultAuthEndpoint = "https://accounts.google.com/o/oauth2/v2/auth" + GoogleSettingsDefaultTokenEndpoint = "https://www.googleapis.com/oauth2/v4/token" + GoogleSettingsDefaultUserAPIEndpoint = "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata" + + Office365SettingsDefaultScope = "User.Read" + Office365SettingsDefaultAuthEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" + Office365SettingsDefaultTokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token" + Office365SettingsDefaultUserAPIEndpoint = "https://graph.microsoft.com/v1.0/me" + + CloudSettingsDefaultCwsURL = "https://customers.mattermost.com" + CloudSettingsDefaultCwsAPIURL = "https://portal.internal.prod.cloud.mattermost.com" + OpenidSettingsDefaultScope = "profile openid email" + + LocalModeSocketPath = "/var/tmp/mattermost_local.socket" +) + +func GetDefaultAppCustomURLSchemes() []string { + return []string{"mmauth://", "mmauthbeta://"} +} + +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 `access:"environment_web_server,authentication_saml,write_restrictable"` + WebsocketURL *string `access:"write_restrictable,cloud_restrictable"` + LicenseFileLocation *string `access:"write_restrictable,cloud_restrictable"` // telemetry: none + ListenAddress *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` // telemetry: none + ConnectionSecurity *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` + TLSCertFile *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` + TLSKeyFile *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` + TLSMinVer *string `access:"write_restrictable,cloud_restrictable"` // telemetry: none + TLSStrictTransport *bool `access:"write_restrictable,cloud_restrictable"` + TLSStrictTransportMaxAge *int64 `access:"write_restrictable,cloud_restrictable"` // telemetry: none + TLSOverwriteCiphers []string `access:"write_restrictable,cloud_restrictable"` // telemetry: none + UseLetsEncrypt *bool `access:"environment_web_server,write_restrictable,cloud_restrictable"` + LetsEncryptCertificateCacheFile *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` // telemetry: none + Forward80To443 *bool `access:"environment_web_server,write_restrictable,cloud_restrictable"` + TrustedProxyIPHeader []string `access:"write_restrictable,cloud_restrictable"` // telemetry: none + ReadTimeout *int `access:"environment_web_server,write_restrictable,cloud_restrictable"` + WriteTimeout *int `access:"environment_web_server,write_restrictable,cloud_restrictable"` + IdleTimeout *int `access:"write_restrictable,cloud_restrictable"` + MaximumLoginAttempts *int `access:"authentication_password,write_restrictable,cloud_restrictable"` + GoroutineHealthThreshold *int `access:"write_restrictable,cloud_restrictable"` // telemetry: none + EnableOAuthServiceProvider *bool `access:"integrations_integration_management"` + EnableIncomingWebhooks *bool `access:"integrations_integration_management"` + EnableOutgoingWebhooks *bool `access:"integrations_integration_management"` + EnableCommands *bool `access:"integrations_integration_management"` + EnablePostUsernameOverride *bool `access:"integrations_integration_management"` + EnablePostIconOverride *bool `access:"integrations_integration_management"` + GoogleDeveloperKey *string `access:"site_posts,write_restrictable,cloud_restrictable"` + EnableLinkPreviews *bool `access:"site_posts"` + EnablePermalinkPreviews *bool `access:"site_posts"` + RestrictLinkPreviews *string `access:"site_posts"` + EnableTesting *bool `access:"environment_developer,write_restrictable,cloud_restrictable"` + EnableDeveloper *bool `access:"environment_developer,write_restrictable,cloud_restrictable"` + EnableOpenTracing *bool `access:"write_restrictable,cloud_restrictable"` + EnableSecurityFixAlert *bool `access:"environment_smtp,write_restrictable,cloud_restrictable"` + EnableInsecureOutgoingConnections *bool `access:"environment_web_server,write_restrictable,cloud_restrictable"` + AllowedUntrustedInternalConnections *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` + EnableMultifactorAuthentication *bool `access:"authentication_mfa"` + EnforceMultifactorAuthentication *bool `access:"authentication_mfa"` + EnableUserAccessTokens *bool `access:"integrations_integration_management"` + AllowCorsFrom *string `access:"integrations_cors,write_restrictable,cloud_restrictable"` + CorsExposedHeaders *string `access:"integrations_cors,write_restrictable,cloud_restrictable"` + CorsAllowCredentials *bool `access:"integrations_cors,write_restrictable,cloud_restrictable"` + CorsDebug *bool `access:"integrations_cors,write_restrictable,cloud_restrictable"` + AllowCookiesForSubdomains *bool `access:"write_restrictable,cloud_restrictable"` + ExtendSessionLengthWithActivity *bool `access:"environment_session_lengths,write_restrictable,cloud_restrictable"` + SessionLengthWebInDays *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"` + SessionLengthMobileInDays *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"` + SessionLengthSSOInDays *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"` + SessionCacheInMinutes *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"` + SessionIdleTimeoutInMinutes *int `access:"environment_session_lengths,write_restrictable,cloud_restrictable"` + WebsocketSecurePort *int `access:"write_restrictable,cloud_restrictable"` // telemetry: none + WebsocketPort *int `access:"write_restrictable,cloud_restrictable"` // telemetry: none + WebserverMode *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` + EnableGifPicker *bool `access:"integrations_gif"` + GfycatAPIKey *string `access:"integrations_gif"` + GfycatAPISecret *string `access:"integrations_gif"` + EnableCustomEmoji *bool `access:"site_emoji"` + EnableEmojiPicker *bool `access:"site_emoji"` + PostEditTimeLimit *int `access:"user_management_permissions"` + TimeBetweenUserTypingUpdatesMilliseconds *int64 `access:"experimental_features,write_restrictable,cloud_restrictable"` + EnablePostSearch *bool `access:"write_restrictable,cloud_restrictable"` + EnableFileSearch *bool `access:"write_restrictable"` + MinimumHashtagLength *int `access:"environment_database,write_restrictable,cloud_restrictable"` + EnableUserTypingMessages *bool `access:"experimental_features,write_restrictable,cloud_restrictable"` + EnableChannelViewedMessages *bool `access:"experimental_features,write_restrictable,cloud_restrictable"` + EnableUserStatuses *bool `access:"write_restrictable,cloud_restrictable"` + ExperimentalEnableAuthenticationTransfer *bool `access:"experimental_features,write_restrictable,cloud_restrictable"` + ClusterLogTimeoutMilliseconds *int `access:"write_restrictable,cloud_restrictable"` + EnablePreviewFeatures *bool `access:"experimental_features"` + EnableTutorial *bool `access:"experimental_features"` + EnableOnboardingFlow *bool `access:"experimental_features"` + ExperimentalEnableDefaultChannelLeaveJoinMessages *bool `access:"experimental_features"` + ExperimentalGroupUnreadChannels *string `access:"experimental_features"` + EnableAPITeamDeletion *bool + EnableAPIUserDeletion *bool + ExperimentalEnableHardenedMode *bool `access:"experimental_features"` + ExperimentalStrictCSRFEnforcement *bool `access:"experimental_features,write_restrictable,cloud_restrictable"` + EnableEmailInvitations *bool `access:"authentication_signup"` + DisableBotsWhenOwnerIsDeactivated *bool `access:"integrations_bot_accounts,write_restrictable,cloud_restrictable"` + EnableBotAccountCreation *bool `access:"integrations_bot_accounts"` + EnableSVGs *bool `access:"site_posts"` + EnableLatex *bool `access:"site_posts"` + EnableAPIChannelDeletion *bool + EnableLocalMode *bool + LocalModeSocketLocation *string // telemetry: none + EnableAWSMetering *bool // telemetry: none + SplitKey *string `access:"experimental_feature_flags,write_restrictable"` // telemetry: none + FeatureFlagSyncIntervalSeconds *int `access:"experimental_feature_flags,write_restrictable"` // telemetry: none + DebugSplit *bool `access:"experimental_feature_flags,write_restrictable"` // telemetry: none + ThreadAutoFollow *bool `access:"experimental_features"` + CollapsedThreads *string `access:"experimental_features"` + ManagedResourcePaths *string `access:"environment_web_server,write_restrictable,cloud_restrictable"` + EnableReliableWebSockets *bool `access:"experimental_features"` // telemetry: none +} + +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(ServiceSettingsDefaultSiteURL) + } 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(ServiceSettingsDefaultListenAndAddress) + } + + if s.EnableLinkPreviews == nil { + s.EnableLinkPreviews = NewBool(true) + } + + if s.EnablePermalinkPreviews == nil { + s.EnablePermalinkPreviews = NewBool(true) + } + + if s.RestrictLinkPreviews == nil { + s.RestrictLinkPreviews = NewString("") + } + + 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(ServiceSettingsDefaultTLSKeyFile) + } + + if s.TLSCertFile == nil { + s.TLSCertFile = NewString(ServiceSettingsDefaultTLSCertFile) + } + + 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(ServiceSettingsDefaultReadTimeout) + } + + if s.WriteTimeout == nil { + s.WriteTimeout = NewInt(ServiceSettingsDefaultWriteTimeout) + } + + if s.IdleTimeout == nil { + s.IdleTimeout = NewInt(ServiceSettingsDefaultIdleTimeout) + } + + if s.MaximumLoginAttempts == nil { + s.MaximumLoginAttempts = NewInt(ServiceSettingsDefaultMaxLoginAttempts) + } + + 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{HeaderForwarded, HeaderRealIP} + } + } 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.EnableFileSearch == nil { + s.EnableFileSearch = 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.EnableTutorial == nil { + s.EnableTutorial = NewBool(true) + } + + if s.EnableOnboardingFlow == nil { + s.EnableOnboardingFlow = 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.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(ServiceSettingsDefaultAllowCorsFrom) + } + + 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(true) + } + + if s.EnableEmojiPicker == nil { + s.EnableEmojiPicker = NewBool(true) + } + + if s.EnableGifPicker == nil { + s.EnableGifPicker = NewBool(true) + } + + if s.GfycatAPIKey == nil || *s.GfycatAPIKey == "" { + s.GfycatAPIKey = NewString(ServiceSettingsDefaultGfycatAPIKey) + } + + if s.GfycatAPISecret == nil || *s.GfycatAPISecret == "" { + s.GfycatAPISecret = NewString(ServiceSettingsDefaultGfycatAPISecret) + } + + 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(GroupUnreadChannelsDisabled) + } else if *s.ExperimentalGroupUnreadChannels == "0" { + s.ExperimentalGroupUnreadChannels = NewString(GroupUnreadChannelsDisabled) + } else if *s.ExperimentalGroupUnreadChannels == "1" { + s.ExperimentalGroupUnreadChannels = NewString(GroupUnreadChannelsDefaultOn) + } + + if s.EnableAPITeamDeletion == nil { + s.EnableAPITeamDeletion = NewBool(false) + } + + if s.EnableAPIUserDeletion == nil { + s.EnableAPIUserDeletion = NewBool(false) + } + + if s.EnableAPIChannelDeletion == nil { + s.EnableAPIChannelDeletion = NewBool(false) + } + + if s.ExperimentalEnableHardenedMode == nil { + s.ExperimentalEnableHardenedMode = NewBool(false) + } + + 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(LocalModeSocketPath) + } + + if s.EnableAWSMetering == nil { + s.EnableAWSMetering = NewBool(false) + } + + if s.SplitKey == nil { + s.SplitKey = NewString("") + } + + if s.FeatureFlagSyncIntervalSeconds == nil { + s.FeatureFlagSyncIntervalSeconds = NewInt(30) + } + + if s.DebugSplit == nil { + s.DebugSplit = NewBool(false) + } + + if s.ThreadAutoFollow == nil { + s.ThreadAutoFollow = NewBool(true) + } + + if s.CollapsedThreads == nil { + s.CollapsedThreads = NewString(CollapsedThreadsDisabled) + } + + if s.ManagedResourcePaths == nil { + s.ManagedResourcePaths = NewString("") + } + + if s.EnableReliableWebSockets == nil { + s.EnableReliableWebSockets = NewBool(true) + } +} + +type ClusterSettings struct { + Enable *bool `access:"environment_high_availability,write_restrictable"` + ClusterName *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none + OverrideHostname *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none + NetworkInterface *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"` + BindAddress *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"` + AdvertiseAddress *string `access:"environment_high_availability,write_restrictable,cloud_restrictable"` + UseIPAddress *bool `access:"environment_high_availability,write_restrictable,cloud_restrictable"` + EnableGossipCompression *bool `access:"environment_high_availability,write_restrictable,cloud_restrictable"` + EnableExperimentalGossipEncryption *bool `access:"environment_high_availability,write_restrictable,cloud_restrictable"` + ReadOnlyConfig *bool `access:"environment_high_availability,write_restrictable,cloud_restrictable"` + GossipPort *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none + StreamingPort *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none + MaxIdleConns *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none + MaxIdleConnsPerHost *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none + IdleConnTimeoutMilliseconds *int `access:"environment_high_availability,write_restrictable,cloud_restrictable"` // telemetry: none +} + +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.EnableExperimentalGossipEncryption == nil { + s.EnableExperimentalGossipEncryption = NewBool(false) + } + + if s.EnableGossipCompression == nil { + s.EnableGossipCompression = NewBool(true) + } + + 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 `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` + BlockProfileRate *int `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` + ListenAddress *string `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` // telemetry: none +} + +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 `access:"experimental_features,cloud_restrictable"` + ClientSideCertCheck *string `access:"experimental_features,cloud_restrictable"` + EnableClickToReply *bool `access:"experimental_features,write_restrictable,cloud_restrictable"` + LinkMetadataTimeoutMilliseconds *int64 `access:"experimental_features,write_restrictable,cloud_restrictable"` + RestrictSystemAdmin *bool `access:"experimental_features,write_restrictable"` + UseNewSAMLLibrary *bool `access:"experimental_features,cloud_restrictable"` + CloudUserLimit *int64 `access:"experimental_features,write_restrictable"` + CloudBilling *bool `access:"experimental_features,write_restrictable"` + EnableSharedChannels *bool `access:"experimental_features"` + EnableRemoteClusterService *bool `access:"experimental_features"` +} + +func (s *ExperimentalSettings) SetDefaults() { + if s.ClientSideCertEnable == nil { + s.ClientSideCertEnable = NewBool(false) + } + + if s.ClientSideCertCheck == nil { + s.ClientSideCertCheck = NewString(ClientSideCertCheckSecondaryAuth) + } + + if s.EnableClickToReply == nil { + s.EnableClickToReply = NewBool(false) + } + + if s.LinkMetadataTimeoutMilliseconds == nil { + s.LinkMetadataTimeoutMilliseconds = NewInt64(ExperimentalSettingsDefaultLinkMetadataTimeoutMilliseconds) + } + + if s.RestrictSystemAdmin == nil { + s.RestrictSystemAdmin = NewBool(false) + } + + if s.CloudUserLimit == nil { + // User limit 0 is treated as no limit + s.CloudUserLimit = NewInt64(0) + } + + if s.CloudBilling == nil { + s.CloudBilling = NewBool(false) + } + + if s.UseNewSAMLLibrary == nil { + s.UseNewSAMLLibrary = NewBool(false) + } + + if s.EnableSharedChannels == nil { + s.EnableSharedChannels = NewBool(false) + } + + if s.EnableRemoteClusterService == nil { + s.EnableRemoteClusterService = NewBool(false) + } +} + +type AnalyticsSettings struct { + MaxUsersForStatistics *int `access:"write_restrictable,cloud_restrictable"` +} + +func (s *AnalyticsSettings) SetDefaults() { + if s.MaxUsersForStatistics == nil { + s.MaxUsersForStatistics = NewInt(AnalyticsSettingsDefaultMaxUsersForStatistics) + } +} + +type SSOSettings struct { + Enable *bool `access:"authentication_openid"` + Secret *string `access:"authentication_openid"` // telemetry: none + Id *string `access:"authentication_openid"` // telemetry: none + Scope *string `access:"authentication_openid"` // telemetry: none + AuthEndpoint *string `access:"authentication_openid"` // telemetry: none + TokenEndpoint *string `access:"authentication_openid"` // telemetry: none + UserAPIEndpoint *string `access:"authentication_openid"` // telemetry: none + DiscoveryEndpoint *string `access:"authentication_openid"` // telemetry: none + ButtonText *string `access:"authentication_openid"` // telemetry: none + ButtonColor *string `access:"authentication_openid"` // telemetry: none +} + +func (s *SSOSettings) setDefaults(scope, authEndpoint, tokenEndpoint, userAPIEndpoint, buttonColor 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.DiscoveryEndpoint == nil { + s.DiscoveryEndpoint = NewString("") + } + + if s.AuthEndpoint == nil { + s.AuthEndpoint = NewString(authEndpoint) + } + + if s.TokenEndpoint == nil { + s.TokenEndpoint = NewString(tokenEndpoint) + } + + if s.UserAPIEndpoint == nil { + s.UserAPIEndpoint = NewString(userAPIEndpoint) + } + + if s.ButtonText == nil { + s.ButtonText = NewString("") + } + + if s.ButtonColor == nil { + s.ButtonColor = NewString(buttonColor) + } +} + +type Office365Settings struct { + Enable *bool `access:"authentication_openid"` + Secret *string `access:"authentication_openid"` // telemetry: none + Id *string `access:"authentication_openid"` // telemetry: none + Scope *string `access:"authentication_openid"` + AuthEndpoint *string `access:"authentication_openid"` // telemetry: none + TokenEndpoint *string `access:"authentication_openid"` // telemetry: none + UserAPIEndpoint *string `access:"authentication_openid"` // telemetry: none + DiscoveryEndpoint *string `access:"authentication_openid"` // telemetry: none + DirectoryId *string `access:"authentication_openid"` // telemetry: none +} + +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(Office365SettingsDefaultScope) + } + + if s.DiscoveryEndpoint == nil { + s.DiscoveryEndpoint = NewString("") + } + + if s.AuthEndpoint == nil { + s.AuthEndpoint = NewString(Office365SettingsDefaultAuthEndpoint) + } + + if s.TokenEndpoint == nil { + s.TokenEndpoint = NewString(Office365SettingsDefaultTokenEndpoint) + } + + if s.UserAPIEndpoint == nil { + s.UserAPIEndpoint = NewString(Office365SettingsDefaultUserAPIEndpoint) + } + + 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.DiscoveryEndpoint = s.DiscoveryEndpoint + ssoSettings.AuthEndpoint = s.AuthEndpoint + ssoSettings.TokenEndpoint = s.TokenEndpoint + ssoSettings.UserAPIEndpoint = s.UserAPIEndpoint + return &ssoSettings +} + +type ReplicaLagSettings struct { + DataSource *string `access:"environment,write_restrictable,cloud_restrictable"` // telemetry: none + QueryAbsoluteLag *string `access:"environment,write_restrictable,cloud_restrictable"` // telemetry: none + QueryTimeLag *string `access:"environment,write_restrictable,cloud_restrictable"` // telemetry: none +} + +type SqlSettings struct { + DriverName *string `access:"environment_database,write_restrictable,cloud_restrictable"` + DataSource *string `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none + DataSourceReplicas []string `access:"environment_database,write_restrictable,cloud_restrictable"` + DataSourceSearchReplicas []string `access:"environment_database,write_restrictable,cloud_restrictable"` + MaxIdleConns *int `access:"environment_database,write_restrictable,cloud_restrictable"` + ConnMaxLifetimeMilliseconds *int `access:"environment_database,write_restrictable,cloud_restrictable"` + ConnMaxIdleTimeMilliseconds *int `access:"environment_database,write_restrictable,cloud_restrictable"` + MaxOpenConns *int `access:"environment_database,write_restrictable,cloud_restrictable"` + Trace *bool `access:"environment_database,write_restrictable,cloud_restrictable"` + AtRestEncryptKey *string `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none + QueryTimeout *int `access:"environment_database,write_restrictable,cloud_restrictable"` + DisableDatabaseSearch *bool `access:"environment_database,write_restrictable,cloud_restrictable"` + ReplicaLagSettings []*ReplicaLagSettings `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none +} + +func (s *SqlSettings) SetDefaults(isUpdate bool) { + if s.DriverName == nil { + s.DriverName = NewString(DatabaseDriverPostgres) + } + + if s.DataSource == nil { + s.DataSource = NewString(SqlSettingsDefaultDataSource) + } + + 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 || *s.AtRestEncryptKey == "" { + 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.ConnMaxIdleTimeMilliseconds == nil { + s.ConnMaxIdleTimeMilliseconds = NewInt(300000) + } + + if s.Trace == nil { + s.Trace = NewBool(false) + } + + if s.QueryTimeout == nil { + s.QueryTimeout = NewInt(30) + } + + if s.DisableDatabaseSearch == nil { + s.DisableDatabaseSearch = NewBool(false) + } + + if s.ReplicaLagSettings == nil { + s.ReplicaLagSettings = []*ReplicaLagSettings{} + } +} + +type LogSettings struct { + EnableConsole *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` + ConsoleLevel *string `access:"environment_logging,write_restrictable,cloud_restrictable"` + ConsoleJson *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` + EnableColor *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` // telemetry: none + EnableFile *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` + FileLevel *string `access:"environment_logging,write_restrictable,cloud_restrictable"` + FileJson *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` + FileLocation *string `access:"environment_logging,write_restrictable,cloud_restrictable"` + EnableWebhookDebugging *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` + EnableDiagnostics *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` // telemetry: none + EnableSentry *bool `access:"environment_logging,write_restrictable,cloud_restrictable"` // telemetry: none + AdvancedLoggingConfig *string `access:"environment_logging,write_restrictable,cloud_restrictable"` +} + +func NewLogSettings() *LogSettings { + settings := &LogSettings{} + settings.SetDefaults() + return settings +} + +func (s *LogSettings) SetDefaults() { + if s.EnableConsole == nil { + s.EnableConsole = NewBool(true) + } + + if s.ConsoleLevel == nil { + s.ConsoleLevel = NewString("DEBUG") + } + + if s.EnableColor == nil { + s.EnableColor = NewBool(false) + } + + 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.EnableSentry == nil { + s.EnableSentry = NewBool(*s.EnableDiagnostics) + } + + if s.ConsoleJson == nil { + s.ConsoleJson = NewBool(true) + } + + if s.FileJson == nil { + s.FileJson = NewBool(true) + } + + if s.AdvancedLoggingConfig == nil { + s.AdvancedLoggingConfig = NewString("") + } +} + +type ExperimentalAuditSettings struct { + FileEnabled *bool `access:"experimental_features,write_restrictable,cloud_restrictable"` + FileName *string `access:"experimental_features,write_restrictable,cloud_restrictable"` // telemetry: none + FileMaxSizeMB *int `access:"experimental_features,write_restrictable,cloud_restrictable"` + FileMaxAgeDays *int `access:"experimental_features,write_restrictable,cloud_restrictable"` + FileMaxBackups *int `access:"experimental_features,write_restrictable,cloud_restrictable"` + FileCompress *bool `access:"experimental_features,write_restrictable,cloud_restrictable"` + FileMaxQueueSize *int `access:"experimental_features,write_restrictable,cloud_restrictable"` + AdvancedLoggingConfig *string `access:"experimental_features,write_restrictable,cloud_restrictable"` +} + +func (s *ExperimentalAuditSettings) SetDefaults() { + 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) + } + + if s.AdvancedLoggingConfig == nil { + s.AdvancedLoggingConfig = NewString("") + } +} + +type NotificationLogSettings struct { + EnableConsole *bool `access:"write_restrictable,cloud_restrictable"` + ConsoleLevel *string `access:"write_restrictable,cloud_restrictable"` + ConsoleJson *bool `access:"write_restrictable,cloud_restrictable"` + EnableColor *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none + EnableFile *bool `access:"write_restrictable,cloud_restrictable"` + FileLevel *string `access:"write_restrictable,cloud_restrictable"` + FileJson *bool `access:"write_restrictable,cloud_restrictable"` + FileLocation *string `access:"write_restrictable,cloud_restrictable"` + AdvancedLoggingConfig *string `access:"write_restrictable,cloud_restrictable"` +} + +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.EnableColor == nil { + s.EnableColor = NewBool(false) + } + + if s.FileJson == nil { + s.FileJson = NewBool(true) + } + + if s.AdvancedLoggingConfig == nil { + s.AdvancedLoggingConfig = NewString("") + } +} + +type PasswordSettings struct { + MinimumLength *int `access:"authentication_password"` + Lowercase *bool `access:"authentication_password"` + Number *bool `access:"authentication_password"` + Uppercase *bool `access:"authentication_password"` + Symbol *bool `access:"authentication_password"` +} + +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 `access:"site_file_sharing_and_downloads,cloud_restrictable"` + EnableMobileUpload *bool `access:"site_file_sharing_and_downloads,cloud_restrictable"` + EnableMobileDownload *bool `access:"site_file_sharing_and_downloads,cloud_restrictable"` + MaxFileSize *int64 `access:"environment_file_storage,cloud_restrictable"` + MaxImageResolution *int64 `access:"environment_file_storage,cloud_restrictable"` + DriverName *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + Directory *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + EnablePublicLink *bool `access:"site_public_links,cloud_restrictable"` + ExtractContent *bool `access:"environment_file_storage,write_restrictable"` + ArchiveRecursion *bool `access:"environment_file_storage,write_restrictable"` + PublicLinkSalt *string `access:"site_public_links,cloud_restrictable"` // telemetry: none + InitialFont *string `access:"environment_file_storage,cloud_restrictable"` // telemetry: none + AmazonS3AccessKeyId *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3SecretAccessKey *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3Bucket *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3PathPrefix *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3Region *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3Endpoint *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3SSL *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + AmazonS3SignV2 *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + AmazonS3SSE *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + AmazonS3Trace *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` +} + +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(100 * 1024 * 1024) // 100MB (IEC) + } + + if s.MaxImageResolution == nil { + s.MaxImageResolution = NewInt64(7680 * 4320) // 8K, ~33MPX + } + + if s.DriverName == nil { + s.DriverName = NewString(ImageDriverLocal) + } + + if s.Directory == nil || *s.Directory == "" { + s.Directory = NewString(FileSettingsDefaultDirectory) + } + + if s.EnablePublicLink == nil { + s.EnablePublicLink = NewBool(false) + } + + if s.ExtractContent == nil { + s.ExtractContent = NewBool(true) + } + + if s.ArchiveRecursion == nil { + s.ArchiveRecursion = NewBool(false) + } + + if isUpdate { + // When updating an existing configuration, ensure link salt has been specified. + if s.PublicLinkSalt == nil || *s.PublicLinkSalt == "" { + 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.AmazonS3PathPrefix == nil { + s.AmazonS3PathPrefix = NewString("") + } + + if s.AmazonS3Region == nil { + s.AmazonS3Region = NewString("") + } + + if s.AmazonS3Endpoint == nil || *s.AmazonS3Endpoint == "" { + // 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) + } +} + +func (s *FileSettings) ToFileBackendSettings(enableComplianceFeature bool) filestore.FileBackendSettings { + if *s.DriverName == ImageDriverLocal { + return filestore.FileBackendSettings{ + DriverName: *s.DriverName, + Directory: *s.Directory, + } + } + return filestore.FileBackendSettings{ + DriverName: *s.DriverName, + AmazonS3AccessKeyId: *s.AmazonS3AccessKeyId, + AmazonS3SecretAccessKey: *s.AmazonS3SecretAccessKey, + AmazonS3Bucket: *s.AmazonS3Bucket, + AmazonS3PathPrefix: *s.AmazonS3PathPrefix, + AmazonS3Region: *s.AmazonS3Region, + AmazonS3Endpoint: *s.AmazonS3Endpoint, + AmazonS3SSL: s.AmazonS3SSL == nil || *s.AmazonS3SSL, + AmazonS3SignV2: s.AmazonS3SignV2 != nil && *s.AmazonS3SignV2, + AmazonS3SSE: s.AmazonS3SSE != nil && *s.AmazonS3SSE && enableComplianceFeature, + AmazonS3Trace: s.AmazonS3Trace != nil && *s.AmazonS3Trace, + } +} + +type EmailSettings struct { + EnableSignUpWithEmail *bool `access:"authentication_email"` + EnableSignInWithEmail *bool `access:"authentication_email"` + EnableSignInWithUsername *bool `access:"authentication_email"` + SendEmailNotifications *bool `access:"site_notifications"` + UseChannelInEmailNotifications *bool `access:"experimental_features"` + RequireEmailVerification *bool `access:"authentication_email"` + FeedbackName *string `access:"site_notifications"` + FeedbackEmail *string `access:"site_notifications,cloud_restrictable"` + ReplyToAddress *string `access:"site_notifications,cloud_restrictable"` + FeedbackOrganization *string `access:"site_notifications"` + EnableSMTPAuth *bool `access:"environment_smtp,write_restrictable,cloud_restrictable"` + SMTPUsername *string `access:"environment_smtp,write_restrictable,cloud_restrictable"` // telemetry: none + SMTPPassword *string `access:"environment_smtp,write_restrictable,cloud_restrictable"` // telemetry: none + SMTPServer *string `access:"environment_smtp,write_restrictable,cloud_restrictable"` // telemetry: none + SMTPPort *string `access:"environment_smtp,write_restrictable,cloud_restrictable"` // telemetry: none + SMTPServerTimeout *int `access:"cloud_restrictable"` + ConnectionSecurity *string `access:"environment_smtp,write_restrictable,cloud_restrictable"` + SendPushNotifications *bool `access:"environment_push_notification_server"` + PushNotificationServer *string `access:"environment_push_notification_server"` // telemetry: none + PushNotificationContents *string `access:"site_notifications"` + PushNotificationBuffer *int // telemetry: none + EnableEmailBatching *bool `access:"site_notifications"` + EmailBatchingBufferSize *int `access:"experimental_features"` + EmailBatchingInterval *int `access:"experimental_features"` + EnablePreviewModeBanner *bool `access:"site_notifications"` + SkipServerCertificateVerification *bool `access:"environment_smtp,write_restrictable,cloud_restrictable"` + EmailNotificationContentsType *string `access:"site_notifications"` + LoginButtonColor *string `access:"experimental_features"` + LoginButtonBorderColor *string `access:"experimental_features"` + LoginButtonTextColor *string `access:"experimental_features"` +} + +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(EmailSettingsDefaultFeedbackOrganization) + } + + if s.EnableSMTPAuth == nil { + if s.ConnectionSecurity == nil || *s.ConnectionSecurity == ConnSecurityNone { + 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 || *s.SMTPServer == "" { + s.SMTPServer = NewString("localhost") + } + + if s.SMTPPort == nil || *s.SMTPPort == "" { + s.SMTPPort = NewString("10025") + } + + if s.SMTPServerTimeout == nil || *s.SMTPServerTimeout == 0 { + s.SMTPServerTimeout = NewInt(10) + } + + if s.ConnectionSecurity == nil || *s.ConnectionSecurity == ConnSecurityPlain { + s.ConnectionSecurity = NewString(ConnSecurityNone) + } + + if s.SendPushNotifications == nil { + s.SendPushNotifications = NewBool(!isUpdate) + } + + if s.PushNotificationServer == nil { + if isUpdate { + s.PushNotificationServer = NewString("") + } else { + s.PushNotificationServer = NewString(GenericNotificationServer) + } + } + + if s.PushNotificationContents == nil { + s.PushNotificationContents = NewString(FullNotification) + } + + if s.PushNotificationBuffer == nil { + s.PushNotificationBuffer = NewInt(1000) + } + + if s.EnableEmailBatching == nil { + s.EnableEmailBatching = NewBool(false) + } + + if s.EmailBatchingBufferSize == nil { + s.EmailBatchingBufferSize = NewInt(EmailBatchingBufferSize) + } + + if s.EmailBatchingInterval == nil { + s.EmailBatchingInterval = NewInt(EmailBatchingInterval) + } + + if s.EnablePreviewModeBanner == nil { + s.EnablePreviewModeBanner = NewBool(true) + } + + if s.EnableSMTPAuth == nil { + if *s.ConnectionSecurity == ConnSecurityNone { + s.EnableSMTPAuth = NewBool(false) + } else { + s.EnableSMTPAuth = NewBool(true) + } + } + + if *s.ConnectionSecurity == ConnSecurityPlain { + *s.ConnectionSecurity = ConnSecurityNone + } + + if s.SkipServerCertificateVerification == nil { + s.SkipServerCertificateVerification = NewBool(false) + } + + if s.EmailNotificationContentsType == nil { + s.EmailNotificationContentsType = NewString(EmailNotificationContentsFull) + } + + 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 `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"` + PerSec *int `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"` + MaxBurst *int `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"` + MemoryStoreSize *int `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"` + VaryByRemoteAddr *bool `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"` + VaryByUser *bool `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"` + VaryByHeader string `access:"environment_rate_limiting,write_restrictable,cloud_restrictable"` +} + +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 `access:"site_users_and_teams"` + ShowFullName *bool `access:"site_users_and_teams"` +} + +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 `access:"site_customization,write_restrictable,cloud_restrictable"` + PrivacyPolicyLink *string `access:"site_customization,write_restrictable,cloud_restrictable"` + AboutLink *string `access:"site_customization,write_restrictable,cloud_restrictable"` + HelpLink *string `access:"site_customization,write_restrictable,cloud_restrictable"` + ReportAProblemLink *string `access:"site_customization,write_restrictable,cloud_restrictable"` + SupportEmail *string `access:"site_customization"` + CustomTermsOfServiceEnabled *bool `access:"compliance_custom_terms_of_service"` + CustomTermsOfServiceReAcceptancePeriod *int `access:"compliance_custom_terms_of_service"` + EnableAskCommunityLink *bool `access:"site_customization"` +} + +func (s *SupportSettings) SetDefaults() { + if !isSafeLink(s.TermsOfServiceLink) { + *s.TermsOfServiceLink = SupportSettingsDefaultTermsOfServiceLink + } + + if s.TermsOfServiceLink == nil { + s.TermsOfServiceLink = NewString(SupportSettingsDefaultTermsOfServiceLink) + } + + if !isSafeLink(s.PrivacyPolicyLink) { + *s.PrivacyPolicyLink = "" + } + + if s.PrivacyPolicyLink == nil { + s.PrivacyPolicyLink = NewString(SupportSettingsDefaultPrivacyPolicyLink) + } + + if !isSafeLink(s.AboutLink) { + *s.AboutLink = "" + } + + if s.AboutLink == nil { + s.AboutLink = NewString(SupportSettingsDefaultAboutLink) + } + + if !isSafeLink(s.HelpLink) { + *s.HelpLink = "" + } + + if s.HelpLink == nil { + s.HelpLink = NewString(SupportSettingsDefaultHelpLink) + } + + if !isSafeLink(s.ReportAProblemLink) { + *s.ReportAProblemLink = "" + } + + if s.ReportAProblemLink == nil { + s.ReportAProblemLink = NewString(SupportSettingsDefaultReportAProblemLink) + } + + if s.SupportEmail == nil { + s.SupportEmail = NewString(SupportSettingsDefaultSupportEmail) + } + + if s.CustomTermsOfServiceEnabled == nil { + s.CustomTermsOfServiceEnabled = NewBool(false) + } + + if s.CustomTermsOfServiceReAcceptancePeriod == nil { + s.CustomTermsOfServiceReAcceptancePeriod = NewInt(SupportSettingsDefaultReAcceptancePeriod) + } + + if s.EnableAskCommunityLink == nil { + s.EnableAskCommunityLink = NewBool(true) + } +} + +type AnnouncementSettings struct { + EnableBanner *bool `access:"site_announcement_banner"` + BannerText *string `access:"site_announcement_banner"` // telemetry: none + BannerColor *string `access:"site_announcement_banner"` + BannerTextColor *string `access:"site_announcement_banner"` + AllowBannerDismissal *bool `access:"site_announcement_banner"` + AdminNoticesEnabled *bool `access:"site_notices"` + UserNoticesEnabled *bool `access:"site_notices"` + NoticesURL *string `access:"site_notices,write_restrictable"` // telemetry: none + NoticesFetchFrequency *int `access:"site_notices,write_restrictable"` // telemetry: none + NoticesSkipCache *bool `access:"site_notices,write_restrictable"` // telemetry: none +} + +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(AnnouncementSettingsDefaultBannerColor) + } + + if s.BannerTextColor == nil { + s.BannerTextColor = NewString(AnnouncementSettingsDefaultBannerTextColor) + } + + if s.AllowBannerDismissal == nil { + s.AllowBannerDismissal = NewBool(true) + } + + if s.AdminNoticesEnabled == nil { + s.AdminNoticesEnabled = NewBool(true) + } + + if s.UserNoticesEnabled == nil { + s.UserNoticesEnabled = NewBool(true) + } + if s.NoticesURL == nil { + s.NoticesURL = NewString(AnnouncementSettingsDefaultNoticesJsonURL) + } + if s.NoticesSkipCache == nil { + s.NoticesSkipCache = NewBool(false) + } + if s.NoticesFetchFrequency == nil { + s.NoticesFetchFrequency = NewInt(AnnouncementSettingsDefaultNoticesFetchFrequencySeconds) + } + +} + +type ThemeSettings struct { + EnableThemeSelection *bool `access:"experimental_features"` + DefaultTheme *string `access:"experimental_features"` + AllowCustomThemes *bool `access:"experimental_features"` + AllowedThemes []string +} + +func (s *ThemeSettings) SetDefaults() { + if s.EnableThemeSelection == nil { + s.EnableThemeSelection = NewBool(true) + } + + if s.DefaultTheme == nil { + s.DefaultTheme = NewString(TeamSettingsDefaultTeamText) + } + + if s.AllowCustomThemes == nil { + s.AllowCustomThemes = NewBool(true) + } + + if s.AllowedThemes == nil { + s.AllowedThemes = []string{} + } +} + +type TeamSettings struct { + SiteName *string `access:"site_customization"` + MaxUsersPerTeam *int `access:"site_users_and_teams"` + EnableUserCreation *bool `access:"authentication_signup"` + EnableOpenServer *bool `access:"authentication_signup"` + EnableUserDeactivation *bool `access:"experimental_features"` + RestrictCreationToDomains *string `access:"authentication_signup"` // telemetry: none + EnableCustomUserStatuses *bool `access:"site_users_and_teams"` + EnableCustomBrand *bool `access:"site_customization"` + CustomBrandText *string `access:"site_customization"` + CustomDescriptionText *string `access:"site_customization"` + RestrictDirectMessage *string `access:"site_users_and_teams"` + UserStatusAwayTimeout *int64 `access:"experimental_features"` + MaxChannelsPerTeam *int64 `access:"site_users_and_teams"` + MaxNotificationsPerChannel *int64 `access:"environment_push_notification_server"` + EnableConfirmNotificationsToChannel *bool `access:"site_notifications"` + TeammateNameDisplay *string `access:"site_users_and_teams"` + ExperimentalViewArchivedChannels *bool `access:"experimental_features,site_users_and_teams"` + ExperimentalEnableAutomaticReplies *bool `access:"experimental_features"` + LockTeammateNameDisplay *bool `access:"site_users_and_teams"` + ExperimentalPrimaryTeam *string `access:"experimental_features"` + ExperimentalDefaultChannels []string `access:"experimental_features"` +} + +func (s *TeamSettings) SetDefaults() { + + if s.SiteName == nil || *s.SiteName == "" { + s.SiteName = NewString(TeamSettingsDefaultSiteName) + } + + if s.MaxUsersPerTeam == nil { + s.MaxUsersPerTeam = NewInt(TeamSettingsDefaultMaxUsersPerTeam) + } + + 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.EnableCustomUserStatuses == nil { + s.EnableCustomUserStatuses = NewBool(true) + } + + if s.EnableCustomBrand == nil { + s.EnableCustomBrand = NewBool(false) + } + + if s.EnableUserDeactivation == nil { + s.EnableUserDeactivation = NewBool(false) + } + + if s.CustomBrandText == nil { + s.CustomBrandText = NewString(TeamSettingsDefaultCustomBrandText) + } + + if s.CustomDescriptionText == nil { + s.CustomDescriptionText = NewString(TeamSettingsDefaultCustomDescriptionText) + } + + if s.RestrictDirectMessage == nil { + s.RestrictDirectMessage = NewString(DirectMessageAny) + } + + if s.UserStatusAwayTimeout == nil { + s.UserStatusAwayTimeout = NewInt64(TeamSettingsDefaultUserStatusAwayTimeout) + } + + 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.ExperimentalPrimaryTeam == nil { + s.ExperimentalPrimaryTeam = NewString("") + } + + if s.ExperimentalDefaultChannels == nil { + s.ExperimentalDefaultChannels = []string{} + } + + if s.EnableUserCreation == nil { + s.EnableUserCreation = NewBool(true) + } + + if s.ExperimentalViewArchivedChannels == nil { + s.ExperimentalViewArchivedChannels = NewBool(true) + } + + if s.LockTeammateNameDisplay == nil { + s.LockTeammateNameDisplay = NewBool(false) + } +} + +type ClientRequirements struct { + AndroidLatestVersion string `access:"write_restrictable,cloud_restrictable"` + AndroidMinVersion string `access:"write_restrictable,cloud_restrictable"` + DesktopLatestVersion string `access:"write_restrictable,cloud_restrictable"` + DesktopMinVersion string `access:"write_restrictable,cloud_restrictable"` + IosLatestVersion string `access:"write_restrictable,cloud_restrictable"` + IosMinVersion string `access:"write_restrictable,cloud_restrictable"` +} + +type LdapSettings struct { + // Basic + Enable *bool `access:"authentication_ldap"` + EnableSync *bool `access:"authentication_ldap"` + LdapServer *string `access:"authentication_ldap"` // telemetry: none + LdapPort *int `access:"authentication_ldap"` // telemetry: none + ConnectionSecurity *string `access:"authentication_ldap"` + BaseDN *string `access:"authentication_ldap"` // telemetry: none + BindUsername *string `access:"authentication_ldap"` // telemetry: none + BindPassword *string `access:"authentication_ldap"` // telemetry: none + + // Filtering + UserFilter *string `access:"authentication_ldap"` // telemetry: none + GroupFilter *string `access:"authentication_ldap"` + GuestFilter *string `access:"authentication_ldap"` + EnableAdminFilter *bool + AdminFilter *string + + // Group Mapping + GroupDisplayNameAttribute *string `access:"authentication_ldap"` + GroupIdAttribute *string `access:"authentication_ldap"` + + // User Mapping + FirstNameAttribute *string `access:"authentication_ldap"` + LastNameAttribute *string `access:"authentication_ldap"` + EmailAttribute *string `access:"authentication_ldap"` + UsernameAttribute *string `access:"authentication_ldap"` + NicknameAttribute *string `access:"authentication_ldap"` + IdAttribute *string `access:"authentication_ldap"` + PositionAttribute *string `access:"authentication_ldap"` + LoginIdAttribute *string `access:"authentication_ldap"` + PictureAttribute *string `access:"authentication_ldap"` + + // Synchronization + SyncIntervalMinutes *int `access:"authentication_ldap"` + + // Advanced + SkipCertificateVerification *bool `access:"authentication_ldap"` + PublicCertificateFile *string `access:"authentication_ldap"` + PrivateKeyFile *string `access:"authentication_ldap"` + QueryTimeout *int `access:"authentication_ldap"` + MaxPageSize *int `access:"authentication_ldap"` + + // Customization + LoginFieldName *string `access:"authentication_ldap"` + + LoginButtonColor *string `access:"experimental_features"` + LoginButtonBorderColor *string `access:"experimental_features"` + LoginButtonTextColor *string `access:"experimental_features"` + + Trace *bool `access:"authentication_ldap"` // telemetry: none +} + +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.PublicCertificateFile == nil { + s.PublicCertificateFile = NewString("") + } + + if s.PrivateKeyFile == nil { + s.PrivateKeyFile = 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(LdapSettingsDefaultGroupDisplayNameAttribute) + } + + if s.GroupIdAttribute == nil { + s.GroupIdAttribute = NewString(LdapSettingsDefaultGroupIdAttribute) + } + + if s.FirstNameAttribute == nil { + s.FirstNameAttribute = NewString(LdapSettingsDefaultFirstNameAttribute) + } + + if s.LastNameAttribute == nil { + s.LastNameAttribute = NewString(LdapSettingsDefaultLastNameAttribute) + } + + if s.EmailAttribute == nil { + s.EmailAttribute = NewString(LdapSettingsDefaultEmailAttribute) + } + + if s.UsernameAttribute == nil { + s.UsernameAttribute = NewString(LdapSettingsDefaultUsernameAttribute) + } + + if s.NicknameAttribute == nil { + s.NicknameAttribute = NewString(LdapSettingsDefaultNicknameAttribute) + } + + if s.IdAttribute == nil { + s.IdAttribute = NewString(LdapSettingsDefaultIdAttribute) + } + + if s.PositionAttribute == nil { + s.PositionAttribute = NewString(LdapSettingsDefaultPositionAttribute) + } + + if s.PictureAttribute == nil { + s.PictureAttribute = NewString(LdapSettingsDefaultPictureAttribute) + } + + // 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(LdapSettingsDefaultLoginFieldName) + } + + 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 `access:"compliance_compliance_monitoring"` + Directory *string `access:"compliance_compliance_monitoring"` // telemetry: none + EnableDaily *bool `access:"compliance_compliance_monitoring"` + BatchSize *int `access:"compliance_compliance_monitoring"` // telemetry: none +} + +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) + } + + if s.BatchSize == nil { + s.BatchSize = NewInt(30000) + } +} + +type LocalizationSettings struct { + DefaultServerLocale *string `access:"site_localization"` + DefaultClientLocale *string `access:"site_localization"` + AvailableLocales *string `access:"site_localization"` +} + +func (s *LocalizationSettings) SetDefaults() { + if s.DefaultServerLocale == nil { + s.DefaultServerLocale = NewString(DefaultLocale) + } + + if s.DefaultClientLocale == nil { + s.DefaultClientLocale = NewString(DefaultLocale) + } + + if s.AvailableLocales == nil { + s.AvailableLocales = NewString("") + } +} + +type SamlSettings struct { + // Basic + Enable *bool `access:"authentication_saml"` + EnableSyncWithLdap *bool `access:"authentication_saml"` + EnableSyncWithLdapIncludeAuth *bool `access:"authentication_saml"` + IgnoreGuestsLdapSync *bool `access:"authentication_saml"` + + Verify *bool `access:"authentication_saml"` + Encrypt *bool `access:"authentication_saml"` + SignRequest *bool `access:"authentication_saml"` + + IdpURL *string `access:"authentication_saml"` // telemetry: none + IdpDescriptorURL *string `access:"authentication_saml"` // telemetry: none + IdpMetadataURL *string `access:"authentication_saml"` // telemetry: none + ServiceProviderIdentifier *string `access:"authentication_saml"` // telemetry: none + AssertionConsumerServiceURL *string `access:"authentication_saml"` // telemetry: none + + SignatureAlgorithm *string `access:"authentication_saml"` + CanonicalAlgorithm *string `access:"authentication_saml"` + + ScopingIDPProviderId *string `access:"authentication_saml"` + ScopingIDPName *string `access:"authentication_saml"` + + IdpCertificateFile *string `access:"authentication_saml"` // telemetry: none + PublicCertificateFile *string `access:"authentication_saml"` // telemetry: none + PrivateKeyFile *string `access:"authentication_saml"` // telemetry: none + + // User Mapping + IdAttribute *string `access:"authentication_saml"` + GuestAttribute *string `access:"authentication_saml"` + EnableAdminAttribute *bool + AdminAttribute *string + FirstNameAttribute *string `access:"authentication_saml"` + LastNameAttribute *string `access:"authentication_saml"` + EmailAttribute *string `access:"authentication_saml"` + UsernameAttribute *string `access:"authentication_saml"` + NicknameAttribute *string `access:"authentication_saml"` + LocaleAttribute *string `access:"authentication_saml"` + PositionAttribute *string `access:"authentication_saml"` + + LoginButtonText *string `access:"authentication_saml"` + + LoginButtonColor *string `access:"experimental_features"` + LoginButtonBorderColor *string `access:"experimental_features"` + LoginButtonTextColor *string `access:"experimental_features"` +} + +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.IgnoreGuestsLdapSync == nil { + s.IgnoreGuestsLdapSync = 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(SamlSettingsDefaultSignatureAlgorithm) + } + + if s.CanonicalAlgorithm == nil { + s.CanonicalAlgorithm = NewString(SamlSettingsDefaultCanonicalAlgorithm) + } + + 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(UserAuthServiceSamlText) + } + + if s.IdAttribute == nil { + s.IdAttribute = NewString(SamlSettingsDefaultIdAttribute) + } + + if s.GuestAttribute == nil { + s.GuestAttribute = NewString(SamlSettingsDefaultGuestAttribute) + } + if s.AdminAttribute == nil { + s.AdminAttribute = NewString(SamlSettingsDefaultAdminAttribute) + } + if s.FirstNameAttribute == nil { + s.FirstNameAttribute = NewString(SamlSettingsDefaultFirstNameAttribute) + } + + if s.LastNameAttribute == nil { + s.LastNameAttribute = NewString(SamlSettingsDefaultLastNameAttribute) + } + + if s.EmailAttribute == nil { + s.EmailAttribute = NewString(SamlSettingsDefaultEmailAttribute) + } + + if s.UsernameAttribute == nil { + s.UsernameAttribute = NewString(SamlSettingsDefaultUsernameAttribute) + } + + if s.NicknameAttribute == nil { + s.NicknameAttribute = NewString(SamlSettingsDefaultNicknameAttribute) + } + + if s.PositionAttribute == nil { + s.PositionAttribute = NewString(SamlSettingsDefaultPositionAttribute) + } + + if s.LocaleAttribute == nil { + s.LocaleAttribute = NewString(SamlSettingsDefaultLocaleAttribute) + } + + 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 { + AppCustomURLSchemes []string `access:"site_customization,write_restrictable,cloud_restrictable"` // telemetry: none + AppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"` + AndroidAppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"` + IosAppDownloadLink *string `access:"site_customization,write_restrictable,cloud_restrictable"` +} + +func (s *NativeAppSettings) SetDefaults() { + if s.AppDownloadLink == nil { + s.AppDownloadLink = NewString(NativeappSettingsDefaultAppDownloadLink) + } + + if s.AndroidAppDownloadLink == nil { + s.AndroidAppDownloadLink = NewString(NativeappSettingsDefaultAndroidAppDownloadLink) + } + + if s.IosAppDownloadLink == nil { + s.IosAppDownloadLink = NewString(NativeappSettingsDefaultIosAppDownloadLink) + } + + if s.AppCustomURLSchemes == nil { + s.AppCustomURLSchemes = GetDefaultAppCustomURLSchemes() + } +} + +type ElasticsearchSettings struct { + ConnectionURL *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + Username *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + Password *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + EnableIndexing *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + EnableSearching *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + EnableAutocomplete *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + Sniff *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + PostIndexReplicas *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + PostIndexShards *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + ChannelIndexReplicas *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + ChannelIndexShards *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + UserIndexReplicas *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + UserIndexShards *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + AggregatePostsAfterDays *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` // telemetry: none + PostsAggregatorJobStartTime *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` // telemetry: none + IndexPrefix *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + LiveIndexingBatchSize *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + BulkIndexingTimeWindowSeconds *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + RequestTimeoutSeconds *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + SkipTLSVerification *bool `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + Trace *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` +} + +func (s *ElasticsearchSettings) SetDefaults() { + if s.ConnectionURL == nil { + s.ConnectionURL = NewString(ElasticsearchSettingsDefaultConnectionURL) + } + + if s.Username == nil { + s.Username = NewString(ElasticsearchSettingsDefaultUsername) + } + + if s.Password == nil { + s.Password = NewString(ElasticsearchSettingsDefaultPassword) + } + + 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(ElasticsearchSettingsDefaultPostIndexReplicas) + } + + if s.PostIndexShards == nil { + s.PostIndexShards = NewInt(ElasticsearchSettingsDefaultPostIndexShards) + } + + if s.ChannelIndexReplicas == nil { + s.ChannelIndexReplicas = NewInt(ElasticsearchSettingsDefaultChannelIndexReplicas) + } + + if s.ChannelIndexShards == nil { + s.ChannelIndexShards = NewInt(ElasticsearchSettingsDefaultChannelIndexShards) + } + + if s.UserIndexReplicas == nil { + s.UserIndexReplicas = NewInt(ElasticsearchSettingsDefaultUserIndexReplicas) + } + + if s.UserIndexShards == nil { + s.UserIndexShards = NewInt(ElasticsearchSettingsDefaultUserIndexShards) + } + + if s.AggregatePostsAfterDays == nil { + s.AggregatePostsAfterDays = NewInt(ElasticsearchSettingsDefaultAggregatePostsAfterDays) + } + + if s.PostsAggregatorJobStartTime == nil { + s.PostsAggregatorJobStartTime = NewString(ElasticsearchSettingsDefaultPostsAggregatorJobStartTime) + } + + if s.IndexPrefix == nil { + s.IndexPrefix = NewString(ElasticsearchSettingsDefaultIndexPrefix) + } + + if s.LiveIndexingBatchSize == nil { + s.LiveIndexingBatchSize = NewInt(ElasticsearchSettingsDefaultLiveIndexingBatchSize) + } + + if s.BulkIndexingTimeWindowSeconds == nil { + s.BulkIndexingTimeWindowSeconds = NewInt(ElasticsearchSettingsDefaultBulkIndexingTimeWindowSeconds) + } + + if s.RequestTimeoutSeconds == nil { + s.RequestTimeoutSeconds = NewInt(ElasticsearchSettingsDefaultRequestTimeoutSeconds) + } + + if s.SkipTLSVerification == nil { + s.SkipTLSVerification = NewBool(false) + } + + if s.Trace == nil { + s.Trace = NewString("") + } +} + +type BleveSettings struct { + IndexDir *string `access:"experimental_bleve"` // telemetry: none + EnableIndexing *bool `access:"experimental_bleve"` + EnableSearching *bool `access:"experimental_bleve"` + EnableAutocomplete *bool `access:"experimental_bleve"` + BulkIndexingTimeWindowSeconds *int `access:"experimental_bleve"` +} + +func (bs *BleveSettings) SetDefaults() { + if bs.IndexDir == nil { + bs.IndexDir = NewString(BleveSettingsDefaultIndexDir) + } + + 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(BleveSettingsDefaultBulkIndexingTimeWindowSeconds) + } +} + +type DataRetentionSettings struct { + EnableMessageDeletion *bool `access:"compliance_data_retention_policy"` + EnableFileDeletion *bool `access:"compliance_data_retention_policy"` + MessageRetentionDays *int `access:"compliance_data_retention_policy"` + FileRetentionDays *int `access:"compliance_data_retention_policy"` + DeletionJobStartTime *string `access:"compliance_data_retention_policy"` + BatchSize *int `access:"compliance_data_retention_policy"` +} + +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(DataRetentionSettingsDefaultMessageRetentionDays) + } + + if s.FileRetentionDays == nil { + s.FileRetentionDays = NewInt(DataRetentionSettingsDefaultFileRetentionDays) + } + + if s.DeletionJobStartTime == nil { + s.DeletionJobStartTime = NewString(DataRetentionSettingsDefaultDeletionJobStartTime) + } + + if s.BatchSize == nil { + s.BatchSize = NewInt(DataRetentionSettingsDefaultBatchSize) + } +} + +type JobSettings struct { + RunJobs *bool `access:"write_restrictable,cloud_restrictable"` + RunScheduler *bool `access:"write_restrictable,cloud_restrictable"` +} + +func (s *JobSettings) SetDefaults() { + if s.RunJobs == nil { + s.RunJobs = NewBool(true) + } + + if s.RunScheduler == nil { + s.RunScheduler = NewBool(true) + } +} + +type CloudSettings struct { + CWSURL *string `access:"write_restrictable"` + CWSAPIURL *string `access:"write_restrictable"` +} + +func (s *CloudSettings) SetDefaults() { + if s.CWSURL == nil { + s.CWSURL = NewString(CloudSettingsDefaultCwsURL) + } + if s.CWSAPIURL == nil { + s.CWSAPIURL = NewString(CloudSettingsDefaultCwsAPIURL) + } +} + +type PluginState struct { + Enable bool +} + +type PluginSettings struct { + Enable *bool `access:"plugins,write_restrictable"` + EnableUploads *bool `access:"plugins,write_restrictable,cloud_restrictable"` + AllowInsecureDownloadURL *bool `access:"plugins,write_restrictable,cloud_restrictable"` + EnableHealthCheck *bool `access:"plugins,write_restrictable,cloud_restrictable"` + Directory *string `access:"plugins,write_restrictable,cloud_restrictable"` // telemetry: none + ClientDirectory *string `access:"plugins,write_restrictable,cloud_restrictable"` // telemetry: none + Plugins map[string]map[string]interface{} `access:"plugins"` // telemetry: none + PluginStates map[string]*PluginState `access:"plugins"` // telemetry: none + EnableMarketplace *bool `access:"plugins,write_restrictable,cloud_restrictable"` + EnableRemoteMarketplace *bool `access:"plugins,write_restrictable,cloud_restrictable"` + AutomaticPrepackagedPlugins *bool `access:"plugins,write_restrictable,cloud_restrictable"` + RequirePluginSignature *bool `access:"plugins,write_restrictable,cloud_restrictable"` + MarketplaceURL *string `access:"plugins,write_restrictable,cloud_restrictable"` + SignaturePublicKeyFiles []string `access:"plugins,write_restrictable,cloud_restrictable"` + ChimeraOAuthProxyURL *string `access:"plugins,write_restrictable,cloud_restrictable"` +} + +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(PluginSettingsDefaultDirectory) + } + + if s.ClientDirectory == nil || *s.ClientDirectory == "" { + s.ClientDirectory = NewString(PluginSettingsDefaultClientDirectory) + } + + 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.PluginStates["playbooks"] == nil { + // Enable the playbooks plugin by default + s.PluginStates["playbooks"] = &PluginState{Enable: true} + } + + if s.PluginStates["com.mattermost.plugin-channel-export"] == nil && BuildEnterpriseReady == "true" { + // Enable the channel export plugin by default + s.PluginStates["com.mattermost.plugin-channel-export"] = &PluginState{Enable: true} + } + + if s.PluginStates["focalboard"] == nil { + // Enable the focalboard plugin by default + s.PluginStates["focalboard"] = &PluginState{Enable: true} + } + + if s.EnableMarketplace == nil { + s.EnableMarketplace = NewBool(PluginSettingsDefaultEnableMarketplace) + } + + if s.EnableRemoteMarketplace == nil { + s.EnableRemoteMarketplace = NewBool(true) + } + + if s.AutomaticPrepackagedPlugins == nil { + s.AutomaticPrepackagedPlugins = NewBool(true) + } + + if s.MarketplaceURL == nil || *s.MarketplaceURL == "" || *s.MarketplaceURL == PluginSettingsOldMarketplaceURL { + s.MarketplaceURL = NewString(PluginSettingsDefaultMarketplaceURL) + } + + if s.RequirePluginSignature == nil { + s.RequirePluginSignature = NewBool(false) + } + + if s.SignaturePublicKeyFiles == nil { + s.SignaturePublicKeyFiles = []string{} + } + + if s.ChimeraOAuthProxyURL == nil { + s.ChimeraOAuthProxyURL = NewString("") + } +} + +type GlobalRelayMessageExportSettings struct { + CustomerType *string `access:"compliance_compliance_export"` // must be either A9 or A10, dictates SMTP server url + SMTPUsername *string `access:"compliance_compliance_export"` + SMTPPassword *string `access:"compliance_compliance_export"` + EmailAddress *string `access:"compliance_compliance_export"` // the address to send messages to + SMTPServerTimeout *int `access:"compliance_compliance_export"` +} + +func (s *GlobalRelayMessageExportSettings) SetDefaults() { + if s.CustomerType == nil { + s.CustomerType = NewString(GlobalrelayCustomerTypeA9) + } + if s.SMTPUsername == nil { + s.SMTPUsername = NewString("") + } + if s.SMTPPassword == nil { + s.SMTPPassword = NewString("") + } + if s.EmailAddress == nil { + s.EmailAddress = NewString("") + } + if s.SMTPServerTimeout == nil || *s.SMTPServerTimeout == 0 { + s.SMTPServerTimeout = NewInt(1800) + } +} + +type MessageExportSettings struct { + EnableExport *bool `access:"compliance_compliance_export"` + ExportFormat *string `access:"compliance_compliance_export"` + DailyRunTime *string `access:"compliance_compliance_export"` + ExportFromTimestamp *int64 `access:"compliance_compliance_export"` + BatchSize *int `access:"compliance_compliance_export"` + DownloadExportResults *bool `access:"compliance_compliance_export"` + + // formatter-specific settings - these are only expected to be non-nil if ExportFormat is set to the associated format + GlobalRelaySettings *GlobalRelayMessageExportSettings `access:"compliance_compliance_export"` +} + +func (s *MessageExportSettings) SetDefaults() { + if s.EnableExport == nil { + s.EnableExport = NewBool(false) + } + + if s.DownloadExportResults == nil { + s.DownloadExportResults = NewBool(false) + } + + if s.ExportFormat == nil { + s.ExportFormat = NewString(ComplianceExportTypeActiance) + } + + 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 `access:"site_customization"` + ExperimentalTimezone *bool `access:"experimental_features"` +} + +func (s *DisplaySettings) SetDefaults() { + if s.CustomURLSchemes == nil { + customURLSchemes := []string{} + s.CustomURLSchemes = customURLSchemes + } + + if s.ExperimentalTimezone == nil { + s.ExperimentalTimezone = NewBool(true) + } +} + +type GuestAccountsSettings struct { + Enable *bool `access:"authentication_guest_access"` + AllowEmailAccounts *bool `access:"authentication_guest_access"` + EnforceMultifactorAuthentication *bool `access:"authentication_guest_access"` + RestrictCreationToDomains *string `access:"authentication_guest_access"` +} + +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 `access:"environment_image_proxy"` + ImageProxyType *string `access:"environment_image_proxy"` + RemoteImageProxyURL *string `access:"environment_image_proxy"` + RemoteImageProxyOptions *string `access:"environment_image_proxy"` +} + +func (s *ImageProxySettings) SetDefaults() { + if s.Enable == nil { + s.Enable = NewBool(false) + } + + if s.ImageProxyType == nil { + s.ImageProxyType = NewString(ImageProxyTypeLocal) + } + + if s.RemoteImageProxyURL == nil { + s.RemoteImageProxyURL = NewString("") + } + + if s.RemoteImageProxyOptions == nil { + s.RemoteImageProxyOptions = NewString("") + } +} + +// ImportSettings defines configuration settings for file imports. +type ImportSettings struct { + // The directory where to store the imported files. + Directory *string + // The number of days to retain the imported files before deleting them. + RetentionDays *int +} + +func (s *ImportSettings) isValid() *AppError { + if *s.Directory == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.import.directory.app_error", nil, "", http.StatusBadRequest) + } + + if *s.RetentionDays <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.import.retention_days_too_low.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +// SetDefaults applies the default settings to the struct. +func (s *ImportSettings) SetDefaults() { + if s.Directory == nil || *s.Directory == "" { + s.Directory = NewString(ImportSettingsDefaultDirectory) + } + + if s.RetentionDays == nil { + s.RetentionDays = NewInt(ImportSettingsDefaultRetentionDays) + } +} + +// ExportSettings defines configuration settings for file exports. +type ExportSettings struct { + // The directory where to store the exported files. + Directory *string // telemetry: none + // The number of days to retain the exported files before deleting them. + RetentionDays *int +} + +func (s *ExportSettings) isValid() *AppError { + if *s.Directory == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.export.directory.app_error", nil, "", http.StatusBadRequest) + } + + if *s.RetentionDays <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.export.retention_days_too_low.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +// SetDefaults applies the default settings to the struct. +func (s *ExportSettings) SetDefaults() { + if s.Directory == nil || *s.Directory == "" { + s.Directory = NewString(ExportSettingsDefaultDirectory) + } + + if s.RetentionDays == nil { + s.RetentionDays = NewInt(ExportSettingsDefaultRetentionDays) + } +} + +type ConfigFunc func() *Config + +const ConfigAccessTagType = "access" +const ConfigAccessTagWriteRestrictable = "write_restrictable" +const ConfigAccessTagCloudRestrictable = "cloud_restrictable" + +// Allows read access if any PermissionSysconsoleRead* is allowed +const ConfigAccessTagAnySysConsoleRead = "*_read" + +// Config fields support the 'access' tag with the following values corresponding to the suffix of the associated +// PermissionSysconsole* permission Id: 'about', 'reporting', 'user_management_users', +// 'user_management_groups', 'user_management_teams', 'user_management_channels', +// 'user_management_permissions', 'environment_web_server', 'environment_database', 'environment_elasticsearch', +// 'environment_file_storage', 'environment_image_proxy', 'environment_smtp', 'environment_push_notification_server', +// 'environment_high_availability', 'environment_rate_limiting', 'environment_logging', 'environment_session_lengths', +// 'environment_performance_monitoring', 'environment_developer', 'site', 'authentication', 'plugins', +// 'integrations', 'compliance', 'plugins', and 'experimental'. They grant read and/or write access to the config field +// to roles without PermissionManageSystem. +// +// The 'access' tag '*_read' checks for any Sysconsole read permission and grants access if any read permission is allowed. +// +// By default config values can be written with PermissionManageSystem, but if ExperimentalSettings.RestrictSystemAdmin is true +// and the access tag contains the value 'write_restrictable', then even PermissionManageSystem, does not grant write access. +// +// PermissionManageSystem always grants read access. +// +// Config values with the access tag 'cloud_restrictable' mean that are marked to be filtered when it's used in a cloud licensed +// environment with ExperimentalSettings.RestrictedSystemAdmin set to true. +// +// Example: +// type HairSettings struct { +// // Colour is writeable with either PermissionSysconsoleWriteReporting or PermissionSysconsoleWriteUserManagementGroups. +// // It is readable by PermissionSysconsoleReadReporting and PermissionSysconsoleReadUserManagementGroups permissions. +// // PermissionManageSystem grants read and write access. +// Colour string `access:"reporting,user_management_groups"` +// +// // Length is only readable and writable via PermissionManageSystem. +// Length string +// +// // Product is only writeable by PermissionManageSystem if ExperimentalSettings.RestrictSystemAdmin is false. +// // PermissionManageSystem can always read the value. +// Product bool `access:write_restrictable` +// } +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 + OpenIdSettings SSOSettings + 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 // telemetry: none + PluginSettings PluginSettings + DisplaySettings DisplaySettings + GuestAccountsSettings GuestAccountsSettings + ImageProxySettings ImageProxySettings + CloudSettings CloudSettings // telemetry: none + FeatureFlags *FeatureFlags `access:"*_read" json:",omitempty"` + ImportSettings ImportSettings // telemetry: none + ExportSettings ExportSettings +} + +func (o *Config) Clone() *Config { + buf, err := json.Marshal(o) + if err != nil { + panic(err) + } + var ret Config + err = json.Unmarshal(buf, &ret) + if err != nil { + panic(err) + } + return &ret +} + +func (o *Config) ToJSONFiltered(tagType, tagValue string) ([]byte, error) { + filteredConfigMap := structToMapFilteredByTag(*o, tagType, tagValue) + for key, value := range filteredConfigMap { + v, ok := value.(map[string]interface{}) + if ok && len(v) == 0 { + delete(filteredConfigMap, key) + } + } + return json.Marshal(filteredConfigMap) +} + +func (o *Config) GetSSOService(service string) *SSOSettings { + switch service { + case ServiceGitlab: + return &o.GitLabSettings + case ServiceGoogle: + return &o.GoogleSettings + case ServiceOffice365: + return o.Office365Settings.SSOSettings() + case ServiceOpenid: + return &o.OpenIdSettings + } + + 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(ShowUsername) + + if *o.SamlSettings.Enable || *o.LdapSettings.Enable { + *o.TeamSettings.TeammateNameDisplay = ShowFullName + } + } + + o.SqlSettings.SetDefaults(isUpdate) + o.FileSettings.SetDefaults(isUpdate) + o.EmailSettings.SetDefaults(isUpdate) + o.PrivacySettings.setDefaults() + o.Office365Settings.setDefaults() + o.Office365Settings.setDefaults() + o.GitLabSettings.setDefaults("", "", "", "", "") + o.GoogleSettings.setDefaults(GoogleSettingsDefaultScope, GoogleSettingsDefaultAuthEndpoint, GoogleSettingsDefaultTokenEndpoint, GoogleSettingsDefaultUserAPIEndpoint, "") + o.OpenIdSettings.setDefaults(OpenidSettingsDefaultScope, "", "", "", "#145DBF") + 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.CloudSettings.SetDefaults() + if o.FeatureFlags == nil { + o.FeatureFlags = &FeatureFlags{} + o.FeatureFlags.SetDefaults() + } + o.ImportSettings.SetDefaults() + o.ExportSettings.SetDefaults() +} + +func (o *Config) IsValid() *AppError { + if *o.ServiceSettings.SiteURL == "" && *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 *o.ServiceSettings.SiteURL == "" && *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 < PasswordMinimumLength || *o.PasswordSettings.MinimumLength > PasswordMaximumLength { + return NewAppError("Config.IsValid", "model.config.is_valid.password_length.app_error", map[string]interface{}{"MinLength": PasswordMinimumLength, "MaxLength": PasswordMaximumLength}, "", 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(); err != nil { + return err + } + + if err := o.DisplaySettings.isValid(); err != nil { + return err + } + + if err := o.ImageProxySettings.isValid(); err != nil { + return err + } + + if err := o.ImportSettings.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 == DirectMessageAny || *s.RestrictDirectMessage == DirectMessageTeam) { + return NewAppError("Config.IsValid", "model.config.is_valid.restrict_direct_message.app_error", nil, "", http.StatusBadRequest) + } + + if !(*s.TeammateNameDisplay == ShowFullName || *s.TeammateNameDisplay == ShowNicknameFullName || *s.TeammateNameDisplay == ShowUsername) { + return NewAppError("Config.IsValid", "model.config.is_valid.teammate_name_display.app_error", nil, "", http.StatusBadRequest) + } + + if len(*s.SiteName) > SitenameMaxLength { + return NewAppError("Config.IsValid", "model.config.is_valid.sitename_length.app_error", map[string]interface{}{"MaxLength": SitenameMaxLength}, "", 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 == DatabaseDriverMysql || *s.DriverName == DatabaseDriverPostgres) { + 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.ConnMaxIdleTimeMilliseconds < 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.sql_conn_max_idle_time_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 *s.DataSource == "" { + 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 == ImageDriverLocal || *s.DriverName == ImageDriverS3) { + 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) + } + + if *s.Directory == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.directory.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (s *EmailSettings) isValid() *AppError { + if !(*s.ConnectionSecurity == ConnSecurityNone || *s.ConnectionSecurity == ConnSecurityTLS || *s.ConnectionSecurity == ConnSecurityStarttls || *s.ConnectionSecurity == ConnSecurityPlain) { + 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 == EmailNotificationContentsFull || *s.EmailNotificationContentsType == EmailNotificationContentsGeneric) { + 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 == ConnSecurityNone || *s.ConnectionSecurity == ConnSecurityTLS || *s.ConnectionSecurity == ConnSecurityStarttls) { + 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 *s.IdpURL == "" || !IsValidHTTPURL(*s.IdpURL) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_idp_url.app_error", nil, "", http.StatusBadRequest) + } + + if *s.IdpDescriptorURL == "" || !IsValidHTTPURL(*s.IdpDescriptorURL) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_idp_descriptor_url.app_error", nil, "", http.StatusBadRequest) + } + + if *s.IdpCertificateFile == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_idp_cert.app_error", nil, "", http.StatusBadRequest) + } + + if *s.EmailAttribute == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "", http.StatusBadRequest) + } + + if *s.UsernameAttribute == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_username_attribute.app_error", nil, "", http.StatusBadRequest) + } + + if *s.ServiceProviderIdentifier == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_spidentifier_attribute.app_error", nil, "", http.StatusBadRequest) + } + + if *s.Verify { + if *s.AssertionConsumerServiceURL == "" || !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 *s.PrivateKeyFile == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_private_key.app_error", nil, "", http.StatusBadRequest) + } + + if *s.PublicCertificateFile == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_public_cert.app_error", nil, "", http.StatusBadRequest) + } + } + + if *s.EmailAttribute == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "", http.StatusBadRequest) + } + + if !(*s.SignatureAlgorithm == SamlSettingsSignatureAlgorithmSha1 || *s.SignatureAlgorithm == SamlSettingsSignatureAlgorithmSha256 || *s.SignatureAlgorithm == SamlSettingsSignatureAlgorithmSha512) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_signature_algorithm.app_error", nil, "", http.StatusBadRequest) + } + if !(*s.CanonicalAlgorithm == SamlSettingsCanonicalAlgorithmC14n || *s.CanonicalAlgorithm == SamlSettingsCanonicalAlgorithmC14n11) { + return NewAppError("Config.IsValid", "model.config.is_valid.saml_canonical_algorithm.app_error", nil, "", http.StatusBadRequest) + } + + if *s.GuestAttribute != "" { + 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 *s.AdminAttribute != "" { + 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 == ConnSecurityNone || *s.ConnectionSecurity == ConnSecurityTLS) { + return NewAppError("Config.IsValid", "model.config.is_valid.webserver_security.app_error", nil, "", http.StatusBadRequest) + } + + if *s.ConnectionSecurity == ConnSecurityTLS && !*s.UseLetsEncrypt { + appErr := NewAppError("Config.IsValid", "model.config.is_valid.tls_cert_file_missing.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_missing.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 *s.SiteURL != "" { + if _, err := url.ParseRequestURI(*s.SiteURL); err != nil { + return NewAppError("Config.IsValid", "model.config.is_valid.site_url.app_error", nil, "", http.StatusBadRequest) + } + } + + if *s.WebsocketURL != "" { + 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 != GroupUnreadChannelsDisabled && + *s.ExperimentalGroupUnreadChannels != GroupUnreadChannelsDefaultOn && + *s.ExperimentalGroupUnreadChannels != GroupUnreadChannelsDefaultOff { + return NewAppError("Config.IsValid", "model.config.is_valid.group_unread_channels.app_error", nil, "", http.StatusBadRequest) + } + + if *s.CollapsedThreads != CollapsedThreadsDisabled && !*s.ThreadAutoFollow { + return NewAppError("Config.IsValid", "model.config.is_valid.collapsed_threads.autofollow.app_error", nil, "", http.StatusBadRequest) + } + + if *s.CollapsedThreads != CollapsedThreadsDisabled && + *s.CollapsedThreads != CollapsedThreadsDefaultOn && + *s.CollapsedThreads != CollapsedThreadsDefaultOff { + return NewAppError("Config.IsValid", "model.config.is_valid.collapsed_threads.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (s *ElasticsearchSettings) isValid() *AppError { + if *s.EnableIndexing { + if *s.ConnectionURL == "" { + 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 *bs.IndexDir == "" { + 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 *s.AvailableLocales != "" { + 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() *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 != ComplianceExportTypeActiance && *s.ExportFormat != ComplianceExportTypeGlobalrelay && *s.ExportFormat != ComplianceExportTypeCsv) { + return NewAppError("Config.IsValid", "model.config.is_valid.message_export.export_type.app_error", nil, "", http.StatusBadRequest) + } + + if *s.ExportFormat == ComplianceExportTypeGlobalrelay { + 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 != GlobalrelayCustomerTypeA9 && *s.GlobalRelaySettings.CustomerType != GlobalrelayCustomerTypeA10) { + 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 ImageProxyTypeLocal: + // No other settings to validate + case ImageProxyTypeAtmosCamo: + 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 && *o.LdapSettings.BindPassword != "" { + *o.LdapSettings.BindPassword = FakeSetting + } + + *o.FileSettings.PublicLinkSalt = FakeSetting + + if *o.FileSettings.AmazonS3SecretAccessKey != "" { + *o.FileSettings.AmazonS3SecretAccessKey = FakeSetting + } + + if o.EmailSettings.SMTPPassword != nil && *o.EmailSettings.SMTPPassword != "" { + *o.EmailSettings.SMTPPassword = FakeSetting + } + + if *o.GitLabSettings.Secret != "" { + *o.GitLabSettings.Secret = FakeSetting + } + + if o.GoogleSettings.Secret != nil && *o.GoogleSettings.Secret != "" { + *o.GoogleSettings.Secret = FakeSetting + } + + if o.Office365Settings.Secret != nil && *o.Office365Settings.Secret != "" { + *o.Office365Settings.Secret = FakeSetting + } + + if o.OpenIdSettings.Secret != nil && *o.OpenIdSettings.Secret != "" { + *o.OpenIdSettings.Secret = FakeSetting + } + + *o.SqlSettings.DataSource = FakeSetting + *o.SqlSettings.AtRestEncryptKey = FakeSetting + + *o.ElasticsearchSettings.Password = FakeSetting + + for i := range o.SqlSettings.DataSourceReplicas { + o.SqlSettings.DataSourceReplicas[i] = FakeSetting + } + + for i := range o.SqlSettings.DataSourceSearchReplicas { + o.SqlSettings.DataSourceSearchReplicas[i] = FakeSetting + } + + if o.MessageExportSettings.GlobalRelaySettings.SMTPPassword != nil && *o.MessageExportSettings.GlobalRelaySettings.SMTPPassword != "" { + *o.MessageExportSettings.GlobalRelaySettings.SMTPPassword = FakeSetting + } + + if o.ServiceSettings.GfycatAPISecret != nil && *o.ServiceSettings.GfycatAPISecret != "" { + *o.ServiceSettings.GfycatAPISecret = FakeSetting + } + + *o.ServiceSettings.SplitKey = FakeSetting +} + +// structToMapFilteredByTag converts a struct into a map removing those fields that has the tag passed +// as argument +func structToMapFilteredByTag(t interface{}, typeOfTag, filterTag string) map[string]interface{} { + defer func() { + if r := recover(); r != nil { + mlog.Warn("Panicked in structToMapFilteredByTag. This should never happen.", mlog.Any("recover", r)) + } + }() + + val := reflect.ValueOf(t) + elemField := reflect.TypeOf(t) + + if val.Kind() != reflect.Struct { + return nil + } + + out := map[string]interface{}{} + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + + structField := elemField.Field(i) + tagPermissions := strings.Split(structField.Tag.Get(typeOfTag), ",") + if isTagPresent(filterTag, tagPermissions) { + continue + } + + var value interface{} + + switch field.Kind() { + case reflect.Struct: + value = structToMapFilteredByTag(field.Interface(), typeOfTag, filterTag) + case reflect.Ptr: + indirectType := field.Elem() + if indirectType.Kind() == reflect.Struct { + value = structToMapFilteredByTag(indirectType.Interface(), typeOfTag, filterTag) + } else if indirectType.Kind() != reflect.Invalid { + value = indirectType.Interface() + } + default: + value = field.Interface() + } + + out[val.Type().Field(i).Name] = value + } + + return out +} + +func isTagPresent(tag string, tags []string) bool { + for _, val := range tags { + tagValue := strings.TrimSpace(val) + if tagValue != "" && tagValue == tag { + return true + } + } + + return false +} + +// 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 isSafeLink(link *string) bool { + if link != nil { + if IsValidHTTPURL(*link) { + return true + } else if strings.HasPrefix(*link, "/") { + return true + } else { + return false + } + } + + return true +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/custom_status.go b/vendor/github.com/mattermost/mattermost-server/v6/model/custom_status.go new file mode 100644 index 00000000..2f5084b8 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/custom_status.go @@ -0,0 +1,140 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "bytes" + "encoding/json" + "fmt" + "time" +) + +const ( + UserPropsKeyCustomStatus = "customStatus" + + CustomStatusTextMaxRunes = 100 + MaxRecentCustomStatuses = 5 + DefaultCustomStatusEmoji = "speech_balloon" +) + +var validCustomStatusDuration = map[string]bool{ + "thirty_minutes": true, + "one_hour": true, + "four_hours": true, + "today": true, + "this_week": true, + "date_and_time": true, +} + +type CustomStatus struct { + Emoji string `json:"emoji"` + Text string `json:"text"` + Duration string `json:"duration"` + ExpiresAt time.Time `json:"expires_at"` +} + +func (cs *CustomStatus) PreSave() { + if cs.Emoji == "" { + cs.Emoji = DefaultCustomStatusEmoji + } + + if cs.Duration == "" && !cs.ExpiresAt.Before(time.Now()) { + cs.Duration = "date_and_time" + } + + runes := []rune(cs.Text) + if len(runes) > CustomStatusTextMaxRunes { + cs.Text = string(runes[:CustomStatusTextMaxRunes]) + } +} + +func (cs *CustomStatus) AreDurationAndExpirationTimeValid() bool { + if cs.Duration == "" && (cs.ExpiresAt.IsZero() || !cs.ExpiresAt.Before(time.Now())) { + return true + } + + if validCustomStatusDuration[cs.Duration] && !cs.ExpiresAt.Before(time.Now()) { + return true + } + + return false +} + +func RuneToHexadecimalString(r rune) string { + return fmt.Sprintf("%04x", r) +} + +type RecentCustomStatuses []CustomStatus + +func (rcs RecentCustomStatuses) Contains(cs *CustomStatus) (bool, error) { + if cs == nil { + return false, nil + } + + csJSON, jsonErr := json.Marshal(cs) + if jsonErr != nil { + return false, jsonErr + } + + // status is empty + if len(csJSON) == 0 || (cs.Emoji == "" && cs.Text == "") { + return false, nil + } + + for _, status := range rcs { + js, jsonErr := json.Marshal(status) + if jsonErr != nil { + return false, jsonErr + } + if bytes.Equal(js, csJSON) { + return true, nil + } + } + + return false, nil +} + +func (rcs RecentCustomStatuses) Add(cs *CustomStatus) RecentCustomStatuses { + newRCS := rcs[:0] + + // if same `text` exists in existing recent custom statuses, modify existing status + for _, status := range rcs { + if status.Text != cs.Text { + newRCS = append(newRCS, status) + } + } + newRCS = append(RecentCustomStatuses{*cs}, newRCS...) + if len(newRCS) > MaxRecentCustomStatuses { + newRCS = newRCS[:MaxRecentCustomStatuses] + } + return newRCS +} + +func (rcs RecentCustomStatuses) Remove(cs *CustomStatus) (RecentCustomStatuses, error) { + if cs == nil { + return rcs, nil + } + + csJSON, jsonErr := json.Marshal(cs) + if jsonErr != nil { + return rcs, jsonErr + } + + if len(csJSON) == 0 || (cs.Emoji == "" && cs.Text == "") { + return rcs, nil + } + + newRCS := rcs[:0] + for _, status := range rcs { + js, jsonErr := json.Marshal(status) + if jsonErr != nil { + return rcs, jsonErr + } + if !bytes.Equal(js, csJSON) { + newRCS = append(newRCS, status) + } + } + + return newRCS, nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/data_retention_policy.go b/vendor/github.com/mattermost/mattermost-server/v6/model/data_retention_policy.go new file mode 100644 index 00000000..0a1d318e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/data_retention_policy.go @@ -0,0 +1,70 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type GlobalRetentionPolicy 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"` +} + +type RetentionPolicy struct { + ID string `db:"Id" json:"id"` + DisplayName string `json:"display_name"` + PostDuration *int64 `json:"post_duration"` +} + +type RetentionPolicyWithTeamAndChannelIDs struct { + RetentionPolicy + TeamIDs []string `json:"team_ids"` + ChannelIDs []string `json:"channel_ids"` +} + +type RetentionPolicyWithTeamAndChannelCounts struct { + RetentionPolicy + ChannelCount int64 `json:"channel_count"` + TeamCount int64 `json:"team_count"` +} + +type RetentionPolicyChannel struct { + PolicyID string `db:"PolicyId"` + ChannelID string `db:"ChannelId"` +} + +type RetentionPolicyTeam struct { + PolicyID string `db:"PolicyId"` + TeamID string `db:"TeamId"` +} + +type RetentionPolicyWithTeamAndChannelCountsList struct { + Policies []*RetentionPolicyWithTeamAndChannelCounts `json:"policies"` + TotalCount int64 `json:"total_count"` +} + +type RetentionPolicyForTeam struct { + TeamID string `db:"Id" json:"team_id"` + PostDuration int64 `json:"post_duration"` +} + +type RetentionPolicyForTeamList struct { + Policies []*RetentionPolicyForTeam `json:"policies"` + TotalCount int64 `json:"total_count"` +} + +type RetentionPolicyForChannel struct { + ChannelID string `db:"Id" json:"channel_id"` + PostDuration int64 `json:"post_duration"` +} + +type RetentionPolicyForChannelList struct { + Policies []*RetentionPolicyForChannel `json:"policies"` + TotalCount int64 `json:"total_count"` +} + +type RetentionPolicyCursor struct { + ChannelPoliciesDone bool + TeamPoliciesDone bool + GlobalPoliciesDone bool +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/emoji.go b/vendor/github.com/mattermost/mattermost-server/v6/model/emoji.go new file mode 100644 index 00000000..fd0e8ab3 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/emoji.go @@ -0,0 +1,95 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "regexp" + "sort" +) + +const ( + EmojiNameMaxLength = 64 + EmojiSortByName = "name" +) + +var EmojiPattern = 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 makeReverseEmojiMap() map[string][]string { + reverseEmojiMap := make(map[string][]string) + for key, value := range SystemEmojis { + emojiNames := reverseEmojiMap[value] + emojiNames = append(emojiNames, key) + sort.Strings(emojiNames) + reverseEmojiMap[value] = emojiNames + } + + return reverseEmojiMap +} + +var reverseSystemEmojisMap = makeReverseEmojiMap() + +func GetEmojiNameFromUnicode(unicode string) (emojiName string, count int) { + if emojiNames, found := reverseSystemEmojisMap[unicode]; found { + return emojiNames[0], len(emojiNames) + } + + return "", 0 +} + +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 name == "" || len(name) > EmojiNameMaxLength || !IsValidAlphaNumHyphenUnderscorePlus(name) || 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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/emoji_data.go b/vendor/github.com/mattermost/mattermost-server/v6/model/emoji_data.go new file mode 100644 index 00000000..849c2046 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/emoji_data.go @@ -0,0 +1,7 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +// This file is automatically generated via `make emojis`. Do not modify it manually. + +package model + +var SystemEmojis = map[string]string{"grinning": "1f600", "smiley": "1f603", "smile": "1f604", "grin": "1f601", "laughing": "1f606", "satisfied": "1f606", "sweat_smile": "1f605", "rolling_on_the_floor_laughing": "1f923", "rofl": "1f923", "joy": "1f602", "slightly_smiling_face": "1f642", "upside_down_face": "1f643", "wink": "1f609", "blush": "1f60a", "innocent": "1f607", "smiling_face_with_3_hearts": "1f970", "heart_eyes": "1f60d", "star-struck": "1f929", "grinning_face_with_star_eyes": "1f929", "kissing_heart": "1f618", "kissing": "1f617", "relaxed": "263a-fe0f", "kissing_closed_eyes": "1f61a", "kissing_smiling_eyes": "1f619", "smiling_face_with_tear": "1f972", "yum": "1f60b", "stuck_out_tongue": "1f61b", "stuck_out_tongue_winking_eye": "1f61c", "zany_face": "1f92a", "grinning_face_with_one_large_and_one_small_eye": "1f92a", "stuck_out_tongue_closed_eyes": "1f61d", "money_mouth_face": "1f911", "hugging_face": "1f917", "hugs": "1f917", "face_with_hand_over_mouth": "1f92d", "smiling_face_with_smiling_eyes_and_hand_covering_mouth": "1f92d", "shushing_face": "1f92b", "face_with_finger_covering_closed_lips": "1f92b", "thinking_face": "1f914", "thinking": "1f914", "zipper_mouth_face": "1f910", "face_with_raised_eyebrow": "1f928", "face_with_one_eyebrow_raised": "1f928", "neutral_face": "1f610", "expressionless": "1f611", "no_mouth": "1f636", "smirk": "1f60f", "unamused": "1f612", "face_with_rolling_eyes": "1f644", "roll_eyes": "1f644", "grimacing": "1f62c", "lying_face": "1f925", "relieved": "1f60c", "pensive": "1f614", "sleepy": "1f62a", "drooling_face": "1f924", "sleeping": "1f634", "mask": "1f637", "face_with_thermometer": "1f912", "face_with_head_bandage": "1f915", "nauseated_face": "1f922", "face_vomiting": "1f92e", "face_with_open_mouth_vomiting": "1f92e", "sneezing_face": "1f927", "hot_face": "1f975", "cold_face": "1f976", "woozy_face": "1f974", "dizzy_face": "1f635", "exploding_head": "1f92f", "shocked_face_with_exploding_head": "1f92f", "face_with_cowboy_hat": "1f920", "cowboy_hat_face": "1f920", "partying_face": "1f973", "disguised_face": "1f978", "sunglasses": "1f60e", "nerd_face": "1f913", "face_with_monocle": "1f9d0", "confused": "1f615", "worried": "1f61f", "slightly_frowning_face": "1f641", "white_frowning_face": "2639-fe0f", "frowning_face": "2639-fe0f", "open_mouth": "1f62e", "hushed": "1f62f", "astonished": "1f632", "flushed": "1f633", "pleading_face": "1f97a", "frowning": "1f626", "anguished": "1f627", "fearful": "1f628", "cold_sweat": "1f630", "disappointed_relieved": "1f625", "cry": "1f622", "sob": "1f62d", "scream": "1f631", "confounded": "1f616", "persevere": "1f623", "disappointed": "1f61e", "sweat": "1f613", "weary": "1f629", "tired_face": "1f62b", "yawning_face": "1f971", "triumph": "1f624", "rage": "1f621", "pout": "1f621", "angry": "1f620", "face_with_symbols_on_mouth": "1f92c", "serious_face_with_symbols_covering_mouth": "1f92c", "smiling_imp": "1f608", "imp": "1f47f", "skull": "1f480", "skull_and_crossbones": "2620-fe0f", "hankey": "1f4a9", "poop": "1f4a9", "shit": "1f4a9", "clown_face": "1f921", "japanese_ogre": "1f479", "japanese_goblin": "1f47a", "ghost": "1f47b", "alien": "1f47d", "space_invader": "1f47e", "robot_face": "1f916", "robot": "1f916", "smiley_cat": "1f63a", "smile_cat": "1f638", "joy_cat": "1f639", "heart_eyes_cat": "1f63b", "smirk_cat": "1f63c", "kissing_cat": "1f63d", "scream_cat": "1f640", "crying_cat_face": "1f63f", "pouting_cat": "1f63e", "see_no_evil": "1f648", "hear_no_evil": "1f649", "speak_no_evil": "1f64a", "kiss": "1f48b", "love_letter": "1f48c", "cupid": "1f498", "gift_heart": "1f49d", "sparkling_heart": "1f496", "heartpulse": "1f497", "heartbeat": "1f493", "revolving_hearts": "1f49e", "two_hearts": "1f495", "heart_decoration": "1f49f", "heavy_heart_exclamation_mark_ornament": "2763-fe0f", "heavy_heart_exclamation": "2763-fe0f", "broken_heart": "1f494", "heart": "2764-fe0f", "orange_heart": "1f9e1", "yellow_heart": "1f49b", "green_heart": "1f49a", "blue_heart": "1f499", "purple_heart": "1f49c", "brown_heart": "1f90e", "black_heart": "1f5a4", "white_heart": "1f90d", "100": "1f4af", "anger": "1f4a2", "boom": "1f4a5", "collision": "1f4a5", "dizzy": "1f4ab", "sweat_drops": "1f4a6", "dash": "1f4a8", "hole": "1f573-fe0f", "bomb": "1f4a3", "speech_balloon": "1f4ac", "eye-in-speech-bubble": "1f441-fe0f-200d-1f5e8-fe0f", "left_speech_bubble": "1f5e8-fe0f", "right_anger_bubble": "1f5ef-fe0f", "thought_balloon": "1f4ad", "zzz": "1f4a4", "wave": "1f44b", "raised_back_of_hand": "1f91a", "raised_hand_with_fingers_splayed": "1f590-fe0f", "hand": "270b", "raised_hand": "270b", "spock-hand": "1f596", "vulcan_salute": "1f596", "ok_hand": "1f44c", "pinched_fingers": "1f90c", "pinching_hand": "1f90f", "v": "270c-fe0f", "crossed_fingers": "1f91e", "hand_with_index_and_middle_fingers_crossed": "1f91e", "i_love_you_hand_sign": "1f91f", "the_horns": "1f918", "sign_of_the_horns": "1f918", "metal": "1f918", "call_me_hand": "1f919", "point_left": "1f448", "point_right": "1f449", "point_up_2": "1f446", "middle_finger": "1f595", "reversed_hand_with_middle_finger_extended": "1f595", "fu": "1f595", "point_down": "1f447", "point_up": "261d-fe0f", "+1": "1f44d", "thumbsup": "1f44d", "-1": "1f44e", "thumbsdown": "1f44e", "fist": "270a", "fist_raised": "270a", "facepunch": "1f44a", "punch": "1f44a", "fist_oncoming": "1f44a", "left-facing_fist": "1f91b", "fist_left": "1f91b", "right-facing_fist": "1f91c", "fist_right": "1f91c", "clap": "1f44f", "raised_hands": "1f64c", "open_hands": "1f450", "palms_up_together": "1f932", "handshake": "1f91d", "pray": "1f64f", "writing_hand": "270d-fe0f", "nail_care": "1f485", "selfie": "1f933", "muscle": "1f4aa", "mechanical_arm": "1f9be", "mechanical_leg": "1f9bf", "leg": "1f9b5", "foot": "1f9b6", "ear": "1f442", "ear_with_hearing_aid": "1f9bb", "nose": "1f443", "brain": "1f9e0", "anatomical_heart": "1fac0", "lungs": "1fac1", "tooth": "1f9b7", "bone": "1f9b4", "eyes": "1f440", "eye": "1f441-fe0f", "tongue": "1f445", "lips": "1f444", "baby": "1f476", "child": "1f9d2", "boy": "1f466", "girl": "1f467", "adult": "1f9d1", "person_with_blond_hair": "1f471", "man": "1f468", "bearded_person": "1f9d4", "red_haired_man": "1f468-200d-1f9b0", "curly_haired_man": "1f468-200d-1f9b1", "white_haired_man": "1f468-200d-1f9b3", "bald_man": "1f468-200d-1f9b2", "woman": "1f469", "red_haired_woman": "1f469-200d-1f9b0", "red_haired_person": "1f9d1-200d-1f9b0", "curly_haired_woman": "1f469-200d-1f9b1", "curly_haired_person": "1f9d1-200d-1f9b1", "white_haired_woman": "1f469-200d-1f9b3", "white_haired_person": "1f9d1-200d-1f9b3", "bald_woman": "1f469-200d-1f9b2", "bald_person": "1f9d1-200d-1f9b2", "blond-haired-woman": "1f471-200d-2640-fe0f", "blonde_woman": "1f471-200d-2640-fe0f", "blond-haired-man": "1f471-200d-2642-fe0f", "blonde_man": "1f471-200d-2642-fe0f", "older_adult": "1f9d3", "older_man": "1f474", "older_woman": "1f475", "person_frowning": "1f64d", "man-frowning": "1f64d-200d-2642-fe0f", "frowning_man": "1f64d-200d-2642-fe0f", "woman-frowning": "1f64d-200d-2640-fe0f", "frowning_woman": "1f64d-200d-2640-fe0f", "person_with_pouting_face": "1f64e", "man-pouting": "1f64e-200d-2642-fe0f", "pouting_man": "1f64e-200d-2642-fe0f", "woman-pouting": "1f64e-200d-2640-fe0f", "pouting_woman": "1f64e-200d-2640-fe0f", "no_good": "1f645", "man-gesturing-no": "1f645-200d-2642-fe0f", "ng_man": "1f645-200d-2642-fe0f", "no_good_man": "1f645-200d-2642-fe0f", "woman-gesturing-no": "1f645-200d-2640-fe0f", "no_good_woman": "1f645-200d-2640-fe0f", "ng_woman": "1f645-200d-2640-fe0f", "ok_woman": "1f646", "man-gesturing-ok": "1f646-200d-2642-fe0f", "ok_man": "1f646-200d-2642-fe0f", "woman-gesturing-ok": "1f646-200d-2640-fe0f", "information_desk_person": "1f481", "man-tipping-hand": "1f481-200d-2642-fe0f", "tipping_hand_man": "1f481-200d-2642-fe0f", "woman-tipping-hand": "1f481-200d-2640-fe0f", "tipping_hand_woman": "1f481-200d-2640-fe0f", "raising_hand": "1f64b", "man-raising-hand": "1f64b-200d-2642-fe0f", "raising_hand_man": "1f64b-200d-2642-fe0f", "woman-raising-hand": "1f64b-200d-2640-fe0f", "raising_hand_woman": "1f64b-200d-2640-fe0f", "deaf_person": "1f9cf", "deaf_man": "1f9cf-200d-2642-fe0f", "deaf_woman": "1f9cf-200d-2640-fe0f", "bow": "1f647", "man-bowing": "1f647-200d-2642-fe0f", "bowing_man": "1f647-200d-2642-fe0f", "woman-bowing": "1f647-200d-2640-fe0f", "bowing_woman": "1f647-200d-2640-fe0f", "face_palm": "1f926", "man-facepalming": "1f926-200d-2642-fe0f", "man_facepalming": "1f926-200d-2642-fe0f", "woman-facepalming": "1f926-200d-2640-fe0f", "woman_facepalming": "1f926-200d-2640-fe0f", "shrug": "1f937", "man-shrugging": "1f937-200d-2642-fe0f", "man_shrugging": "1f937-200d-2642-fe0f", "woman-shrugging": "1f937-200d-2640-fe0f", "woman_shrugging": "1f937-200d-2640-fe0f", "health_worker": "1f9d1-200d-2695-fe0f", "male-doctor": "1f468-200d-2695-fe0f", "man_health_worker": "1f468-200d-2695-fe0f", "female-doctor": "1f469-200d-2695-fe0f", "woman_health_worker": "1f469-200d-2695-fe0f", "student": "1f9d1-200d-1f393", "male-student": "1f468-200d-1f393", "man_student": "1f468-200d-1f393", "female-student": "1f469-200d-1f393", "woman_student": "1f469-200d-1f393", "teacher": "1f9d1-200d-1f3eb", "male-teacher": "1f468-200d-1f3eb", "man_teacher": "1f468-200d-1f3eb", "female-teacher": "1f469-200d-1f3eb", "woman_teacher": "1f469-200d-1f3eb", "judge": "1f9d1-200d-2696-fe0f", "male-judge": "1f468-200d-2696-fe0f", "man_judge": "1f468-200d-2696-fe0f", "female-judge": "1f469-200d-2696-fe0f", "woman_judge": "1f469-200d-2696-fe0f", "farmer": "1f9d1-200d-1f33e", "male-farmer": "1f468-200d-1f33e", "man_farmer": "1f468-200d-1f33e", "female-farmer": "1f469-200d-1f33e", "woman_farmer": "1f469-200d-1f33e", "cook": "1f9d1-200d-1f373", "male-cook": "1f468-200d-1f373", "man_cook": "1f468-200d-1f373", "female-cook": "1f469-200d-1f373", "woman_cook": "1f469-200d-1f373", "mechanic": "1f9d1-200d-1f527", "male-mechanic": "1f468-200d-1f527", "man_mechanic": "1f468-200d-1f527", "female-mechanic": "1f469-200d-1f527", "woman_mechanic": "1f469-200d-1f527", "factory_worker": "1f9d1-200d-1f3ed", "male-factory-worker": "1f468-200d-1f3ed", "man_factory_worker": "1f468-200d-1f3ed", "female-factory-worker": "1f469-200d-1f3ed", "woman_factory_worker": "1f469-200d-1f3ed", "office_worker": "1f9d1-200d-1f4bc", "male-office-worker": "1f468-200d-1f4bc", "man_office_worker": "1f468-200d-1f4bc", "female-office-worker": "1f469-200d-1f4bc", "woman_office_worker": "1f469-200d-1f4bc", "scientist": "1f9d1-200d-1f52c", "male-scientist": "1f468-200d-1f52c", "man_scientist": "1f468-200d-1f52c", "female-scientist": "1f469-200d-1f52c", "woman_scientist": "1f469-200d-1f52c", "technologist": "1f9d1-200d-1f4bb", "male-technologist": "1f468-200d-1f4bb", "man_technologist": "1f468-200d-1f4bb", "female-technologist": "1f469-200d-1f4bb", "woman_technologist": "1f469-200d-1f4bb", "singer": "1f9d1-200d-1f3a4", "male-singer": "1f468-200d-1f3a4", "man_singer": "1f468-200d-1f3a4", "female-singer": "1f469-200d-1f3a4", "woman_singer": "1f469-200d-1f3a4", "artist": "1f9d1-200d-1f3a8", "male-artist": "1f468-200d-1f3a8", "man_artist": "1f468-200d-1f3a8", "female-artist": "1f469-200d-1f3a8", "woman_artist": "1f469-200d-1f3a8", "pilot": "1f9d1-200d-2708-fe0f", "male-pilot": "1f468-200d-2708-fe0f", "man_pilot": "1f468-200d-2708-fe0f", "female-pilot": "1f469-200d-2708-fe0f", "woman_pilot": "1f469-200d-2708-fe0f", "astronaut": "1f9d1-200d-1f680", "male-astronaut": "1f468-200d-1f680", "man_astronaut": "1f468-200d-1f680", "female-astronaut": "1f469-200d-1f680", "woman_astronaut": "1f469-200d-1f680", "firefighter": "1f9d1-200d-1f692", "male-firefighter": "1f468-200d-1f692", "man_firefighter": "1f468-200d-1f692", "female-firefighter": "1f469-200d-1f692", "woman_firefighter": "1f469-200d-1f692", "cop": "1f46e", "male-police-officer": "1f46e-200d-2642-fe0f", "policeman": "1f46e-200d-2642-fe0f", "female-police-officer": "1f46e-200d-2640-fe0f", "policewoman": "1f46e-200d-2640-fe0f", "sleuth_or_spy": "1f575-fe0f", "detective": "1f575-fe0f", "male-detective": "1f575-fe0f-200d-2642-fe0f", "male_detective": "1f575-fe0f-200d-2642-fe0f", "female-detective": "1f575-fe0f-200d-2640-fe0f", "female_detective": "1f575-fe0f-200d-2640-fe0f", "guardsman": "1f482", "male-guard": "1f482-200d-2642-fe0f", "female-guard": "1f482-200d-2640-fe0f", "guardswoman": "1f482-200d-2640-fe0f", "ninja": "1f977", "construction_worker": "1f477", "male-construction-worker": "1f477-200d-2642-fe0f", "construction_worker_man": "1f477-200d-2642-fe0f", "female-construction-worker": "1f477-200d-2640-fe0f", "construction_worker_woman": "1f477-200d-2640-fe0f", "prince": "1f934", "princess": "1f478", "man_with_turban": "1f473", "man-wearing-turban": "1f473-200d-2642-fe0f", "woman-wearing-turban": "1f473-200d-2640-fe0f", "woman_with_turban": "1f473-200d-2640-fe0f", "man_with_gua_pi_mao": "1f472", "person_with_headscarf": "1f9d5", "person_in_tuxedo": "1f935", "man_in_tuxedo": "1f935-200d-2642-fe0f", "woman_in_tuxedo": "1f935-200d-2640-fe0f", "bride_with_veil": "1f470", "man_with_veil": "1f470-200d-2642-fe0f", "woman_with_veil": "1f470-200d-2640-fe0f", "pregnant_woman": "1f930", "breast-feeding": "1f931", "woman_feeding_baby": "1f469-200d-1f37c", "man_feeding_baby": "1f468-200d-1f37c", "person_feeding_baby": "1f9d1-200d-1f37c", "angel": "1f47c", "santa": "1f385", "mrs_claus": "1f936", "mother_christmas": "1f936", "mx_claus": "1f9d1-200d-1f384", "superhero": "1f9b8", "male_superhero": "1f9b8-200d-2642-fe0f", "female_superhero": "1f9b8-200d-2640-fe0f", "supervillain": "1f9b9", "male_supervillain": "1f9b9-200d-2642-fe0f", "female_supervillain": "1f9b9-200d-2640-fe0f", "mage": "1f9d9", "male_mage": "1f9d9-200d-2642-fe0f", "female_mage": "1f9d9-200d-2640-fe0f", "fairy": "1f9da", "male_fairy": "1f9da-200d-2642-fe0f", "female_fairy": "1f9da-200d-2640-fe0f", "vampire": "1f9db", "male_vampire": "1f9db-200d-2642-fe0f", "female_vampire": "1f9db-200d-2640-fe0f", "merperson": "1f9dc", "merman": "1f9dc-200d-2642-fe0f", "mermaid": "1f9dc-200d-2640-fe0f", "elf": "1f9dd", "male_elf": "1f9dd-200d-2642-fe0f", "female_elf": "1f9dd-200d-2640-fe0f", "genie": "1f9de", "male_genie": "1f9de-200d-2642-fe0f", "female_genie": "1f9de-200d-2640-fe0f", "zombie": "1f9df", "male_zombie": "1f9df-200d-2642-fe0f", "female_zombie": "1f9df-200d-2640-fe0f", "massage": "1f486", "man-getting-massage": "1f486-200d-2642-fe0f", "massage_man": "1f486-200d-2642-fe0f", "woman-getting-massage": "1f486-200d-2640-fe0f", "massage_woman": "1f486-200d-2640-fe0f", "haircut": "1f487", "man-getting-haircut": "1f487-200d-2642-fe0f", "haircut_man": "1f487-200d-2642-fe0f", "woman-getting-haircut": "1f487-200d-2640-fe0f", "haircut_woman": "1f487-200d-2640-fe0f", "walking": "1f6b6", "man-walking": "1f6b6-200d-2642-fe0f", "walking_man": "1f6b6-200d-2642-fe0f", "woman-walking": "1f6b6-200d-2640-fe0f", "walking_woman": "1f6b6-200d-2640-fe0f", "standing_person": "1f9cd", "man_standing": "1f9cd-200d-2642-fe0f", "woman_standing": "1f9cd-200d-2640-fe0f", "kneeling_person": "1f9ce", "man_kneeling": "1f9ce-200d-2642-fe0f", "woman_kneeling": "1f9ce-200d-2640-fe0f", "person_with_probing_cane": "1f9d1-200d-1f9af", "man_with_probing_cane": "1f468-200d-1f9af", "woman_with_probing_cane": "1f469-200d-1f9af", "person_in_motorized_wheelchair": "1f9d1-200d-1f9bc", "man_in_motorized_wheelchair": "1f468-200d-1f9bc", "woman_in_motorized_wheelchair": "1f469-200d-1f9bc", "person_in_manual_wheelchair": "1f9d1-200d-1f9bd", "man_in_manual_wheelchair": "1f468-200d-1f9bd", "woman_in_manual_wheelchair": "1f469-200d-1f9bd", "runner": "1f3c3", "running": "1f3c3", "man-running": "1f3c3-200d-2642-fe0f", "running_man": "1f3c3-200d-2642-fe0f", "woman-running": "1f3c3-200d-2640-fe0f", "running_woman": "1f3c3-200d-2640-fe0f", "dancer": "1f483", "man_dancing": "1f57a", "man_in_business_suit_levitating": "1f574-fe0f", "business_suit_levitating": "1f574-fe0f", "dancers": "1f46f", "man-with-bunny-ears-partying": "1f46f-200d-2642-fe0f", "dancing_men": "1f46f-200d-2642-fe0f", "woman-with-bunny-ears-partying": "1f46f-200d-2640-fe0f", "dancing_women": "1f46f-200d-2640-fe0f", "person_in_steamy_room": "1f9d6", "man_in_steamy_room": "1f9d6-200d-2642-fe0f", "woman_in_steamy_room": "1f9d6-200d-2640-fe0f", "person_climbing": "1f9d7", "man_climbing": "1f9d7-200d-2642-fe0f", "woman_climbing": "1f9d7-200d-2640-fe0f", "fencer": "1f93a", "person_fencing": "1f93a", "horse_racing": "1f3c7", "skier": "26f7-fe0f", "snowboarder": "1f3c2", "golfer": "1f3cc-fe0f", "man-golfing": "1f3cc-fe0f-200d-2642-fe0f", "golfing_man": "1f3cc-fe0f-200d-2642-fe0f", "woman-golfing": "1f3cc-fe0f-200d-2640-fe0f", "golfing_woman": "1f3cc-fe0f-200d-2640-fe0f", "surfer": "1f3c4", "man-surfing": "1f3c4-200d-2642-fe0f", "surfing_man": "1f3c4-200d-2642-fe0f", "woman-surfing": "1f3c4-200d-2640-fe0f", "surfing_woman": "1f3c4-200d-2640-fe0f", "rowboat": "1f6a3", "man-rowing-boat": "1f6a3-200d-2642-fe0f", "rowing_man": "1f6a3-200d-2642-fe0f", "woman-rowing-boat": "1f6a3-200d-2640-fe0f", "rowing_woman": "1f6a3-200d-2640-fe0f", "swimmer": "1f3ca", "man-swimming": "1f3ca-200d-2642-fe0f", "swimming_man": "1f3ca-200d-2642-fe0f", "woman-swimming": "1f3ca-200d-2640-fe0f", "swimming_woman": "1f3ca-200d-2640-fe0f", "person_with_ball": "26f9-fe0f", "man-bouncing-ball": "26f9-fe0f-200d-2642-fe0f", "basketball_man": "26f9-fe0f-200d-2642-fe0f", "woman-bouncing-ball": "26f9-fe0f-200d-2640-fe0f", "basketball_woman": "26f9-fe0f-200d-2640-fe0f", "weight_lifter": "1f3cb-fe0f", "man-lifting-weights": "1f3cb-fe0f-200d-2642-fe0f", "weight_lifting_man": "1f3cb-fe0f-200d-2642-fe0f", "woman-lifting-weights": "1f3cb-fe0f-200d-2640-fe0f", "weight_lifting_woman": "1f3cb-fe0f-200d-2640-fe0f", "bicyclist": "1f6b4", "man-biking": "1f6b4-200d-2642-fe0f", "biking_man": "1f6b4-200d-2642-fe0f", "woman-biking": "1f6b4-200d-2640-fe0f", "biking_woman": "1f6b4-200d-2640-fe0f", "mountain_bicyclist": "1f6b5", "man-mountain-biking": "1f6b5-200d-2642-fe0f", "mountain_biking_man": "1f6b5-200d-2642-fe0f", "woman-mountain-biking": "1f6b5-200d-2640-fe0f", "mountain_biking_woman": "1f6b5-200d-2640-fe0f", "person_doing_cartwheel": "1f938", "man-cartwheeling": "1f938-200d-2642-fe0f", "man_cartwheeling": "1f938-200d-2642-fe0f", "woman-cartwheeling": "1f938-200d-2640-fe0f", "woman_cartwheeling": "1f938-200d-2640-fe0f", "wrestlers": "1f93c", "man-wrestling": "1f93c-200d-2642-fe0f", "men_wrestling": "1f93c-200d-2642-fe0f", "woman-wrestling": "1f93c-200d-2640-fe0f", "women_wrestling": "1f93c-200d-2640-fe0f", "water_polo": "1f93d", "man-playing-water-polo": "1f93d-200d-2642-fe0f", "man_playing_water_polo": "1f93d-200d-2642-fe0f", "woman-playing-water-polo": "1f93d-200d-2640-fe0f", "woman_playing_water_polo": "1f93d-200d-2640-fe0f", "handball": "1f93e", "man-playing-handball": "1f93e-200d-2642-fe0f", "man_playing_handball": "1f93e-200d-2642-fe0f", "woman-playing-handball": "1f93e-200d-2640-fe0f", "woman_playing_handball": "1f93e-200d-2640-fe0f", "juggling": "1f939", "man-juggling": "1f939-200d-2642-fe0f", "man_juggling": "1f939-200d-2642-fe0f", "woman-juggling": "1f939-200d-2640-fe0f", "woman_juggling": "1f939-200d-2640-fe0f", "person_in_lotus_position": "1f9d8", "man_in_lotus_position": "1f9d8-200d-2642-fe0f", "woman_in_lotus_position": "1f9d8-200d-2640-fe0f", "bath": "1f6c0", "sleeping_accommodation": "1f6cc", "sleeping_bed": "1f6cc", "people_holding_hands": "1f9d1-200d-1f91d-200d-1f9d1", "two_women_holding_hands": "1f46d", "women_holding_hands": "1f46d", "man_and_woman_holding_hands": "1f46b", "woman_and_man_holding_hands": "1f46b", "couple": "1f46b", "two_men_holding_hands": "1f46c", "men_holding_hands": "1f46c", "couplekiss": "1f48f", "woman-kiss-man": "1f469-200d-2764-fe0f-200d-1f48b-200d-1f468", "couplekiss_man_woman": "1f469-200d-2764-fe0f-200d-1f48b-200d-1f468", "man-kiss-man": "1f468-200d-2764-fe0f-200d-1f48b-200d-1f468", "couplekiss_man_man": "1f468-200d-2764-fe0f-200d-1f48b-200d-1f468", "woman-kiss-woman": "1f469-200d-2764-fe0f-200d-1f48b-200d-1f469", "couplekiss_woman_woman": "1f469-200d-2764-fe0f-200d-1f48b-200d-1f469", "couple_with_heart": "1f491", "woman-heart-man": "1f469-200d-2764-fe0f-200d-1f468", "couple_with_heart_woman_man": "1f469-200d-2764-fe0f-200d-1f468", "man-heart-man": "1f468-200d-2764-fe0f-200d-1f468", "couple_with_heart_man_man": "1f468-200d-2764-fe0f-200d-1f468", "woman-heart-woman": "1f469-200d-2764-fe0f-200d-1f469", "couple_with_heart_woman_woman": "1f469-200d-2764-fe0f-200d-1f469", "family": "1f46a", "man-woman-boy": "1f468-200d-1f469-200d-1f466", "family_man_woman_boy": "1f468-200d-1f469-200d-1f466", "man-woman-girl": "1f468-200d-1f469-200d-1f467", "family_man_woman_girl": "1f468-200d-1f469-200d-1f467", "man-woman-girl-boy": "1f468-200d-1f469-200d-1f467-200d-1f466", "family_man_woman_girl_boy": "1f468-200d-1f469-200d-1f467-200d-1f466", "man-woman-boy-boy": "1f468-200d-1f469-200d-1f466-200d-1f466", "family_man_woman_boy_boy": "1f468-200d-1f469-200d-1f466-200d-1f466", "man-woman-girl-girl": "1f468-200d-1f469-200d-1f467-200d-1f467", "family_man_woman_girl_girl": "1f468-200d-1f469-200d-1f467-200d-1f467", "man-man-boy": "1f468-200d-1f468-200d-1f466", "family_man_man_boy": "1f468-200d-1f468-200d-1f466", "man-man-girl": "1f468-200d-1f468-200d-1f467", "family_man_man_girl": "1f468-200d-1f468-200d-1f467", "man-man-girl-boy": "1f468-200d-1f468-200d-1f467-200d-1f466", "family_man_man_girl_boy": "1f468-200d-1f468-200d-1f467-200d-1f466", "man-man-boy-boy": "1f468-200d-1f468-200d-1f466-200d-1f466", "family_man_man_boy_boy": "1f468-200d-1f468-200d-1f466-200d-1f466", "man-man-girl-girl": "1f468-200d-1f468-200d-1f467-200d-1f467", "family_man_man_girl_girl": "1f468-200d-1f468-200d-1f467-200d-1f467", "woman-woman-boy": "1f469-200d-1f469-200d-1f466", "family_woman_woman_boy": "1f469-200d-1f469-200d-1f466", "woman-woman-girl": "1f469-200d-1f469-200d-1f467", "family_woman_woman_girl": "1f469-200d-1f469-200d-1f467", "woman-woman-girl-boy": "1f469-200d-1f469-200d-1f467-200d-1f466", "family_woman_woman_girl_boy": "1f469-200d-1f469-200d-1f467-200d-1f466", "woman-woman-boy-boy": "1f469-200d-1f469-200d-1f466-200d-1f466", "family_woman_woman_boy_boy": "1f469-200d-1f469-200d-1f466-200d-1f466", "woman-woman-girl-girl": "1f469-200d-1f469-200d-1f467-200d-1f467", "family_woman_woman_girl_girl": "1f469-200d-1f469-200d-1f467-200d-1f467", "man-boy": "1f468-200d-1f466", "family_man_boy": "1f468-200d-1f466", "man-boy-boy": "1f468-200d-1f466-200d-1f466", "family_man_boy_boy": "1f468-200d-1f466-200d-1f466", "man-girl": "1f468-200d-1f467", "family_man_girl": "1f468-200d-1f467", "man-girl-boy": "1f468-200d-1f467-200d-1f466", "family_man_girl_boy": "1f468-200d-1f467-200d-1f466", "man-girl-girl": "1f468-200d-1f467-200d-1f467", "family_man_girl_girl": "1f468-200d-1f467-200d-1f467", "woman-boy": "1f469-200d-1f466", "family_woman_boy": "1f469-200d-1f466", "woman-boy-boy": "1f469-200d-1f466-200d-1f466", "family_woman_boy_boy": "1f469-200d-1f466-200d-1f466", "woman-girl": "1f469-200d-1f467", "family_woman_girl": "1f469-200d-1f467", "woman-girl-boy": "1f469-200d-1f467-200d-1f466", "family_woman_girl_boy": "1f469-200d-1f467-200d-1f466", "woman-girl-girl": "1f469-200d-1f467-200d-1f467", "family_woman_girl_girl": "1f469-200d-1f467-200d-1f467", "speaking_head_in_silhouette": "1f5e3-fe0f", "speaking_head": "1f5e3-fe0f", "bust_in_silhouette": "1f464", "busts_in_silhouette": "1f465", "people_hugging": "1fac2", "footprints": "1f463", "skin-tone-2": "1f3fb", "skin-tone-3": "1f3fc", "skin-tone-4": "1f3fd", "skin-tone-5": "1f3fe", "skin-tone-6": "1f3ff", "monkey_face": "1f435", "monkey": "1f412", "gorilla": "1f98d", "orangutan": "1f9a7", "dog": "1f436", "dog2": "1f415", "guide_dog": "1f9ae", "service_dog": "1f415-200d-1f9ba", "poodle": "1f429", "wolf": "1f43a", "fox_face": "1f98a", "raccoon": "1f99d", "cat": "1f431", "cat2": "1f408", "black_cat": "1f408-200d-2b1b", "lion_face": "1f981", "lion": "1f981", "tiger": "1f42f", "tiger2": "1f405", "leopard": "1f406", "horse": "1f434", "racehorse": "1f40e", "unicorn_face": "1f984", "unicorn": "1f984", "zebra_face": "1f993", "deer": "1f98c", "bison": "1f9ac", "cow": "1f42e", "ox": "1f402", "water_buffalo": "1f403", "cow2": "1f404", "pig": "1f437", "pig2": "1f416", "boar": "1f417", "pig_nose": "1f43d", "ram": "1f40f", "sheep": "1f411", "goat": "1f410", "dromedary_camel": "1f42a", "camel": "1f42b", "llama": "1f999", "giraffe_face": "1f992", "elephant": "1f418", "mammoth": "1f9a3", "rhinoceros": "1f98f", "hippopotamus": "1f99b", "mouse": "1f42d", "mouse2": "1f401", "rat": "1f400", "hamster": "1f439", "rabbit": "1f430", "rabbit2": "1f407", "chipmunk": "1f43f-fe0f", "beaver": "1f9ab", "hedgehog": "1f994", "bat": "1f987", "bear": "1f43b", "polar_bear": "1f43b-200d-2744-fe0f", "koala": "1f428", "panda_face": "1f43c", "sloth": "1f9a5", "otter": "1f9a6", "skunk": "1f9a8", "kangaroo": "1f998", "badger": "1f9a1", "feet": "1f43e", "paw_prints": "1f43e", "turkey": "1f983", "chicken": "1f414", "rooster": "1f413", "hatching_chick": "1f423", "baby_chick": "1f424", "hatched_chick": "1f425", "bird": "1f426", "penguin": "1f427", "dove_of_peace": "1f54a-fe0f", "dove": "1f54a-fe0f", "eagle": "1f985", "duck": "1f986", "swan": "1f9a2", "owl": "1f989", "dodo": "1f9a4", "feather": "1fab6", "flamingo": "1f9a9", "peacock": "1f99a", "parrot": "1f99c", "frog": "1f438", "crocodile": "1f40a", "turtle": "1f422", "lizard": "1f98e", "snake": "1f40d", "dragon_face": "1f432", "dragon": "1f409", "sauropod": "1f995", "t-rex": "1f996", "whale": "1f433", "whale2": "1f40b", "dolphin": "1f42c", "flipper": "1f42c", "seal": "1f9ad", "fish": "1f41f", "tropical_fish": "1f420", "blowfish": "1f421", "shark": "1f988", "octopus": "1f419", "shell": "1f41a", "snail": "1f40c", "butterfly": "1f98b", "bug": "1f41b", "ant": "1f41c", "bee": "1f41d", "honeybee": "1f41d", "beetle": "1fab2", "ladybug": "1f41e", "lady_beetle": "1f41e", "cricket": "1f997", "cockroach": "1fab3", "spider": "1f577-fe0f", "spider_web": "1f578-fe0f", "scorpion": "1f982", "mosquito": "1f99f", "fly": "1fab0", "worm": "1fab1", "microbe": "1f9a0", "bouquet": "1f490", "cherry_blossom": "1f338", "white_flower": "1f4ae", "rosette": "1f3f5-fe0f", "rose": "1f339", "wilted_flower": "1f940", "hibiscus": "1f33a", "sunflower": "1f33b", "blossom": "1f33c", "tulip": "1f337", "seedling": "1f331", "potted_plant": "1fab4", "evergreen_tree": "1f332", "deciduous_tree": "1f333", "palm_tree": "1f334", "cactus": "1f335", "ear_of_rice": "1f33e", "herb": "1f33f", "shamrock": "2618-fe0f", "four_leaf_clover": "1f340", "maple_leaf": "1f341", "fallen_leaf": "1f342", "leaves": "1f343", "grapes": "1f347", "melon": "1f348", "watermelon": "1f349", "tangerine": "1f34a", "mandarin": "1f34a", "orange": "1f34a", "lemon": "1f34b", "banana": "1f34c", "pineapple": "1f34d", "mango": "1f96d", "apple": "1f34e", "green_apple": "1f34f", "pear": "1f350", "peach": "1f351", "cherries": "1f352", "strawberry": "1f353", "blueberries": "1fad0", "kiwifruit": "1f95d", "kiwi_fruit": "1f95d", "tomato": "1f345", "olive": "1fad2", "coconut": "1f965", "avocado": "1f951", "eggplant": "1f346", "potato": "1f954", "carrot": "1f955", "corn": "1f33d", "hot_pepper": "1f336-fe0f", "bell_pepper": "1fad1", "cucumber": "1f952", "leafy_green": "1f96c", "broccoli": "1f966", "garlic": "1f9c4", "onion": "1f9c5", "mushroom": "1f344", "peanuts": "1f95c", "chestnut": "1f330", "bread": "1f35e", "croissant": "1f950", "baguette_bread": "1f956", "flatbread": "1fad3", "pretzel": "1f968", "bagel": "1f96f", "pancakes": "1f95e", "waffle": "1f9c7", "cheese_wedge": "1f9c0", "cheese": "1f9c0", "meat_on_bone": "1f356", "poultry_leg": "1f357", "cut_of_meat": "1f969", "bacon": "1f953", "hamburger": "1f354", "fries": "1f35f", "pizza": "1f355", "hotdog": "1f32d", "sandwich": "1f96a", "taco": "1f32e", "burrito": "1f32f", "tamale": "1fad4", "stuffed_flatbread": "1f959", "falafel": "1f9c6", "egg": "1f95a", "fried_egg": "1f373", "cooking": "1f373", "shallow_pan_of_food": "1f958", "stew": "1f372", "fondue": "1fad5", "bowl_with_spoon": "1f963", "green_salad": "1f957", "popcorn": "1f37f", "butter": "1f9c8", "salt": "1f9c2", "canned_food": "1f96b", "bento": "1f371", "rice_cracker": "1f358", "rice_ball": "1f359", "rice": "1f35a", "curry": "1f35b", "ramen": "1f35c", "spaghetti": "1f35d", "sweet_potato": "1f360", "oden": "1f362", "sushi": "1f363", "fried_shrimp": "1f364", "fish_cake": "1f365", "moon_cake": "1f96e", "dango": "1f361", "dumpling": "1f95f", "fortune_cookie": "1f960", "takeout_box": "1f961", "crab": "1f980", "lobster": "1f99e", "shrimp": "1f990", "squid": "1f991", "oyster": "1f9aa", "icecream": "1f366", "shaved_ice": "1f367", "ice_cream": "1f368", "doughnut": "1f369", "cookie": "1f36a", "birthday": "1f382", "cake": "1f370", "cupcake": "1f9c1", "pie": "1f967", "chocolate_bar": "1f36b", "candy": "1f36c", "lollipop": "1f36d", "custard": "1f36e", "honey_pot": "1f36f", "baby_bottle": "1f37c", "glass_of_milk": "1f95b", "milk_glass": "1f95b", "coffee": "2615", "teapot": "1fad6", "tea": "1f375", "sake": "1f376", "champagne": "1f37e", "wine_glass": "1f377", "cocktail": "1f378", "tropical_drink": "1f379", "beer": "1f37a", "beers": "1f37b", "clinking_glasses": "1f942", "tumbler_glass": "1f943", "cup_with_straw": "1f964", "bubble_tea": "1f9cb", "beverage_box": "1f9c3", "mate_drink": "1f9c9", "ice_cube": "1f9ca", "chopsticks": "1f962", "knife_fork_plate": "1f37d-fe0f", "plate_with_cutlery": "1f37d-fe0f", "fork_and_knife": "1f374", "spoon": "1f944", "hocho": "1f52a", "knife": "1f52a", "amphora": "1f3fa", "earth_africa": "1f30d", "earth_americas": "1f30e", "earth_asia": "1f30f", "globe_with_meridians": "1f310", "world_map": "1f5fa-fe0f", "japan": "1f5fe", "compass": "1f9ed", "snow_capped_mountain": "1f3d4-fe0f", "mountain_snow": "1f3d4-fe0f", "mountain": "26f0-fe0f", "volcano": "1f30b", "mount_fuji": "1f5fb", "camping": "1f3d5-fe0f", "beach_with_umbrella": "1f3d6-fe0f", "beach_umbrella": "1f3d6-fe0f", "desert": "1f3dc-fe0f", "desert_island": "1f3dd-fe0f", "national_park": "1f3de-fe0f", "stadium": "1f3df-fe0f", "classical_building": "1f3db-fe0f", "building_construction": "1f3d7-fe0f", "bricks": "1f9f1", "rock": "1faa8", "wood": "1fab5", "hut": "1f6d6", "house_buildings": "1f3d8-fe0f", "houses": "1f3d8-fe0f", "derelict_house_building": "1f3da-fe0f", "derelict_house": "1f3da-fe0f", "house": "1f3e0", "house_with_garden": "1f3e1", "office": "1f3e2", "post_office": "1f3e3", "european_post_office": "1f3e4", "hospital": "1f3e5", "bank": "1f3e6", "hotel": "1f3e8", "love_hotel": "1f3e9", "convenience_store": "1f3ea", "school": "1f3eb", "department_store": "1f3ec", "factory": "1f3ed", "japanese_castle": "1f3ef", "european_castle": "1f3f0", "wedding": "1f492", "tokyo_tower": "1f5fc", "statue_of_liberty": "1f5fd", "church": "26ea", "mosque": "1f54c", "hindu_temple": "1f6d5", "synagogue": "1f54d", "shinto_shrine": "26e9-fe0f", "kaaba": "1f54b", "fountain": "26f2", "tent": "26fa", "foggy": "1f301", "night_with_stars": "1f303", "cityscape": "1f3d9-fe0f", "sunrise_over_mountains": "1f304", "sunrise": "1f305", "city_sunset": "1f306", "city_sunrise": "1f307", "bridge_at_night": "1f309", "hotsprings": "2668-fe0f", "carousel_horse": "1f3a0", "ferris_wheel": "1f3a1", "roller_coaster": "1f3a2", "barber": "1f488", "circus_tent": "1f3aa", "steam_locomotive": "1f682", "railway_car": "1f683", "bullettrain_side": "1f684", "bullettrain_front": "1f685", "train2": "1f686", "metro": "1f687", "light_rail": "1f688", "station": "1f689", "tram": "1f68a", "monorail": "1f69d", "mountain_railway": "1f69e", "train": "1f68b", "bus": "1f68c", "oncoming_bus": "1f68d", "trolleybus": "1f68e", "minibus": "1f690", "ambulance": "1f691", "fire_engine": "1f692", "police_car": "1f693", "oncoming_police_car": "1f694", "taxi": "1f695", "oncoming_taxi": "1f696", "car": "1f697", "red_car": "1f697", "oncoming_automobile": "1f698", "blue_car": "1f699", "pickup_truck": "1f6fb", "truck": "1f69a", "articulated_lorry": "1f69b", "tractor": "1f69c", "racing_car": "1f3ce-fe0f", "racing_motorcycle": "1f3cd-fe0f", "motorcycle": "1f3cd-fe0f", "motor_scooter": "1f6f5", "manual_wheelchair": "1f9bd", "motorized_wheelchair": "1f9bc", "auto_rickshaw": "1f6fa", "bike": "1f6b2", "scooter": "1f6f4", "kick_scooter": "1f6f4", "skateboard": "1f6f9", "roller_skate": "1f6fc", "busstop": "1f68f", "motorway": "1f6e3-fe0f", "railway_track": "1f6e4-fe0f", "oil_drum": "1f6e2-fe0f", "fuelpump": "26fd", "rotating_light": "1f6a8", "traffic_light": "1f6a5", "vertical_traffic_light": "1f6a6", "octagonal_sign": "1f6d1", "stop_sign": "1f6d1", "construction": "1f6a7", "anchor": "2693", "boat": "26f5", "sailboat": "26f5", "canoe": "1f6f6", "speedboat": "1f6a4", "passenger_ship": "1f6f3-fe0f", "ferry": "26f4-fe0f", "motor_boat": "1f6e5-fe0f", "ship": "1f6a2", "airplane": "2708-fe0f", "small_airplane": "1f6e9-fe0f", "airplane_departure": "1f6eb", "flight_departure": "1f6eb", "airplane_arriving": "1f6ec", "flight_arrival": "1f6ec", "parachute": "1fa82", "seat": "1f4ba", "helicopter": "1f681", "suspension_railway": "1f69f", "mountain_cableway": "1f6a0", "aerial_tramway": "1f6a1", "satellite": "1f6f0-fe0f", "artificial_satellite": "1f6f0-fe0f", "rocket": "1f680", "flying_saucer": "1f6f8", "bellhop_bell": "1f6ce-fe0f", "luggage": "1f9f3", "hourglass": "231b", "hourglass_flowing_sand": "23f3", "watch": "231a", "alarm_clock": "23f0", "stopwatch": "23f1-fe0f", "timer_clock": "23f2-fe0f", "mantelpiece_clock": "1f570-fe0f", "clock12": "1f55b", "clock1230": "1f567", "clock1": "1f550", "clock130": "1f55c", "clock2": "1f551", "clock230": "1f55d", "clock3": "1f552", "clock330": "1f55e", "clock4": "1f553", "clock430": "1f55f", "clock5": "1f554", "clock530": "1f560", "clock6": "1f555", "clock630": "1f561", "clock7": "1f556", "clock730": "1f562", "clock8": "1f557", "clock830": "1f563", "clock9": "1f558", "clock930": "1f564", "clock10": "1f559", "clock1030": "1f565", "clock11": "1f55a", "clock1130": "1f566", "new_moon": "1f311", "waxing_crescent_moon": "1f312", "first_quarter_moon": "1f313", "moon": "1f314", "waxing_gibbous_moon": "1f314", "full_moon": "1f315", "waning_gibbous_moon": "1f316", "last_quarter_moon": "1f317", "waning_crescent_moon": "1f318", "crescent_moon": "1f319", "new_moon_with_face": "1f31a", "first_quarter_moon_with_face": "1f31b", "last_quarter_moon_with_face": "1f31c", "thermometer": "1f321-fe0f", "sunny": "2600-fe0f", "full_moon_with_face": "1f31d", "sun_with_face": "1f31e", "ringed_planet": "1fa90", "star": "2b50", "star2": "1f31f", "stars": "1f320", "milky_way": "1f30c", "cloud": "2601-fe0f", "partly_sunny": "26c5", "thunder_cloud_and_rain": "26c8-fe0f", "cloud_with_lightning_and_rain": "26c8-fe0f", "mostly_sunny": "1f324-fe0f", "sun_small_cloud": "1f324-fe0f", "sun_behind_small_cloud": "1f324-fe0f", "barely_sunny": "1f325-fe0f", "sun_behind_cloud": "1f325-fe0f", "sun_behind_large_cloud": "1f325-fe0f", "partly_sunny_rain": "1f326-fe0f", "sun_behind_rain_cloud": "1f326-fe0f", "rain_cloud": "1f327-fe0f", "cloud_with_rain": "1f327-fe0f", "snow_cloud": "1f328-fe0f", "cloud_with_snow": "1f328-fe0f", "lightning": "1f329-fe0f", "lightning_cloud": "1f329-fe0f", "cloud_with_lightning": "1f329-fe0f", "tornado": "1f32a-fe0f", "tornado_cloud": "1f32a-fe0f", "fog": "1f32b-fe0f", "wind_blowing_face": "1f32c-fe0f", "wind_face": "1f32c-fe0f", "cyclone": "1f300", "rainbow": "1f308", "closed_umbrella": "1f302", "umbrella": "2602-fe0f", "open_umbrella": "2602-fe0f", "umbrella_with_rain_drops": "2614", "umbrella_on_ground": "26f1-fe0f", "parasol_on_ground": "26f1-fe0f", "zap": "26a1", "snowflake": "2744-fe0f", "snowman": "2603-fe0f", "snowman_with_snow": "2603-fe0f", "snowman_without_snow": "26c4", "comet": "2604-fe0f", "fire": "1f525", "droplet": "1f4a7", "ocean": "1f30a", "jack_o_lantern": "1f383", "christmas_tree": "1f384", "fireworks": "1f386", "sparkler": "1f387", "firecracker": "1f9e8", "sparkles": "2728", "balloon": "1f388", "tada": "1f389", "confetti_ball": "1f38a", "tanabata_tree": "1f38b", "bamboo": "1f38d", "dolls": "1f38e", "flags": "1f38f", "wind_chime": "1f390", "rice_scene": "1f391", "red_envelope": "1f9e7", "ribbon": "1f380", "gift": "1f381", "reminder_ribbon": "1f397-fe0f", "admission_tickets": "1f39f-fe0f", "tickets": "1f39f-fe0f", "ticket": "1f3ab", "medal": "1f396-fe0f", "medal_military": "1f396-fe0f", "trophy": "1f3c6", "sports_medal": "1f3c5", "medal_sports": "1f3c5", "first_place_medal": "1f947", "1st_place_medal": "1f947", "second_place_medal": "1f948", "2nd_place_medal": "1f948", "third_place_medal": "1f949", "3rd_place_medal": "1f949", "soccer": "26bd", "baseball": "26be", "softball": "1f94e", "basketball": "1f3c0", "volleyball": "1f3d0", "football": "1f3c8", "rugby_football": "1f3c9", "tennis": "1f3be", "flying_disc": "1f94f", "bowling": "1f3b3", "cricket_bat_and_ball": "1f3cf", "field_hockey_stick_and_ball": "1f3d1", "field_hockey": "1f3d1", "ice_hockey_stick_and_puck": "1f3d2", "ice_hockey": "1f3d2", "lacrosse": "1f94d", "table_tennis_paddle_and_ball": "1f3d3", "ping_pong": "1f3d3", "badminton_racquet_and_shuttlecock": "1f3f8", "badminton": "1f3f8", "boxing_glove": "1f94a", "martial_arts_uniform": "1f94b", "goal_net": "1f945", "golf": "26f3", "ice_skate": "26f8-fe0f", "fishing_pole_and_fish": "1f3a3", "diving_mask": "1f93f", "running_shirt_with_sash": "1f3bd", "ski": "1f3bf", "sled": "1f6f7", "curling_stone": "1f94c", "dart": "1f3af", "yo-yo": "1fa80", "kite": "1fa81", "8ball": "1f3b1", "crystal_ball": "1f52e", "magic_wand": "1fa84", "nazar_amulet": "1f9ff", "video_game": "1f3ae", "joystick": "1f579-fe0f", "slot_machine": "1f3b0", "game_die": "1f3b2", "jigsaw": "1f9e9", "teddy_bear": "1f9f8", "pinata": "1fa85", "nesting_dolls": "1fa86", "spades": "2660-fe0f", "hearts": "2665-fe0f", "diamonds": "2666-fe0f", "clubs": "2663-fe0f", "chess_pawn": "265f-fe0f", "black_joker": "1f0cf", "mahjong": "1f004", "flower_playing_cards": "1f3b4", "performing_arts": "1f3ad", "frame_with_picture": "1f5bc-fe0f", "framed_picture": "1f5bc-fe0f", "art": "1f3a8", "thread": "1f9f5", "sewing_needle": "1faa1", "yarn": "1f9f6", "knot": "1faa2", "eyeglasses": "1f453", "dark_sunglasses": "1f576-fe0f", "goggles": "1f97d", "lab_coat": "1f97c", "safety_vest": "1f9ba", "necktie": "1f454", "shirt": "1f455", "tshirt": "1f455", "jeans": "1f456", "scarf": "1f9e3", "gloves": "1f9e4", "coat": "1f9e5", "socks": "1f9e6", "dress": "1f457", "kimono": "1f458", "sari": "1f97b", "one-piece_swimsuit": "1fa71", "briefs": "1fa72", "shorts": "1fa73", "bikini": "1f459", "womans_clothes": "1f45a", "purse": "1f45b", "handbag": "1f45c", "pouch": "1f45d", "shopping_bags": "1f6cd-fe0f", "shopping": "1f6cd-fe0f", "school_satchel": "1f392", "thong_sandal": "1fa74", "mans_shoe": "1f45e", "shoe": "1f45e", "athletic_shoe": "1f45f", "hiking_boot": "1f97e", "womans_flat_shoe": "1f97f", "high_heel": "1f460", "sandal": "1f461", "ballet_shoes": "1fa70", "boot": "1f462", "crown": "1f451", "womans_hat": "1f452", "tophat": "1f3a9", "mortar_board": "1f393", "billed_cap": "1f9e2", "military_helmet": "1fa96", "helmet_with_white_cross": "26d1-fe0f", "rescue_worker_helmet": "26d1-fe0f", "prayer_beads": "1f4ff", "lipstick": "1f484", "ring": "1f48d", "gem": "1f48e", "mute": "1f507", "speaker": "1f508", "sound": "1f509", "loud_sound": "1f50a", "loudspeaker": "1f4e2", "mega": "1f4e3", "postal_horn": "1f4ef", "bell": "1f514", "no_bell": "1f515", "musical_score": "1f3bc", "musical_note": "1f3b5", "notes": "1f3b6", "studio_microphone": "1f399-fe0f", "level_slider": "1f39a-fe0f", "control_knobs": "1f39b-fe0f", "microphone": "1f3a4", "headphones": "1f3a7", "radio": "1f4fb", "saxophone": "1f3b7", "accordion": "1fa97", "guitar": "1f3b8", "musical_keyboard": "1f3b9", "trumpet": "1f3ba", "violin": "1f3bb", "banjo": "1fa95", "drum_with_drumsticks": "1f941", "drum": "1f941", "long_drum": "1fa98", "iphone": "1f4f1", "calling": "1f4f2", "phone": "260e-fe0f", "telephone": "260e-fe0f", "telephone_receiver": "1f4de", "pager": "1f4df", "fax": "1f4e0", "battery": "1f50b", "electric_plug": "1f50c", "computer": "1f4bb", "desktop_computer": "1f5a5-fe0f", "printer": "1f5a8-fe0f", "keyboard": "2328-fe0f", "three_button_mouse": "1f5b1-fe0f", "computer_mouse": "1f5b1-fe0f", "trackball": "1f5b2-fe0f", "minidisc": "1f4bd", "floppy_disk": "1f4be", "cd": "1f4bf", "dvd": "1f4c0", "abacus": "1f9ee", "movie_camera": "1f3a5", "film_frames": "1f39e-fe0f", "film_strip": "1f39e-fe0f", "film_projector": "1f4fd-fe0f", "clapper": "1f3ac", "tv": "1f4fa", "camera": "1f4f7", "camera_with_flash": "1f4f8", "camera_flash": "1f4f8", "video_camera": "1f4f9", "vhs": "1f4fc", "mag": "1f50d", "mag_right": "1f50e", "candle": "1f56f-fe0f", "bulb": "1f4a1", "flashlight": "1f526", "izakaya_lantern": "1f3ee", "lantern": "1f3ee", "diya_lamp": "1fa94", "notebook_with_decorative_cover": "1f4d4", "closed_book": "1f4d5", "book": "1f4d6", "open_book": "1f4d6", "green_book": "1f4d7", "blue_book": "1f4d8", "orange_book": "1f4d9", "books": "1f4da", "notebook": "1f4d3", "ledger": "1f4d2", "page_with_curl": "1f4c3", "scroll": "1f4dc", "page_facing_up": "1f4c4", "newspaper": "1f4f0", "rolled_up_newspaper": "1f5de-fe0f", "newspaper_roll": "1f5de-fe0f", "bookmark_tabs": "1f4d1", "bookmark": "1f516", "label": "1f3f7-fe0f", "moneybag": "1f4b0", "coin": "1fa99", "yen": "1f4b4", "dollar": "1f4b5", "euro": "1f4b6", "pound": "1f4b7", "money_with_wings": "1f4b8", "credit_card": "1f4b3", "receipt": "1f9fe", "chart": "1f4b9", "email": "2709-fe0f", "envelope": "2709-fe0f", "e-mail": "1f4e7", "incoming_envelope": "1f4e8", "envelope_with_arrow": "1f4e9", "outbox_tray": "1f4e4", "inbox_tray": "1f4e5", "package": "1f4e6", "mailbox": "1f4eb", "mailbox_closed": "1f4ea", "mailbox_with_mail": "1f4ec", "mailbox_with_no_mail": "1f4ed", "postbox": "1f4ee", "ballot_box_with_ballot": "1f5f3-fe0f", "ballot_box": "1f5f3-fe0f", "pencil2": "270f-fe0f", "black_nib": "2712-fe0f", "lower_left_fountain_pen": "1f58b-fe0f", "fountain_pen": "1f58b-fe0f", "lower_left_ballpoint_pen": "1f58a-fe0f", "pen": "1f58a-fe0f", "lower_left_paintbrush": "1f58c-fe0f", "paintbrush": "1f58c-fe0f", "lower_left_crayon": "1f58d-fe0f", "crayon": "1f58d-fe0f", "memo": "1f4dd", "pencil": "1f4dd", "briefcase": "1f4bc", "file_folder": "1f4c1", "open_file_folder": "1f4c2", "card_index_dividers": "1f5c2-fe0f", "date": "1f4c5", "calendar": "1f4c6", "spiral_note_pad": "1f5d2-fe0f", "spiral_notepad": "1f5d2-fe0f", "spiral_calendar_pad": "1f5d3-fe0f", "spiral_calendar": "1f5d3-fe0f", "card_index": "1f4c7", "chart_with_upwards_trend": "1f4c8", "chart_with_downwards_trend": "1f4c9", "bar_chart": "1f4ca", "clipboard": "1f4cb", "pushpin": "1f4cc", "round_pushpin": "1f4cd", "paperclip": "1f4ce", "linked_paperclips": "1f587-fe0f", "paperclips": "1f587-fe0f", "straight_ruler": "1f4cf", "triangular_ruler": "1f4d0", "scissors": "2702-fe0f", "card_file_box": "1f5c3-fe0f", "file_cabinet": "1f5c4-fe0f", "wastebasket": "1f5d1-fe0f", "lock": "1f512", "unlock": "1f513", "lock_with_ink_pen": "1f50f", "closed_lock_with_key": "1f510", "key": "1f511", "old_key": "1f5dd-fe0f", "hammer": "1f528", "axe": "1fa93", "pick": "26cf-fe0f", "hammer_and_pick": "2692-fe0f", "hammer_and_wrench": "1f6e0-fe0f", "dagger_knife": "1f5e1-fe0f", "dagger": "1f5e1-fe0f", "crossed_swords": "2694-fe0f", "gun": "1f52b", "boomerang": "1fa83", "bow_and_arrow": "1f3f9", "shield": "1f6e1-fe0f", "carpentry_saw": "1fa9a", "wrench": "1f527", "screwdriver": "1fa9b", "nut_and_bolt": "1f529", "gear": "2699-fe0f", "compression": "1f5dc-fe0f", "clamp": "1f5dc-fe0f", "scales": "2696-fe0f", "balance_scale": "2696-fe0f", "probing_cane": "1f9af", "link": "1f517", "chains": "26d3-fe0f", "hook": "1fa9d", "toolbox": "1f9f0", "magnet": "1f9f2", "ladder": "1fa9c", "alembic": "2697-fe0f", "test_tube": "1f9ea", "petri_dish": "1f9eb", "dna": "1f9ec", "microscope": "1f52c", "telescope": "1f52d", "satellite_antenna": "1f4e1", "syringe": "1f489", "drop_of_blood": "1fa78", "pill": "1f48a", "adhesive_bandage": "1fa79", "stethoscope": "1fa7a", "door": "1f6aa", "elevator": "1f6d7", "mirror": "1fa9e", "window": "1fa9f", "bed": "1f6cf-fe0f", "couch_and_lamp": "1f6cb-fe0f", "chair": "1fa91", "toilet": "1f6bd", "plunger": "1faa0", "shower": "1f6bf", "bathtub": "1f6c1", "mouse_trap": "1faa4", "razor": "1fa92", "lotion_bottle": "1f9f4", "safety_pin": "1f9f7", "broom": "1f9f9", "basket": "1f9fa", "roll_of_paper": "1f9fb", "bucket": "1faa3", "soap": "1f9fc", "toothbrush": "1faa5", "sponge": "1f9fd", "fire_extinguisher": "1f9ef", "shopping_trolley": "1f6d2", "shopping_cart": "1f6d2", "smoking": "1f6ac", "coffin": "26b0-fe0f", "headstone": "1faa6", "funeral_urn": "26b1-fe0f", "moyai": "1f5ff", "placard": "1faa7", "atm": "1f3e7", "put_litter_in_its_place": "1f6ae", "potable_water": "1f6b0", "wheelchair": "267f", "mens": "1f6b9", "womens": "1f6ba", "restroom": "1f6bb", "baby_symbol": "1f6bc", "wc": "1f6be", "passport_control": "1f6c2", "customs": "1f6c3", "baggage_claim": "1f6c4", "left_luggage": "1f6c5", "warning": "26a0-fe0f", "children_crossing": "1f6b8", "no_entry": "26d4", "no_entry_sign": "1f6ab", "no_bicycles": "1f6b3", "no_smoking": "1f6ad", "do_not_litter": "1f6af", "non-potable_water": "1f6b1", "no_pedestrians": "1f6b7", "no_mobile_phones": "1f4f5", "underage": "1f51e", "radioactive_sign": "2622-fe0f", "radioactive": "2622-fe0f", "biohazard_sign": "2623-fe0f", "biohazard": "2623-fe0f", "arrow_up": "2b06-fe0f", "arrow_upper_right": "2197-fe0f", "arrow_right": "27a1-fe0f", "arrow_lower_right": "2198-fe0f", "arrow_down": "2b07-fe0f", "arrow_lower_left": "2199-fe0f", "arrow_left": "2b05-fe0f", "arrow_upper_left": "2196-fe0f", "arrow_up_down": "2195-fe0f", "left_right_arrow": "2194-fe0f", "leftwards_arrow_with_hook": "21a9-fe0f", "arrow_right_hook": "21aa-fe0f", "arrow_heading_up": "2934-fe0f", "arrow_heading_down": "2935-fe0f", "arrows_clockwise": "1f503", "arrows_counterclockwise": "1f504", "back": "1f519", "end": "1f51a", "on": "1f51b", "soon": "1f51c", "top": "1f51d", "place_of_worship": "1f6d0", "atom_symbol": "269b-fe0f", "om_symbol": "1f549-fe0f", "om": "1f549-fe0f", "star_of_david": "2721-fe0f", "wheel_of_dharma": "2638-fe0f", "yin_yang": "262f-fe0f", "latin_cross": "271d-fe0f", "orthodox_cross": "2626-fe0f", "star_and_crescent": "262a-fe0f", "peace_symbol": "262e-fe0f", "menorah_with_nine_branches": "1f54e", "menorah": "1f54e", "six_pointed_star": "1f52f", "aries": "2648", "taurus": "2649", "gemini": "264a", "cancer": "264b", "leo": "264c", "virgo": "264d", "libra": "264e", "scorpius": "264f", "sagittarius": "2650", "capricorn": "2651", "aquarius": "2652", "pisces": "2653", "ophiuchus": "26ce", "twisted_rightwards_arrows": "1f500", "repeat": "1f501", "repeat_one": "1f502", "arrow_forward": "25b6-fe0f", "fast_forward": "23e9", "black_right_pointing_double_triangle_with_vertical_bar": "23ed-fe0f", "next_track_button": "23ed-fe0f", "black_right_pointing_triangle_with_double_vertical_bar": "23ef-fe0f", "play_or_pause_button": "23ef-fe0f", "arrow_backward": "25c0-fe0f", "rewind": "23ea", "black_left_pointing_double_triangle_with_vertical_bar": "23ee-fe0f", "previous_track_button": "23ee-fe0f", "arrow_up_small": "1f53c", "arrow_double_up": "23eb", "arrow_down_small": "1f53d", "arrow_double_down": "23ec", "double_vertical_bar": "23f8-fe0f", "pause_button": "23f8-fe0f", "black_square_for_stop": "23f9-fe0f", "stop_button": "23f9-fe0f", "black_circle_for_record": "23fa-fe0f", "record_button": "23fa-fe0f", "eject": "23cf-fe0f", "cinema": "1f3a6", "low_brightness": "1f505", "high_brightness": "1f506", "signal_strength": "1f4f6", "vibration_mode": "1f4f3", "mobile_phone_off": "1f4f4", "female_sign": "2640-fe0f", "male_sign": "2642-fe0f", "transgender_symbol": "26a7-fe0f", "heavy_multiplication_x": "2716-fe0f", "heavy_plus_sign": "2795", "heavy_minus_sign": "2796", "heavy_division_sign": "2797", "infinity": "267e-fe0f", "bangbang": "203c-fe0f", "interrobang": "2049-fe0f", "question": "2753", "grey_question": "2754", "grey_exclamation": "2755", "exclamation": "2757", "heavy_exclamation_mark": "2757", "wavy_dash": "3030-fe0f", "currency_exchange": "1f4b1", "heavy_dollar_sign": "1f4b2", "medical_symbol": "2695-fe0f", "staff_of_aesculapius": "2695-fe0f", "recycle": "267b-fe0f", "fleur_de_lis": "269c-fe0f", "trident": "1f531", "name_badge": "1f4db", "beginner": "1f530", "o": "2b55", "white_check_mark": "2705", "ballot_box_with_check": "2611-fe0f", "heavy_check_mark": "2714-fe0f", "x": "274c", "negative_squared_cross_mark": "274e", "curly_loop": "27b0", "loop": "27bf", "part_alternation_mark": "303d-fe0f", "eight_spoked_asterisk": "2733-fe0f", "eight_pointed_black_star": "2734-fe0f", "sparkle": "2747-fe0f", "copyright": "00a9-fe0f", "registered": "00ae-fe0f", "tm": "2122-fe0f", "hash": "0023-fe0f-20e3", "keycap_star": "002a-fe0f-20e3", "asterisk": "002a-fe0f-20e3", "zero": "0030-fe0f-20e3", "one": "0031-fe0f-20e3", "two": "0032-fe0f-20e3", "three": "0033-fe0f-20e3", "four": "0034-fe0f-20e3", "five": "0035-fe0f-20e3", "six": "0036-fe0f-20e3", "seven": "0037-fe0f-20e3", "eight": "0038-fe0f-20e3", "nine": "0039-fe0f-20e3", "keycap_ten": "1f51f", "capital_abcd": "1f520", "abcd": "1f521", "1234": "1f522", "symbols": "1f523", "abc": "1f524", "a": "1f170-fe0f", "ab": "1f18e", "b": "1f171-fe0f", "cl": "1f191", "cool": "1f192", "free": "1f193", "information_source": "2139-fe0f", "id": "1f194", "m": "24c2-fe0f", "new": "1f195", "ng": "1f196", "o2": "1f17e-fe0f", "ok": "1f197", "parking": "1f17f-fe0f", "sos": "1f198", "up": "1f199", "vs": "1f19a", "koko": "1f201", "sa": "1f202-fe0f", "u6708": "1f237-fe0f", "u6709": "1f236", "u6307": "1f22f", "ideograph_advantage": "1f250", "u5272": "1f239", "u7121": "1f21a", "u7981": "1f232", "accept": "1f251", "u7533": "1f238", "u5408": "1f234", "u7a7a": "1f233", "congratulations": "3297-fe0f", "secret": "3299-fe0f", "u55b6": "1f23a", "u6e80": "1f235", "red_circle": "1f534", "large_orange_circle": "1f7e0", "large_yellow_circle": "1f7e1", "large_green_circle": "1f7e2", "large_blue_circle": "1f535", "large_purple_circle": "1f7e3", "large_brown_circle": "1f7e4", "black_circle": "26ab", "white_circle": "26aa", "large_red_square": "1f7e5", "large_orange_square": "1f7e7", "large_yellow_square": "1f7e8", "large_green_square": "1f7e9", "large_blue_square": "1f7e6", "large_purple_square": "1f7ea", "large_brown_square": "1f7eb", "black_large_square": "2b1b", "white_large_square": "2b1c", "black_medium_square": "25fc-fe0f", "white_medium_square": "25fb-fe0f", "black_medium_small_square": "25fe", "white_medium_small_square": "25fd", "black_small_square": "25aa-fe0f", "white_small_square": "25ab-fe0f", "large_orange_diamond": "1f536", "large_blue_diamond": "1f537", "small_orange_diamond": "1f538", "small_blue_diamond": "1f539", "small_red_triangle": "1f53a", "small_red_triangle_down": "1f53b", "diamond_shape_with_a_dot_inside": "1f4a0", "radio_button": "1f518", "white_square_button": "1f533", "black_square_button": "1f532", "checkered_flag": "1f3c1", "triangular_flag_on_post": "1f6a9", "crossed_flags": "1f38c", "waving_black_flag": "1f3f4", "black_flag": "1f3f4", "waving_white_flag": "1f3f3-fe0f", "white_flag": "1f3f3-fe0f", "rainbow-flag": "1f3f3-fe0f-200d-1f308", "rainbow_flag": "1f3f3-fe0f-200d-1f308", "transgender_flag": "1f3f3-fe0f-200d-26a7-fe0f", "pirate_flag": "1f3f4-200d-2620-fe0f", "flag-ac": "1f1e6-1f1e8", "flag-ad": "1f1e6-1f1e9", "andorra": "1f1e6-1f1e9", "flag-ae": "1f1e6-1f1ea", "united_arab_emirates": "1f1e6-1f1ea", "flag-af": "1f1e6-1f1eb", "afghanistan": "1f1e6-1f1eb", "flag-ag": "1f1e6-1f1ec", "antigua_barbuda": "1f1e6-1f1ec", "flag-ai": "1f1e6-1f1ee", "anguilla": "1f1e6-1f1ee", "flag-al": "1f1e6-1f1f1", "albania": "1f1e6-1f1f1", "flag-am": "1f1e6-1f1f2", "armenia": "1f1e6-1f1f2", "flag-ao": "1f1e6-1f1f4", "angola": "1f1e6-1f1f4", "flag-aq": "1f1e6-1f1f6", "antarctica": "1f1e6-1f1f6", "flag-ar": "1f1e6-1f1f7", "argentina": "1f1e6-1f1f7", "flag-as": "1f1e6-1f1f8", "american_samoa": "1f1e6-1f1f8", "flag-at": "1f1e6-1f1f9", "austria": "1f1e6-1f1f9", "flag-au": "1f1e6-1f1fa", "australia": "1f1e6-1f1fa", "flag-aw": "1f1e6-1f1fc", "aruba": "1f1e6-1f1fc", "flag-ax": "1f1e6-1f1fd", "aland_islands": "1f1e6-1f1fd", "flag-az": "1f1e6-1f1ff", "azerbaijan": "1f1e6-1f1ff", "flag-ba": "1f1e7-1f1e6", "bosnia_herzegovina": "1f1e7-1f1e6", "flag-bb": "1f1e7-1f1e7", "barbados": "1f1e7-1f1e7", "flag-bd": "1f1e7-1f1e9", "bangladesh": "1f1e7-1f1e9", "flag-be": "1f1e7-1f1ea", "belgium": "1f1e7-1f1ea", "flag-bf": "1f1e7-1f1eb", "burkina_faso": "1f1e7-1f1eb", "flag-bg": "1f1e7-1f1ec", "bulgaria": "1f1e7-1f1ec", "flag-bh": "1f1e7-1f1ed", "bahrain": "1f1e7-1f1ed", "flag-bi": "1f1e7-1f1ee", "burundi": "1f1e7-1f1ee", "flag-bj": "1f1e7-1f1ef", "benin": "1f1e7-1f1ef", "flag-bl": "1f1e7-1f1f1", "st_barthelemy": "1f1e7-1f1f1", "flag-bm": "1f1e7-1f1f2", "bermuda": "1f1e7-1f1f2", "flag-bn": "1f1e7-1f1f3", "brunei": "1f1e7-1f1f3", "flag-bo": "1f1e7-1f1f4", "bolivia": "1f1e7-1f1f4", "flag-bq": "1f1e7-1f1f6", "caribbean_netherlands": "1f1e7-1f1f6", "flag-br": "1f1e7-1f1f7", "brazil": "1f1e7-1f1f7", "flag-bs": "1f1e7-1f1f8", "bahamas": "1f1e7-1f1f8", "flag-bt": "1f1e7-1f1f9", "bhutan": "1f1e7-1f1f9", "flag-bv": "1f1e7-1f1fb", "flag-bw": "1f1e7-1f1fc", "botswana": "1f1e7-1f1fc", "flag-by": "1f1e7-1f1fe", "belarus": "1f1e7-1f1fe", "flag-bz": "1f1e7-1f1ff", "belize": "1f1e7-1f1ff", "flag-ca": "1f1e8-1f1e6", "ca": "1f1e8-1f1e6", "canada": "1f1e8-1f1e6", "flag-cc": "1f1e8-1f1e8", "cocos_islands": "1f1e8-1f1e8", "flag-cd": "1f1e8-1f1e9", "congo_kinshasa": "1f1e8-1f1e9", "flag-cf": "1f1e8-1f1eb", "central_african_republic": "1f1e8-1f1eb", "flag-cg": "1f1e8-1f1ec", "congo_brazzaville": "1f1e8-1f1ec", "flag-ch": "1f1e8-1f1ed", "switzerland": "1f1e8-1f1ed", "flag-ci": "1f1e8-1f1ee", "cote_divoire": "1f1e8-1f1ee", "flag-ck": "1f1e8-1f1f0", "cook_islands": "1f1e8-1f1f0", "flag-cl": "1f1e8-1f1f1", "chile": "1f1e8-1f1f1", "flag-cm": "1f1e8-1f1f2", "cameroon": "1f1e8-1f1f2", "cn": "1f1e8-1f1f3", "flag-cn": "1f1e8-1f1f3", "flag-co": "1f1e8-1f1f4", "colombia": "1f1e8-1f1f4", "flag-cp": "1f1e8-1f1f5", "flag-cr": "1f1e8-1f1f7", "costa_rica": "1f1e8-1f1f7", "flag-cu": "1f1e8-1f1fa", "cuba": "1f1e8-1f1fa", "flag-cv": "1f1e8-1f1fb", "cape_verde": "1f1e8-1f1fb", "flag-cw": "1f1e8-1f1fc", "curacao": "1f1e8-1f1fc", "flag-cx": "1f1e8-1f1fd", "christmas_island": "1f1e8-1f1fd", "flag-cy": "1f1e8-1f1fe", "cyprus": "1f1e8-1f1fe", "flag-cz": "1f1e8-1f1ff", "czech_republic": "1f1e8-1f1ff", "de": "1f1e9-1f1ea", "flag-de": "1f1e9-1f1ea", "flag-dg": "1f1e9-1f1ec", "flag-dj": "1f1e9-1f1ef", "djibouti": "1f1e9-1f1ef", "flag-dk": "1f1e9-1f1f0", "denmark": "1f1e9-1f1f0", "flag-dm": "1f1e9-1f1f2", "dominica": "1f1e9-1f1f2", "flag-do": "1f1e9-1f1f4", "dominican_republic": "1f1e9-1f1f4", "flag-dz": "1f1e9-1f1ff", "algeria": "1f1e9-1f1ff", "flag-ea": "1f1ea-1f1e6", "flag-ec": "1f1ea-1f1e8", "ecuador": "1f1ea-1f1e8", "flag-ee": "1f1ea-1f1ea", "estonia": "1f1ea-1f1ea", "flag-eg": "1f1ea-1f1ec", "egypt": "1f1ea-1f1ec", "flag-eh": "1f1ea-1f1ed", "western_sahara": "1f1ea-1f1ed", "flag-er": "1f1ea-1f1f7", "eritrea": "1f1ea-1f1f7", "es": "1f1ea-1f1f8", "flag-es": "1f1ea-1f1f8", "flag-et": "1f1ea-1f1f9", "ethiopia": "1f1ea-1f1f9", "flag-eu": "1f1ea-1f1fa", "eu": "1f1ea-1f1fa", "european_union": "1f1ea-1f1fa", "flag-fi": "1f1eb-1f1ee", "finland": "1f1eb-1f1ee", "flag-fj": "1f1eb-1f1ef", "fiji": "1f1eb-1f1ef", "flag-fk": "1f1eb-1f1f0", "falkland_islands": "1f1eb-1f1f0", "flag-fm": "1f1eb-1f1f2", "micronesia": "1f1eb-1f1f2", "flag-fo": "1f1eb-1f1f4", "faroe_islands": "1f1eb-1f1f4", "fr": "1f1eb-1f1f7", "flag-fr": "1f1eb-1f1f7", "flag-ga": "1f1ec-1f1e6", "gabon": "1f1ec-1f1e6", "gb": "1f1ec-1f1e7", "uk": "1f1ec-1f1e7", "flag-gb": "1f1ec-1f1e7", "flag-gd": "1f1ec-1f1e9", "grenada": "1f1ec-1f1e9", "flag-ge": "1f1ec-1f1ea", "georgia": "1f1ec-1f1ea", "flag-gf": "1f1ec-1f1eb", "french_guiana": "1f1ec-1f1eb", "flag-gg": "1f1ec-1f1ec", "guernsey": "1f1ec-1f1ec", "flag-gh": "1f1ec-1f1ed", "ghana": "1f1ec-1f1ed", "flag-gi": "1f1ec-1f1ee", "gibraltar": "1f1ec-1f1ee", "flag-gl": "1f1ec-1f1f1", "greenland": "1f1ec-1f1f1", "flag-gm": "1f1ec-1f1f2", "gambia": "1f1ec-1f1f2", "flag-gn": "1f1ec-1f1f3", "guinea": "1f1ec-1f1f3", "flag-gp": "1f1ec-1f1f5", "guadeloupe": "1f1ec-1f1f5", "flag-gq": "1f1ec-1f1f6", "equatorial_guinea": "1f1ec-1f1f6", "flag-gr": "1f1ec-1f1f7", "greece": "1f1ec-1f1f7", "flag-gs": "1f1ec-1f1f8", "south_georgia_south_sandwich_islands": "1f1ec-1f1f8", "flag-gt": "1f1ec-1f1f9", "guatemala": "1f1ec-1f1f9", "flag-gu": "1f1ec-1f1fa", "guam": "1f1ec-1f1fa", "flag-gw": "1f1ec-1f1fc", "guinea_bissau": "1f1ec-1f1fc", "flag-gy": "1f1ec-1f1fe", "guyana": "1f1ec-1f1fe", "flag-hk": "1f1ed-1f1f0", "hong_kong": "1f1ed-1f1f0", "flag-hm": "1f1ed-1f1f2", "flag-hn": "1f1ed-1f1f3", "honduras": "1f1ed-1f1f3", "flag-hr": "1f1ed-1f1f7", "croatia": "1f1ed-1f1f7", "flag-ht": "1f1ed-1f1f9", "haiti": "1f1ed-1f1f9", "flag-hu": "1f1ed-1f1fa", "hungary": "1f1ed-1f1fa", "flag-ic": "1f1ee-1f1e8", "canary_islands": "1f1ee-1f1e8", "flag-id": "1f1ee-1f1e9", "indonesia": "1f1ee-1f1e9", "flag-ie": "1f1ee-1f1ea", "ireland": "1f1ee-1f1ea", "flag-il": "1f1ee-1f1f1", "israel": "1f1ee-1f1f1", "flag-im": "1f1ee-1f1f2", "isle_of_man": "1f1ee-1f1f2", "flag-in": "1f1ee-1f1f3", "india": "1f1ee-1f1f3", "flag-io": "1f1ee-1f1f4", "british_indian_ocean_territory": "1f1ee-1f1f4", "flag-iq": "1f1ee-1f1f6", "iraq": "1f1ee-1f1f6", "flag-ir": "1f1ee-1f1f7", "iran": "1f1ee-1f1f7", "flag-is": "1f1ee-1f1f8", "iceland": "1f1ee-1f1f8", "it": "1f1ee-1f1f9", "flag-it": "1f1ee-1f1f9", "flag-je": "1f1ef-1f1ea", "jersey": "1f1ef-1f1ea", "flag-jm": "1f1ef-1f1f2", "jamaica": "1f1ef-1f1f2", "flag-jo": "1f1ef-1f1f4", "jordan": "1f1ef-1f1f4", "jp": "1f1ef-1f1f5", "flag-jp": "1f1ef-1f1f5", "flag-ke": "1f1f0-1f1ea", "kenya": "1f1f0-1f1ea", "flag-kg": "1f1f0-1f1ec", "kyrgyzstan": "1f1f0-1f1ec", "flag-kh": "1f1f0-1f1ed", "cambodia": "1f1f0-1f1ed", "flag-ki": "1f1f0-1f1ee", "kiribati": "1f1f0-1f1ee", "flag-km": "1f1f0-1f1f2", "comoros": "1f1f0-1f1f2", "flag-kn": "1f1f0-1f1f3", "st_kitts_nevis": "1f1f0-1f1f3", "flag-kp": "1f1f0-1f1f5", "north_korea": "1f1f0-1f1f5", "kr": "1f1f0-1f1f7", "flag-kr": "1f1f0-1f1f7", "flag-kw": "1f1f0-1f1fc", "kuwait": "1f1f0-1f1fc", "flag-ky": "1f1f0-1f1fe", "cayman_islands": "1f1f0-1f1fe", "flag-kz": "1f1f0-1f1ff", "kazakhstan": "1f1f0-1f1ff", "flag-la": "1f1f1-1f1e6", "laos": "1f1f1-1f1e6", "flag-lb": "1f1f1-1f1e7", "lebanon": "1f1f1-1f1e7", "flag-lc": "1f1f1-1f1e8", "st_lucia": "1f1f1-1f1e8", "flag-li": "1f1f1-1f1ee", "liechtenstein": "1f1f1-1f1ee", "flag-lk": "1f1f1-1f1f0", "sri_lanka": "1f1f1-1f1f0", "flag-lr": "1f1f1-1f1f7", "liberia": "1f1f1-1f1f7", "flag-ls": "1f1f1-1f1f8", "lesotho": "1f1f1-1f1f8", "flag-lt": "1f1f1-1f1f9", "lithuania": "1f1f1-1f1f9", "flag-lu": "1f1f1-1f1fa", "luxembourg": "1f1f1-1f1fa", "flag-lv": "1f1f1-1f1fb", "latvia": "1f1f1-1f1fb", "flag-ly": "1f1f1-1f1fe", "libya": "1f1f1-1f1fe", "flag-ma": "1f1f2-1f1e6", "morocco": "1f1f2-1f1e6", "flag-mc": "1f1f2-1f1e8", "monaco": "1f1f2-1f1e8", "flag-md": "1f1f2-1f1e9", "moldova": "1f1f2-1f1e9", "flag-me": "1f1f2-1f1ea", "montenegro": "1f1f2-1f1ea", "flag-mf": "1f1f2-1f1eb", "flag-mg": "1f1f2-1f1ec", "madagascar": "1f1f2-1f1ec", "flag-mh": "1f1f2-1f1ed", "marshall_islands": "1f1f2-1f1ed", "flag-mk": "1f1f2-1f1f0", "macedonia": "1f1f2-1f1f0", "flag-ml": "1f1f2-1f1f1", "mali": "1f1f2-1f1f1", "flag-mm": "1f1f2-1f1f2", "myanmar": "1f1f2-1f1f2", "flag-mn": "1f1f2-1f1f3", "mongolia": "1f1f2-1f1f3", "flag-mo": "1f1f2-1f1f4", "macau": "1f1f2-1f1f4", "flag-mp": "1f1f2-1f1f5", "northern_mariana_islands": "1f1f2-1f1f5", "flag-mq": "1f1f2-1f1f6", "martinique": "1f1f2-1f1f6", "flag-mr": "1f1f2-1f1f7", "mauritania": "1f1f2-1f1f7", "flag-ms": "1f1f2-1f1f8", "montserrat": "1f1f2-1f1f8", "flag-mt": "1f1f2-1f1f9", "malta": "1f1f2-1f1f9", "flag-mu": "1f1f2-1f1fa", "mauritius": "1f1f2-1f1fa", "flag-mv": "1f1f2-1f1fb", "maldives": "1f1f2-1f1fb", "flag-mw": "1f1f2-1f1fc", "malawi": "1f1f2-1f1fc", "flag-mx": "1f1f2-1f1fd", "mexico": "1f1f2-1f1fd", "flag-my": "1f1f2-1f1fe", "malaysia": "1f1f2-1f1fe", "flag-mz": "1f1f2-1f1ff", "mozambique": "1f1f2-1f1ff", "flag-na": "1f1f3-1f1e6", "namibia": "1f1f3-1f1e6", "flag-nc": "1f1f3-1f1e8", "new_caledonia": "1f1f3-1f1e8", "flag-ne": "1f1f3-1f1ea", "niger": "1f1f3-1f1ea", "flag-nf": "1f1f3-1f1eb", "norfolk_island": "1f1f3-1f1eb", "flag-ng": "1f1f3-1f1ec", "nigeria": "1f1f3-1f1ec", "flag-ni": "1f1f3-1f1ee", "nicaragua": "1f1f3-1f1ee", "flag-nl": "1f1f3-1f1f1", "netherlands": "1f1f3-1f1f1", "flag-no": "1f1f3-1f1f4", "norway": "1f1f3-1f1f4", "flag-np": "1f1f3-1f1f5", "nepal": "1f1f3-1f1f5", "flag-nr": "1f1f3-1f1f7", "nauru": "1f1f3-1f1f7", "flag-nu": "1f1f3-1f1fa", "niue": "1f1f3-1f1fa", "flag-nz": "1f1f3-1f1ff", "new_zealand": "1f1f3-1f1ff", "flag-om": "1f1f4-1f1f2", "oman": "1f1f4-1f1f2", "flag-pa": "1f1f5-1f1e6", "panama": "1f1f5-1f1e6", "flag-pe": "1f1f5-1f1ea", "peru": "1f1f5-1f1ea", "flag-pf": "1f1f5-1f1eb", "french_polynesia": "1f1f5-1f1eb", "flag-pg": "1f1f5-1f1ec", "papua_new_guinea": "1f1f5-1f1ec", "flag-ph": "1f1f5-1f1ed", "philippines": "1f1f5-1f1ed", "flag-pk": "1f1f5-1f1f0", "pakistan": "1f1f5-1f1f0", "pk": "1f1f5-1f1f0", "flag-pl": "1f1f5-1f1f1", "poland": "1f1f5-1f1f1", "flag-pm": "1f1f5-1f1f2", "st_pierre_miquelon": "1f1f5-1f1f2", "flag-pn": "1f1f5-1f1f3", "pitcairn_islands": "1f1f5-1f1f3", "flag-pr": "1f1f5-1f1f7", "puerto_rico": "1f1f5-1f1f7", "flag-ps": "1f1f5-1f1f8", "palestinian_territories": "1f1f5-1f1f8", "flag-pt": "1f1f5-1f1f9", "portugal": "1f1f5-1f1f9", "flag-pw": "1f1f5-1f1fc", "palau": "1f1f5-1f1fc", "flag-py": "1f1f5-1f1fe", "paraguay": "1f1f5-1f1fe", "flag-qa": "1f1f6-1f1e6", "qatar": "1f1f6-1f1e6", "flag-re": "1f1f7-1f1ea", "reunion": "1f1f7-1f1ea", "flag-ro": "1f1f7-1f1f4", "romania": "1f1f7-1f1f4", "flag-rs": "1f1f7-1f1f8", "serbia": "1f1f7-1f1f8", "ru": "1f1f7-1f1fa", "flag-ru": "1f1f7-1f1fa", "flag-rw": "1f1f7-1f1fc", "rwanda": "1f1f7-1f1fc", "flag-sa": "1f1f8-1f1e6", "saudi_arabia": "1f1f8-1f1e6", "flag-sb": "1f1f8-1f1e7", "solomon_islands": "1f1f8-1f1e7", "flag-sc": "1f1f8-1f1e8", "seychelles": "1f1f8-1f1e8", "flag-sd": "1f1f8-1f1e9", "sudan": "1f1f8-1f1e9", "flag-se": "1f1f8-1f1ea", "sweden": "1f1f8-1f1ea", "flag-sg": "1f1f8-1f1ec", "singapore": "1f1f8-1f1ec", "flag-sh": "1f1f8-1f1ed", "st_helena": "1f1f8-1f1ed", "flag-si": "1f1f8-1f1ee", "slovenia": "1f1f8-1f1ee", "flag-sj": "1f1f8-1f1ef", "flag-sk": "1f1f8-1f1f0", "slovakia": "1f1f8-1f1f0", "flag-sl": "1f1f8-1f1f1", "sierra_leone": "1f1f8-1f1f1", "flag-sm": "1f1f8-1f1f2", "san_marino": "1f1f8-1f1f2", "flag-sn": "1f1f8-1f1f3", "senegal": "1f1f8-1f1f3", "flag-so": "1f1f8-1f1f4", "somalia": "1f1f8-1f1f4", "flag-sr": "1f1f8-1f1f7", "suriname": "1f1f8-1f1f7", "flag-ss": "1f1f8-1f1f8", "south_sudan": "1f1f8-1f1f8", "flag-st": "1f1f8-1f1f9", "sao_tome_principe": "1f1f8-1f1f9", "flag-sv": "1f1f8-1f1fb", "el_salvador": "1f1f8-1f1fb", "flag-sx": "1f1f8-1f1fd", "sint_maarten": "1f1f8-1f1fd", "flag-sy": "1f1f8-1f1fe", "syria": "1f1f8-1f1fe", "flag-sz": "1f1f8-1f1ff", "swaziland": "1f1f8-1f1ff", "flag-ta": "1f1f9-1f1e6", "flag-tc": "1f1f9-1f1e8", "turks_caicos_islands": "1f1f9-1f1e8", "flag-td": "1f1f9-1f1e9", "chad": "1f1f9-1f1e9", "flag-tf": "1f1f9-1f1eb", "french_southern_territories": "1f1f9-1f1eb", "flag-tg": "1f1f9-1f1ec", "togo": "1f1f9-1f1ec", "flag-th": "1f1f9-1f1ed", "thailand": "1f1f9-1f1ed", "flag-tj": "1f1f9-1f1ef", "tajikistan": "1f1f9-1f1ef", "flag-tk": "1f1f9-1f1f0", "tokelau": "1f1f9-1f1f0", "flag-tl": "1f1f9-1f1f1", "timor_leste": "1f1f9-1f1f1", "flag-tm": "1f1f9-1f1f2", "turkmenistan": "1f1f9-1f1f2", "flag-tn": "1f1f9-1f1f3", "tunisia": "1f1f9-1f1f3", "flag-to": "1f1f9-1f1f4", "tonga": "1f1f9-1f1f4", "flag-tr": "1f1f9-1f1f7", "tr": "1f1f9-1f1f7", "flag-tt": "1f1f9-1f1f9", "trinidad_tobago": "1f1f9-1f1f9", "flag-tv": "1f1f9-1f1fb", "tuvalu": "1f1f9-1f1fb", "flag-tw": "1f1f9-1f1fc", "taiwan": "1f1f9-1f1fc", "flag-tz": "1f1f9-1f1ff", "tanzania": "1f1f9-1f1ff", "flag-ua": "1f1fa-1f1e6", "ukraine": "1f1fa-1f1e6", "flag-ug": "1f1fa-1f1ec", "uganda": "1f1fa-1f1ec", "flag-um": "1f1fa-1f1f2", "flag-un": "1f1fa-1f1f3", "us": "1f1fa-1f1f8", "flag-us": "1f1fa-1f1f8", "flag-uy": "1f1fa-1f1fe", "uruguay": "1f1fa-1f1fe", "flag-uz": "1f1fa-1f1ff", "uzbekistan": "1f1fa-1f1ff", "flag-va": "1f1fb-1f1e6", "vatican_city": "1f1fb-1f1e6", "flag-vc": "1f1fb-1f1e8", "st_vincent_grenadines": "1f1fb-1f1e8", "flag-ve": "1f1fb-1f1ea", "venezuela": "1f1fb-1f1ea", "flag-vg": "1f1fb-1f1ec", "british_virgin_islands": "1f1fb-1f1ec", "flag-vi": "1f1fb-1f1ee", "us_virgin_islands": "1f1fb-1f1ee", "flag-vn": "1f1fb-1f1f3", "vietnam": "1f1fb-1f1f3", "flag-vu": "1f1fb-1f1fa", "vanuatu": "1f1fb-1f1fa", "flag-wf": "1f1fc-1f1eb", "wallis_futuna": "1f1fc-1f1eb", "flag-ws": "1f1fc-1f1f8", "samoa": "1f1fc-1f1f8", "flag-xk": "1f1fd-1f1f0", "kosovo": "1f1fd-1f1f0", "flag-ye": "1f1fe-1f1ea", "yemen": "1f1fe-1f1ea", "flag-yt": "1f1fe-1f1f9", "mayotte": "1f1fe-1f1f9", "flag-za": "1f1ff-1f1e6", "south_africa": "1f1ff-1f1e6", "za": "1f1ff-1f1e6", "flag-zm": "1f1ff-1f1f2", "zambia": "1f1ff-1f1f2", "flag-zw": "1f1ff-1f1fc", "zimbabwe": "1f1ff-1f1fc", "flag-england": "1f3f4-e0067-e0062-e0065-e006e-e0067-e007f", "flag-scotland": "1f3f4-e0067-e0062-e0073-e0063-e0074-e007f", "flag-wales": "1f3f4-e0067-e0062-e0077-e006c-e0073-e007f", "santa_light_skin_tone": "1f385-1f3fb", "santa_medium_light_skin_tone": "1f385-1f3fc", "santa_medium_skin_tone": "1f385-1f3fd", "santa_medium_dark_skin_tone": "1f385-1f3fe", "santa_dark_skin_tone": "1f385-1f3ff", "snowboarder_light_skin_tone": "1f3c2-1f3fb", "snowboarder_medium_light_skin_tone": "1f3c2-1f3fc", "snowboarder_medium_skin_tone": "1f3c2-1f3fd", "snowboarder_medium_dark_skin_tone": "1f3c2-1f3fe", "snowboarder_dark_skin_tone": "1f3c2-1f3ff", "woman-running_light_skin_tone": "1f3c3-1f3fb-200d-2640-fe0f", "running_woman_light_skin_tone": "1f3c3-1f3fb-200d-2640-fe0f", "woman-running_medium_light_skin_tone": "1f3c3-1f3fc-200d-2640-fe0f", "running_woman_medium_light_skin_tone": "1f3c3-1f3fc-200d-2640-fe0f", "woman-running_medium_skin_tone": "1f3c3-1f3fd-200d-2640-fe0f", "running_woman_medium_skin_tone": "1f3c3-1f3fd-200d-2640-fe0f", "woman-running_medium_dark_skin_tone": "1f3c3-1f3fe-200d-2640-fe0f", "running_woman_medium_dark_skin_tone": "1f3c3-1f3fe-200d-2640-fe0f", "woman-running_dark_skin_tone": "1f3c3-1f3ff-200d-2640-fe0f", "running_woman_dark_skin_tone": "1f3c3-1f3ff-200d-2640-fe0f", "man-running_light_skin_tone": "1f3c3-1f3fb-200d-2642-fe0f", "running_man_light_skin_tone": "1f3c3-1f3fb-200d-2642-fe0f", "man-running_medium_light_skin_tone": "1f3c3-1f3fc-200d-2642-fe0f", "running_man_medium_light_skin_tone": "1f3c3-1f3fc-200d-2642-fe0f", "man-running_medium_skin_tone": "1f3c3-1f3fd-200d-2642-fe0f", "running_man_medium_skin_tone": "1f3c3-1f3fd-200d-2642-fe0f", "man-running_medium_dark_skin_tone": "1f3c3-1f3fe-200d-2642-fe0f", "running_man_medium_dark_skin_tone": "1f3c3-1f3fe-200d-2642-fe0f", "man-running_dark_skin_tone": "1f3c3-1f3ff-200d-2642-fe0f", "running_man_dark_skin_tone": "1f3c3-1f3ff-200d-2642-fe0f", "runner_light_skin_tone": "1f3c3-1f3fb", "running_light_skin_tone": "1f3c3-1f3fb", "runner_medium_light_skin_tone": "1f3c3-1f3fc", "running_medium_light_skin_tone": "1f3c3-1f3fc", "runner_medium_skin_tone": "1f3c3-1f3fd", "running_medium_skin_tone": "1f3c3-1f3fd", "runner_medium_dark_skin_tone": "1f3c3-1f3fe", "running_medium_dark_skin_tone": "1f3c3-1f3fe", "runner_dark_skin_tone": "1f3c3-1f3ff", "running_dark_skin_tone": "1f3c3-1f3ff", "woman-surfing_light_skin_tone": "1f3c4-1f3fb-200d-2640-fe0f", "surfing_woman_light_skin_tone": "1f3c4-1f3fb-200d-2640-fe0f", "woman-surfing_medium_light_skin_tone": "1f3c4-1f3fc-200d-2640-fe0f", "surfing_woman_medium_light_skin_tone": "1f3c4-1f3fc-200d-2640-fe0f", "woman-surfing_medium_skin_tone": "1f3c4-1f3fd-200d-2640-fe0f", "surfing_woman_medium_skin_tone": "1f3c4-1f3fd-200d-2640-fe0f", "woman-surfing_medium_dark_skin_tone": "1f3c4-1f3fe-200d-2640-fe0f", "surfing_woman_medium_dark_skin_tone": "1f3c4-1f3fe-200d-2640-fe0f", "woman-surfing_dark_skin_tone": "1f3c4-1f3ff-200d-2640-fe0f", "surfing_woman_dark_skin_tone": "1f3c4-1f3ff-200d-2640-fe0f", "man-surfing_light_skin_tone": "1f3c4-1f3fb-200d-2642-fe0f", "surfing_man_light_skin_tone": "1f3c4-1f3fb-200d-2642-fe0f", "man-surfing_medium_light_skin_tone": "1f3c4-1f3fc-200d-2642-fe0f", "surfing_man_medium_light_skin_tone": "1f3c4-1f3fc-200d-2642-fe0f", "man-surfing_medium_skin_tone": "1f3c4-1f3fd-200d-2642-fe0f", "surfing_man_medium_skin_tone": "1f3c4-1f3fd-200d-2642-fe0f", "man-surfing_medium_dark_skin_tone": "1f3c4-1f3fe-200d-2642-fe0f", "surfing_man_medium_dark_skin_tone": "1f3c4-1f3fe-200d-2642-fe0f", "man-surfing_dark_skin_tone": "1f3c4-1f3ff-200d-2642-fe0f", "surfing_man_dark_skin_tone": "1f3c4-1f3ff-200d-2642-fe0f", "surfer_light_skin_tone": "1f3c4-1f3fb", "surfer_medium_light_skin_tone": "1f3c4-1f3fc", "surfer_medium_skin_tone": "1f3c4-1f3fd", "surfer_medium_dark_skin_tone": "1f3c4-1f3fe", "surfer_dark_skin_tone": "1f3c4-1f3ff", "horse_racing_light_skin_tone": "1f3c7-1f3fb", "horse_racing_medium_light_skin_tone": "1f3c7-1f3fc", "horse_racing_medium_skin_tone": "1f3c7-1f3fd", "horse_racing_medium_dark_skin_tone": "1f3c7-1f3fe", "horse_racing_dark_skin_tone": "1f3c7-1f3ff", "woman-swimming_light_skin_tone": "1f3ca-1f3fb-200d-2640-fe0f", "swimming_woman_light_skin_tone": "1f3ca-1f3fb-200d-2640-fe0f", "woman-swimming_medium_light_skin_tone": "1f3ca-1f3fc-200d-2640-fe0f", "swimming_woman_medium_light_skin_tone": "1f3ca-1f3fc-200d-2640-fe0f", "woman-swimming_medium_skin_tone": "1f3ca-1f3fd-200d-2640-fe0f", "swimming_woman_medium_skin_tone": "1f3ca-1f3fd-200d-2640-fe0f", "woman-swimming_medium_dark_skin_tone": "1f3ca-1f3fe-200d-2640-fe0f", "swimming_woman_medium_dark_skin_tone": "1f3ca-1f3fe-200d-2640-fe0f", "woman-swimming_dark_skin_tone": "1f3ca-1f3ff-200d-2640-fe0f", "swimming_woman_dark_skin_tone": "1f3ca-1f3ff-200d-2640-fe0f", "man-swimming_light_skin_tone": "1f3ca-1f3fb-200d-2642-fe0f", "swimming_man_light_skin_tone": "1f3ca-1f3fb-200d-2642-fe0f", "man-swimming_medium_light_skin_tone": "1f3ca-1f3fc-200d-2642-fe0f", "swimming_man_medium_light_skin_tone": "1f3ca-1f3fc-200d-2642-fe0f", "man-swimming_medium_skin_tone": "1f3ca-1f3fd-200d-2642-fe0f", "swimming_man_medium_skin_tone": "1f3ca-1f3fd-200d-2642-fe0f", "man-swimming_medium_dark_skin_tone": "1f3ca-1f3fe-200d-2642-fe0f", "swimming_man_medium_dark_skin_tone": "1f3ca-1f3fe-200d-2642-fe0f", "man-swimming_dark_skin_tone": "1f3ca-1f3ff-200d-2642-fe0f", "swimming_man_dark_skin_tone": "1f3ca-1f3ff-200d-2642-fe0f", "swimmer_light_skin_tone": "1f3ca-1f3fb", "swimmer_medium_light_skin_tone": "1f3ca-1f3fc", "swimmer_medium_skin_tone": "1f3ca-1f3fd", "swimmer_medium_dark_skin_tone": "1f3ca-1f3fe", "swimmer_dark_skin_tone": "1f3ca-1f3ff", "woman-lifting-weights_light_skin_tone": "1f3cb-1f3fb-200d-2640-fe0f", "weight_lifting_woman_light_skin_tone": "1f3cb-1f3fb-200d-2640-fe0f", "woman-lifting-weights_medium_light_skin_tone": "1f3cb-1f3fc-200d-2640-fe0f", "weight_lifting_woman_medium_light_skin_tone": "1f3cb-1f3fc-200d-2640-fe0f", "woman-lifting-weights_medium_skin_tone": "1f3cb-1f3fd-200d-2640-fe0f", "weight_lifting_woman_medium_skin_tone": "1f3cb-1f3fd-200d-2640-fe0f", "woman-lifting-weights_medium_dark_skin_tone": "1f3cb-1f3fe-200d-2640-fe0f", "weight_lifting_woman_medium_dark_skin_tone": "1f3cb-1f3fe-200d-2640-fe0f", "woman-lifting-weights_dark_skin_tone": "1f3cb-1f3ff-200d-2640-fe0f", "weight_lifting_woman_dark_skin_tone": "1f3cb-1f3ff-200d-2640-fe0f", "man-lifting-weights_light_skin_tone": "1f3cb-1f3fb-200d-2642-fe0f", "weight_lifting_man_light_skin_tone": "1f3cb-1f3fb-200d-2642-fe0f", "man-lifting-weights_medium_light_skin_tone": "1f3cb-1f3fc-200d-2642-fe0f", "weight_lifting_man_medium_light_skin_tone": "1f3cb-1f3fc-200d-2642-fe0f", "man-lifting-weights_medium_skin_tone": "1f3cb-1f3fd-200d-2642-fe0f", "weight_lifting_man_medium_skin_tone": "1f3cb-1f3fd-200d-2642-fe0f", "man-lifting-weights_medium_dark_skin_tone": "1f3cb-1f3fe-200d-2642-fe0f", "weight_lifting_man_medium_dark_skin_tone": "1f3cb-1f3fe-200d-2642-fe0f", "man-lifting-weights_dark_skin_tone": "1f3cb-1f3ff-200d-2642-fe0f", "weight_lifting_man_dark_skin_tone": "1f3cb-1f3ff-200d-2642-fe0f", "weight_lifter_light_skin_tone": "1f3cb-1f3fb", "weight_lifter_medium_light_skin_tone": "1f3cb-1f3fc", "weight_lifter_medium_skin_tone": "1f3cb-1f3fd", "weight_lifter_medium_dark_skin_tone": "1f3cb-1f3fe", "weight_lifter_dark_skin_tone": "1f3cb-1f3ff", "woman-golfing_light_skin_tone": "1f3cc-1f3fb-200d-2640-fe0f", "golfing_woman_light_skin_tone": "1f3cc-1f3fb-200d-2640-fe0f", "woman-golfing_medium_light_skin_tone": "1f3cc-1f3fc-200d-2640-fe0f", "golfing_woman_medium_light_skin_tone": "1f3cc-1f3fc-200d-2640-fe0f", "woman-golfing_medium_skin_tone": "1f3cc-1f3fd-200d-2640-fe0f", "golfing_woman_medium_skin_tone": "1f3cc-1f3fd-200d-2640-fe0f", "woman-golfing_medium_dark_skin_tone": "1f3cc-1f3fe-200d-2640-fe0f", "golfing_woman_medium_dark_skin_tone": "1f3cc-1f3fe-200d-2640-fe0f", "woman-golfing_dark_skin_tone": "1f3cc-1f3ff-200d-2640-fe0f", "golfing_woman_dark_skin_tone": "1f3cc-1f3ff-200d-2640-fe0f", "man-golfing_light_skin_tone": "1f3cc-1f3fb-200d-2642-fe0f", "golfing_man_light_skin_tone": "1f3cc-1f3fb-200d-2642-fe0f", "man-golfing_medium_light_skin_tone": "1f3cc-1f3fc-200d-2642-fe0f", "golfing_man_medium_light_skin_tone": "1f3cc-1f3fc-200d-2642-fe0f", "man-golfing_medium_skin_tone": "1f3cc-1f3fd-200d-2642-fe0f", "golfing_man_medium_skin_tone": "1f3cc-1f3fd-200d-2642-fe0f", "man-golfing_medium_dark_skin_tone": "1f3cc-1f3fe-200d-2642-fe0f", "golfing_man_medium_dark_skin_tone": "1f3cc-1f3fe-200d-2642-fe0f", "man-golfing_dark_skin_tone": "1f3cc-1f3ff-200d-2642-fe0f", "golfing_man_dark_skin_tone": "1f3cc-1f3ff-200d-2642-fe0f", "golfer_light_skin_tone": "1f3cc-1f3fb", "golfer_medium_light_skin_tone": "1f3cc-1f3fc", "golfer_medium_skin_tone": "1f3cc-1f3fd", "golfer_medium_dark_skin_tone": "1f3cc-1f3fe", "golfer_dark_skin_tone": "1f3cc-1f3ff", "ear_light_skin_tone": "1f442-1f3fb", "ear_medium_light_skin_tone": "1f442-1f3fc", "ear_medium_skin_tone": "1f442-1f3fd", "ear_medium_dark_skin_tone": "1f442-1f3fe", "ear_dark_skin_tone": "1f442-1f3ff", "nose_light_skin_tone": "1f443-1f3fb", "nose_medium_light_skin_tone": "1f443-1f3fc", "nose_medium_skin_tone": "1f443-1f3fd", "nose_medium_dark_skin_tone": "1f443-1f3fe", "nose_dark_skin_tone": "1f443-1f3ff", "point_up_2_light_skin_tone": "1f446-1f3fb", "point_up_2_medium_light_skin_tone": "1f446-1f3fc", "point_up_2_medium_skin_tone": "1f446-1f3fd", "point_up_2_medium_dark_skin_tone": "1f446-1f3fe", "point_up_2_dark_skin_tone": "1f446-1f3ff", "point_down_light_skin_tone": "1f447-1f3fb", "point_down_medium_light_skin_tone": "1f447-1f3fc", "point_down_medium_skin_tone": "1f447-1f3fd", "point_down_medium_dark_skin_tone": "1f447-1f3fe", "point_down_dark_skin_tone": "1f447-1f3ff", "point_left_light_skin_tone": "1f448-1f3fb", "point_left_medium_light_skin_tone": "1f448-1f3fc", "point_left_medium_skin_tone": "1f448-1f3fd", "point_left_medium_dark_skin_tone": "1f448-1f3fe", "point_left_dark_skin_tone": "1f448-1f3ff", "point_right_light_skin_tone": "1f449-1f3fb", "point_right_medium_light_skin_tone": "1f449-1f3fc", "point_right_medium_skin_tone": "1f449-1f3fd", "point_right_medium_dark_skin_tone": "1f449-1f3fe", "point_right_dark_skin_tone": "1f449-1f3ff", "facepunch_light_skin_tone": "1f44a-1f3fb", "punch_light_skin_tone": "1f44a-1f3fb", "fist_oncoming_light_skin_tone": "1f44a-1f3fb", "facepunch_medium_light_skin_tone": "1f44a-1f3fc", "punch_medium_light_skin_tone": "1f44a-1f3fc", "fist_oncoming_medium_light_skin_tone": "1f44a-1f3fc", "facepunch_medium_skin_tone": "1f44a-1f3fd", "punch_medium_skin_tone": "1f44a-1f3fd", "fist_oncoming_medium_skin_tone": "1f44a-1f3fd", "facepunch_medium_dark_skin_tone": "1f44a-1f3fe", "punch_medium_dark_skin_tone": "1f44a-1f3fe", "fist_oncoming_medium_dark_skin_tone": "1f44a-1f3fe", "facepunch_dark_skin_tone": "1f44a-1f3ff", "punch_dark_skin_tone": "1f44a-1f3ff", "fist_oncoming_dark_skin_tone": "1f44a-1f3ff", "wave_light_skin_tone": "1f44b-1f3fb", "wave_medium_light_skin_tone": "1f44b-1f3fc", "wave_medium_skin_tone": "1f44b-1f3fd", "wave_medium_dark_skin_tone": "1f44b-1f3fe", "wave_dark_skin_tone": "1f44b-1f3ff", "ok_hand_light_skin_tone": "1f44c-1f3fb", "ok_hand_medium_light_skin_tone": "1f44c-1f3fc", "ok_hand_medium_skin_tone": "1f44c-1f3fd", "ok_hand_medium_dark_skin_tone": "1f44c-1f3fe", "ok_hand_dark_skin_tone": "1f44c-1f3ff", "+1_light_skin_tone": "1f44d-1f3fb", "thumbsup_light_skin_tone": "1f44d-1f3fb", "+1_medium_light_skin_tone": "1f44d-1f3fc", "thumbsup_medium_light_skin_tone": "1f44d-1f3fc", "+1_medium_skin_tone": "1f44d-1f3fd", "thumbsup_medium_skin_tone": "1f44d-1f3fd", "+1_medium_dark_skin_tone": "1f44d-1f3fe", "thumbsup_medium_dark_skin_tone": "1f44d-1f3fe", "+1_dark_skin_tone": "1f44d-1f3ff", "thumbsup_dark_skin_tone": "1f44d-1f3ff", "-1_light_skin_tone": "1f44e-1f3fb", "thumbsdown_light_skin_tone": "1f44e-1f3fb", "-1_medium_light_skin_tone": "1f44e-1f3fc", "thumbsdown_medium_light_skin_tone": "1f44e-1f3fc", "-1_medium_skin_tone": "1f44e-1f3fd", "thumbsdown_medium_skin_tone": "1f44e-1f3fd", "-1_medium_dark_skin_tone": "1f44e-1f3fe", "thumbsdown_medium_dark_skin_tone": "1f44e-1f3fe", "-1_dark_skin_tone": "1f44e-1f3ff", "thumbsdown_dark_skin_tone": "1f44e-1f3ff", "clap_light_skin_tone": "1f44f-1f3fb", "clap_medium_light_skin_tone": "1f44f-1f3fc", "clap_medium_skin_tone": "1f44f-1f3fd", "clap_medium_dark_skin_tone": "1f44f-1f3fe", "clap_dark_skin_tone": "1f44f-1f3ff", "open_hands_light_skin_tone": "1f450-1f3fb", "open_hands_medium_light_skin_tone": "1f450-1f3fc", "open_hands_medium_skin_tone": "1f450-1f3fd", "open_hands_medium_dark_skin_tone": "1f450-1f3fe", "open_hands_dark_skin_tone": "1f450-1f3ff", "boy_light_skin_tone": "1f466-1f3fb", "boy_medium_light_skin_tone": "1f466-1f3fc", "boy_medium_skin_tone": "1f466-1f3fd", "boy_medium_dark_skin_tone": "1f466-1f3fe", "boy_dark_skin_tone": "1f466-1f3ff", "girl_light_skin_tone": "1f467-1f3fb", "girl_medium_light_skin_tone": "1f467-1f3fc", "girl_medium_skin_tone": "1f467-1f3fd", "girl_medium_dark_skin_tone": "1f467-1f3fe", "girl_dark_skin_tone": "1f467-1f3ff", "male-farmer_light_skin_tone": "1f468-1f3fb-200d-1f33e", "man_farmer_light_skin_tone": "1f468-1f3fb-200d-1f33e", "male-farmer_medium_light_skin_tone": "1f468-1f3fc-200d-1f33e", "man_farmer_medium_light_skin_tone": "1f468-1f3fc-200d-1f33e", "male-farmer_medium_skin_tone": "1f468-1f3fd-200d-1f33e", "man_farmer_medium_skin_tone": "1f468-1f3fd-200d-1f33e", "male-farmer_medium_dark_skin_tone": "1f468-1f3fe-200d-1f33e", "man_farmer_medium_dark_skin_tone": "1f468-1f3fe-200d-1f33e", "male-farmer_dark_skin_tone": "1f468-1f3ff-200d-1f33e", "man_farmer_dark_skin_tone": "1f468-1f3ff-200d-1f33e", "male-cook_light_skin_tone": "1f468-1f3fb-200d-1f373", "man_cook_light_skin_tone": "1f468-1f3fb-200d-1f373", "male-cook_medium_light_skin_tone": "1f468-1f3fc-200d-1f373", "man_cook_medium_light_skin_tone": "1f468-1f3fc-200d-1f373", "male-cook_medium_skin_tone": "1f468-1f3fd-200d-1f373", "man_cook_medium_skin_tone": "1f468-1f3fd-200d-1f373", "male-cook_medium_dark_skin_tone": "1f468-1f3fe-200d-1f373", "man_cook_medium_dark_skin_tone": "1f468-1f3fe-200d-1f373", "male-cook_dark_skin_tone": "1f468-1f3ff-200d-1f373", "man_cook_dark_skin_tone": "1f468-1f3ff-200d-1f373", "man_feeding_baby_light_skin_tone": "1f468-1f3fb-200d-1f37c", "man_feeding_baby_medium_light_skin_tone": "1f468-1f3fc-200d-1f37c", "man_feeding_baby_medium_skin_tone": "1f468-1f3fd-200d-1f37c", "man_feeding_baby_medium_dark_skin_tone": "1f468-1f3fe-200d-1f37c", "man_feeding_baby_dark_skin_tone": "1f468-1f3ff-200d-1f37c", "male-student_light_skin_tone": "1f468-1f3fb-200d-1f393", "man_student_light_skin_tone": "1f468-1f3fb-200d-1f393", "male-student_medium_light_skin_tone": "1f468-1f3fc-200d-1f393", "man_student_medium_light_skin_tone": "1f468-1f3fc-200d-1f393", "male-student_medium_skin_tone": "1f468-1f3fd-200d-1f393", "man_student_medium_skin_tone": "1f468-1f3fd-200d-1f393", "male-student_medium_dark_skin_tone": "1f468-1f3fe-200d-1f393", "man_student_medium_dark_skin_tone": "1f468-1f3fe-200d-1f393", "male-student_dark_skin_tone": "1f468-1f3ff-200d-1f393", "man_student_dark_skin_tone": "1f468-1f3ff-200d-1f393", "male-singer_light_skin_tone": "1f468-1f3fb-200d-1f3a4", "man_singer_light_skin_tone": "1f468-1f3fb-200d-1f3a4", "male-singer_medium_light_skin_tone": "1f468-1f3fc-200d-1f3a4", "man_singer_medium_light_skin_tone": "1f468-1f3fc-200d-1f3a4", "male-singer_medium_skin_tone": "1f468-1f3fd-200d-1f3a4", "man_singer_medium_skin_tone": "1f468-1f3fd-200d-1f3a4", "male-singer_medium_dark_skin_tone": "1f468-1f3fe-200d-1f3a4", "man_singer_medium_dark_skin_tone": "1f468-1f3fe-200d-1f3a4", "male-singer_dark_skin_tone": "1f468-1f3ff-200d-1f3a4", "man_singer_dark_skin_tone": "1f468-1f3ff-200d-1f3a4", "male-artist_light_skin_tone": "1f468-1f3fb-200d-1f3a8", "man_artist_light_skin_tone": "1f468-1f3fb-200d-1f3a8", "male-artist_medium_light_skin_tone": "1f468-1f3fc-200d-1f3a8", "man_artist_medium_light_skin_tone": "1f468-1f3fc-200d-1f3a8", "male-artist_medium_skin_tone": "1f468-1f3fd-200d-1f3a8", "man_artist_medium_skin_tone": "1f468-1f3fd-200d-1f3a8", "male-artist_medium_dark_skin_tone": "1f468-1f3fe-200d-1f3a8", "man_artist_medium_dark_skin_tone": "1f468-1f3fe-200d-1f3a8", "male-artist_dark_skin_tone": "1f468-1f3ff-200d-1f3a8", "man_artist_dark_skin_tone": "1f468-1f3ff-200d-1f3a8", "male-teacher_light_skin_tone": "1f468-1f3fb-200d-1f3eb", "man_teacher_light_skin_tone": "1f468-1f3fb-200d-1f3eb", "male-teacher_medium_light_skin_tone": "1f468-1f3fc-200d-1f3eb", "man_teacher_medium_light_skin_tone": "1f468-1f3fc-200d-1f3eb", "male-teacher_medium_skin_tone": "1f468-1f3fd-200d-1f3eb", "man_teacher_medium_skin_tone": "1f468-1f3fd-200d-1f3eb", "male-teacher_medium_dark_skin_tone": "1f468-1f3fe-200d-1f3eb", "man_teacher_medium_dark_skin_tone": "1f468-1f3fe-200d-1f3eb", "male-teacher_dark_skin_tone": "1f468-1f3ff-200d-1f3eb", "man_teacher_dark_skin_tone": "1f468-1f3ff-200d-1f3eb", "male-factory-worker_light_skin_tone": "1f468-1f3fb-200d-1f3ed", "man_factory_worker_light_skin_tone": "1f468-1f3fb-200d-1f3ed", "male-factory-worker_medium_light_skin_tone": "1f468-1f3fc-200d-1f3ed", "man_factory_worker_medium_light_skin_tone": "1f468-1f3fc-200d-1f3ed", "male-factory-worker_medium_skin_tone": "1f468-1f3fd-200d-1f3ed", "man_factory_worker_medium_skin_tone": "1f468-1f3fd-200d-1f3ed", "male-factory-worker_medium_dark_skin_tone": "1f468-1f3fe-200d-1f3ed", "man_factory_worker_medium_dark_skin_tone": "1f468-1f3fe-200d-1f3ed", "male-factory-worker_dark_skin_tone": "1f468-1f3ff-200d-1f3ed", "man_factory_worker_dark_skin_tone": "1f468-1f3ff-200d-1f3ed", "male-technologist_light_skin_tone": "1f468-1f3fb-200d-1f4bb", "man_technologist_light_skin_tone": "1f468-1f3fb-200d-1f4bb", "male-technologist_medium_light_skin_tone": "1f468-1f3fc-200d-1f4bb", "man_technologist_medium_light_skin_tone": "1f468-1f3fc-200d-1f4bb", "male-technologist_medium_skin_tone": "1f468-1f3fd-200d-1f4bb", "man_technologist_medium_skin_tone": "1f468-1f3fd-200d-1f4bb", "male-technologist_medium_dark_skin_tone": "1f468-1f3fe-200d-1f4bb", "man_technologist_medium_dark_skin_tone": "1f468-1f3fe-200d-1f4bb", "male-technologist_dark_skin_tone": "1f468-1f3ff-200d-1f4bb", "man_technologist_dark_skin_tone": "1f468-1f3ff-200d-1f4bb", "male-office-worker_light_skin_tone": "1f468-1f3fb-200d-1f4bc", "man_office_worker_light_skin_tone": "1f468-1f3fb-200d-1f4bc", "male-office-worker_medium_light_skin_tone": "1f468-1f3fc-200d-1f4bc", "man_office_worker_medium_light_skin_tone": "1f468-1f3fc-200d-1f4bc", "male-office-worker_medium_skin_tone": "1f468-1f3fd-200d-1f4bc", "man_office_worker_medium_skin_tone": "1f468-1f3fd-200d-1f4bc", "male-office-worker_medium_dark_skin_tone": "1f468-1f3fe-200d-1f4bc", "man_office_worker_medium_dark_skin_tone": "1f468-1f3fe-200d-1f4bc", "male-office-worker_dark_skin_tone": "1f468-1f3ff-200d-1f4bc", "man_office_worker_dark_skin_tone": "1f468-1f3ff-200d-1f4bc", "male-mechanic_light_skin_tone": "1f468-1f3fb-200d-1f527", "man_mechanic_light_skin_tone": "1f468-1f3fb-200d-1f527", "male-mechanic_medium_light_skin_tone": "1f468-1f3fc-200d-1f527", "man_mechanic_medium_light_skin_tone": "1f468-1f3fc-200d-1f527", "male-mechanic_medium_skin_tone": "1f468-1f3fd-200d-1f527", "man_mechanic_medium_skin_tone": "1f468-1f3fd-200d-1f527", "male-mechanic_medium_dark_skin_tone": "1f468-1f3fe-200d-1f527", "man_mechanic_medium_dark_skin_tone": "1f468-1f3fe-200d-1f527", "male-mechanic_dark_skin_tone": "1f468-1f3ff-200d-1f527", "man_mechanic_dark_skin_tone": "1f468-1f3ff-200d-1f527", "male-scientist_light_skin_tone": "1f468-1f3fb-200d-1f52c", "man_scientist_light_skin_tone": "1f468-1f3fb-200d-1f52c", "male-scientist_medium_light_skin_tone": "1f468-1f3fc-200d-1f52c", "man_scientist_medium_light_skin_tone": "1f468-1f3fc-200d-1f52c", "male-scientist_medium_skin_tone": "1f468-1f3fd-200d-1f52c", "man_scientist_medium_skin_tone": "1f468-1f3fd-200d-1f52c", "male-scientist_medium_dark_skin_tone": "1f468-1f3fe-200d-1f52c", "man_scientist_medium_dark_skin_tone": "1f468-1f3fe-200d-1f52c", "male-scientist_dark_skin_tone": "1f468-1f3ff-200d-1f52c", "man_scientist_dark_skin_tone": "1f468-1f3ff-200d-1f52c", "male-astronaut_light_skin_tone": "1f468-1f3fb-200d-1f680", "man_astronaut_light_skin_tone": "1f468-1f3fb-200d-1f680", "male-astronaut_medium_light_skin_tone": "1f468-1f3fc-200d-1f680", "man_astronaut_medium_light_skin_tone": "1f468-1f3fc-200d-1f680", "male-astronaut_medium_skin_tone": "1f468-1f3fd-200d-1f680", "man_astronaut_medium_skin_tone": "1f468-1f3fd-200d-1f680", "male-astronaut_medium_dark_skin_tone": "1f468-1f3fe-200d-1f680", "man_astronaut_medium_dark_skin_tone": "1f468-1f3fe-200d-1f680", "male-astronaut_dark_skin_tone": "1f468-1f3ff-200d-1f680", "man_astronaut_dark_skin_tone": "1f468-1f3ff-200d-1f680", "male-firefighter_light_skin_tone": "1f468-1f3fb-200d-1f692", "man_firefighter_light_skin_tone": "1f468-1f3fb-200d-1f692", "male-firefighter_medium_light_skin_tone": "1f468-1f3fc-200d-1f692", "man_firefighter_medium_light_skin_tone": "1f468-1f3fc-200d-1f692", "male-firefighter_medium_skin_tone": "1f468-1f3fd-200d-1f692", "man_firefighter_medium_skin_tone": "1f468-1f3fd-200d-1f692", "male-firefighter_medium_dark_skin_tone": "1f468-1f3fe-200d-1f692", "man_firefighter_medium_dark_skin_tone": "1f468-1f3fe-200d-1f692", "male-firefighter_dark_skin_tone": "1f468-1f3ff-200d-1f692", "man_firefighter_dark_skin_tone": "1f468-1f3ff-200d-1f692", "man_with_probing_cane_light_skin_tone": "1f468-1f3fb-200d-1f9af", "man_with_probing_cane_medium_light_skin_tone": "1f468-1f3fc-200d-1f9af", "man_with_probing_cane_medium_skin_tone": "1f468-1f3fd-200d-1f9af", "man_with_probing_cane_medium_dark_skin_tone": "1f468-1f3fe-200d-1f9af", "man_with_probing_cane_dark_skin_tone": "1f468-1f3ff-200d-1f9af", "red_haired_man_light_skin_tone": "1f468-1f3fb-200d-1f9b0", "red_haired_man_medium_light_skin_tone": "1f468-1f3fc-200d-1f9b0", "red_haired_man_medium_skin_tone": "1f468-1f3fd-200d-1f9b0", "red_haired_man_medium_dark_skin_tone": "1f468-1f3fe-200d-1f9b0", "red_haired_man_dark_skin_tone": "1f468-1f3ff-200d-1f9b0", "curly_haired_man_light_skin_tone": "1f468-1f3fb-200d-1f9b1", "curly_haired_man_medium_light_skin_tone": "1f468-1f3fc-200d-1f9b1", "curly_haired_man_medium_skin_tone": "1f468-1f3fd-200d-1f9b1", "curly_haired_man_medium_dark_skin_tone": "1f468-1f3fe-200d-1f9b1", "curly_haired_man_dark_skin_tone": "1f468-1f3ff-200d-1f9b1", "bald_man_light_skin_tone": "1f468-1f3fb-200d-1f9b2", "bald_man_medium_light_skin_tone": "1f468-1f3fc-200d-1f9b2", "bald_man_medium_skin_tone": "1f468-1f3fd-200d-1f9b2", "bald_man_medium_dark_skin_tone": "1f468-1f3fe-200d-1f9b2", "bald_man_dark_skin_tone": "1f468-1f3ff-200d-1f9b2", "white_haired_man_light_skin_tone": "1f468-1f3fb-200d-1f9b3", "white_haired_man_medium_light_skin_tone": "1f468-1f3fc-200d-1f9b3", "white_haired_man_medium_skin_tone": "1f468-1f3fd-200d-1f9b3", "white_haired_man_medium_dark_skin_tone": "1f468-1f3fe-200d-1f9b3", "white_haired_man_dark_skin_tone": "1f468-1f3ff-200d-1f9b3", "man_in_motorized_wheelchair_light_skin_tone": "1f468-1f3fb-200d-1f9bc", "man_in_motorized_wheelchair_medium_light_skin_tone": "1f468-1f3fc-200d-1f9bc", "man_in_motorized_wheelchair_medium_skin_tone": "1f468-1f3fd-200d-1f9bc", "man_in_motorized_wheelchair_medium_dark_skin_tone": "1f468-1f3fe-200d-1f9bc", "man_in_motorized_wheelchair_dark_skin_tone": "1f468-1f3ff-200d-1f9bc", "man_in_manual_wheelchair_light_skin_tone": "1f468-1f3fb-200d-1f9bd", "man_in_manual_wheelchair_medium_light_skin_tone": "1f468-1f3fc-200d-1f9bd", "man_in_manual_wheelchair_medium_skin_tone": "1f468-1f3fd-200d-1f9bd", "man_in_manual_wheelchair_medium_dark_skin_tone": "1f468-1f3fe-200d-1f9bd", "man_in_manual_wheelchair_dark_skin_tone": "1f468-1f3ff-200d-1f9bd", "male-doctor_light_skin_tone": "1f468-1f3fb-200d-2695-fe0f", "man_health_worker_light_skin_tone": "1f468-1f3fb-200d-2695-fe0f", "male-doctor_medium_light_skin_tone": "1f468-1f3fc-200d-2695-fe0f", "man_health_worker_medium_light_skin_tone": "1f468-1f3fc-200d-2695-fe0f", "male-doctor_medium_skin_tone": "1f468-1f3fd-200d-2695-fe0f", "man_health_worker_medium_skin_tone": "1f468-1f3fd-200d-2695-fe0f", "male-doctor_medium_dark_skin_tone": "1f468-1f3fe-200d-2695-fe0f", "man_health_worker_medium_dark_skin_tone": "1f468-1f3fe-200d-2695-fe0f", "male-doctor_dark_skin_tone": "1f468-1f3ff-200d-2695-fe0f", "man_health_worker_dark_skin_tone": "1f468-1f3ff-200d-2695-fe0f", "male-judge_light_skin_tone": "1f468-1f3fb-200d-2696-fe0f", "man_judge_light_skin_tone": "1f468-1f3fb-200d-2696-fe0f", "male-judge_medium_light_skin_tone": "1f468-1f3fc-200d-2696-fe0f", "man_judge_medium_light_skin_tone": "1f468-1f3fc-200d-2696-fe0f", "male-judge_medium_skin_tone": "1f468-1f3fd-200d-2696-fe0f", "man_judge_medium_skin_tone": "1f468-1f3fd-200d-2696-fe0f", "male-judge_medium_dark_skin_tone": "1f468-1f3fe-200d-2696-fe0f", "man_judge_medium_dark_skin_tone": "1f468-1f3fe-200d-2696-fe0f", "male-judge_dark_skin_tone": "1f468-1f3ff-200d-2696-fe0f", "man_judge_dark_skin_tone": "1f468-1f3ff-200d-2696-fe0f", "male-pilot_light_skin_tone": "1f468-1f3fb-200d-2708-fe0f", "man_pilot_light_skin_tone": "1f468-1f3fb-200d-2708-fe0f", "male-pilot_medium_light_skin_tone": "1f468-1f3fc-200d-2708-fe0f", "man_pilot_medium_light_skin_tone": "1f468-1f3fc-200d-2708-fe0f", "male-pilot_medium_skin_tone": "1f468-1f3fd-200d-2708-fe0f", "man_pilot_medium_skin_tone": "1f468-1f3fd-200d-2708-fe0f", "male-pilot_medium_dark_skin_tone": "1f468-1f3fe-200d-2708-fe0f", "man_pilot_medium_dark_skin_tone": "1f468-1f3fe-200d-2708-fe0f", "male-pilot_dark_skin_tone": "1f468-1f3ff-200d-2708-fe0f", "man_pilot_dark_skin_tone": "1f468-1f3ff-200d-2708-fe0f", "man_light_skin_tone": "1f468-1f3fb", "man_medium_light_skin_tone": "1f468-1f3fc", "man_medium_skin_tone": "1f468-1f3fd", "man_medium_dark_skin_tone": "1f468-1f3fe", "man_dark_skin_tone": "1f468-1f3ff", "female-farmer_light_skin_tone": "1f469-1f3fb-200d-1f33e", "woman_farmer_light_skin_tone": "1f469-1f3fb-200d-1f33e", "female-farmer_medium_light_skin_tone": "1f469-1f3fc-200d-1f33e", "woman_farmer_medium_light_skin_tone": "1f469-1f3fc-200d-1f33e", "female-farmer_medium_skin_tone": "1f469-1f3fd-200d-1f33e", "woman_farmer_medium_skin_tone": "1f469-1f3fd-200d-1f33e", "female-farmer_medium_dark_skin_tone": "1f469-1f3fe-200d-1f33e", "woman_farmer_medium_dark_skin_tone": "1f469-1f3fe-200d-1f33e", "female-farmer_dark_skin_tone": "1f469-1f3ff-200d-1f33e", "woman_farmer_dark_skin_tone": "1f469-1f3ff-200d-1f33e", "female-cook_light_skin_tone": "1f469-1f3fb-200d-1f373", "woman_cook_light_skin_tone": "1f469-1f3fb-200d-1f373", "female-cook_medium_light_skin_tone": "1f469-1f3fc-200d-1f373", "woman_cook_medium_light_skin_tone": "1f469-1f3fc-200d-1f373", "female-cook_medium_skin_tone": "1f469-1f3fd-200d-1f373", "woman_cook_medium_skin_tone": "1f469-1f3fd-200d-1f373", "female-cook_medium_dark_skin_tone": "1f469-1f3fe-200d-1f373", "woman_cook_medium_dark_skin_tone": "1f469-1f3fe-200d-1f373", "female-cook_dark_skin_tone": "1f469-1f3ff-200d-1f373", "woman_cook_dark_skin_tone": "1f469-1f3ff-200d-1f373", "woman_feeding_baby_light_skin_tone": "1f469-1f3fb-200d-1f37c", "woman_feeding_baby_medium_light_skin_tone": "1f469-1f3fc-200d-1f37c", "woman_feeding_baby_medium_skin_tone": "1f469-1f3fd-200d-1f37c", "woman_feeding_baby_medium_dark_skin_tone": "1f469-1f3fe-200d-1f37c", "woman_feeding_baby_dark_skin_tone": "1f469-1f3ff-200d-1f37c", "female-student_light_skin_tone": "1f469-1f3fb-200d-1f393", "woman_student_light_skin_tone": "1f469-1f3fb-200d-1f393", "female-student_medium_light_skin_tone": "1f469-1f3fc-200d-1f393", "woman_student_medium_light_skin_tone": "1f469-1f3fc-200d-1f393", "female-student_medium_skin_tone": "1f469-1f3fd-200d-1f393", "woman_student_medium_skin_tone": "1f469-1f3fd-200d-1f393", "female-student_medium_dark_skin_tone": "1f469-1f3fe-200d-1f393", "woman_student_medium_dark_skin_tone": "1f469-1f3fe-200d-1f393", "female-student_dark_skin_tone": "1f469-1f3ff-200d-1f393", "woman_student_dark_skin_tone": "1f469-1f3ff-200d-1f393", "female-singer_light_skin_tone": "1f469-1f3fb-200d-1f3a4", "woman_singer_light_skin_tone": "1f469-1f3fb-200d-1f3a4", "female-singer_medium_light_skin_tone": "1f469-1f3fc-200d-1f3a4", "woman_singer_medium_light_skin_tone": "1f469-1f3fc-200d-1f3a4", "female-singer_medium_skin_tone": "1f469-1f3fd-200d-1f3a4", "woman_singer_medium_skin_tone": "1f469-1f3fd-200d-1f3a4", "female-singer_medium_dark_skin_tone": "1f469-1f3fe-200d-1f3a4", "woman_singer_medium_dark_skin_tone": "1f469-1f3fe-200d-1f3a4", "female-singer_dark_skin_tone": "1f469-1f3ff-200d-1f3a4", "woman_singer_dark_skin_tone": "1f469-1f3ff-200d-1f3a4", "female-artist_light_skin_tone": "1f469-1f3fb-200d-1f3a8", "woman_artist_light_skin_tone": "1f469-1f3fb-200d-1f3a8", "female-artist_medium_light_skin_tone": "1f469-1f3fc-200d-1f3a8", "woman_artist_medium_light_skin_tone": "1f469-1f3fc-200d-1f3a8", "female-artist_medium_skin_tone": "1f469-1f3fd-200d-1f3a8", "woman_artist_medium_skin_tone": "1f469-1f3fd-200d-1f3a8", "female-artist_medium_dark_skin_tone": "1f469-1f3fe-200d-1f3a8", "woman_artist_medium_dark_skin_tone": "1f469-1f3fe-200d-1f3a8", "female-artist_dark_skin_tone": "1f469-1f3ff-200d-1f3a8", "woman_artist_dark_skin_tone": "1f469-1f3ff-200d-1f3a8", "female-teacher_light_skin_tone": "1f469-1f3fb-200d-1f3eb", "woman_teacher_light_skin_tone": "1f469-1f3fb-200d-1f3eb", "female-teacher_medium_light_skin_tone": "1f469-1f3fc-200d-1f3eb", "woman_teacher_medium_light_skin_tone": "1f469-1f3fc-200d-1f3eb", "female-teacher_medium_skin_tone": "1f469-1f3fd-200d-1f3eb", "woman_teacher_medium_skin_tone": "1f469-1f3fd-200d-1f3eb", "female-teacher_medium_dark_skin_tone": "1f469-1f3fe-200d-1f3eb", "woman_teacher_medium_dark_skin_tone": "1f469-1f3fe-200d-1f3eb", "female-teacher_dark_skin_tone": "1f469-1f3ff-200d-1f3eb", "woman_teacher_dark_skin_tone": "1f469-1f3ff-200d-1f3eb", "female-factory-worker_light_skin_tone": "1f469-1f3fb-200d-1f3ed", "woman_factory_worker_light_skin_tone": "1f469-1f3fb-200d-1f3ed", "female-factory-worker_medium_light_skin_tone": "1f469-1f3fc-200d-1f3ed", "woman_factory_worker_medium_light_skin_tone": "1f469-1f3fc-200d-1f3ed", "female-factory-worker_medium_skin_tone": "1f469-1f3fd-200d-1f3ed", "woman_factory_worker_medium_skin_tone": "1f469-1f3fd-200d-1f3ed", "female-factory-worker_medium_dark_skin_tone": "1f469-1f3fe-200d-1f3ed", "woman_factory_worker_medium_dark_skin_tone": "1f469-1f3fe-200d-1f3ed", "female-factory-worker_dark_skin_tone": "1f469-1f3ff-200d-1f3ed", "woman_factory_worker_dark_skin_tone": "1f469-1f3ff-200d-1f3ed", "female-technologist_light_skin_tone": "1f469-1f3fb-200d-1f4bb", "woman_technologist_light_skin_tone": "1f469-1f3fb-200d-1f4bb", "female-technologist_medium_light_skin_tone": "1f469-1f3fc-200d-1f4bb", "woman_technologist_medium_light_skin_tone": "1f469-1f3fc-200d-1f4bb", "female-technologist_medium_skin_tone": "1f469-1f3fd-200d-1f4bb", "woman_technologist_medium_skin_tone": "1f469-1f3fd-200d-1f4bb", "female-technologist_medium_dark_skin_tone": "1f469-1f3fe-200d-1f4bb", "woman_technologist_medium_dark_skin_tone": "1f469-1f3fe-200d-1f4bb", "female-technologist_dark_skin_tone": "1f469-1f3ff-200d-1f4bb", "woman_technologist_dark_skin_tone": "1f469-1f3ff-200d-1f4bb", "female-office-worker_light_skin_tone": "1f469-1f3fb-200d-1f4bc", "woman_office_worker_light_skin_tone": "1f469-1f3fb-200d-1f4bc", "female-office-worker_medium_light_skin_tone": "1f469-1f3fc-200d-1f4bc", "woman_office_worker_medium_light_skin_tone": "1f469-1f3fc-200d-1f4bc", "female-office-worker_medium_skin_tone": "1f469-1f3fd-200d-1f4bc", "woman_office_worker_medium_skin_tone": "1f469-1f3fd-200d-1f4bc", "female-office-worker_medium_dark_skin_tone": "1f469-1f3fe-200d-1f4bc", "woman_office_worker_medium_dark_skin_tone": "1f469-1f3fe-200d-1f4bc", "female-office-worker_dark_skin_tone": "1f469-1f3ff-200d-1f4bc", "woman_office_worker_dark_skin_tone": "1f469-1f3ff-200d-1f4bc", "female-mechanic_light_skin_tone": "1f469-1f3fb-200d-1f527", "woman_mechanic_light_skin_tone": "1f469-1f3fb-200d-1f527", "female-mechanic_medium_light_skin_tone": "1f469-1f3fc-200d-1f527", "woman_mechanic_medium_light_skin_tone": "1f469-1f3fc-200d-1f527", "female-mechanic_medium_skin_tone": "1f469-1f3fd-200d-1f527", "woman_mechanic_medium_skin_tone": "1f469-1f3fd-200d-1f527", "female-mechanic_medium_dark_skin_tone": "1f469-1f3fe-200d-1f527", "woman_mechanic_medium_dark_skin_tone": "1f469-1f3fe-200d-1f527", "female-mechanic_dark_skin_tone": "1f469-1f3ff-200d-1f527", "woman_mechanic_dark_skin_tone": "1f469-1f3ff-200d-1f527", "female-scientist_light_skin_tone": "1f469-1f3fb-200d-1f52c", "woman_scientist_light_skin_tone": "1f469-1f3fb-200d-1f52c", "female-scientist_medium_light_skin_tone": "1f469-1f3fc-200d-1f52c", "woman_scientist_medium_light_skin_tone": "1f469-1f3fc-200d-1f52c", "female-scientist_medium_skin_tone": "1f469-1f3fd-200d-1f52c", "woman_scientist_medium_skin_tone": "1f469-1f3fd-200d-1f52c", "female-scientist_medium_dark_skin_tone": "1f469-1f3fe-200d-1f52c", "woman_scientist_medium_dark_skin_tone": "1f469-1f3fe-200d-1f52c", "female-scientist_dark_skin_tone": "1f469-1f3ff-200d-1f52c", "woman_scientist_dark_skin_tone": "1f469-1f3ff-200d-1f52c", "female-astronaut_light_skin_tone": "1f469-1f3fb-200d-1f680", "woman_astronaut_light_skin_tone": "1f469-1f3fb-200d-1f680", "female-astronaut_medium_light_skin_tone": "1f469-1f3fc-200d-1f680", "woman_astronaut_medium_light_skin_tone": "1f469-1f3fc-200d-1f680", "female-astronaut_medium_skin_tone": "1f469-1f3fd-200d-1f680", "woman_astronaut_medium_skin_tone": "1f469-1f3fd-200d-1f680", "female-astronaut_medium_dark_skin_tone": "1f469-1f3fe-200d-1f680", "woman_astronaut_medium_dark_skin_tone": "1f469-1f3fe-200d-1f680", "female-astronaut_dark_skin_tone": "1f469-1f3ff-200d-1f680", "woman_astronaut_dark_skin_tone": "1f469-1f3ff-200d-1f680", "female-firefighter_light_skin_tone": "1f469-1f3fb-200d-1f692", "woman_firefighter_light_skin_tone": "1f469-1f3fb-200d-1f692", "female-firefighter_medium_light_skin_tone": "1f469-1f3fc-200d-1f692", "woman_firefighter_medium_light_skin_tone": "1f469-1f3fc-200d-1f692", "female-firefighter_medium_skin_tone": "1f469-1f3fd-200d-1f692", "woman_firefighter_medium_skin_tone": "1f469-1f3fd-200d-1f692", "female-firefighter_medium_dark_skin_tone": "1f469-1f3fe-200d-1f692", "woman_firefighter_medium_dark_skin_tone": "1f469-1f3fe-200d-1f692", "female-firefighter_dark_skin_tone": "1f469-1f3ff-200d-1f692", "woman_firefighter_dark_skin_tone": "1f469-1f3ff-200d-1f692", "woman_with_probing_cane_light_skin_tone": "1f469-1f3fb-200d-1f9af", "woman_with_probing_cane_medium_light_skin_tone": "1f469-1f3fc-200d-1f9af", "woman_with_probing_cane_medium_skin_tone": "1f469-1f3fd-200d-1f9af", "woman_with_probing_cane_medium_dark_skin_tone": "1f469-1f3fe-200d-1f9af", "woman_with_probing_cane_dark_skin_tone": "1f469-1f3ff-200d-1f9af", "red_haired_woman_light_skin_tone": "1f469-1f3fb-200d-1f9b0", "red_haired_woman_medium_light_skin_tone": "1f469-1f3fc-200d-1f9b0", "red_haired_woman_medium_skin_tone": "1f469-1f3fd-200d-1f9b0", "red_haired_woman_medium_dark_skin_tone": "1f469-1f3fe-200d-1f9b0", "red_haired_woman_dark_skin_tone": "1f469-1f3ff-200d-1f9b0", "curly_haired_woman_light_skin_tone": "1f469-1f3fb-200d-1f9b1", "curly_haired_woman_medium_light_skin_tone": "1f469-1f3fc-200d-1f9b1", "curly_haired_woman_medium_skin_tone": "1f469-1f3fd-200d-1f9b1", "curly_haired_woman_medium_dark_skin_tone": "1f469-1f3fe-200d-1f9b1", "curly_haired_woman_dark_skin_tone": "1f469-1f3ff-200d-1f9b1", "bald_woman_light_skin_tone": "1f469-1f3fb-200d-1f9b2", "bald_woman_medium_light_skin_tone": "1f469-1f3fc-200d-1f9b2", "bald_woman_medium_skin_tone": "1f469-1f3fd-200d-1f9b2", "bald_woman_medium_dark_skin_tone": "1f469-1f3fe-200d-1f9b2", "bald_woman_dark_skin_tone": "1f469-1f3ff-200d-1f9b2", "white_haired_woman_light_skin_tone": "1f469-1f3fb-200d-1f9b3", "white_haired_woman_medium_light_skin_tone": "1f469-1f3fc-200d-1f9b3", "white_haired_woman_medium_skin_tone": "1f469-1f3fd-200d-1f9b3", "white_haired_woman_medium_dark_skin_tone": "1f469-1f3fe-200d-1f9b3", "white_haired_woman_dark_skin_tone": "1f469-1f3ff-200d-1f9b3", "woman_in_motorized_wheelchair_light_skin_tone": "1f469-1f3fb-200d-1f9bc", "woman_in_motorized_wheelchair_medium_light_skin_tone": "1f469-1f3fc-200d-1f9bc", "woman_in_motorized_wheelchair_medium_skin_tone": "1f469-1f3fd-200d-1f9bc", "woman_in_motorized_wheelchair_medium_dark_skin_tone": "1f469-1f3fe-200d-1f9bc", "woman_in_motorized_wheelchair_dark_skin_tone": "1f469-1f3ff-200d-1f9bc", "woman_in_manual_wheelchair_light_skin_tone": "1f469-1f3fb-200d-1f9bd", "woman_in_manual_wheelchair_medium_light_skin_tone": "1f469-1f3fc-200d-1f9bd", "woman_in_manual_wheelchair_medium_skin_tone": "1f469-1f3fd-200d-1f9bd", "woman_in_manual_wheelchair_medium_dark_skin_tone": "1f469-1f3fe-200d-1f9bd", "woman_in_manual_wheelchair_dark_skin_tone": "1f469-1f3ff-200d-1f9bd", "female-doctor_light_skin_tone": "1f469-1f3fb-200d-2695-fe0f", "woman_health_worker_light_skin_tone": "1f469-1f3fb-200d-2695-fe0f", "female-doctor_medium_light_skin_tone": "1f469-1f3fc-200d-2695-fe0f", "woman_health_worker_medium_light_skin_tone": "1f469-1f3fc-200d-2695-fe0f", "female-doctor_medium_skin_tone": "1f469-1f3fd-200d-2695-fe0f", "woman_health_worker_medium_skin_tone": "1f469-1f3fd-200d-2695-fe0f", "female-doctor_medium_dark_skin_tone": "1f469-1f3fe-200d-2695-fe0f", "woman_health_worker_medium_dark_skin_tone": "1f469-1f3fe-200d-2695-fe0f", "female-doctor_dark_skin_tone": "1f469-1f3ff-200d-2695-fe0f", "woman_health_worker_dark_skin_tone": "1f469-1f3ff-200d-2695-fe0f", "female-judge_light_skin_tone": "1f469-1f3fb-200d-2696-fe0f", "woman_judge_light_skin_tone": "1f469-1f3fb-200d-2696-fe0f", "female-judge_medium_light_skin_tone": "1f469-1f3fc-200d-2696-fe0f", "woman_judge_medium_light_skin_tone": "1f469-1f3fc-200d-2696-fe0f", "female-judge_medium_skin_tone": "1f469-1f3fd-200d-2696-fe0f", "woman_judge_medium_skin_tone": "1f469-1f3fd-200d-2696-fe0f", "female-judge_medium_dark_skin_tone": "1f469-1f3fe-200d-2696-fe0f", "woman_judge_medium_dark_skin_tone": "1f469-1f3fe-200d-2696-fe0f", "female-judge_dark_skin_tone": "1f469-1f3ff-200d-2696-fe0f", "woman_judge_dark_skin_tone": "1f469-1f3ff-200d-2696-fe0f", "female-pilot_light_skin_tone": "1f469-1f3fb-200d-2708-fe0f", "woman_pilot_light_skin_tone": "1f469-1f3fb-200d-2708-fe0f", "female-pilot_medium_light_skin_tone": "1f469-1f3fc-200d-2708-fe0f", "woman_pilot_medium_light_skin_tone": "1f469-1f3fc-200d-2708-fe0f", "female-pilot_medium_skin_tone": "1f469-1f3fd-200d-2708-fe0f", "woman_pilot_medium_skin_tone": "1f469-1f3fd-200d-2708-fe0f", "female-pilot_medium_dark_skin_tone": "1f469-1f3fe-200d-2708-fe0f", "woman_pilot_medium_dark_skin_tone": "1f469-1f3fe-200d-2708-fe0f", "female-pilot_dark_skin_tone": "1f469-1f3ff-200d-2708-fe0f", "woman_pilot_dark_skin_tone": "1f469-1f3ff-200d-2708-fe0f", "woman_light_skin_tone": "1f469-1f3fb", "woman_medium_light_skin_tone": "1f469-1f3fc", "woman_medium_skin_tone": "1f469-1f3fd", "woman_medium_dark_skin_tone": "1f469-1f3fe", "woman_dark_skin_tone": "1f469-1f3ff", "man_and_woman_holding_hands_light_skin_tone": "1f46b-1f3fb", "woman_and_man_holding_hands_light_skin_tone": "1f46b-1f3fb", "couple_light_skin_tone": "1f46b-1f3fb", "man_and_woman_holding_hands_medium_light_skin_tone": "1f46b-1f3fc", "woman_and_man_holding_hands_medium_light_skin_tone": "1f46b-1f3fc", "couple_medium_light_skin_tone": "1f46b-1f3fc", "man_and_woman_holding_hands_medium_skin_tone": "1f46b-1f3fd", "woman_and_man_holding_hands_medium_skin_tone": "1f46b-1f3fd", "couple_medium_skin_tone": "1f46b-1f3fd", "man_and_woman_holding_hands_medium_dark_skin_tone": "1f46b-1f3fe", "woman_and_man_holding_hands_medium_dark_skin_tone": "1f46b-1f3fe", "couple_medium_dark_skin_tone": "1f46b-1f3fe", "man_and_woman_holding_hands_dark_skin_tone": "1f46b-1f3ff", "woman_and_man_holding_hands_dark_skin_tone": "1f46b-1f3ff", "couple_dark_skin_tone": "1f46b-1f3ff", "man_and_woman_holding_hands_light_skin_tone_medium_light_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc", "woman_and_man_holding_hands_light_skin_tone_medium_light_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc", "couple_light_skin_tone_medium_light_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc", "man_and_woman_holding_hands_light_skin_tone_medium_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd", "woman_and_man_holding_hands_light_skin_tone_medium_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd", "couple_light_skin_tone_medium_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd", "man_and_woman_holding_hands_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe", "woman_and_man_holding_hands_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe", "couple_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe", "man_and_woman_holding_hands_light_skin_tone_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff", "woman_and_man_holding_hands_light_skin_tone_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff", "couple_light_skin_tone_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff", "man_and_woman_holding_hands_medium_light_skin_tone_light_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb", "woman_and_man_holding_hands_medium_light_skin_tone_light_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb", "couple_medium_light_skin_tone_light_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb", "man_and_woman_holding_hands_medium_light_skin_tone_medium_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd", "woman_and_man_holding_hands_medium_light_skin_tone_medium_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd", "couple_medium_light_skin_tone_medium_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd", "man_and_woman_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe", "woman_and_man_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe", "couple_medium_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe", "man_and_woman_holding_hands_medium_light_skin_tone_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff", "woman_and_man_holding_hands_medium_light_skin_tone_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff", "couple_medium_light_skin_tone_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff", "man_and_woman_holding_hands_medium_skin_tone_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb", "woman_and_man_holding_hands_medium_skin_tone_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb", "couple_medium_skin_tone_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb", "man_and_woman_holding_hands_medium_skin_tone_medium_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc", "woman_and_man_holding_hands_medium_skin_tone_medium_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc", "couple_medium_skin_tone_medium_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc", "man_and_woman_holding_hands_medium_skin_tone_medium_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe", "woman_and_man_holding_hands_medium_skin_tone_medium_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe", "couple_medium_skin_tone_medium_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe", "man_and_woman_holding_hands_medium_skin_tone_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff", "woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff", "couple_medium_skin_tone_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff", "man_and_woman_holding_hands_medium_dark_skin_tone_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb", "woman_and_man_holding_hands_medium_dark_skin_tone_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb", "couple_medium_dark_skin_tone_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb", "man_and_woman_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc", "woman_and_man_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc", "couple_medium_dark_skin_tone_medium_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc", "man_and_woman_holding_hands_medium_dark_skin_tone_medium_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd", "woman_and_man_holding_hands_medium_dark_skin_tone_medium_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd", "couple_medium_dark_skin_tone_medium_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd", "man_and_woman_holding_hands_medium_dark_skin_tone_dark_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff", "woman_and_man_holding_hands_medium_dark_skin_tone_dark_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff", "couple_medium_dark_skin_tone_dark_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff", "man_and_woman_holding_hands_dark_skin_tone_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb", "woman_and_man_holding_hands_dark_skin_tone_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb", "couple_dark_skin_tone_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb", "man_and_woman_holding_hands_dark_skin_tone_medium_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc", "woman_and_man_holding_hands_dark_skin_tone_medium_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc", "couple_dark_skin_tone_medium_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc", "man_and_woman_holding_hands_dark_skin_tone_medium_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd", "woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd", "couple_dark_skin_tone_medium_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd", "man_and_woman_holding_hands_dark_skin_tone_medium_dark_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe", "woman_and_man_holding_hands_dark_skin_tone_medium_dark_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe", "couple_dark_skin_tone_medium_dark_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe", "two_men_holding_hands_light_skin_tone": "1f46c-1f3fb", "men_holding_hands_light_skin_tone": "1f46c-1f3fb", "two_men_holding_hands_medium_light_skin_tone": "1f46c-1f3fc", "men_holding_hands_medium_light_skin_tone": "1f46c-1f3fc", "two_men_holding_hands_medium_skin_tone": "1f46c-1f3fd", "men_holding_hands_medium_skin_tone": "1f46c-1f3fd", "two_men_holding_hands_medium_dark_skin_tone": "1f46c-1f3fe", "men_holding_hands_medium_dark_skin_tone": "1f46c-1f3fe", "two_men_holding_hands_dark_skin_tone": "1f46c-1f3ff", "men_holding_hands_dark_skin_tone": "1f46c-1f3ff", "two_men_holding_hands_light_skin_tone_medium_light_skin_tone": "1f468-1f3fb-200d-1f91d-200d-1f468-1f3fc", "men_holding_hands_light_skin_tone_medium_light_skin_tone": "1f468-1f3fb-200d-1f91d-200d-1f468-1f3fc", "two_men_holding_hands_light_skin_tone_medium_skin_tone": "1f468-1f3fb-200d-1f91d-200d-1f468-1f3fd", "men_holding_hands_light_skin_tone_medium_skin_tone": "1f468-1f3fb-200d-1f91d-200d-1f468-1f3fd", "two_men_holding_hands_light_skin_tone_medium_dark_skin_tone": "1f468-1f3fb-200d-1f91d-200d-1f468-1f3fe", "men_holding_hands_light_skin_tone_medium_dark_skin_tone": "1f468-1f3fb-200d-1f91d-200d-1f468-1f3fe", "two_men_holding_hands_light_skin_tone_dark_skin_tone": "1f468-1f3fb-200d-1f91d-200d-1f468-1f3ff", "men_holding_hands_light_skin_tone_dark_skin_tone": "1f468-1f3fb-200d-1f91d-200d-1f468-1f3ff", "two_men_holding_hands_medium_light_skin_tone_light_skin_tone": "1f468-1f3fc-200d-1f91d-200d-1f468-1f3fb", "men_holding_hands_medium_light_skin_tone_light_skin_tone": "1f468-1f3fc-200d-1f91d-200d-1f468-1f3fb", "two_men_holding_hands_medium_light_skin_tone_medium_skin_tone": "1f468-1f3fc-200d-1f91d-200d-1f468-1f3fd", "men_holding_hands_medium_light_skin_tone_medium_skin_tone": "1f468-1f3fc-200d-1f91d-200d-1f468-1f3fd", "two_men_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "1f468-1f3fc-200d-1f91d-200d-1f468-1f3fe", "men_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "1f468-1f3fc-200d-1f91d-200d-1f468-1f3fe", "two_men_holding_hands_medium_light_skin_tone_dark_skin_tone": "1f468-1f3fc-200d-1f91d-200d-1f468-1f3ff", "men_holding_hands_medium_light_skin_tone_dark_skin_tone": "1f468-1f3fc-200d-1f91d-200d-1f468-1f3ff", "two_men_holding_hands_medium_skin_tone_light_skin_tone": "1f468-1f3fd-200d-1f91d-200d-1f468-1f3fb", "men_holding_hands_medium_skin_tone_light_skin_tone": "1f468-1f3fd-200d-1f91d-200d-1f468-1f3fb", "two_men_holding_hands_medium_skin_tone_medium_light_skin_tone": "1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc", "men_holding_hands_medium_skin_tone_medium_light_skin_tone": "1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc", "two_men_holding_hands_medium_skin_tone_medium_dark_skin_tone": "1f468-1f3fd-200d-1f91d-200d-1f468-1f3fe", "men_holding_hands_medium_skin_tone_medium_dark_skin_tone": "1f468-1f3fd-200d-1f91d-200d-1f468-1f3fe", "two_men_holding_hands_medium_skin_tone_dark_skin_tone": "1f468-1f3fd-200d-1f91d-200d-1f468-1f3ff", "men_holding_hands_medium_skin_tone_dark_skin_tone": "1f468-1f3fd-200d-1f91d-200d-1f468-1f3ff", "two_men_holding_hands_medium_dark_skin_tone_light_skin_tone": "1f468-1f3fe-200d-1f91d-200d-1f468-1f3fb", "men_holding_hands_medium_dark_skin_tone_light_skin_tone": "1f468-1f3fe-200d-1f91d-200d-1f468-1f3fb", "two_men_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "1f468-1f3fe-200d-1f91d-200d-1f468-1f3fc", "men_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "1f468-1f3fe-200d-1f91d-200d-1f468-1f3fc", "two_men_holding_hands_medium_dark_skin_tone_medium_skin_tone": "1f468-1f3fe-200d-1f91d-200d-1f468-1f3fd", "men_holding_hands_medium_dark_skin_tone_medium_skin_tone": "1f468-1f3fe-200d-1f91d-200d-1f468-1f3fd", "two_men_holding_hands_medium_dark_skin_tone_dark_skin_tone": "1f468-1f3fe-200d-1f91d-200d-1f468-1f3ff", "men_holding_hands_medium_dark_skin_tone_dark_skin_tone": "1f468-1f3fe-200d-1f91d-200d-1f468-1f3ff", "two_men_holding_hands_dark_skin_tone_light_skin_tone": "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fb", "men_holding_hands_dark_skin_tone_light_skin_tone": "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fb", "two_men_holding_hands_dark_skin_tone_medium_light_skin_tone": "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fc", "men_holding_hands_dark_skin_tone_medium_light_skin_tone": "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fc", "two_men_holding_hands_dark_skin_tone_medium_skin_tone": "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fd", "men_holding_hands_dark_skin_tone_medium_skin_tone": "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fd", "two_men_holding_hands_dark_skin_tone_medium_dark_skin_tone": "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fe", "men_holding_hands_dark_skin_tone_medium_dark_skin_tone": "1f468-1f3ff-200d-1f91d-200d-1f468-1f3fe", "two_women_holding_hands_light_skin_tone": "1f46d-1f3fb", "women_holding_hands_light_skin_tone": "1f46d-1f3fb", "two_women_holding_hands_medium_light_skin_tone": "1f46d-1f3fc", "women_holding_hands_medium_light_skin_tone": "1f46d-1f3fc", "two_women_holding_hands_medium_skin_tone": "1f46d-1f3fd", "women_holding_hands_medium_skin_tone": "1f46d-1f3fd", "two_women_holding_hands_medium_dark_skin_tone": "1f46d-1f3fe", "women_holding_hands_medium_dark_skin_tone": "1f46d-1f3fe", "two_women_holding_hands_dark_skin_tone": "1f46d-1f3ff", "women_holding_hands_dark_skin_tone": "1f46d-1f3ff", "two_women_holding_hands_light_skin_tone_medium_light_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f469-1f3fc", "women_holding_hands_light_skin_tone_medium_light_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f469-1f3fc", "two_women_holding_hands_light_skin_tone_medium_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f469-1f3fd", "women_holding_hands_light_skin_tone_medium_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f469-1f3fd", "two_women_holding_hands_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f469-1f3fe", "women_holding_hands_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f469-1f3fe", "two_women_holding_hands_light_skin_tone_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f469-1f3ff", "women_holding_hands_light_skin_tone_dark_skin_tone": "1f469-1f3fb-200d-1f91d-200d-1f469-1f3ff", "two_women_holding_hands_medium_light_skin_tone_light_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f469-1f3fb", "women_holding_hands_medium_light_skin_tone_light_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f469-1f3fb", "two_women_holding_hands_medium_light_skin_tone_medium_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f469-1f3fd", "women_holding_hands_medium_light_skin_tone_medium_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f469-1f3fd", "two_women_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f469-1f3fe", "women_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f469-1f3fe", "two_women_holding_hands_medium_light_skin_tone_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f469-1f3ff", "women_holding_hands_medium_light_skin_tone_dark_skin_tone": "1f469-1f3fc-200d-1f91d-200d-1f469-1f3ff", "two_women_holding_hands_medium_skin_tone_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f469-1f3fb", "women_holding_hands_medium_skin_tone_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f469-1f3fb", "two_women_holding_hands_medium_skin_tone_medium_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f469-1f3fc", "women_holding_hands_medium_skin_tone_medium_light_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f469-1f3fc", "two_women_holding_hands_medium_skin_tone_medium_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f469-1f3fe", "women_holding_hands_medium_skin_tone_medium_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f469-1f3fe", "two_women_holding_hands_medium_skin_tone_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f469-1f3ff", "women_holding_hands_medium_skin_tone_dark_skin_tone": "1f469-1f3fd-200d-1f91d-200d-1f469-1f3ff", "two_women_holding_hands_medium_dark_skin_tone_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f469-1f3fb", "women_holding_hands_medium_dark_skin_tone_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f469-1f3fb", "two_women_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f469-1f3fc", "women_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f469-1f3fc", "two_women_holding_hands_medium_dark_skin_tone_medium_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f469-1f3fd", "women_holding_hands_medium_dark_skin_tone_medium_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f469-1f3fd", "two_women_holding_hands_medium_dark_skin_tone_dark_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f469-1f3ff", "women_holding_hands_medium_dark_skin_tone_dark_skin_tone": "1f469-1f3fe-200d-1f91d-200d-1f469-1f3ff", "two_women_holding_hands_dark_skin_tone_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fb", "women_holding_hands_dark_skin_tone_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fb", "two_women_holding_hands_dark_skin_tone_medium_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fc", "women_holding_hands_dark_skin_tone_medium_light_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fc", "two_women_holding_hands_dark_skin_tone_medium_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fd", "women_holding_hands_dark_skin_tone_medium_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fd", "two_women_holding_hands_dark_skin_tone_medium_dark_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fe", "women_holding_hands_dark_skin_tone_medium_dark_skin_tone": "1f469-1f3ff-200d-1f91d-200d-1f469-1f3fe", "female-police-officer_light_skin_tone": "1f46e-1f3fb-200d-2640-fe0f", "policewoman_light_skin_tone": "1f46e-1f3fb-200d-2640-fe0f", "female-police-officer_medium_light_skin_tone": "1f46e-1f3fc-200d-2640-fe0f", "policewoman_medium_light_skin_tone": "1f46e-1f3fc-200d-2640-fe0f", "female-police-officer_medium_skin_tone": "1f46e-1f3fd-200d-2640-fe0f", "policewoman_medium_skin_tone": "1f46e-1f3fd-200d-2640-fe0f", "female-police-officer_medium_dark_skin_tone": "1f46e-1f3fe-200d-2640-fe0f", "policewoman_medium_dark_skin_tone": "1f46e-1f3fe-200d-2640-fe0f", "female-police-officer_dark_skin_tone": "1f46e-1f3ff-200d-2640-fe0f", "policewoman_dark_skin_tone": "1f46e-1f3ff-200d-2640-fe0f", "male-police-officer_light_skin_tone": "1f46e-1f3fb-200d-2642-fe0f", "policeman_light_skin_tone": "1f46e-1f3fb-200d-2642-fe0f", "male-police-officer_medium_light_skin_tone": "1f46e-1f3fc-200d-2642-fe0f", "policeman_medium_light_skin_tone": "1f46e-1f3fc-200d-2642-fe0f", "male-police-officer_medium_skin_tone": "1f46e-1f3fd-200d-2642-fe0f", "policeman_medium_skin_tone": "1f46e-1f3fd-200d-2642-fe0f", "male-police-officer_medium_dark_skin_tone": "1f46e-1f3fe-200d-2642-fe0f", "policeman_medium_dark_skin_tone": "1f46e-1f3fe-200d-2642-fe0f", "male-police-officer_dark_skin_tone": "1f46e-1f3ff-200d-2642-fe0f", "policeman_dark_skin_tone": "1f46e-1f3ff-200d-2642-fe0f", "cop_light_skin_tone": "1f46e-1f3fb", "cop_medium_light_skin_tone": "1f46e-1f3fc", "cop_medium_skin_tone": "1f46e-1f3fd", "cop_medium_dark_skin_tone": "1f46e-1f3fe", "cop_dark_skin_tone": "1f46e-1f3ff", "woman_with_veil_light_skin_tone": "1f470-1f3fb-200d-2640-fe0f", "woman_with_veil_medium_light_skin_tone": "1f470-1f3fc-200d-2640-fe0f", "woman_with_veil_medium_skin_tone": "1f470-1f3fd-200d-2640-fe0f", "woman_with_veil_medium_dark_skin_tone": "1f470-1f3fe-200d-2640-fe0f", "woman_with_veil_dark_skin_tone": "1f470-1f3ff-200d-2640-fe0f", "man_with_veil_light_skin_tone": "1f470-1f3fb-200d-2642-fe0f", "man_with_veil_medium_light_skin_tone": "1f470-1f3fc-200d-2642-fe0f", "man_with_veil_medium_skin_tone": "1f470-1f3fd-200d-2642-fe0f", "man_with_veil_medium_dark_skin_tone": "1f470-1f3fe-200d-2642-fe0f", "man_with_veil_dark_skin_tone": "1f470-1f3ff-200d-2642-fe0f", "bride_with_veil_light_skin_tone": "1f470-1f3fb", "bride_with_veil_medium_light_skin_tone": "1f470-1f3fc", "bride_with_veil_medium_skin_tone": "1f470-1f3fd", "bride_with_veil_medium_dark_skin_tone": "1f470-1f3fe", "bride_with_veil_dark_skin_tone": "1f470-1f3ff", "blond-haired-woman_light_skin_tone": "1f471-1f3fb-200d-2640-fe0f", "blonde_woman_light_skin_tone": "1f471-1f3fb-200d-2640-fe0f", "blond-haired-woman_medium_light_skin_tone": "1f471-1f3fc-200d-2640-fe0f", "blonde_woman_medium_light_skin_tone": "1f471-1f3fc-200d-2640-fe0f", "blond-haired-woman_medium_skin_tone": "1f471-1f3fd-200d-2640-fe0f", "blonde_woman_medium_skin_tone": "1f471-1f3fd-200d-2640-fe0f", "blond-haired-woman_medium_dark_skin_tone": "1f471-1f3fe-200d-2640-fe0f", "blonde_woman_medium_dark_skin_tone": "1f471-1f3fe-200d-2640-fe0f", "blond-haired-woman_dark_skin_tone": "1f471-1f3ff-200d-2640-fe0f", "blonde_woman_dark_skin_tone": "1f471-1f3ff-200d-2640-fe0f", "blond-haired-man_light_skin_tone": "1f471-1f3fb-200d-2642-fe0f", "blonde_man_light_skin_tone": "1f471-1f3fb-200d-2642-fe0f", "blond-haired-man_medium_light_skin_tone": "1f471-1f3fc-200d-2642-fe0f", "blonde_man_medium_light_skin_tone": "1f471-1f3fc-200d-2642-fe0f", "blond-haired-man_medium_skin_tone": "1f471-1f3fd-200d-2642-fe0f", "blonde_man_medium_skin_tone": "1f471-1f3fd-200d-2642-fe0f", "blond-haired-man_medium_dark_skin_tone": "1f471-1f3fe-200d-2642-fe0f", "blonde_man_medium_dark_skin_tone": "1f471-1f3fe-200d-2642-fe0f", "blond-haired-man_dark_skin_tone": "1f471-1f3ff-200d-2642-fe0f", "blonde_man_dark_skin_tone": "1f471-1f3ff-200d-2642-fe0f", "person_with_blond_hair_light_skin_tone": "1f471-1f3fb", "person_with_blond_hair_medium_light_skin_tone": "1f471-1f3fc", "person_with_blond_hair_medium_skin_tone": "1f471-1f3fd", "person_with_blond_hair_medium_dark_skin_tone": "1f471-1f3fe", "person_with_blond_hair_dark_skin_tone": "1f471-1f3ff", "man_with_gua_pi_mao_light_skin_tone": "1f472-1f3fb", "man_with_gua_pi_mao_medium_light_skin_tone": "1f472-1f3fc", "man_with_gua_pi_mao_medium_skin_tone": "1f472-1f3fd", "man_with_gua_pi_mao_medium_dark_skin_tone": "1f472-1f3fe", "man_with_gua_pi_mao_dark_skin_tone": "1f472-1f3ff", "woman-wearing-turban_light_skin_tone": "1f473-1f3fb-200d-2640-fe0f", "woman_with_turban_light_skin_tone": "1f473-1f3fb-200d-2640-fe0f", "woman-wearing-turban_medium_light_skin_tone": "1f473-1f3fc-200d-2640-fe0f", "woman_with_turban_medium_light_skin_tone": "1f473-1f3fc-200d-2640-fe0f", "woman-wearing-turban_medium_skin_tone": "1f473-1f3fd-200d-2640-fe0f", "woman_with_turban_medium_skin_tone": "1f473-1f3fd-200d-2640-fe0f", "woman-wearing-turban_medium_dark_skin_tone": "1f473-1f3fe-200d-2640-fe0f", "woman_with_turban_medium_dark_skin_tone": "1f473-1f3fe-200d-2640-fe0f", "woman-wearing-turban_dark_skin_tone": "1f473-1f3ff-200d-2640-fe0f", "woman_with_turban_dark_skin_tone": "1f473-1f3ff-200d-2640-fe0f", "man-wearing-turban_light_skin_tone": "1f473-1f3fb-200d-2642-fe0f", "man-wearing-turban_medium_light_skin_tone": "1f473-1f3fc-200d-2642-fe0f", "man-wearing-turban_medium_skin_tone": "1f473-1f3fd-200d-2642-fe0f", "man-wearing-turban_medium_dark_skin_tone": "1f473-1f3fe-200d-2642-fe0f", "man-wearing-turban_dark_skin_tone": "1f473-1f3ff-200d-2642-fe0f", "man_with_turban_light_skin_tone": "1f473-1f3fb", "man_with_turban_medium_light_skin_tone": "1f473-1f3fc", "man_with_turban_medium_skin_tone": "1f473-1f3fd", "man_with_turban_medium_dark_skin_tone": "1f473-1f3fe", "man_with_turban_dark_skin_tone": "1f473-1f3ff", "older_man_light_skin_tone": "1f474-1f3fb", "older_man_medium_light_skin_tone": "1f474-1f3fc", "older_man_medium_skin_tone": "1f474-1f3fd", "older_man_medium_dark_skin_tone": "1f474-1f3fe", "older_man_dark_skin_tone": "1f474-1f3ff", "older_woman_light_skin_tone": "1f475-1f3fb", "older_woman_medium_light_skin_tone": "1f475-1f3fc", "older_woman_medium_skin_tone": "1f475-1f3fd", "older_woman_medium_dark_skin_tone": "1f475-1f3fe", "older_woman_dark_skin_tone": "1f475-1f3ff", "baby_light_skin_tone": "1f476-1f3fb", "baby_medium_light_skin_tone": "1f476-1f3fc", "baby_medium_skin_tone": "1f476-1f3fd", "baby_medium_dark_skin_tone": "1f476-1f3fe", "baby_dark_skin_tone": "1f476-1f3ff", "female-construction-worker_light_skin_tone": "1f477-1f3fb-200d-2640-fe0f", "construction_worker_woman_light_skin_tone": "1f477-1f3fb-200d-2640-fe0f", "female-construction-worker_medium_light_skin_tone": "1f477-1f3fc-200d-2640-fe0f", "construction_worker_woman_medium_light_skin_tone": "1f477-1f3fc-200d-2640-fe0f", "female-construction-worker_medium_skin_tone": "1f477-1f3fd-200d-2640-fe0f", "construction_worker_woman_medium_skin_tone": "1f477-1f3fd-200d-2640-fe0f", "female-construction-worker_medium_dark_skin_tone": "1f477-1f3fe-200d-2640-fe0f", "construction_worker_woman_medium_dark_skin_tone": "1f477-1f3fe-200d-2640-fe0f", "female-construction-worker_dark_skin_tone": "1f477-1f3ff-200d-2640-fe0f", "construction_worker_woman_dark_skin_tone": "1f477-1f3ff-200d-2640-fe0f", "male-construction-worker_light_skin_tone": "1f477-1f3fb-200d-2642-fe0f", "construction_worker_man_light_skin_tone": "1f477-1f3fb-200d-2642-fe0f", "male-construction-worker_medium_light_skin_tone": "1f477-1f3fc-200d-2642-fe0f", "construction_worker_man_medium_light_skin_tone": "1f477-1f3fc-200d-2642-fe0f", "male-construction-worker_medium_skin_tone": "1f477-1f3fd-200d-2642-fe0f", "construction_worker_man_medium_skin_tone": "1f477-1f3fd-200d-2642-fe0f", "male-construction-worker_medium_dark_skin_tone": "1f477-1f3fe-200d-2642-fe0f", "construction_worker_man_medium_dark_skin_tone": "1f477-1f3fe-200d-2642-fe0f", "male-construction-worker_dark_skin_tone": "1f477-1f3ff-200d-2642-fe0f", "construction_worker_man_dark_skin_tone": "1f477-1f3ff-200d-2642-fe0f", "construction_worker_light_skin_tone": "1f477-1f3fb", "construction_worker_medium_light_skin_tone": "1f477-1f3fc", "construction_worker_medium_skin_tone": "1f477-1f3fd", "construction_worker_medium_dark_skin_tone": "1f477-1f3fe", "construction_worker_dark_skin_tone": "1f477-1f3ff", "princess_light_skin_tone": "1f478-1f3fb", "princess_medium_light_skin_tone": "1f478-1f3fc", "princess_medium_skin_tone": "1f478-1f3fd", "princess_medium_dark_skin_tone": "1f478-1f3fe", "princess_dark_skin_tone": "1f478-1f3ff", "angel_light_skin_tone": "1f47c-1f3fb", "angel_medium_light_skin_tone": "1f47c-1f3fc", "angel_medium_skin_tone": "1f47c-1f3fd", "angel_medium_dark_skin_tone": "1f47c-1f3fe", "angel_dark_skin_tone": "1f47c-1f3ff", "woman-tipping-hand_light_skin_tone": "1f481-1f3fb-200d-2640-fe0f", "tipping_hand_woman_light_skin_tone": "1f481-1f3fb-200d-2640-fe0f", "woman-tipping-hand_medium_light_skin_tone": "1f481-1f3fc-200d-2640-fe0f", "tipping_hand_woman_medium_light_skin_tone": "1f481-1f3fc-200d-2640-fe0f", "woman-tipping-hand_medium_skin_tone": "1f481-1f3fd-200d-2640-fe0f", "tipping_hand_woman_medium_skin_tone": "1f481-1f3fd-200d-2640-fe0f", "woman-tipping-hand_medium_dark_skin_tone": "1f481-1f3fe-200d-2640-fe0f", "tipping_hand_woman_medium_dark_skin_tone": "1f481-1f3fe-200d-2640-fe0f", "woman-tipping-hand_dark_skin_tone": "1f481-1f3ff-200d-2640-fe0f", "tipping_hand_woman_dark_skin_tone": "1f481-1f3ff-200d-2640-fe0f", "man-tipping-hand_light_skin_tone": "1f481-1f3fb-200d-2642-fe0f", "tipping_hand_man_light_skin_tone": "1f481-1f3fb-200d-2642-fe0f", "man-tipping-hand_medium_light_skin_tone": "1f481-1f3fc-200d-2642-fe0f", "tipping_hand_man_medium_light_skin_tone": "1f481-1f3fc-200d-2642-fe0f", "man-tipping-hand_medium_skin_tone": "1f481-1f3fd-200d-2642-fe0f", "tipping_hand_man_medium_skin_tone": "1f481-1f3fd-200d-2642-fe0f", "man-tipping-hand_medium_dark_skin_tone": "1f481-1f3fe-200d-2642-fe0f", "tipping_hand_man_medium_dark_skin_tone": "1f481-1f3fe-200d-2642-fe0f", "man-tipping-hand_dark_skin_tone": "1f481-1f3ff-200d-2642-fe0f", "tipping_hand_man_dark_skin_tone": "1f481-1f3ff-200d-2642-fe0f", "information_desk_person_light_skin_tone": "1f481-1f3fb", "information_desk_person_medium_light_skin_tone": "1f481-1f3fc", "information_desk_person_medium_skin_tone": "1f481-1f3fd", "information_desk_person_medium_dark_skin_tone": "1f481-1f3fe", "information_desk_person_dark_skin_tone": "1f481-1f3ff", "female-guard_light_skin_tone": "1f482-1f3fb-200d-2640-fe0f", "guardswoman_light_skin_tone": "1f482-1f3fb-200d-2640-fe0f", "female-guard_medium_light_skin_tone": "1f482-1f3fc-200d-2640-fe0f", "guardswoman_medium_light_skin_tone": "1f482-1f3fc-200d-2640-fe0f", "female-guard_medium_skin_tone": "1f482-1f3fd-200d-2640-fe0f", "guardswoman_medium_skin_tone": "1f482-1f3fd-200d-2640-fe0f", "female-guard_medium_dark_skin_tone": "1f482-1f3fe-200d-2640-fe0f", "guardswoman_medium_dark_skin_tone": "1f482-1f3fe-200d-2640-fe0f", "female-guard_dark_skin_tone": "1f482-1f3ff-200d-2640-fe0f", "guardswoman_dark_skin_tone": "1f482-1f3ff-200d-2640-fe0f", "male-guard_light_skin_tone": "1f482-1f3fb-200d-2642-fe0f", "male-guard_medium_light_skin_tone": "1f482-1f3fc-200d-2642-fe0f", "male-guard_medium_skin_tone": "1f482-1f3fd-200d-2642-fe0f", "male-guard_medium_dark_skin_tone": "1f482-1f3fe-200d-2642-fe0f", "male-guard_dark_skin_tone": "1f482-1f3ff-200d-2642-fe0f", "guardsman_light_skin_tone": "1f482-1f3fb", "guardsman_medium_light_skin_tone": "1f482-1f3fc", "guardsman_medium_skin_tone": "1f482-1f3fd", "guardsman_medium_dark_skin_tone": "1f482-1f3fe", "guardsman_dark_skin_tone": "1f482-1f3ff", "dancer_light_skin_tone": "1f483-1f3fb", "dancer_medium_light_skin_tone": "1f483-1f3fc", "dancer_medium_skin_tone": "1f483-1f3fd", "dancer_medium_dark_skin_tone": "1f483-1f3fe", "dancer_dark_skin_tone": "1f483-1f3ff", "nail_care_light_skin_tone": "1f485-1f3fb", "nail_care_medium_light_skin_tone": "1f485-1f3fc", "nail_care_medium_skin_tone": "1f485-1f3fd", "nail_care_medium_dark_skin_tone": "1f485-1f3fe", "nail_care_dark_skin_tone": "1f485-1f3ff", "woman-getting-massage_light_skin_tone": "1f486-1f3fb-200d-2640-fe0f", "massage_woman_light_skin_tone": "1f486-1f3fb-200d-2640-fe0f", "woman-getting-massage_medium_light_skin_tone": "1f486-1f3fc-200d-2640-fe0f", "massage_woman_medium_light_skin_tone": "1f486-1f3fc-200d-2640-fe0f", "woman-getting-massage_medium_skin_tone": "1f486-1f3fd-200d-2640-fe0f", "massage_woman_medium_skin_tone": "1f486-1f3fd-200d-2640-fe0f", "woman-getting-massage_medium_dark_skin_tone": "1f486-1f3fe-200d-2640-fe0f", "massage_woman_medium_dark_skin_tone": "1f486-1f3fe-200d-2640-fe0f", "woman-getting-massage_dark_skin_tone": "1f486-1f3ff-200d-2640-fe0f", "massage_woman_dark_skin_tone": "1f486-1f3ff-200d-2640-fe0f", "man-getting-massage_light_skin_tone": "1f486-1f3fb-200d-2642-fe0f", "massage_man_light_skin_tone": "1f486-1f3fb-200d-2642-fe0f", "man-getting-massage_medium_light_skin_tone": "1f486-1f3fc-200d-2642-fe0f", "massage_man_medium_light_skin_tone": "1f486-1f3fc-200d-2642-fe0f", "man-getting-massage_medium_skin_tone": "1f486-1f3fd-200d-2642-fe0f", "massage_man_medium_skin_tone": "1f486-1f3fd-200d-2642-fe0f", "man-getting-massage_medium_dark_skin_tone": "1f486-1f3fe-200d-2642-fe0f", "massage_man_medium_dark_skin_tone": "1f486-1f3fe-200d-2642-fe0f", "man-getting-massage_dark_skin_tone": "1f486-1f3ff-200d-2642-fe0f", "massage_man_dark_skin_tone": "1f486-1f3ff-200d-2642-fe0f", "massage_light_skin_tone": "1f486-1f3fb", "massage_medium_light_skin_tone": "1f486-1f3fc", "massage_medium_skin_tone": "1f486-1f3fd", "massage_medium_dark_skin_tone": "1f486-1f3fe", "massage_dark_skin_tone": "1f486-1f3ff", "woman-getting-haircut_light_skin_tone": "1f487-1f3fb-200d-2640-fe0f", "haircut_woman_light_skin_tone": "1f487-1f3fb-200d-2640-fe0f", "woman-getting-haircut_medium_light_skin_tone": "1f487-1f3fc-200d-2640-fe0f", "haircut_woman_medium_light_skin_tone": "1f487-1f3fc-200d-2640-fe0f", "woman-getting-haircut_medium_skin_tone": "1f487-1f3fd-200d-2640-fe0f", "haircut_woman_medium_skin_tone": "1f487-1f3fd-200d-2640-fe0f", "woman-getting-haircut_medium_dark_skin_tone": "1f487-1f3fe-200d-2640-fe0f", "haircut_woman_medium_dark_skin_tone": "1f487-1f3fe-200d-2640-fe0f", "woman-getting-haircut_dark_skin_tone": "1f487-1f3ff-200d-2640-fe0f", "haircut_woman_dark_skin_tone": "1f487-1f3ff-200d-2640-fe0f", "man-getting-haircut_light_skin_tone": "1f487-1f3fb-200d-2642-fe0f", "haircut_man_light_skin_tone": "1f487-1f3fb-200d-2642-fe0f", "man-getting-haircut_medium_light_skin_tone": "1f487-1f3fc-200d-2642-fe0f", "haircut_man_medium_light_skin_tone": "1f487-1f3fc-200d-2642-fe0f", "man-getting-haircut_medium_skin_tone": "1f487-1f3fd-200d-2642-fe0f", "haircut_man_medium_skin_tone": "1f487-1f3fd-200d-2642-fe0f", "man-getting-haircut_medium_dark_skin_tone": "1f487-1f3fe-200d-2642-fe0f", "haircut_man_medium_dark_skin_tone": "1f487-1f3fe-200d-2642-fe0f", "man-getting-haircut_dark_skin_tone": "1f487-1f3ff-200d-2642-fe0f", "haircut_man_dark_skin_tone": "1f487-1f3ff-200d-2642-fe0f", "haircut_light_skin_tone": "1f487-1f3fb", "haircut_medium_light_skin_tone": "1f487-1f3fc", "haircut_medium_skin_tone": "1f487-1f3fd", "haircut_medium_dark_skin_tone": "1f487-1f3fe", "haircut_dark_skin_tone": "1f487-1f3ff", "muscle_light_skin_tone": "1f4aa-1f3fb", "muscle_medium_light_skin_tone": "1f4aa-1f3fc", "muscle_medium_skin_tone": "1f4aa-1f3fd", "muscle_medium_dark_skin_tone": "1f4aa-1f3fe", "muscle_dark_skin_tone": "1f4aa-1f3ff", "man_in_business_suit_levitating_light_skin_tone": "1f574-1f3fb", "business_suit_levitating_light_skin_tone": "1f574-1f3fb", "man_in_business_suit_levitating_medium_light_skin_tone": "1f574-1f3fc", "business_suit_levitating_medium_light_skin_tone": "1f574-1f3fc", "man_in_business_suit_levitating_medium_skin_tone": "1f574-1f3fd", "business_suit_levitating_medium_skin_tone": "1f574-1f3fd", "man_in_business_suit_levitating_medium_dark_skin_tone": "1f574-1f3fe", "business_suit_levitating_medium_dark_skin_tone": "1f574-1f3fe", "man_in_business_suit_levitating_dark_skin_tone": "1f574-1f3ff", "business_suit_levitating_dark_skin_tone": "1f574-1f3ff", "female-detective_light_skin_tone": "1f575-1f3fb-200d-2640-fe0f", "female_detective_light_skin_tone": "1f575-1f3fb-200d-2640-fe0f", "female-detective_medium_light_skin_tone": "1f575-1f3fc-200d-2640-fe0f", "female_detective_medium_light_skin_tone": "1f575-1f3fc-200d-2640-fe0f", "female-detective_medium_skin_tone": "1f575-1f3fd-200d-2640-fe0f", "female_detective_medium_skin_tone": "1f575-1f3fd-200d-2640-fe0f", "female-detective_medium_dark_skin_tone": "1f575-1f3fe-200d-2640-fe0f", "female_detective_medium_dark_skin_tone": "1f575-1f3fe-200d-2640-fe0f", "female-detective_dark_skin_tone": "1f575-1f3ff-200d-2640-fe0f", "female_detective_dark_skin_tone": "1f575-1f3ff-200d-2640-fe0f", "male-detective_light_skin_tone": "1f575-1f3fb-200d-2642-fe0f", "male_detective_light_skin_tone": "1f575-1f3fb-200d-2642-fe0f", "male-detective_medium_light_skin_tone": "1f575-1f3fc-200d-2642-fe0f", "male_detective_medium_light_skin_tone": "1f575-1f3fc-200d-2642-fe0f", "male-detective_medium_skin_tone": "1f575-1f3fd-200d-2642-fe0f", "male_detective_medium_skin_tone": "1f575-1f3fd-200d-2642-fe0f", "male-detective_medium_dark_skin_tone": "1f575-1f3fe-200d-2642-fe0f", "male_detective_medium_dark_skin_tone": "1f575-1f3fe-200d-2642-fe0f", "male-detective_dark_skin_tone": "1f575-1f3ff-200d-2642-fe0f", "male_detective_dark_skin_tone": "1f575-1f3ff-200d-2642-fe0f", "sleuth_or_spy_light_skin_tone": "1f575-1f3fb", "sleuth_or_spy_medium_light_skin_tone": "1f575-1f3fc", "sleuth_or_spy_medium_skin_tone": "1f575-1f3fd", "sleuth_or_spy_medium_dark_skin_tone": "1f575-1f3fe", "sleuth_or_spy_dark_skin_tone": "1f575-1f3ff", "man_dancing_light_skin_tone": "1f57a-1f3fb", "man_dancing_medium_light_skin_tone": "1f57a-1f3fc", "man_dancing_medium_skin_tone": "1f57a-1f3fd", "man_dancing_medium_dark_skin_tone": "1f57a-1f3fe", "man_dancing_dark_skin_tone": "1f57a-1f3ff", "raised_hand_with_fingers_splayed_light_skin_tone": "1f590-1f3fb", "raised_hand_with_fingers_splayed_medium_light_skin_tone": "1f590-1f3fc", "raised_hand_with_fingers_splayed_medium_skin_tone": "1f590-1f3fd", "raised_hand_with_fingers_splayed_medium_dark_skin_tone": "1f590-1f3fe", "raised_hand_with_fingers_splayed_dark_skin_tone": "1f590-1f3ff", "middle_finger_light_skin_tone": "1f595-1f3fb", "reversed_hand_with_middle_finger_extended_light_skin_tone": "1f595-1f3fb", "middle_finger_medium_light_skin_tone": "1f595-1f3fc", "reversed_hand_with_middle_finger_extended_medium_light_skin_tone": "1f595-1f3fc", "middle_finger_medium_skin_tone": "1f595-1f3fd", "reversed_hand_with_middle_finger_extended_medium_skin_tone": "1f595-1f3fd", "middle_finger_medium_dark_skin_tone": "1f595-1f3fe", "reversed_hand_with_middle_finger_extended_medium_dark_skin_tone": "1f595-1f3fe", "middle_finger_dark_skin_tone": "1f595-1f3ff", "reversed_hand_with_middle_finger_extended_dark_skin_tone": "1f595-1f3ff", "spock-hand_light_skin_tone": "1f596-1f3fb", "vulcan_salute_light_skin_tone": "1f596-1f3fb", "spock-hand_medium_light_skin_tone": "1f596-1f3fc", "vulcan_salute_medium_light_skin_tone": "1f596-1f3fc", "spock-hand_medium_skin_tone": "1f596-1f3fd", "vulcan_salute_medium_skin_tone": "1f596-1f3fd", "spock-hand_medium_dark_skin_tone": "1f596-1f3fe", "vulcan_salute_medium_dark_skin_tone": "1f596-1f3fe", "spock-hand_dark_skin_tone": "1f596-1f3ff", "vulcan_salute_dark_skin_tone": "1f596-1f3ff", "woman-gesturing-no_light_skin_tone": "1f645-1f3fb-200d-2640-fe0f", "no_good_woman_light_skin_tone": "1f645-1f3fb-200d-2640-fe0f", "woman-gesturing-no_medium_light_skin_tone": "1f645-1f3fc-200d-2640-fe0f", "no_good_woman_medium_light_skin_tone": "1f645-1f3fc-200d-2640-fe0f", "woman-gesturing-no_medium_skin_tone": "1f645-1f3fd-200d-2640-fe0f", "no_good_woman_medium_skin_tone": "1f645-1f3fd-200d-2640-fe0f", "woman-gesturing-no_medium_dark_skin_tone": "1f645-1f3fe-200d-2640-fe0f", "no_good_woman_medium_dark_skin_tone": "1f645-1f3fe-200d-2640-fe0f", "woman-gesturing-no_dark_skin_tone": "1f645-1f3ff-200d-2640-fe0f", "no_good_woman_dark_skin_tone": "1f645-1f3ff-200d-2640-fe0f", "man-gesturing-no_light_skin_tone": "1f645-1f3fb-200d-2642-fe0f", "no_good_man_light_skin_tone": "1f645-1f3fb-200d-2642-fe0f", "man-gesturing-no_medium_light_skin_tone": "1f645-1f3fc-200d-2642-fe0f", "no_good_man_medium_light_skin_tone": "1f645-1f3fc-200d-2642-fe0f", "man-gesturing-no_medium_skin_tone": "1f645-1f3fd-200d-2642-fe0f", "no_good_man_medium_skin_tone": "1f645-1f3fd-200d-2642-fe0f", "man-gesturing-no_medium_dark_skin_tone": "1f645-1f3fe-200d-2642-fe0f", "no_good_man_medium_dark_skin_tone": "1f645-1f3fe-200d-2642-fe0f", "man-gesturing-no_dark_skin_tone": "1f645-1f3ff-200d-2642-fe0f", "no_good_man_dark_skin_tone": "1f645-1f3ff-200d-2642-fe0f", "no_good_light_skin_tone": "1f645-1f3fb", "no_good_medium_light_skin_tone": "1f645-1f3fc", "no_good_medium_skin_tone": "1f645-1f3fd", "no_good_medium_dark_skin_tone": "1f645-1f3fe", "no_good_dark_skin_tone": "1f645-1f3ff", "woman-gesturing-ok_light_skin_tone": "1f646-1f3fb-200d-2640-fe0f", "woman-gesturing-ok_medium_light_skin_tone": "1f646-1f3fc-200d-2640-fe0f", "woman-gesturing-ok_medium_skin_tone": "1f646-1f3fd-200d-2640-fe0f", "woman-gesturing-ok_medium_dark_skin_tone": "1f646-1f3fe-200d-2640-fe0f", "woman-gesturing-ok_dark_skin_tone": "1f646-1f3ff-200d-2640-fe0f", "man-gesturing-ok_light_skin_tone": "1f646-1f3fb-200d-2642-fe0f", "ok_man_light_skin_tone": "1f646-1f3fb-200d-2642-fe0f", "man-gesturing-ok_medium_light_skin_tone": "1f646-1f3fc-200d-2642-fe0f", "ok_man_medium_light_skin_tone": "1f646-1f3fc-200d-2642-fe0f", "man-gesturing-ok_medium_skin_tone": "1f646-1f3fd-200d-2642-fe0f", "ok_man_medium_skin_tone": "1f646-1f3fd-200d-2642-fe0f", "man-gesturing-ok_medium_dark_skin_tone": "1f646-1f3fe-200d-2642-fe0f", "ok_man_medium_dark_skin_tone": "1f646-1f3fe-200d-2642-fe0f", "man-gesturing-ok_dark_skin_tone": "1f646-1f3ff-200d-2642-fe0f", "ok_man_dark_skin_tone": "1f646-1f3ff-200d-2642-fe0f", "ok_woman_light_skin_tone": "1f646-1f3fb", "ok_woman_medium_light_skin_tone": "1f646-1f3fc", "ok_woman_medium_skin_tone": "1f646-1f3fd", "ok_woman_medium_dark_skin_tone": "1f646-1f3fe", "ok_woman_dark_skin_tone": "1f646-1f3ff", "woman-bowing_light_skin_tone": "1f647-1f3fb-200d-2640-fe0f", "bowing_woman_light_skin_tone": "1f647-1f3fb-200d-2640-fe0f", "woman-bowing_medium_light_skin_tone": "1f647-1f3fc-200d-2640-fe0f", "bowing_woman_medium_light_skin_tone": "1f647-1f3fc-200d-2640-fe0f", "woman-bowing_medium_skin_tone": "1f647-1f3fd-200d-2640-fe0f", "bowing_woman_medium_skin_tone": "1f647-1f3fd-200d-2640-fe0f", "woman-bowing_medium_dark_skin_tone": "1f647-1f3fe-200d-2640-fe0f", "bowing_woman_medium_dark_skin_tone": "1f647-1f3fe-200d-2640-fe0f", "woman-bowing_dark_skin_tone": "1f647-1f3ff-200d-2640-fe0f", "bowing_woman_dark_skin_tone": "1f647-1f3ff-200d-2640-fe0f", "man-bowing_light_skin_tone": "1f647-1f3fb-200d-2642-fe0f", "bowing_man_light_skin_tone": "1f647-1f3fb-200d-2642-fe0f", "man-bowing_medium_light_skin_tone": "1f647-1f3fc-200d-2642-fe0f", "bowing_man_medium_light_skin_tone": "1f647-1f3fc-200d-2642-fe0f", "man-bowing_medium_skin_tone": "1f647-1f3fd-200d-2642-fe0f", "bowing_man_medium_skin_tone": "1f647-1f3fd-200d-2642-fe0f", "man-bowing_medium_dark_skin_tone": "1f647-1f3fe-200d-2642-fe0f", "bowing_man_medium_dark_skin_tone": "1f647-1f3fe-200d-2642-fe0f", "man-bowing_dark_skin_tone": "1f647-1f3ff-200d-2642-fe0f", "bowing_man_dark_skin_tone": "1f647-1f3ff-200d-2642-fe0f", "bow_light_skin_tone": "1f647-1f3fb", "bow_medium_light_skin_tone": "1f647-1f3fc", "bow_medium_skin_tone": "1f647-1f3fd", "bow_medium_dark_skin_tone": "1f647-1f3fe", "bow_dark_skin_tone": "1f647-1f3ff", "woman-raising-hand_light_skin_tone": "1f64b-1f3fb-200d-2640-fe0f", "raising_hand_woman_light_skin_tone": "1f64b-1f3fb-200d-2640-fe0f", "woman-raising-hand_medium_light_skin_tone": "1f64b-1f3fc-200d-2640-fe0f", "raising_hand_woman_medium_light_skin_tone": "1f64b-1f3fc-200d-2640-fe0f", "woman-raising-hand_medium_skin_tone": "1f64b-1f3fd-200d-2640-fe0f", "raising_hand_woman_medium_skin_tone": "1f64b-1f3fd-200d-2640-fe0f", "woman-raising-hand_medium_dark_skin_tone": "1f64b-1f3fe-200d-2640-fe0f", "raising_hand_woman_medium_dark_skin_tone": "1f64b-1f3fe-200d-2640-fe0f", "woman-raising-hand_dark_skin_tone": "1f64b-1f3ff-200d-2640-fe0f", "raising_hand_woman_dark_skin_tone": "1f64b-1f3ff-200d-2640-fe0f", "man-raising-hand_light_skin_tone": "1f64b-1f3fb-200d-2642-fe0f", "raising_hand_man_light_skin_tone": "1f64b-1f3fb-200d-2642-fe0f", "man-raising-hand_medium_light_skin_tone": "1f64b-1f3fc-200d-2642-fe0f", "raising_hand_man_medium_light_skin_tone": "1f64b-1f3fc-200d-2642-fe0f", "man-raising-hand_medium_skin_tone": "1f64b-1f3fd-200d-2642-fe0f", "raising_hand_man_medium_skin_tone": "1f64b-1f3fd-200d-2642-fe0f", "man-raising-hand_medium_dark_skin_tone": "1f64b-1f3fe-200d-2642-fe0f", "raising_hand_man_medium_dark_skin_tone": "1f64b-1f3fe-200d-2642-fe0f", "man-raising-hand_dark_skin_tone": "1f64b-1f3ff-200d-2642-fe0f", "raising_hand_man_dark_skin_tone": "1f64b-1f3ff-200d-2642-fe0f", "raising_hand_light_skin_tone": "1f64b-1f3fb", "raising_hand_medium_light_skin_tone": "1f64b-1f3fc", "raising_hand_medium_skin_tone": "1f64b-1f3fd", "raising_hand_medium_dark_skin_tone": "1f64b-1f3fe", "raising_hand_dark_skin_tone": "1f64b-1f3ff", "raised_hands_light_skin_tone": "1f64c-1f3fb", "raised_hands_medium_light_skin_tone": "1f64c-1f3fc", "raised_hands_medium_skin_tone": "1f64c-1f3fd", "raised_hands_medium_dark_skin_tone": "1f64c-1f3fe", "raised_hands_dark_skin_tone": "1f64c-1f3ff", "woman-frowning_light_skin_tone": "1f64d-1f3fb-200d-2640-fe0f", "frowning_woman_light_skin_tone": "1f64d-1f3fb-200d-2640-fe0f", "woman-frowning_medium_light_skin_tone": "1f64d-1f3fc-200d-2640-fe0f", "frowning_woman_medium_light_skin_tone": "1f64d-1f3fc-200d-2640-fe0f", "woman-frowning_medium_skin_tone": "1f64d-1f3fd-200d-2640-fe0f", "frowning_woman_medium_skin_tone": "1f64d-1f3fd-200d-2640-fe0f", "woman-frowning_medium_dark_skin_tone": "1f64d-1f3fe-200d-2640-fe0f", "frowning_woman_medium_dark_skin_tone": "1f64d-1f3fe-200d-2640-fe0f", "woman-frowning_dark_skin_tone": "1f64d-1f3ff-200d-2640-fe0f", "frowning_woman_dark_skin_tone": "1f64d-1f3ff-200d-2640-fe0f", "man-frowning_light_skin_tone": "1f64d-1f3fb-200d-2642-fe0f", "frowning_man_light_skin_tone": "1f64d-1f3fb-200d-2642-fe0f", "man-frowning_medium_light_skin_tone": "1f64d-1f3fc-200d-2642-fe0f", "frowning_man_medium_light_skin_tone": "1f64d-1f3fc-200d-2642-fe0f", "man-frowning_medium_skin_tone": "1f64d-1f3fd-200d-2642-fe0f", "frowning_man_medium_skin_tone": "1f64d-1f3fd-200d-2642-fe0f", "man-frowning_medium_dark_skin_tone": "1f64d-1f3fe-200d-2642-fe0f", "frowning_man_medium_dark_skin_tone": "1f64d-1f3fe-200d-2642-fe0f", "man-frowning_dark_skin_tone": "1f64d-1f3ff-200d-2642-fe0f", "frowning_man_dark_skin_tone": "1f64d-1f3ff-200d-2642-fe0f", "person_frowning_light_skin_tone": "1f64d-1f3fb", "person_frowning_medium_light_skin_tone": "1f64d-1f3fc", "person_frowning_medium_skin_tone": "1f64d-1f3fd", "person_frowning_medium_dark_skin_tone": "1f64d-1f3fe", "person_frowning_dark_skin_tone": "1f64d-1f3ff", "woman-pouting_light_skin_tone": "1f64e-1f3fb-200d-2640-fe0f", "pouting_woman_light_skin_tone": "1f64e-1f3fb-200d-2640-fe0f", "woman-pouting_medium_light_skin_tone": "1f64e-1f3fc-200d-2640-fe0f", "pouting_woman_medium_light_skin_tone": "1f64e-1f3fc-200d-2640-fe0f", "woman-pouting_medium_skin_tone": "1f64e-1f3fd-200d-2640-fe0f", "pouting_woman_medium_skin_tone": "1f64e-1f3fd-200d-2640-fe0f", "woman-pouting_medium_dark_skin_tone": "1f64e-1f3fe-200d-2640-fe0f", "pouting_woman_medium_dark_skin_tone": "1f64e-1f3fe-200d-2640-fe0f", "woman-pouting_dark_skin_tone": "1f64e-1f3ff-200d-2640-fe0f", "pouting_woman_dark_skin_tone": "1f64e-1f3ff-200d-2640-fe0f", "man-pouting_light_skin_tone": "1f64e-1f3fb-200d-2642-fe0f", "pouting_man_light_skin_tone": "1f64e-1f3fb-200d-2642-fe0f", "man-pouting_medium_light_skin_tone": "1f64e-1f3fc-200d-2642-fe0f", "pouting_man_medium_light_skin_tone": "1f64e-1f3fc-200d-2642-fe0f", "man-pouting_medium_skin_tone": "1f64e-1f3fd-200d-2642-fe0f", "pouting_man_medium_skin_tone": "1f64e-1f3fd-200d-2642-fe0f", "man-pouting_medium_dark_skin_tone": "1f64e-1f3fe-200d-2642-fe0f", "pouting_man_medium_dark_skin_tone": "1f64e-1f3fe-200d-2642-fe0f", "man-pouting_dark_skin_tone": "1f64e-1f3ff-200d-2642-fe0f", "pouting_man_dark_skin_tone": "1f64e-1f3ff-200d-2642-fe0f", "person_with_pouting_face_light_skin_tone": "1f64e-1f3fb", "person_with_pouting_face_medium_light_skin_tone": "1f64e-1f3fc", "person_with_pouting_face_medium_skin_tone": "1f64e-1f3fd", "person_with_pouting_face_medium_dark_skin_tone": "1f64e-1f3fe", "person_with_pouting_face_dark_skin_tone": "1f64e-1f3ff", "pray_light_skin_tone": "1f64f-1f3fb", "pray_medium_light_skin_tone": "1f64f-1f3fc", "pray_medium_skin_tone": "1f64f-1f3fd", "pray_medium_dark_skin_tone": "1f64f-1f3fe", "pray_dark_skin_tone": "1f64f-1f3ff", "woman-rowing-boat_light_skin_tone": "1f6a3-1f3fb-200d-2640-fe0f", "rowing_woman_light_skin_tone": "1f6a3-1f3fb-200d-2640-fe0f", "woman-rowing-boat_medium_light_skin_tone": "1f6a3-1f3fc-200d-2640-fe0f", "rowing_woman_medium_light_skin_tone": "1f6a3-1f3fc-200d-2640-fe0f", "woman-rowing-boat_medium_skin_tone": "1f6a3-1f3fd-200d-2640-fe0f", "rowing_woman_medium_skin_tone": "1f6a3-1f3fd-200d-2640-fe0f", "woman-rowing-boat_medium_dark_skin_tone": "1f6a3-1f3fe-200d-2640-fe0f", "rowing_woman_medium_dark_skin_tone": "1f6a3-1f3fe-200d-2640-fe0f", "woman-rowing-boat_dark_skin_tone": "1f6a3-1f3ff-200d-2640-fe0f", "rowing_woman_dark_skin_tone": "1f6a3-1f3ff-200d-2640-fe0f", "man-rowing-boat_light_skin_tone": "1f6a3-1f3fb-200d-2642-fe0f", "rowing_man_light_skin_tone": "1f6a3-1f3fb-200d-2642-fe0f", "man-rowing-boat_medium_light_skin_tone": "1f6a3-1f3fc-200d-2642-fe0f", "rowing_man_medium_light_skin_tone": "1f6a3-1f3fc-200d-2642-fe0f", "man-rowing-boat_medium_skin_tone": "1f6a3-1f3fd-200d-2642-fe0f", "rowing_man_medium_skin_tone": "1f6a3-1f3fd-200d-2642-fe0f", "man-rowing-boat_medium_dark_skin_tone": "1f6a3-1f3fe-200d-2642-fe0f", "rowing_man_medium_dark_skin_tone": "1f6a3-1f3fe-200d-2642-fe0f", "man-rowing-boat_dark_skin_tone": "1f6a3-1f3ff-200d-2642-fe0f", "rowing_man_dark_skin_tone": "1f6a3-1f3ff-200d-2642-fe0f", "rowboat_light_skin_tone": "1f6a3-1f3fb", "rowboat_medium_light_skin_tone": "1f6a3-1f3fc", "rowboat_medium_skin_tone": "1f6a3-1f3fd", "rowboat_medium_dark_skin_tone": "1f6a3-1f3fe", "rowboat_dark_skin_tone": "1f6a3-1f3ff", "woman-biking_light_skin_tone": "1f6b4-1f3fb-200d-2640-fe0f", "biking_woman_light_skin_tone": "1f6b4-1f3fb-200d-2640-fe0f", "woman-biking_medium_light_skin_tone": "1f6b4-1f3fc-200d-2640-fe0f", "biking_woman_medium_light_skin_tone": "1f6b4-1f3fc-200d-2640-fe0f", "woman-biking_medium_skin_tone": "1f6b4-1f3fd-200d-2640-fe0f", "biking_woman_medium_skin_tone": "1f6b4-1f3fd-200d-2640-fe0f", "woman-biking_medium_dark_skin_tone": "1f6b4-1f3fe-200d-2640-fe0f", "biking_woman_medium_dark_skin_tone": "1f6b4-1f3fe-200d-2640-fe0f", "woman-biking_dark_skin_tone": "1f6b4-1f3ff-200d-2640-fe0f", "biking_woman_dark_skin_tone": "1f6b4-1f3ff-200d-2640-fe0f", "man-biking_light_skin_tone": "1f6b4-1f3fb-200d-2642-fe0f", "biking_man_light_skin_tone": "1f6b4-1f3fb-200d-2642-fe0f", "man-biking_medium_light_skin_tone": "1f6b4-1f3fc-200d-2642-fe0f", "biking_man_medium_light_skin_tone": "1f6b4-1f3fc-200d-2642-fe0f", "man-biking_medium_skin_tone": "1f6b4-1f3fd-200d-2642-fe0f", "biking_man_medium_skin_tone": "1f6b4-1f3fd-200d-2642-fe0f", "man-biking_medium_dark_skin_tone": "1f6b4-1f3fe-200d-2642-fe0f", "biking_man_medium_dark_skin_tone": "1f6b4-1f3fe-200d-2642-fe0f", "man-biking_dark_skin_tone": "1f6b4-1f3ff-200d-2642-fe0f", "biking_man_dark_skin_tone": "1f6b4-1f3ff-200d-2642-fe0f", "bicyclist_light_skin_tone": "1f6b4-1f3fb", "bicyclist_medium_light_skin_tone": "1f6b4-1f3fc", "bicyclist_medium_skin_tone": "1f6b4-1f3fd", "bicyclist_medium_dark_skin_tone": "1f6b4-1f3fe", "bicyclist_dark_skin_tone": "1f6b4-1f3ff", "woman-mountain-biking_light_skin_tone": "1f6b5-1f3fb-200d-2640-fe0f", "mountain_biking_woman_light_skin_tone": "1f6b5-1f3fb-200d-2640-fe0f", "woman-mountain-biking_medium_light_skin_tone": "1f6b5-1f3fc-200d-2640-fe0f", "mountain_biking_woman_medium_light_skin_tone": "1f6b5-1f3fc-200d-2640-fe0f", "woman-mountain-biking_medium_skin_tone": "1f6b5-1f3fd-200d-2640-fe0f", "mountain_biking_woman_medium_skin_tone": "1f6b5-1f3fd-200d-2640-fe0f", "woman-mountain-biking_medium_dark_skin_tone": "1f6b5-1f3fe-200d-2640-fe0f", "mountain_biking_woman_medium_dark_skin_tone": "1f6b5-1f3fe-200d-2640-fe0f", "woman-mountain-biking_dark_skin_tone": "1f6b5-1f3ff-200d-2640-fe0f", "mountain_biking_woman_dark_skin_tone": "1f6b5-1f3ff-200d-2640-fe0f", "man-mountain-biking_light_skin_tone": "1f6b5-1f3fb-200d-2642-fe0f", "mountain_biking_man_light_skin_tone": "1f6b5-1f3fb-200d-2642-fe0f", "man-mountain-biking_medium_light_skin_tone": "1f6b5-1f3fc-200d-2642-fe0f", "mountain_biking_man_medium_light_skin_tone": "1f6b5-1f3fc-200d-2642-fe0f", "man-mountain-biking_medium_skin_tone": "1f6b5-1f3fd-200d-2642-fe0f", "mountain_biking_man_medium_skin_tone": "1f6b5-1f3fd-200d-2642-fe0f", "man-mountain-biking_medium_dark_skin_tone": "1f6b5-1f3fe-200d-2642-fe0f", "mountain_biking_man_medium_dark_skin_tone": "1f6b5-1f3fe-200d-2642-fe0f", "man-mountain-biking_dark_skin_tone": "1f6b5-1f3ff-200d-2642-fe0f", "mountain_biking_man_dark_skin_tone": "1f6b5-1f3ff-200d-2642-fe0f", "mountain_bicyclist_light_skin_tone": "1f6b5-1f3fb", "mountain_bicyclist_medium_light_skin_tone": "1f6b5-1f3fc", "mountain_bicyclist_medium_skin_tone": "1f6b5-1f3fd", "mountain_bicyclist_medium_dark_skin_tone": "1f6b5-1f3fe", "mountain_bicyclist_dark_skin_tone": "1f6b5-1f3ff", "woman-walking_light_skin_tone": "1f6b6-1f3fb-200d-2640-fe0f", "walking_woman_light_skin_tone": "1f6b6-1f3fb-200d-2640-fe0f", "woman-walking_medium_light_skin_tone": "1f6b6-1f3fc-200d-2640-fe0f", "walking_woman_medium_light_skin_tone": "1f6b6-1f3fc-200d-2640-fe0f", "woman-walking_medium_skin_tone": "1f6b6-1f3fd-200d-2640-fe0f", "walking_woman_medium_skin_tone": "1f6b6-1f3fd-200d-2640-fe0f", "woman-walking_medium_dark_skin_tone": "1f6b6-1f3fe-200d-2640-fe0f", "walking_woman_medium_dark_skin_tone": "1f6b6-1f3fe-200d-2640-fe0f", "woman-walking_dark_skin_tone": "1f6b6-1f3ff-200d-2640-fe0f", "walking_woman_dark_skin_tone": "1f6b6-1f3ff-200d-2640-fe0f", "man-walking_light_skin_tone": "1f6b6-1f3fb-200d-2642-fe0f", "walking_man_light_skin_tone": "1f6b6-1f3fb-200d-2642-fe0f", "man-walking_medium_light_skin_tone": "1f6b6-1f3fc-200d-2642-fe0f", "walking_man_medium_light_skin_tone": "1f6b6-1f3fc-200d-2642-fe0f", "man-walking_medium_skin_tone": "1f6b6-1f3fd-200d-2642-fe0f", "walking_man_medium_skin_tone": "1f6b6-1f3fd-200d-2642-fe0f", "man-walking_medium_dark_skin_tone": "1f6b6-1f3fe-200d-2642-fe0f", "walking_man_medium_dark_skin_tone": "1f6b6-1f3fe-200d-2642-fe0f", "man-walking_dark_skin_tone": "1f6b6-1f3ff-200d-2642-fe0f", "walking_man_dark_skin_tone": "1f6b6-1f3ff-200d-2642-fe0f", "walking_light_skin_tone": "1f6b6-1f3fb", "walking_medium_light_skin_tone": "1f6b6-1f3fc", "walking_medium_skin_tone": "1f6b6-1f3fd", "walking_medium_dark_skin_tone": "1f6b6-1f3fe", "walking_dark_skin_tone": "1f6b6-1f3ff", "bath_light_skin_tone": "1f6c0-1f3fb", "bath_medium_light_skin_tone": "1f6c0-1f3fc", "bath_medium_skin_tone": "1f6c0-1f3fd", "bath_medium_dark_skin_tone": "1f6c0-1f3fe", "bath_dark_skin_tone": "1f6c0-1f3ff", "sleeping_accommodation_light_skin_tone": "1f6cc-1f3fb", "sleeping_accommodation_medium_light_skin_tone": "1f6cc-1f3fc", "sleeping_accommodation_medium_skin_tone": "1f6cc-1f3fd", "sleeping_accommodation_medium_dark_skin_tone": "1f6cc-1f3fe", "sleeping_accommodation_dark_skin_tone": "1f6cc-1f3ff", "pinched_fingers_light_skin_tone": "1f90c-1f3fb", "pinched_fingers_medium_light_skin_tone": "1f90c-1f3fc", "pinched_fingers_medium_skin_tone": "1f90c-1f3fd", "pinched_fingers_medium_dark_skin_tone": "1f90c-1f3fe", "pinched_fingers_dark_skin_tone": "1f90c-1f3ff", "pinching_hand_light_skin_tone": "1f90f-1f3fb", "pinching_hand_medium_light_skin_tone": "1f90f-1f3fc", "pinching_hand_medium_skin_tone": "1f90f-1f3fd", "pinching_hand_medium_dark_skin_tone": "1f90f-1f3fe", "pinching_hand_dark_skin_tone": "1f90f-1f3ff", "the_horns_light_skin_tone": "1f918-1f3fb", "sign_of_the_horns_light_skin_tone": "1f918-1f3fb", "metal_light_skin_tone": "1f918-1f3fb", "the_horns_medium_light_skin_tone": "1f918-1f3fc", "sign_of_the_horns_medium_light_skin_tone": "1f918-1f3fc", "metal_medium_light_skin_tone": "1f918-1f3fc", "the_horns_medium_skin_tone": "1f918-1f3fd", "sign_of_the_horns_medium_skin_tone": "1f918-1f3fd", "metal_medium_skin_tone": "1f918-1f3fd", "the_horns_medium_dark_skin_tone": "1f918-1f3fe", "sign_of_the_horns_medium_dark_skin_tone": "1f918-1f3fe", "metal_medium_dark_skin_tone": "1f918-1f3fe", "the_horns_dark_skin_tone": "1f918-1f3ff", "sign_of_the_horns_dark_skin_tone": "1f918-1f3ff", "metal_dark_skin_tone": "1f918-1f3ff", "call_me_hand_light_skin_tone": "1f919-1f3fb", "call_me_hand_medium_light_skin_tone": "1f919-1f3fc", "call_me_hand_medium_skin_tone": "1f919-1f3fd", "call_me_hand_medium_dark_skin_tone": "1f919-1f3fe", "call_me_hand_dark_skin_tone": "1f919-1f3ff", "raised_back_of_hand_light_skin_tone": "1f91a-1f3fb", "raised_back_of_hand_medium_light_skin_tone": "1f91a-1f3fc", "raised_back_of_hand_medium_skin_tone": "1f91a-1f3fd", "raised_back_of_hand_medium_dark_skin_tone": "1f91a-1f3fe", "raised_back_of_hand_dark_skin_tone": "1f91a-1f3ff", "left-facing_fist_light_skin_tone": "1f91b-1f3fb", "fist_left_light_skin_tone": "1f91b-1f3fb", "left-facing_fist_medium_light_skin_tone": "1f91b-1f3fc", "fist_left_medium_light_skin_tone": "1f91b-1f3fc", "left-facing_fist_medium_skin_tone": "1f91b-1f3fd", "fist_left_medium_skin_tone": "1f91b-1f3fd", "left-facing_fist_medium_dark_skin_tone": "1f91b-1f3fe", "fist_left_medium_dark_skin_tone": "1f91b-1f3fe", "left-facing_fist_dark_skin_tone": "1f91b-1f3ff", "fist_left_dark_skin_tone": "1f91b-1f3ff", "right-facing_fist_light_skin_tone": "1f91c-1f3fb", "fist_right_light_skin_tone": "1f91c-1f3fb", "right-facing_fist_medium_light_skin_tone": "1f91c-1f3fc", "fist_right_medium_light_skin_tone": "1f91c-1f3fc", "right-facing_fist_medium_skin_tone": "1f91c-1f3fd", "fist_right_medium_skin_tone": "1f91c-1f3fd", "right-facing_fist_medium_dark_skin_tone": "1f91c-1f3fe", "fist_right_medium_dark_skin_tone": "1f91c-1f3fe", "right-facing_fist_dark_skin_tone": "1f91c-1f3ff", "fist_right_dark_skin_tone": "1f91c-1f3ff", "crossed_fingers_light_skin_tone": "1f91e-1f3fb", "hand_with_index_and_middle_fingers_crossed_light_skin_tone": "1f91e-1f3fb", "crossed_fingers_medium_light_skin_tone": "1f91e-1f3fc", "hand_with_index_and_middle_fingers_crossed_medium_light_skin_tone": "1f91e-1f3fc", "crossed_fingers_medium_skin_tone": "1f91e-1f3fd", "hand_with_index_and_middle_fingers_crossed_medium_skin_tone": "1f91e-1f3fd", "crossed_fingers_medium_dark_skin_tone": "1f91e-1f3fe", "hand_with_index_and_middle_fingers_crossed_medium_dark_skin_tone": "1f91e-1f3fe", "crossed_fingers_dark_skin_tone": "1f91e-1f3ff", "hand_with_index_and_middle_fingers_crossed_dark_skin_tone": "1f91e-1f3ff", "i_love_you_hand_sign_light_skin_tone": "1f91f-1f3fb", "i_love_you_hand_sign_medium_light_skin_tone": "1f91f-1f3fc", "i_love_you_hand_sign_medium_skin_tone": "1f91f-1f3fd", "i_love_you_hand_sign_medium_dark_skin_tone": "1f91f-1f3fe", "i_love_you_hand_sign_dark_skin_tone": "1f91f-1f3ff", "woman-facepalming_light_skin_tone": "1f926-1f3fb-200d-2640-fe0f", "woman_facepalming_light_skin_tone": "1f926-1f3fb-200d-2640-fe0f", "woman-facepalming_medium_light_skin_tone": "1f926-1f3fc-200d-2640-fe0f", "woman_facepalming_medium_light_skin_tone": "1f926-1f3fc-200d-2640-fe0f", "woman-facepalming_medium_skin_tone": "1f926-1f3fd-200d-2640-fe0f", "woman_facepalming_medium_skin_tone": "1f926-1f3fd-200d-2640-fe0f", "woman-facepalming_medium_dark_skin_tone": "1f926-1f3fe-200d-2640-fe0f", "woman_facepalming_medium_dark_skin_tone": "1f926-1f3fe-200d-2640-fe0f", "woman-facepalming_dark_skin_tone": "1f926-1f3ff-200d-2640-fe0f", "woman_facepalming_dark_skin_tone": "1f926-1f3ff-200d-2640-fe0f", "man-facepalming_light_skin_tone": "1f926-1f3fb-200d-2642-fe0f", "man_facepalming_light_skin_tone": "1f926-1f3fb-200d-2642-fe0f", "man-facepalming_medium_light_skin_tone": "1f926-1f3fc-200d-2642-fe0f", "man_facepalming_medium_light_skin_tone": "1f926-1f3fc-200d-2642-fe0f", "man-facepalming_medium_skin_tone": "1f926-1f3fd-200d-2642-fe0f", "man_facepalming_medium_skin_tone": "1f926-1f3fd-200d-2642-fe0f", "man-facepalming_medium_dark_skin_tone": "1f926-1f3fe-200d-2642-fe0f", "man_facepalming_medium_dark_skin_tone": "1f926-1f3fe-200d-2642-fe0f", "man-facepalming_dark_skin_tone": "1f926-1f3ff-200d-2642-fe0f", "man_facepalming_dark_skin_tone": "1f926-1f3ff-200d-2642-fe0f", "face_palm_light_skin_tone": "1f926-1f3fb", "face_palm_medium_light_skin_tone": "1f926-1f3fc", "face_palm_medium_skin_tone": "1f926-1f3fd", "face_palm_medium_dark_skin_tone": "1f926-1f3fe", "face_palm_dark_skin_tone": "1f926-1f3ff", "pregnant_woman_light_skin_tone": "1f930-1f3fb", "pregnant_woman_medium_light_skin_tone": "1f930-1f3fc", "pregnant_woman_medium_skin_tone": "1f930-1f3fd", "pregnant_woman_medium_dark_skin_tone": "1f930-1f3fe", "pregnant_woman_dark_skin_tone": "1f930-1f3ff", "breast-feeding_light_skin_tone": "1f931-1f3fb", "breast-feeding_medium_light_skin_tone": "1f931-1f3fc", "breast-feeding_medium_skin_tone": "1f931-1f3fd", "breast-feeding_medium_dark_skin_tone": "1f931-1f3fe", "breast-feeding_dark_skin_tone": "1f931-1f3ff", "palms_up_together_light_skin_tone": "1f932-1f3fb", "palms_up_together_medium_light_skin_tone": "1f932-1f3fc", "palms_up_together_medium_skin_tone": "1f932-1f3fd", "palms_up_together_medium_dark_skin_tone": "1f932-1f3fe", "palms_up_together_dark_skin_tone": "1f932-1f3ff", "selfie_light_skin_tone": "1f933-1f3fb", "selfie_medium_light_skin_tone": "1f933-1f3fc", "selfie_medium_skin_tone": "1f933-1f3fd", "selfie_medium_dark_skin_tone": "1f933-1f3fe", "selfie_dark_skin_tone": "1f933-1f3ff", "prince_light_skin_tone": "1f934-1f3fb", "prince_medium_light_skin_tone": "1f934-1f3fc", "prince_medium_skin_tone": "1f934-1f3fd", "prince_medium_dark_skin_tone": "1f934-1f3fe", "prince_dark_skin_tone": "1f934-1f3ff", "woman_in_tuxedo_light_skin_tone": "1f935-1f3fb-200d-2640-fe0f", "woman_in_tuxedo_medium_light_skin_tone": "1f935-1f3fc-200d-2640-fe0f", "woman_in_tuxedo_medium_skin_tone": "1f935-1f3fd-200d-2640-fe0f", "woman_in_tuxedo_medium_dark_skin_tone": "1f935-1f3fe-200d-2640-fe0f", "woman_in_tuxedo_dark_skin_tone": "1f935-1f3ff-200d-2640-fe0f", "man_in_tuxedo_light_skin_tone": "1f935-1f3fb-200d-2642-fe0f", "man_in_tuxedo_medium_light_skin_tone": "1f935-1f3fc-200d-2642-fe0f", "man_in_tuxedo_medium_skin_tone": "1f935-1f3fd-200d-2642-fe0f", "man_in_tuxedo_medium_dark_skin_tone": "1f935-1f3fe-200d-2642-fe0f", "man_in_tuxedo_dark_skin_tone": "1f935-1f3ff-200d-2642-fe0f", "person_in_tuxedo_light_skin_tone": "1f935-1f3fb", "person_in_tuxedo_medium_light_skin_tone": "1f935-1f3fc", "person_in_tuxedo_medium_skin_tone": "1f935-1f3fd", "person_in_tuxedo_medium_dark_skin_tone": "1f935-1f3fe", "person_in_tuxedo_dark_skin_tone": "1f935-1f3ff", "mrs_claus_light_skin_tone": "1f936-1f3fb", "mother_christmas_light_skin_tone": "1f936-1f3fb", "mrs_claus_medium_light_skin_tone": "1f936-1f3fc", "mother_christmas_medium_light_skin_tone": "1f936-1f3fc", "mrs_claus_medium_skin_tone": "1f936-1f3fd", "mother_christmas_medium_skin_tone": "1f936-1f3fd", "mrs_claus_medium_dark_skin_tone": "1f936-1f3fe", "mother_christmas_medium_dark_skin_tone": "1f936-1f3fe", "mrs_claus_dark_skin_tone": "1f936-1f3ff", "mother_christmas_dark_skin_tone": "1f936-1f3ff", "woman-shrugging_light_skin_tone": "1f937-1f3fb-200d-2640-fe0f", "woman_shrugging_light_skin_tone": "1f937-1f3fb-200d-2640-fe0f", "woman-shrugging_medium_light_skin_tone": "1f937-1f3fc-200d-2640-fe0f", "woman_shrugging_medium_light_skin_tone": "1f937-1f3fc-200d-2640-fe0f", "woman-shrugging_medium_skin_tone": "1f937-1f3fd-200d-2640-fe0f", "woman_shrugging_medium_skin_tone": "1f937-1f3fd-200d-2640-fe0f", "woman-shrugging_medium_dark_skin_tone": "1f937-1f3fe-200d-2640-fe0f", "woman_shrugging_medium_dark_skin_tone": "1f937-1f3fe-200d-2640-fe0f", "woman-shrugging_dark_skin_tone": "1f937-1f3ff-200d-2640-fe0f", "woman_shrugging_dark_skin_tone": "1f937-1f3ff-200d-2640-fe0f", "man-shrugging_light_skin_tone": "1f937-1f3fb-200d-2642-fe0f", "man_shrugging_light_skin_tone": "1f937-1f3fb-200d-2642-fe0f", "man-shrugging_medium_light_skin_tone": "1f937-1f3fc-200d-2642-fe0f", "man_shrugging_medium_light_skin_tone": "1f937-1f3fc-200d-2642-fe0f", "man-shrugging_medium_skin_tone": "1f937-1f3fd-200d-2642-fe0f", "man_shrugging_medium_skin_tone": "1f937-1f3fd-200d-2642-fe0f", "man-shrugging_medium_dark_skin_tone": "1f937-1f3fe-200d-2642-fe0f", "man_shrugging_medium_dark_skin_tone": "1f937-1f3fe-200d-2642-fe0f", "man-shrugging_dark_skin_tone": "1f937-1f3ff-200d-2642-fe0f", "man_shrugging_dark_skin_tone": "1f937-1f3ff-200d-2642-fe0f", "shrug_light_skin_tone": "1f937-1f3fb", "shrug_medium_light_skin_tone": "1f937-1f3fc", "shrug_medium_skin_tone": "1f937-1f3fd", "shrug_medium_dark_skin_tone": "1f937-1f3fe", "shrug_dark_skin_tone": "1f937-1f3ff", "woman-cartwheeling_light_skin_tone": "1f938-1f3fb-200d-2640-fe0f", "woman_cartwheeling_light_skin_tone": "1f938-1f3fb-200d-2640-fe0f", "woman-cartwheeling_medium_light_skin_tone": "1f938-1f3fc-200d-2640-fe0f", "woman_cartwheeling_medium_light_skin_tone": "1f938-1f3fc-200d-2640-fe0f", "woman-cartwheeling_medium_skin_tone": "1f938-1f3fd-200d-2640-fe0f", "woman_cartwheeling_medium_skin_tone": "1f938-1f3fd-200d-2640-fe0f", "woman-cartwheeling_medium_dark_skin_tone": "1f938-1f3fe-200d-2640-fe0f", "woman_cartwheeling_medium_dark_skin_tone": "1f938-1f3fe-200d-2640-fe0f", "woman-cartwheeling_dark_skin_tone": "1f938-1f3ff-200d-2640-fe0f", "woman_cartwheeling_dark_skin_tone": "1f938-1f3ff-200d-2640-fe0f", "man-cartwheeling_light_skin_tone": "1f938-1f3fb-200d-2642-fe0f", "man_cartwheeling_light_skin_tone": "1f938-1f3fb-200d-2642-fe0f", "man-cartwheeling_medium_light_skin_tone": "1f938-1f3fc-200d-2642-fe0f", "man_cartwheeling_medium_light_skin_tone": "1f938-1f3fc-200d-2642-fe0f", "man-cartwheeling_medium_skin_tone": "1f938-1f3fd-200d-2642-fe0f", "man_cartwheeling_medium_skin_tone": "1f938-1f3fd-200d-2642-fe0f", "man-cartwheeling_medium_dark_skin_tone": "1f938-1f3fe-200d-2642-fe0f", "man_cartwheeling_medium_dark_skin_tone": "1f938-1f3fe-200d-2642-fe0f", "man-cartwheeling_dark_skin_tone": "1f938-1f3ff-200d-2642-fe0f", "man_cartwheeling_dark_skin_tone": "1f938-1f3ff-200d-2642-fe0f", "person_doing_cartwheel_light_skin_tone": "1f938-1f3fb", "person_doing_cartwheel_medium_light_skin_tone": "1f938-1f3fc", "person_doing_cartwheel_medium_skin_tone": "1f938-1f3fd", "person_doing_cartwheel_medium_dark_skin_tone": "1f938-1f3fe", "person_doing_cartwheel_dark_skin_tone": "1f938-1f3ff", "woman-juggling_light_skin_tone": "1f939-1f3fb-200d-2640-fe0f", "woman_juggling_light_skin_tone": "1f939-1f3fb-200d-2640-fe0f", "woman-juggling_medium_light_skin_tone": "1f939-1f3fc-200d-2640-fe0f", "woman_juggling_medium_light_skin_tone": "1f939-1f3fc-200d-2640-fe0f", "woman-juggling_medium_skin_tone": "1f939-1f3fd-200d-2640-fe0f", "woman_juggling_medium_skin_tone": "1f939-1f3fd-200d-2640-fe0f", "woman-juggling_medium_dark_skin_tone": "1f939-1f3fe-200d-2640-fe0f", "woman_juggling_medium_dark_skin_tone": "1f939-1f3fe-200d-2640-fe0f", "woman-juggling_dark_skin_tone": "1f939-1f3ff-200d-2640-fe0f", "woman_juggling_dark_skin_tone": "1f939-1f3ff-200d-2640-fe0f", "man-juggling_light_skin_tone": "1f939-1f3fb-200d-2642-fe0f", "man_juggling_light_skin_tone": "1f939-1f3fb-200d-2642-fe0f", "man-juggling_medium_light_skin_tone": "1f939-1f3fc-200d-2642-fe0f", "man_juggling_medium_light_skin_tone": "1f939-1f3fc-200d-2642-fe0f", "man-juggling_medium_skin_tone": "1f939-1f3fd-200d-2642-fe0f", "man_juggling_medium_skin_tone": "1f939-1f3fd-200d-2642-fe0f", "man-juggling_medium_dark_skin_tone": "1f939-1f3fe-200d-2642-fe0f", "man_juggling_medium_dark_skin_tone": "1f939-1f3fe-200d-2642-fe0f", "man-juggling_dark_skin_tone": "1f939-1f3ff-200d-2642-fe0f", "man_juggling_dark_skin_tone": "1f939-1f3ff-200d-2642-fe0f", "juggling_light_skin_tone": "1f939-1f3fb", "juggling_medium_light_skin_tone": "1f939-1f3fc", "juggling_medium_skin_tone": "1f939-1f3fd", "juggling_medium_dark_skin_tone": "1f939-1f3fe", "juggling_dark_skin_tone": "1f939-1f3ff", "woman-playing-water-polo_light_skin_tone": "1f93d-1f3fb-200d-2640-fe0f", "woman_playing_water_polo_light_skin_tone": "1f93d-1f3fb-200d-2640-fe0f", "woman-playing-water-polo_medium_light_skin_tone": "1f93d-1f3fc-200d-2640-fe0f", "woman_playing_water_polo_medium_light_skin_tone": "1f93d-1f3fc-200d-2640-fe0f", "woman-playing-water-polo_medium_skin_tone": "1f93d-1f3fd-200d-2640-fe0f", "woman_playing_water_polo_medium_skin_tone": "1f93d-1f3fd-200d-2640-fe0f", "woman-playing-water-polo_medium_dark_skin_tone": "1f93d-1f3fe-200d-2640-fe0f", "woman_playing_water_polo_medium_dark_skin_tone": "1f93d-1f3fe-200d-2640-fe0f", "woman-playing-water-polo_dark_skin_tone": "1f93d-1f3ff-200d-2640-fe0f", "woman_playing_water_polo_dark_skin_tone": "1f93d-1f3ff-200d-2640-fe0f", "man-playing-water-polo_light_skin_tone": "1f93d-1f3fb-200d-2642-fe0f", "man_playing_water_polo_light_skin_tone": "1f93d-1f3fb-200d-2642-fe0f", "man-playing-water-polo_medium_light_skin_tone": "1f93d-1f3fc-200d-2642-fe0f", "man_playing_water_polo_medium_light_skin_tone": "1f93d-1f3fc-200d-2642-fe0f", "man-playing-water-polo_medium_skin_tone": "1f93d-1f3fd-200d-2642-fe0f", "man_playing_water_polo_medium_skin_tone": "1f93d-1f3fd-200d-2642-fe0f", "man-playing-water-polo_medium_dark_skin_tone": "1f93d-1f3fe-200d-2642-fe0f", "man_playing_water_polo_medium_dark_skin_tone": "1f93d-1f3fe-200d-2642-fe0f", "man-playing-water-polo_dark_skin_tone": "1f93d-1f3ff-200d-2642-fe0f", "man_playing_water_polo_dark_skin_tone": "1f93d-1f3ff-200d-2642-fe0f", "water_polo_light_skin_tone": "1f93d-1f3fb", "water_polo_medium_light_skin_tone": "1f93d-1f3fc", "water_polo_medium_skin_tone": "1f93d-1f3fd", "water_polo_medium_dark_skin_tone": "1f93d-1f3fe", "water_polo_dark_skin_tone": "1f93d-1f3ff", "woman-playing-handball_light_skin_tone": "1f93e-1f3fb-200d-2640-fe0f", "woman_playing_handball_light_skin_tone": "1f93e-1f3fb-200d-2640-fe0f", "woman-playing-handball_medium_light_skin_tone": "1f93e-1f3fc-200d-2640-fe0f", "woman_playing_handball_medium_light_skin_tone": "1f93e-1f3fc-200d-2640-fe0f", "woman-playing-handball_medium_skin_tone": "1f93e-1f3fd-200d-2640-fe0f", "woman_playing_handball_medium_skin_tone": "1f93e-1f3fd-200d-2640-fe0f", "woman-playing-handball_medium_dark_skin_tone": "1f93e-1f3fe-200d-2640-fe0f", "woman_playing_handball_medium_dark_skin_tone": "1f93e-1f3fe-200d-2640-fe0f", "woman-playing-handball_dark_skin_tone": "1f93e-1f3ff-200d-2640-fe0f", "woman_playing_handball_dark_skin_tone": "1f93e-1f3ff-200d-2640-fe0f", "man-playing-handball_light_skin_tone": "1f93e-1f3fb-200d-2642-fe0f", "man_playing_handball_light_skin_tone": "1f93e-1f3fb-200d-2642-fe0f", "man-playing-handball_medium_light_skin_tone": "1f93e-1f3fc-200d-2642-fe0f", "man_playing_handball_medium_light_skin_tone": "1f93e-1f3fc-200d-2642-fe0f", "man-playing-handball_medium_skin_tone": "1f93e-1f3fd-200d-2642-fe0f", "man_playing_handball_medium_skin_tone": "1f93e-1f3fd-200d-2642-fe0f", "man-playing-handball_medium_dark_skin_tone": "1f93e-1f3fe-200d-2642-fe0f", "man_playing_handball_medium_dark_skin_tone": "1f93e-1f3fe-200d-2642-fe0f", "man-playing-handball_dark_skin_tone": "1f93e-1f3ff-200d-2642-fe0f", "man_playing_handball_dark_skin_tone": "1f93e-1f3ff-200d-2642-fe0f", "handball_light_skin_tone": "1f93e-1f3fb", "handball_medium_light_skin_tone": "1f93e-1f3fc", "handball_medium_skin_tone": "1f93e-1f3fd", "handball_medium_dark_skin_tone": "1f93e-1f3fe", "handball_dark_skin_tone": "1f93e-1f3ff", "ninja_light_skin_tone": "1f977-1f3fb", "ninja_medium_light_skin_tone": "1f977-1f3fc", "ninja_medium_skin_tone": "1f977-1f3fd", "ninja_medium_dark_skin_tone": "1f977-1f3fe", "ninja_dark_skin_tone": "1f977-1f3ff", "leg_light_skin_tone": "1f9b5-1f3fb", "leg_medium_light_skin_tone": "1f9b5-1f3fc", "leg_medium_skin_tone": "1f9b5-1f3fd", "leg_medium_dark_skin_tone": "1f9b5-1f3fe", "leg_dark_skin_tone": "1f9b5-1f3ff", "foot_light_skin_tone": "1f9b6-1f3fb", "foot_medium_light_skin_tone": "1f9b6-1f3fc", "foot_medium_skin_tone": "1f9b6-1f3fd", "foot_medium_dark_skin_tone": "1f9b6-1f3fe", "foot_dark_skin_tone": "1f9b6-1f3ff", "female_superhero_light_skin_tone": "1f9b8-1f3fb-200d-2640-fe0f", "female_superhero_medium_light_skin_tone": "1f9b8-1f3fc-200d-2640-fe0f", "female_superhero_medium_skin_tone": "1f9b8-1f3fd-200d-2640-fe0f", "female_superhero_medium_dark_skin_tone": "1f9b8-1f3fe-200d-2640-fe0f", "female_superhero_dark_skin_tone": "1f9b8-1f3ff-200d-2640-fe0f", "male_superhero_light_skin_tone": "1f9b8-1f3fb-200d-2642-fe0f", "male_superhero_medium_light_skin_tone": "1f9b8-1f3fc-200d-2642-fe0f", "male_superhero_medium_skin_tone": "1f9b8-1f3fd-200d-2642-fe0f", "male_superhero_medium_dark_skin_tone": "1f9b8-1f3fe-200d-2642-fe0f", "male_superhero_dark_skin_tone": "1f9b8-1f3ff-200d-2642-fe0f", "superhero_light_skin_tone": "1f9b8-1f3fb", "superhero_medium_light_skin_tone": "1f9b8-1f3fc", "superhero_medium_skin_tone": "1f9b8-1f3fd", "superhero_medium_dark_skin_tone": "1f9b8-1f3fe", "superhero_dark_skin_tone": "1f9b8-1f3ff", "female_supervillain_light_skin_tone": "1f9b9-1f3fb-200d-2640-fe0f", "female_supervillain_medium_light_skin_tone": "1f9b9-1f3fc-200d-2640-fe0f", "female_supervillain_medium_skin_tone": "1f9b9-1f3fd-200d-2640-fe0f", "female_supervillain_medium_dark_skin_tone": "1f9b9-1f3fe-200d-2640-fe0f", "female_supervillain_dark_skin_tone": "1f9b9-1f3ff-200d-2640-fe0f", "male_supervillain_light_skin_tone": "1f9b9-1f3fb-200d-2642-fe0f", "male_supervillain_medium_light_skin_tone": "1f9b9-1f3fc-200d-2642-fe0f", "male_supervillain_medium_skin_tone": "1f9b9-1f3fd-200d-2642-fe0f", "male_supervillain_medium_dark_skin_tone": "1f9b9-1f3fe-200d-2642-fe0f", "male_supervillain_dark_skin_tone": "1f9b9-1f3ff-200d-2642-fe0f", "supervillain_light_skin_tone": "1f9b9-1f3fb", "supervillain_medium_light_skin_tone": "1f9b9-1f3fc", "supervillain_medium_skin_tone": "1f9b9-1f3fd", "supervillain_medium_dark_skin_tone": "1f9b9-1f3fe", "supervillain_dark_skin_tone": "1f9b9-1f3ff", "ear_with_hearing_aid_light_skin_tone": "1f9bb-1f3fb", "ear_with_hearing_aid_medium_light_skin_tone": "1f9bb-1f3fc", "ear_with_hearing_aid_medium_skin_tone": "1f9bb-1f3fd", "ear_with_hearing_aid_medium_dark_skin_tone": "1f9bb-1f3fe", "ear_with_hearing_aid_dark_skin_tone": "1f9bb-1f3ff", "woman_standing_light_skin_tone": "1f9cd-1f3fb-200d-2640-fe0f", "woman_standing_medium_light_skin_tone": "1f9cd-1f3fc-200d-2640-fe0f", "woman_standing_medium_skin_tone": "1f9cd-1f3fd-200d-2640-fe0f", "woman_standing_medium_dark_skin_tone": "1f9cd-1f3fe-200d-2640-fe0f", "woman_standing_dark_skin_tone": "1f9cd-1f3ff-200d-2640-fe0f", "man_standing_light_skin_tone": "1f9cd-1f3fb-200d-2642-fe0f", "man_standing_medium_light_skin_tone": "1f9cd-1f3fc-200d-2642-fe0f", "man_standing_medium_skin_tone": "1f9cd-1f3fd-200d-2642-fe0f", "man_standing_medium_dark_skin_tone": "1f9cd-1f3fe-200d-2642-fe0f", "man_standing_dark_skin_tone": "1f9cd-1f3ff-200d-2642-fe0f", "standing_person_light_skin_tone": "1f9cd-1f3fb", "standing_person_medium_light_skin_tone": "1f9cd-1f3fc", "standing_person_medium_skin_tone": "1f9cd-1f3fd", "standing_person_medium_dark_skin_tone": "1f9cd-1f3fe", "standing_person_dark_skin_tone": "1f9cd-1f3ff", "woman_kneeling_light_skin_tone": "1f9ce-1f3fb-200d-2640-fe0f", "woman_kneeling_medium_light_skin_tone": "1f9ce-1f3fc-200d-2640-fe0f", "woman_kneeling_medium_skin_tone": "1f9ce-1f3fd-200d-2640-fe0f", "woman_kneeling_medium_dark_skin_tone": "1f9ce-1f3fe-200d-2640-fe0f", "woman_kneeling_dark_skin_tone": "1f9ce-1f3ff-200d-2640-fe0f", "man_kneeling_light_skin_tone": "1f9ce-1f3fb-200d-2642-fe0f", "man_kneeling_medium_light_skin_tone": "1f9ce-1f3fc-200d-2642-fe0f", "man_kneeling_medium_skin_tone": "1f9ce-1f3fd-200d-2642-fe0f", "man_kneeling_medium_dark_skin_tone": "1f9ce-1f3fe-200d-2642-fe0f", "man_kneeling_dark_skin_tone": "1f9ce-1f3ff-200d-2642-fe0f", "kneeling_person_light_skin_tone": "1f9ce-1f3fb", "kneeling_person_medium_light_skin_tone": "1f9ce-1f3fc", "kneeling_person_medium_skin_tone": "1f9ce-1f3fd", "kneeling_person_medium_dark_skin_tone": "1f9ce-1f3fe", "kneeling_person_dark_skin_tone": "1f9ce-1f3ff", "deaf_woman_light_skin_tone": "1f9cf-1f3fb-200d-2640-fe0f", "deaf_woman_medium_light_skin_tone": "1f9cf-1f3fc-200d-2640-fe0f", "deaf_woman_medium_skin_tone": "1f9cf-1f3fd-200d-2640-fe0f", "deaf_woman_medium_dark_skin_tone": "1f9cf-1f3fe-200d-2640-fe0f", "deaf_woman_dark_skin_tone": "1f9cf-1f3ff-200d-2640-fe0f", "deaf_man_light_skin_tone": "1f9cf-1f3fb-200d-2642-fe0f", "deaf_man_medium_light_skin_tone": "1f9cf-1f3fc-200d-2642-fe0f", "deaf_man_medium_skin_tone": "1f9cf-1f3fd-200d-2642-fe0f", "deaf_man_medium_dark_skin_tone": "1f9cf-1f3fe-200d-2642-fe0f", "deaf_man_dark_skin_tone": "1f9cf-1f3ff-200d-2642-fe0f", "deaf_person_light_skin_tone": "1f9cf-1f3fb", "deaf_person_medium_light_skin_tone": "1f9cf-1f3fc", "deaf_person_medium_skin_tone": "1f9cf-1f3fd", "deaf_person_medium_dark_skin_tone": "1f9cf-1f3fe", "deaf_person_dark_skin_tone": "1f9cf-1f3ff", "farmer_light_skin_tone": "1f9d1-1f3fb-200d-1f33e", "farmer_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f33e", "farmer_medium_skin_tone": "1f9d1-1f3fd-200d-1f33e", "farmer_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f33e", "farmer_dark_skin_tone": "1f9d1-1f3ff-200d-1f33e", "cook_light_skin_tone": "1f9d1-1f3fb-200d-1f373", "cook_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f373", "cook_medium_skin_tone": "1f9d1-1f3fd-200d-1f373", "cook_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f373", "cook_dark_skin_tone": "1f9d1-1f3ff-200d-1f373", "person_feeding_baby_light_skin_tone": "1f9d1-1f3fb-200d-1f37c", "person_feeding_baby_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f37c", "person_feeding_baby_medium_skin_tone": "1f9d1-1f3fd-200d-1f37c", "person_feeding_baby_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f37c", "person_feeding_baby_dark_skin_tone": "1f9d1-1f3ff-200d-1f37c", "mx_claus_light_skin_tone": "1f9d1-1f3fb-200d-1f384", "mx_claus_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f384", "mx_claus_medium_skin_tone": "1f9d1-1f3fd-200d-1f384", "mx_claus_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f384", "mx_claus_dark_skin_tone": "1f9d1-1f3ff-200d-1f384", "student_light_skin_tone": "1f9d1-1f3fb-200d-1f393", "student_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f393", "student_medium_skin_tone": "1f9d1-1f3fd-200d-1f393", "student_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f393", "student_dark_skin_tone": "1f9d1-1f3ff-200d-1f393", "singer_light_skin_tone": "1f9d1-1f3fb-200d-1f3a4", "singer_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f3a4", "singer_medium_skin_tone": "1f9d1-1f3fd-200d-1f3a4", "singer_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f3a4", "singer_dark_skin_tone": "1f9d1-1f3ff-200d-1f3a4", "artist_light_skin_tone": "1f9d1-1f3fb-200d-1f3a8", "artist_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f3a8", "artist_medium_skin_tone": "1f9d1-1f3fd-200d-1f3a8", "artist_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f3a8", "artist_dark_skin_tone": "1f9d1-1f3ff-200d-1f3a8", "teacher_light_skin_tone": "1f9d1-1f3fb-200d-1f3eb", "teacher_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f3eb", "teacher_medium_skin_tone": "1f9d1-1f3fd-200d-1f3eb", "teacher_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f3eb", "teacher_dark_skin_tone": "1f9d1-1f3ff-200d-1f3eb", "factory_worker_light_skin_tone": "1f9d1-1f3fb-200d-1f3ed", "factory_worker_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f3ed", "factory_worker_medium_skin_tone": "1f9d1-1f3fd-200d-1f3ed", "factory_worker_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f3ed", "factory_worker_dark_skin_tone": "1f9d1-1f3ff-200d-1f3ed", "technologist_light_skin_tone": "1f9d1-1f3fb-200d-1f4bb", "technologist_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f4bb", "technologist_medium_skin_tone": "1f9d1-1f3fd-200d-1f4bb", "technologist_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f4bb", "technologist_dark_skin_tone": "1f9d1-1f3ff-200d-1f4bb", "office_worker_light_skin_tone": "1f9d1-1f3fb-200d-1f4bc", "office_worker_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f4bc", "office_worker_medium_skin_tone": "1f9d1-1f3fd-200d-1f4bc", "office_worker_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f4bc", "office_worker_dark_skin_tone": "1f9d1-1f3ff-200d-1f4bc", "mechanic_light_skin_tone": "1f9d1-1f3fb-200d-1f527", "mechanic_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f527", "mechanic_medium_skin_tone": "1f9d1-1f3fd-200d-1f527", "mechanic_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f527", "mechanic_dark_skin_tone": "1f9d1-1f3ff-200d-1f527", "scientist_light_skin_tone": "1f9d1-1f3fb-200d-1f52c", "scientist_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f52c", "scientist_medium_skin_tone": "1f9d1-1f3fd-200d-1f52c", "scientist_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f52c", "scientist_dark_skin_tone": "1f9d1-1f3ff-200d-1f52c", "astronaut_light_skin_tone": "1f9d1-1f3fb-200d-1f680", "astronaut_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f680", "astronaut_medium_skin_tone": "1f9d1-1f3fd-200d-1f680", "astronaut_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f680", "astronaut_dark_skin_tone": "1f9d1-1f3ff-200d-1f680", "firefighter_light_skin_tone": "1f9d1-1f3fb-200d-1f692", "firefighter_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f692", "firefighter_medium_skin_tone": "1f9d1-1f3fd-200d-1f692", "firefighter_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f692", "firefighter_dark_skin_tone": "1f9d1-1f3ff-200d-1f692", "people_holding_hands_light_skin_tone_light_skin_tone": "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fb", "people_holding_hands_light_skin_tone_medium_light_skin_tone": "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fc", "people_holding_hands_light_skin_tone_medium_skin_tone": "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fd", "people_holding_hands_light_skin_tone_medium_dark_skin_tone": "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fe", "people_holding_hands_light_skin_tone_dark_skin_tone": "1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3ff", "people_holding_hands_medium_light_skin_tone_light_skin_tone": "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fb", "people_holding_hands_medium_light_skin_tone_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fc", "people_holding_hands_medium_light_skin_tone_medium_skin_tone": "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fd", "people_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fe", "people_holding_hands_medium_light_skin_tone_dark_skin_tone": "1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3ff", "people_holding_hands_medium_skin_tone_light_skin_tone": "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fb", "people_holding_hands_medium_skin_tone_medium_light_skin_tone": "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fc", "people_holding_hands_medium_skin_tone_medium_skin_tone": "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fd", "people_holding_hands_medium_skin_tone_medium_dark_skin_tone": "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fe", "people_holding_hands_medium_skin_tone_dark_skin_tone": "1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3ff", "people_holding_hands_medium_dark_skin_tone_light_skin_tone": "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fb", "people_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fc", "people_holding_hands_medium_dark_skin_tone_medium_skin_tone": "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fd", "people_holding_hands_medium_dark_skin_tone_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fe", "people_holding_hands_medium_dark_skin_tone_dark_skin_tone": "1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3ff", "people_holding_hands_dark_skin_tone_light_skin_tone": "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fb", "people_holding_hands_dark_skin_tone_medium_light_skin_tone": "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fc", "people_holding_hands_dark_skin_tone_medium_skin_tone": "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fd", "people_holding_hands_dark_skin_tone_medium_dark_skin_tone": "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fe", "people_holding_hands_dark_skin_tone_dark_skin_tone": "1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3ff", "person_with_probing_cane_light_skin_tone": "1f9d1-1f3fb-200d-1f9af", "person_with_probing_cane_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f9af", "person_with_probing_cane_medium_skin_tone": "1f9d1-1f3fd-200d-1f9af", "person_with_probing_cane_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f9af", "person_with_probing_cane_dark_skin_tone": "1f9d1-1f3ff-200d-1f9af", "red_haired_person_light_skin_tone": "1f9d1-1f3fb-200d-1f9b0", "red_haired_person_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f9b0", "red_haired_person_medium_skin_tone": "1f9d1-1f3fd-200d-1f9b0", "red_haired_person_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f9b0", "red_haired_person_dark_skin_tone": "1f9d1-1f3ff-200d-1f9b0", "curly_haired_person_light_skin_tone": "1f9d1-1f3fb-200d-1f9b1", "curly_haired_person_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f9b1", "curly_haired_person_medium_skin_tone": "1f9d1-1f3fd-200d-1f9b1", "curly_haired_person_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f9b1", "curly_haired_person_dark_skin_tone": "1f9d1-1f3ff-200d-1f9b1", "bald_person_light_skin_tone": "1f9d1-1f3fb-200d-1f9b2", "bald_person_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f9b2", "bald_person_medium_skin_tone": "1f9d1-1f3fd-200d-1f9b2", "bald_person_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f9b2", "bald_person_dark_skin_tone": "1f9d1-1f3ff-200d-1f9b2", "white_haired_person_light_skin_tone": "1f9d1-1f3fb-200d-1f9b3", "white_haired_person_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f9b3", "white_haired_person_medium_skin_tone": "1f9d1-1f3fd-200d-1f9b3", "white_haired_person_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f9b3", "white_haired_person_dark_skin_tone": "1f9d1-1f3ff-200d-1f9b3", "person_in_motorized_wheelchair_light_skin_tone": "1f9d1-1f3fb-200d-1f9bc", "person_in_motorized_wheelchair_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f9bc", "person_in_motorized_wheelchair_medium_skin_tone": "1f9d1-1f3fd-200d-1f9bc", "person_in_motorized_wheelchair_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f9bc", "person_in_motorized_wheelchair_dark_skin_tone": "1f9d1-1f3ff-200d-1f9bc", "person_in_manual_wheelchair_light_skin_tone": "1f9d1-1f3fb-200d-1f9bd", "person_in_manual_wheelchair_medium_light_skin_tone": "1f9d1-1f3fc-200d-1f9bd", "person_in_manual_wheelchair_medium_skin_tone": "1f9d1-1f3fd-200d-1f9bd", "person_in_manual_wheelchair_medium_dark_skin_tone": "1f9d1-1f3fe-200d-1f9bd", "person_in_manual_wheelchair_dark_skin_tone": "1f9d1-1f3ff-200d-1f9bd", "health_worker_light_skin_tone": "1f9d1-1f3fb-200d-2695-fe0f", "health_worker_medium_light_skin_tone": "1f9d1-1f3fc-200d-2695-fe0f", "health_worker_medium_skin_tone": "1f9d1-1f3fd-200d-2695-fe0f", "health_worker_medium_dark_skin_tone": "1f9d1-1f3fe-200d-2695-fe0f", "health_worker_dark_skin_tone": "1f9d1-1f3ff-200d-2695-fe0f", "judge_light_skin_tone": "1f9d1-1f3fb-200d-2696-fe0f", "judge_medium_light_skin_tone": "1f9d1-1f3fc-200d-2696-fe0f", "judge_medium_skin_tone": "1f9d1-1f3fd-200d-2696-fe0f", "judge_medium_dark_skin_tone": "1f9d1-1f3fe-200d-2696-fe0f", "judge_dark_skin_tone": "1f9d1-1f3ff-200d-2696-fe0f", "pilot_light_skin_tone": "1f9d1-1f3fb-200d-2708-fe0f", "pilot_medium_light_skin_tone": "1f9d1-1f3fc-200d-2708-fe0f", "pilot_medium_skin_tone": "1f9d1-1f3fd-200d-2708-fe0f", "pilot_medium_dark_skin_tone": "1f9d1-1f3fe-200d-2708-fe0f", "pilot_dark_skin_tone": "1f9d1-1f3ff-200d-2708-fe0f", "adult_light_skin_tone": "1f9d1-1f3fb", "adult_medium_light_skin_tone": "1f9d1-1f3fc", "adult_medium_skin_tone": "1f9d1-1f3fd", "adult_medium_dark_skin_tone": "1f9d1-1f3fe", "adult_dark_skin_tone": "1f9d1-1f3ff", "child_light_skin_tone": "1f9d2-1f3fb", "child_medium_light_skin_tone": "1f9d2-1f3fc", "child_medium_skin_tone": "1f9d2-1f3fd", "child_medium_dark_skin_tone": "1f9d2-1f3fe", "child_dark_skin_tone": "1f9d2-1f3ff", "older_adult_light_skin_tone": "1f9d3-1f3fb", "older_adult_medium_light_skin_tone": "1f9d3-1f3fc", "older_adult_medium_skin_tone": "1f9d3-1f3fd", "older_adult_medium_dark_skin_tone": "1f9d3-1f3fe", "older_adult_dark_skin_tone": "1f9d3-1f3ff", "bearded_person_light_skin_tone": "1f9d4-1f3fb", "bearded_person_medium_light_skin_tone": "1f9d4-1f3fc", "bearded_person_medium_skin_tone": "1f9d4-1f3fd", "bearded_person_medium_dark_skin_tone": "1f9d4-1f3fe", "bearded_person_dark_skin_tone": "1f9d4-1f3ff", "person_with_headscarf_light_skin_tone": "1f9d5-1f3fb", "person_with_headscarf_medium_light_skin_tone": "1f9d5-1f3fc", "person_with_headscarf_medium_skin_tone": "1f9d5-1f3fd", "person_with_headscarf_medium_dark_skin_tone": "1f9d5-1f3fe", "person_with_headscarf_dark_skin_tone": "1f9d5-1f3ff", "woman_in_steamy_room_light_skin_tone": "1f9d6-1f3fb-200d-2640-fe0f", "woman_in_steamy_room_medium_light_skin_tone": "1f9d6-1f3fc-200d-2640-fe0f", "woman_in_steamy_room_medium_skin_tone": "1f9d6-1f3fd-200d-2640-fe0f", "woman_in_steamy_room_medium_dark_skin_tone": "1f9d6-1f3fe-200d-2640-fe0f", "woman_in_steamy_room_dark_skin_tone": "1f9d6-1f3ff-200d-2640-fe0f", "man_in_steamy_room_light_skin_tone": "1f9d6-1f3fb-200d-2642-fe0f", "man_in_steamy_room_medium_light_skin_tone": "1f9d6-1f3fc-200d-2642-fe0f", "man_in_steamy_room_medium_skin_tone": "1f9d6-1f3fd-200d-2642-fe0f", "man_in_steamy_room_medium_dark_skin_tone": "1f9d6-1f3fe-200d-2642-fe0f", "man_in_steamy_room_dark_skin_tone": "1f9d6-1f3ff-200d-2642-fe0f", "person_in_steamy_room_light_skin_tone": "1f9d6-1f3fb", "person_in_steamy_room_medium_light_skin_tone": "1f9d6-1f3fc", "person_in_steamy_room_medium_skin_tone": "1f9d6-1f3fd", "person_in_steamy_room_medium_dark_skin_tone": "1f9d6-1f3fe", "person_in_steamy_room_dark_skin_tone": "1f9d6-1f3ff", "woman_climbing_light_skin_tone": "1f9d7-1f3fb-200d-2640-fe0f", "woman_climbing_medium_light_skin_tone": "1f9d7-1f3fc-200d-2640-fe0f", "woman_climbing_medium_skin_tone": "1f9d7-1f3fd-200d-2640-fe0f", "woman_climbing_medium_dark_skin_tone": "1f9d7-1f3fe-200d-2640-fe0f", "woman_climbing_dark_skin_tone": "1f9d7-1f3ff-200d-2640-fe0f", "man_climbing_light_skin_tone": "1f9d7-1f3fb-200d-2642-fe0f", "man_climbing_medium_light_skin_tone": "1f9d7-1f3fc-200d-2642-fe0f", "man_climbing_medium_skin_tone": "1f9d7-1f3fd-200d-2642-fe0f", "man_climbing_medium_dark_skin_tone": "1f9d7-1f3fe-200d-2642-fe0f", "man_climbing_dark_skin_tone": "1f9d7-1f3ff-200d-2642-fe0f", "person_climbing_light_skin_tone": "1f9d7-1f3fb", "person_climbing_medium_light_skin_tone": "1f9d7-1f3fc", "person_climbing_medium_skin_tone": "1f9d7-1f3fd", "person_climbing_medium_dark_skin_tone": "1f9d7-1f3fe", "person_climbing_dark_skin_tone": "1f9d7-1f3ff", "woman_in_lotus_position_light_skin_tone": "1f9d8-1f3fb-200d-2640-fe0f", "woman_in_lotus_position_medium_light_skin_tone": "1f9d8-1f3fc-200d-2640-fe0f", "woman_in_lotus_position_medium_skin_tone": "1f9d8-1f3fd-200d-2640-fe0f", "woman_in_lotus_position_medium_dark_skin_tone": "1f9d8-1f3fe-200d-2640-fe0f", "woman_in_lotus_position_dark_skin_tone": "1f9d8-1f3ff-200d-2640-fe0f", "man_in_lotus_position_light_skin_tone": "1f9d8-1f3fb-200d-2642-fe0f", "man_in_lotus_position_medium_light_skin_tone": "1f9d8-1f3fc-200d-2642-fe0f", "man_in_lotus_position_medium_skin_tone": "1f9d8-1f3fd-200d-2642-fe0f", "man_in_lotus_position_medium_dark_skin_tone": "1f9d8-1f3fe-200d-2642-fe0f", "man_in_lotus_position_dark_skin_tone": "1f9d8-1f3ff-200d-2642-fe0f", "person_in_lotus_position_light_skin_tone": "1f9d8-1f3fb", "person_in_lotus_position_medium_light_skin_tone": "1f9d8-1f3fc", "person_in_lotus_position_medium_skin_tone": "1f9d8-1f3fd", "person_in_lotus_position_medium_dark_skin_tone": "1f9d8-1f3fe", "person_in_lotus_position_dark_skin_tone": "1f9d8-1f3ff", "female_mage_light_skin_tone": "1f9d9-1f3fb-200d-2640-fe0f", "female_mage_medium_light_skin_tone": "1f9d9-1f3fc-200d-2640-fe0f", "female_mage_medium_skin_tone": "1f9d9-1f3fd-200d-2640-fe0f", "female_mage_medium_dark_skin_tone": "1f9d9-1f3fe-200d-2640-fe0f", "female_mage_dark_skin_tone": "1f9d9-1f3ff-200d-2640-fe0f", "male_mage_light_skin_tone": "1f9d9-1f3fb-200d-2642-fe0f", "male_mage_medium_light_skin_tone": "1f9d9-1f3fc-200d-2642-fe0f", "male_mage_medium_skin_tone": "1f9d9-1f3fd-200d-2642-fe0f", "male_mage_medium_dark_skin_tone": "1f9d9-1f3fe-200d-2642-fe0f", "male_mage_dark_skin_tone": "1f9d9-1f3ff-200d-2642-fe0f", "mage_light_skin_tone": "1f9d9-1f3fb", "mage_medium_light_skin_tone": "1f9d9-1f3fc", "mage_medium_skin_tone": "1f9d9-1f3fd", "mage_medium_dark_skin_tone": "1f9d9-1f3fe", "mage_dark_skin_tone": "1f9d9-1f3ff", "female_fairy_light_skin_tone": "1f9da-1f3fb-200d-2640-fe0f", "female_fairy_medium_light_skin_tone": "1f9da-1f3fc-200d-2640-fe0f", "female_fairy_medium_skin_tone": "1f9da-1f3fd-200d-2640-fe0f", "female_fairy_medium_dark_skin_tone": "1f9da-1f3fe-200d-2640-fe0f", "female_fairy_dark_skin_tone": "1f9da-1f3ff-200d-2640-fe0f", "male_fairy_light_skin_tone": "1f9da-1f3fb-200d-2642-fe0f", "male_fairy_medium_light_skin_tone": "1f9da-1f3fc-200d-2642-fe0f", "male_fairy_medium_skin_tone": "1f9da-1f3fd-200d-2642-fe0f", "male_fairy_medium_dark_skin_tone": "1f9da-1f3fe-200d-2642-fe0f", "male_fairy_dark_skin_tone": "1f9da-1f3ff-200d-2642-fe0f", "fairy_light_skin_tone": "1f9da-1f3fb", "fairy_medium_light_skin_tone": "1f9da-1f3fc", "fairy_medium_skin_tone": "1f9da-1f3fd", "fairy_medium_dark_skin_tone": "1f9da-1f3fe", "fairy_dark_skin_tone": "1f9da-1f3ff", "female_vampire_light_skin_tone": "1f9db-1f3fb-200d-2640-fe0f", "female_vampire_medium_light_skin_tone": "1f9db-1f3fc-200d-2640-fe0f", "female_vampire_medium_skin_tone": "1f9db-1f3fd-200d-2640-fe0f", "female_vampire_medium_dark_skin_tone": "1f9db-1f3fe-200d-2640-fe0f", "female_vampire_dark_skin_tone": "1f9db-1f3ff-200d-2640-fe0f", "male_vampire_light_skin_tone": "1f9db-1f3fb-200d-2642-fe0f", "male_vampire_medium_light_skin_tone": "1f9db-1f3fc-200d-2642-fe0f", "male_vampire_medium_skin_tone": "1f9db-1f3fd-200d-2642-fe0f", "male_vampire_medium_dark_skin_tone": "1f9db-1f3fe-200d-2642-fe0f", "male_vampire_dark_skin_tone": "1f9db-1f3ff-200d-2642-fe0f", "vampire_light_skin_tone": "1f9db-1f3fb", "vampire_medium_light_skin_tone": "1f9db-1f3fc", "vampire_medium_skin_tone": "1f9db-1f3fd", "vampire_medium_dark_skin_tone": "1f9db-1f3fe", "vampire_dark_skin_tone": "1f9db-1f3ff", "mermaid_light_skin_tone": "1f9dc-1f3fb-200d-2640-fe0f", "mermaid_medium_light_skin_tone": "1f9dc-1f3fc-200d-2640-fe0f", "mermaid_medium_skin_tone": "1f9dc-1f3fd-200d-2640-fe0f", "mermaid_medium_dark_skin_tone": "1f9dc-1f3fe-200d-2640-fe0f", "mermaid_dark_skin_tone": "1f9dc-1f3ff-200d-2640-fe0f", "merman_light_skin_tone": "1f9dc-1f3fb-200d-2642-fe0f", "merman_medium_light_skin_tone": "1f9dc-1f3fc-200d-2642-fe0f", "merman_medium_skin_tone": "1f9dc-1f3fd-200d-2642-fe0f", "merman_medium_dark_skin_tone": "1f9dc-1f3fe-200d-2642-fe0f", "merman_dark_skin_tone": "1f9dc-1f3ff-200d-2642-fe0f", "merperson_light_skin_tone": "1f9dc-1f3fb", "merperson_medium_light_skin_tone": "1f9dc-1f3fc", "merperson_medium_skin_tone": "1f9dc-1f3fd", "merperson_medium_dark_skin_tone": "1f9dc-1f3fe", "merperson_dark_skin_tone": "1f9dc-1f3ff", "female_elf_light_skin_tone": "1f9dd-1f3fb-200d-2640-fe0f", "female_elf_medium_light_skin_tone": "1f9dd-1f3fc-200d-2640-fe0f", "female_elf_medium_skin_tone": "1f9dd-1f3fd-200d-2640-fe0f", "female_elf_medium_dark_skin_tone": "1f9dd-1f3fe-200d-2640-fe0f", "female_elf_dark_skin_tone": "1f9dd-1f3ff-200d-2640-fe0f", "male_elf_light_skin_tone": "1f9dd-1f3fb-200d-2642-fe0f", "male_elf_medium_light_skin_tone": "1f9dd-1f3fc-200d-2642-fe0f", "male_elf_medium_skin_tone": "1f9dd-1f3fd-200d-2642-fe0f", "male_elf_medium_dark_skin_tone": "1f9dd-1f3fe-200d-2642-fe0f", "male_elf_dark_skin_tone": "1f9dd-1f3ff-200d-2642-fe0f", "elf_light_skin_tone": "1f9dd-1f3fb", "elf_medium_light_skin_tone": "1f9dd-1f3fc", "elf_medium_skin_tone": "1f9dd-1f3fd", "elf_medium_dark_skin_tone": "1f9dd-1f3fe", "elf_dark_skin_tone": "1f9dd-1f3ff", "point_up_light_skin_tone": "261d-1f3fb", "point_up_medium_light_skin_tone": "261d-1f3fc", "point_up_medium_skin_tone": "261d-1f3fd", "point_up_medium_dark_skin_tone": "261d-1f3fe", "point_up_dark_skin_tone": "261d-1f3ff", "woman-bouncing-ball_light_skin_tone": "26f9-1f3fb-200d-2640-fe0f", "basketball_woman_light_skin_tone": "26f9-1f3fb-200d-2640-fe0f", "woman-bouncing-ball_medium_light_skin_tone": "26f9-1f3fc-200d-2640-fe0f", "basketball_woman_medium_light_skin_tone": "26f9-1f3fc-200d-2640-fe0f", "woman-bouncing-ball_medium_skin_tone": "26f9-1f3fd-200d-2640-fe0f", "basketball_woman_medium_skin_tone": "26f9-1f3fd-200d-2640-fe0f", "woman-bouncing-ball_medium_dark_skin_tone": "26f9-1f3fe-200d-2640-fe0f", "basketball_woman_medium_dark_skin_tone": "26f9-1f3fe-200d-2640-fe0f", "woman-bouncing-ball_dark_skin_tone": "26f9-1f3ff-200d-2640-fe0f", "basketball_woman_dark_skin_tone": "26f9-1f3ff-200d-2640-fe0f", "man-bouncing-ball_light_skin_tone": "26f9-1f3fb-200d-2642-fe0f", "basketball_man_light_skin_tone": "26f9-1f3fb-200d-2642-fe0f", "man-bouncing-ball_medium_light_skin_tone": "26f9-1f3fc-200d-2642-fe0f", "basketball_man_medium_light_skin_tone": "26f9-1f3fc-200d-2642-fe0f", "man-bouncing-ball_medium_skin_tone": "26f9-1f3fd-200d-2642-fe0f", "basketball_man_medium_skin_tone": "26f9-1f3fd-200d-2642-fe0f", "man-bouncing-ball_medium_dark_skin_tone": "26f9-1f3fe-200d-2642-fe0f", "basketball_man_medium_dark_skin_tone": "26f9-1f3fe-200d-2642-fe0f", "man-bouncing-ball_dark_skin_tone": "26f9-1f3ff-200d-2642-fe0f", "basketball_man_dark_skin_tone": "26f9-1f3ff-200d-2642-fe0f", "person_with_ball_light_skin_tone": "26f9-1f3fb", "person_with_ball_medium_light_skin_tone": "26f9-1f3fc", "person_with_ball_medium_skin_tone": "26f9-1f3fd", "person_with_ball_medium_dark_skin_tone": "26f9-1f3fe", "person_with_ball_dark_skin_tone": "26f9-1f3ff", "fist_light_skin_tone": "270a-1f3fb", "fist_raised_light_skin_tone": "270a-1f3fb", "fist_medium_light_skin_tone": "270a-1f3fc", "fist_raised_medium_light_skin_tone": "270a-1f3fc", "fist_medium_skin_tone": "270a-1f3fd", "fist_raised_medium_skin_tone": "270a-1f3fd", "fist_medium_dark_skin_tone": "270a-1f3fe", "fist_raised_medium_dark_skin_tone": "270a-1f3fe", "fist_dark_skin_tone": "270a-1f3ff", "fist_raised_dark_skin_tone": "270a-1f3ff", "hand_light_skin_tone": "270b-1f3fb", "raised_hand_light_skin_tone": "270b-1f3fb", "hand_medium_light_skin_tone": "270b-1f3fc", "raised_hand_medium_light_skin_tone": "270b-1f3fc", "hand_medium_skin_tone": "270b-1f3fd", "raised_hand_medium_skin_tone": "270b-1f3fd", "hand_medium_dark_skin_tone": "270b-1f3fe", "raised_hand_medium_dark_skin_tone": "270b-1f3fe", "hand_dark_skin_tone": "270b-1f3ff", "raised_hand_dark_skin_tone": "270b-1f3ff", "v_light_skin_tone": "270c-1f3fb", "v_medium_light_skin_tone": "270c-1f3fc", "v_medium_skin_tone": "270c-1f3fd", "v_medium_dark_skin_tone": "270c-1f3fe", "v_dark_skin_tone": "270c-1f3ff", "writing_hand_light_skin_tone": "270d-1f3fb", "writing_hand_medium_light_skin_tone": "270d-1f3fc", "writing_hand_medium_skin_tone": "270d-1f3fd", "writing_hand_medium_dark_skin_tone": "270d-1f3fe", "writing_hand_dark_skin_tone": "270d-1f3ff", "mattermost": "mattermost"} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/emoji_search.go b/vendor/github.com/mattermost/mattermost-server/v6/model/emoji_search.go new file mode 100644 index 00000000..4d947a11 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/emoji_search.go @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type EmojiSearch struct { + Term string `json:"term"` + PrefixOnly bool `json:"prefix_only"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/feature_flags.go b/vendor/github.com/mattermost/mattermost-server/v6/model/feature_flags.go new file mode 100644 index 00000000..a341c254 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/feature_flags.go @@ -0,0 +1,104 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "reflect" + "strconv" +) + +type FeatureFlags struct { + // Exists only for unit and manual testing. + // When set to a value, will be returned by the ping endpoint. + TestFeature string + // Exists only for testing bool functionality. Boolean feature flags interpret "on" or "true" as true and + // all other values as false. + TestBoolFeature bool + + // Toggle on and off scheduled jobs for cloud user limit emails see MM-29999 + CloudDelinquentEmailJobsEnabled bool + + // Toggle on and off support for Collapsed Threads + CollapsedThreads bool + + // Enable the remote cluster service for shared channels. + EnableRemoteClusterService bool + + // AppsEnabled toggle the Apps framework functionalities both in server and client side + AppsEnabled bool + + // Feature flags to control plugin versions + PluginPlaybooks string `plugin_id:"playbooks"` + PluginApps string `plugin_id:"com.mattermost.apps"` + PluginFocalboard string `plugin_id:"focalboard"` + + // Enable timed dnd support for user status + TimedDND bool + + PermalinkPreviews bool + + // Enable the Global Header + GlobalHeader bool + + // Enable different team menu button treatments, possible values = ("none", "by_team_name", "inverted_sidebar_bg_color") + AddChannelButton string +} + +func (f *FeatureFlags) SetDefaults() { + f.TestFeature = "off" + f.TestBoolFeature = false + f.CloudDelinquentEmailJobsEnabled = false + f.CollapsedThreads = true + f.EnableRemoteClusterService = false + f.AppsEnabled = false + f.PluginApps = "" + f.PluginFocalboard = "" + f.TimedDND = false + f.PermalinkPreviews = true + f.GlobalHeader = true + f.AddChannelButton = "by_team_name" +} + +func (f *FeatureFlags) Plugins() map[string]string { + rFFVal := reflect.ValueOf(f).Elem() + rFFType := reflect.TypeOf(f).Elem() + + pluginVersions := make(map[string]string) + for i := 0; i < rFFVal.NumField(); i++ { + rFieldVal := rFFVal.Field(i) + rFieldType := rFFType.Field(i) + + pluginId, hasPluginId := rFieldType.Tag.Lookup("plugin_id") + if !hasPluginId { + continue + } + + pluginVersions[pluginId] = rFieldVal.String() + } + + return pluginVersions +} + +// ToMap returns the feature flags as a map[string]string +// Supports boolean and string feature flags. +func (f *FeatureFlags) ToMap() map[string]string { + refStructVal := reflect.ValueOf(*f) + refStructType := reflect.TypeOf(*f) + ret := make(map[string]string) + for i := 0; i < refStructVal.NumField(); i++ { + refFieldVal := refStructVal.Field(i) + if !refFieldVal.IsValid() { + continue + } + refFieldType := refStructType.Field(i) + switch refFieldType.Type.Kind() { + case reflect.Bool: + ret[refFieldType.Name] = strconv.FormatBool(refFieldVal.Bool()) + default: + ret[refFieldType.Name] = refFieldVal.String() + } + } + + return ret +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/file.go b/vendor/github.com/mattermost/mattermost-server/v6/model/file.go new file mode 100644 index 00000000..63d1ad33 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/file.go @@ -0,0 +1,13 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + MaxImageSize = int64(6048 * 4032) // 24 megapixels, roughly 36MB as a raw image +) + +type FileUploadResponse struct { + FileInfos []*FileInfo `json:"file_infos"` + ClientIds []string `json:"client_ids"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/file_info.go b/vendor/github.com/mattermost/mattermost-server/v6/model/file_info.go new file mode 100644 index 00000000..9519ef45 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/file_info.go @@ -0,0 +1,184 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "image" + "image/gif" + "io" + "mime" + "net/http" + "path/filepath" + "strings" +) + +const ( + FileinfoSortByCreated = "CreateAt" + FileinfoSortBySize = "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"` + ChannelId string `db:"-" json:"channel_id"` + 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"` + MiniPreview *[]byte `json:"mini_preview"` // declared as *[]byte to avoid postgres/mysql differences in deserialization + Content string `json:"-"` + RemoteId *string `json:"remote_id"` +} + +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 + } + + if fi.RemoteId == nil { + fi.RemoteId = NewString("") + } +} + +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 fi.PostId != "" && !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 io.ReadSeeker, size int) (*FileInfo, *AppError) { + info := &FileInfo{ + Name: name, + Size: int64(size), + } + 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(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 + data.Seek(0, io.SeekStart) + gifConfig, err := gif.DecodeAll(data) + if 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, err.Error(), http.StatusBadRequest) + } + 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/v6/model/file_info_list.go b/vendor/github.com/mattermost/mattermost-server/v6/model/file_info_list.go new file mode 100644 index 00000000..219b24f1 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/file_info_list.go @@ -0,0 +1,111 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "sort" +) + +type FileInfoList struct { + Order []string `json:"order"` + FileInfos map[string]*FileInfo `json:"file_infos"` + NextFileInfoId string `json:"next_file_info_id"` + PrevFileInfoId string `json:"prev_file_info_id"` +} + +func NewFileInfoList() *FileInfoList { + return &FileInfoList{ + Order: make([]string, 0), + FileInfos: make(map[string]*FileInfo), + NextFileInfoId: "", + PrevFileInfoId: "", + } +} + +func (o *FileInfoList) ToSlice() []*FileInfo { + var fileInfos []*FileInfo + for _, id := range o.Order { + fileInfos = append(fileInfos, o.FileInfos[id]) + } + return fileInfos +} + +func (o *FileInfoList) MakeNonNil() { + if o.Order == nil { + o.Order = make([]string, 0) + } + + if o.FileInfos == nil { + o.FileInfos = make(map[string]*FileInfo) + } +} + +func (o *FileInfoList) AddOrder(id string) { + if o.Order == nil { + o.Order = make([]string, 0, 128) + } + + o.Order = append(o.Order, id) +} + +func (o *FileInfoList) AddFileInfo(fileInfo *FileInfo) { + if o.FileInfos == nil { + o.FileInfos = make(map[string]*FileInfo) + } + + o.FileInfos[fileInfo.Id] = fileInfo +} + +func (o *FileInfoList) UniqueOrder() { + keys := make(map[string]bool) + order := []string{} + for _, fileInfoId := range o.Order { + if _, value := keys[fileInfoId]; !value { + keys[fileInfoId] = true + order = append(order, fileInfoId) + } + } + + o.Order = order +} + +func (o *FileInfoList) Extend(other *FileInfoList) { + for fileInfoId := range other.FileInfos { + o.AddFileInfo(other.FileInfos[fileInfoId]) + } + + for _, fileInfoId := range other.Order { + o.AddOrder(fileInfoId) + } + + o.UniqueOrder() +} + +func (o *FileInfoList) SortByCreateAt() { + sort.Slice(o.Order, func(i, j int) bool { + return o.FileInfos[o.Order[i]].CreateAt > o.FileInfos[o.Order[j]].CreateAt + }) +} + +func (o *FileInfoList) Etag() string { + id := "0" + var t int64 = 0 + + for _, v := range o.FileInfos { + 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) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/file_info_search_results.go b/vendor/github.com/mattermost/mattermost-server/v6/model/file_info_search_results.go new file mode 100644 index 00000000..fddbffd4 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/file_info_search_results.go @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type FileInfoSearchMatches map[string][]string + +type FileInfoSearchResults struct { + *FileInfoList + Matches FileInfoSearchMatches `json:"matches"` +} + +func MakeFileInfoSearchResults(fileInfos *FileInfoList, matches FileInfoSearchMatches) *FileInfoSearchResults { + return &FileInfoSearchResults{ + fileInfos, + matches, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/gitlab.go b/vendor/github.com/mattermost/mattermost-server/v6/model/gitlab.go new file mode 100644 index 00000000..c6233f13 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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 ( + UserAuthServiceGitlab = "gitlab" +) diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/group.go b/vendor/github.com/mattermost/mattermost-server/v6/model/group.go new file mode 100644 index 00000000..566b2361 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/group.go @@ -0,0 +1,190 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "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 +} + +type GroupStats struct { + GroupID string `json:"group_id"` + TotalMemberCount int64 `json:"total_member_count"` +} + +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 || (group.RemoteId == "" && 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", "app.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 +} + +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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/group_member.go b/vendor/github.com/mattermost/mattermost-server/v6/model/group_member.go new file mode 100644 index 00000000..d18d7849 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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/v6/model/group_syncable.go b/vendor/github.com/mattermost/mattermost-server/v6/model/group_syncable.go new file mode 100644 index 00000000..1f1bf60f --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/group_syncable.go @@ -0,0 +1,175 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "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 + } + var channelId string + var teamId string + for key, value := range kvp { + switch key { + case "team_id": + teamId = value.(string) + case "channel_id": + channelId = value.(string) + case "group_id": + syncable.GroupId = value.(string) + case "auto_add": + syncable.AutoAdd = value.(bool) + default: + } + } + if channelId != "" { + syncable.TeamID = teamId + syncable.SyncableId = channelId + syncable.Type = GroupSyncableTypeChannel + } else { + syncable.SyncableId = teamId + syncable.Type = GroupSyncableTypeTeam + } + 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"` + Type GroupSyncableType `json:"type,omitempty"` + *Alias + }{ + TeamDisplayName: syncable.TeamDisplayName, + TeamType: syncable.TeamType, + TeamID: syncable.SyncableId, + Type: syncable.Type, + 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"` + Type GroupSyncableType `json:"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, + Type: syncable.Type, + + 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 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/v6/model/guest_invite.go b/vendor/github.com/mattermost/mattermost-server/v6/model/guest_invite.go new file mode 100644 index 00000000..5103ccd1 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/guest_invite.go @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "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) > UserEmailMaxLength || email == "" || !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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/incoming_webhook.go b/vendor/github.com/mattermost/mattermost-server/v6/model/incoming_webhook.go new file mode 100644 index 00000000..ce7828e4 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/incoming_webhook.go @@ -0,0 +1,184 @@ +// 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 ( + DefaultWebhookUsername = "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) 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 + } + 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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/initial_load.go b/vendor/github.com/mattermost/mattermost-server/v6/model/initial_load.go new file mode 100644 index 00000000..5ecddda2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/initial_load.go @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +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"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/integration_action.go b/vendor/github.com/mattermost/mattermost-server/v6/model/integration_action.go new file mode 100644 index 00000000..c61cc6d4 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/integration_action.go @@ -0,0 +1,470 @@ +// 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" + "reflect" + "strconv" + "strings" +) + +const ( + PostActionTypeButton = "button" + PostActionTypeSelect = "select" + InteractiveDialogTriggerTimeoutMilliseconds = 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 + } + + switch inputValue.(type) { + case string, bool, int, float64: + if value != inputValue { + return false + } + default: + if !reflect.DeepEqual(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 > InteractiveDialogTriggerTimeoutMilliseconds { + return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.expired", map[string]interface{}{"Seconds": InteractiveDialogTriggerTimeoutMilliseconds / 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 (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 != nil && 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 != nil && 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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/integrity.go b/vendor/github.com/mattermost/mattermost-server/v6/model/integrity.go new file mode 100644 index 00000000..744ad07c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/integrity.go @@ -0,0 +1,58 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "errors" +) + +type OrphanedRecord struct { + ParentId *string `json:"parent_id"` + ChildId *string `json:"child_id"` +} + +type RelationalIntegrityCheckData struct { + ParentName string `json:"parent_name"` + ChildName string `json:"child_name"` + ParentIdAttr string `json:"parent_id_attr"` + ChildIdAttr string `json:"child_id_attr"` + Records []OrphanedRecord `json:"records"` +} + +type IntegrityCheckResult struct { + Data interface{} `json:"data"` + Err error `json:"err"` +} + +func (r *IntegrityCheckResult) UnmarshalJSON(b []byte) error { + var data map[string]interface{} + if err := json.Unmarshal(b, &data); err != nil { + return err + } + if d, ok := data["data"]; ok && d != nil { + var rdata RelationalIntegrityCheckData + m := d.(map[string]interface{}) + rdata.ParentName = m["parent_name"].(string) + rdata.ChildName = m["child_name"].(string) + rdata.ParentIdAttr = m["parent_id_attr"].(string) + rdata.ChildIdAttr = m["child_id_attr"].(string) + for _, recData := range m["records"].([]interface{}) { + var record OrphanedRecord + m := recData.(map[string]interface{}) + if val := m["parent_id"]; val != nil { + record.ParentId = NewString(val.(string)) + } + if val := m["child_id"]; val != nil { + record.ChildId = NewString(val.(string)) + } + rdata.Records = append(rdata.Records, record) + } + r.Data = rdata + } + if err, ok := data["err"]; ok && err != nil { + r.Err = errors.New(data["err"].(string)) + } + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/job.go b/vendor/github.com/mattermost/mattermost-server/v6/model/job.go new file mode 100644 index 00000000..13c6154f --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/job.go @@ -0,0 +1,130 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "time" +) + +const ( + JobTypeDataRetention = "data_retention" + JobTypeMessageExport = "message_export" + JobTypeElasticsearchPostIndexing = "elasticsearch_post_indexing" + JobTypeElasticsearchPostAggregation = "elasticsearch_post_aggregation" + JobTypeBlevePostIndexing = "bleve_post_indexing" + JobTypeLdapSync = "ldap_sync" + JobTypeMigrations = "migrations" + JobTypePlugins = "plugins" + JobTypeExpiryNotify = "expiry_notify" + JobTypeProductNotices = "product_notices" + JobTypeActiveUsers = "active_users" + JobTypeImportProcess = "import_process" + JobTypeImportDelete = "import_delete" + JobTypeExportProcess = "export_process" + JobTypeExportDelete = "export_delete" + JobTypeCloud = "cloud" + JobTypeResendInvitationEmail = "resend_invitation_email" + JobTypeExtractContent = "extract_content" + + JobStatusPending = "pending" + JobStatusInProgress = "in_progress" + JobStatusSuccess = "success" + JobStatusError = "error" + JobStatusCancelRequested = "cancel_requested" + JobStatusCanceled = "canceled" + JobStatusWarning = "warning" +) + +var AllJobTypes = [...]string{ + JobTypeDataRetention, + JobTypeMessageExport, + JobTypeElasticsearchPostIndexing, + JobTypeElasticsearchPostAggregation, + JobTypeBlevePostIndexing, + JobTypeLdapSync, + JobTypeMigrations, + JobTypePlugins, + JobTypeExpiryNotify, + JobTypeProductNotices, + JobTypeActiveUsers, + JobTypeImportProcess, + JobTypeImportDelete, + JobTypeExportProcess, + JobTypeExportDelete, + JobTypeCloud, + JobTypeExtractContent, +} + +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 JobTypeDataRetention: + case JobTypeElasticsearchPostIndexing: + case JobTypeElasticsearchPostAggregation: + case JobTypeBlevePostIndexing: + case JobTypeLdapSync: + case JobTypeMessageExport: + case JobTypeMigrations: + case JobTypePlugins: + case JobTypeProductNotices: + case JobTypeExpiryNotify: + case JobTypeActiveUsers: + case JobTypeImportProcess: + case JobTypeImportDelete: + case JobTypeExportProcess: + case JobTypeExportDelete: + case JobTypeCloud: + case JobTypeResendInvitationEmail: + case JobTypeExtractContent: + default: + return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest) + } + + switch j.Status { + case JobStatusPending: + case JobStatusInProgress: + case JobStatusSuccess: + case JobStatusError: + case JobStatusCancelRequested: + case JobStatusCanceled: + default: + return NewAppError("Job.IsValid", "model.job.is_valid.status.app_error", nil, "id="+j.Id, http.StatusBadRequest) + } + + return nil +} + +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/v6/model/ldap.go b/vendor/github.com/mattermost/mattermost-server/v6/model/ldap.go new file mode 100644 index 00000000..314e7222 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/ldap.go @@ -0,0 +1,10 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + UserAuthServiceLdap = "ldap" + LdapPublicCertificateName = "ldap-public.crt" + LdapPrivateKeyName = "ldap-private.key" +) diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/license.go b/vendor/github.com/mattermost/mattermost-server/v6/model/license.go new file mode 100644 index 00000000..d83a61f9 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/license.go @@ -0,0 +1,343 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +const ( + ExpiredLicenseError = "api.license.add_license.expired.app_error" + InvalidLicenseError = "api.license.add_license.invalid.app_error" + LicenseGracePeriod = 1000 * 60 * 60 * 24 * 10 //10 days + LicenseRenewalLink = "https://mattermost.com/renew/" + + LicenseShortSkuE10 = "E10" + LicenseShortSkuE20 = "E20" + LicenseShortSkuProfessional = "professional" + LicenseShortSkuEnterprise = "enterprise" +) + +const ( + LicenseUpForRenewalEmailSent = "LicenseUpForRenewalEmailSent" +) + +var ( + trialDuration = 30*(time.Hour*24) + (time.Hour * 8) // 720 hours (30 days) + 8 hours is trial license duration + adminTrialDuration = 30*(time.Hour*24) + (time.Hour * 23) + (time.Minute * 59) + (time.Second * 59) // 720 hours (30 days) + 23 hours, 59 mins and 59 seconds + + // a sanctioned trial's duration is either more than the upper bound, + // or less than the lower bound + sanctionedTrialDurationLowerBound = 31*(time.Hour*24) + (time.Hour * 23) + (time.Minute * 59) + (time.Second * 59) // 744 hours (31 days) + 23 hours, 59 mins and 59 seconds + sanctionedTrialDurationUpperBound = 29*(time.Hour*24) + (time.Hour * 23) + (time.Minute * 59) + (time.Second * 59) // 696 hours (29 days) + 23 hours, 59 mins and 59 seconds +) + +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"` + IsTrial bool `json:"is_trial"` +} + +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"` +} + +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"` + OpenId *bool `json:"openid"` + 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"` + AdvancedLogging *bool `json:"advanced_logging"` + Cloud *bool `json:"cloud"` + SharedChannels *bool `json:"shared_channels"` + RemoteClusterService *bool `json:"remote_cluster_service"` + + // 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, + "openid": *f.OpenId, + "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, + "advanced_logging": *f.AdvancedLogging, + "cloud": *f.Cloud, + "shared_channels": *f.SharedChannels, + "remote_cluster_service": *f.RemoteClusterService, + "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.OpenId == nil { + f.OpenId = 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) + } + + if f.AdvancedLogging == nil { + f.AdvancedLogging = NewBool(*f.FutureFeatures) + } + + if f.Cloud == nil { + f.Cloud = NewBool(false) + } + + if f.SharedChannels == nil { + f.SharedChannels = NewBool(*f.FutureFeatures) + } + + if f.RemoteClusterService == nil { + f.RemoteClusterService = NewBool(*f.FutureFeatures) + } +} + +func (l *License) IsExpired() bool { + return l.ExpiresAt < GetMillis() +} + +func (l *License) IsPastGracePeriod() bool { + timeDiff := GetMillis() - l.ExpiresAt + return timeDiff > LicenseGracePeriod +} + +func (l *License) IsWithinExpirationPeriod() bool { + days := l.DaysToExpiration() + return days <= 60 && days >= 58 +} + +func (l *License) DaysToExpiration() int { + dif := l.ExpiresAt - GetMillis() + d, _ := time.ParseDuration(fmt.Sprint(dif) + "ms") + days := d.Hours() / 24 + return int(days) +} + +func (l *License) IsStarted() bool { + return l.StartsAt < GetMillis() +} + +func (l *License) IsTrialLicense() bool { + return l.IsTrial || (l.ExpiresAt-l.StartsAt) == trialDuration.Milliseconds() || (l.ExpiresAt-l.StartsAt) == adminTrialDuration.Milliseconds() +} + +func (l *License) IsSanctionedTrial() bool { + duration := l.ExpiresAt - l.StartsAt + + return l.IsTrialLicense() && + (duration >= sanctionedTrialDurationLowerBound.Milliseconds() || duration <= sanctionedTrialDurationUpperBound.Milliseconds()) +} + +func (l *License) HasEnterpriseMarketplacePlugins() bool { + return *l.Features.EnterprisePlugins || + l.SkuShortName == LicenseShortSkuE20 || + l.SkuShortName == LicenseShortSkuProfessional || + l.SkuShortName == LicenseShortSkuEnterprise +} + +// 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 (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 lr.Bytes == "" || 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/v6/model/link_metadata.go b/vendor/github.com/mattermost/mattermost-server/v6/model/link_metadata.go new file mode 100644 index 00000000..66d10739 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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 ( + LinkMetadataTypeImage LinkMetadataType = "image" + LinkMetadataTypeNone LinkMetadataType = "none" + LinkMetadataTypeOpengraph LinkMetadataType = "opengraph" + LinkMetadataMaxImages 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 { // don't break stuff, if it's weird, go for sane defaults + maxImages = LinkMetadataMaxImages + } + 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, LinkMetadataMaxImages) + 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 LinkMetadataTypeImage: + 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 LinkMetadataTypeNone: + if o.Data != nil { + return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest) + } + case LinkMetadataTypeOpengraph: + 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 LinkMetadataTypeImage: + image := &PostImage{} + + err = json.Unmarshal(b, &image) + + data = image + case LinkMetadataTypeOpengraph: + 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)).UTC() + + return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, time.UTC).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/v6/model/manifest.go b/vendor/github.com/mattermost/mattermost-server/v6/model/manifest.go new file mode 100644 index 00000000..67b0d120 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/manifest.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" + "fmt" + "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" yaml:"name"` + + // 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"` + + // 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 pluginapi.Configuration.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 map[string]string `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"` +} + +// ManifestExecutables is a legacy structure capturing a subet of the known platform executables. +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) 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 + + if server == nil { + return "" + } + + var executable string + if len(server.Executables) > 0 { + osArch := fmt.Sprintf("%s-%s", goOs, goArch) + executable = server.Executables[osArch] + } + + if executable == "" { + executable = server.Executable + } + + return executable +} + +func (m *Manifest) HasServer() bool { + return m.Server != 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 strings.TrimSpace(m.Name) == "" { + return errors.New("a plugin name is needed") + } + + 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/v6/model/marketplace_plugin.go b/vendor/github.com/mattermost/mattermost-server/v6/model/marketplace_plugin.go new file mode 100644 index 00000000..8f0371b1 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/marketplace_plugin.go @@ -0,0 +1,127 @@ +// 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,omitempty"` + Hosting string `json:"hosting"` // Indicated if the plugin is limited to a certain hosting type + AuthorType string `json:"author_type"` // The maintainer of the plugin + ReleaseStage string `json:"release_stage"` // The stage in the software release cycle that the plugin is in + Enterprise bool `json:"enterprise"` // Indicated if the plugin is an enterprise plugin + 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 + Cloud bool + LocalOnly bool + Platform string + PluginId string + ReturnAllVersions 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("cloud", strconv.FormatBool(filter.Cloud)) + q.Add("local_only", strconv.FormatBool(filter.LocalOnly)) + q.Add("platform", filter.Platform) + q.Add("plugin_id", filter.PluginId) + q.Add("return_all_versions", strconv.FormatBool(filter.ReturnAllVersions)) + 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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/mention_map.go b/vendor/github.com/mattermost/mattermost-server/v6/model/mention_map.go new file mode 100644 index 00000000..2f3444dd --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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/v6/model/message_export.go b/vendor/github.com/mattermost/mattermost-server/v6/model/message_export.go new file mode 100644 index 00000000..cc8f882b --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/message_export.go @@ -0,0 +1,50 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import "encoding/json" + +type MessageExport struct { + TeamId *string + TeamName *string + TeamDisplayName *string + + ChannelId *string + ChannelName *string + ChannelDisplayName *string + ChannelType *ChannelType + + 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 +} + +type MessageExportCursor struct { + LastPostUpdateAt int64 + LastPostId string +} + +// PreviewID returns the value of the post's previewed_post prop, if present, or an empty string. +func (m *MessageExport) PreviewID() string { + var previewID string + props := map[string]interface{}{} + if m.PostProps != nil && json.Unmarshal([]byte(*m.PostProps), &props) == nil { + if val, ok := props[PostPropsPreviewedPost]; ok { + previewID = val.(string) + } + } + return previewID +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/mfa_secret.go b/vendor/github.com/mattermost/mattermost-server/v6/model/mfa_secret.go new file mode 100644 index 00000000..8cfa675e --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/mfa_secret.go @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type MfaSecret struct { + Secret string `json:"secret"` + QRCode string `json:"qr_code"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/migration.go b/vendor/github.com/mattermost/mattermost-server/v6/model/migration.go new file mode 100644 index 00000000..2e0efb46 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/migration.go @@ -0,0 +1,38 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + AdvancedPermissionsMigrationKey = "AdvancedPermissionsMigrationComplete" + MigrationKeyAdvancedPermissionsPhase2 = "migration_advanced_permissions_phase_2" + + MigrationKeyEmojiPermissionsSplit = "emoji_permissions_split" + MigrationKeyWebhookPermissionsSplit = "webhook_permissions_split" + MigrationKeyListJoinPublicPrivateTeams = "list_join_public_private_teams" + MigrationKeyRemovePermanentDeleteUser = "remove_permanent_delete_user" + MigrationKeyAddBotPermissions = "add_bot_permissions" + MigrationKeyApplyChannelManageDeleteToChannelUser = "apply_channel_manage_delete_to_channel_user" + MigrationKeyRemoveChannelManageDeleteFromTeamUser = "remove_channel_manage_delete_from_team_user" + MigrationKeyViewMembersNewPermission = "view_members_new_permission" + MigrationKeyAddManageGuestsPermissions = "add_manage_guests_permissions" + MigrationKeyChannelModerationsPermissions = "channel_moderations_permissions" + MigrationKeyAddUseGroupMentionsPermission = "add_use_group_mentions_permission" + MigrationKeyAddSystemConsolePermissions = "add_system_console_permissions" + MigrationKeySidebarCategoriesPhase2 = "migration_sidebar_categories_phase_2" + MigrationKeyAddConvertChannelPermissions = "add_convert_channel_permissions" + MigrationKeyAddSystemRolesPermissions = "add_system_roles_permissions" + MigrationKeyAddBillingPermissions = "add_billing_permissions" + MigrationKeyAddManageSharedChannelPermissions = "manage_shared_channel_permissions" + MigrationKeyAddManageSecureConnectionsPermissions = "manage_secure_connections_permissions" + MigrationKeyAddDownloadComplianceExportResults = "download_compliance_export_results" + MigrationKeyAddComplianceSubsectionPermissions = "compliance_subsection_permissions" + MigrationKeyAddExperimentalSubsectionPermissions = "experimental_subsection_permissions" + MigrationKeyAddAuthenticationSubsectionPermissions = "authentication_subsection_permissions" + MigrationKeyAddSiteSubsectionPermissions = "site_subsection_permissions" + MigrationKeyAddEnvironmentSubsectionPermissions = "environment_subsection_permissions" + MigrationKeyAddReportingSubsectionPermissions = "reporting_subsection_permissions" + MigrationKeyAddTestEmailAncillaryPermission = "test_email_ancillary_permission" + MigrationKeyAddAboutSubsectionPermissions = "about_subsection_permissions" + MigrationKeyAddIntegrationsSubsectionPermissions = "integrations_subsection_permissions" +) diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/oauth.go b/vendor/github.com/mattermost/mattermost-server/v6/model/oauth.go new file mode 100644 index 00000000..a3d7f95f --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/oauth.go @@ -0,0 +1,127 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "net/http" + "unicode/utf8" +) + +const ( + OAuthActionSignup = "signup" + OAuthActionLogin = "login" + OAuthActionEmailToSSO = "email_to_sso" + OAuthActionSSOToEmail = "sso_to_email" + OAuthActionMobile = "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 a.ClientSecret == "" || len(a.ClientSecret) > 128 { + return NewAppError("OAuthApp.IsValid", "model.oauth.is_valid.client_secret.app_error", nil, "app_id="+a.Id, http.StatusBadRequest) + } + + if a.Name == "" || 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 a.Homepage == "" || 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 a.IconURL != "" { + 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() +} + +// 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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/outgoing_webhook.go b/vendor/github.com/mattermost/mattermost-server/v6/model/outgoing_webhook.go new file mode 100644 index 00000000..b24aed67 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/outgoing_webhook.go @@ -0,0 +1,224 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "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 OutgoingHookResponseTypeComment = "comment" + +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) 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 o.ChannelId != "" && !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 triggerWord == "" { + 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 word == "" { + return false + } + + for _, trigger := range o.TriggerWords { + if trigger == word { + return true + } + } + + return false +} + +func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool { + if word == "" { + 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 word == "" { + 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/v6/model/permalink.go b/vendor/github.com/mattermost/mattermost-server/v6/model/permalink.go new file mode 100644 index 00000000..6a19fb75 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/permalink.go @@ -0,0 +1,27 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type Permalink struct { + PreviewPost *PreviewPost `json:"preview_post"` +} + +type PreviewPost struct { + PostID string `json:"post_id"` + Post *Post `json:"post"` + TeamName string `json:"team_name"` + ChannelDisplayName string `json:"channel_display_name"` +} + +func NewPreviewPost(post *Post, team *Team, channel *Channel) *PreviewPost { + if post == nil { + return nil + } + return &PreviewPost{ + PostID: post.Id, + Post: post, + TeamName: team.Name, + ChannelDisplayName: channel.DisplayName, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/permission.go b/vendor/github.com/mattermost/mattermost-server/v6/model/permission.go new file mode 100644 index 00000000..6c10bab0 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/permission.go @@ -0,0 +1,2192 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + PermissionScopeSystem = "system_scope" + PermissionScopeTeam = "team_scope" + PermissionScopeChannel = "channel_scope" +) + +type Permission struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Scope string `json:"scope"` +} + +var PermissionInviteUser *Permission +var PermissionAddUserToTeam *Permission +var PermissionUseSlashCommands *Permission +var PermissionManageSlashCommands *Permission +var PermissionManageOthersSlashCommands *Permission +var PermissionCreatePublicChannel *Permission +var PermissionCreatePrivateChannel *Permission +var PermissionManagePublicChannelMembers *Permission +var PermissionManagePrivateChannelMembers *Permission +var PermissionConvertPublicChannelToPrivate *Permission +var PermissionConvertPrivateChannelToPublic *Permission +var PermissionAssignSystemAdminRole *Permission +var PermissionManageRoles *Permission +var PermissionManageTeamRoles *Permission +var PermissionManageChannelRoles *Permission +var PermissionCreateDirectChannel *Permission +var PermissionCreateGroupChannel *Permission +var PermissionManagePublicChannelProperties *Permission +var PermissionManagePrivateChannelProperties *Permission +var PermissionListPublicTeams *Permission +var PermissionJoinPublicTeams *Permission +var PermissionListPrivateTeams *Permission +var PermissionJoinPrivateTeams *Permission +var PermissionListTeamChannels *Permission +var PermissionJoinPublicChannels *Permission +var PermissionDeletePublicChannel *Permission +var PermissionDeletePrivateChannel *Permission +var PermissionEditOtherUsers *Permission +var PermissionReadChannel *Permission +var PermissionReadPublicChannelGroups *Permission +var PermissionReadPrivateChannelGroups *Permission +var PermissionReadPublicChannel *Permission +var PermissionAddReaction *Permission +var PermissionRemoveReaction *Permission +var PermissionRemoveOthersReactions *Permission +var PermissionPermanentDeleteUser *Permission +var PermissionUploadFile *Permission +var PermissionGetPublicLink *Permission +var PermissionManageWebhooks *Permission +var PermissionManageOthersWebhooks *Permission +var PermissionManageIncomingWebhooks *Permission +var PermissionManageOutgoingWebhooks *Permission +var PermissionManageOthersIncomingWebhooks *Permission +var PermissionManageOthersOutgoingWebhooks *Permission +var PermissionManageOAuth *Permission +var PermissionManageSystemWideOAuth *Permission +var PermissionManageEmojis *Permission +var PermissionManageOthersEmojis *Permission +var PermissionCreateEmojis *Permission +var PermissionDeleteEmojis *Permission +var PermissionDeleteOthersEmojis *Permission +var PermissionCreatePost *Permission +var PermissionCreatePostPublic *Permission +var PermissionCreatePostEphemeral *Permission +var PermissionEditPost *Permission +var PermissionEditOthersPosts *Permission +var PermissionDeletePost *Permission +var PermissionDeleteOthersPosts *Permission +var PermissionRemoveUserFromTeam *Permission +var PermissionCreateTeam *Permission +var PermissionManageTeam *Permission +var PermissionImportTeam *Permission +var PermissionViewTeam *Permission +var PermissionListUsersWithoutTeam *Permission +var PermissionReadJobs *Permission +var PermissionManageJobs *Permission +var PermissionCreateUserAccessToken *Permission +var PermissionReadUserAccessToken *Permission +var PermissionRevokeUserAccessToken *Permission +var PermissionCreateBot *Permission +var PermissionAssignBot *Permission +var PermissionReadBots *Permission +var PermissionReadOthersBots *Permission +var PermissionManageBots *Permission +var PermissionManageOthersBots *Permission +var PermissionViewMembers *Permission +var PermissionInviteGuest *Permission +var PermissionPromoteGuest *Permission +var PermissionDemoteToGuest *Permission +var PermissionUseChannelMentions *Permission +var PermissionUseGroupMentions *Permission +var PermissionReadOtherUsersTeams *Permission +var PermissionEditBrand *Permission +var PermissionManageSharedChannels *Permission +var PermissionManageSecureConnections *Permission +var PermissionDownloadComplianceExportResult *Permission +var PermissionCreateDataRetentionJob *Permission +var PermissionReadDataRetentionJob *Permission +var PermissionCreateComplianceExportJob *Permission +var PermissionReadComplianceExportJob *Permission +var PermissionReadAudits *Permission +var PermissionTestElasticsearch *Permission +var PermissionTestSiteURL *Permission +var PermissionTestS3 *Permission +var PermissionReloadConfig *Permission +var PermissionInvalidateCaches *Permission +var PermissionRecycleDatabaseConnections *Permission +var PermissionPurgeElasticsearchIndexes *Permission +var PermissionTestEmail *Permission +var PermissionCreateElasticsearchPostIndexingJob *Permission +var PermissionCreateElasticsearchPostAggregationJob *Permission +var PermissionReadElasticsearchPostIndexingJob *Permission +var PermissionReadElasticsearchPostAggregationJob *Permission +var PermissionPurgeBleveIndexes *Permission +var PermissionCreatePostBleveIndexesJob *Permission +var PermissionCreateLdapSyncJob *Permission +var PermissionReadLdapSyncJob *Permission +var PermissionTestLdap *Permission +var PermissionInvalidateEmailInvite *Permission +var PermissionGetSamlMetadataFromIdp *Permission +var PermissionAddSamlPublicCert *Permission +var PermissionAddSamlPrivateCert *Permission +var PermissionAddSamlIdpCert *Permission +var PermissionRemoveSamlPublicCert *Permission +var PermissionRemoveSamlPrivateCert *Permission +var PermissionRemoveSamlIdpCert *Permission +var PermissionGetSamlCertStatus *Permission +var PermissionAddLdapPublicCert *Permission +var PermissionAddLdapPrivateCert *Permission +var PermissionRemoveLdapPublicCert *Permission +var PermissionRemoveLdapPrivateCert *Permission +var PermissionGetLogs *Permission +var PermissionGetAnalytics *Permission +var PermissionReadLicenseInformation *Permission +var PermissionManageLicenseInformation *Permission + +var PermissionSysconsoleReadAbout *Permission +var PermissionSysconsoleWriteAbout *Permission + +var PermissionSysconsoleReadAboutEditionAndLicense *Permission +var PermissionSysconsoleWriteAboutEditionAndLicense *Permission + +var PermissionSysconsoleReadBilling *Permission +var PermissionSysconsoleWriteBilling *Permission + +var PermissionSysconsoleReadReporting *Permission +var PermissionSysconsoleWriteReporting *Permission + +var PermissionSysconsoleReadReportingSiteStatistics *Permission +var PermissionSysconsoleWriteReportingSiteStatistics *Permission + +var PermissionSysconsoleReadReportingTeamStatistics *Permission +var PermissionSysconsoleWriteReportingTeamStatistics *Permission + +var PermissionSysconsoleReadReportingServerLogs *Permission +var PermissionSysconsoleWriteReportingServerLogs *Permission + +var PermissionSysconsoleReadUserManagementUsers *Permission +var PermissionSysconsoleWriteUserManagementUsers *Permission + +var PermissionSysconsoleReadUserManagementGroups *Permission +var PermissionSysconsoleWriteUserManagementGroups *Permission + +var PermissionSysconsoleReadUserManagementTeams *Permission +var PermissionSysconsoleWriteUserManagementTeams *Permission + +var PermissionSysconsoleReadUserManagementChannels *Permission +var PermissionSysconsoleWriteUserManagementChannels *Permission + +var PermissionSysconsoleReadUserManagementPermissions *Permission +var PermissionSysconsoleWriteUserManagementPermissions *Permission + +var PermissionSysconsoleReadUserManagementSystemRoles *Permission +var PermissionSysconsoleWriteUserManagementSystemRoles *Permission + +// DEPRECATED +var PermissionSysconsoleReadEnvironment *Permission + +// DEPRECATED +var PermissionSysconsoleWriteEnvironment *Permission + +var PermissionSysconsoleReadEnvironmentWebServer *Permission +var PermissionSysconsoleWriteEnvironmentWebServer *Permission + +var PermissionSysconsoleReadEnvironmentDatabase *Permission +var PermissionSysconsoleWriteEnvironmentDatabase *Permission + +var PermissionSysconsoleReadEnvironmentElasticsearch *Permission +var PermissionSysconsoleWriteEnvironmentElasticsearch *Permission + +var PermissionSysconsoleReadEnvironmentFileStorage *Permission +var PermissionSysconsoleWriteEnvironmentFileStorage *Permission + +var PermissionSysconsoleReadEnvironmentImageProxy *Permission +var PermissionSysconsoleWriteEnvironmentImageProxy *Permission + +var PermissionSysconsoleReadEnvironmentSMTP *Permission +var PermissionSysconsoleWriteEnvironmentSMTP *Permission + +var PermissionSysconsoleReadEnvironmentPushNotificationServer *Permission +var PermissionSysconsoleWriteEnvironmentPushNotificationServer *Permission + +var PermissionSysconsoleReadEnvironmentHighAvailability *Permission +var PermissionSysconsoleWriteEnvironmentHighAvailability *Permission + +var PermissionSysconsoleReadEnvironmentRateLimiting *Permission +var PermissionSysconsoleWriteEnvironmentRateLimiting *Permission + +var PermissionSysconsoleReadEnvironmentLogging *Permission +var PermissionSysconsoleWriteEnvironmentLogging *Permission + +var PermissionSysconsoleReadEnvironmentSessionLengths *Permission +var PermissionSysconsoleWriteEnvironmentSessionLengths *Permission + +var PermissionSysconsoleReadEnvironmentPerformanceMonitoring *Permission +var PermissionSysconsoleWriteEnvironmentPerformanceMonitoring *Permission + +var PermissionSysconsoleReadEnvironmentDeveloper *Permission +var PermissionSysconsoleWriteEnvironmentDeveloper *Permission + +var PermissionSysconsoleReadSite *Permission +var PermissionSysconsoleWriteSite *Permission + +var PermissionSysconsoleReadSiteCustomization *Permission +var PermissionSysconsoleWriteSiteCustomization *Permission + +var PermissionSysconsoleReadSiteLocalization *Permission +var PermissionSysconsoleWriteSiteLocalization *Permission + +var PermissionSysconsoleReadSiteUsersAndTeams *Permission +var PermissionSysconsoleWriteSiteUsersAndTeams *Permission + +var PermissionSysconsoleReadSiteNotifications *Permission +var PermissionSysconsoleWriteSiteNotifications *Permission + +var PermissionSysconsoleReadSiteAnnouncementBanner *Permission +var PermissionSysconsoleWriteSiteAnnouncementBanner *Permission + +var PermissionSysconsoleReadSiteEmoji *Permission +var PermissionSysconsoleWriteSiteEmoji *Permission + +var PermissionSysconsoleReadSitePosts *Permission +var PermissionSysconsoleWriteSitePosts *Permission + +var PermissionSysconsoleReadSiteFileSharingAndDownloads *Permission +var PermissionSysconsoleWriteSiteFileSharingAndDownloads *Permission + +var PermissionSysconsoleReadSitePublicLinks *Permission +var PermissionSysconsoleWriteSitePublicLinks *Permission + +var PermissionSysconsoleReadSiteNotices *Permission +var PermissionSysconsoleWriteSiteNotices *Permission + +var PermissionSysconsoleReadAuthentication *Permission +var PermissionSysconsoleWriteAuthentication *Permission + +var PermissionSysconsoleReadAuthenticationSignup *Permission +var PermissionSysconsoleWriteAuthenticationSignup *Permission + +var PermissionSysconsoleReadAuthenticationEmail *Permission +var PermissionSysconsoleWriteAuthenticationEmail *Permission + +var PermissionSysconsoleReadAuthenticationPassword *Permission +var PermissionSysconsoleWriteAuthenticationPassword *Permission + +var PermissionSysconsoleReadAuthenticationMfa *Permission +var PermissionSysconsoleWriteAuthenticationMfa *Permission + +var PermissionSysconsoleReadAuthenticationLdap *Permission +var PermissionSysconsoleWriteAuthenticationLdap *Permission + +var PermissionSysconsoleReadAuthenticationSaml *Permission +var PermissionSysconsoleWriteAuthenticationSaml *Permission + +var PermissionSysconsoleReadAuthenticationOpenid *Permission +var PermissionSysconsoleWriteAuthenticationOpenid *Permission + +var PermissionSysconsoleReadAuthenticationGuestAccess *Permission +var PermissionSysconsoleWriteAuthenticationGuestAccess *Permission + +var PermissionSysconsoleReadPlugins *Permission +var PermissionSysconsoleWritePlugins *Permission + +var PermissionSysconsoleReadIntegrations *Permission +var PermissionSysconsoleWriteIntegrations *Permission + +var PermissionSysconsoleReadIntegrationsIntegrationManagement *Permission +var PermissionSysconsoleWriteIntegrationsIntegrationManagement *Permission + +var PermissionSysconsoleReadIntegrationsBotAccounts *Permission +var PermissionSysconsoleWriteIntegrationsBotAccounts *Permission + +var PermissionSysconsoleReadIntegrationsGif *Permission +var PermissionSysconsoleWriteIntegrationsGif *Permission + +var PermissionSysconsoleReadIntegrationsCors *Permission +var PermissionSysconsoleWriteIntegrationsCors *Permission + +var PermissionSysconsoleReadCompliance *Permission +var PermissionSysconsoleWriteCompliance *Permission + +var PermissionSysconsoleReadComplianceDataRetentionPolicy *Permission +var PermissionSysconsoleWriteComplianceDataRetentionPolicy *Permission + +var PermissionSysconsoleReadComplianceComplianceExport *Permission +var PermissionSysconsoleWriteComplianceComplianceExport *Permission + +var PermissionSysconsoleReadComplianceComplianceMonitoring *Permission +var PermissionSysconsoleWriteComplianceComplianceMonitoring *Permission + +var PermissionSysconsoleReadComplianceCustomTermsOfService *Permission +var PermissionSysconsoleWriteComplianceCustomTermsOfService *Permission + +var PermissionSysconsoleReadExperimental *Permission +var PermissionSysconsoleWriteExperimental *Permission + +var PermissionSysconsoleReadExperimentalFeatures *Permission +var PermissionSysconsoleWriteExperimentalFeatures *Permission + +var PermissionSysconsoleReadExperimentalFeatureFlags *Permission +var PermissionSysconsoleWriteExperimentalFeatureFlags *Permission + +var PermissionSysconsoleReadExperimentalBleve *Permission +var PermissionSysconsoleWriteExperimentalBleve *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 PermissionManageSystem *Permission + +var AllPermissions []*Permission +var DeprecatedPermissions []*Permission + +var ChannelModeratedPermissions []string +var ChannelModeratedPermissionsMap map[string]string + +var SysconsoleReadPermissions []*Permission +var SysconsoleWritePermissions []*Permission + +func initializePermissions() { + PermissionInviteUser = &Permission{ + "invite_user", + "authentication.permissions.team_invite_user.name", + "authentication.permissions.team_invite_user.description", + PermissionScopeTeam, + } + PermissionAddUserToTeam = &Permission{ + "add_user_to_team", + "authentication.permissions.add_user_to_team.name", + "authentication.permissions.add_user_to_team.description", + PermissionScopeTeam, + } + PermissionUseSlashCommands = &Permission{ + "use_slash_commands", + "authentication.permissions.team_use_slash_commands.name", + "authentication.permissions.team_use_slash_commands.description", + PermissionScopeChannel, + } + PermissionManageSlashCommands = &Permission{ + "manage_slash_commands", + "authentication.permissions.manage_slash_commands.name", + "authentication.permissions.manage_slash_commands.description", + PermissionScopeTeam, + } + PermissionManageOthersSlashCommands = &Permission{ + "manage_others_slash_commands", + "authentication.permissions.manage_others_slash_commands.name", + "authentication.permissions.manage_others_slash_commands.description", + PermissionScopeTeam, + } + PermissionCreatePublicChannel = &Permission{ + "create_public_channel", + "authentication.permissions.create_public_channel.name", + "authentication.permissions.create_public_channel.description", + PermissionScopeTeam, + } + PermissionCreatePrivateChannel = &Permission{ + "create_private_channel", + "authentication.permissions.create_private_channel.name", + "authentication.permissions.create_private_channel.description", + PermissionScopeTeam, + } + PermissionManagePublicChannelMembers = &Permission{ + "manage_public_channel_members", + "authentication.permissions.manage_public_channel_members.name", + "authentication.permissions.manage_public_channel_members.description", + PermissionScopeChannel, + } + PermissionManagePrivateChannelMembers = &Permission{ + "manage_private_channel_members", + "authentication.permissions.manage_private_channel_members.name", + "authentication.permissions.manage_private_channel_members.description", + PermissionScopeChannel, + } + PermissionConvertPublicChannelToPrivate = &Permission{ + "convert_public_channel_to_private", + "authentication.permissions.convert_public_channel_to_private.name", + "authentication.permissions.convert_public_channel_to_private.description", + PermissionScopeChannel, + } + PermissionConvertPrivateChannelToPublic = &Permission{ + "convert_private_channel_to_public", + "authentication.permissions.convert_private_channel_to_public.name", + "authentication.permissions.convert_private_channel_to_public.description", + PermissionScopeChannel, + } + PermissionAssignSystemAdminRole = &Permission{ + "assign_system_admin_role", + "authentication.permissions.assign_system_admin_role.name", + "authentication.permissions.assign_system_admin_role.description", + PermissionScopeSystem, + } + PermissionManageRoles = &Permission{ + "manage_roles", + "authentication.permissions.manage_roles.name", + "authentication.permissions.manage_roles.description", + PermissionScopeSystem, + } + PermissionManageTeamRoles = &Permission{ + "manage_team_roles", + "authentication.permissions.manage_team_roles.name", + "authentication.permissions.manage_team_roles.description", + PermissionScopeTeam, + } + PermissionManageChannelRoles = &Permission{ + "manage_channel_roles", + "authentication.permissions.manage_channel_roles.name", + "authentication.permissions.manage_channel_roles.description", + PermissionScopeChannel, + } + PermissionManageSystem = &Permission{ + "manage_system", + "authentication.permissions.manage_system.name", + "authentication.permissions.manage_system.description", + PermissionScopeSystem, + } + PermissionCreateDirectChannel = &Permission{ + "create_direct_channel", + "authentication.permissions.create_direct_channel.name", + "authentication.permissions.create_direct_channel.description", + PermissionScopeSystem, + } + PermissionCreateGroupChannel = &Permission{ + "create_group_channel", + "authentication.permissions.create_group_channel.name", + "authentication.permissions.create_group_channel.description", + PermissionScopeSystem, + } + PermissionManagePublicChannelProperties = &Permission{ + "manage_public_channel_properties", + "authentication.permissions.manage_public_channel_properties.name", + "authentication.permissions.manage_public_channel_properties.description", + PermissionScopeChannel, + } + PermissionManagePrivateChannelProperties = &Permission{ + "manage_private_channel_properties", + "authentication.permissions.manage_private_channel_properties.name", + "authentication.permissions.manage_private_channel_properties.description", + PermissionScopeChannel, + } + PermissionListPublicTeams = &Permission{ + "list_public_teams", + "authentication.permissions.list_public_teams.name", + "authentication.permissions.list_public_teams.description", + PermissionScopeSystem, + } + PermissionJoinPublicTeams = &Permission{ + "join_public_teams", + "authentication.permissions.join_public_teams.name", + "authentication.permissions.join_public_teams.description", + PermissionScopeSystem, + } + PermissionListPrivateTeams = &Permission{ + "list_private_teams", + "authentication.permissions.list_private_teams.name", + "authentication.permissions.list_private_teams.description", + PermissionScopeSystem, + } + PermissionJoinPrivateTeams = &Permission{ + "join_private_teams", + "authentication.permissions.join_private_teams.name", + "authentication.permissions.join_private_teams.description", + PermissionScopeSystem, + } + PermissionListTeamChannels = &Permission{ + "list_team_channels", + "authentication.permissions.list_team_channels.name", + "authentication.permissions.list_team_channels.description", + PermissionScopeTeam, + } + PermissionJoinPublicChannels = &Permission{ + "join_public_channels", + "authentication.permissions.join_public_channels.name", + "authentication.permissions.join_public_channels.description", + PermissionScopeTeam, + } + PermissionDeletePublicChannel = &Permission{ + "delete_public_channel", + "authentication.permissions.delete_public_channel.name", + "authentication.permissions.delete_public_channel.description", + PermissionScopeChannel, + } + PermissionDeletePrivateChannel = &Permission{ + "delete_private_channel", + "authentication.permissions.delete_private_channel.name", + "authentication.permissions.delete_private_channel.description", + PermissionScopeChannel, + } + PermissionEditOtherUsers = &Permission{ + "edit_other_users", + "authentication.permissions.edit_other_users.name", + "authentication.permissions.edit_other_users.description", + PermissionScopeSystem, + } + PermissionReadChannel = &Permission{ + "read_channel", + "authentication.permissions.read_channel.name", + "authentication.permissions.read_channel.description", + PermissionScopeChannel, + } + PermissionReadPublicChannelGroups = &Permission{ + "read_public_channel_groups", + "authentication.permissions.read_public_channel_groups.name", + "authentication.permissions.read_public_channel_groups.description", + PermissionScopeChannel, + } + PermissionReadPrivateChannelGroups = &Permission{ + "read_private_channel_groups", + "authentication.permissions.read_private_channel_groups.name", + "authentication.permissions.read_private_channel_groups.description", + PermissionScopeChannel, + } + PermissionReadPublicChannel = &Permission{ + "read_public_channel", + "authentication.permissions.read_public_channel.name", + "authentication.permissions.read_public_channel.description", + PermissionScopeTeam, + } + PermissionAddReaction = &Permission{ + "add_reaction", + "authentication.permissions.add_reaction.name", + "authentication.permissions.add_reaction.description", + PermissionScopeChannel, + } + PermissionRemoveReaction = &Permission{ + "remove_reaction", + "authentication.permissions.remove_reaction.name", + "authentication.permissions.remove_reaction.description", + PermissionScopeChannel, + } + PermissionRemoveOthersReactions = &Permission{ + "remove_others_reactions", + "authentication.permissions.remove_others_reactions.name", + "authentication.permissions.remove_others_reactions.description", + PermissionScopeChannel, + } + // DEPRECATED + PermissionPermanentDeleteUser = &Permission{ + "permanent_delete_user", + "authentication.permissions.permanent_delete_user.name", + "authentication.permissions.permanent_delete_user.description", + PermissionScopeSystem, + } + PermissionUploadFile = &Permission{ + "upload_file", + "authentication.permissions.upload_file.name", + "authentication.permissions.upload_file.description", + PermissionScopeChannel, + } + PermissionGetPublicLink = &Permission{ + "get_public_link", + "authentication.permissions.get_public_link.name", + "authentication.permissions.get_public_link.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionManageWebhooks = &Permission{ + "manage_webhooks", + "authentication.permissions.manage_webhooks.name", + "authentication.permissions.manage_webhooks.description", + PermissionScopeTeam, + } + // DEPRECATED + PermissionManageOthersWebhooks = &Permission{ + "manage_others_webhooks", + "authentication.permissions.manage_others_webhooks.name", + "authentication.permissions.manage_others_webhooks.description", + PermissionScopeTeam, + } + PermissionManageIncomingWebhooks = &Permission{ + "manage_incoming_webhooks", + "authentication.permissions.manage_incoming_webhooks.name", + "authentication.permissions.manage_incoming_webhooks.description", + PermissionScopeTeam, + } + PermissionManageOutgoingWebhooks = &Permission{ + "manage_outgoing_webhooks", + "authentication.permissions.manage_outgoing_webhooks.name", + "authentication.permissions.manage_outgoing_webhooks.description", + PermissionScopeTeam, + } + PermissionManageOthersIncomingWebhooks = &Permission{ + "manage_others_incoming_webhooks", + "authentication.permissions.manage_others_incoming_webhooks.name", + "authentication.permissions.manage_others_incoming_webhooks.description", + PermissionScopeTeam, + } + PermissionManageOthersOutgoingWebhooks = &Permission{ + "manage_others_outgoing_webhooks", + "authentication.permissions.manage_others_outgoing_webhooks.name", + "authentication.permissions.manage_others_outgoing_webhooks.description", + PermissionScopeTeam, + } + PermissionManageOAuth = &Permission{ + "manage_oauth", + "authentication.permissions.manage_oauth.name", + "authentication.permissions.manage_oauth.description", + PermissionScopeSystem, + } + PermissionManageSystemWideOAuth = &Permission{ + "manage_system_wide_oauth", + "authentication.permissions.manage_system_wide_oauth.name", + "authentication.permissions.manage_system_wide_oauth.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionManageEmojis = &Permission{ + "manage_emojis", + "authentication.permissions.manage_emojis.name", + "authentication.permissions.manage_emojis.description", + PermissionScopeTeam, + } + // DEPRECATED + PermissionManageOthersEmojis = &Permission{ + "manage_others_emojis", + "authentication.permissions.manage_others_emojis.name", + "authentication.permissions.manage_others_emojis.description", + PermissionScopeTeam, + } + PermissionCreateEmojis = &Permission{ + "create_emojis", + "authentication.permissions.create_emojis.name", + "authentication.permissions.create_emojis.description", + PermissionScopeTeam, + } + PermissionDeleteEmojis = &Permission{ + "delete_emojis", + "authentication.permissions.delete_emojis.name", + "authentication.permissions.delete_emojis.description", + PermissionScopeTeam, + } + PermissionDeleteOthersEmojis = &Permission{ + "delete_others_emojis", + "authentication.permissions.delete_others_emojis.name", + "authentication.permissions.delete_others_emojis.description", + PermissionScopeTeam, + } + PermissionCreatePost = &Permission{ + "create_post", + "authentication.permissions.create_post.name", + "authentication.permissions.create_post.description", + PermissionScopeChannel, + } + PermissionCreatePostPublic = &Permission{ + "create_post_public", + "authentication.permissions.create_post_public.name", + "authentication.permissions.create_post_public.description", + PermissionScopeChannel, + } + PermissionCreatePostEphemeral = &Permission{ + "create_post_ephemeral", + "authentication.permissions.create_post_ephemeral.name", + "authentication.permissions.create_post_ephemeral.description", + PermissionScopeChannel, + } + PermissionEditPost = &Permission{ + "edit_post", + "authentication.permissions.edit_post.name", + "authentication.permissions.edit_post.description", + PermissionScopeChannel, + } + PermissionEditOthersPosts = &Permission{ + "edit_others_posts", + "authentication.permissions.edit_others_posts.name", + "authentication.permissions.edit_others_posts.description", + PermissionScopeChannel, + } + PermissionDeletePost = &Permission{ + "delete_post", + "authentication.permissions.delete_post.name", + "authentication.permissions.delete_post.description", + PermissionScopeChannel, + } + PermissionDeleteOthersPosts = &Permission{ + "delete_others_posts", + "authentication.permissions.delete_others_posts.name", + "authentication.permissions.delete_others_posts.description", + PermissionScopeChannel, + } + PermissionManageSharedChannels = &Permission{ + "manage_shared_channels", + "authentication.permissions.manage_shared_channels.name", + "authentication.permissions.manage_shared_channels.description", + PermissionScopeSystem, + } + PermissionManageSecureConnections = &Permission{ + "manage_secure_connections", + "authentication.permissions.manage_secure_connections.name", + "authentication.permissions.manage_secure_connections.description", + PermissionScopeSystem, + } + + PermissionCreateDataRetentionJob = &Permission{ + "create_data_retention_job", + "", + "", + PermissionScopeSystem, + } + PermissionReadDataRetentionJob = &Permission{ + "read_data_retention_job", + "", + "", + PermissionScopeSystem, + } + + PermissionCreateComplianceExportJob = &Permission{ + "create_compliance_export_job", + "", + "", + PermissionScopeSystem, + } + PermissionReadComplianceExportJob = &Permission{ + "read_compliance_export_job", + "", + "", + PermissionScopeSystem, + } + + PermissionReadAudits = &Permission{ + "read_audits", + "", + "", + PermissionScopeSystem, + } + + PermissionPurgeBleveIndexes = &Permission{ + "purge_bleve_indexes", + "", + "", + PermissionScopeSystem, + } + + PermissionCreatePostBleveIndexesJob = &Permission{ + "create_post_bleve_indexes_job", + "", + "", + PermissionScopeSystem, + } + + PermissionCreateLdapSyncJob = &Permission{ + "create_ldap_sync_job", + "", + "", + PermissionScopeSystem, + } + PermissionReadLdapSyncJob = &Permission{ + "read_ldap_sync_job", + "", + "", + PermissionScopeSystem, + } + + PermissionTestLdap = &Permission{ + "test_ldap", + "", + "", + PermissionScopeSystem, + } + + PermissionInvalidateEmailInvite = &Permission{ + "invalidate_email_invite", + "", + "", + PermissionScopeSystem, + } + PermissionGetSamlMetadataFromIdp = &Permission{ + "get_saml_metadata_from_idp", + "", + "", + PermissionScopeSystem, + } + PermissionAddSamlPublicCert = &Permission{ + "add_saml_public_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionAddSamlPrivateCert = &Permission{ + "add_saml_private_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionAddSamlIdpCert = &Permission{ + "add_saml_idp_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionRemoveSamlPublicCert = &Permission{ + "remove_saml_public_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionRemoveSamlPrivateCert = &Permission{ + "remove_saml_private_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionRemoveSamlIdpCert = &Permission{ + "remove_saml_idp_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionGetSamlCertStatus = &Permission{ + "get_saml_cert_status", + "", + "", + PermissionScopeSystem, + } + + PermissionAddLdapPublicCert = &Permission{ + "add_ldap_public_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionAddLdapPrivateCert = &Permission{ + "add_ldap_private_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionRemoveLdapPublicCert = &Permission{ + "remove_ldap_public_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionRemoveLdapPrivateCert = &Permission{ + "remove_ldap_private_cert", + "", + "", + PermissionScopeSystem, + } + + PermissionGetLogs = &Permission{ + "get_logs", + "", + "", + PermissionScopeSystem, + } + + PermissionReadLicenseInformation = &Permission{ + "read_license_information", + "", + "", + PermissionScopeSystem, + } + + PermissionGetAnalytics = &Permission{ + "get_analytics", + "", + "", + PermissionScopeSystem, + } + + PermissionManageLicenseInformation = &Permission{ + "manage_license_information", + "", + "", + PermissionScopeSystem, + } + + PermissionDownloadComplianceExportResult = &Permission{ + "download_compliance_export_result", + "authentication.permissions.download_compliance_export_result.name", + "authentication.permissions.download_compliance_export_result.description", + PermissionScopeSystem, + } + + PermissionTestSiteURL = &Permission{ + "test_site_url", + "", + "", + PermissionScopeSystem, + } + PermissionTestElasticsearch = &Permission{ + "test_elasticsearch", + "", + "", + PermissionScopeSystem, + } + PermissionTestS3 = &Permission{ + "test_s3", + "", + "", + PermissionScopeSystem, + } + PermissionReloadConfig = &Permission{ + "reload_config", + "", + "", + PermissionScopeSystem, + } + PermissionInvalidateCaches = &Permission{ + "invalidate_caches", + "", + "", + PermissionScopeSystem, + } + PermissionRecycleDatabaseConnections = &Permission{ + "recycle_database_connections", + "", + "", + PermissionScopeSystem, + } + PermissionPurgeElasticsearchIndexes = &Permission{ + "purge_elasticsearch_indexes", + "", + "", + PermissionScopeSystem, + } + PermissionTestEmail = &Permission{ + "test_email", + "", + "", + PermissionScopeSystem, + } + PermissionCreateElasticsearchPostIndexingJob = &Permission{ + "create_elasticsearch_post_indexing_job", + "", + "", + PermissionScopeSystem, + } + PermissionCreateElasticsearchPostAggregationJob = &Permission{ + "create_elasticsearch_post_aggregation_job", + "", + "", + PermissionScopeSystem, + } + PermissionReadElasticsearchPostIndexingJob = &Permission{ + "read_elasticsearch_post_indexing_job", + "", + "", + PermissionScopeSystem, + } + PermissionReadElasticsearchPostAggregationJob = &Permission{ + "read_elasticsearch_post_aggregation_job", + "", + "", + PermissionScopeSystem, + } + + PermissionRemoveUserFromTeam = &Permission{ + "remove_user_from_team", + "authentication.permissions.remove_user_from_team.name", + "authentication.permissions.remove_user_from_team.description", + PermissionScopeTeam, + } + PermissionCreateTeam = &Permission{ + "create_team", + "authentication.permissions.create_team.name", + "authentication.permissions.create_team.description", + PermissionScopeSystem, + } + PermissionManageTeam = &Permission{ + "manage_team", + "authentication.permissions.manage_team.name", + "authentication.permissions.manage_team.description", + PermissionScopeTeam, + } + PermissionImportTeam = &Permission{ + "import_team", + "authentication.permissions.import_team.name", + "authentication.permissions.import_team.description", + PermissionScopeTeam, + } + PermissionViewTeam = &Permission{ + "view_team", + "authentication.permissions.view_team.name", + "authentication.permissions.view_team.description", + PermissionScopeTeam, + } + PermissionListUsersWithoutTeam = &Permission{ + "list_users_without_team", + "authentication.permissions.list_users_without_team.name", + "authentication.permissions.list_users_without_team.description", + PermissionScopeSystem, + } + PermissionCreateUserAccessToken = &Permission{ + "create_user_access_token", + "authentication.permissions.create_user_access_token.name", + "authentication.permissions.create_user_access_token.description", + PermissionScopeSystem, + } + PermissionReadUserAccessToken = &Permission{ + "read_user_access_token", + "authentication.permissions.read_user_access_token.name", + "authentication.permissions.read_user_access_token.description", + PermissionScopeSystem, + } + PermissionRevokeUserAccessToken = &Permission{ + "revoke_user_access_token", + "authentication.permissions.revoke_user_access_token.name", + "authentication.permissions.revoke_user_access_token.description", + PermissionScopeSystem, + } + PermissionCreateBot = &Permission{ + "create_bot", + "authentication.permissions.create_bot.name", + "authentication.permissions.create_bot.description", + PermissionScopeSystem, + } + PermissionAssignBot = &Permission{ + "assign_bot", + "authentication.permissions.assign_bot.name", + "authentication.permissions.assign_bot.description", + PermissionScopeSystem, + } + PermissionReadBots = &Permission{ + "read_bots", + "authentication.permissions.read_bots.name", + "authentication.permissions.read_bots.description", + PermissionScopeSystem, + } + PermissionReadOthersBots = &Permission{ + "read_others_bots", + "authentication.permissions.read_others_bots.name", + "authentication.permissions.read_others_bots.description", + PermissionScopeSystem, + } + PermissionManageBots = &Permission{ + "manage_bots", + "authentication.permissions.manage_bots.name", + "authentication.permissions.manage_bots.description", + PermissionScopeSystem, + } + PermissionManageOthersBots = &Permission{ + "manage_others_bots", + "authentication.permissions.manage_others_bots.name", + "authentication.permissions.manage_others_bots.description", + PermissionScopeSystem, + } + PermissionReadJobs = &Permission{ + "read_jobs", + "authentication.permisssions.read_jobs.name", + "authentication.permisssions.read_jobs.description", + PermissionScopeSystem, + } + PermissionManageJobs = &Permission{ + "manage_jobs", + "authentication.permisssions.manage_jobs.name", + "authentication.permisssions.manage_jobs.description", + PermissionScopeSystem, + } + PermissionViewMembers = &Permission{ + "view_members", + "authentication.permisssions.view_members.name", + "authentication.permisssions.view_members.description", + PermissionScopeTeam, + } + PermissionInviteGuest = &Permission{ + "invite_guest", + "authentication.permissions.invite_guest.name", + "authentication.permissions.invite_guest.description", + PermissionScopeTeam, + } + PermissionPromoteGuest = &Permission{ + "promote_guest", + "authentication.permissions.promote_guest.name", + "authentication.permissions.promote_guest.description", + PermissionScopeSystem, + } + PermissionDemoteToGuest = &Permission{ + "demote_to_guest", + "authentication.permissions.demote_to_guest.name", + "authentication.permissions.demote_to_guest.description", + PermissionScopeSystem, + } + PermissionUseChannelMentions = &Permission{ + "use_channel_mentions", + "authentication.permissions.use_channel_mentions.name", + "authentication.permissions.use_channel_mentions.description", + PermissionScopeChannel, + } + PermissionUseGroupMentions = &Permission{ + "use_group_mentions", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeChannel, + } + PermissionReadOtherUsersTeams = &Permission{ + "read_other_users_teams", + "authentication.permissions.read_other_users_teams.name", + "authentication.permissions.read_other_users_teams.description", + PermissionScopeSystem, + } + PermissionEditBrand = &Permission{ + "edit_brand", + "authentication.permissions.edit_brand.name", + "authentication.permissions.edit_brand.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleReadAbout = &Permission{ + "sysconsole_read_about", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleWriteAbout = &Permission{ + "sysconsole_write_about", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadAboutEditionAndLicense = &Permission{ + "sysconsole_read_about_edition_and_license", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteAboutEditionAndLicense = &Permission{ + "sysconsole_write_about_edition_and_license", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadBilling = &Permission{ + "sysconsole_read_billing", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteBilling = &Permission{ + "sysconsole_write_billing", + "", + "", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleReadReporting = &Permission{ + "sysconsole_read_reporting", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleWriteReporting = &Permission{ + "sysconsole_write_reporting", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadReportingSiteStatistics = &Permission{ + "sysconsole_read_reporting_site_statistics", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteReportingSiteStatistics = &Permission{ + "sysconsole_write_reporting_site_statistics", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadReportingTeamStatistics = &Permission{ + "sysconsole_read_reporting_team_statistics", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteReportingTeamStatistics = &Permission{ + "sysconsole_write_reporting_team_statistics", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadReportingServerLogs = &Permission{ + "sysconsole_read_reporting_server_logs", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteReportingServerLogs = &Permission{ + "sysconsole_write_reporting_server_logs", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadUserManagementUsers = &Permission{ + "sysconsole_read_user_management_users", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleWriteUserManagementUsers = &Permission{ + "sysconsole_write_user_management_users", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadUserManagementGroups = &Permission{ + "sysconsole_read_user_management_groups", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleWriteUserManagementGroups = &Permission{ + "sysconsole_write_user_management_groups", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadUserManagementTeams = &Permission{ + "sysconsole_read_user_management_teams", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleWriteUserManagementTeams = &Permission{ + "sysconsole_write_user_management_teams", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadUserManagementChannels = &Permission{ + "sysconsole_read_user_management_channels", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleWriteUserManagementChannels = &Permission{ + "sysconsole_write_user_management_channels", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadUserManagementPermissions = &Permission{ + "sysconsole_read_user_management_permissions", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleWriteUserManagementPermissions = &Permission{ + "sysconsole_write_user_management_permissions", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadUserManagementSystemRoles = &Permission{ + "sysconsole_read_user_management_system_roles", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleWriteUserManagementSystemRoles = &Permission{ + "sysconsole_write_user_management_system_roles", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleReadEnvironment = &Permission{ + "sysconsole_read_environment", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleWriteEnvironment = &Permission{ + "sysconsole_write_environment", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentWebServer = &Permission{ + "sysconsole_read_environment_web_server", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentWebServer = &Permission{ + "sysconsole_write_environment_web_server", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentDatabase = &Permission{ + "sysconsole_read_environment_database", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentDatabase = &Permission{ + "sysconsole_write_environment_database", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentElasticsearch = &Permission{ + "sysconsole_read_environment_elasticsearch", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentElasticsearch = &Permission{ + "sysconsole_write_environment_elasticsearch", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentFileStorage = &Permission{ + "sysconsole_read_environment_file_storage", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentFileStorage = &Permission{ + "sysconsole_write_environment_file_storage", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentImageProxy = &Permission{ + "sysconsole_read_environment_image_proxy", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentImageProxy = &Permission{ + "sysconsole_write_environment_image_proxy", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentSMTP = &Permission{ + "sysconsole_read_environment_smtp", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentSMTP = &Permission{ + "sysconsole_write_environment_smtp", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentPushNotificationServer = &Permission{ + "sysconsole_read_environment_push_notification_server", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentPushNotificationServer = &Permission{ + "sysconsole_write_environment_push_notification_server", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentHighAvailability = &Permission{ + "sysconsole_read_environment_high_availability", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentHighAvailability = &Permission{ + "sysconsole_write_environment_high_availability", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentRateLimiting = &Permission{ + "sysconsole_read_environment_rate_limiting", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentRateLimiting = &Permission{ + "sysconsole_write_environment_rate_limiting", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentLogging = &Permission{ + "sysconsole_read_environment_logging", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentLogging = &Permission{ + "sysconsole_write_environment_logging", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentSessionLengths = &Permission{ + "sysconsole_read_environment_session_lengths", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentSessionLengths = &Permission{ + "sysconsole_write_environment_session_lengths", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentPerformanceMonitoring = &Permission{ + "sysconsole_read_environment_performance_monitoring", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentPerformanceMonitoring = &Permission{ + "sysconsole_write_environment_performance_monitoring", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadEnvironmentDeveloper = &Permission{ + "sysconsole_read_environment_developer", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteEnvironmentDeveloper = &Permission{ + "sysconsole_write_environment_developer", + "", + "", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleReadSite = &Permission{ + "sysconsole_read_site", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleWriteSite = &Permission{ + "sysconsole_write_site", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + + PermissionSysconsoleReadSiteCustomization = &Permission{ + "sysconsole_read_site_customization", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSiteCustomization = &Permission{ + "sysconsole_write_site_customization", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadSiteLocalization = &Permission{ + "sysconsole_read_site_localization", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSiteLocalization = &Permission{ + "sysconsole_write_site_localization", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadSiteUsersAndTeams = &Permission{ + "sysconsole_read_site_users_and_teams", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSiteUsersAndTeams = &Permission{ + "sysconsole_write_site_users_and_teams", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadSiteNotifications = &Permission{ + "sysconsole_read_site_notifications", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSiteNotifications = &Permission{ + "sysconsole_write_site_notifications", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadSiteAnnouncementBanner = &Permission{ + "sysconsole_read_site_announcement_banner", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSiteAnnouncementBanner = &Permission{ + "sysconsole_write_site_announcement_banner", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadSiteEmoji = &Permission{ + "sysconsole_read_site_emoji", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSiteEmoji = &Permission{ + "sysconsole_write_site_emoji", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadSitePosts = &Permission{ + "sysconsole_read_site_posts", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSitePosts = &Permission{ + "sysconsole_write_site_posts", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadSiteFileSharingAndDownloads = &Permission{ + "sysconsole_read_site_file_sharing_and_downloads", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSiteFileSharingAndDownloads = &Permission{ + "sysconsole_write_site_file_sharing_and_downloads", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadSitePublicLinks = &Permission{ + "sysconsole_read_site_public_links", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSitePublicLinks = &Permission{ + "sysconsole_write_site_public_links", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadSiteNotices = &Permission{ + "sysconsole_read_site_notices", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteSiteNotices = &Permission{ + "sysconsole_write_site_notices", + "", + "", + PermissionScopeSystem, + } + + // Deprecated + PermissionSysconsoleReadAuthentication = &Permission{ + "sysconsole_read_authentication", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // Deprecated + PermissionSysconsoleWriteAuthentication = &Permission{ + "sysconsole_write_authentication", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadAuthenticationSignup = &Permission{ + "sysconsole_read_authentication_signup", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteAuthenticationSignup = &Permission{ + "sysconsole_write_authentication_signup", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadAuthenticationEmail = &Permission{ + "sysconsole_read_authentication_email", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteAuthenticationEmail = &Permission{ + "sysconsole_write_authentication_email", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadAuthenticationPassword = &Permission{ + "sysconsole_read_authentication_password", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteAuthenticationPassword = &Permission{ + "sysconsole_write_authentication_password", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadAuthenticationMfa = &Permission{ + "sysconsole_read_authentication_mfa", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteAuthenticationMfa = &Permission{ + "sysconsole_write_authentication_mfa", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadAuthenticationLdap = &Permission{ + "sysconsole_read_authentication_ldap", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteAuthenticationLdap = &Permission{ + "sysconsole_write_authentication_ldap", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadAuthenticationSaml = &Permission{ + "sysconsole_read_authentication_saml", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteAuthenticationSaml = &Permission{ + "sysconsole_write_authentication_saml", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadAuthenticationOpenid = &Permission{ + "sysconsole_read_authentication_openid", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteAuthenticationOpenid = &Permission{ + "sysconsole_write_authentication_openid", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadAuthenticationGuestAccess = &Permission{ + "sysconsole_read_authentication_guest_access", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteAuthenticationGuestAccess = &Permission{ + "sysconsole_write_authentication_guest_access", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadPlugins = &Permission{ + "sysconsole_read_plugins", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleWritePlugins = &Permission{ + "sysconsole_write_plugins", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleReadIntegrations = &Permission{ + "sysconsole_read_integrations", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleWriteIntegrations = &Permission{ + "sysconsole_write_integrations", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadIntegrationsIntegrationManagement = &Permission{ + "sysconsole_read_integrations_integration_management", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteIntegrationsIntegrationManagement = &Permission{ + "sysconsole_write_integrations_integration_management", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadIntegrationsBotAccounts = &Permission{ + "sysconsole_read_integrations_bot_accounts", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteIntegrationsBotAccounts = &Permission{ + "sysconsole_write_integrations_bot_accounts", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadIntegrationsGif = &Permission{ + "sysconsole_read_integrations_gif", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteIntegrationsGif = &Permission{ + "sysconsole_write_integrations_gif", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadIntegrationsCors = &Permission{ + "sysconsole_read_integrations_cors", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteIntegrationsCors = &Permission{ + "sysconsole_write_integrations_cors", + "", + "", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleReadCompliance = &Permission{ + "sysconsole_read_compliance", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleWriteCompliance = &Permission{ + "sysconsole_write_compliance", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadComplianceDataRetentionPolicy = &Permission{ + "sysconsole_read_compliance_data_retention_policy", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteComplianceDataRetentionPolicy = &Permission{ + "sysconsole_write_compliance_data_retention_policy", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadComplianceComplianceExport = &Permission{ + "sysconsole_read_compliance_compliance_export", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteComplianceComplianceExport = &Permission{ + "sysconsole_write_compliance_compliance_export", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadComplianceComplianceMonitoring = &Permission{ + "sysconsole_read_compliance_compliance_monitoring", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteComplianceComplianceMonitoring = &Permission{ + "sysconsole_write_compliance_compliance_monitoring", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadComplianceCustomTermsOfService = &Permission{ + "sysconsole_read_compliance_custom_terms_of_service", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteComplianceCustomTermsOfService = &Permission{ + "sysconsole_write_compliance_custom_terms_of_service", + "", + "", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleReadExperimental = &Permission{ + "sysconsole_read_experimental", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + // DEPRECATED + PermissionSysconsoleWriteExperimental = &Permission{ + "sysconsole_write_experimental", + "authentication.permissions.use_group_mentions.name", + "authentication.permissions.use_group_mentions.description", + PermissionScopeSystem, + } + PermissionSysconsoleReadExperimentalFeatures = &Permission{ + "sysconsole_read_experimental_features", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteExperimentalFeatures = &Permission{ + "sysconsole_write_experimental_features", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadExperimentalFeatureFlags = &Permission{ + "sysconsole_read_experimental_feature_flags", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteExperimentalFeatureFlags = &Permission{ + "sysconsole_write_experimental_feature_flags", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleReadExperimentalBleve = &Permission{ + "sysconsole_read_experimental_bleve", + "", + "", + PermissionScopeSystem, + } + PermissionSysconsoleWriteExperimentalBleve = &Permission{ + "sysconsole_write_experimental_bleve", + "", + "", + PermissionScopeSystem, + } + + SysconsoleReadPermissions = []*Permission{ + PermissionSysconsoleReadAboutEditionAndLicense, + PermissionSysconsoleReadBilling, + PermissionSysconsoleReadReportingSiteStatistics, + PermissionSysconsoleReadReportingTeamStatistics, + PermissionSysconsoleReadReportingServerLogs, + PermissionSysconsoleReadUserManagementUsers, + PermissionSysconsoleReadUserManagementGroups, + PermissionSysconsoleReadUserManagementTeams, + PermissionSysconsoleReadUserManagementChannels, + PermissionSysconsoleReadUserManagementPermissions, + PermissionSysconsoleReadUserManagementSystemRoles, + PermissionSysconsoleReadEnvironmentWebServer, + PermissionSysconsoleReadEnvironmentDatabase, + PermissionSysconsoleReadEnvironmentElasticsearch, + PermissionSysconsoleReadEnvironmentFileStorage, + PermissionSysconsoleReadEnvironmentImageProxy, + PermissionSysconsoleReadEnvironmentSMTP, + PermissionSysconsoleReadEnvironmentPushNotificationServer, + PermissionSysconsoleReadEnvironmentHighAvailability, + PermissionSysconsoleReadEnvironmentRateLimiting, + PermissionSysconsoleReadEnvironmentLogging, + PermissionSysconsoleReadEnvironmentSessionLengths, + PermissionSysconsoleReadEnvironmentPerformanceMonitoring, + PermissionSysconsoleReadEnvironmentDeveloper, + PermissionSysconsoleReadSiteCustomization, + PermissionSysconsoleReadSiteLocalization, + PermissionSysconsoleReadSiteUsersAndTeams, + PermissionSysconsoleReadSiteNotifications, + PermissionSysconsoleReadSiteAnnouncementBanner, + PermissionSysconsoleReadSiteEmoji, + PermissionSysconsoleReadSitePosts, + PermissionSysconsoleReadSiteFileSharingAndDownloads, + PermissionSysconsoleReadSitePublicLinks, + PermissionSysconsoleReadSiteNotices, + PermissionSysconsoleReadAuthenticationSignup, + PermissionSysconsoleReadAuthenticationEmail, + PermissionSysconsoleReadAuthenticationPassword, + PermissionSysconsoleReadAuthenticationMfa, + PermissionSysconsoleReadAuthenticationLdap, + PermissionSysconsoleReadAuthenticationSaml, + PermissionSysconsoleReadAuthenticationOpenid, + PermissionSysconsoleReadAuthenticationGuestAccess, + PermissionSysconsoleReadPlugins, + PermissionSysconsoleReadIntegrationsIntegrationManagement, + PermissionSysconsoleReadIntegrationsBotAccounts, + PermissionSysconsoleReadIntegrationsGif, + PermissionSysconsoleReadIntegrationsCors, + PermissionSysconsoleReadComplianceDataRetentionPolicy, + PermissionSysconsoleReadComplianceComplianceExport, + PermissionSysconsoleReadComplianceComplianceMonitoring, + PermissionSysconsoleReadComplianceCustomTermsOfService, + PermissionSysconsoleReadExperimentalFeatures, + PermissionSysconsoleReadExperimentalFeatureFlags, + PermissionSysconsoleReadExperimentalBleve, + } + + SysconsoleWritePermissions = []*Permission{ + PermissionSysconsoleWriteAboutEditionAndLicense, + PermissionSysconsoleWriteBilling, + PermissionSysconsoleWriteReportingSiteStatistics, + PermissionSysconsoleWriteReportingTeamStatistics, + PermissionSysconsoleWriteReportingServerLogs, + PermissionSysconsoleWriteUserManagementUsers, + PermissionSysconsoleWriteUserManagementGroups, + PermissionSysconsoleWriteUserManagementTeams, + PermissionSysconsoleWriteUserManagementChannels, + PermissionSysconsoleWriteUserManagementPermissions, + PermissionSysconsoleWriteUserManagementSystemRoles, + PermissionSysconsoleWriteEnvironmentWebServer, + PermissionSysconsoleWriteEnvironmentDatabase, + PermissionSysconsoleWriteEnvironmentElasticsearch, + PermissionSysconsoleWriteEnvironmentFileStorage, + PermissionSysconsoleWriteEnvironmentImageProxy, + PermissionSysconsoleWriteEnvironmentSMTP, + PermissionSysconsoleWriteEnvironmentPushNotificationServer, + PermissionSysconsoleWriteEnvironmentHighAvailability, + PermissionSysconsoleWriteEnvironmentRateLimiting, + PermissionSysconsoleWriteEnvironmentLogging, + PermissionSysconsoleWriteEnvironmentSessionLengths, + PermissionSysconsoleWriteEnvironmentPerformanceMonitoring, + PermissionSysconsoleWriteEnvironmentDeveloper, + PermissionSysconsoleWriteSiteCustomization, + PermissionSysconsoleWriteSiteLocalization, + PermissionSysconsoleWriteSiteUsersAndTeams, + PermissionSysconsoleWriteSiteNotifications, + PermissionSysconsoleWriteSiteAnnouncementBanner, + PermissionSysconsoleWriteSiteEmoji, + PermissionSysconsoleWriteSitePosts, + PermissionSysconsoleWriteSiteFileSharingAndDownloads, + PermissionSysconsoleWriteSitePublicLinks, + PermissionSysconsoleWriteSiteNotices, + PermissionSysconsoleWriteAuthenticationSignup, + PermissionSysconsoleWriteAuthenticationEmail, + PermissionSysconsoleWriteAuthenticationPassword, + PermissionSysconsoleWriteAuthenticationMfa, + PermissionSysconsoleWriteAuthenticationLdap, + PermissionSysconsoleWriteAuthenticationSaml, + PermissionSysconsoleWriteAuthenticationOpenid, + PermissionSysconsoleWriteAuthenticationGuestAccess, + PermissionSysconsoleWritePlugins, + PermissionSysconsoleWriteIntegrationsIntegrationManagement, + PermissionSysconsoleWriteIntegrationsBotAccounts, + PermissionSysconsoleWriteIntegrationsGif, + PermissionSysconsoleWriteIntegrationsCors, + PermissionSysconsoleWriteComplianceDataRetentionPolicy, + PermissionSysconsoleWriteComplianceComplianceExport, + PermissionSysconsoleWriteComplianceComplianceMonitoring, + PermissionSysconsoleWriteComplianceCustomTermsOfService, + PermissionSysconsoleWriteExperimentalFeatures, + PermissionSysconsoleWriteExperimentalFeatureFlags, + PermissionSysconsoleWriteExperimentalBleve, + } + + SystemScopedPermissionsMinusSysconsole := []*Permission{ + PermissionAssignSystemAdminRole, + PermissionManageRoles, + PermissionManageSystem, + PermissionCreateDirectChannel, + PermissionCreateGroupChannel, + PermissionListPublicTeams, + PermissionJoinPublicTeams, + PermissionListPrivateTeams, + PermissionJoinPrivateTeams, + PermissionEditOtherUsers, + PermissionReadOtherUsersTeams, + PermissionGetPublicLink, + PermissionManageOAuth, + PermissionManageSystemWideOAuth, + PermissionCreateTeam, + PermissionListUsersWithoutTeam, + PermissionCreateUserAccessToken, + PermissionReadUserAccessToken, + PermissionRevokeUserAccessToken, + PermissionCreateBot, + PermissionAssignBot, + PermissionReadBots, + PermissionReadOthersBots, + PermissionManageBots, + PermissionManageOthersBots, + PermissionReadJobs, + PermissionManageJobs, + PermissionPromoteGuest, + PermissionDemoteToGuest, + PermissionEditBrand, + PermissionManageSharedChannels, + PermissionManageSecureConnections, + PermissionDownloadComplianceExportResult, + PermissionCreateDataRetentionJob, + PermissionReadDataRetentionJob, + PermissionCreateComplianceExportJob, + PermissionReadComplianceExportJob, + PermissionReadAudits, + PermissionTestSiteURL, + PermissionTestElasticsearch, + PermissionTestS3, + PermissionReloadConfig, + PermissionInvalidateCaches, + PermissionRecycleDatabaseConnections, + PermissionPurgeElasticsearchIndexes, + PermissionTestEmail, + PermissionCreateElasticsearchPostIndexingJob, + PermissionCreateElasticsearchPostAggregationJob, + PermissionReadElasticsearchPostIndexingJob, + PermissionReadElasticsearchPostAggregationJob, + PermissionPurgeBleveIndexes, + PermissionCreatePostBleveIndexesJob, + PermissionCreateLdapSyncJob, + PermissionReadLdapSyncJob, + PermissionTestLdap, + PermissionInvalidateEmailInvite, + PermissionGetSamlMetadataFromIdp, + PermissionAddSamlPublicCert, + PermissionAddSamlPrivateCert, + PermissionAddSamlIdpCert, + PermissionRemoveSamlPublicCert, + PermissionRemoveSamlPrivateCert, + PermissionRemoveSamlIdpCert, + PermissionGetSamlCertStatus, + PermissionAddLdapPublicCert, + PermissionAddLdapPrivateCert, + PermissionRemoveLdapPublicCert, + PermissionRemoveLdapPrivateCert, + PermissionGetAnalytics, + PermissionGetLogs, + PermissionReadLicenseInformation, + PermissionManageLicenseInformation, + } + + TeamScopedPermissions := []*Permission{ + PermissionInviteUser, + PermissionAddUserToTeam, + PermissionManageSlashCommands, + PermissionManageOthersSlashCommands, + PermissionCreatePublicChannel, + PermissionCreatePrivateChannel, + PermissionManageTeamRoles, + PermissionListTeamChannels, + PermissionJoinPublicChannels, + PermissionReadPublicChannel, + PermissionManageIncomingWebhooks, + PermissionManageOutgoingWebhooks, + PermissionManageOthersIncomingWebhooks, + PermissionManageOthersOutgoingWebhooks, + PermissionCreateEmojis, + PermissionDeleteEmojis, + PermissionDeleteOthersEmojis, + PermissionRemoveUserFromTeam, + PermissionManageTeam, + PermissionImportTeam, + PermissionViewTeam, + PermissionViewMembers, + PermissionInviteGuest, + } + + ChannelScopedPermissions := []*Permission{ + PermissionUseSlashCommands, + PermissionManagePublicChannelMembers, + PermissionManagePrivateChannelMembers, + PermissionManageChannelRoles, + PermissionManagePublicChannelProperties, + PermissionManagePrivateChannelProperties, + PermissionConvertPublicChannelToPrivate, + PermissionConvertPrivateChannelToPublic, + PermissionDeletePublicChannel, + PermissionDeletePrivateChannel, + PermissionReadChannel, + PermissionReadPublicChannelGroups, + PermissionReadPrivateChannelGroups, + PermissionAddReaction, + PermissionRemoveReaction, + PermissionRemoveOthersReactions, + PermissionUploadFile, + PermissionCreatePost, + PermissionCreatePostPublic, + PermissionCreatePostEphemeral, + PermissionEditPost, + PermissionEditOthersPosts, + PermissionDeletePost, + PermissionDeleteOthersPosts, + PermissionUseChannelMentions, + PermissionUseGroupMentions, + } + + DeprecatedPermissions = []*Permission{ + PermissionPermanentDeleteUser, + PermissionManageWebhooks, + PermissionManageOthersWebhooks, + PermissionManageEmojis, + PermissionManageOthersEmojis, + PermissionSysconsoleReadAuthentication, + PermissionSysconsoleWriteAuthentication, + PermissionSysconsoleReadSite, + PermissionSysconsoleWriteSite, + PermissionSysconsoleReadEnvironment, + PermissionSysconsoleWriteEnvironment, + PermissionSysconsoleReadReporting, + PermissionSysconsoleWriteReporting, + PermissionSysconsoleReadAbout, + PermissionSysconsoleWriteAbout, + PermissionSysconsoleReadExperimental, + PermissionSysconsoleWriteExperimental, + PermissionSysconsoleReadIntegrations, + PermissionSysconsoleWriteIntegrations, + PermissionSysconsoleReadCompliance, + PermissionSysconsoleWriteCompliance, + } + + AllPermissions = []*Permission{} + AllPermissions = append(AllPermissions, SystemScopedPermissionsMinusSysconsole...) + AllPermissions = append(AllPermissions, TeamScopedPermissions...) + AllPermissions = append(AllPermissions, ChannelScopedPermissions...) + AllPermissions = append(AllPermissions, SysconsoleReadPermissions...) + AllPermissions = append(AllPermissions, SysconsoleWritePermissions...) + + ChannelModeratedPermissions = []string{ + PermissionCreatePost.Id, + "create_reactions", + "manage_members", + PermissionUseChannelMentions.Id, + } + + ChannelModeratedPermissionsMap = map[string]string{ + PermissionCreatePost.Id: ChannelModeratedPermissions[0], + PermissionAddReaction.Id: ChannelModeratedPermissions[1], + PermissionRemoveReaction.Id: ChannelModeratedPermissions[1], + PermissionManagePublicChannelMembers.Id: ChannelModeratedPermissions[2], + PermissionManagePrivateChannelMembers.Id: ChannelModeratedPermissions[2], + PermissionUseChannelMentions.Id: ChannelModeratedPermissions[3], + } +} + +func init() { + initializePermissions() +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_cluster_event.go b/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_cluster_event.go new file mode 100644 index 00000000..9e227447 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_cluster_event.go @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + PluginClusterEventSendTypeReliable = ClusterSendReliable + PluginClusterEventSendTypeBestEffort = ClusterSendBestEffort +) + +// PluginClusterEvent is used to allow intra-cluster plugin communication. +type PluginClusterEvent struct { + // Id is the unique identifier for the event. + Id string + // Data is the event payload. + Data []byte +} + +// PluginClusterEventSendOptions defines some properties that apply when sending +// plugin events across a cluster. +type PluginClusterEventSendOptions struct { + // SendType defines the type of communication channel used to send the event. + SendType string + // TargetId identifies the cluster node to which the event should be sent. + // It should match the cluster id of the receiving instance. + // If empty, the event gets broadcasted to all other nodes. + TargetId string +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_event_data.go b/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_event_data.go new file mode 100644 index 00000000..1253533d --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_event_data.go @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +// PluginEventData used to notify peers about plugin changes. +type PluginEventData struct { + Id string `json:"id"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_key_value.go b/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_key_value.go new file mode 100644 index 00000000..ad5971dd --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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 ( + KeyValuePluginIdMaxRunes = 190 + KeyValueKeyMaxRunes = 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 kv.PluginId == "" || utf8.RuneCountInString(kv.PluginId) > KeyValuePluginIdMaxRunes { + return NewAppError("PluginKeyValue.IsValid", "model.plugin_key_value.is_valid.plugin_id.app_error", map[string]interface{}{"Max": KeyValueKeyMaxRunes, "Min": 0}, "key="+kv.Key, http.StatusBadRequest) + } + + if kv.Key == "" || utf8.RuneCountInString(kv.Key) > KeyValueKeyMaxRunes { + return NewAppError("PluginKeyValue.IsValid", "model.plugin_key_value.is_valid.key.app_error", map[string]interface{}{"Max": KeyValueKeyMaxRunes, "Min": 0}, "key="+kv.Key, http.StatusBadRequest) + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_kvset_options.go b/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_kvset_options.go new file mode 100644 index 00000000..1d374c80 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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/v6/model/plugin_status.go b/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_status.go new file mode 100644 index 00000000..c206505b --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_status.go @@ -0,0 +1,26 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +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 diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_valid.go b/vendor/github.com/mattermost/mattermost-server/v6/model/plugin_valid.go new file mode 100644 index 00000000..b6144513 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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/v6/model/plugins_response.go b/vendor/github.com/mattermost/mattermost-server/v6/model/plugins_response.go new file mode 100644 index 00000000..5aed0b3c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/plugins_response.go @@ -0,0 +1,13 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type PluginInfo struct { + Manifest +} + +type PluginsResponse struct { + Active []*PluginInfo `json:"active"` + Inactive []*PluginInfo `json:"inactive"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/post.go b/vendor/github.com/mattermost/mattermost-server/v6/model/post.go new file mode 100644 index 00000000..d13fef0a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/post.go @@ -0,0 +1,719 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "errors" + "net/http" + "regexp" + "sort" + "strings" + "sync" + "unicode/utf8" + + "github.com/mattermost/mattermost-server/v6/shared/markdown" +) + +const ( + PostSystemMessagePrefix = "system_" + PostTypeDefault = "" + PostTypeSlackAttachment = "slack_attachment" + PostTypeSystemGeneric = "system_generic" + PostTypeJoinLeave = "system_join_leave" // Deprecated, use PostJoinChannel or PostLeaveChannel instead + PostTypeJoinChannel = "system_join_channel" + PostTypeGuestJoinChannel = "system_guest_join_channel" + PostTypeLeaveChannel = "system_leave_channel" + PostTypeJoinTeam = "system_join_team" + PostTypeLeaveTeam = "system_leave_team" + PostTypeAutoResponder = "system_auto_responder" + PostTypeAddRemove = "system_add_remove" // Deprecated, use PostAddToChannel or PostRemoveFromChannel instead + PostTypeAddToChannel = "system_add_to_channel" + PostTypeAddGuestToChannel = "system_add_guest_to_chan" + PostTypeRemoveFromChannel = "system_remove_from_channel" + PostTypeMoveChannel = "system_move_channel" + PostTypeAddToTeam = "system_add_to_team" + PostTypeRemoveFromTeam = "system_remove_from_team" + PostTypeHeaderChange = "system_header_change" + PostTypeDisplaynameChange = "system_displayname_change" + PostTypeConvertChannel = "system_convert_channel" + PostTypePurposeChange = "system_purpose_change" + PostTypeChannelDeleted = "system_channel_deleted" + PostTypeChannelRestored = "system_channel_restored" + PostTypeEphemeral = "system_ephemeral" + PostTypeChangeChannelPrivacy = "system_change_chan_privacy" + PostTypeAddBotTeamsChannels = "add_bot_teams_channels" + PostTypeSystemWarnMetricStatus = "warn_metric_status" + PostTypeMe = "me" + PostCustomTypePrefix = "custom_" + + PostFileidsMaxRunes = 300 + PostFilenamesMaxRunes = 4000 + PostHashtagsMaxRunes = 1000 + PostMessageMaxRunesV1 = 4000 + PostMessageMaxBytesV2 = 65535 // Maximum size of a TEXT column in MySQL + PostMessageMaxRunesV2 = PostMessageMaxBytesV2 / 4 // Assume a worst-case representation + PostPropsMaxRunes = 800000 + PostPropsMaxUserRunes = PostPropsMaxRunes - 40000 // Leave some room for system / pre-save modifications + + PropsAddChannelMember = "add_channel_member" + + PostPropsAddedUserId = "addedUserId" + PostPropsDeleteBy = "deleteBy" + PostPropsOverrideIconURL = "override_icon_url" + PostPropsOverrideIconEmoji = "override_icon_emoji" + + PostPropsMentionHighlightDisabled = "mentionHighlightDisabled" + PostPropsGroupHighlightDisabled = "disable_group_highlight" + + PostPropsPreviewedPost = "previewed_post" +) + +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"` + 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:"-"` // 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"` + RemoteId *string `json:"remote_id,omitempty"` + + // Transient data populated before sending a post to the client + ReplyCount int64 `json:"reply_count" db:"-"` + LastReplyAt int64 `json:"last_reply_at" db:"-"` + Participants []*User `json:"participants" db:"-"` + IsFollowing *bool `json:"is_following,omitempty" db:"-"` // for root posts in collapsed thread mode indicates if the current user is following this thread + 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"` +} + +type FileForIndexing struct { + FileInfo + ChannelId string `json:"channel_id"` + Content string `json:"content"` +} + +// 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.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.Participants = o.Participants + dst.LastReplyAt = o.LastReplyAt + dst.Metadata = o.Metadata + if o.IsFollowing != nil { + dst.IsFollowing = NewBool(*o.IsFollowing) + } + dst.RemoteId = o.RemoteId + 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, error) { + copy := o.Clone() + copy.StripActionIntegrations() + b, err := json.Marshal(copy) + return string(b), err +} + +type GetPostsSinceOptions struct { + UserId string + ChannelId string + Time int64 + SkipFetchThreads bool + CollapsedThreads bool + CollapsedThreadsExtended bool + SortAscending bool +} + +type GetPostsSinceForSyncCursor struct { + LastPostUpdateAt int64 + LastPostId string +} + +type GetPostsSinceForSyncOptions struct { + ChannelId string + ExcludeRemoteId string + IncludeDeleted bool +} + +type GetPostsOptions struct { + UserId string + ChannelId string + PostId string + Page int + PerPage int + SkipFetchThreads bool + CollapsedThreads bool + CollapsedThreadsExtended bool +} + +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) || o.RootId == "") { + return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest) + } + + if !(len(o.OriginalId) == 26 || o.OriginalId == "") { + 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) > PostHashtagsMaxRunes { + return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + switch o.Type { + case + PostTypeDefault, + PostTypeSystemGeneric, + PostTypeJoinLeave, + PostTypeAutoResponder, + PostTypeAddRemove, + PostTypeJoinChannel, + PostTypeGuestJoinChannel, + PostTypeLeaveChannel, + PostTypeJoinTeam, + PostTypeLeaveTeam, + PostTypeAddToChannel, + PostTypeAddGuestToChannel, + PostTypeRemoveFromChannel, + PostTypeMoveChannel, + PostTypeAddToTeam, + PostTypeRemoveFromTeam, + PostTypeSlackAttachment, + PostTypeHeaderChange, + PostTypePurposeChange, + PostTypeDisplaynameChange, + PostTypeConvertChannel, + PostTypeChannelDeleted, + PostTypeChannelRestored, + PostTypeChangeChannelPrivacy, + PostTypeAddBotTeamsChannels, + PostTypeSystemWarnMetricStatus, + PostTypeMe: + default: + if !strings.HasPrefix(o.Type, PostCustomTypePrefix) { + return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) + } + } + + if utf8.RuneCountInString(ArrayToJSON(o.Filenames)) > PostFilenamesMaxRunes { + return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(ArrayToJSON(o.FileIds)) > PostFileidsMaxRunes { + return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if utf8.RuneCountInString(StringInterfaceToJSON(o.GetProps())) > PostPropsMaxRunes { + return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + return nil +} + +func (o *Post) SanitizeProps() { + if o == nil { + return + } + membersToSanitize := []string{ + PropsAddChannelMember, + } + + for _, member := range membersToSanitize { + if _, ok := o.GetProps()[member]; ok { + o.DelProp(member) + } + } + for _, p := range o.Participants { + p.Sanitize(map[string]bool{}) + } +} + +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(PostSystemMessagePrefix) && o.Type[:len(PostSystemMessagePrefix)] == PostSystemMessagePrefix +} + +// IsRemote returns true if the post originated on a remote cluster. +func (o *Post) IsRemote() bool { + return o.RemoteId != nil && *o.RemoteId != "" +} + +// GetRemoteID safely returns the remoteID or empty string if not remote. +func (o *Post) GetRemoteID() string { + if o.RemoteId != nil { + return *o.RemoteId + } + return "" +} + +func (o *Post) IsJoinLeaveMessage() bool { + return o.Type == PostTypeJoinLeave || + o.Type == PostTypeAddRemove || + o.Type == PostTypeJoinChannel || + o.Type == PostTypeLeaveChannel || + o.Type == PostTypeJoinTeam || + o.Type == PostTypeLeaveTeam || + o.Type == PostTypeAddToChannel || + o.Type == PostTypeRemoveFromChannel || + o.Type == PostTypeAddToTeam || + o.Type == PostTypeRemoveFromTeam +} + +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 *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(PostPropsMentionHighlightDisabled, true) + } + return mention +} + +// DisableMentionHighlights disables mention highlighting for a post patch if required. +func (o *PostPatch) DisableMentionHighlights() { + if o.Message == nil { + return + } + if _, hasMentions := findAtChannelMention(*o.Message); hasMentions { + if o.Props == nil { + o.Props = &StringInterface{} + } + (*o.Props)[PostPropsMentionHighlightDisabled] = 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 { + // Ignoring nil actions + i := 0 + for _, action := range decoded.Actions { + if action != nil { + decoded.Actions[i] = action + i++ + } + } + decoded.Actions = decoded.Actions[:i] + + // Ignoring nil fields + i = 0 + for _, field := range decoded.Fields { + if field != nil { + decoded.Fields[i] = field + i++ + } + } + decoded.Fields = decoded.Fields[:i] + 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 +} + +// 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) +} + +func (o *Post) IsFromOAuthBot() bool { + props := o.GetProps() + return props["from_webhook"] == "true" && props["override_username"] != "" +} + +func (o *Post) ToNilIfInvalid() *Post { + if o.Id == "" { + return nil + } + return o +} + +func (o *Post) RemovePreviewPost() { + if o.Metadata == nil || o.Metadata.Embeds == nil { + return + } + n := 0 + for _, embed := range o.Metadata.Embeds { + if embed.Type != PostEmbedPermalink { + o.Metadata.Embeds[n] = embed + n++ + } + } + o.Metadata.Embeds = o.Metadata.Embeds[:n] +} + +func (o *Post) GetPreviewPost() *PreviewPost { + for _, embed := range o.Metadata.Embeds { + if embed.Type == PostEmbedPermalink { + if previewPost, ok := embed.Data.(*PreviewPost); ok { + return previewPost + } + } + } + return nil +} + +func (o *Post) GetPreviewedPostProp() string { + if val, ok := o.GetProp(PostPropsPreviewedPost).(string); ok { + return val + } + return "" +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/post_embed.go b/vendor/github.com/mattermost/mattermost-server/v6/model/post_embed.go new file mode 100644 index 00000000..ea3e2c5b --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/post_embed.go @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + PostEmbedImage PostEmbedType = "image" + PostEmbedMessageAttachment PostEmbedType = "message_attachment" + PostEmbedOpengraph PostEmbedType = "opengraph" + PostEmbedLink PostEmbedType = "link" + PostEmbedPermalink PostEmbedType = "permalink" +) + +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/v6/model/post_list.go b/vendor/github.com/mattermost/mattermost-server/v6/model/post_list.go new file mode 100644 index 00000000..933d1f8f --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/post_list.go @@ -0,0 +1,177 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "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) Clone() *PostList { + orderCopy := make([]string, len(o.Order)) + postsCopy := make(map[string]*Post) + for i, v := range o.Order { + orderCopy[i] = v + } + for k, v := range o.Posts { + postsCopy[k] = v.Clone() + } + return &PostList{ + Order: orderCopy, + Posts: postsCopy, + NextPostId: o.NextPostId, + PrevPostId: o.PrevPostId, + } +} + +func (o *PostList) ToSlice() []*Post { + var posts []*Post + + if l := len(o.Posts); l > 0 { + posts = make([]*Post, 0, l) + } + + 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, error) { + copy := *o + copy.StripActionIntegrations() + b, err := json.Marshal(©) + return string(b), err +} + +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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/post_metadata.go b/vendor/github.com/mattermost/mattermost-server/v6/model/post_metadata.go new file mode 100644 index 00000000..0f9e61d3 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/post_metadata.go @@ -0,0 +1,64 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +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"` +} + +// Copy does a deep copy +func (p *PostMetadata) Copy() *PostMetadata { + embedsCopy := make([]*PostEmbed, len(p.Embeds)) + copy(embedsCopy, p.Embeds) + + emojisCopy := make([]*Emoji, len(p.Emojis)) + copy(emojisCopy, p.Emojis) + + filesCopy := make([]*FileInfo, len(p.Files)) + copy(filesCopy, p.Files) + + imagesCopy := map[string]*PostImage{} + for k, v := range p.Images { + imagesCopy[k] = v + } + + reactionsCopy := make([]*Reaction, len(p.Reactions)) + copy(reactionsCopy, p.Reactions) + + return &PostMetadata{ + Embeds: embedsCopy, + Emojis: emojisCopy, + Files: filesCopy, + Images: imagesCopy, + Reactions: reactionsCopy, + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/post_search_results.go b/vendor/github.com/mattermost/mattermost-server/v6/model/post_search_results.go new file mode 100644 index 00000000..92e044a7 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/post_search_results.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" +) + +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, error) { + copy := *o + copy.PostList.StripActionIntegrations() + b, err := json.Marshal(©) + return string(b), err +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/preference.go b/vendor/github.com/mattermost/mattermost-server/v6/model/preference.go new file mode 100644 index 00000000..41a58235 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/preference.go @@ -0,0 +1,122 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "net/http" + "regexp" + "strings" + "unicode/utf8" +) + +const ( + PreferenceCategoryDirectChannelShow = "direct_channel_show" + PreferenceCategoryGroupChannelShow = "group_channel_show" + PreferenceCategoryTutorialSteps = "tutorial_step" + PreferenceCategoryAdvancedSettings = "advanced_settings" + PreferenceCategoryFlaggedPost = "flagged_post" + PreferenceCategoryFavoriteChannel = "favorite_channel" + PreferenceCategorySidebarSettings = "sidebar_settings" + + PreferenceCategoryDisplaySettings = "display_settings" + PreferenceNameCollapsedThreadsEnabled = "collapsed_reply_threads" + PreferenceNameChannelDisplayMode = "channel_display_mode" + PreferenceNameCollapseSetting = "collapse_previews" + PreferenceNameMessageDisplay = "message_display" + PreferenceNameNameFormat = "name_format" + PreferenceNameUseMilitaryTime = "use_military_time" + PreferenceRecommendedNextSteps = "recommended_next_steps" + + PreferenceCategoryTheme = "theme" + // the name for theme props is the team id + + PreferenceCategoryAuthorizedOAuthApp = "oauth_app" + // the name for oauth_app is the client_id and value is the current scope + + PreferenceCategoryLast = "last" + PreferenceNameLastChannel = "channel" + PreferenceNameLastTeam = "team" + + PreferenceCategoryCustomStatus = "custom_status" + PreferenceNameRecentCustomStatuses = "recent_custom_statuses" + PreferenceNameCustomStatusTutorialState = "custom_status_tutorial_state" + + PreferenceCustomStatusModalViewed = "custom_status_modal_viewed" + + PreferenceCategoryNotifications = "notifications" + PreferenceNameEmailInterval = "email_interval" + + PreferenceEmailIntervalNoBatchingSeconds = "30" // the "immediate" setting is actually 30s + PreferenceEmailIntervalBatchingSeconds = "900" // fifteen minutes is 900 seconds + PreferenceEmailIntervalImmediately = "immediately" + PreferenceEmailIntervalFifteen = "fifteen" + PreferenceEmailIntervalFifteenAsSeconds = "900" + PreferenceEmailIntervalHour = "hour" + PreferenceEmailIntervalHourAsSeconds = "3600" +) + +type Preference struct { + UserId string `json:"user_id"` + Category string `json:"category"` + Name string `json:"name"` + Value string `json:"value"` +} + +type Preferences []Preference + +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 o.Category == "" || 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 == PreferenceCategoryTheme { + 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 == PreferenceCategoryTheme { + // 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/v6/model/product_notices.go b/vendor/github.com/mattermost/mattermost-server/v6/model/product_notices.go new file mode 100644 index 00000000..94343cb4 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/product_notices.go @@ -0,0 +1,220 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" + + "github.com/pkg/errors" +) + +type ProductNotices []ProductNotice + +func (r *ProductNotices) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +func UnmarshalProductNotices(data []byte) (ProductNotices, error) { + var r ProductNotices + err := json.Unmarshal(data, &r) + return r, err +} + +// List of product notices. Order is important and is used to resolve priorities. +// Each notice will only be show if conditions are met. +type ProductNotice struct { + Conditions Conditions `json:"conditions"` + ID string `json:"id"` // Unique identifier for this notice. Can be a running number. Used for storing 'viewed'; state on the server. + LocalizedMessages map[string]NoticeMessageInternal `json:"localizedMessages"` // Notice message data, organized by locale.; Example:; "localizedMessages": {; "en": { "title": "English", description: "English description"},; "frFR": { "title": "Frances", description: "French description"}; } + Repeatable *bool `json:"repeatable,omitempty"` // Configurable flag if the notice should reappear after it’s seen and dismissed +} + +func (n *ProductNotice) SysAdminOnly() bool { + return n.Conditions.Audience != nil && *n.Conditions.Audience == NoticeAudienceSysadmin +} + +func (n *ProductNotice) TeamAdminOnly() bool { + return n.Conditions.Audience != nil && *n.Conditions.Audience == NoticeAudienceTeamAdmin +} + +type Conditions struct { + Audience *NoticeAudience `json:"audience,omitempty"` + ClientType *NoticeClientType `json:"clientType,omitempty"` // Only show the notice on specific clients. Defaults to 'all' + DesktopVersion []string `json:"desktopVersion,omitempty"` // What desktop client versions does this notice apply to.; Format: semver ranges (https://devhints.io/semver); Example: [">=1.2.3 < ~2.4.x"]; Example: ["<v5.19", "v5.20-v5.22"] + DisplayDate *string `json:"displayDate,omitempty"` // When to display the notice.; Examples:; "2020-03-01T00:00:00Z" - show on specified date; ">= 2020-03-01T00:00:00Z" - show after specified date; "< 2020-03-01T00:00:00Z" - show before the specified date; "> 2020-03-01T00:00:00Z <= 2020-04-01T00:00:00Z" - show only between the specified dates + InstanceType *NoticeInstanceType `json:"instanceType,omitempty"` + MobileVersion []string `json:"mobileVersion,omitempty"` // What mobile client versions does this notice apply to.; Format: semver ranges (https://devhints.io/semver); Example: [">=1.2.3 < ~2.4.x"]; Example: ["<v5.19", "v5.20-v5.22"] + NumberOfPosts *int64 `json:"numberOfPosts,omitempty"` // Only show the notice when server has more than specified number of posts + NumberOfUsers *int64 `json:"numberOfUsers,omitempty"` // Only show the notice when server has more than specified number of users + ServerConfig map[string]interface{} `json:"serverConfig,omitempty"` // Map of mattermost server config paths and their values. Notice will be displayed only if; the values match the target server config; Example: serverConfig: { "PluginSettings.Enable": true, "GuestAccountsSettings.Enable":; false } + ServerVersion []string `json:"serverVersion,omitempty"` // What server versions does this notice apply to.; Format: semver ranges (https://devhints.io/semver); Example: [">=1.2.3 < ~2.4.x"]; Example: ["<v5.19", "v5.20-v5.22"] + Sku *NoticeSKU `json:"sku,omitempty"` + UserConfig map[string]interface{} `json:"userConfig,omitempty"` // Map of user's settings and their values. Notice will be displayed only if the values; match the viewing users' config; Example: userConfig: { "new_sidebar.disabled": true } + DeprecatingDependency *ExternalDependency `json:"deprecating_dependency,omitempty"` // External dependency which is going to be deprecated +} + +type NoticeMessageInternal struct { + Action *NoticeAction `json:"action,omitempty"` // Optional action to perform on action button click. (defaults to closing the notice) + ActionParam *string `json:"actionParam,omitempty"` // Optional action parameter.; Example: {"action": "url", actionParam: "/console/some-page"} + ActionText *string `json:"actionText,omitempty"` // Optional override for the action button text (defaults to OK) + Description string `json:"description"` // Notice content. Use {{Mattermost}} instead of plain text to support white-labeling. Text; supports Markdown. + Image *string `json:"image,omitempty"` + Title string `json:"title"` // Notice title. Use {{Mattermost}} instead of plain text to support white-labeling. Text; supports Markdown. +} +type NoticeMessages []NoticeMessage + +type NoticeMessage struct { + NoticeMessageInternal + ID string `json:"id"` + SysAdminOnly bool `json:"sysAdminOnly"` + TeamAdminOnly bool `json:"teamAdminOnly"` +} + +func (r *NoticeMessages) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +func UnmarshalProductNoticeMessages(data io.Reader) (NoticeMessages, error) { + var r NoticeMessages + err := json.NewDecoder(data).Decode(&r) + return r, err +} + +// User role, i.e. who will see the notice. Defaults to "all" +type NoticeAudience string + +func NewNoticeAudience(s NoticeAudience) *NoticeAudience { + return &s +} + +func (a *NoticeAudience) Matches(sysAdmin bool, teamAdmin bool) bool { + switch *a { + case NoticeAudienceAll: + return true + case NoticeAudienceMember: + return !sysAdmin && !teamAdmin + case NoticeAudienceSysadmin: + return sysAdmin + case NoticeAudienceTeamAdmin: + return teamAdmin + } + return false +} + +const ( + NoticeAudienceAll NoticeAudience = "all" + NoticeAudienceMember NoticeAudience = "member" + NoticeAudienceSysadmin NoticeAudience = "sysadmin" + NoticeAudienceTeamAdmin NoticeAudience = "teamadmin" +) + +// Only show the notice on specific clients. Defaults to 'all' +// +// Client type. Defaults to "all" +type NoticeClientType string + +func NewNoticeClientType(s NoticeClientType) *NoticeClientType { return &s } + +func (c *NoticeClientType) Matches(other NoticeClientType) bool { + switch *c { + case NoticeClientTypeAll: + return true + case NoticeClientTypeMobile: + return other == NoticeClientTypeMobileIos || other == NoticeClientTypeMobileAndroid + default: + return *c == other + } +} + +const ( + NoticeClientTypeAll NoticeClientType = "all" + NoticeClientTypeDesktop NoticeClientType = "desktop" + NoticeClientTypeMobile NoticeClientType = "mobile" + NoticeClientTypeMobileAndroid NoticeClientType = "mobile-android" + NoticeClientTypeMobileIos NoticeClientType = "mobile-ios" + NoticeClientTypeWeb NoticeClientType = "web" +) + +func NoticeClientTypeFromString(s string) (NoticeClientType, error) { + switch s { + case "web": + return NoticeClientTypeWeb, nil + case "mobile-ios": + return NoticeClientTypeMobileIos, nil + case "mobile-android": + return NoticeClientTypeMobileAndroid, nil + case "desktop": + return NoticeClientTypeDesktop, nil + } + return NoticeClientTypeAll, errors.New("Invalid client type supplied") +} + +// Instance type. Defaults to "both" +type NoticeInstanceType string + +func NewNoticeInstanceType(n NoticeInstanceType) *NoticeInstanceType { return &n } +func (t *NoticeInstanceType) Matches(isCloud bool) bool { + if *t == NoticeInstanceTypeBoth { + return true + } + if *t == NoticeInstanceTypeCloud && !isCloud { + return false + } + if *t == NoticeInstanceTypeOnPrem && isCloud { + return false + } + return true +} + +const ( + NoticeInstanceTypeBoth NoticeInstanceType = "both" + NoticeInstanceTypeCloud NoticeInstanceType = "cloud" + NoticeInstanceTypeOnPrem NoticeInstanceType = "onprem" +) + +// SKU. Defaults to "all" +type NoticeSKU string + +func NewNoticeSKU(s NoticeSKU) *NoticeSKU { return &s } +func (c *NoticeSKU) Matches(s string) bool { + switch *c { + case NoticeSKUAll: + return true + case NoticeSKUE0, NoticeSKUTeam: + return s == "" + default: + return s == string(*c) + } +} + +const ( + NoticeSKUE0 NoticeSKU = "e0" + NoticeSKUE10 NoticeSKU = "e10" + NoticeSKUE20 NoticeSKU = "e20" + NoticeSKUAll NoticeSKU = "all" + NoticeSKUTeam NoticeSKU = "team" +) + +// Optional action to perform on action button click. (defaults to closing the notice) +// +// Possible actions to execute on button press +type NoticeAction string + +const ( + URL NoticeAction = "url" +) + +// Definition of the table keeping the 'viewed' state of each in-product notice per user +type ProductNoticeViewState struct { + UserId string + NoticeId string + Viewed int32 + Timestamp int64 +} + +type ExternalDependency struct { + Name string `json:"name"` + MinimumVersion string `json:"minimum_version"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/push_notification.go b/vendor/github.com/mattermost/mattermost-server/v6/model/push_notification.go new file mode 100644 index 00000000..cad84f83 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/push_notification.go @@ -0,0 +1,82 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "strings" +) + +const ( + PushNotifyApple = "apple" + PushNotifyAndroid = "android" + PushNotifyAppleReactNative = "apple_rn" + PushNotifyAndroidReactNative = "android_rn" + + PushTypeMessage = "message" + PushTypeClear = "clear" + PushTypeUpdateBadge = "update_badge" + PushTypeSession = "session" + PushMessageV2 = "v2" + + PushSoundNone = "none" + + // The category is set to handle a set of interactive Actions + // with the push notifications + CategoryCanReply = "CAN_REPLY" + + MHPNS = "https://push.mattermost.com" + + PushSendPrepare = "Prepared to send" + PushSendSuccess = "Successful" + PushNotSent = "Not Sent due to preferences" + PushReceived = "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 (pn *PushNotification) DeepCopy() *PushNotification { + copy := *pn + return © +} + +func (pn *PushNotification) SetDeviceIdAndPlatform(deviceId string) { + index := strings.Index(deviceId, ":") + + if index > -1 { + pn.Platform = deviceId[:index] + pn.DeviceId = deviceId[index+1:] + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/push_response.go b/vendor/github.com/mattermost/mattermost-server/v6/model/push_response.go new file mode 100644 index 00000000..a88b4339 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/push_response.go @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ( + PushStatus = "status" + PushStatusOk = "OK" + PushStatusFail = "FAIL" + PushStatusRemove = "REMOVE" + PushStatusErrorMsg = "error" +) + +type PushResponse map[string]string + +func NewOkPushResponse() PushResponse { + m := make(map[string]string) + m[PushStatus] = PushStatusOk + return m +} + +func NewRemovePushResponse() PushResponse { + m := make(map[string]string) + m[PushStatus] = PushStatusRemove + return m +} + +func NewErrorPushResponse(message string) PushResponse { + m := make(map[string]string) + m[PushStatus] = PushStatusFail + m[PushStatusErrorMsg] = message + return m +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/reaction.go b/vendor/github.com/mattermost/mattermost-server/v6/model/reaction.go new file mode 100644 index 00000000..335cb904 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/reaction.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" + "regexp" +) + +type Reaction struct { + UserId string `json:"user_id"` + PostId string `json:"post_id"` + EmojiName string `json:"emoji_name"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + RemoteId *string `json:"remote_id"` +} + +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 o.EmojiName == "" || len(o.EmojiName) > EmojiNameMaxLength || !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) + } + + if o.UpdateAt == 0 { + return NewAppError("Reaction.IsValid", "model.reaction.is_valid.update_at.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} + +func (o *Reaction) PreSave() { + if o.CreateAt == 0 { + o.CreateAt = GetMillis() + } + o.UpdateAt = GetMillis() + o.DeleteAt = 0 + + if o.RemoteId == nil { + o.RemoteId = NewString("") + } +} + +func (o *Reaction) PreUpdate() { + o.UpdateAt = GetMillis() + + if o.RemoteId == nil { + o.RemoteId = NewString("") + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/remote_cluster.go b/vendor/github.com/mattermost/mattermost-server/v6/model/remote_cluster.go new file mode 100644 index 00000000..44d11f16 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/remote_cluster.go @@ -0,0 +1,302 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/json" + "errors" + "io" + "net/http" + "regexp" + "strings" + + "golang.org/x/crypto/scrypt" +) + +const ( + RemoteOfflineAfterMillis = 1000 * 60 * 5 // 5 minutes + RemoteNameMinLength = 1 + RemoteNameMaxLength = 64 +) + +var ( + validRemoteNameChars = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+$`) +) + +type RemoteCluster struct { + RemoteId string `json:"remote_id"` + RemoteTeamId string `json:"remote_team_id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + SiteURL string `json:"site_url"` + CreateAt int64 `json:"create_at"` + LastPingAt int64 `json:"last_ping_at"` + Token string `json:"token"` + RemoteToken string `json:"remote_token"` + Topics string `json:"topics"` + CreatorId string `json:"creator_id"` +} + +func (rc *RemoteCluster) PreSave() { + if rc.RemoteId == "" { + rc.RemoteId = NewId() + } + + if rc.DisplayName == "" { + rc.DisplayName = rc.Name + } + + rc.Name = SanitizeUnicode(rc.Name) + rc.DisplayName = SanitizeUnicode(rc.DisplayName) + rc.Name = NormalizeRemoteName(rc.Name) + + if rc.Token == "" { + rc.Token = NewId() + } + + if rc.CreateAt == 0 { + rc.CreateAt = GetMillis() + } + rc.fixTopics() +} + +func (rc *RemoteCluster) IsValid() *AppError { + if !IsValidId(rc.RemoteId) { + return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "id="+rc.RemoteId, http.StatusBadRequest) + } + + if !IsValidRemoteName(rc.Name) { + return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.name.app_error", nil, "name="+rc.Name, http.StatusBadRequest) + } + + if rc.CreateAt == 0 { + return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.create_at.app_error", nil, "create_at=0", http.StatusBadRequest) + } + + if !IsValidId(rc.CreatorId) { + return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.id.app_error", nil, "creator_id="+rc.CreatorId, http.StatusBadRequest) + } + return nil +} + +func IsValidRemoteName(s string) bool { + if len(s) < RemoteNameMinLength || len(s) > RemoteNameMaxLength { + return false + } + return validRemoteNameChars.MatchString(s) +} + +func (rc *RemoteCluster) PreUpdate() { + if rc.DisplayName == "" { + rc.DisplayName = rc.Name + } + + rc.Name = SanitizeUnicode(rc.Name) + rc.DisplayName = SanitizeUnicode(rc.DisplayName) + rc.Name = NormalizeRemoteName(rc.Name) + rc.fixTopics() +} + +func (rc *RemoteCluster) IsOnline() bool { + return rc.LastPingAt > GetMillis()-RemoteOfflineAfterMillis +} + +// fixTopics ensures all topics are separated by one, and only one, space. +func (rc *RemoteCluster) fixTopics() { + trimmed := strings.TrimSpace(rc.Topics) + if trimmed == "" || trimmed == "*" { + rc.Topics = trimmed + return + } + + var sb strings.Builder + sb.WriteString(" ") + + ss := strings.Split(rc.Topics, " ") + for _, c := range ss { + cc := strings.TrimSpace(c) + if cc != "" { + sb.WriteString(cc) + sb.WriteString(" ") + } + } + rc.Topics = sb.String() +} + +func (rc *RemoteCluster) ToRemoteClusterInfo() RemoteClusterInfo { + return RemoteClusterInfo{ + Name: rc.Name, + DisplayName: rc.DisplayName, + CreateAt: rc.CreateAt, + LastPingAt: rc.LastPingAt, + } +} + +func NormalizeRemoteName(name string) string { + return strings.ToLower(name) +} + +// RemoteClusterInfo provides a subset of RemoteCluster fields suitable for sending to clients. +type RemoteClusterInfo struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + CreateAt int64 `json:"create_at"` + LastPingAt int64 `json:"last_ping_at"` +} + +// RemoteClusterFrame wraps a `RemoteClusterMsg` with credentials specific to a remote cluster. +type RemoteClusterFrame struct { + RemoteId string `json:"remote_id"` + Msg RemoteClusterMsg `json:"msg"` +} + +func (f *RemoteClusterFrame) IsValid() *AppError { + if !IsValidId(f.RemoteId) { + return NewAppError("RemoteClusterFrame.IsValid", "api.remote_cluster.invalid_id.app_error", nil, "RemoteId="+f.RemoteId, http.StatusBadRequest) + } + + if err := f.Msg.IsValid(); err != nil { + return err + } + + return nil +} + +// RemoteClusterMsg represents a message that is sent and received between clusters. +// These are processed and routed via the RemoteClusters service. +type RemoteClusterMsg struct { + Id string `json:"id"` + Topic string `json:"topic"` + CreateAt int64 `json:"create_at"` + Payload json.RawMessage `json:"payload"` +} + +func NewRemoteClusterMsg(topic string, payload json.RawMessage) RemoteClusterMsg { + return RemoteClusterMsg{ + Id: NewId(), + Topic: topic, + CreateAt: GetMillis(), + Payload: payload, + } +} + +func (m RemoteClusterMsg) IsValid() *AppError { + if !IsValidId(m.Id) { + return NewAppError("RemoteClusterMsg.IsValid", "api.remote_cluster.invalid_id.app_error", nil, "Id="+m.Id, http.StatusBadRequest) + } + + if m.Topic == "" { + return NewAppError("RemoteClusterMsg.IsValid", "api.remote_cluster.invalid_topic.app_error", nil, "Topic empty", http.StatusBadRequest) + } + + if len(m.Payload) == 0 { + return NewAppError("RemoteClusterMsg.IsValid", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": "PayLoad"}, "", http.StatusBadRequest) + } + + return nil +} + +// RemoteClusterPing represents a ping that is sent and received between clusters +// to indicate a connection is alive. This is the payload for a `RemoteClusterMsg`. +type RemoteClusterPing struct { + SentAt int64 `json:"sent_at"` + RecvAt int64 `json:"recv_at"` +} + +// RemoteClusterInvite represents an invitation to establish a simple trust with a remote cluster. +type RemoteClusterInvite struct { + RemoteId string `json:"remote_id"` + RemoteTeamId string `json:"remote_team_id"` + SiteURL string `json:"site_url"` + Token string `json:"token"` +} + +func (rci *RemoteClusterInvite) Encrypt(password string) ([]byte, error) { + raw, err := json.Marshal(&rci) + if err != nil { + return nil, err + } + + // create random salt to be prepended to the blob. + salt := make([]byte, 16) + if _, err = io.ReadFull(rand.Reader, salt); err != nil { + return nil, err + } + + key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key[:]) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // create random nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + // prefix the nonce to the cyphertext so we don't need to keep track of it. + sealed := gcm.Seal(nonce, nonce, raw, nil) + + return append(salt, sealed...), nil +} + +func (rci *RemoteClusterInvite) Decrypt(encrypted []byte, password string) error { + if len(encrypted) <= 16 { + return errors.New("invalid length") + } + + // first 16 bytes is the salt that was used to derive a key + salt := encrypted[:16] + encrypted = encrypted[16:] + + key, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) + if err != nil { + return err + } + + block, err := aes.NewCipher(key[:]) + if err != nil { + return err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + + // nonce was prefixed to the cyphertext when encrypting so we need to extract it. + nonceSize := gcm.NonceSize() + nonce, cyphertext := encrypted[:nonceSize], encrypted[nonceSize:] + + plain, err := gcm.Open(nil, nonce, cyphertext, nil) + if err != nil { + return err + } + + // try to unmarshall the decrypted JSON to this invite struct. + return json.Unmarshal(plain, &rci) +} + +// RemoteClusterQueryFilter provides filter criteria for RemoteClusterStore.GetAll +type RemoteClusterQueryFilter struct { + ExcludeOffline bool + InChannel string + NotInChannel string + Topic string + CreatorId string + OnlyConfirmed bool +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/role.go b/vendor/github.com/mattermost/mattermost-server/v6/model/role.go new file mode 100644 index 00000000..68697838 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/role.go @@ -0,0 +1,939 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "strings" +) + +// SysconsoleAncillaryPermissions maps the non-sysconsole permissions required by each sysconsole view. +var SysconsoleAncillaryPermissions map[string][]*Permission +var SystemManagerDefaultPermissions []string +var SystemUserManagerDefaultPermissions []string +var SystemReadOnlyAdminDefaultPermissions []string + +var BuiltInSchemeManagedRoleIDs []string + +var NewSystemRoleIDs []string + +func init() { + NewSystemRoleIDs = []string{ + SystemUserManagerRoleId, + SystemReadOnlyAdminRoleId, + SystemManagerRoleId, + } + + BuiltInSchemeManagedRoleIDs = append([]string{ + SystemGuestRoleId, + SystemUserRoleId, + SystemAdminRoleId, + SystemPostAllRoleId, + SystemPostAllPublicRoleId, + SystemUserAccessTokenRoleId, + + TeamGuestRoleId, + TeamUserRoleId, + TeamAdminRoleId, + TeamPostAllRoleId, + TeamPostAllPublicRoleId, + + ChannelGuestRoleId, + ChannelUserRoleId, + ChannelAdminRoleId, + }, NewSystemRoleIDs...) + + // When updating the values here, the values in mattermost-redux must also be updated. + SysconsoleAncillaryPermissions = map[string][]*Permission{ + PermissionSysconsoleReadAboutEditionAndLicense.Id: { + PermissionReadLicenseInformation, + }, + PermissionSysconsoleWriteAboutEditionAndLicense.Id: { + PermissionManageLicenseInformation, + }, + PermissionSysconsoleReadUserManagementChannels.Id: { + PermissionReadPublicChannel, + PermissionReadChannel, + PermissionReadPublicChannelGroups, + PermissionReadPrivateChannelGroups, + }, + PermissionSysconsoleReadUserManagementUsers.Id: { + PermissionReadOtherUsersTeams, + PermissionGetAnalytics, + }, + PermissionSysconsoleReadUserManagementTeams.Id: { + PermissionListPrivateTeams, + PermissionListPublicTeams, + PermissionViewTeam, + }, + PermissionSysconsoleReadEnvironmentElasticsearch.Id: { + PermissionReadElasticsearchPostIndexingJob, + PermissionReadElasticsearchPostAggregationJob, + }, + PermissionSysconsoleWriteEnvironmentWebServer.Id: { + PermissionTestSiteURL, + PermissionReloadConfig, + PermissionInvalidateCaches, + }, + PermissionSysconsoleWriteEnvironmentDatabase.Id: { + PermissionRecycleDatabaseConnections, + }, + PermissionSysconsoleWriteEnvironmentElasticsearch.Id: { + PermissionTestElasticsearch, + PermissionCreateElasticsearchPostIndexingJob, + PermissionCreateElasticsearchPostAggregationJob, + PermissionPurgeElasticsearchIndexes, + }, + PermissionSysconsoleWriteEnvironmentFileStorage.Id: { + PermissionTestS3, + }, + PermissionSysconsoleWriteEnvironmentSMTP.Id: { + PermissionTestEmail, + }, + PermissionSysconsoleReadReportingServerLogs.Id: { + PermissionGetLogs, + }, + PermissionSysconsoleReadReportingSiteStatistics.Id: { + PermissionGetAnalytics, + }, + PermissionSysconsoleReadReportingTeamStatistics.Id: { + PermissionViewTeam, + }, + PermissionSysconsoleWriteUserManagementUsers.Id: { + PermissionEditOtherUsers, + PermissionDemoteToGuest, + PermissionPromoteGuest, + }, + PermissionSysconsoleWriteUserManagementChannels.Id: { + PermissionManageTeam, + PermissionManagePublicChannelProperties, + PermissionManagePrivateChannelProperties, + PermissionManagePrivateChannelMembers, + PermissionManagePublicChannelMembers, + PermissionDeletePrivateChannel, + PermissionDeletePublicChannel, + PermissionManageChannelRoles, + PermissionConvertPublicChannelToPrivate, + PermissionConvertPrivateChannelToPublic, + }, + PermissionSysconsoleWriteUserManagementTeams.Id: { + PermissionManageTeam, + PermissionManageTeamRoles, + PermissionRemoveUserFromTeam, + PermissionJoinPrivateTeams, + PermissionJoinPublicTeams, + PermissionAddUserToTeam, + }, + PermissionSysconsoleWriteUserManagementGroups.Id: { + PermissionManageTeam, + PermissionManagePrivateChannelMembers, + PermissionManagePublicChannelMembers, + PermissionConvertPublicChannelToPrivate, + PermissionConvertPrivateChannelToPublic, + }, + PermissionSysconsoleWriteSiteCustomization.Id: { + PermissionEditBrand, + }, + PermissionSysconsoleWriteComplianceDataRetentionPolicy.Id: { + PermissionCreateDataRetentionJob, + }, + PermissionSysconsoleReadComplianceDataRetentionPolicy.Id: { + PermissionReadDataRetentionJob, + }, + PermissionSysconsoleWriteComplianceComplianceExport.Id: { + PermissionCreateComplianceExportJob, + PermissionDownloadComplianceExportResult, + }, + PermissionSysconsoleReadComplianceComplianceExport.Id: { + PermissionReadComplianceExportJob, + PermissionDownloadComplianceExportResult, + }, + PermissionSysconsoleReadComplianceCustomTermsOfService.Id: { + PermissionReadAudits, + }, + PermissionSysconsoleWriteExperimentalBleve.Id: { + PermissionCreatePostBleveIndexesJob, + PermissionPurgeBleveIndexes, + }, + PermissionSysconsoleWriteAuthenticationLdap.Id: { + PermissionCreateLdapSyncJob, + PermissionAddLdapPublicCert, + PermissionRemoveLdapPublicCert, + PermissionAddLdapPrivateCert, + PermissionRemoveLdapPrivateCert, + }, + PermissionSysconsoleReadAuthenticationLdap.Id: { + PermissionTestLdap, + PermissionReadLdapSyncJob, + }, + PermissionSysconsoleWriteAuthenticationEmail.Id: { + PermissionInvalidateEmailInvite, + }, + PermissionSysconsoleWriteAuthenticationSaml.Id: { + PermissionGetSamlMetadataFromIdp, + PermissionAddSamlPublicCert, + PermissionAddSamlPrivateCert, + PermissionAddSamlIdpCert, + PermissionRemoveSamlPublicCert, + PermissionRemoveSamlPrivateCert, + PermissionRemoveSamlIdpCert, + PermissionGetSamlCertStatus, + }, + } + + SystemUserManagerDefaultPermissions = []string{ + PermissionSysconsoleReadUserManagementGroups.Id, + PermissionSysconsoleReadUserManagementTeams.Id, + PermissionSysconsoleReadUserManagementChannels.Id, + PermissionSysconsoleReadUserManagementPermissions.Id, + PermissionSysconsoleWriteUserManagementGroups.Id, + PermissionSysconsoleWriteUserManagementTeams.Id, + PermissionSysconsoleWriteUserManagementChannels.Id, + PermissionSysconsoleReadAuthenticationSignup.Id, + PermissionSysconsoleReadAuthenticationEmail.Id, + PermissionSysconsoleReadAuthenticationPassword.Id, + PermissionSysconsoleReadAuthenticationMfa.Id, + PermissionSysconsoleReadAuthenticationLdap.Id, + PermissionSysconsoleReadAuthenticationSaml.Id, + PermissionSysconsoleReadAuthenticationOpenid.Id, + PermissionSysconsoleReadAuthenticationGuestAccess.Id, + } + + SystemReadOnlyAdminDefaultPermissions = []string{ + PermissionSysconsoleReadAboutEditionAndLicense.Id, + PermissionSysconsoleReadReportingSiteStatistics.Id, + PermissionSysconsoleReadReportingTeamStatistics.Id, + PermissionSysconsoleReadReportingServerLogs.Id, + PermissionSysconsoleReadUserManagementUsers.Id, + PermissionSysconsoleReadUserManagementGroups.Id, + PermissionSysconsoleReadUserManagementTeams.Id, + PermissionSysconsoleReadUserManagementChannels.Id, + PermissionSysconsoleReadUserManagementPermissions.Id, + PermissionSysconsoleReadEnvironmentWebServer.Id, + PermissionSysconsoleReadEnvironmentDatabase.Id, + PermissionSysconsoleReadEnvironmentElasticsearch.Id, + PermissionSysconsoleReadEnvironmentFileStorage.Id, + PermissionSysconsoleReadEnvironmentImageProxy.Id, + PermissionSysconsoleReadEnvironmentSMTP.Id, + PermissionSysconsoleReadEnvironmentPushNotificationServer.Id, + PermissionSysconsoleReadEnvironmentHighAvailability.Id, + PermissionSysconsoleReadEnvironmentRateLimiting.Id, + PermissionSysconsoleReadEnvironmentLogging.Id, + PermissionSysconsoleReadEnvironmentSessionLengths.Id, + PermissionSysconsoleReadEnvironmentPerformanceMonitoring.Id, + PermissionSysconsoleReadEnvironmentDeveloper.Id, + PermissionSysconsoleReadSiteCustomization.Id, + PermissionSysconsoleReadSiteLocalization.Id, + PermissionSysconsoleReadSiteUsersAndTeams.Id, + PermissionSysconsoleReadSiteNotifications.Id, + PermissionSysconsoleReadSiteAnnouncementBanner.Id, + PermissionSysconsoleReadSiteEmoji.Id, + PermissionSysconsoleReadSitePosts.Id, + PermissionSysconsoleReadSiteFileSharingAndDownloads.Id, + PermissionSysconsoleReadSitePublicLinks.Id, + PermissionSysconsoleReadSiteNotices.Id, + PermissionSysconsoleReadAuthenticationSignup.Id, + PermissionSysconsoleReadAuthenticationEmail.Id, + PermissionSysconsoleReadAuthenticationPassword.Id, + PermissionSysconsoleReadAuthenticationMfa.Id, + PermissionSysconsoleReadAuthenticationLdap.Id, + PermissionSysconsoleReadAuthenticationSaml.Id, + PermissionSysconsoleReadAuthenticationOpenid.Id, + PermissionSysconsoleReadAuthenticationGuestAccess.Id, + PermissionSysconsoleReadPlugins.Id, + PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, + PermissionSysconsoleReadIntegrationsBotAccounts.Id, + PermissionSysconsoleReadIntegrationsGif.Id, + PermissionSysconsoleReadIntegrationsCors.Id, + PermissionSysconsoleReadComplianceDataRetentionPolicy.Id, + PermissionSysconsoleReadComplianceComplianceExport.Id, + PermissionSysconsoleReadComplianceComplianceMonitoring.Id, + PermissionSysconsoleReadComplianceCustomTermsOfService.Id, + PermissionSysconsoleReadExperimentalFeatures.Id, + PermissionSysconsoleReadExperimentalFeatureFlags.Id, + PermissionSysconsoleReadExperimentalBleve.Id, + } + + SystemManagerDefaultPermissions = []string{ + PermissionSysconsoleReadAboutEditionAndLicense.Id, + PermissionSysconsoleReadReportingSiteStatistics.Id, + PermissionSysconsoleReadReportingTeamStatistics.Id, + PermissionSysconsoleReadReportingServerLogs.Id, + PermissionSysconsoleReadUserManagementGroups.Id, + PermissionSysconsoleReadUserManagementTeams.Id, + PermissionSysconsoleReadUserManagementChannels.Id, + PermissionSysconsoleReadUserManagementPermissions.Id, + PermissionSysconsoleWriteUserManagementGroups.Id, + PermissionSysconsoleWriteUserManagementTeams.Id, + PermissionSysconsoleWriteUserManagementChannels.Id, + PermissionSysconsoleWriteUserManagementPermissions.Id, + PermissionSysconsoleReadEnvironmentWebServer.Id, + PermissionSysconsoleReadEnvironmentDatabase.Id, + PermissionSysconsoleReadEnvironmentElasticsearch.Id, + PermissionSysconsoleReadEnvironmentFileStorage.Id, + PermissionSysconsoleReadEnvironmentImageProxy.Id, + PermissionSysconsoleReadEnvironmentSMTP.Id, + PermissionSysconsoleReadEnvironmentPushNotificationServer.Id, + PermissionSysconsoleReadEnvironmentHighAvailability.Id, + PermissionSysconsoleReadEnvironmentRateLimiting.Id, + PermissionSysconsoleReadEnvironmentLogging.Id, + PermissionSysconsoleReadEnvironmentSessionLengths.Id, + PermissionSysconsoleReadEnvironmentPerformanceMonitoring.Id, + PermissionSysconsoleReadEnvironmentDeveloper.Id, + PermissionSysconsoleWriteEnvironmentWebServer.Id, + PermissionSysconsoleWriteEnvironmentDatabase.Id, + PermissionSysconsoleWriteEnvironmentElasticsearch.Id, + PermissionSysconsoleWriteEnvironmentFileStorage.Id, + PermissionSysconsoleWriteEnvironmentImageProxy.Id, + PermissionSysconsoleWriteEnvironmentSMTP.Id, + PermissionSysconsoleWriteEnvironmentPushNotificationServer.Id, + PermissionSysconsoleWriteEnvironmentHighAvailability.Id, + PermissionSysconsoleWriteEnvironmentRateLimiting.Id, + PermissionSysconsoleWriteEnvironmentLogging.Id, + PermissionSysconsoleWriteEnvironmentSessionLengths.Id, + PermissionSysconsoleWriteEnvironmentPerformanceMonitoring.Id, + PermissionSysconsoleWriteEnvironmentDeveloper.Id, + PermissionSysconsoleReadSiteCustomization.Id, + PermissionSysconsoleWriteSiteCustomization.Id, + PermissionSysconsoleReadSiteLocalization.Id, + PermissionSysconsoleWriteSiteLocalization.Id, + PermissionSysconsoleReadSiteUsersAndTeams.Id, + PermissionSysconsoleWriteSiteUsersAndTeams.Id, + PermissionSysconsoleReadSiteNotifications.Id, + PermissionSysconsoleWriteSiteNotifications.Id, + PermissionSysconsoleReadSiteAnnouncementBanner.Id, + PermissionSysconsoleWriteSiteAnnouncementBanner.Id, + PermissionSysconsoleReadSiteEmoji.Id, + PermissionSysconsoleWriteSiteEmoji.Id, + PermissionSysconsoleReadSitePosts.Id, + PermissionSysconsoleWriteSitePosts.Id, + PermissionSysconsoleReadSiteFileSharingAndDownloads.Id, + PermissionSysconsoleWriteSiteFileSharingAndDownloads.Id, + PermissionSysconsoleReadSitePublicLinks.Id, + PermissionSysconsoleWriteSitePublicLinks.Id, + PermissionSysconsoleReadSiteNotices.Id, + PermissionSysconsoleWriteSiteNotices.Id, + PermissionSysconsoleReadAuthenticationSignup.Id, + PermissionSysconsoleReadAuthenticationEmail.Id, + PermissionSysconsoleReadAuthenticationPassword.Id, + PermissionSysconsoleReadAuthenticationMfa.Id, + PermissionSysconsoleReadAuthenticationLdap.Id, + PermissionSysconsoleReadAuthenticationSaml.Id, + PermissionSysconsoleReadAuthenticationOpenid.Id, + PermissionSysconsoleReadAuthenticationGuestAccess.Id, + PermissionSysconsoleReadPlugins.Id, + PermissionSysconsoleReadIntegrationsIntegrationManagement.Id, + PermissionSysconsoleReadIntegrationsBotAccounts.Id, + PermissionSysconsoleReadIntegrationsGif.Id, + PermissionSysconsoleReadIntegrationsCors.Id, + PermissionSysconsoleWriteIntegrationsIntegrationManagement.Id, + PermissionSysconsoleWriteIntegrationsBotAccounts.Id, + PermissionSysconsoleWriteIntegrationsGif.Id, + PermissionSysconsoleWriteIntegrationsCors.Id, + } + + // Add the ancillary permissions to each system role + SystemUserManagerDefaultPermissions = AddAncillaryPermissions(SystemUserManagerDefaultPermissions) + SystemReadOnlyAdminDefaultPermissions = AddAncillaryPermissions(SystemReadOnlyAdminDefaultPermissions) + SystemManagerDefaultPermissions = AddAncillaryPermissions(SystemManagerDefaultPermissions) +} + +type RoleType string +type RoleScope string + +const ( + SystemGuestRoleId = "system_guest" + SystemUserRoleId = "system_user" + SystemAdminRoleId = "system_admin" + SystemPostAllRoleId = "system_post_all" + SystemPostAllPublicRoleId = "system_post_all_public" + SystemUserAccessTokenRoleId = "system_user_access_token" + SystemUserManagerRoleId = "system_user_manager" + SystemReadOnlyAdminRoleId = "system_read_only_admin" + SystemManagerRoleId = "system_manager" + + TeamGuestRoleId = "team_guest" + TeamUserRoleId = "team_user" + TeamAdminRoleId = "team_admin" + TeamPostAllRoleId = "team_post_all" + TeamPostAllPublicRoleId = "team_post_all_public" + + ChannelGuestRoleId = "channel_guest" + ChannelUserRoleId = "channel_user" + ChannelAdminRoleId = "channel_admin" + + RoleNameMaxLength = 64 + RoleDisplayNameMaxLength = 128 + RoleDescriptionMaxLength = 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) 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 AllPermissions { + if cp.Scope != PermissionScopeChannel { + continue + } + + _, presentOnHigherScope := higherScopedPermissionsMap[cp.Id] + + // For the channel admin role always look to the higher scope to determine if the role has their 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 == ChannelAdminRoleId && presentOnHigherScope { + mergedPermissions = append(mergedPermissions, cp.Id) + continue + } + + _, permissionIsModerated := ChannelModeratedPermissionsMap[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 := ChannelModeratedPermissionsMap[permission]; found { + roleMap[channelModeratedPermissionName] = true + } + } + + for _, permission := range *patch.Permissions { + if channelModeratedPermissionName, found := ChannelModeratedPermissionsMap[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 ChannelType) map[string]bool { + moderatedPermissions := make(map[string]bool) + for _, permission := range r.Permissions { + if _, found := ChannelModeratedPermissionsMap[permission]; !found { + continue + } + + for moderated, moderatedPermissionValue := range ChannelModeratedPermissionsMap { + // 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 == PermissionManagePublicChannelMembers.Id || moderated == PermissionManagePrivateChannelMembers.Id { + canManagePublic := channelType == ChannelTypeOpen && moderated == PermissionManagePublicChannelMembers.Id + canManagePrivate := channelType == ChannelTypePrivate && moderated == PermissionManagePrivateChannelMembers.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 := ChannelModeratedPermissionsMap[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 == ChannelModeratedPermissionsMap[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 ChannelModeratedPermissionsMap { + 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 r.DisplayName == "" || len(r.DisplayName) > RoleDisplayNameMaxLength { + return false + } + + if len(r.Description) > RoleDescriptionMaxLength { + return false + } + + check := func(perms []*Permission, permission string) bool { + for _, p := range perms { + if permission == p.Id { + return true + } + } + return false + } + for _, permission := range r.Permissions { + permissionValidated := check(AllPermissions, permission) || check(DeprecatedPermissions, permission) + if !permissionValidated { + return false + } + } + + return true +} + +func CleanRoleNames(roleNames []string) ([]string, bool) { + var cleanedRoleNames []string + for _, roleName := range roleNames { + if strings.TrimSpace(roleName) == "" { + continue + } + + if !IsValidRoleName(roleName) { + return roleNames, false + } + + cleanedRoleNames = append(cleanedRoleNames, roleName) + } + + return cleanedRoleNames, true +} + +func IsValidRoleName(roleName string) bool { + if roleName == "" || len(roleName) > RoleNameMaxLength { + return false + } + + if strings.TrimLeft(roleName, "abcdefghijklmnopqrstuvwxyz0123456789_") != "" { + return false + } + + return true +} + +func MakeDefaultRoles() map[string]*Role { + roles := make(map[string]*Role) + + roles[ChannelGuestRoleId] = &Role{ + Name: "channel_guest", + DisplayName: "authentication.roles.channel_guest.name", + Description: "authentication.roles.channel_guest.description", + Permissions: []string{ + PermissionReadChannel.Id, + PermissionAddReaction.Id, + PermissionRemoveReaction.Id, + PermissionUploadFile.Id, + PermissionEditPost.Id, + PermissionCreatePost.Id, + PermissionUseChannelMentions.Id, + PermissionUseSlashCommands.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[ChannelUserRoleId] = &Role{ + Name: "channel_user", + DisplayName: "authentication.roles.channel_user.name", + Description: "authentication.roles.channel_user.description", + Permissions: []string{ + PermissionReadChannel.Id, + PermissionAddReaction.Id, + PermissionRemoveReaction.Id, + PermissionManagePublicChannelMembers.Id, + PermissionUploadFile.Id, + PermissionGetPublicLink.Id, + PermissionCreatePost.Id, + PermissionUseChannelMentions.Id, + PermissionUseSlashCommands.Id, + PermissionManagePublicChannelProperties.Id, + PermissionDeletePublicChannel.Id, + PermissionManagePrivateChannelProperties.Id, + PermissionDeletePrivateChannel.Id, + PermissionManagePrivateChannelMembers.Id, + PermissionDeletePost.Id, + PermissionEditPost.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[ChannelAdminRoleId] = &Role{ + Name: "channel_admin", + DisplayName: "authentication.roles.channel_admin.name", + Description: "authentication.roles.channel_admin.description", + Permissions: []string{ + PermissionManageChannelRoles.Id, + PermissionUseGroupMentions.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[TeamGuestRoleId] = &Role{ + Name: "team_guest", + DisplayName: "authentication.roles.team_guest.name", + Description: "authentication.roles.team_guest.description", + Permissions: []string{ + PermissionViewTeam.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[TeamUserRoleId] = &Role{ + Name: "team_user", + DisplayName: "authentication.roles.team_user.name", + Description: "authentication.roles.team_user.description", + Permissions: []string{ + PermissionListTeamChannels.Id, + PermissionJoinPublicChannels.Id, + PermissionReadPublicChannel.Id, + PermissionViewTeam.Id, + PermissionCreatePublicChannel.Id, + PermissionCreatePrivateChannel.Id, + PermissionInviteUser.Id, + PermissionAddUserToTeam.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[TeamPostAllRoleId] = &Role{ + Name: "team_post_all", + DisplayName: "authentication.roles.team_post_all.name", + Description: "authentication.roles.team_post_all.description", + Permissions: []string{ + PermissionCreatePost.Id, + PermissionUseChannelMentions.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[TeamPostAllPublicRoleId] = &Role{ + Name: "team_post_all_public", + DisplayName: "authentication.roles.team_post_all_public.name", + Description: "authentication.roles.team_post_all_public.description", + Permissions: []string{ + PermissionCreatePostPublic.Id, + PermissionUseChannelMentions.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[TeamAdminRoleId] = &Role{ + Name: "team_admin", + DisplayName: "authentication.roles.team_admin.name", + Description: "authentication.roles.team_admin.description", + Permissions: []string{ + PermissionRemoveUserFromTeam.Id, + PermissionManageTeam.Id, + PermissionImportTeam.Id, + PermissionManageTeamRoles.Id, + PermissionManageChannelRoles.Id, + PermissionManageOthersIncomingWebhooks.Id, + PermissionManageOthersOutgoingWebhooks.Id, + PermissionManageSlashCommands.Id, + PermissionManageOthersSlashCommands.Id, + PermissionManageIncomingWebhooks.Id, + PermissionManageOutgoingWebhooks.Id, + PermissionConvertPublicChannelToPrivate.Id, + PermissionConvertPrivateChannelToPublic.Id, + PermissionDeletePost.Id, + PermissionDeleteOthersPosts.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[SystemGuestRoleId] = &Role{ + Name: "system_guest", + DisplayName: "authentication.roles.global_guest.name", + Description: "authentication.roles.global_guest.description", + Permissions: []string{ + PermissionCreateDirectChannel.Id, + PermissionCreateGroupChannel.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[SystemUserRoleId] = &Role{ + Name: "system_user", + DisplayName: "authentication.roles.global_user.name", + Description: "authentication.roles.global_user.description", + Permissions: []string{ + PermissionListPublicTeams.Id, + PermissionJoinPublicTeams.Id, + PermissionCreateDirectChannel.Id, + PermissionCreateGroupChannel.Id, + PermissionViewMembers.Id, + PermissionCreateTeam.Id, + }, + SchemeManaged: true, + BuiltIn: true, + } + + roles[SystemPostAllRoleId] = &Role{ + Name: "system_post_all", + DisplayName: "authentication.roles.system_post_all.name", + Description: "authentication.roles.system_post_all.description", + Permissions: []string{ + PermissionCreatePost.Id, + PermissionUseChannelMentions.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SystemPostAllPublicRoleId] = &Role{ + Name: "system_post_all_public", + DisplayName: "authentication.roles.system_post_all_public.name", + Description: "authentication.roles.system_post_all_public.description", + Permissions: []string{ + PermissionCreatePostPublic.Id, + PermissionUseChannelMentions.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SystemUserAccessTokenRoleId] = &Role{ + Name: "system_user_access_token", + DisplayName: "authentication.roles.system_user_access_token.name", + Description: "authentication.roles.system_user_access_token.description", + Permissions: []string{ + PermissionCreateUserAccessToken.Id, + PermissionReadUserAccessToken.Id, + PermissionRevokeUserAccessToken.Id, + }, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SystemUserManagerRoleId] = &Role{ + Name: "system_user_manager", + DisplayName: "authentication.roles.system_user_manager.name", + Description: "authentication.roles.system_user_manager.description", + Permissions: SystemUserManagerDefaultPermissions, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SystemReadOnlyAdminRoleId] = &Role{ + Name: "system_read_only_admin", + DisplayName: "authentication.roles.system_read_only_admin.name", + Description: "authentication.roles.system_read_only_admin.description", + Permissions: SystemReadOnlyAdminDefaultPermissions, + SchemeManaged: false, + BuiltIn: true, + } + + roles[SystemManagerRoleId] = &Role{ + Name: "system_manager", + DisplayName: "authentication.roles.system_manager.name", + Description: "authentication.roles.system_manager.description", + Permissions: SystemManagerDefaultPermissions, + SchemeManaged: false, + BuiltIn: true, + } + + allPermissionIDs := []string{} + for _, permission := range AllPermissions { + allPermissionIDs = append(allPermissionIDs, permission.Id) + } + + roles[SystemAdminRoleId] = &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: allPermissionIDs, + SchemeManaged: true, + BuiltIn: true, + } + + return roles +} + +func AddAncillaryPermissions(permissions []string) []string { + for _, permission := range permissions { + if ancillaryPermissions, ok := SysconsoleAncillaryPermissions[permission]; ok { + for _, ancillaryPermission := range ancillaryPermissions { + permissions = append(permissions, ancillaryPermission.Id) + } + } + } + return permissions +} + +func asStringBoolMap(list []string) map[string]bool { + listMap := make(map[string]bool, len(list)) + for _, p := range list { + listMap[p] = true + } + return listMap +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/saml.go b/vendor/github.com/mattermost/mattermost-server/v6/model/saml.go new file mode 100644 index 00000000..e9e987d8 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/saml.go @@ -0,0 +1,176 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/xml" + "time" +) + +const ( + UserAuthServiceSaml = "saml" + UserAuthServiceSamlText = "SAML" + UserAuthServiceIsSaml = "isSaml" + UserAuthServiceIsMobile = "isMobile" + UserAuthServiceIsOAuth = "isOAuthUser" +) + +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"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/scheduled_task.go b/vendor/github.com/mattermost/mattermost-server/v6/model/scheduled_task.go new file mode 100644 index 00000000..cf20db63 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/scheduled_task.go @@ -0,0 +1,100 @@ +// 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{} + fromNextIntervalTime bool +} + +func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask { + return createTask(name, function, timeToExecution, false, false) +} + +func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask { + return createTask(name, function, interval, true, false) +} + +func CreateRecurringTaskFromNextIntervalTime(name string, function TaskFunc, interval time.Duration) *ScheduledTask { + return createTask(name, function, interval, true, true) +} + +func createTask(name string, function TaskFunc, interval time.Duration, recurring bool, fromNextIntervalTime bool) *ScheduledTask { + task := &ScheduledTask{ + Name: name, + Interval: interval, + Recurring: recurring, + function: function, + cancel: make(chan struct{}), + cancelled: make(chan struct{}), + fromNextIntervalTime: fromNextIntervalTime, + } + + go func() { + defer close(task.cancelled) + + var firstTick <-chan time.Time + var ticker *time.Ticker + + if task.fromNextIntervalTime { + currTime := time.Now() + first := currTime.Truncate(interval) + if first.Before(currTime) { + first = first.Add(interval) + } + firstTick = time.After(time.Until(first)) + ticker = &time.Ticker{C: nil} + } else { + firstTick = nil + ticker = time.NewTicker(interval) + } + defer func() { + ticker.Stop() + }() + + for { + select { + case <-firstTick: + ticker = time.NewTicker(interval) + function() + 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/v6/model/scheme.go b/vendor/github.com/mattermost/mattermost-server/v6/model/scheme.go new file mode 100644 index 00000000..c861c1e6 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/scheme.go @@ -0,0 +1,167 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "regexp" +) + +const ( + SchemeDisplayNameMaxLength = 128 + SchemeNameMaxLength = 64 + SchemeDescriptionMaxLength = 1024 + SchemeScopeTeam = "team" + SchemeScopeChannel = "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) IsValid() bool { + if !IsValidId(scheme.Id) { + return false + } + + return scheme.IsValidForCreate() +} + +func (scheme *Scheme) IsValidForCreate() bool { + if scheme.DisplayName == "" || len(scheme.DisplayName) > SchemeDisplayNameMaxLength { + return false + } + + if !IsValidSchemeName(scheme.Name) { + return false + } + + if len(scheme.Description) > SchemeDescriptionMaxLength { + return false + } + + switch scheme.Scope { + case SchemeScopeTeam, SchemeScopeChannel: + default: + return false + } + + if !IsValidRoleName(scheme.DefaultChannelAdminRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultChannelUserRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultChannelGuestRole) { + return false + } + + if scheme.Scope == SchemeScopeTeam { + if !IsValidRoleName(scheme.DefaultTeamAdminRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultTeamUserRole) { + return false + } + + if !IsValidRoleName(scheme.DefaultTeamGuestRole) { + return false + } + } + + if scheme.Scope == SchemeScopeChannel { + if scheme.DefaultTeamAdminRole != "" { + return false + } + + if scheme.DefaultTeamUserRole != "" { + return false + } + + if scheme.DefaultTeamGuestRole != "" { + 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 IsValidSchemeName(name string) bool { + re := regexp.MustCompile(fmt.Sprintf("^[a-z0-9_]{2,%d}$", SchemeNameMaxLength)) + return re.MatchString(name) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/search_params.go b/vendor/github.com/mattermost/mattermost-server/v6/model/search_params.go new file mode 100644 index 00000000..41a2db2a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/search_params.go @@ -0,0 +1,397 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "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 + Extensions []string + ExcludedExtensions []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", "ext"} + +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 word != "" { + 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 := "" + excludedExtensions := []string{} + extensions := []string{} + + 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 + } + } else if flag.name == "ext" { + if flag.exclude { + excludedExtensions = append(excludedExtensions, flag.value) + } else { + extensions = append(extensions, flag.value) + } + } + } + + paramsList := []*SearchParams{} + + if plainTerms != "" || excludedPlainTerms != "" { + 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, + Extensions: extensions, + ExcludedExtensions: excludedExtensions, + OnDate: onDate, + ExcludedDate: excludedDate, + TimeZoneOffset: timeZoneOffset, + }) + } + + if hashtagTerms != "" || excludedHashtagTerms != "" { + 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, + Extensions: extensions, + ExcludedExtensions: excludedExtensions, + OnDate: onDate, + ExcludedDate: excludedDate, + TimeZoneOffset: timeZoneOffset, + }) + } + + // special case for when no terms are specified but we still have a filter + if plainTerms == "" && hashtagTerms == "" && + excludedPlainTerms == "" && excludedHashtagTerms == "" && + (len(inChannels) != 0 || len(fromUsers) != 0 || + len(excludedChannels) != 0 || len(excludedUsers) != 0 || + len(extensions) != 0 || len(excludedExtensions) != 0 || + afterDate != "" || excludedAfterDate != "" || + beforeDate != "" || excludedBeforeDate != "" || + onDate != "" || excludedDate != "") { + paramsList = append(paramsList, &SearchParams{ + Terms: "", + ExcludedTerms: "", + IsHashtag: false, + InChannels: inChannels, + ExcludedChannels: excludedChannels, + FromUsers: fromUsers, + ExcludedUsers: excludedUsers, + AfterDate: afterDate, + ExcludedAfterDate: excludedAfterDate, + BeforeDate: beforeDate, + ExcludedBeforeDate: excludedBeforeDate, + Extensions: extensions, + ExcludedExtensions: excludedExtensions, + OnDate: onDate, + ExcludedDate: excludedDate, + TimeZoneOffset: timeZoneOffset, + }) + } + + return paramsList +} + +func IsSearchParamsListValid(paramsList []*SearchParams) *AppError { + // All SearchParams should have same IncludeDeletedChannels value. + for _, params := range paramsList { + if params.IncludeDeletedChannels != paramsList[0].IncludeDeletedChannels { + return NewAppError("IsSearchParamsListValid", "model.search_params_list.is_valid.include_deleted_channels.app_error", nil, "", http.StatusInternalServerError) + } + } + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/security_bulletin.go b/vendor/github.com/mattermost/mattermost-server/v6/model/security_bulletin.go new file mode 100644 index 00000000..fa5662cf --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/security_bulletin.go @@ -0,0 +1,11 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type SecurityBulletin struct { + Id string `json:"id"` + AppliesToVersion string `json:"applies_to_version"` +} + +type SecurityBulletins []SecurityBulletin diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/session.go b/vendor/github.com/mattermost/mattermost-server/v6/model/session.go new file mode 100644 index 00000000..d3bbc6e4 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/session.go @@ -0,0 +1,197 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "strconv" + "strings" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +const ( + SessionCookieToken = "MMAUTHTOKEN" + SessionCookieUser = "MMUSERID" + SessionCookieCsrf = "MMCSRF" + SessionCacheSize = 35000 + SessionPropPlatform = "platform" + SessionPropOs = "os" + SessionPropBrowser = "browser" + SessionPropType = "type" + SessionPropUserAccessTokenId = "user_access_token_id" + SessionPropIsBot = "is_bot" + SessionPropIsBotValue = "true" + SessionTypeUserAccessToken = "UserAccessToken" + SessionTypeCloudKey = "CloudKey" + SessionTypeRemoteclusterToken = "RemoteClusterToken" + SessionPropIsGuest = "is_guest" + SessionActivityTimeout = 1000 * 60 * 5 // 5 minutes + SessionUserAccessTokenExpiry = 100 * 365 // 100 years +) + +//msgp StringMap +type StringMap map[string]string + +//msgp:tuple Session + +// Session contains the user session details. +// This struct's serializer methods are auto-generated. If a new field is added/removed, +// please run make gen-serialized. +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"` + ExpiredNotify bool `json:"expired_notify"` + 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 (s *Session) IsUnrestricted() bool { + return s.Local +} + +func (s *Session) DeepCopy() *Session { + copySession := *s + + if s.Props != nil { + copySession.Props = CopyStringMap(s.Props) + } + + if s.TeamMembers != nil { + copySession.TeamMembers = make([]*TeamMember, len(s.TeamMembers)) + for index, tm := range s.TeamMembers { + copySession.TeamMembers[index] = new(TeamMember) + *copySession.TeamMembers[index] = *tm + } + } + + return ©Session +} + +func (s *Session) PreSave() { + if s.Id == "" { + s.Id = NewId() + } + + if s.Token == "" { + s.Token = NewId() + } + + s.CreateAt = GetMillis() + s.LastActivityAt = s.CreateAt + + if s.Props == nil { + s.Props = make(map[string]string) + } +} + +func (s *Session) Sanitize() { + s.Token = "" +} + +func (s *Session) IsExpired() bool { + + if s.ExpiresAt <= 0 { + return false + } + + if GetMillis() > s.ExpiresAt { + return true + } + + return false +} + +func (s *Session) AddProp(key string, value string) { + + if s.Props == nil { + s.Props = make(map[string]string) + } + + s.Props[key] = value +} + +func (s *Session) GetTeamByTeamId(teamId string) *TeamMember { + for _, team := range s.TeamMembers { + if team.TeamId == teamId { + return team + } + } + + return nil +} + +func (s *Session) IsMobileApp() bool { + return s.DeviceId != "" || s.IsMobile() +} + +func (s *Session) IsMobile() bool { + val, ok := s.Props[UserAuthServiceIsMobile] + if !ok { + return false + } + isMobile, err := strconv.ParseBool(val) + if err != nil { + mlog.Debug("Error parsing boolean property from Session", mlog.Err(err)) + return false + } + return isMobile +} + +func (s *Session) IsSaml() bool { + val, ok := s.Props[UserAuthServiceIsSaml] + if !ok { + return false + } + isSaml, err := strconv.ParseBool(val) + if err != nil { + mlog.Debug("Error parsing boolean property from Session", mlog.Err(err)) + return false + } + return isSaml +} + +func (s *Session) IsOAuthUser() bool { + val, ok := s.Props[UserAuthServiceIsOAuth] + if !ok { + return false + } + isOAuthUser, err := strconv.ParseBool(val) + if err != nil { + mlog.Debug("Error parsing boolean property from Session", mlog.Err(err)) + return false + } + return isOAuthUser +} + +func (s *Session) IsSSOLogin() bool { + return s.IsOAuthUser() || s.IsSaml() +} + +func (s *Session) GetUserRoles() []string { + return strings.Fields(s.Roles) +} + +func (s *Session) GenerateCSRF() string { + token := NewId() + s.AddProp("csrf", token) + return token +} + +func (s *Session) GetCSRF() string { + if s.Props == nil { + return "" + } + + return s.Props["csrf"] +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/session_serial_gen.go b/vendor/github.com/mattermost/mattermost-server/v6/model/session_serial_gen.go new file mode 100644 index 00000000..612bbb89 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/session_serial_gen.go @@ -0,0 +1,540 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *Session) DecodeMsg(dc *msgp.Reader) (err error) { + var zb0001 uint32 + zb0001, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 != 13 { + err = msgp.ArrayError{Wanted: 13, Got: zb0001} + return + } + z.Id, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Id") + return + } + z.Token, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Token") + return + } + z.CreateAt, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "CreateAt") + return + } + z.ExpiresAt, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "ExpiresAt") + return + } + z.LastActivityAt, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "LastActivityAt") + return + } + z.UserId, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "UserId") + return + } + z.DeviceId, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "DeviceId") + return + } + z.Roles, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Roles") + return + } + z.IsOAuth, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "IsOAuth") + return + } + z.ExpiredNotify, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "ExpiredNotify") + return + } + var zb0002 uint32 + zb0002, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + if z.Props == nil { + z.Props = make(StringMap, zb0002) + } else if len(z.Props) > 0 { + for key := range z.Props { + delete(z.Props, key) + } + } + for zb0002 > 0 { + zb0002-- + var za0001 string + var za0002 string + za0001, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + za0002, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Props", za0001) + return + } + z.Props[za0001] = za0002 + } + var zb0003 uint32 + zb0003, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "TeamMembers") + return + } + if cap(z.TeamMembers) >= int(zb0003) { + z.TeamMembers = (z.TeamMembers)[:zb0003] + } else { + z.TeamMembers = make([]*TeamMember, zb0003) + } + for za0003 := range z.TeamMembers { + if dc.IsNil() { + err = dc.ReadNil() + if err != nil { + err = msgp.WrapError(err, "TeamMembers", za0003) + return + } + z.TeamMembers[za0003] = nil + } else { + if z.TeamMembers[za0003] == nil { + z.TeamMembers[za0003] = new(TeamMember) + } + err = z.TeamMembers[za0003].DecodeMsg(dc) + if err != nil { + err = msgp.WrapError(err, "TeamMembers", za0003) + return + } + } + } + z.Local, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "Local") + return + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *Session) EncodeMsg(en *msgp.Writer) (err error) { + // array header, size 13 + err = en.Append(0x9d) + if err != nil { + return + } + err = en.WriteString(z.Id) + if err != nil { + err = msgp.WrapError(err, "Id") + return + } + err = en.WriteString(z.Token) + if err != nil { + err = msgp.WrapError(err, "Token") + return + } + err = en.WriteInt64(z.CreateAt) + if err != nil { + err = msgp.WrapError(err, "CreateAt") + return + } + err = en.WriteInt64(z.ExpiresAt) + if err != nil { + err = msgp.WrapError(err, "ExpiresAt") + return + } + err = en.WriteInt64(z.LastActivityAt) + if err != nil { + err = msgp.WrapError(err, "LastActivityAt") + return + } + err = en.WriteString(z.UserId) + if err != nil { + err = msgp.WrapError(err, "UserId") + return + } + err = en.WriteString(z.DeviceId) + if err != nil { + err = msgp.WrapError(err, "DeviceId") + return + } + err = en.WriteString(z.Roles) + if err != nil { + err = msgp.WrapError(err, "Roles") + return + } + err = en.WriteBool(z.IsOAuth) + if err != nil { + err = msgp.WrapError(err, "IsOAuth") + return + } + err = en.WriteBool(z.ExpiredNotify) + if err != nil { + err = msgp.WrapError(err, "ExpiredNotify") + return + } + err = en.WriteMapHeader(uint32(len(z.Props))) + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + for za0001, za0002 := range z.Props { + err = en.WriteString(za0001) + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + err = en.WriteString(za0002) + if err != nil { + err = msgp.WrapError(err, "Props", za0001) + return + } + } + err = en.WriteArrayHeader(uint32(len(z.TeamMembers))) + if err != nil { + err = msgp.WrapError(err, "TeamMembers") + return + } + for za0003 := range z.TeamMembers { + if z.TeamMembers[za0003] == nil { + err = en.WriteNil() + if err != nil { + return + } + } else { + err = z.TeamMembers[za0003].EncodeMsg(en) + if err != nil { + err = msgp.WrapError(err, "TeamMembers", za0003) + return + } + } + } + err = en.WriteBool(z.Local) + if err != nil { + err = msgp.WrapError(err, "Local") + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z *Session) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // array header, size 13 + o = append(o, 0x9d) + o = msgp.AppendString(o, z.Id) + o = msgp.AppendString(o, z.Token) + o = msgp.AppendInt64(o, z.CreateAt) + o = msgp.AppendInt64(o, z.ExpiresAt) + o = msgp.AppendInt64(o, z.LastActivityAt) + o = msgp.AppendString(o, z.UserId) + o = msgp.AppendString(o, z.DeviceId) + o = msgp.AppendString(o, z.Roles) + o = msgp.AppendBool(o, z.IsOAuth) + o = msgp.AppendBool(o, z.ExpiredNotify) + o = msgp.AppendMapHeader(o, uint32(len(z.Props))) + for za0001, za0002 := range z.Props { + o = msgp.AppendString(o, za0001) + o = msgp.AppendString(o, za0002) + } + o = msgp.AppendArrayHeader(o, uint32(len(z.TeamMembers))) + for za0003 := range z.TeamMembers { + if z.TeamMembers[za0003] == nil { + o = msgp.AppendNil(o) + } else { + o, err = z.TeamMembers[za0003].MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "TeamMembers", za0003) + return + } + } + } + o = msgp.AppendBool(o, z.Local) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *Session) UnmarshalMsg(bts []byte) (o []byte, err error) { + var zb0001 uint32 + zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 != 13 { + err = msgp.ArrayError{Wanted: 13, Got: zb0001} + return + } + z.Id, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Id") + return + } + z.Token, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Token") + return + } + z.CreateAt, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "CreateAt") + return + } + z.ExpiresAt, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "ExpiresAt") + return + } + z.LastActivityAt, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LastActivityAt") + return + } + z.UserId, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "UserId") + return + } + z.DeviceId, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "DeviceId") + return + } + z.Roles, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Roles") + return + } + z.IsOAuth, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "IsOAuth") + return + } + z.ExpiredNotify, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ExpiredNotify") + return + } + var zb0002 uint32 + zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + if z.Props == nil { + z.Props = make(StringMap, zb0002) + } else if len(z.Props) > 0 { + for key := range z.Props { + delete(z.Props, key) + } + } + for zb0002 > 0 { + var za0001 string + var za0002 string + zb0002-- + za0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + za0002, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Props", za0001) + return + } + z.Props[za0001] = za0002 + } + var zb0003 uint32 + zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "TeamMembers") + return + } + if cap(z.TeamMembers) >= int(zb0003) { + z.TeamMembers = (z.TeamMembers)[:zb0003] + } else { + z.TeamMembers = make([]*TeamMember, zb0003) + } + for za0003 := range z.TeamMembers { + if msgp.IsNil(bts) { + bts, err = msgp.ReadNilBytes(bts) + if err != nil { + return + } + z.TeamMembers[za0003] = nil + } else { + if z.TeamMembers[za0003] == nil { + z.TeamMembers[za0003] = new(TeamMember) + } + bts, err = z.TeamMembers[za0003].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "TeamMembers", za0003) + return + } + } + } + z.Local, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Local") + return + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *Session) Msgsize() (s int) { + s = 1 + msgp.StringPrefixSize + len(z.Id) + msgp.StringPrefixSize + len(z.Token) + msgp.Int64Size + msgp.Int64Size + msgp.Int64Size + msgp.StringPrefixSize + len(z.UserId) + msgp.StringPrefixSize + len(z.DeviceId) + msgp.StringPrefixSize + len(z.Roles) + msgp.BoolSize + msgp.BoolSize + msgp.MapHeaderSize + if z.Props != nil { + for za0001, za0002 := range z.Props { + _ = za0002 + s += msgp.StringPrefixSize + len(za0001) + msgp.StringPrefixSize + len(za0002) + } + } + s += msgp.ArrayHeaderSize + for za0003 := range z.TeamMembers { + if z.TeamMembers[za0003] == nil { + s += msgp.NilSize + } else { + s += z.TeamMembers[za0003].Msgsize() + } + } + s += msgp.BoolSize + return +} + +// DecodeMsg implements msgp.Decodable +func (z *StringMap) DecodeMsg(dc *msgp.Reader) (err error) { + var zb0003 uint32 + zb0003, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + if (*z) == nil { + (*z) = make(StringMap, zb0003) + } else if len((*z)) > 0 { + for key := range *z { + delete((*z), key) + } + } + for zb0003 > 0 { + zb0003-- + var zb0001 string + var zb0002 string + zb0001, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err) + return + } + zb0002, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, zb0001) + return + } + (*z)[zb0001] = zb0002 + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z StringMap) EncodeMsg(en *msgp.Writer) (err error) { + err = en.WriteMapHeader(uint32(len(z))) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0004, zb0005 := range z { + err = en.WriteString(zb0004) + if err != nil { + err = msgp.WrapError(err) + return + } + err = en.WriteString(zb0005) + if err != nil { + err = msgp.WrapError(err, zb0004) + return + } + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z StringMap) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendMapHeader(o, uint32(len(z))) + for zb0004, zb0005 := range z { + o = msgp.AppendString(o, zb0004) + o = msgp.AppendString(o, zb0005) + } + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *StringMap) UnmarshalMsg(bts []byte) (o []byte, err error) { + var zb0003 uint32 + zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if (*z) == nil { + (*z) = make(StringMap, zb0003) + } else if len((*z)) > 0 { + for key := range *z { + delete((*z), key) + } + } + for zb0003 > 0 { + var zb0001 string + var zb0002 string + zb0003-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + zb0002, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, zb0001) + return + } + (*z)[zb0001] = zb0002 + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z StringMap) Msgsize() (s int) { + s = msgp.MapHeaderSize + if z != nil { + for zb0004, zb0005 := range z { + _ = zb0005 + s += msgp.StringPrefixSize + len(zb0004) + msgp.StringPrefixSize + len(zb0005) + } + } + return +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/shared_channel.go b/vendor/github.com/mattermost/mattermost-server/v6/model/shared_channel.go new file mode 100644 index 00000000..08a29292 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/shared_channel.go @@ -0,0 +1,249 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" + "unicode/utf8" +) + +// SharedChannel represents a channel that can be synchronized with a remote cluster. +// If "home" is true, then the shared channel is homed locally and "SharedChannelRemote" +// table contains the remote clusters that have been invited. +// If "home" is false, then the shared channel is homed remotely, and "RemoteId" +// field points to the remote cluster connection in "RemoteClusters" table. +type SharedChannel struct { + ChannelId string `json:"id"` + TeamId string `json:"team_id"` + Home bool `json:"home"` + ReadOnly bool `json:"readonly"` + ShareName string `json:"name"` + ShareDisplayName string `json:"display_name"` + SharePurpose string `json:"purpose"` + ShareHeader string `json:"header"` + CreatorId string `json:"creator_id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + RemoteId string `json:"remote_id,omitempty"` // if not "home" + Type ChannelType `db:"-"` +} + +func (sc *SharedChannel) IsValid() *AppError { + if !IsValidId(sc.ChannelId) { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.id.app_error", nil, "ChannelId="+sc.ChannelId, http.StatusBadRequest) + } + + if sc.Type != ChannelTypeDirect && !IsValidId(sc.TeamId) { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.id.app_error", nil, "TeamId="+sc.TeamId, http.StatusBadRequest) + } + + if sc.CreateAt == 0 { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest) + } + + if sc.UpdateAt == 0 { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest) + } + + if utf8.RuneCountInString(sc.ShareDisplayName) > ChannelDisplayNameMaxRunes { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.display_name.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest) + } + + if !IsValidChannelIdentifier(sc.ShareName) { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.2_or_more.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest) + } + + if utf8.RuneCountInString(sc.ShareHeader) > ChannelHeaderMaxRunes { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest) + } + + if utf8.RuneCountInString(sc.SharePurpose) > ChannelPurposeMaxRunes { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.purpose.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest) + } + + if !IsValidId(sc.CreatorId) { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "CreatorId="+sc.CreatorId, http.StatusBadRequest) + } + + if !sc.Home { + if !IsValidId(sc.RemoteId) { + return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.id.app_error", nil, "RemoteId="+sc.RemoteId, http.StatusBadRequest) + } + } + return nil +} + +func (sc *SharedChannel) PreSave() { + sc.ShareName = SanitizeUnicode(sc.ShareName) + sc.ShareDisplayName = SanitizeUnicode(sc.ShareDisplayName) + + sc.CreateAt = GetMillis() + sc.UpdateAt = sc.CreateAt +} + +func (sc *SharedChannel) PreUpdate() { + sc.UpdateAt = GetMillis() + sc.ShareName = SanitizeUnicode(sc.ShareName) + sc.ShareDisplayName = SanitizeUnicode(sc.ShareDisplayName) +} + +// SharedChannelRemote represents a remote cluster that has been invited +// to a shared channel. +type SharedChannelRemote struct { + Id string `json:"id"` + ChannelId string `json:"channel_id"` + CreatorId string `json:"creator_id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + IsInviteAccepted bool `json:"is_invite_accepted"` + IsInviteConfirmed bool `json:"is_invite_confirmed"` + RemoteId string `json:"remote_id"` + LastPostUpdateAt int64 `json:"last_post_update_at"` + LastPostId string `json:"last_post_id"` +} + +func (sc *SharedChannelRemote) IsValid() *AppError { + if !IsValidId(sc.Id) { + return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.id.app_error", nil, "Id="+sc.Id, http.StatusBadRequest) + } + + if !IsValidId(sc.ChannelId) { + return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.id.app_error", nil, "ChannelId="+sc.ChannelId, http.StatusBadRequest) + } + + if sc.CreateAt == 0 { + return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest) + } + + if sc.UpdateAt == 0 { + return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest) + } + + if !IsValidId(sc.CreatorId) { + return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "id="+sc.CreatorId, http.StatusBadRequest) + } + return nil +} + +func (sc *SharedChannelRemote) PreSave() { + if sc.Id == "" { + sc.Id = NewId() + } + sc.CreateAt = GetMillis() + sc.UpdateAt = sc.CreateAt +} + +func (sc *SharedChannelRemote) PreUpdate() { + sc.UpdateAt = GetMillis() +} + +type SharedChannelRemoteStatus struct { + ChannelId string `json:"channel_id"` + DisplayName string `json:"display_name"` + SiteURL string `json:"site_url"` + LastPingAt int64 `json:"last_ping_at"` + NextSyncAt int64 `json:"next_sync_at"` + ReadOnly bool `json:"readonly"` + IsInviteAccepted bool `json:"is_invite_accepted"` + Token string `json:"token"` +} + +// SharedChannelUser stores a lastSyncAt timestamp on behalf of a remote cluster for +// each user that has been synchronized. +type SharedChannelUser struct { + Id string `json:"id"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + RemoteId string `json:"remote_id"` + CreateAt int64 `json:"create_at"` + LastSyncAt int64 `json:"last_sync_at"` +} + +func (scu *SharedChannelUser) PreSave() { + scu.Id = NewId() + scu.CreateAt = GetMillis() +} + +func (scu *SharedChannelUser) IsValid() *AppError { + if !IsValidId(scu.Id) { + return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "Id="+scu.Id, http.StatusBadRequest) + } + + if !IsValidId(scu.UserId) { + return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "UserId="+scu.UserId, http.StatusBadRequest) + } + + if !IsValidId(scu.ChannelId) { + return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "ChannelId="+scu.ChannelId, http.StatusBadRequest) + } + + if !IsValidId(scu.RemoteId) { + return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.id.app_error", nil, "RemoteId="+scu.RemoteId, http.StatusBadRequest) + } + + if scu.CreateAt == 0 { + return NewAppError("SharedChannelUser.IsValid", "model.channel.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) + } + return nil +} + +type GetUsersForSyncFilter struct { + CheckProfileImage bool + ChannelID string + Limit uint64 +} + +// SharedChannelAttachment stores a lastSyncAt timestamp on behalf of a remote cluster for +// each file attachment that has been synchronized. +type SharedChannelAttachment struct { + Id string `json:"id"` + FileId string `json:"file_id"` + RemoteId string `json:"remote_id"` + CreateAt int64 `json:"create_at"` + LastSyncAt int64 `json:"last_sync_at"` +} + +func (scf *SharedChannelAttachment) PreSave() { + if scf.Id == "" { + scf.Id = NewId() + } + if scf.CreateAt == 0 { + scf.CreateAt = GetMillis() + scf.LastSyncAt = scf.CreateAt + } else { + scf.LastSyncAt = GetMillis() + } +} + +func (scf *SharedChannelAttachment) IsValid() *AppError { + if !IsValidId(scf.Id) { + return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.id.app_error", nil, "Id="+scf.Id, http.StatusBadRequest) + } + + if !IsValidId(scf.FileId) { + return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.id.app_error", nil, "FileId="+scf.FileId, http.StatusBadRequest) + } + + if !IsValidId(scf.RemoteId) { + return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.id.app_error", nil, "RemoteId="+scf.RemoteId, http.StatusBadRequest) + } + + if scf.CreateAt == 0 { + return NewAppError("SharedChannelAttachment.IsValid", "model.channel.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) + } + return nil +} + +type SharedChannelFilterOpts struct { + TeamId string + CreatorId string + ExcludeHome bool + ExcludeRemote bool +} + +type SharedChannelRemoteFilterOpts struct { + ChannelId string + RemoteId string + InclUnconfirmed bool +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/slack_attachment.go b/vendor/github.com/mattermost/mattermost-server/v6/model/slack_attachment.go new file mode 100644 index 00000000..94ba9935 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/slack_attachment.go @@ -0,0 +1,196 @@ +// 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 = PostTypeSlackAttachment + } + + 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 field == nil { + continue + } + 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/v6/model/slack_compatibility.go b/vendor/github.com/mattermost/mattermost-server/v6/model/slack_compatibility.go new file mode 100644 index 00000000..2d3e2878 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/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/v6/model/status.go b/vendor/github.com/mattermost/mattermost-server/v6/model/status.go new file mode 100644 index 00000000..45a6d5d2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/status.go @@ -0,0 +1,55 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" +) + +const ( + StatusOutOfOffice = "ooo" + StatusOffline = "offline" + StatusAway = "away" + StatusDnd = "dnd" + StatusOnline = "online" + StatusCacheSize = SessionCacheSize + StatusChannelTimeout = 20000 // 20 seconds + StatusMinUpdateTime = 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:"-"` + DNDEndTime int64 `json:"dnd_end_time"` + PrevStatus string `json:"-"` +} + +func (s *Status) ToJSON() ([]byte, error) { + sCopy := *s + sCopy.ActiveChannel = "" + return json.Marshal(sCopy) +} + +func StatusListToJSON(u []*Status) ([]byte, error) { + list := make([]Status, len(u)) + for i, s := range u { + list[i] = *s + list[i].ActiveChannel = "" + } + return json.Marshal(list) +} + +func StatusMapToInterfaceMap(statusMap map[string]*Status) map[string]interface{} { + interfaceMap := map[string]interface{}{} + for _, s := range statusMap { + // Omitted statues mean offline + if s.Status != StatusOffline { + interfaceMap[s.UserId] = s.Status + } + } + return interfaceMap +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/suggest_command.go b/vendor/github.com/mattermost/mattermost-server/v6/model/suggest_command.go new file mode 100644 index 00000000..7fb045fc --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/suggest_command.go @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type SuggestCommand struct { + Suggestion string `json:"suggestion"` + Description string `json:"description"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/switch_request.go b/vendor/github.com/mattermost/mattermost-server/v6/model/switch_request.go new file mode 100644 index 00000000..70694cb1 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/switch_request.go @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +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) EmailToOAuth() bool { + return o.CurrentService == UserAuthServiceEmail && + (o.NewService == UserAuthServiceSaml || + o.NewService == UserAuthServiceGitlab || + o.NewService == ServiceGoogle || + o.NewService == ServiceOffice365 || + o.NewService == ServiceOpenid) +} + +func (o *SwitchRequest) OAuthToEmail() bool { + return (o.CurrentService == UserAuthServiceSaml || + o.CurrentService == UserAuthServiceGitlab || + o.CurrentService == ServiceGoogle || + o.CurrentService == ServiceOffice365 || + o.CurrentService == ServiceOpenid) && o.NewService == UserAuthServiceEmail +} + +func (o *SwitchRequest) EmailToLdap() bool { + return o.CurrentService == UserAuthServiceEmail && o.NewService == UserAuthServiceLdap +} + +func (o *SwitchRequest) LdapToEmail() bool { + return o.CurrentService == UserAuthServiceLdap && o.NewService == UserAuthServiceEmail +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/system.go b/vendor/github.com/mattermost/mattermost-server/v6/model/system.go new file mode 100644 index 00000000..c8bcaba2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/system.go @@ -0,0 +1,182 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "math/big" +) + +const ( + SystemTelemetryId = "DiagnosticId" + SystemRanUnitTests = "RanUnitTests" + SystemLastSecurityTime = "LastSecurityTime" + SystemActiveLicenseId = "ActiveLicenseId" + SystemLicenseRenewalToken = "LicenseRenewalToken" + SystemLastComplianceTime = "LastComplianceTime" + SystemAsymmetricSigningKeyKey = "AsymmetricSigningKey" + SystemPostActionCookieSecretKey = "PostActionCookieSecret" + SystemInstallationDateKey = "InstallationDate" + SystemFirstServerRunTimestampKey = "FirstServerRunTimestamp" + SystemClusterEncryptionKey = "ClusterEncryptionKey" + SystemUpgradedFromTeId = "UpgradedFromTE" + SystemWarnMetricNumberOfTeams5 = "warn_metric_number_of_teams_5" + SystemWarnMetricNumberOfChannels50 = "warn_metric_number_of_channels_50" + SystemWarnMetricMfa = "warn_metric_mfa" + SystemWarnMetricEmailDomain = "warn_metric_email_domain" + SystemWarnMetricNumberOfActiveUsers100 = "warn_metric_number_of_active_users_100" + SystemWarnMetricNumberOfActiveUsers200 = "warn_metric_number_of_active_users_200" + SystemWarnMetricNumberOfActiveUsers300 = "warn_metric_number_of_active_users_300" + SystemWarnMetricNumberOfActiveUsers500 = "warn_metric_number_of_active_users_500" + SystemWarnMetricNumberOfPosts2m = "warn_metric_number_of_posts_2M" + SystemWarnMetricLastRunTimestampKey = "LastWarnMetricRunTimestamp" + SystemMetricSupportEmailNotConfigured = "warn_metric_support_email_not_configured" + SystemFirstAdminVisitMarketplace = "FirstAdminVisitMarketplace" + AwsMeteringReportInterval = 1 + AwsMeteringDimensionUsageHrs = "UsageHrs" + UserLimitOverageCycleEndDate = "UserLimitOverageCycleEndDate" + OverUserLimitForgivenCount = "OverUserLimitForgivenCount" + OverUserLimitLastEmailSent = "OverUserLimitLastEmailSent" +) + +const ( + WarnMetricStatusLimitReached = "true" + WarnMetricStatusRunonce = "runonce" + WarnMetricStatusAck = "ack" + WarnMetricStatusStorePrefix = "warn_metric_" + WarnMetricJobInterval = 24 * 7 + WarnMetricNumberOfActiveUsers25 = 25 + WarnMetricJobWaitTime = 1000 * 3600 * 24 * 7 // 7 days +) + +type System struct { + Name string `json:"name"` + Value string `json:"value"` +} + +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"` + ExpiresTS string `json:"expires_ts,omitempty"` +} + +type SupportPacket struct { + ServerOS string `yaml:"server_os"` + ServerArchitecture string `yaml:"server_architecture"` + DatabaseType string `yaml:"database_type"` + DatabaseVersion string `yaml:"database_version"` + LdapVendorName string `yaml:"ldap_vendor_name,omitempty"` + LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"` + ElasticServerVersion string `yaml:"elastic_server_version,omitempty"` + ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"` +} + +type FileData struct { + Filename string + Body []byte +} + +var WarnMetricsTable = map[string]WarnMetric{ + SystemWarnMetricMfa: { + Id: SystemWarnMetricMfa, + Limit: -1, + IsBotOnly: true, + IsRunOnce: true, + }, + SystemWarnMetricEmailDomain: { + Id: SystemWarnMetricEmailDomain, + Limit: -1, + IsBotOnly: true, + IsRunOnce: true, + }, + SystemWarnMetricNumberOfTeams5: { + Id: SystemWarnMetricNumberOfTeams5, + Limit: 5, + IsBotOnly: true, + IsRunOnce: true, + }, + SystemWarnMetricNumberOfChannels50: { + Id: SystemWarnMetricNumberOfChannels50, + Limit: 50, + IsBotOnly: true, + IsRunOnce: true, + }, + SystemWarnMetricNumberOfActiveUsers100: { + Id: SystemWarnMetricNumberOfActiveUsers100, + Limit: 100, + IsBotOnly: true, + IsRunOnce: true, + }, + SystemWarnMetricNumberOfActiveUsers200: { + Id: SystemWarnMetricNumberOfActiveUsers200, + Limit: 200, + IsBotOnly: true, + IsRunOnce: true, + }, + SystemWarnMetricNumberOfActiveUsers300: { + Id: SystemWarnMetricNumberOfActiveUsers300, + Limit: 300, + IsBotOnly: true, + IsRunOnce: true, + }, + SystemWarnMetricNumberOfActiveUsers500: { + Id: SystemWarnMetricNumberOfActiveUsers500, + Limit: 500, + IsBotOnly: false, + IsRunOnce: true, + }, + SystemWarnMetricNumberOfPosts2m: { + Id: SystemWarnMetricNumberOfPosts2m, + Limit: 2000000, + IsBotOnly: false, + IsRunOnce: true, + }, + SystemMetricSupportEmailNotConfigured: { + Id: SystemMetricSupportEmailNotConfigured, + Limit: -1, + IsBotOnly: true, + IsRunOnce: false, + SkipAction: true, + }, +} + +type WarnMetric struct { + Id string + Limit int64 + IsBotOnly bool + IsRunOnce bool + SkipAction bool +} + +type WarnMetricDisplayTexts struct { + BotTitle string + BotMessageBody string + BotSuccessMessage string + EmailBody string +} +type WarnMetricStatus struct { + Id string `json:"id"` + Limit int64 `json:"limit"` + Acked bool `json:"acked"` + StoreStatus string `json:"store_status,omitempty"` +} + +type SendWarnMetricAck struct { + ForceAck bool `json:"forceAck"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/team.go b/vendor/github.com/mattermost/mattermost-server/v6/model/team.go new file mode 100644 index 00000000..d18d1620 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/team.go @@ -0,0 +1,253 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "net/http" + "regexp" + "strings" + "unicode/utf8" +) + +const ( + TeamOpen = "O" + TeamInvite = "I" + TeamAllowedDomainsMaxLength = 500 + TeamCompanyNameMaxLength = 64 + TeamDescriptionMaxLength = 255 + TeamDisplayNameMaxRunes = 64 + TeamEmailMaxLength = 128 + TeamNameMaxLength = 64 + TeamNameMinLength = 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"` + PolicyID *string `json:"policy_id" db:"-"` +} + +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 (o *Invites) ToEmailList() []string { + emailList := make([]string, len(o.Invites)) + for _, invite := range o.Invites { + emailList = append(emailList, invite["email"]) + } + return emailList +} + +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) > TeamEmailMaxLength { + return NewAppError("Team.IsValid", "model.team.is_valid.email.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if o.Email != "" && !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) > TeamDisplayNameMaxRunes { + return NewAppError("Team.IsValid", "model.team.is_valid.name.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.Name) > TeamNameMaxLength { + return NewAppError("Team.IsValid", "model.team.is_valid.url.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.Description) > TeamDescriptionMaxLength { + return NewAppError("Team.IsValid", "model.team.is_valid.description.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if o.InviteId == "" { + 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 == TeamOpen || o.Type == TeamInvite) { + return NewAppError("Team.IsValid", "model.team.is_valid.type.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.CompanyName) > TeamCompanyNameMaxLength { + return NewAppError("Team.IsValid", "model.team.is_valid.company.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.AllowedDomains) > TeamAllowedDomainsMaxLength { + 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 o.InviteId == "" { + 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) < TeamNameMinLength { + 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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/team_member.go b/vendor/github.com/mattermost/mattermost-server/v6/model/team_member.go new file mode 100644 index 00000000..2c928d2d --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/team_member.go @@ -0,0 +1,118 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "net/http" + "strings" +) + +const ( + USERNAME = "Username" +) + +//msgp:tuple TeamMember +// This struct's serializer methods are auto-generated. If a new field is added/removed, +// please run make gen-serialized. +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"` +} + +//msgp:ignore TeamUnread +type TeamUnread struct { + TeamId string `json:"team_id"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + MentionCountRoot int64 `json:"mention_count_root"` + MsgCountRoot int64 `json:"msg_count_root"` + ThreadCount int64 `json:"thread_count"` + ThreadMentionCount int64 `json:"thread_mention_count"` +} + +//msgp:ignore TeamMemberForExport +type TeamMemberForExport struct { + TeamMember + TeamName string +} + +//msgp:ignore TeamMemberWithError +type TeamMemberWithError struct { + UserId string `json:"user_id"` + Member *TeamMember `json:"member"` + Error *AppError `json:"error"` +} + +//msgp:ignore EmailInviteWithError +type EmailInviteWithError struct { + Email string `json:"email"` + Error *AppError `json:"error"` +} + +//msgp:ignore TeamMembersGetOptions +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 EmailInviteWithErrorToEmails(o []*EmailInviteWithError) []string { + var ret []string + for _, o := range o { + if o.Error == nil { + ret = append(ret, o.Email) + } + } + return ret +} + +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 TeamMemberWithErrorToString(o *TeamMemberWithError) string { + return fmt.Sprintf("%s:%s", o.UserId, o.Error.Error()) +} + +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/v6/model/team_member_serial_gen.go b/vendor/github.com/mattermost/mattermost-server/v6/model/team_member_serial_gen.go new file mode 100644 index 00000000..044a608a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/team_member_serial_gen.go @@ -0,0 +1,193 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *TeamMember) DecodeMsg(dc *msgp.Reader) (err error) { + var zb0001 uint32 + zb0001, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 != 8 { + err = msgp.ArrayError{Wanted: 8, Got: zb0001} + return + } + z.TeamId, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "TeamId") + return + } + z.UserId, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "UserId") + return + } + z.Roles, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Roles") + return + } + z.DeleteAt, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "DeleteAt") + return + } + z.SchemeGuest, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "SchemeGuest") + return + } + z.SchemeUser, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "SchemeUser") + return + } + z.SchemeAdmin, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "SchemeAdmin") + return + } + z.ExplicitRoles, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "ExplicitRoles") + return + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *TeamMember) EncodeMsg(en *msgp.Writer) (err error) { + // array header, size 8 + err = en.Append(0x98) + if err != nil { + return + } + err = en.WriteString(z.TeamId) + if err != nil { + err = msgp.WrapError(err, "TeamId") + return + } + err = en.WriteString(z.UserId) + if err != nil { + err = msgp.WrapError(err, "UserId") + return + } + err = en.WriteString(z.Roles) + if err != nil { + err = msgp.WrapError(err, "Roles") + return + } + err = en.WriteInt64(z.DeleteAt) + if err != nil { + err = msgp.WrapError(err, "DeleteAt") + return + } + err = en.WriteBool(z.SchemeGuest) + if err != nil { + err = msgp.WrapError(err, "SchemeGuest") + return + } + err = en.WriteBool(z.SchemeUser) + if err != nil { + err = msgp.WrapError(err, "SchemeUser") + return + } + err = en.WriteBool(z.SchemeAdmin) + if err != nil { + err = msgp.WrapError(err, "SchemeAdmin") + return + } + err = en.WriteString(z.ExplicitRoles) + if err != nil { + err = msgp.WrapError(err, "ExplicitRoles") + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z *TeamMember) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // array header, size 8 + o = append(o, 0x98) + o = msgp.AppendString(o, z.TeamId) + o = msgp.AppendString(o, z.UserId) + o = msgp.AppendString(o, z.Roles) + o = msgp.AppendInt64(o, z.DeleteAt) + o = msgp.AppendBool(o, z.SchemeGuest) + o = msgp.AppendBool(o, z.SchemeUser) + o = msgp.AppendBool(o, z.SchemeAdmin) + o = msgp.AppendString(o, z.ExplicitRoles) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *TeamMember) UnmarshalMsg(bts []byte) (o []byte, err error) { + var zb0001 uint32 + zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 != 8 { + err = msgp.ArrayError{Wanted: 8, Got: zb0001} + return + } + z.TeamId, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "TeamId") + return + } + z.UserId, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "UserId") + return + } + z.Roles, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Roles") + return + } + z.DeleteAt, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "DeleteAt") + return + } + z.SchemeGuest, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "SchemeGuest") + return + } + z.SchemeUser, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "SchemeUser") + return + } + z.SchemeAdmin, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "SchemeAdmin") + return + } + z.ExplicitRoles, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ExplicitRoles") + return + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *TeamMember) Msgsize() (s int) { + s = 1 + msgp.StringPrefixSize + len(z.TeamId) + msgp.StringPrefixSize + len(z.UserId) + msgp.StringPrefixSize + len(z.Roles) + msgp.Int64Size + msgp.BoolSize + msgp.BoolSize + msgp.BoolSize + msgp.StringPrefixSize + len(z.ExplicitRoles) + return +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/team_search.go b/vendor/github.com/mattermost/mattermost-server/v6/model/team_search.go new file mode 100644 index 00000000..c4a39275 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/team_search.go @@ -0,0 +1,22 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type TeamSearch struct { + Term string `json:"term"` + Page *int `json:"page,omitempty"` + PerPage *int `json:"per_page,omitempty"` + AllowOpenInvite *bool `json:"allow_open_invite,omitempty"` + GroupConstrained *bool `json:"group_constrained,omitempty"` + IncludeGroupConstrained *bool `json:"include_group_constrained,omitempty"` + PolicyID *string `json:"policy_id,omitempty"` + ExcludePolicyConstrained *bool `json:"exclude_policy_constrained,omitempty"` + IncludePolicyID *bool `json:"-"` + IncludeDeleted *bool `json:"-"` + TeamType *string `json:"-"` +} + +func (t *TeamSearch) IsPaginated() bool { + return t.Page != nil && t.PerPage != nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/team_stats.go b/vendor/github.com/mattermost/mattermost-server/v6/model/team_stats.go new file mode 100644 index 00000000..0a3a7387 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/team_stats.go @@ -0,0 +1,10 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type TeamStats struct { + TeamId string `json:"team_id"` + TotalMemberCount int64 `json:"total_member_count"` + ActiveMemberCount int64 `json:"active_member_count"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/terms_of_service.go b/vendor/github.com/mattermost/mattermost-server/v6/model/terms_of_service.go new file mode 100644 index 00000000..b6e531a2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/terms_of_service.go @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "net/http" + "unicode/utf8" +) + +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) > PostMessageMaxRunesV2 { + return InvalidTermsOfServiceError("text", t.Id) + } + + return nil +} + +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": PostMessageMaxRunesV2}, 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/v6/model/thread.go b/vendor/github.com/mattermost/mattermost-server/v6/model/thread.go new file mode 100644 index 00000000..e774e87a --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/thread.go @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type Thread struct { + PostId string `json:"id"` + ChannelId string `json:"channel_id"` + ReplyCount int64 `json:"reply_count"` + LastReplyAt int64 `json:"last_reply_at"` + Participants StringArray `json:"participants"` +} + +type ThreadResponse struct { + PostId string `json:"id"` + ReplyCount int64 `json:"reply_count"` + LastReplyAt int64 `json:"last_reply_at"` + LastViewedAt int64 `json:"last_viewed_at"` + Participants []*User `json:"participants"` + Post *Post `json:"post"` + UnreadReplies int64 `json:"unread_replies"` + UnreadMentions int64 `json:"unread_mentions"` +} + +type Threads struct { + Total int64 `json:"total"` + TotalUnreadThreads int64 `json:"total_unread_threads"` + TotalUnreadMentions int64 `json:"total_unread_mentions"` + Threads []*ThreadResponse `json:"threads"` +} + +type GetUserThreadsOpts struct { + // PageSize specifies the size of the returned chunk of results. Default = 30 + PageSize uint64 + + // Extended will enrich the response with participant details. Default = false + Extended bool + + // Deleted will specify that even deleted threads should be returned (For mobile sync). Default = false + Deleted bool + + // Since filters the threads based on their LastUpdateAt timestamp. + Since uint64 + + // Before specifies thread id as a cursor for pagination and will return `PageSize` threads before the cursor + Before string + + // After specifies thread id as a cursor for pagination and will return `PageSize` threads after the cursor + After string + + // Unread will make sure that only threads with unread replies are returned + Unread bool + + // TotalsOnly will not fetch any threads and just fetch the total counts + TotalsOnly bool + + // TeamOnly will only fetch threads and unreads for the specified team and excludes DMs/GMs + TeamOnly bool +} + +func (o *Thread) Etag() string { + return Etag(o.PostId, o.LastReplyAt) +} + +type ThreadMembership struct { + PostId string `json:"post_id"` + UserId string `json:"user_id"` + Following bool `json:"following"` + LastViewed int64 `json:"last_view_at"` + LastUpdated int64 `json:"last_update_at"` + UnreadMentions int64 `json:"unread_mentions"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/token.go b/vendor/github.com/mattermost/mattermost-server/v6/model/token.go new file mode 100644 index 00000000..90fc729f --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/token.go @@ -0,0 +1,42 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "net/http" +) + +const ( + TokenSize = 64 + MaxTokenExipryTime = 1000 * 60 * 60 * 48 // 48 hour + TokenTypeOAuth = "oauth" +) + +type Token struct { + Token string + CreateAt int64 + Type string + Extra string +} + +func NewToken(tokentype, extra string) *Token { + return &Token{ + Token: NewRandomString(TokenSize), + CreateAt: GetMillis(), + Type: tokentype, + Extra: extra, + } +} + +func (t *Token) IsValid() *AppError { + if len(t.Token) != TokenSize { + 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/v6/model/typing_request.go b/vendor/github.com/mattermost/mattermost-server/v6/model/typing_request.go new file mode 100644 index 00000000..f7a34341 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/typing_request.go @@ -0,0 +1,9 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type TypingRequest struct { + ChannelId string `json:"channel_id"` + ParentId string `json:"parent_id"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/upload_session.go b/vendor/github.com/mattermost/mattermost-server/v6/model/upload_session.go new file mode 100644 index 00000000..994e7fb3 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/upload_session.go @@ -0,0 +1,113 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "net/http" +) + +// UploadType defines the type of an upload. +type UploadType string + +const ( + UploadTypeAttachment UploadType = "attachment" + UploadTypeImport UploadType = "import" +) + +// UploadNoUserID is a "fake" user id used by the API layer when in local mode. +const UploadNoUserID = "nouser" + +// UploadSession contains information used to keep track of a file upload. +type UploadSession struct { + // The unique identifier for the session. + Id string `json:"id"` + // The type of the upload. + Type UploadType `json:"type"` + // The timestamp of creation. + CreateAt int64 `json:"create_at"` + // The id of the user performing the upload. + UserId string `json:"user_id"` + // The id of the channel to upload to. + ChannelId string `json:"channel_id,omitempty"` + // The name of the file to upload. + Filename string `json:"filename"` + // The path where the file is stored. + Path string `json:"-"` + // The size of the file to upload. + FileSize int64 `json:"file_size"` + // The amount of received data in bytes. If equal to FileSize it means the + // upload has finished. + FileOffset int64 `json:"file_offset"` + // Id of remote cluster if uploading for shared channel + RemoteId string `json:"remote_id"` + // Requested file id if uploading for shared channel + ReqFileId string `json:"req_file_id"` +} + +// PreSave is a utility function used to fill required information. +func (us *UploadSession) PreSave() { + if us.Id == "" { + us.Id = NewId() + } + + if us.CreateAt == 0 { + us.CreateAt = GetMillis() + } +} + +// IsValid validates an UploadType. It returns an error in case of +// failure. +func (t UploadType) IsValid() error { + switch t { + case UploadTypeAttachment: + return nil + case UploadTypeImport: + return nil + default: + } + return fmt.Errorf("invalid UploadType %s", t) +} + +// IsValid validates an UploadSession. It returns an error in case of +// failure. +func (us *UploadSession) IsValid() *AppError { + if !IsValidId(us.Id) { + return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.id.app_error", nil, "", http.StatusBadRequest) + } + + if err := us.Type.IsValid(); err != nil { + return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.type.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if !IsValidId(us.UserId) && us.UserId != UploadNoUserID { + return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.user_id.app_error", nil, "id="+us.Id, http.StatusBadRequest) + } + + if us.Type == UploadTypeAttachment && !IsValidId(us.ChannelId) { + return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.channel_id.app_error", nil, "id="+us.Id, http.StatusBadRequest) + } + + if us.CreateAt == 0 { + return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.create_at.app_error", nil, "id="+us.Id, http.StatusBadRequest) + } + + if us.Filename == "" { + return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.filename.app_error", nil, "id="+us.Id, http.StatusBadRequest) + } + + if us.FileSize <= 0 { + return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.file_size.app_error", nil, "id="+us.Id, http.StatusBadRequest) + } + + if us.FileOffset < 0 || us.FileOffset > us.FileSize { + return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.file_offset.app_error", nil, "id="+us.Id, http.StatusBadRequest) + } + + if us.Path == "" { + return NewAppError("UploadSession.IsValid", "model.upload_session.is_valid.path.app_error", nil, "id="+us.Id, http.StatusBadRequest) + } + + return nil +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/user.go b/vendor/github.com/mattermost/mattermost-server/v6/model/user.go new file mode 100644 index 00000000..2e843ea1 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/user.go @@ -0,0 +1,912 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "net/http" + "regexp" + "sort" + "strings" + "unicode/utf8" + + "golang.org/x/crypto/bcrypt" + "golang.org/x/text/language" + + "github.com/mattermost/mattermost-server/v6/services/timezones" + "github.com/mattermost/mattermost-server/v6/shared/mlog" +) + +const ( + Me = "me" + UserNotifyAll = "all" + UserNotifyHere = "here" + UserNotifyMention = "mention" + UserNotifyNone = "none" + DesktopNotifyProp = "desktop" + DesktopSoundNotifyProp = "desktop_sound" + MarkUnreadNotifyProp = "mark_unread" + PushNotifyProp = "push" + PushStatusNotifyProp = "push_status" + EmailNotifyProp = "email" + ChannelMentionsNotifyProp = "channel" + CommentsNotifyProp = "comments" + MentionKeysNotifyProp = "mention_keys" + CommentsNotifyNever = "never" + CommentsNotifyRoot = "root" + CommentsNotifyAny = "any" + CommentsNotifyCRT = "crt" + FirstNameNotifyProp = "first_name" + AutoResponderActiveNotifyProp = "auto_responder_active" + AutoResponderMessageNotifyProp = "auto_responder_message" + DesktopThreadsNotifyProp = "desktop_threads" + PushThreadsNotifyProp = "push_threads" + EmailThreadsNotifyProp = "email_threads" + + DefaultLocale = "en" + UserAuthServiceEmail = "email" + + UserEmailMaxLength = 128 + UserNicknameMaxRunes = 64 + UserPositionMaxRunes = 128 + UserFirstNameMaxRunes = 64 + UserLastNameMaxRunes = 64 + UserAuthDataMaxLength = 128 + UserNameMaxLength = 64 + UserNameMinLength = 1 + UserPasswordMaxLength = 72 + UserLocaleMaxLength = 5 + UserTimezoneMaxRunes = 256 +) + +//msgp:tuple User + +// User contains the details about the user. +// This struct's serializer methods are auto-generated. If a new field is added/removed, +// please run make gen-serialized. +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"` + RemoteId *string `json:"remote_id,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"` + DisableWelcomeEmail bool `db:"-" json:"disable_welcome_email"` +} + +//msgp UserMap + +// UserMap is a map from a userId to a user object. +// It is used to generate methods which can be used for fast serialization/de-serialization. +type UserMap map[string]*User + +//msgp:ignore UserUpdate +type UserUpdate struct { + Old *User + New *User +} + +//msgp:ignore UserPatch +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"` + RemoteId *string `json:"remote_id"` +} + +//msgp:ignore UserAuth +type UserAuth struct { + Password string `json:"password,omitempty"` // DEPRECATED: It is not used. + AuthData *string `json:"auth_data,omitempty"` + AuthService string `json:"auth_service,omitempty"` +} + +//msgp:ignore UserForIndexing +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"` + Roles string `json:"roles"` + CreateAt int64 `json:"create_at"` + DeleteAt int64 `json:"delete_at"` + TeamsIds []string `json:"team_id"` + ChannelsIds []string `json:"channel_id"` +} + +//msgp:ignore ViewUsersRestrictions +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)) +} + +//msgp:ignore UserSlice +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 u.IsRemote() { + if !IsValidUsernameAllowRemote(u.Username) { + return InvalidUserError("username", u.Id) + } + } else { + if !IsValidUsername(u.Username) { + return InvalidUserError("username", u.Id) + } + } + + if len(u.Email) > UserEmailMaxLength || u.Email == "" || !IsValidEmail(u.Email) { + return InvalidUserError("email", u.Id) + } + + if utf8.RuneCountInString(u.Nickname) > UserNicknameMaxRunes { + return InvalidUserError("nickname", u.Id) + } + + if utf8.RuneCountInString(u.Position) > UserPositionMaxRunes { + return InvalidUserError("position", u.Id) + } + + if utf8.RuneCountInString(u.FirstName) > UserFirstNameMaxRunes { + return InvalidUserError("first_name", u.Id) + } + + if utf8.RuneCountInString(u.LastName) > UserLastNameMaxRunes { + return InvalidUserError("last_name", u.Id) + } + + if u.AuthData != nil && len(*u.AuthData) > UserAuthDataMaxLength { + return InvalidUserError("auth_data", u.Id) + } + + if u.AuthData != nil && *u.AuthData != "" && u.AuthService == "" { + return InvalidUserError("auth_data_type", u.Id) + } + + if u.Password != "" && u.AuthData != nil && *u.AuthData != "" { + return InvalidUserError("auth_data_pwd", u.Id) + } + + if len(u.Password) > UserPasswordMaxLength { + return InvalidUserError("password_limit", u.Id) + } + + if !IsValidLocale(u.Locale) { + return InvalidUserError("locale", u.Id) + } + + if len(u.Timezone) > 0 { + if tzJSON, err := json.Marshal(u.Timezone); err != nil { + return NewAppError("User.IsValid", "model.user.is_valid.marshal.app_error", nil, err.Error(), http.StatusInternalServerError) + } else if utf8.RuneCount(tzJSON) > UserTimezoneMaxRunes { + return InvalidUserError("timezone_limit", 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 = DefaultLocale + } + + 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 u.Password != "" { + 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[MentionKeysNotifyProp]; ok { + // Remove any blank mention keys + splitKeys := strings.Split(u.NotifyProps[MentionKeysNotifyProp], ",") + goodKeys := []string{} + for _, key := range splitKeys { + if key != "" { + goodKeys = append(goodKeys, strings.ToLower(key)) + } + } + u.NotifyProps[MentionKeysNotifyProp] = strings.Join(goodKeys, ",") + } +} + +func (u *User) SetDefaultNotifications() { + u.NotifyProps = make(map[string]string) + u.NotifyProps[EmailNotifyProp] = "true" + u.NotifyProps[PushNotifyProp] = UserNotifyMention + u.NotifyProps[DesktopNotifyProp] = UserNotifyMention + u.NotifyProps[DesktopSoundNotifyProp] = "true" + u.NotifyProps[MentionKeysNotifyProp] = "" + u.NotifyProps[ChannelMentionsNotifyProp] = "true" + u.NotifyProps[PushStatusNotifyProp] = StatusAway + u.NotifyProps[CommentsNotifyProp] = CommentsNotifyNever + u.NotifyProps[FirstNameNotifyProp] = "false" + u.NotifyProps[DesktopThreadsNotifyProp] = UserNotifyAll + u.NotifyProps[EmailThreadsNotifyProp] = UserNotifyAll + u.NotifyProps[PushThreadsNotifyProp] = UserNotifyAll +} + +func (u *User) UpdateMentionKeysFromUsername(oldUsername string) { + nonUsernameKeys := []string{} + for _, key := range u.GetMentionKeys() { + if key != oldUsername && key != "@"+oldUsername { + nonUsernameKeys = append(nonUsernameKeys, key) + } + } + + u.NotifyProps[MentionKeysNotifyProp] = "" + if len(nonUsernameKeys) > 0 { + u.NotifyProps[MentionKeysNotifyProp] += "," + strings.Join(nonUsernameKeys, ",") + } +} + +func (u *User) GetMentionKeys() []string { + var keys []string + + for _, key := range strings.Split(u.NotifyProps[MentionKeysNotifyProp], ",") { + 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 + } + + if patch.RemoteId != nil { + u.RemoteId = patch.RemoteId + } +} + +// 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.EmailVerified = false + } + u.LastPasswordUpdate = 0 + u.LastPictureUpdate = 0 + u.FailedAttempts = 0 + u.MfaActive = false + u.MfaSecret = "" + u.Email = strings.TrimSpace(u.Email) +} + +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) SetCustomStatus(cs *CustomStatus) error { + u.MakeNonNil() + statusJSON, jsonErr := json.Marshal(cs) + if jsonErr != nil { + return jsonErr + } + u.Props[UserPropsKeyCustomStatus] = string(statusJSON) + return nil +} + +func (u *User) ClearCustomStatus() { + u.MakeNonNil() + u.Props[UserPropsKeyCustomStatus] = "" +} + +func (u *User) GetFullName() string { + if u.FirstName != "" && u.LastName != "" { + return u.FirstName + " " + u.LastName + } else if u.FirstName != "" { + return u.FirstName + } else if u.LastName != "" { + return u.LastName + } else { + return "" + } +} + +func (u *User) getDisplayName(baseName, nameFormat string) string { + displayName := baseName + + if nameFormat == ShowNicknameFullName { + if u.Nickname != "" { + displayName = u.Nickname + } else if fullName := u.GetFullName(); fullName != "" { + displayName = fullName + } + } else if nameFormat == ShowFullName { + if fullName := u.GetFullName(); fullName != "" { + 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, SystemGuestRoleId) +} + +func (u *User) IsSystemAdmin() bool { + return IsInRole(u.Roles, SystemAdminRoleId) +} + +// 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 != UserAuthServiceEmail +} + +func (u *User) IsOAuthUser() bool { + return u.AuthService == ServiceGitlab || + u.AuthService == ServiceGoogle || + u.AuthService == ServiceOffice365 || + u.AuthService == ServiceOpenid +} + +func (u *User) IsLDAPUser() bool { + return u.AuthService == UserAuthServiceLdap +} + +func (u *User) IsSAMLUser() bool { + return u.AuthService == UserAuthServiceSaml +} + +func (u *User) GetPreferredTimezone() string { + return GetPreferredTimezone(u.Timezone) +} + +// IsRemote returns true if the user belongs to a remote cluster (has RemoteId). +func (u *User) IsRemote() bool { + return u.RemoteId != nil && *u.RemoteId != "" +} + +// GetRemoteID returns the remote id for this user or "" if not a remote user. +func (u *User) GetRemoteID() string { + if u.RemoteId != nil { + return *u.RemoteId + } + return "" +} + +// GetProp fetches a prop value by name. +func (u *User) GetProp(name string) (string, bool) { + val, ok := u.Props[name] + return val, ok +} + +// SetProp sets a prop value by name, creating the map if nil. +// Not thread safe. +func (u *User) SetProp(name string, value string) { + if u.Props == nil { + u.Props = make(map[string]string) + } + u.Props[name] = value +} + +func (u *User) ToPatch() *UserPatch { + return &UserPatch{ + Username: &u.Username, Password: &u.Password, + Nickname: &u.Nickname, FirstName: &u.FirstName, LastName: &u.LastName, + Position: &u.Position, Email: &u.Email, + Props: u.Props, NotifyProps: u.NotifyProps, + Locale: &u.Locale, Timezone: u.Timezone, + } +} + +func (u *UserPatch) SetField(fieldName string, fieldValue string) { + switch fieldName { + case "FirstName": + u.FirstName = &fieldValue + case "LastName": + u.LastName = &fieldValue + case "Nickname": + u.Nickname = &fieldValue + case "Email": + u.Email = &fieldValue + case "Position": + u.Position = &fieldValue + case "Username": + u.Username = &fieldValue + } +} + +// 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) +} + +var validUsernameChars = regexp.MustCompile(`^[a-z0-9\.\-_]+$`) +var validUsernameCharsForRemote = regexp.MustCompile(`^[a-z0-9\.\-_:]+$`) + +var restrictedUsernames = map[string]struct{}{ + "all": {}, + "channel": {}, + "matterbot": {}, + "system": {}, +} + +func IsValidUsername(s string) bool { + if len(s) < UserNameMinLength || len(s) > UserNameMaxLength { + return false + } + + if !validUsernameChars.MatchString(s) { + return false + } + + _, found := restrictedUsernames[s] + return !found +} + +func IsValidUsernameAllowRemote(s string) bool { + if len(s) < UserNameMinLength || len(s) > UserNameMaxLength { + return false + } + + if !validUsernameCharsForRemote.MatchString(s) { + return false + } + + _, found := restrictedUsernames[s] + return !found +} + +func CleanUsername(username string) string { + s := NormalizeUsername(strings.Replace(username, " ", "-", -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() + mlog.Warn("Generating new username since provided username was invalid", + mlog.String("provided_username", username), mlog.String("new_username", s)) + } + + return s +} + +func IsValidLocale(locale string) bool { + if locale != "" { + if len(locale) > UserLocaleMaxLength { + return false + } else if _, err := language.Parse(locale); err != nil { + return false + } + } + + return true +} + +//msgp:ignore UserWithGroups +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 trimmed == "" { + return nil + } + return strings.Split(trimmed, ",") +} + +//msgp:ignore UsersWithGroupsAndCount +type UsersWithGroupsAndCount struct { + Users []*UserWithGroups `json:"users"` + Count int64 `json:"total_count"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/user_access_token.go b/vendor/github.com/mattermost/mattermost-server/v6/model/user_access_token.go new file mode 100644 index 00000000..dee31f18 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/user_access_token.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "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 +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/user_access_token_search.go b/vendor/github.com/mattermost/mattermost-server/v6/model/user_access_token_search.go new file mode 100644 index 00000000..97fcde12 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/user_access_token_search.go @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type UserAccessTokenSearch struct { + Term string `json:"term"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/user_autocomplete.go b/vendor/github.com/mattermost/mattermost-server/v6/model/user_autocomplete.go new file mode 100644 index 00000000..b07131b3 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/user_autocomplete.go @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +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"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/user_count.go b/vendor/github.com/mattermost/mattermost-server/v6/model/user_count.go new file mode 100644 index 00000000..ee474883 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/user_count.go @@ -0,0 +1,26 @@ +// 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 + // Only include users on a specific channel. "" for any channel. + ChannelId string + // Restrict to search in a list of teams and channels + ViewRestrictions *ViewUsersRestrictions + // Only include users matching any of the given system wide roles. + Roles []string + // Only include users matching any of the given channel roles, must be used with ChannelId. + ChannelRoles []string + // Only include users matching any of the given team roles, must be used with TeamId. + TeamRoles []string +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/user_get.go b/vendor/github.com/mattermost/mattermost-server/v6/model/user_get.go new file mode 100644 index 00000000..2748d735 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/user_get.go @@ -0,0 +1,46 @@ +// 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 in the group + InGroupId 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 + // Filters for users matching any of the given system wide roles + Roles []string + // Filters for users matching any of the given channel roles, must be used with InChannelId + ChannelRoles []string + // Filters for users matching any of the given team roles, must be used with InTeamId + TeamRoles []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/v6/model/user_search.go b/vendor/github.com/mattermost/mattermost-server/v6/model/user_search.go new file mode 100644 index 00000000..93bf6009 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/user_search.go @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const UserSearchMaxLimit = 1000 +const UserSearchDefaultLimit = 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"` + InGroupId string `json:"in_group_id"` + GroupConstrained bool `json:"group_constrained"` + AllowInactive bool `json:"allow_inactive"` + WithoutTeam bool `json:"without_team"` + Limit int `json:"limit"` + Role string `json:"role"` + Roles []string `json:"roles"` + ChannelRoles []string `json:"channel_roles"` + TeamRoles []string `json:"team_roles"` +} + +// 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 + // Filters for users that have any of the given system roles + Roles []string + // Filters for users that have the given channel roles to be used when searching in a channel + ChannelRoles []string + // Filters for users that have the given team roles to be used when searching in a team + TeamRoles []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/v6/model/user_serial_gen.go b/vendor/github.com/mattermost/mattermost-server/v6/model/user_serial_gen.go new file mode 100644 index 00000000..fb40b577 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/user_serial_gen.go @@ -0,0 +1,826 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *User) DecodeMsg(dc *msgp.Reader) (err error) { + var zb0001 uint32 + zb0001, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 != 32 { + err = msgp.ArrayError{Wanted: 32, Got: zb0001} + return + } + z.Id, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Id") + return + } + z.CreateAt, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "CreateAt") + return + } + z.UpdateAt, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "UpdateAt") + return + } + z.DeleteAt, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "DeleteAt") + return + } + z.Username, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Username") + return + } + z.Password, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Password") + return + } + if dc.IsNil() { + err = dc.ReadNil() + if err != nil { + err = msgp.WrapError(err, "AuthData") + return + } + z.AuthData = nil + } else { + if z.AuthData == nil { + z.AuthData = new(string) + } + *z.AuthData, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "AuthData") + return + } + } + z.AuthService, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "AuthService") + return + } + z.Email, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Email") + return + } + z.EmailVerified, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "EmailVerified") + return + } + z.Nickname, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Nickname") + return + } + z.FirstName, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "FirstName") + return + } + z.LastName, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "LastName") + return + } + z.Position, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Position") + return + } + z.Roles, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Roles") + return + } + z.AllowMarketing, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "AllowMarketing") + return + } + err = z.Props.DecodeMsg(dc) + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + err = z.NotifyProps.DecodeMsg(dc) + if err != nil { + err = msgp.WrapError(err, "NotifyProps") + return + } + z.LastPasswordUpdate, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "LastPasswordUpdate") + return + } + z.LastPictureUpdate, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "LastPictureUpdate") + return + } + z.FailedAttempts, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "FailedAttempts") + return + } + z.Locale, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Locale") + return + } + err = z.Timezone.DecodeMsg(dc) + if err != nil { + err = msgp.WrapError(err, "Timezone") + return + } + z.MfaActive, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "MfaActive") + return + } + z.MfaSecret, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "MfaSecret") + return + } + if dc.IsNil() { + err = dc.ReadNil() + if err != nil { + err = msgp.WrapError(err, "RemoteId") + return + } + z.RemoteId = nil + } else { + if z.RemoteId == nil { + z.RemoteId = new(string) + } + *z.RemoteId, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "RemoteId") + return + } + } + z.LastActivityAt, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "LastActivityAt") + return + } + z.IsBot, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "IsBot") + return + } + z.BotDescription, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "BotDescription") + return + } + z.BotLastIconUpdate, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "BotLastIconUpdate") + return + } + z.TermsOfServiceId, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "TermsOfServiceId") + return + } + z.TermsOfServiceCreateAt, err = dc.ReadInt64() + if err != nil { + err = msgp.WrapError(err, "TermsOfServiceCreateAt") + return + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *User) EncodeMsg(en *msgp.Writer) (err error) { + // array header, size 32 + err = en.Append(0xdc, 0x0, 0x20) + if err != nil { + return + } + err = en.WriteString(z.Id) + if err != nil { + err = msgp.WrapError(err, "Id") + return + } + err = en.WriteInt64(z.CreateAt) + if err != nil { + err = msgp.WrapError(err, "CreateAt") + return + } + err = en.WriteInt64(z.UpdateAt) + if err != nil { + err = msgp.WrapError(err, "UpdateAt") + return + } + err = en.WriteInt64(z.DeleteAt) + if err != nil { + err = msgp.WrapError(err, "DeleteAt") + return + } + err = en.WriteString(z.Username) + if err != nil { + err = msgp.WrapError(err, "Username") + return + } + err = en.WriteString(z.Password) + if err != nil { + err = msgp.WrapError(err, "Password") + return + } + if z.AuthData == nil { + err = en.WriteNil() + if err != nil { + return + } + } else { + err = en.WriteString(*z.AuthData) + if err != nil { + err = msgp.WrapError(err, "AuthData") + return + } + } + err = en.WriteString(z.AuthService) + if err != nil { + err = msgp.WrapError(err, "AuthService") + return + } + err = en.WriteString(z.Email) + if err != nil { + err = msgp.WrapError(err, "Email") + return + } + err = en.WriteBool(z.EmailVerified) + if err != nil { + err = msgp.WrapError(err, "EmailVerified") + return + } + err = en.WriteString(z.Nickname) + if err != nil { + err = msgp.WrapError(err, "Nickname") + return + } + err = en.WriteString(z.FirstName) + if err != nil { + err = msgp.WrapError(err, "FirstName") + return + } + err = en.WriteString(z.LastName) + if err != nil { + err = msgp.WrapError(err, "LastName") + return + } + err = en.WriteString(z.Position) + if err != nil { + err = msgp.WrapError(err, "Position") + return + } + err = en.WriteString(z.Roles) + if err != nil { + err = msgp.WrapError(err, "Roles") + return + } + err = en.WriteBool(z.AllowMarketing) + if err != nil { + err = msgp.WrapError(err, "AllowMarketing") + return + } + err = z.Props.EncodeMsg(en) + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + err = z.NotifyProps.EncodeMsg(en) + if err != nil { + err = msgp.WrapError(err, "NotifyProps") + return + } + err = en.WriteInt64(z.LastPasswordUpdate) + if err != nil { + err = msgp.WrapError(err, "LastPasswordUpdate") + return + } + err = en.WriteInt64(z.LastPictureUpdate) + if err != nil { + err = msgp.WrapError(err, "LastPictureUpdate") + return + } + err = en.WriteInt(z.FailedAttempts) + if err != nil { + err = msgp.WrapError(err, "FailedAttempts") + return + } + err = en.WriteString(z.Locale) + if err != nil { + err = msgp.WrapError(err, "Locale") + return + } + err = z.Timezone.EncodeMsg(en) + if err != nil { + err = msgp.WrapError(err, "Timezone") + return + } + err = en.WriteBool(z.MfaActive) + if err != nil { + err = msgp.WrapError(err, "MfaActive") + return + } + err = en.WriteString(z.MfaSecret) + if err != nil { + err = msgp.WrapError(err, "MfaSecret") + return + } + if z.RemoteId == nil { + err = en.WriteNil() + if err != nil { + return + } + } else { + err = en.WriteString(*z.RemoteId) + if err != nil { + err = msgp.WrapError(err, "RemoteId") + return + } + } + err = en.WriteInt64(z.LastActivityAt) + if err != nil { + err = msgp.WrapError(err, "LastActivityAt") + return + } + err = en.WriteBool(z.IsBot) + if err != nil { + err = msgp.WrapError(err, "IsBot") + return + } + err = en.WriteString(z.BotDescription) + if err != nil { + err = msgp.WrapError(err, "BotDescription") + return + } + err = en.WriteInt64(z.BotLastIconUpdate) + if err != nil { + err = msgp.WrapError(err, "BotLastIconUpdate") + return + } + err = en.WriteString(z.TermsOfServiceId) + if err != nil { + err = msgp.WrapError(err, "TermsOfServiceId") + return + } + err = en.WriteInt64(z.TermsOfServiceCreateAt) + if err != nil { + err = msgp.WrapError(err, "TermsOfServiceCreateAt") + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z *User) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // array header, size 32 + o = append(o, 0xdc, 0x0, 0x20) + o = msgp.AppendString(o, z.Id) + o = msgp.AppendInt64(o, z.CreateAt) + o = msgp.AppendInt64(o, z.UpdateAt) + o = msgp.AppendInt64(o, z.DeleteAt) + o = msgp.AppendString(o, z.Username) + o = msgp.AppendString(o, z.Password) + if z.AuthData == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendString(o, *z.AuthData) + } + o = msgp.AppendString(o, z.AuthService) + o = msgp.AppendString(o, z.Email) + o = msgp.AppendBool(o, z.EmailVerified) + o = msgp.AppendString(o, z.Nickname) + o = msgp.AppendString(o, z.FirstName) + o = msgp.AppendString(o, z.LastName) + o = msgp.AppendString(o, z.Position) + o = msgp.AppendString(o, z.Roles) + o = msgp.AppendBool(o, z.AllowMarketing) + o, err = z.Props.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + o, err = z.NotifyProps.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "NotifyProps") + return + } + o = msgp.AppendInt64(o, z.LastPasswordUpdate) + o = msgp.AppendInt64(o, z.LastPictureUpdate) + o = msgp.AppendInt(o, z.FailedAttempts) + o = msgp.AppendString(o, z.Locale) + o, err = z.Timezone.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Timezone") + return + } + o = msgp.AppendBool(o, z.MfaActive) + o = msgp.AppendString(o, z.MfaSecret) + if z.RemoteId == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendString(o, *z.RemoteId) + } + o = msgp.AppendInt64(o, z.LastActivityAt) + o = msgp.AppendBool(o, z.IsBot) + o = msgp.AppendString(o, z.BotDescription) + o = msgp.AppendInt64(o, z.BotLastIconUpdate) + o = msgp.AppendString(o, z.TermsOfServiceId) + o = msgp.AppendInt64(o, z.TermsOfServiceCreateAt) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *User) UnmarshalMsg(bts []byte) (o []byte, err error) { + var zb0001 uint32 + zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 != 32 { + err = msgp.ArrayError{Wanted: 32, Got: zb0001} + return + } + z.Id, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Id") + return + } + z.CreateAt, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "CreateAt") + return + } + z.UpdateAt, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "UpdateAt") + return + } + z.DeleteAt, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "DeleteAt") + return + } + z.Username, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Username") + return + } + z.Password, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Password") + return + } + if msgp.IsNil(bts) { + bts, err = msgp.ReadNilBytes(bts) + if err != nil { + return + } + z.AuthData = nil + } else { + if z.AuthData == nil { + z.AuthData = new(string) + } + *z.AuthData, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AuthData") + return + } + } + z.AuthService, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AuthService") + return + } + z.Email, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Email") + return + } + z.EmailVerified, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "EmailVerified") + return + } + z.Nickname, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Nickname") + return + } + z.FirstName, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "FirstName") + return + } + z.LastName, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "LastName") + return + } + z.Position, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Position") + return + } + z.Roles, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Roles") + return + } + z.AllowMarketing, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AllowMarketing") + return + } + bts, err = z.Props.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Props") + return + } + bts, err = z.NotifyProps.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "NotifyProps") + return + } + z.LastPasswordUpdate, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LastPasswordUpdate") + return + } + z.LastPictureUpdate, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LastPictureUpdate") + return + } + z.FailedAttempts, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + err = msgp.WrapError(err, "FailedAttempts") + return + } + z.Locale, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Locale") + return + } + bts, err = z.Timezone.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Timezone") + return + } + z.MfaActive, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "MfaActive") + return + } + z.MfaSecret, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "MfaSecret") + return + } + if msgp.IsNil(bts) { + bts, err = msgp.ReadNilBytes(bts) + if err != nil { + return + } + z.RemoteId = nil + } else { + if z.RemoteId == nil { + z.RemoteId = new(string) + } + *z.RemoteId, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "RemoteId") + return + } + } + z.LastActivityAt, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LastActivityAt") + return + } + z.IsBot, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "IsBot") + return + } + z.BotDescription, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "BotDescription") + return + } + z.BotLastIconUpdate, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "BotLastIconUpdate") + return + } + z.TermsOfServiceId, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "TermsOfServiceId") + return + } + z.TermsOfServiceCreateAt, bts, err = msgp.ReadInt64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TermsOfServiceCreateAt") + return + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *User) Msgsize() (s int) { + s = 3 + msgp.StringPrefixSize + len(z.Id) + msgp.Int64Size + msgp.Int64Size + msgp.Int64Size + msgp.StringPrefixSize + len(z.Username) + msgp.StringPrefixSize + len(z.Password) + if z.AuthData == nil { + s += msgp.NilSize + } else { + s += msgp.StringPrefixSize + len(*z.AuthData) + } + s += msgp.StringPrefixSize + len(z.AuthService) + msgp.StringPrefixSize + len(z.Email) + msgp.BoolSize + msgp.StringPrefixSize + len(z.Nickname) + msgp.StringPrefixSize + len(z.FirstName) + msgp.StringPrefixSize + len(z.LastName) + msgp.StringPrefixSize + len(z.Position) + msgp.StringPrefixSize + len(z.Roles) + msgp.BoolSize + z.Props.Msgsize() + z.NotifyProps.Msgsize() + msgp.Int64Size + msgp.Int64Size + msgp.IntSize + msgp.StringPrefixSize + len(z.Locale) + z.Timezone.Msgsize() + msgp.BoolSize + msgp.StringPrefixSize + len(z.MfaSecret) + if z.RemoteId == nil { + s += msgp.NilSize + } else { + s += msgp.StringPrefixSize + len(*z.RemoteId) + } + s += msgp.Int64Size + msgp.BoolSize + msgp.StringPrefixSize + len(z.BotDescription) + msgp.Int64Size + msgp.StringPrefixSize + len(z.TermsOfServiceId) + msgp.Int64Size + return +} + +// DecodeMsg implements msgp.Decodable +func (z *UserMap) DecodeMsg(dc *msgp.Reader) (err error) { + var zb0003 uint32 + zb0003, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + if (*z) == nil { + (*z) = make(UserMap, zb0003) + } else if len((*z)) > 0 { + for key := range *z { + delete((*z), key) + } + } + for zb0003 > 0 { + zb0003-- + var zb0001 string + var zb0002 *User + zb0001, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err) + return + } + if dc.IsNil() { + err = dc.ReadNil() + if err != nil { + err = msgp.WrapError(err, zb0001) + return + } + zb0002 = nil + } else { + if zb0002 == nil { + zb0002 = new(User) + } + err = zb0002.DecodeMsg(dc) + if err != nil { + err = msgp.WrapError(err, zb0001) + return + } + } + (*z)[zb0001] = zb0002 + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z UserMap) EncodeMsg(en *msgp.Writer) (err error) { + err = en.WriteMapHeader(uint32(len(z))) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0004, zb0005 := range z { + err = en.WriteString(zb0004) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0005 == nil { + err = en.WriteNil() + if err != nil { + return + } + } else { + err = zb0005.EncodeMsg(en) + if err != nil { + err = msgp.WrapError(err, zb0004) + return + } + } + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z UserMap) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendMapHeader(o, uint32(len(z))) + for zb0004, zb0005 := range z { + o = msgp.AppendString(o, zb0004) + if zb0005 == nil { + o = msgp.AppendNil(o) + } else { + o, err = zb0005.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, zb0004) + return + } + } + } + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *UserMap) UnmarshalMsg(bts []byte) (o []byte, err error) { + var zb0003 uint32 + zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if (*z) == nil { + (*z) = make(UserMap, zb0003) + } else if len((*z)) > 0 { + for key := range *z { + delete((*z), key) + } + } + for zb0003 > 0 { + var zb0001 string + var zb0002 *User + zb0003-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if msgp.IsNil(bts) { + bts, err = msgp.ReadNilBytes(bts) + if err != nil { + return + } + zb0002 = nil + } else { + if zb0002 == nil { + zb0002 = new(User) + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, zb0001) + return + } + } + (*z)[zb0001] = zb0002 + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z UserMap) Msgsize() (s int) { + s = msgp.MapHeaderSize + if z != nil { + for zb0004, zb0005 := range z { + _ = zb0005 + s += msgp.StringPrefixSize + len(zb0004) + if zb0005 == nil { + s += msgp.NilSize + } else { + s += zb0005.Msgsize() + } + } + } + return +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/user_terms_of_service.go b/vendor/github.com/mattermost/mattermost-server/v6/model/user_terms_of_service.go new file mode 100644 index 00000000..880c0786 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/user_terms_of_service.go @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "fmt" + "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) PreSave() { + if ut.UserId == "" { + ut.UserId = NewId() + } + + ut.CreateAt = GetMillis() +} + +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/v6/model/users_stats.go b/vendor/github.com/mattermost/mattermost-server/v6/model/users_stats.go new file mode 100644 index 00000000..1a7becf2 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/users_stats.go @@ -0,0 +1,8 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type UsersStats struct { + TotalUsersCount int64 `json:"total_users_count"` +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/utils.go b/vendor/github.com/mattermost/mattermost-server/v6/model/utils.go new file mode 100644 index 00000000..fab3f494 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/utils.go @@ -0,0 +1,587 @@ +// 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" + "sort" + "strings" + "sync" + "time" + "unicode" + + "github.com/mattermost/mattermost-server/v6/shared/i18n" + "github.com/pborman/uuid" +) + +const ( + LowercaseLetters = "abcdefghijklmnopqrstuvwxyz" + UppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + NUMBERS = "0123456789" + SYMBOLS = " !\"\\#$%&'()*+,-./:;<=>?@[]^_`|~" +) + +type StringInterface map[string]interface{} +type StringArray []string + +func (sa StringArray) Remove(input string) StringArray { + for index := range sa { + if sa[index] == input { + ret := make(StringArray, 0, len(sa)-1) + ret = append(ret, sa[:index]...) + return append(ret, sa[index+1:]...) + } + } + return sa +} + +func (sa StringArray) Contains(input string) bool { + for index := range sa { + if sa[index] == input { + return true + } + } + + return false +} +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 i18n.TranslateFunc +var translateFuncOnce sync.Once + +func AppErrorInit(t i18n.TranslateFunc) { + translateFuncOnce.Do(func() { + 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 i18n.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 i18n.TranslateFunc) string { + if er.params == nil { + return T(er.Id) + } + 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 NewAppError("AppErrorFromJSON", "model.utils.decode_json.app_error", nil, "body: "+str, http.StatusInternalServerError) + } + return &er +} + +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] +} + +// 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) +} + +// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch. +func GetTimeForMillis(millis int64) time.Time { + return time.Unix(0, millis*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, len(originalMap)) + 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) + } + 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) + } + 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) + } + 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{}) + } + return objmap +} + +// ToJSON serializes an arbitrary data type to JSON, discarding the error. +func ToJSON(v interface{}) []byte { + b, _ := json.Marshal(v) + return b +} + +func GetServerIPAddress(iface string) string { + var addrs []net.Addr + if iface == "" { + 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", + "files", + "help", + "landing", + "login", + "mfa", + "oauth", + "plug", + "plugins", + "post", + "signup", + "boards", + "playbooks", +} + +func IsValidChannelIdentifier(s string) bool { + + if !IsValidAlphaNumHyphenUnderscore(s, true) { + return false + } + + if len(s) < ChannelNameMinLength { + return false + } + + return true +} + +var ( + validAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`) + validAlphaNumHyphenUnderscore = regexp.MustCompile(`^[a-z0-9]+([a-z\-\_0-9]+|(__)?)[a-z0-9]+$`) + validSimpleAlphaNumHyphenUnderscore = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`) + validSimpleAlphaNumHyphenUnderscorePlus = regexp.MustCompile(`^[a-zA-Z0-9+_-]+$`) +) + +func isValidAlphaNum(s string) bool { + return validAlphaNum.MatchString(s) +} + +func IsValidAlphaNumHyphenUnderscore(s string, withFormat bool) bool { + if withFormat { + return validAlphaNumHyphenUnderscore.MatchString(s) + } + return validSimpleAlphaNumHyphenUnderscore.MatchString(s) +} + +func IsValidAlphaNumHyphenUnderscorePlus(s string) bool { + return validSimpleAlphaNumHyphenUnderscorePlus.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])$`) + puncStart = regexp.MustCompile(`^[^\pL\d\s#]+`) + hashtagStart = regexp.MustCompile(`^#{2,}`) + 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 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 u, err := url.ParseRequestURI(rawURL); err != nil || u.Scheme == "" || u.Host == "" { + 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 +} + +// RemoveDuplicateStrings does an in-place removal of duplicate strings +// from the input slice. The original slice gets modified. +func RemoveDuplicateStrings(in []string) []string { + // In-place de-dup. + // Copied from https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable + if len(in) == 0 { + return in + } + sort.Strings(in) + j := 0 + for i := 1; i < len(in); i++ { + if in[j] == in[i] { + continue + } + j++ + in[j] = in[i] + } + return in[:j+1] +} + +func GetPreferredTimezone(timezone StringMap) string { + if timezone["useAutomaticTimezone"] == "true" { + return timezone["automaticTimezone"] + } + + return timezone["manualTimezone"] +} + +// SanitizeUnicode will remove undesirable Unicode characters from a string. +func SanitizeUnicode(s string) string { + return strings.Map(filterBlocklist, s) +} + +// filterBlocklist returns `r` if it is not in the blocklist, otherwise drop (-1). +// Blocklist is taken from https://www.w3.org/TR/unicode-xml/#Charlist +func filterBlocklist(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/v6/model/version.go b/vendor/github.com/mattermost/mattermost-server/v6/model/version.go new file mode 100644 index 00000000..b75a4e9c --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/version.go @@ -0,0 +1,190 @@ +// 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{ + "6.0.0", + "5.39.0", + "5.38.0", + "5.37.0", + "5.36.0", + "5.35.0", + "5.34.0", + "5.33.0", + "5.32.0", + "5.31.0", + "5.30.0", + "5.29.0", + "5.28.0", + "5.27.0", + "5.26.0", + "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 + } + 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/v6/model/websocket_client.go b/vendor/github.com/mattermost/mattermost-server/v6/model/websocket_client.go new file mode 100644 index 00000000..2bd8a3b7 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/websocket_client.go @@ -0,0 +1,324 @@ +// 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/mattermost/mattermost-server/v6/shared/mlog" + + "github.com/gorilla/websocket" +) + +const ( + SocketMaxMessageSizeKb = 8 * 1024 // 8KB + PingTimeoutBufferSeconds = 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 occurring 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, error) { + 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, error) { + conn, _, err := dialer.Dial(url+APIURLSuffix+"/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 + APIURLSuffix, + ConnectURL: url + APIURLSuffix + "/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(WebsocketAuthenticationChallenge, 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, error) { + 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, error) { + 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(WebsocketAuthenticationChallenge, 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, jsonErr := WebSocketEventFromJSON(bytes.NewReader(buf.Bytes())) + if jsonErr != nil { + mlog.Warn("Failed to decode from JSON", mlog.Err(jsonErr)) + 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 + PingTimeoutBufferSeconds)) + 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 + PingTimeoutBufferSeconds)) + + case <-wsc.pingTimeoutTimer.C: + wsc.PingTimeoutChannel <- true + wsc.pingTimeoutTimer.Reset(time.Second * (60 + PingTimeoutBufferSeconds)) + case <-wsc.quitPingWatchdog: + return + } + } +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/websocket_message.go b/vendor/github.com/mattermost/mattermost-server/v6/model/websocket_message.go new file mode 100644 index 00000000..005eed33 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/websocket_message.go @@ -0,0 +1,293 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" +) + +const ( + WebsocketEventTyping = "typing" + WebsocketEventPosted = "posted" + WebsocketEventPostEdited = "post_edited" + WebsocketEventPostDeleted = "post_deleted" + WebsocketEventPostUnread = "post_unread" + WebsocketEventChannelConverted = "channel_converted" + WebsocketEventChannelCreated = "channel_created" + WebsocketEventChannelDeleted = "channel_deleted" + WebsocketEventChannelRestored = "channel_restored" + WebsocketEventChannelUpdated = "channel_updated" + WebsocketEventChannelMemberUpdated = "channel_member_updated" + WebsocketEventChannelSchemeUpdated = "channel_scheme_updated" + WebsocketEventDirectAdded = "direct_added" + WebsocketEventGroupAdded = "group_added" + WebsocketEventNewUser = "new_user" + WebsocketEventAddedToTeam = "added_to_team" + WebsocketEventLeaveTeam = "leave_team" + WebsocketEventUpdateTeam = "update_team" + WebsocketEventDeleteTeam = "delete_team" + WebsocketEventRestoreTeam = "restore_team" + WebsocketEventUpdateTeamScheme = "update_team_scheme" + WebsocketEventUserAdded = "user_added" + WebsocketEventUserUpdated = "user_updated" + WebsocketEventUserRoleUpdated = "user_role_updated" + WebsocketEventMemberroleUpdated = "memberrole_updated" + WebsocketEventUserRemoved = "user_removed" + WebsocketEventPreferenceChanged = "preference_changed" + WebsocketEventPreferencesChanged = "preferences_changed" + WebsocketEventPreferencesDeleted = "preferences_deleted" + WebsocketEventEphemeralMessage = "ephemeral_message" + WebsocketEventStatusChange = "status_change" + WebsocketEventHello = "hello" + WebsocketAuthenticationChallenge = "authentication_challenge" + WebsocketEventReactionAdded = "reaction_added" + WebsocketEventReactionRemoved = "reaction_removed" + WebsocketEventResponse = "response" + WebsocketEventEmojiAdded = "emoji_added" + WebsocketEventChannelViewed = "channel_viewed" + WebsocketEventPluginStatusesChanged = "plugin_statuses_changed" + WebsocketEventPluginEnabled = "plugin_enabled" + WebsocketEventPluginDisabled = "plugin_disabled" + WebsocketEventRoleUpdated = "role_updated" + WebsocketEventLicenseChanged = "license_changed" + WebsocketEventConfigChanged = "config_changed" + WebsocketEventOpenDialog = "open_dialog" + WebsocketEventGuestsDeactivated = "guests_deactivated" + WebsocketEventUserActivationStatusChange = "user_activation_status_change" + WebsocketEventReceivedGroup = "received_group" + WebsocketEventReceivedGroupAssociatedToTeam = "received_group_associated_to_team" + WebsocketEventReceivedGroupNotAssociatedToTeam = "received_group_not_associated_to_team" + WebsocketEventReceivedGroupAssociatedToChannel = "received_group_associated_to_channel" + WebsocketEventReceivedGroupNotAssociatedToChannel = "received_group_not_associated_to_channel" + WebsocketEventSidebarCategoryCreated = "sidebar_category_created" + WebsocketEventSidebarCategoryUpdated = "sidebar_category_updated" + WebsocketEventSidebarCategoryDeleted = "sidebar_category_deleted" + WebsocketEventSidebarCategoryOrderUpdated = "sidebar_category_order_updated" + WebsocketWarnMetricStatusReceived = "warn_metric_status_received" + WebsocketWarnMetricStatusRemoved = "warn_metric_status_removed" + WebsocketEventCloudPaymentStatusUpdated = "cloud_payment_status_updated" + WebsocketEventThreadUpdated = "thread_updated" + WebsocketEventThreadFollowChanged = "thread_follow_changed" + WebsocketEventThreadReadChanged = "thread_read_changed" + WebsocketFirstAdminVisitMarketplaceStatusReceived = "first_admin_visit_marketplace_status_received" +) + +type WebSocketMessage interface { + ToJSON() ([]byte, error) + 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"` +} + +type WebSocketEvent struct { + event string + data map[string]interface{} + broadcast *WebsocketBroadcast + sequence int64 + 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() ([]byte, error) { + if ev.precomputedJSON != nil { + return []byte(fmt.Sprintf(`{"event": %s, "data": %s, "broadcast": %s, "seq": %d}`, ev.precomputedJSON.Event, ev.precomputedJSON.Data, ev.precomputedJSON.Broadcast, ev.GetSequence())), nil + } + return json.Marshal(webSocketEventJSON{ + ev.event, + ev.data, + ev.broadcast, + ev.sequence, + }) +} + +// Encode encodes the event to the given encoder. +func (ev *WebSocketEvent) Encode(enc *json.Encoder) error { + if ev.precomputedJSON != nil { + return enc.Encode(json.RawMessage( + fmt.Sprintf(`{"event": %s, "data": %s, "broadcast": %s, "seq": %d}`, ev.precomputedJSON.Event, ev.precomputedJSON.Data, ev.precomputedJSON.Broadcast, ev.sequence), + )) + } + + return enc.Encode(webSocketEventJSON{ + ev.event, + ev.data, + ev.broadcast, + ev.sequence, + }) +} + +func WebSocketEventFromJSON(data io.Reader) (*WebSocketEvent, error) { + var ev WebSocketEvent + var o webSocketEventJSON + if err := json.NewDecoder(data).Decode(&o); err != nil { + return nil, err + } + 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, err + } + + var user User + if err = json.Unmarshal(buf, &user); err != nil { + return nil, err + } + o.Data["user"] = &user + } + ev.data = o.Data + ev.broadcast = o.Broadcast + ev.sequence = o.Sequence + return &ev, nil +} + +// 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: StatusFail, SeqReply: seqReply, Error: err} +} + +func (m *WebSocketResponse) IsValid() bool { + return m.Status != "" +} + +func (m *WebSocketResponse) EventType() string { + return WebsocketEventResponse +} + +func (m *WebSocketResponse) ToJSON() ([]byte, error) { + return json.Marshal(m) +} + +func WebSocketResponseFromJSON(data io.Reader) (*WebSocketResponse, error) { + var o *WebSocketResponse + return o, json.NewDecoder(data).Decode(&o) +} diff --git a/vendor/github.com/mattermost/mattermost-server/v6/model/websocket_request.go b/vendor/github.com/mattermost/mattermost-server/v6/model/websocket_request.go new file mode 100644 index 00000000..9e863978 --- /dev/null +++ b/vendor/github.com/mattermost/mattermost-server/v6/model/websocket_request.go @@ -0,0 +1,36 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + + "github.com/mattermost/mattermost-server/v6/shared/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 i18n.TranslateFunc `json:"-"` + Locale string `json:"-"` +} + +func (o *WebSocketRequest) Clone() (*WebSocketRequest, error) { + buf, err := json.Marshal(o) + if err != nil { + return nil, err + } + var ret WebSocketRequest + err = json.Unmarshal(buf, &ret) + if err != nil { + return nil, err + } + return &ret, nil +} |