summaryrefslogtreecommitdiffstats
path: root/model
diff options
context:
space:
mode:
Diffstat (limited to 'model')
-rw-r--r--model/client4.go76
-rw-r--r--model/config.go20
-rw-r--r--model/scheduled_task.go97
-rw-r--r--model/scheduled_task_test.go163
-rw-r--r--model/team.go27
-rw-r--r--model/version.go1
-rw-r--r--model/websocket_message.go25
-rw-r--r--model/websocket_message_test.go48
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()
+ }
+ })
+}