diff options
Diffstat (limited to 'model')
-rw-r--r-- | model/client4.go | 14 | ||||
-rw-r--r-- | model/command_response.go | 23 | ||||
-rw-r--r-- | model/command_response_test.go | 14 | ||||
-rw-r--r-- | model/config.go | 18 | ||||
-rw-r--r-- | model/incoming_webhook.go | 52 | ||||
-rw-r--r-- | model/incoming_webhook_test.go | 14 | ||||
-rw-r--r-- | model/job.go | 79 | ||||
-rw-r--r-- | model/job_status.go | 59 | ||||
-rw-r--r-- | model/license.go | 10 | ||||
-rw-r--r-- | model/license_test.go | 8 | ||||
-rw-r--r-- | model/outgoing_webhook.go | 63 | ||||
-rw-r--r-- | model/outgoing_webhook_test.go | 2 | ||||
-rw-r--r-- | model/post.go | 6 | ||||
-rw-r--r-- | model/slack_attachment.go | 52 | ||||
-rw-r--r-- | model/slack_attachment_test.go | 38 |
15 files changed, 298 insertions, 154 deletions
diff --git a/model/client4.go b/model/client4.go index 9ecdf9a62..a19a17d3a 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1809,7 +1809,7 @@ func (c *Client4) GetFileInfosForPost(postId string, etag string) ([]*FileInfo, // GetPing will return ok if the running goRoutines are below the threshold and unhealthy for above. func (c *Client4) GetPing() (string, *Response) { - if r, err := c.DoApiGet(c.GetSystemRoute()+"/ping", ""); r.StatusCode == 500 { + if r, err := c.DoApiGet(c.GetSystemRoute()+"/ping", ""); r != nil && r.StatusCode == 500 { defer r.Body.Close() return "unhealthy", BuildErrorResponse(r, err) } else if err != nil { @@ -2788,22 +2788,22 @@ func (c *Client4) OpenGraph(url string) (map[string]string, *Response) { // Jobs Section -// GetJobStatus gets the status of a single job. -func (c *Client4) GetJobStatus(id string) (*JobStatus, *Response) { +// GetJob gets a single job. +func (c *Client4) GetJob(id string) (*Job, *Response) { if r, err := c.DoApiGet(c.GetJobsRoute()+fmt.Sprintf("/%v/status", id), ""); err != nil { return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) - return JobStatusFromJson(r.Body), BuildResponse(r) + return JobFromJson(r.Body), BuildResponse(r) } } -// GetJobStatusesByType gets the status of all jobs of a given type, sorted with the job that most recently started first. -func (c *Client4) GetJobStatusesByType(jobType string, page int, perPage int) ([]*JobStatus, *Response) { +// GetJobsByType gets all jobs of a given type, sorted with the job that most recently started first. +func (c *Client4) GetJobsByType(jobType string, page int, perPage int) ([]*Job, *Response) { if r, err := c.DoApiGet(c.GetJobsRoute()+fmt.Sprintf("/type/%v/statuses?page=%v&per_page=%v", jobType, page, perPage), ""); err != nil { return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) - return JobStatusesFromJson(r.Body), BuildResponse(r) + return JobsFromJson(r.Body), BuildResponse(r) } } diff --git a/model/command_response.go b/model/command_response.go index 1b2e06cdf..e3253acc0 100644 --- a/model/command_response.go +++ b/model/command_response.go @@ -5,7 +5,6 @@ package model import ( "encoding/json" - "fmt" "io" ) @@ -15,12 +14,12 @@ const ( ) type CommandResponse struct { - ResponseType string `json:"response_type"` - Text string `json:"text"` - Username string `json:"username"` - IconURL string `json:"icon_url"` - GotoLocation string `json:"goto_location"` - Attachments []*SlackAttachment `json:"attachments"` + ResponseType string `json:"response_type"` + Text string `json:"text"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + GotoLocation string `json:"goto_location"` + Attachments SlackAttachments `json:"attachments"` } func (o *CommandResponse) ToJson() string { @@ -40,14 +39,8 @@ func CommandResponseFromJson(data io.Reader) *CommandResponse { return nil } - // Ensure attachment fields are stored as strings - for _, attachment := range o.Attachments { - for _, field := range attachment.Fields { - if field.Value != nil { - field.Value = fmt.Sprintf("%v", field.Value) - } - } - } + o.Text = ExpandAnnouncement(o.Text) + o.Attachments.Process() return &o } diff --git a/model/command_response_test.go b/model/command_response_test.go index b57a77608..df478ff2c 100644 --- a/model/command_response_test.go +++ b/model/command_response_test.go @@ -82,3 +82,17 @@ func TestCommandResponseFromJson(t *testing.T) { t.Fatal("should've received correct second attachment value") } } + +func TestCommandResponseNullArrayItems(t *testing.T) { + payload := `{"attachments":[{"fields":[{"title":"foo","value":"bar","short":true}, null]}, null]}` + cr := CommandResponseFromJson(strings.NewReader(payload)) + if cr == nil { + t.Fatal("CommandResponse should not be nil") + } + if len(cr.Attachments) != 1 { + t.Fatalf("expected one attachment") + } + if len(cr.Attachments[0].Fields) != 1 { + t.Fatalf("expected one field") + } +} diff --git a/model/config.go b/model/config.go index 38d27d8bb..5add720d3 100644 --- a/model/config.go +++ b/model/config.go @@ -436,6 +436,11 @@ type DataRetentionSettings struct { Enable *bool } +type JobSettings struct { + RunJobs *bool + RunScheduler *bool +} + type Config struct { ServiceSettings ServiceSettings TeamSettings TeamSettings @@ -462,6 +467,7 @@ type Config struct { WebrtcSettings WebrtcSettings ElasticSearchSettings ElasticSearchSettings DataRetentionSettings DataRetentionSettings + JobSettings JobSettings } func (o *Config) ToJson() string { @@ -1380,6 +1386,16 @@ func (o *Config) SetDefaults() { *o.DataRetentionSettings.Enable = false } + if o.JobSettings.RunJobs == nil { + o.JobSettings.RunJobs = new(bool) + *o.JobSettings.RunJobs = true + } + + if o.JobSettings.RunScheduler == nil { + o.JobSettings.RunScheduler = new(bool) + *o.JobSettings.RunScheduler = true + } + o.defaultWebrtcSettings() } @@ -1646,8 +1662,6 @@ func (o *Config) Sanitize() { o.SqlSettings.DataSourceSearchReplicas[i] = FAKE_SETTING } - *o.ElasticSearchSettings.ConnectionUrl = FAKE_SETTING - *o.ElasticSearchSettings.Username = FAKE_SETTING *o.ElasticSearchSettings.Password = FAKE_SETTING } diff --git a/model/incoming_webhook.go b/model/incoming_webhook.go index 2235cb2c6..4ebc7e8b1 100644 --- a/model/incoming_webhook.go +++ b/model/incoming_webhook.go @@ -6,10 +6,8 @@ package model import ( "bytes" "encoding/json" - "fmt" "io" "regexp" - "strings" ) const ( @@ -29,13 +27,13 @@ type IncomingWebhook struct { } type IncomingWebhookRequest struct { - Text string `json:"text"` - Username string `json:"username"` - IconURL string `json:"icon_url"` - ChannelName string `json:"channel"` - Props StringInterface `json:"props"` - Attachments []*SlackAttachment `json:"attachments"` - Type string `json:"type"` + Text string `json:"text"` + Username string `json:"username"` + IconURL string `json:"icon_url"` + ChannelName string `json:"channel"` + Props StringInterface `json:"props"` + Attachments SlackAttachments `json:"attachments"` + Type string `json:"type"` } func (o *IncomingWebhook) ToJson() string { @@ -193,39 +191,6 @@ func decodeIncomingWebhookRequest(by []byte) (*IncomingWebhookRequest, error) { } } -// To mention @channel via a webhook in Slack, the message should contain -// <!channel>, as explained at the bottom of this article: -// https://get.slack.help/hc/en-us/articles/202009646-Making-announcements -func expandAnnouncement(text string) string { - c1 := "<!channel>" - c2 := "@channel" - if strings.Contains(text, c1) { - return strings.Replace(text, c1, c2, -1) - } - return text -} - -// Expand announcements in incoming webhooks from Slack. Those announcements -// can be found in the text attribute, or in the pretext, text, title and value -// attributes of the attachment structure. The Slack attachment structure is -// documented here: https://api.slack.com/docs/attachments -func expandAnnouncements(i *IncomingWebhookRequest) { - i.Text = expandAnnouncement(i.Text) - - for _, attachment := range i.Attachments { - attachment.Pretext = expandAnnouncement(attachment.Pretext) - attachment.Text = expandAnnouncement(attachment.Text) - attachment.Title = expandAnnouncement(attachment.Title) - - for _, field := range attachment.Fields { - if field.Value != nil { - // Ensure the value is set to a string if it is set - field.Value = expandAnnouncement(fmt.Sprintf("%v", field.Value)) - } - } - } -} - func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest { buf := new(bytes.Buffer) buf.ReadFrom(data) @@ -241,7 +206,8 @@ func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest { } } - expandAnnouncements(o) + o.Text = ExpandAnnouncement(o.Text) + o.Attachments.Process() return o } diff --git a/model/incoming_webhook_test.go b/model/incoming_webhook_test.go index f6baca988..36f8ed6e6 100644 --- a/model/incoming_webhook_test.go +++ b/model/incoming_webhook_test.go @@ -230,3 +230,17 @@ func TestIncomingWebhookRequestFromJson(t *testing.T) { } } } + +func TestIncomingWebhookNullArrayItems(t *testing.T) { + payload := `{"attachments":[{"fields":[{"title":"foo","value":"bar","short":true}, null]}, null]}` + iwr := IncomingWebhookRequestFromJson(strings.NewReader(payload)) + if iwr == nil { + t.Fatal("IncomingWebhookRequest should not be nil") + } + if len(iwr.Attachments) != 1 { + t.Fatalf("expected one attachment") + } + if len(iwr.Attachments[0].Fields) != 1 { + t.Fatalf("expected one field") + } +} diff --git a/model/job.go b/model/job.go index d539b5bf9..b0567bf1a 100644 --- a/model/job.go +++ b/model/job.go @@ -3,7 +3,84 @@ package model -type Job interface { +import ( + "encoding/json" + "io" +) + +const ( + JOB_TYPE_DATA_RETENTION = "data_retention" + JOB_TYPE_SEARCH_INDEXING = "search_indexing" + + JOB_STATUS_PENDING = "pending" + JOB_STATUS_IN_PROGRESS = "in_progress" + JOB_STATUS_SUCCESS = "success" + JOB_STATUS_ERROR = "error" + JOB_STATUS_CANCEL_REQUESTED = "cancel_requested" + JOB_STATUS_CANCELED = "canceled" +) + +type Job struct { + Id string `json:"id"` + Type string `json:"type"` + Priority int64 `json:"priority"` + CreateAt int64 `json:"create_at"` + StartAt int64 `json:"start_at"` + LastActivityAt int64 `json:"last_activity_at"` + Status string `json:"status"` + Progress int64 `json:"progress"` + Data map[string]interface{} `json:"data"` +} + +func (js *Job) ToJson() string { + if b, err := json.Marshal(js); err != nil { + return "" + } else { + return string(b) + } +} + +func JobFromJson(data io.Reader) *Job { + var status Job + if err := json.NewDecoder(data).Decode(&status); err == nil { + return &status + } else { + return nil + } +} + +func JobsToJson(jobs []*Job) string { + if b, err := json.Marshal(jobs); err != nil { + return "" + } else { + return string(b) + } +} + +func JobsFromJson(data io.Reader) []*Job { + var jobs []*Job + if err := json.NewDecoder(data).Decode(&jobs); err == nil { + return jobs + } else { + return nil + } +} + +func (js *Job) DataToJson() string { + if b, err := json.Marshal(js.Data); err != nil { + return "" + } else { + return string(b) + } +} + +type Worker interface { + Run() + Stop() + JobChannel() chan<- Job +} + +type Scheduler interface { Run() Stop() } diff --git a/model/job_status.go b/model/job_status.go deleted file mode 100644 index cf490648f..000000000 --- a/model/job_status.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package model - -import ( - "encoding/json" - "io" -) - -const ( - JOB_TYPE_DATA_RETENTION = "data_retention" - JOB_TYPE_SEARCH_INDEXING = "search_indexing" -) - -type JobStatus struct { - Id string `json:"id"` - Type string `json:"type"` - StartAt int64 `json:"start_at"` - LastActivityAt int64 `json:"last_activity_at"` - LastRunStartedAt int64 `json:"last_run_started_at"` - LastRunCompletedAt int64 `json:"last_run_completed_at"` - Status string `json:"status"` - Data map[string]interface{} `json:"data"` -} - -func (js *JobStatus) ToJson() string { - if b, err := json.Marshal(js); err != nil { - return "" - } else { - return string(b) - } -} - -func JobStatusFromJson(data io.Reader) *JobStatus { - var status JobStatus - if err := json.NewDecoder(data).Decode(&status); err == nil { - return &status - } else { - return nil - } -} - -func JobStatusesToJson(statuses []*JobStatus) string { - if b, err := json.Marshal(statuses); err != nil { - return "" - } else { - return string(b) - } -} - -func JobStatusesFromJson(data io.Reader) []*JobStatus { - var statuses []*JobStatus - if err := json.NewDecoder(data).Decode(&statuses); err == nil { - return statuses - } else { - return nil - } -} diff --git a/model/license.go b/model/license.go index 443d78282..8d53bd4cd 100644 --- a/model/license.go +++ b/model/license.go @@ -49,7 +49,7 @@ type Features struct { MHPNS *bool `json:"mhpns"` SAML *bool `json:"saml"` PasswordRequirements *bool `json:"password_requirements"` - ElasticSearch *bool `json:"elastic_search"` + Elasticsearch *bool `json:"elastic_search"` Announcement *bool `json:"announcement"` // after we enabled more features for webrtc we'll need to control them with this FutureFeatures *bool `json:"future_features"` @@ -68,7 +68,7 @@ func (f *Features) ToMap() map[string]interface{} { "mhpns": *f.MHPNS, "saml": *f.SAML, "password": *f.PasswordRequirements, - "elastic_search": *f.ElasticSearch, + "elastic_search": *f.Elasticsearch, "future": *f.FutureFeatures, } } @@ -139,9 +139,9 @@ func (f *Features) SetDefaults() { *f.PasswordRequirements = *f.FutureFeatures } - if f.ElasticSearch == nil { - f.ElasticSearch = new(bool) - *f.ElasticSearch = *f.FutureFeatures + if f.Elasticsearch == nil { + f.Elasticsearch = new(bool) + *f.Elasticsearch = *f.FutureFeatures } if f.Announcement == nil { diff --git a/model/license_test.go b/model/license_test.go index 8b65d0700..952ab493e 100644 --- a/model/license_test.go +++ b/model/license_test.go @@ -45,7 +45,7 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { CheckTrue(t, *f.MHPNS) CheckTrue(t, *f.SAML) CheckTrue(t, *f.PasswordRequirements) - CheckTrue(t, *f.ElasticSearch) + CheckTrue(t, *f.Elasticsearch) CheckTrue(t, *f.FutureFeatures) f = Features{} @@ -64,7 +64,7 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { *f.MHPNS = true *f.SAML = true *f.PasswordRequirements = true - *f.ElasticSearch = true + *f.Elasticsearch = true f.SetDefaults() @@ -80,7 +80,7 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { CheckTrue(t, *f.MHPNS) CheckTrue(t, *f.SAML) CheckTrue(t, *f.PasswordRequirements) - CheckTrue(t, *f.ElasticSearch) + CheckTrue(t, *f.Elasticsearch) CheckFalse(t, *f.FutureFeatures) } @@ -161,7 +161,7 @@ func TestLicenseToFromJson(t *testing.T) { CheckBool(t, *f1.MHPNS, *f.MHPNS) CheckBool(t, *f1.SAML, *f.SAML) CheckBool(t, *f1.PasswordRequirements, *f.PasswordRequirements) - CheckBool(t, *f1.ElasticSearch, *f.ElasticSearch) + CheckBool(t, *f1.Elasticsearch, *f.Elasticsearch) CheckBool(t, *f1.FutureFeatures, *f.FutureFeatures) invalid := `{"asdf` diff --git a/model/outgoing_webhook.go b/model/outgoing_webhook.go index 3cfed9e74..59408c24e 100644 --- a/model/outgoing_webhook.go +++ b/model/outgoing_webhook.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "net/http" "net/url" "strconv" "strings" @@ -41,6 +42,7 @@ type OutgoingWebhookPayload struct { PostId string `json:"post_id"` Text string `json:"text"` TriggerWord string `json:"trigger_word"` + FileIds string `json:"file_ids"` } func (o *OutgoingWebhookPayload) ToJSON() string { @@ -65,6 +67,7 @@ func (o *OutgoingWebhookPayload) ToFormValues() string { v.Set("post_id", o.PostId) v.Set("text", o.Text) v.Set("trigger_word", o.TriggerWord) + v.Set("file_ids", o.FileIds) return v.Encode() } @@ -112,69 +115,69 @@ func OutgoingWebhookListFromJson(data io.Reader) []*OutgoingWebhook { func (o *OutgoingWebhook) IsValid() *AppError { if len(o.Id) != 26 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.id.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.id.app_error", nil, "", http.StatusBadRequest) } if len(o.Token) != 26 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.token.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.token.app_error", nil, "", http.StatusBadRequest) } if o.CreateAt == 0 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.create_at.app_error", nil, "id="+o.Id) + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) } if o.UpdateAt == 0 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.update_at.app_error", nil, "id="+o.Id) + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) } if len(o.CreatorId) != 26 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.user_id.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) } if len(o.ChannelId) != 0 && len(o.ChannelId) != 26 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.channel_id.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) } if len(o.TeamId) != 26 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.team_id.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.team_id.app_error", nil, "", http.StatusBadRequest) } if len(fmt.Sprintf("%s", o.TriggerWords)) > 1024 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.words.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.words.app_error", nil, "", http.StatusBadRequest) } if len(o.TriggerWords) != 0 { for _, triggerWord := range o.TriggerWords { if len(triggerWord) == 0 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.trigger_words.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.trigger_words.app_error", nil, "", http.StatusBadRequest) } } } if len(o.CallbackURLs) == 0 || len(fmt.Sprintf("%s", o.CallbackURLs)) > 1024 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.callback.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.callback.app_error", nil, "", http.StatusBadRequest) } for _, callback := range o.CallbackURLs { if !IsValidHttpUrl(callback) { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.url.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.url.app_error", nil, "", http.StatusBadRequest) } } if len(o.DisplayName) > 64 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.display_name.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.display_name.app_error", nil, "", http.StatusBadRequest) } if len(o.Description) > 128 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.description.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.description.app_error", nil, "", http.StatusBadRequest) } if len(o.ContentType) > 128 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "", http.StatusBadRequest) } if o.TriggerWhen > 1 { - return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "") + return NewAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.content_type.app_error", nil, "", http.StatusBadRequest) } return nil @@ -197,8 +200,8 @@ func (o *OutgoingWebhook) PreUpdate() { o.UpdateAt = GetMillis() } -func (o *OutgoingWebhook) HasTriggerWord(word string) bool { - if len(o.TriggerWords) == 0 || len(word) == 0 { +func (o *OutgoingWebhook) TriggerWordExactMatch(word string) bool { + if len(word) == 0 { return false } @@ -212,7 +215,7 @@ func (o *OutgoingWebhook) HasTriggerWord(word string) bool { } func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool { - if len(o.TriggerWords) == 0 || len(word) == 0 { + if len(word) == 0 { return false } @@ -224,3 +227,27 @@ func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool { return false } + +func (o *OutgoingWebhook) GetTriggerWord(word string, isExactMatch bool) (triggerWord string) { + if len(word) == 0 { + return + } + + if isExactMatch { + for _, trigger := range o.TriggerWords { + if trigger == word { + triggerWord = trigger + break + } + } + } else { + for _, trigger := range o.TriggerWords { + if strings.HasPrefix(word, trigger) { + triggerWord = trigger + break + } + } + } + + return triggerWord +} diff --git a/model/outgoing_webhook_test.go b/model/outgoing_webhook_test.go index 725423cdf..431b1f6c1 100644 --- a/model/outgoing_webhook_test.go +++ b/model/outgoing_webhook_test.go @@ -136,6 +136,7 @@ func TestOutgoingWebhookPayloadToFormValues(t *testing.T) { PostId: "PostId", Text: "Text", TriggerWord: "TriggerWord", + FileIds: "FileIds", } v := url.Values{} v.Set("token", "Token") @@ -149,6 +150,7 @@ func TestOutgoingWebhookPayloadToFormValues(t *testing.T) { v.Set("post_id", "PostId") v.Set("text", "Text") v.Set("trigger_word", "TriggerWord") + v.Set("file_ids", "FileIds") if got, want := p.ToFormValues(), v.Encode(); !reflect.DeepEqual(got, want) { t.Fatalf("Got %+v, wanted %+v", got, want) } diff --git a/model/post.go b/model/post.go index f5a398656..55e6f591d 100644 --- a/model/post.go +++ b/model/post.go @@ -62,6 +62,12 @@ type PostPatch struct { HasReactions *bool `json:"has_reactions"` } +type PostForIndexing struct { + Post + TeamId string `json:"team_id"` + ParentCreateAt *int64 `json:"parent_create_at"` +} + func (o *Post) ToJson() string { b, err := json.Marshal(o) if err != nil { diff --git a/model/slack_attachment.go b/model/slack_attachment.go index 6fd0071b4..a3199c44c 100644 --- a/model/slack_attachment.go +++ b/model/slack_attachment.go @@ -3,6 +3,11 @@ package model +import ( + "fmt" + "strings" +) + type SlackAttachment struct { Id int64 `json:"id"` Fallback string `json:"fallback"` @@ -27,3 +32,50 @@ type SlackAttachmentField struct { Value interface{} `json:"value"` Short bool `json:"short"` } + +type SlackAttachments []*SlackAttachment + +// To mention @channel via a webhook in Slack, the message should contain +// <!channel>, as explained at the bottom of this article: +// https://get.slack.help/hc/en-us/articles/202009646-Making-announcements +func ExpandAnnouncement(text string) string { + c1 := "<!channel>" + c2 := "@channel" + if strings.Contains(text, c1) { + return strings.Replace(text, c1, c2, -1) + } + return text +} + +// Expand announcements in incoming webhooks from Slack. Those announcements +// can be found in the text attribute, or in the pretext, text, title and value +// attributes of the attachment structure. The Slack attachment structure is +// documented here: https://api.slack.com/docs/attachments +func (a *SlackAttachments) Process() { + var nonNilAttachments []*SlackAttachment + for _, attachment := range *a { + if attachment == nil { + continue + } + nonNilAttachments = append(nonNilAttachments, attachment) + + attachment.Pretext = ExpandAnnouncement(attachment.Pretext) + attachment.Text = ExpandAnnouncement(attachment.Text) + attachment.Title = ExpandAnnouncement(attachment.Title) + + var nonNilFields []*SlackAttachmentField + for _, field := range attachment.Fields { + if field == nil { + continue + } + nonNilFields = append(nonNilFields, field) + + if field.Value != nil { + // Ensure the value is set to a string if it is set + field.Value = ExpandAnnouncement(fmt.Sprintf("%v", field.Value)) + } + } + attachment.Fields = nonNilFields + } + *a = nonNilAttachments +} diff --git a/model/slack_attachment_test.go b/model/slack_attachment_test.go new file mode 100644 index 000000000..ff4df888f --- /dev/null +++ b/model/slack_attachment_test.go @@ -0,0 +1,38 @@ +package model + +import ( + "testing" +) + +func TestExpandAnnouncement(t *testing.T) { + if ExpandAnnouncement("<!channel> foo <!channel>") != "@channel foo @channel" { + t.Fail() + } +} + +func TestSlackAnnouncementProcess(t *testing.T) { + attachments := SlackAttachments{ + { + Pretext: "<!channel> pretext", + Text: "<!channel> text", + Title: "<!channel> title", + Fields: []*SlackAttachmentField{ + { + Title: "foo", + Value: "<!channel> bar", + Short: true, + }, nil, + }, + }, nil, + } + attachments.Process() + if len(attachments) != 1 || len(attachments[0].Fields) != 1 { + t.Fail() + } + if attachments[0].Pretext != "@channel pretext" || + attachments[0].Text != "@channel text" || + attachments[0].Title != "@channel title" || + attachments[0].Fields[0].Value != "@channel bar" { + t.Fail() + } +} |