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)
Этот коммит содержится в:
Doug Lauder
2021-04-01 13:44:56 -04:00
коммит произвёл GitHub
родитель ff980266ac
Коммит 02196e04fa
137 изменённых файлов: 15137 добавлений и 262 удалений

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

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