MM-27493 Shared channels (MVP) (#17301)
Remote Cluster Service - provides ability for multiple Mattermost cluster instances to create a trusted connection with each other and exchange messages - trusted connections are managed via slash commands (for now) - facilitates features requiring inter-cluster communication, such as Shared Channels Shared Channels Service - provides ability to shared channels between one or more Mattermost cluster instances (using trusted connection) - sharing/unsharing of channels is managed via slash commands (for now)
Этот коммит содержится в:
@@ -51,6 +51,8 @@ func AuditModelTypeConv(val interface{}) (newVal interface{}, converted bool) {
|
||||
return newAuditIncomingWebhook(v), true
|
||||
case *OutgoingWebhook:
|
||||
return newAuditOutgoingWebhook(v), true
|
||||
case *RemoteCluster:
|
||||
return newRemoteCluster(v), true
|
||||
}
|
||||
return val, false
|
||||
}
|
||||
@@ -667,3 +669,42 @@ func (h auditOutgoingWebhook) MarshalJSONObject(enc *gojay.Encoder) {
|
||||
func (h auditOutgoingWebhook) IsNil() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type auditRemoteCluster struct {
|
||||
RemoteId string
|
||||
RemoteTeamId 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.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("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
|
||||
}
|
||||
|
||||
@@ -142,6 +142,14 @@ type ChannelMemberCountByGroup struct {
|
||||
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 {
|
||||
|
||||
@@ -19,27 +19,29 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
HEADER_REQUEST_ID = "X-Request-ID"
|
||||
HEADER_VERSION_ID = "X-Version-ID"
|
||||
HEADER_CLUSTER_ID = "X-Cluster-ID"
|
||||
HEADER_ETAG_SERVER = "ETag"
|
||||
HEADER_ETAG_CLIENT = "If-None-Match"
|
||||
HEADER_FORWARDED = "X-Forwarded-For"
|
||||
HEADER_REAL_IP = "X-Real-IP"
|
||||
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
|
||||
HEADER_TOKEN = "token"
|
||||
HEADER_CSRF_TOKEN = "X-CSRF-Token"
|
||||
HEADER_BEARER = "BEARER"
|
||||
HEADER_AUTH = "Authorization"
|
||||
HEADER_CLOUD_TOKEN = "X-Cloud-Token"
|
||||
HEADER_REQUESTED_WITH = "X-Requested-With"
|
||||
HEADER_REQUESTED_WITH_XML = "XMLHttpRequest"
|
||||
HEADER_RANGE = "Range"
|
||||
STATUS = "status"
|
||||
STATUS_OK = "OK"
|
||||
STATUS_FAIL = "FAIL"
|
||||
STATUS_UNHEALTHY = "UNHEALTHY"
|
||||
STATUS_REMOVE = "REMOVE"
|
||||
HEADER_REQUEST_ID = "X-Request-ID"
|
||||
HEADER_VERSION_ID = "X-Version-ID"
|
||||
HEADER_CLUSTER_ID = "X-Cluster-ID"
|
||||
HEADER_ETAG_SERVER = "ETag"
|
||||
HEADER_ETAG_CLIENT = "If-None-Match"
|
||||
HEADER_FORWARDED = "X-Forwarded-For"
|
||||
HEADER_REAL_IP = "X-Real-IP"
|
||||
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
|
||||
HEADER_TOKEN = "token"
|
||||
HEADER_CSRF_TOKEN = "X-CSRF-Token"
|
||||
HEADER_BEARER = "BEARER"
|
||||
HEADER_AUTH = "Authorization"
|
||||
HEADER_CLOUD_TOKEN = "X-Cloud-Token"
|
||||
HEADER_REMOTECLUSTER_TOKEN = "X-RemoteCluster-Token"
|
||||
HEADER_REMOTECLUSTER_ID = "X-RemoteCluster-Id"
|
||||
HEADER_REQUESTED_WITH = "X-Requested-With"
|
||||
HEADER_REQUESTED_WITH_XML = "XMLHttpRequest"
|
||||
HEADER_RANGE = "Range"
|
||||
STATUS = "status"
|
||||
STATUS_OK = "OK"
|
||||
STATUS_FAIL = "FAIL"
|
||||
STATUS_UNHEALTHY = "UNHEALTHY"
|
||||
STATUS_REMOVE = "REMOVE"
|
||||
|
||||
CLIENT_DIR = "client"
|
||||
|
||||
@@ -559,6 +561,14 @@ func (c *Client4) GetExportRoute(name string) string {
|
||||
return fmt.Sprintf(c.GetExportsRoute()+"/%v", name)
|
||||
}
|
||||
|
||||
func (c *Client4) GetRemoteClusterRoute() string {
|
||||
return "/remotecluster"
|
||||
}
|
||||
|
||||
func (c *Client4) GetSharedChannelsRoute() string {
|
||||
return "/sharedchannels"
|
||||
}
|
||||
|
||||
func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) {
|
||||
return c.DoApiRequest(http.MethodGet, c.ApiUrl+url, "", etag)
|
||||
}
|
||||
@@ -5999,3 +6009,31 @@ func (c *Client4) SendAdminUpgradeRequestEmailOnJoin() *Response {
|
||||
|
||||
return BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client4) GetAllSharedChannels(teamID string, page, perPage int) ([]*SharedChannel, *Response) {
|
||||
url := fmt.Sprintf("%s/%s?page=%d&per_page=%d", c.GetSharedChannelsRoute(), teamID, page, perPage)
|
||||
r, appErr := c.DoApiGet(url, "")
|
||||
if appErr != nil {
|
||||
return nil, BuildErrorResponse(r, appErr)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var channels []*SharedChannel
|
||||
json.NewDecoder(r.Body).Decode(&channels)
|
||||
|
||||
return channels, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client4) GetRemoteClusterInfo(remoteID string) (RemoteClusterInfo, *Response) {
|
||||
url := fmt.Sprintf("%s/remote_info/%s", c.GetSharedChannelsRoute(), remoteID)
|
||||
r, appErr := c.DoApiGet(url, "")
|
||||
if appErr != nil {
|
||||
return RemoteClusterInfo{}, BuildErrorResponse(r, appErr)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var rci RemoteClusterInfo
|
||||
json.NewDecoder(r.Body).Decode(&rci)
|
||||
|
||||
return rci, BuildResponse(r)
|
||||
}
|
||||
|
||||
@@ -940,6 +940,7 @@ type ExperimentalSettings struct {
|
||||
CloudUserLimit *int64 `access:"experimental,write_restrictable"`
|
||||
CloudBilling *bool `access:"experimental,write_restrictable"`
|
||||
EnableSharedChannels *bool `access:"experimental"`
|
||||
EnableRemoteClusterService *bool `access:"experimental"`
|
||||
}
|
||||
|
||||
func (s *ExperimentalSettings) SetDefaults() {
|
||||
@@ -979,6 +980,10 @@ func (s *ExperimentalSettings) SetDefaults() {
|
||||
if s.EnableSharedChannels == nil {
|
||||
s.EnableSharedChannels = NewBool(false)
|
||||
}
|
||||
|
||||
if s.EnableRemoteClusterService == nil {
|
||||
s.EnableRemoteClusterService = NewBool(false)
|
||||
}
|
||||
}
|
||||
|
||||
type AnalyticsSettings struct {
|
||||
|
||||
@@ -19,6 +19,12 @@ type FeatureFlags struct {
|
||||
// Toggle on and off support for Collapsed Threads
|
||||
CollapsedThreads bool
|
||||
|
||||
// Enable the remote cluster service for shared channels.
|
||||
EnableRemoteClusterService bool
|
||||
|
||||
// Toggle on and off support for Custom User Statuses
|
||||
CustomUserStatuses bool
|
||||
|
||||
// AppsEnabled toggle the Apps framework functionalities both in server and client side
|
||||
AppsEnabled bool
|
||||
|
||||
@@ -37,6 +43,7 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.TestBoolFeature = false
|
||||
f.CloudDelinquentEmailJobsEnabled = false
|
||||
f.CollapsedThreads = false
|
||||
f.EnableRemoteClusterService = false
|
||||
f.FilesSearch = false
|
||||
f.AppsEnabled = false
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ type FileInfo struct {
|
||||
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) ToJson() string {
|
||||
@@ -105,6 +106,10 @@ func (fi *FileInfo) PreSave() {
|
||||
if fi.UpdateAt < fi.CreateAt {
|
||||
fi.UpdateAt = fi.CreateAt
|
||||
}
|
||||
|
||||
if fi.RemoteId == nil {
|
||||
fi.RemoteId = NewString("")
|
||||
}
|
||||
}
|
||||
|
||||
func (fi *FileInfo) IsValid() *AppError {
|
||||
|
||||
@@ -240,7 +240,7 @@ func (f *Features) SetDefaults() {
|
||||
}
|
||||
|
||||
if f.RemoteClusterService == nil {
|
||||
f.RemoteClusterService = f.SharedChannels
|
||||
f.RemoteClusterService = NewBool(*f.FutureFeatures)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ type Post struct {
|
||||
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:"-"`
|
||||
@@ -206,6 +207,7 @@ func (o *Post) ShallowCopy(dst *Post) error {
|
||||
dst.Participants = o.Participants
|
||||
dst.LastReplyAt = o.LastReplyAt
|
||||
dst.Metadata = o.Metadata
|
||||
dst.RemoteId = o.RemoteId
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -235,6 +237,18 @@ type GetPostsSinceOptions struct {
|
||||
SkipFetchThreads bool
|
||||
CollapsedThreads bool
|
||||
CollapsedThreadsExtended bool
|
||||
SortAscending bool
|
||||
}
|
||||
|
||||
type GetPostsSinceForSyncOptions struct {
|
||||
ChannelId string
|
||||
Since int64 // inclusive
|
||||
Until int64 // inclusive
|
||||
SortDescending bool
|
||||
ExcludeRemoteId string
|
||||
IncludeDeleted bool
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type GetPostsOptions struct {
|
||||
@@ -452,6 +466,11 @@ func (o *Post) IsSystemMessage() bool {
|
||||
return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX
|
||||
}
|
||||
|
||||
// IsRemote returns true if the post originated on a remote cluster.
|
||||
func (o *Post) IsRemote() bool {
|
||||
return o.RemoteId != nil && *o.RemoteId != ""
|
||||
}
|
||||
|
||||
func (o *Post) IsJoinLeaveMessage() bool {
|
||||
return o.Type == POST_JOIN_LEAVE ||
|
||||
o.Type == POST_ADD_REMOVE ||
|
||||
|
||||
@@ -27,6 +27,11 @@ func NewPostList() *PostList {
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@ import (
|
||||
)
|
||||
|
||||
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"`
|
||||
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) ToJson() string {
|
||||
@@ -94,8 +95,16 @@ func (o *Reaction) PreSave() {
|
||||
}
|
||||
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("")
|
||||
}
|
||||
}
|
||||
|
||||
295
model/remote_cluster.go
Обычный файл
295
model/remote_cluster.go
Обычный файл
@@ -0,0 +1,295 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
RemoteOfflineAfterMillis = 1000 * 60 * 5 // 5 minutes
|
||||
)
|
||||
|
||||
type RemoteCluster struct {
|
||||
RemoteId string `json:"remote_id"`
|
||||
RemoteTeamId string `json:"remote_team_id"`
|
||||
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.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 rc.DisplayName == "" {
|
||||
return NewAppError("RemoteCluster.IsValid", "model.cluster.is_valid.name.app_error", nil, "display_name empty", 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 (rc *RemoteCluster) PreUpdate() {
|
||||
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) ToJSON() (string, error) {
|
||||
b, err := json.Marshal(rc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (rc *RemoteCluster) ToRemoteClusterInfo() RemoteClusterInfo {
|
||||
return RemoteClusterInfo{
|
||||
DisplayName: rc.DisplayName,
|
||||
CreateAt: rc.CreateAt,
|
||||
LastPingAt: rc.LastPingAt,
|
||||
}
|
||||
}
|
||||
|
||||
func RemoteClusterFromJSON(data io.Reader) (*RemoteCluster, *AppError) {
|
||||
var rc RemoteCluster
|
||||
err := json.NewDecoder(data).Decode(&rc)
|
||||
if err != nil {
|
||||
return nil, NewAppError("RemoteClusterFromJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return &rc, nil
|
||||
}
|
||||
|
||||
// RemoteClusterInfo provides a subset of RemoteCluster fields suitable for sending to clients.
|
||||
type RemoteClusterInfo struct {
|
||||
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
|
||||
}
|
||||
|
||||
func RemoteClusterFrameFromJSON(data io.Reader) (*RemoteClusterFrame, *AppError) {
|
||||
var frame RemoteClusterFrame
|
||||
err := json.NewDecoder(data).Decode(&frame)
|
||||
if err != nil {
|
||||
return nil, NewAppError("RemoteClusterFrameFromJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return &frame, 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
|
||||
}
|
||||
|
||||
func RemoteClusterMsgFromJSON(data io.Reader) (RemoteClusterMsg, *AppError) {
|
||||
var msg RemoteClusterMsg
|
||||
err := json.NewDecoder(data).Decode(&msg)
|
||||
if err != nil {
|
||||
return RemoteClusterMsg{}, NewAppError("RemoteClusterMsgFromJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return msg, 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"`
|
||||
}
|
||||
|
||||
func RemoteClusterPingFromRawJSON(raw json.RawMessage) (RemoteClusterPing, *AppError) {
|
||||
var ping RemoteClusterPing
|
||||
err := json.Unmarshal(raw, &ping)
|
||||
if err != nil {
|
||||
return RemoteClusterPing{}, NewAppError("RemoteClusterPingFromRawJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return ping, nil
|
||||
}
|
||||
|
||||
// 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 RemoteClusterInviteFromRawJSON(raw json.RawMessage) (*RemoteClusterInvite, *AppError) {
|
||||
var invite RemoteClusterInvite
|
||||
err := json.Unmarshal(raw, &invite)
|
||||
if err != nil {
|
||||
return nil, NewAppError("RemoteClusterInviteFromRawJSON", "model.utils.decode_json.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return &invite, nil
|
||||
}
|
||||
|
||||
func (rci *RemoteClusterInvite) Encrypt(password string) ([]byte, error) {
|
||||
raw, err := json.Marshal(&rci)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// hash the pasword to 32 bytes for AES256
|
||||
key := sha512.Sum512_256([]byte(password))
|
||||
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.
|
||||
return gcm.Seal(nonce, nonce, raw, nil), nil
|
||||
}
|
||||
|
||||
func (rci *RemoteClusterInvite) Decrypt(encrypted []byte, password string) error {
|
||||
// hash the pasword to 32 bytes for AES256
|
||||
key := sha512.Sum512_256([]byte(password))
|
||||
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
|
||||
}
|
||||
158
model/remote_cluster_test.go
Обычный файл
158
model/remote_cluster_test.go
Обычный файл
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRemoteClusterJson(t *testing.T) {
|
||||
o := RemoteCluster{RemoteId: NewId(), DisplayName: "test"}
|
||||
|
||||
json, err := o.ToJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
ro, err := RemoteClusterFromJSON(strings.NewReader(json))
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, o.RemoteId, ro.RemoteId)
|
||||
require.Equal(t, o.DisplayName, ro.DisplayName)
|
||||
}
|
||||
|
||||
func TestRemoteClusterIsValid(t *testing.T) {
|
||||
id := NewId()
|
||||
creator := NewId()
|
||||
now := GetMillis()
|
||||
data := []struct {
|
||||
name string
|
||||
rc *RemoteCluster
|
||||
valid bool
|
||||
}{
|
||||
{name: "Zero value", rc: &RemoteCluster{}, valid: false},
|
||||
{name: "Missing cluster_name", rc: &RemoteCluster{RemoteId: id}, valid: false},
|
||||
{name: "Missing host_name", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster"}, valid: false},
|
||||
{name: "Missing create_at", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "example.com"}, valid: false},
|
||||
{name: "Missing last_ping_at", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "example.com", CreatorId: creator, CreateAt: now}, valid: true},
|
||||
{name: "Missing creator", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "example.com", CreateAt: now, LastPingAt: now}, valid: false},
|
||||
{name: "RemoteCluster valid", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "example.com", CreateAt: now, LastPingAt: now, CreatorId: creator}, valid: true},
|
||||
{name: "Include protocol", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "http://example.com", CreateAt: now, LastPingAt: now, CreatorId: creator}, valid: true},
|
||||
{name: "Include protocol & port", rc: &RemoteCluster{RemoteId: id, DisplayName: "test cluster", SiteURL: "http://example.com:8065", CreateAt: now, LastPingAt: now, CreatorId: creator}, valid: true},
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
err := item.rc.IsValid()
|
||||
if item.valid {
|
||||
assert.Nil(t, err, item.name)
|
||||
} else {
|
||||
assert.NotNil(t, err, item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteClusterPreSave(t *testing.T) {
|
||||
now := GetMillis()
|
||||
|
||||
o := RemoteCluster{RemoteId: NewId(), DisplayName: "test"}
|
||||
o.PreSave()
|
||||
|
||||
require.GreaterOrEqual(t, o.CreateAt, now)
|
||||
}
|
||||
|
||||
func TestRemoteClusterMsgJson(t *testing.T) {
|
||||
o := NewRemoteClusterMsg("shared_channel", []byte("{\"hello\":\"world\"}"))
|
||||
|
||||
json, err := json.Marshal(o)
|
||||
require.NoError(t, err)
|
||||
|
||||
ro, err := RemoteClusterMsgFromJSON(strings.NewReader(string(json)))
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, o.Id, ro.Id)
|
||||
require.Equal(t, o.CreateAt, ro.CreateAt)
|
||||
require.Equal(t, o.Topic, ro.Topic)
|
||||
}
|
||||
|
||||
func TestRemoteClusterMsgIsValid(t *testing.T) {
|
||||
id := NewId()
|
||||
now := GetMillis()
|
||||
data := []struct {
|
||||
name string
|
||||
msg *RemoteClusterMsg
|
||||
valid bool
|
||||
}{
|
||||
{name: "Zero value", msg: &RemoteClusterMsg{}, valid: false},
|
||||
{name: "Missing remote id", msg: &RemoteClusterMsg{Id: id}, valid: false},
|
||||
{name: "Missing Topic", msg: &RemoteClusterMsg{Id: id}, valid: false},
|
||||
{name: "Missing Payload", msg: &RemoteClusterMsg{Id: id, CreateAt: now, Topic: "shared_channel"}, valid: false},
|
||||
{name: "RemoteClusterMsg valid", msg: &RemoteClusterMsg{Id: id, CreateAt: now, Topic: "shared_channel", Payload: []byte("{\"hello\":\"world\"}")}, valid: true},
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
err := item.msg.IsValid()
|
||||
if item.valid {
|
||||
assert.Nil(t, err, item.name)
|
||||
} else {
|
||||
assert.NotNil(t, err, item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixTopics(t *testing.T) {
|
||||
testData := []struct {
|
||||
topics string
|
||||
expected string
|
||||
}{
|
||||
{topics: "", expected: ""},
|
||||
{topics: " ", expected: ""},
|
||||
{topics: "share", expected: " share "},
|
||||
{topics: "share incident", expected: " share incident "},
|
||||
{topics: " share incident ", expected: " share incident "},
|
||||
{topics: " share incident ", expected: " share incident "},
|
||||
}
|
||||
|
||||
for _, tt := range testData {
|
||||
rc := &RemoteCluster{Topics: tt.topics}
|
||||
rc.fixTopics()
|
||||
assert.Equal(t, tt.expected, rc.Topics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteClusterInviteEncryption(t *testing.T) {
|
||||
testData := []struct {
|
||||
name string
|
||||
badDecrypt bool
|
||||
password string
|
||||
invite RemoteClusterInvite
|
||||
}{
|
||||
{name: "empty password", badDecrypt: false, password: "", invite: RemoteClusterInvite{RemoteId: NewId(), SiteURL: "https://example.com:8065", Token: NewId()}},
|
||||
{name: "good password", badDecrypt: false, password: "Ultra secret password!", invite: RemoteClusterInvite{RemoteId: NewId(), SiteURL: "https://example.com:8065", Token: NewId()}},
|
||||
{name: "bad decrypt", badDecrypt: true, password: "correct horse battery staple", invite: RemoteClusterInvite{RemoteId: NewId(), SiteURL: "https://example.com:8065", Token: NewId()}},
|
||||
}
|
||||
|
||||
for _, tt := range testData {
|
||||
encrypted, err := tt.invite.Encrypt(tt.password)
|
||||
require.NoError(t, err)
|
||||
|
||||
invite := RemoteClusterInvite{}
|
||||
if tt.badDecrypt {
|
||||
buf := make([]byte, len(encrypted))
|
||||
_, err = io.ReadFull(rand.Reader, buf)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = invite.Decrypt(buf, tt.password)
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
err = invite.Decrypt(encrypted, tt.password)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.invite, invite)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ const (
|
||||
SESSION_PROP_IS_BOT_VALUE = "true"
|
||||
SESSION_TYPE_USER_ACCESS_TOKEN = "UserAccessToken"
|
||||
SESSION_TYPE_CLOUD_KEY = "CloudKey"
|
||||
SESSION_TYPE_REMOTECLUSTER_TOKEN = "RemoteClusterToken"
|
||||
SESSION_PROP_IS_GUEST = "is_guest"
|
||||
SESSION_ACTIVITY_TIMEOUT = 1000 * 60 * 5 // 5 minutes
|
||||
SESSION_USER_ACCESS_TOKEN_EXPIRY = 100 * 365 // 100 years
|
||||
|
||||
267
model/shared_channel.go
Обычный файл
267
model/shared_channel.go
Обычный файл
@@ -0,0 +1,267 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"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:"channel_id"`
|
||||
TeamId string `json:"team_id"`
|
||||
Home bool `json:"home"`
|
||||
ReadOnly bool `json:"readonly"`
|
||||
ShareName string `json:"share_name"`
|
||||
ShareDisplayName string `json:"share_displayname"`
|
||||
SharePurpose string `json:"share_purpose"`
|
||||
ShareHeader string `json:"share_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 string `db:"-"`
|
||||
}
|
||||
|
||||
func (sc *SharedChannel) ToJson() string {
|
||||
b, _ := json.Marshal(sc)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func SharedChannelFromJson(data io.Reader) (*SharedChannel, error) {
|
||||
var sc *SharedChannel
|
||||
err := json.NewDecoder(data).Decode(&sc)
|
||||
return sc, err
|
||||
}
|
||||
|
||||
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 != CHANNEL_DIRECT && !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) > CHANNEL_DISPLAY_NAME_MAX_RUNES {
|
||||
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) > CHANNEL_HEADER_MAX_RUNES {
|
||||
return NewAppError("SharedChannel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+sc.ChannelId, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if utf8.RuneCountInString(sc.SharePurpose) > CHANNEL_PURPOSE_MAX_RUNES {
|
||||
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"`
|
||||
Description string `json:"description"`
|
||||
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"`
|
||||
NextSyncAt int64 `json:"next_sync_at"`
|
||||
}
|
||||
|
||||
func (sc *SharedChannelRemote) ToJson() string {
|
||||
b, _ := json.Marshal(sc)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func SharedChannelRemoteFromJson(data io.Reader) (*SharedChannelRemote, error) {
|
||||
var sc *SharedChannelRemote
|
||||
err := json.NewDecoder(data).Decode(&sc)
|
||||
return sc, err
|
||||
}
|
||||
|
||||
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 len(sc.Description) > 64 {
|
||||
return NewAppError("SharedChannelRemote.IsValid", "model.channel.is_valid.description.app_error", nil, "description="+sc.Description, 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"`
|
||||
Description string `json:"description"`
|
||||
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"`
|
||||
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.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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
87
model/shared_channel_test.go
Обычный файл
87
model/shared_channel_test.go
Обычный файл
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSharedChannelJson(t *testing.T) {
|
||||
o := SharedChannel{ChannelId: NewId(), ShareName: NewId()}
|
||||
json := o.ToJson()
|
||||
ro, err := SharedChannelFromJson(strings.NewReader(json))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, o.ChannelId, ro.ChannelId)
|
||||
require.Equal(t, o.ShareName, ro.ShareName)
|
||||
}
|
||||
|
||||
func TestSharedChannelIsValid(t *testing.T) {
|
||||
id := NewId()
|
||||
now := GetMillis()
|
||||
data := []struct {
|
||||
name string
|
||||
sc *SharedChannel
|
||||
valid bool
|
||||
}{
|
||||
{name: "Zero value", sc: &SharedChannel{}, valid: false},
|
||||
{name: "Missing team_id", sc: &SharedChannel{ChannelId: id}, valid: false},
|
||||
{name: "Missing create_at", sc: &SharedChannel{ChannelId: id, TeamId: id}, valid: false},
|
||||
{name: "Missing update_at", sc: &SharedChannel{ChannelId: id, TeamId: id, CreateAt: now}, valid: false},
|
||||
{name: "Missing share_name", sc: &SharedChannel{ChannelId: id, TeamId: id, CreateAt: now, UpdateAt: now}, valid: false},
|
||||
{name: "Invalid share_name", sc: &SharedChannel{ChannelId: id, TeamId: id, CreateAt: now, UpdateAt: now,
|
||||
ShareName: "@test@"}, valid: false},
|
||||
{name: "Too long share_name", sc: &SharedChannel{ChannelId: id, TeamId: id, CreateAt: now, UpdateAt: now,
|
||||
ShareName: strings.Repeat("01234567890", 100)}, valid: false},
|
||||
{name: "Missing creator_id", sc: &SharedChannel{ChannelId: id, TeamId: id, CreateAt: now, UpdateAt: now,
|
||||
ShareName: "test"}, valid: false},
|
||||
{name: "Missing remote_id", sc: &SharedChannel{ChannelId: id, TeamId: id, CreateAt: now, UpdateAt: now,
|
||||
ShareName: "test", CreatorId: id}, valid: false},
|
||||
{name: "Valid shared channel", sc: &SharedChannel{ChannelId: id, TeamId: id, CreateAt: now, UpdateAt: now,
|
||||
ShareName: "test", CreatorId: id, RemoteId: id}, valid: true},
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
err := item.sc.IsValid()
|
||||
if item.valid {
|
||||
assert.Nil(t, err, item.name)
|
||||
} else {
|
||||
assert.NotNil(t, err, item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedChannelPreSave(t *testing.T) {
|
||||
now := GetMillis()
|
||||
|
||||
o := SharedChannel{ChannelId: NewId(), ShareName: "test"}
|
||||
o.PreSave()
|
||||
|
||||
require.GreaterOrEqual(t, o.CreateAt, now)
|
||||
require.GreaterOrEqual(t, o.UpdateAt, now)
|
||||
}
|
||||
|
||||
func TestSharedChannelPreUpdate(t *testing.T) {
|
||||
now := GetMillis()
|
||||
|
||||
o := SharedChannel{ChannelId: NewId(), ShareName: "test"}
|
||||
o.PreUpdate()
|
||||
|
||||
require.GreaterOrEqual(t, o.UpdateAt, now)
|
||||
}
|
||||
|
||||
func TestSharedChannelRemoteJson(t *testing.T) {
|
||||
o := SharedChannelRemote{Id: NewId(), ChannelId: NewId(), Description: "Test"}
|
||||
json := o.ToJson()
|
||||
ro, err := SharedChannelRemoteFromJson(strings.NewReader(json))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, o.Id, ro.Id)
|
||||
require.Equal(t, o.ChannelId, ro.ChannelId)
|
||||
require.Equal(t, o.Description, ro.Description)
|
||||
}
|
||||
@@ -42,6 +42,10 @@ type UploadSession struct {
|
||||
// The amount of received data in bytes. If equal to FileSize it means the
|
||||
// upload has finished.
|
||||
FileOffset int64 `json:"file_offset"`
|
||||
// Id of remote cluster if uploading for shared channel
|
||||
RemoteId string `json:"remote_id"`
|
||||
// Requested file id if uploading for shared channel
|
||||
ReqFileId string `json:"req_file_id"`
|
||||
}
|
||||
|
||||
// ToJson serializes the UploadSession into JSON and returns it as string.
|
||||
|
||||
@@ -90,6 +90,7 @@ type User struct {
|
||||
Timezone StringMap `json:"timezone"`
|
||||
MfaActive bool `json:"mfa_active,omitempty"`
|
||||
MfaSecret string `json:"mfa_secret,omitempty"`
|
||||
RemoteId *string `json:"remote_id,omitempty"`
|
||||
LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"`
|
||||
IsBot bool `db:"-" json:"is_bot,omitempty"`
|
||||
BotDescription string `db:"-" json:"bot_description,omitempty"`
|
||||
@@ -124,6 +125,7 @@ type UserPatch struct {
|
||||
NotifyProps StringMap `json:"notify_props,omitempty"`
|
||||
Locale *string `json:"locale"`
|
||||
Timezone StringMap `json:"timezone"`
|
||||
RemoteId *string `json:"remote_id"`
|
||||
}
|
||||
|
||||
//msgp:ignore UserAuth
|
||||
@@ -512,6 +514,10 @@ func (u *User) Patch(patch *UserPatch) {
|
||||
if patch.Timezone != nil {
|
||||
u.Timezone = patch.Timezone
|
||||
}
|
||||
|
||||
if patch.RemoteId != nil {
|
||||
u.RemoteId = patch.RemoteId
|
||||
}
|
||||
}
|
||||
|
||||
// ToJson convert a User to a json string
|
||||
@@ -734,6 +740,11 @@ func (u *User) GetPreferredTimezone() string {
|
||||
return GetPreferredTimezone(u.Timezone)
|
||||
}
|
||||
|
||||
// IsRemote returns true if the user belongs to a remote cluster (has RemoteId).
|
||||
func (u *User) IsRemote() bool {
|
||||
return u.RemoteId != nil && *u.RemoteId != ""
|
||||
}
|
||||
|
||||
func (u *User) ToPatch() *UserPatch {
|
||||
return &UserPatch{
|
||||
Username: &u.Username, Password: &u.Password,
|
||||
|
||||
@@ -17,8 +17,8 @@ func (z *User) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != 31 {
|
||||
err = msgp.ArrayError{Wanted: 31, Got: zb0001}
|
||||
if zb0001 != 32 {
|
||||
err = msgp.ArrayError{Wanted: 32, Got: zb0001}
|
||||
return
|
||||
}
|
||||
z.Id, err = dc.ReadString()
|
||||
@@ -158,6 +158,23 @@ func (z *User) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, "MfaSecret")
|
||||
return
|
||||
}
|
||||
if dc.IsNil() {
|
||||
err = dc.ReadNil()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "RemoteId")
|
||||
return
|
||||
}
|
||||
z.RemoteId = nil
|
||||
} else {
|
||||
if z.RemoteId == nil {
|
||||
z.RemoteId = new(string)
|
||||
}
|
||||
*z.RemoteId, err = dc.ReadString()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "RemoteId")
|
||||
return
|
||||
}
|
||||
}
|
||||
z.LastActivityAt, err = dc.ReadInt64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastActivityAt")
|
||||
@@ -193,8 +210,8 @@ func (z *User) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *User) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// array header, size 31
|
||||
err = en.Append(0xdc, 0x0, 0x1f)
|
||||
// array header, size 32
|
||||
err = en.Append(0xdc, 0x0, 0x20)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -330,6 +347,18 @@ func (z *User) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "MfaSecret")
|
||||
return
|
||||
}
|
||||
if z.RemoteId == nil {
|
||||
err = en.WriteNil()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = en.WriteString(*z.RemoteId)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "RemoteId")
|
||||
return
|
||||
}
|
||||
}
|
||||
err = en.WriteInt64(z.LastActivityAt)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastActivityAt")
|
||||
@@ -366,8 +395,8 @@ func (z *User) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *User) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// array header, size 31
|
||||
o = append(o, 0xdc, 0x0, 0x1f)
|
||||
// array header, size 32
|
||||
o = append(o, 0xdc, 0x0, 0x20)
|
||||
o = msgp.AppendString(o, z.Id)
|
||||
o = msgp.AppendInt64(o, z.CreateAt)
|
||||
o = msgp.AppendInt64(o, z.UpdateAt)
|
||||
@@ -409,6 +438,11 @@ func (z *User) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
}
|
||||
o = msgp.AppendBool(o, z.MfaActive)
|
||||
o = msgp.AppendString(o, z.MfaSecret)
|
||||
if z.RemoteId == nil {
|
||||
o = msgp.AppendNil(o)
|
||||
} else {
|
||||
o = msgp.AppendString(o, *z.RemoteId)
|
||||
}
|
||||
o = msgp.AppendInt64(o, z.LastActivityAt)
|
||||
o = msgp.AppendBool(o, z.IsBot)
|
||||
o = msgp.AppendString(o, z.BotDescription)
|
||||
@@ -426,8 +460,8 @@ func (z *User) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != 31 {
|
||||
err = msgp.ArrayError{Wanted: 31, Got: zb0001}
|
||||
if zb0001 != 32 {
|
||||
err = msgp.ArrayError{Wanted: 32, Got: zb0001}
|
||||
return
|
||||
}
|
||||
z.Id, bts, err = msgp.ReadStringBytes(bts)
|
||||
@@ -566,6 +600,22 @@ func (z *User) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "MfaSecret")
|
||||
return
|
||||
}
|
||||
if msgp.IsNil(bts) {
|
||||
bts, err = msgp.ReadNilBytes(bts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
z.RemoteId = nil
|
||||
} else {
|
||||
if z.RemoteId == nil {
|
||||
z.RemoteId = new(string)
|
||||
}
|
||||
*z.RemoteId, bts, err = msgp.ReadStringBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "RemoteId")
|
||||
return
|
||||
}
|
||||
}
|
||||
z.LastActivityAt, bts, err = msgp.ReadInt64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastActivityAt")
|
||||
@@ -608,7 +658,13 @@ func (z *User) Msgsize() (s int) {
|
||||
} else {
|
||||
s += msgp.StringPrefixSize + len(*z.AuthData)
|
||||
}
|
||||
s += msgp.StringPrefixSize + len(z.AuthService) + msgp.StringPrefixSize + len(z.Email) + msgp.BoolSize + msgp.StringPrefixSize + len(z.Nickname) + msgp.StringPrefixSize + len(z.FirstName) + msgp.StringPrefixSize + len(z.LastName) + msgp.StringPrefixSize + len(z.Position) + msgp.StringPrefixSize + len(z.Roles) + msgp.BoolSize + z.Props.Msgsize() + z.NotifyProps.Msgsize() + msgp.Int64Size + msgp.Int64Size + msgp.IntSize + msgp.StringPrefixSize + len(z.Locale) + z.Timezone.Msgsize() + msgp.BoolSize + msgp.StringPrefixSize + len(z.MfaSecret) + msgp.Int64Size + msgp.BoolSize + msgp.StringPrefixSize + len(z.BotDescription) + msgp.Int64Size + msgp.StringPrefixSize + len(z.TermsOfServiceId) + msgp.Int64Size
|
||||
s += msgp.StringPrefixSize + len(z.AuthService) + msgp.StringPrefixSize + len(z.Email) + msgp.BoolSize + msgp.StringPrefixSize + len(z.Nickname) + msgp.StringPrefixSize + len(z.FirstName) + msgp.StringPrefixSize + len(z.LastName) + msgp.StringPrefixSize + len(z.Position) + msgp.StringPrefixSize + len(z.Roles) + msgp.BoolSize + z.Props.Msgsize() + z.NotifyProps.Msgsize() + msgp.Int64Size + msgp.Int64Size + msgp.IntSize + msgp.StringPrefixSize + len(z.Locale) + z.Timezone.Msgsize() + msgp.BoolSize + msgp.StringPrefixSize + len(z.MfaSecret)
|
||||
if z.RemoteId == nil {
|
||||
s += msgp.NilSize
|
||||
} else {
|
||||
s += msgp.StringPrefixSize + len(*z.RemoteId)
|
||||
}
|
||||
s += msgp.Int64Size + msgp.BoolSize + msgp.StringPrefixSize + len(z.BotDescription) + msgp.Int64Size + msgp.StringPrefixSize + len(z.TermsOfServiceId) + msgp.Int64Size
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -195,6 +195,11 @@ func GetMillisForTime(thisTime time.Time) int64 {
|
||||
return thisTime.UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch.
|
||||
func GetTimeForMillis(millis int64) time.Time {
|
||||
return time.Unix(0, millis*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
// PadDateStringZeros is a convenience method to pad 2 digit date parts with zeros to meet ISO 8601 format
|
||||
func PadDateStringZeros(dateString string) string {
|
||||
parts := strings.Split(dateString, "-")
|
||||
|
||||
@@ -40,6 +40,14 @@ func TestGetMillisForTime(t *testing.T) {
|
||||
require.Equalf(t, thisTimeMillis, result, "millis are not the same: %d and %d", thisTimeMillis, result)
|
||||
}
|
||||
|
||||
func TestGetTimeForMillis(t *testing.T) {
|
||||
thisTimeMillis := int64(1471219200000)
|
||||
thisTime := time.Date(2016, time.August, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
result := GetTimeForMillis(thisTimeMillis)
|
||||
require.True(t, thisTime.Equal(result))
|
||||
}
|
||||
|
||||
func TestPadDateStringZeros(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
Name string
|
||||
|
||||
Ссылка в новой задаче
Block a user