4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-09-11 12:22:30 +00:00

Update vendor

This commit is contained in:
Wim
2021-10-16 23:11:32 +02:00
parent 57fce93af7
commit 20f6c05ec5
588 changed files with 119386 additions and 3424 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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 ""
}

View File

@@ -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)
}

View File

@@ -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 &copy
}
// 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
}

View File

@@ -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 }

View File

@@ -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,
}
}

View File

@@ -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 &copy
}
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))
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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))
}

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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)
}

View File

@@ -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"`
}

View File

@@ -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"`
}

File diff suppressed because it is too large Load Diff

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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 = ""
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 &copy
}
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
}

View File

@@ -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,
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

File diff suppressed because one or more lines are too long

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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,
}
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
const (
UserAuthServiceGitlab = "gitlab"
)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"
)

View File

@@ -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()
}

View File

@@ -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())
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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"
)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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"`
}

View File

@@ -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 &copy
}
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 ""
}

View File

@@ -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"`
}

View File

@@ -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 &copy
}
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(&copy)
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
}

View File

@@ -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,
}
}

View File

@@ -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(&copy)
return string(b), err
}

View File

@@ -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)
}
}
}

View File

@@ -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 its 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"`
}

View File

@@ -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 &copy
}
func (pn *PushNotification) SetDeviceIdAndPlatform(deviceId string) {
index := strings.Index(deviceId, ":")
if index > -1 {
pn.Platform = deviceId[:index]
pn.DeviceId = deviceId[index+1:]
}
}

View File

@@ -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
}

View File

@@ -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("")
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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,
)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 &copySession
}
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"]
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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})")
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}

Some files were not shown because too many files have changed in this diff Show More