diff options
author | George Goldberg <george@gberg.me> | 2018-03-02 15:55:03 +0000 |
---|---|---|
committer | George Goldberg <george@gberg.me> | 2018-03-02 15:55:03 +0000 |
commit | 901acc9703ae58b625b44e7abfd02333b9bab951 (patch) | |
tree | 1a8fc17a85544bc7b8064874923e2fe6e3f44354 /model | |
parent | 21afaf4bedcad578d4f876bb315d1072ccd296e6 (diff) | |
parent | 2b3b6051d265edf131d006b2eb14f55284faf1e5 (diff) | |
download | chat-901acc9703ae58b625b44e7abfd02333b9bab951.tar.gz chat-901acc9703ae58b625b44e7abfd02333b9bab951.tar.bz2 chat-901acc9703ae58b625b44e7abfd02333b9bab951.zip |
Merge branch 'master' into advanced-permissions-phase-1
Diffstat (limited to 'model')
-rw-r--r-- | model/client4.go | 76 | ||||
-rw-r--r-- | model/config.go | 20 | ||||
-rw-r--r-- | model/scheduled_task.go | 97 | ||||
-rw-r--r-- | model/scheduled_task_test.go | 163 | ||||
-rw-r--r-- | model/team.go | 27 | ||||
-rw-r--r-- | model/version.go | 1 | ||||
-rw-r--r-- | model/websocket_message.go | 25 | ||||
-rw-r--r-- | model/websocket_message_test.go | 48 |
8 files changed, 240 insertions, 217 deletions
diff --git a/model/client4.go b/model/client4.go index 4b50aa05f..8b17eaa7d 100644 --- a/model/client4.go +++ b/model/client4.go @@ -198,6 +198,10 @@ func (c *Client4) GetTestEmailRoute() string { return fmt.Sprintf("/email/test") } +func (c *Client4) GetTestS3Route() string { + return fmt.Sprintf("/file/s3_test") +} + func (c *Client4) GetDatabaseRoute() string { return fmt.Sprintf("/database") } @@ -1919,7 +1923,8 @@ func (c *Client4) DoPostAction(postId, actionId string) (bool, *Response) { // File Section -// UploadFile will upload a file to a channel, to be later attached to a post. +// UploadFile will upload a file to a channel using a multipart request, to be later attached to a post. +// This method is functionally equivalent to Client4.UploadFileAsRequestBody. func (c *Client4) UploadFile(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -1943,6 +1948,12 @@ func (c *Client4) UploadFile(data []byte, channelId string, filename string) (*F return c.DoUploadFile(c.GetFilesRoute(), body.Bytes(), writer.FormDataContentType()) } +// UploadFileAsRequestBody will upload a file to a channel as the body of a request, to be later attached +// to a post. This method is functionally equivalent to Client4.UploadFile. +func (c *Client4) UploadFileAsRequestBody(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) { + return c.DoUploadFile(c.GetFilesRoute()+fmt.Sprintf("?channel_id=%v&filename=%v", url.QueryEscape(channelId), url.QueryEscape(filename)), data, http.DetectContentType(data)) +} + // GetFile gets the bytes for a file by id. func (c *Client4) GetFile(fileId string) ([]byte, *Response) { if r, err := c.DoApiGet(c.GetFileRoute(fileId), ""); err != nil { @@ -2089,6 +2100,16 @@ func (c *Client4) TestEmail() (bool, *Response) { } } +// TestS3Connection will attempt to connect to the AWS S3. +func (c *Client4) TestS3Connection(config *Config) (bool, *Response) { + if r, err := c.DoApiPost(c.GetTestS3Route(), config.ToJson()); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + // GetConfig will retrieve the server config with some sanitized items. func (c *Client4) GetConfig() (*Config, *Response) { if r, err := c.DoApiGet(c.GetConfigRoute(), ""); err != nil { @@ -3343,3 +3364,56 @@ func (c *Client4) DeactivatePlugin(id string) (bool, *Response) { return CheckStatusOK(r), BuildResponse(r) } } + +// SetTeamIcon sets team icon of the team +func (c *Client4) SetTeamIcon(teamId string, data []byte) (bool, *Response) { + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + if part, err := writer.CreateFormFile("image", "teamIcon.png"); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } else if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + if err := writer.Close(); err != nil { + return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.writer.app_error", nil, err.Error(), http.StatusBadRequest)} + } + + rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetTeamRoute(teamId)+"/image", bytes.NewReader(body.Bytes())) + rq.Header.Set("Content-Type", writer.FormDataContentType()) + rq.Close = true + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil || rp == nil { + // set to http.StatusForbidden(403) + return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetTeamRoute(teamId)+"/image", "model.client.connecting.app_error", nil, err.Error(), 403)} + } else { + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } else { + return CheckStatusOK(rp), BuildResponse(rp) + } + } +} + +// GetTeamIcon gets the team icon of the team +func (c *Client4) GetTeamIcon(teamId, etag string) ([]byte, *Response) { + if r, err := c.DoApiGet(c.GetTeamRoute(teamId)+"/image", etag); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + + if data, err := ioutil.ReadAll(r.Body); err != nil { + return nil, BuildErrorResponse(r, NewAppError("GetTeamIcon", "model.client.get_team_icon.app_error", nil, err.Error(), r.StatusCode)) + } else { + return data, BuildResponse(r) + } + } +} diff --git a/model/config.go b/model/config.go index 93fa5957c..c8cd0f0a1 100644 --- a/model/config.go +++ b/model/config.go @@ -165,6 +165,7 @@ const ( type ServiceSettings struct { SiteURL *string + WebsocketURL *string LicenseFileLocation *string ListenAddress *string ConnectionSecurity *string @@ -196,6 +197,7 @@ type ServiceSettings struct { EnforceMultifactorAuthentication *bool EnableUserAccessTokens *bool AllowCorsFrom *string + AllowCookiesForSubdomains *bool SessionLengthWebInDays *int SessionLengthMobileInDays *int SessionLengthSSOInDays *int @@ -232,6 +234,10 @@ func (s *ServiceSettings) SetDefaults() { s.SiteURL = NewString(SERVICE_SETTINGS_DEFAULT_SITE_URL) } + if s.WebsocketURL == nil { + s.WebsocketURL = NewString("") + } + if s.LicenseFileLocation == nil { s.LicenseFileLocation = NewString("") } @@ -388,6 +394,10 @@ func (s *ServiceSettings) SetDefaults() { s.AllowCorsFrom = NewString(SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM) } + if s.AllowCookiesForSubdomains == nil { + s.AllowCookiesForSubdomains = NewBool(false) + } + if s.WebserverMode == nil { s.WebserverMode = NewString("gzip") } else if *s.WebserverMode == "regular" { @@ -1782,6 +1792,10 @@ func (o *Config) IsValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.cluster_email_batching.app_error", nil, "", http.StatusBadRequest) } + if len(*o.ServiceSettings.SiteURL) == 0 && *o.ServiceSettings.AllowCookiesForSubdomains { + return NewAppError("Config.IsValid", "Allowing cookies for subdomains requires SiteURL to be set.", nil, "", http.StatusBadRequest) + } + if err := o.TeamSettings.isValid(); err != nil { return err } @@ -2089,6 +2103,12 @@ func (ss *ServiceSettings) isValid() *AppError { } } + if len(*ss.WebsocketURL) != 0 { + if _, err := url.ParseRequestURI(*ss.WebsocketURL); err != nil { + return NewAppError("Config.IsValid", "model.config.is_valid.websocket_url.app_error", nil, "", http.StatusBadRequest) + } + } + if len(*ss.ListenAddress) == 0 { return NewAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "", http.StatusBadRequest) } diff --git a/model/scheduled_task.go b/model/scheduled_task.go index 453828bd2..f3529dedb 100644 --- a/model/scheduled_task.go +++ b/model/scheduled_task.go @@ -5,7 +5,6 @@ package model import ( "fmt" - "sync" "time" ) @@ -15,89 +14,57 @@ type ScheduledTask struct { Name string `json:"name"` Interval time.Duration `json:"interval"` Recurring bool `json:"recurring"` - function TaskFunc - timer *time.Timer -} - -var taskMutex = sync.Mutex{} -var tasks = make(map[string]*ScheduledTask) - -func addTask(task *ScheduledTask) { - taskMutex.Lock() - defer taskMutex.Unlock() - tasks[task.Name] = task -} - -func removeTaskByName(name string) { - taskMutex.Lock() - defer taskMutex.Unlock() - delete(tasks, name) -} - -func GetTaskByName(name string) *ScheduledTask { - taskMutex.Lock() - defer taskMutex.Unlock() - if task, ok := tasks[name]; ok { - return task - } - return nil -} - -func GetAllTasks() *map[string]*ScheduledTask { - taskMutex.Lock() - defer taskMutex.Unlock() - return &tasks + function func() + cancel chan struct{} + cancelled chan struct{} } func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask { - task := &ScheduledTask{ - Name: name, - Interval: timeToExecution, - Recurring: false, - function: function, - } - - taskRunner := func() { - go task.function() - removeTaskByName(task.Name) - } - - task.timer = time.AfterFunc(timeToExecution, taskRunner) - - addTask(task) - - return task + return createTask(name, function, timeToExecution, false) } func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask { + return createTask(name, function, interval, true) +} + +func createTask(name string, function TaskFunc, interval time.Duration, recurring bool) *ScheduledTask { task := &ScheduledTask{ Name: name, Interval: interval, - Recurring: true, + Recurring: recurring, function: function, + cancel: make(chan struct{}), + cancelled: make(chan struct{}), } - taskRecurer := func() { - go task.function() - task.timer.Reset(task.Interval) - } + go func() { + defer close(task.cancelled) - task.timer = time.AfterFunc(interval, taskRecurer) + ticker := time.NewTicker(interval) + defer func() { + ticker.Stop() + }() - addTask(task) + for { + select { + case <-ticker.C: + function() + case <-task.cancel: + return + } + + if !task.Recurring { + break + } + } + }() return task } func (task *ScheduledTask) Cancel() { - task.timer.Stop() - removeTaskByName(task.Name) -} - -// Executes the task immediatly. A recurring task will be run regularally after interval. -func (task *ScheduledTask) Execute() { - task.function() - task.timer.Reset(task.Interval) + close(task.cancel) + <-task.cancelled } func (task *ScheduledTask) String() string { diff --git a/model/scheduled_task_test.go b/model/scheduled_task_test.go index 5af43b1ef..9537a662a 100644 --- a/model/scheduled_task_test.go +++ b/model/scheduled_task_test.go @@ -4,185 +4,72 @@ package model import ( + "sync/atomic" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestCreateTask(t *testing.T) { TASK_NAME := "Test Task" - TASK_TIME := time.Second * 3 + TASK_TIME := time.Second * 2 - testValue := 0 + executionCount := new(int32) testFunc := func() { - testValue = 1 + atomic.AddInt32(executionCount, 1) } task := CreateTask(TASK_NAME, testFunc, TASK_TIME) - if testValue != 0 { - t.Fatal("Unexpected execuition of task") - } + assert.EqualValues(t, 0, atomic.LoadInt32(executionCount)) time.Sleep(TASK_TIME + time.Second) - if testValue != 1 { - t.Fatal("Task did not execute") - } - - if task.Name != TASK_NAME { - t.Fatal("Bad name") - } - - if task.Interval != TASK_TIME { - t.Fatal("Bad interval") - } - - if task.Recurring { - t.Fatal("should not reccur") - } + assert.EqualValues(t, 1, atomic.LoadInt32(executionCount)) + assert.Equal(t, TASK_NAME, task.Name) + assert.Equal(t, TASK_TIME, task.Interval) + assert.False(t, task.Recurring) } func TestCreateRecurringTask(t *testing.T) { TASK_NAME := "Test Recurring Task" - TASK_TIME := time.Second * 3 + TASK_TIME := time.Second * 2 - testValue := 0 + executionCount := new(int32) testFunc := func() { - testValue += 1 + atomic.AddInt32(executionCount, 1) } task := CreateRecurringTask(TASK_NAME, testFunc, TASK_TIME) - if testValue != 0 { - t.Fatal("Unexpected execuition of task") - } + assert.EqualValues(t, 0, atomic.LoadInt32(executionCount)) time.Sleep(TASK_TIME + time.Second) - if testValue != 1 { - t.Fatal("Task did not execute") - } + assert.EqualValues(t, 1, atomic.LoadInt32(executionCount)) time.Sleep(TASK_TIME) - if testValue != 2 { - t.Fatal("Task did not re-execute") - } - - if task.Name != TASK_NAME { - t.Fatal("Bad name") - } - - if task.Interval != TASK_TIME { - t.Fatal("Bad interval") - } - - if !task.Recurring { - t.Fatal("should reccur") - } + assert.EqualValues(t, 2, atomic.LoadInt32(executionCount)) + assert.Equal(t, TASK_NAME, task.Name) + assert.Equal(t, TASK_TIME, task.Interval) + assert.True(t, task.Recurring) task.Cancel() } func TestCancelTask(t *testing.T) { TASK_NAME := "Test Task" - TASK_TIME := time.Second * 3 + TASK_TIME := time.Second - testValue := 0 + executionCount := new(int32) testFunc := func() { - testValue = 1 + atomic.AddInt32(executionCount, 1) } task := CreateTask(TASK_NAME, testFunc, TASK_TIME) - if testValue != 0 { - t.Fatal("Unexpected execuition of task") - } + assert.EqualValues(t, 0, atomic.LoadInt32(executionCount)) task.Cancel() time.Sleep(TASK_TIME + time.Second) - - if testValue != 0 { - t.Fatal("Unexpected execuition of task") - } -} - -func TestGetAllTasks(t *testing.T) { - doNothing := func() {} - - CreateTask("Task1", doNothing, time.Hour) - CreateTask("Task2", doNothing, time.Second) - CreateRecurringTask("Task3", doNothing, time.Second) - task4 := CreateRecurringTask("Task4", doNothing, time.Second) - - task4.Cancel() - - time.Sleep(time.Second * 3) - - tasks := *GetAllTasks() - if len(tasks) != 2 { - t.Fatal("Wrong number of tasks got: ", len(tasks)) - } - for _, task := range tasks { - if task.Name != "Task1" && task.Name != "Task3" { - t.Fatal("Wrong tasks") - } - } -} - -func TestExecuteTask(t *testing.T) { - TASK_NAME := "Test Task" - TASK_TIME := time.Second * 5 - - testValue := 0 - testFunc := func() { - testValue += 1 - } - - task := CreateTask(TASK_NAME, testFunc, TASK_TIME) - if testValue != 0 { - t.Fatal("Unexpected execuition of task") - } - - task.Execute() - - if testValue != 1 { - t.Fatal("Task did not execute") - } - - time.Sleep(TASK_TIME + time.Second) - - if testValue != 2 { - t.Fatal("Task re-executed") - } -} - -func TestExecuteTaskRecurring(t *testing.T) { - TASK_NAME := "Test Recurring Task" - TASK_TIME := time.Second * 5 - - testValue := 0 - testFunc := func() { - testValue += 1 - } - - task := CreateRecurringTask(TASK_NAME, testFunc, TASK_TIME) - if testValue != 0 { - t.Fatal("Unexpected execuition of task") - } - - time.Sleep(time.Second * 3) - - task.Execute() - if testValue != 1 { - t.Fatal("Task did not execute") - } - - time.Sleep(time.Second * 3) - if testValue != 1 { - t.Fatal("Task should not have executed before 5 seconds") - } - - time.Sleep(time.Second * 3) - - if testValue != 2 { - t.Fatal("Task did not re-execute after forced execution") - } + assert.EqualValues(t, 0, atomic.LoadInt32(executionCount)) } diff --git a/model/team.go b/model/team.go index 5b6eb1fa0..15a708220 100644 --- a/model/team.go +++ b/model/team.go @@ -26,19 +26,20 @@ const ( ) type Team struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - DisplayName string `json:"display_name"` - Name string `json:"name"` - Description string `json:"description"` - Email string `json:"email"` - Type string `json:"type"` - CompanyName string `json:"company_name"` - AllowedDomains string `json:"allowed_domains"` - InviteId string `json:"invite_id"` - AllowOpenInvite bool `json:"allow_open_invite"` + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Description string `json:"description"` + Email string `json:"email"` + Type string `json:"type"` + CompanyName string `json:"company_name"` + AllowedDomains string `json:"allowed_domains"` + InviteId string `json:"invite_id"` + AllowOpenInvite bool `json:"allow_open_invite"` + LastTeamIconUpdate int64 `json:"last_team_icon_update,omitempty"` } type TeamPatch struct { diff --git a/model/version.go b/model/version.go index 1bd7baecc..6e461e5d5 100644 --- a/model/version.go +++ b/model/version.go @@ -13,6 +13,7 @@ import ( // It should be maitained in chronological order with most current // release at the front of the list. var versions = []string{ + "4.7.1", "4.7.0", "4.6.0", "4.5.0", diff --git a/model/websocket_message.go b/model/websocket_message.go index a1427e196..aea77b1b6 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -5,6 +5,7 @@ package model import ( "encoding/json" + "fmt" "io" ) @@ -59,11 +60,32 @@ type WebsocketBroadcast struct { TeamId string `json:"team_id"` // broadcast only occurs for users in this team } +type precomputedWebSocketEventJSON struct { + Event json.RawMessage + Data json.RawMessage + Broadcast json.RawMessage +} + type WebSocketEvent struct { Event string `json:"event"` Data map[string]interface{} `json:"data"` Broadcast *WebsocketBroadcast `json:"broadcast"` Sequence int64 `json:"seq"` + + precomputedJSON *precomputedWebSocketEventJSON +} + +// PrecomputeJSON precomputes and stores the serialized JSON for all fields other than Sequence. +// This makes ToJson much more efficient when sending the same event to multiple connections. +func (m *WebSocketEvent) PrecomputeJSON() { + event, _ := json.Marshal(m.Event) + data, _ := json.Marshal(m.Data) + broadcast, _ := json.Marshal(m.Broadcast) + m.precomputedJSON = &precomputedWebSocketEventJSON{ + Event: json.RawMessage(event), + Data: json.RawMessage(data), + Broadcast: json.RawMessage(broadcast), + } } func (m *WebSocketEvent) Add(key string, value interface{}) { @@ -84,6 +106,9 @@ func (o *WebSocketEvent) EventType() string { } func (o *WebSocketEvent) ToJson() string { + if o.precomputedJSON != nil { + return fmt.Sprintf(`{"event": %s, "data": %s, "broadcast": %s, "seq": %d}`, o.precomputedJSON.Event, o.precomputedJSON.Data, o.precomputedJSON.Broadcast, o.Sequence) + } b, _ := json.Marshal(o) return string(b) } diff --git a/model/websocket_message_test.go b/model/websocket_message_test.go index 1b75d0f6e..10404c299 100644 --- a/model/websocket_message_test.go +++ b/model/websocket_message_test.go @@ -6,6 +6,8 @@ package model import ( "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestWebSocketEvent(t *testing.T) { @@ -54,3 +56,49 @@ func TestWebSocketResponse(t *testing.T) { t.Fatal("Ids do not match") } } + +func TestWebSocketEvent_PrecomputeJSON(t *testing.T) { + event := NewWebSocketEvent(WEBSOCKET_EVENT_POSTED, "foo", "bar", "baz", nil) + event.Sequence = 7 + + before := event.ToJson() + event.PrecomputeJSON() + after := event.ToJson() + + assert.JSONEq(t, before, after) +} + +var stringSink string + +func BenchmarkWebSocketEvent_ToJson(b *testing.B) { + event := NewWebSocketEvent(WEBSOCKET_EVENT_POSTED, "foo", "bar", "baz", nil) + for i := 0; i < 100; i++ { + event.Data[NewId()] = NewId() + } + + b.Run("SerializedNTimes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + stringSink = event.ToJson() + } + }) + + b.Run("PrecomputedNTimes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + event.PrecomputeJSON() + } + }) + + b.Run("PrecomputedAndSerializedNTimes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + event.PrecomputeJSON() + stringSink = event.ToJson() + } + }) + + event.PrecomputeJSON() + b.Run("PrecomputedOnceAndSerializedNTimes", func(b *testing.B) { + for i := 0; i < b.N; i++ { + stringSink = event.ToJson() + } + }) +} |