diff options
Diffstat (limited to 'model')
-rw-r--r-- | model/access.go | 2 | ||||
-rw-r--r-- | model/client.go | 73 | ||||
-rw-r--r-- | model/config.go | 1 | ||||
-rw-r--r-- | model/incoming_webhook.go (renamed from model/webhook.go) | 5 | ||||
-rw-r--r-- | model/incoming_webhook_test.go (renamed from model/webhook_test.go) | 0 | ||||
-rw-r--r-- | model/message.go | 16 | ||||
-rw-r--r-- | model/outgoing_webhook.go | 135 | ||||
-rw-r--r-- | model/outgoing_webhook_test.go | 97 | ||||
-rw-r--r-- | model/preference.go | 60 | ||||
-rw-r--r-- | model/preference_test.go | 56 | ||||
-rw-r--r-- | model/preferences.go | 31 | ||||
-rw-r--r-- | model/search_params.go | 130 | ||||
-rw-r--r-- | model/search_params_test.go | 70 | ||||
-rw-r--r-- | model/system.go | 6 | ||||
-rw-r--r-- | model/team.go | 6 | ||||
-rw-r--r-- | model/utils.go | 4 |
16 files changed, 680 insertions, 12 deletions
diff --git a/model/access.go b/model/access.go index 89a1271c1..6c9254004 100644 --- a/model/access.go +++ b/model/access.go @@ -16,7 +16,7 @@ const ( type AccessData struct { AuthCode string `json:"auth_code"` - Token string `json"token"` + Token string `json:"token"` RefreshToken string `json:"refresh_token"` RedirectUri string `json:"redirect_uri"` } diff --git a/model/client.go b/model/client.go index 11beb9a87..9183dcacb 100644 --- a/model/client.go +++ b/model/client.go @@ -185,7 +185,7 @@ func (c *Client) FindTeams(email string) (*Result, *AppError) { } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil + r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil } } @@ -844,6 +844,77 @@ func (c *Client) ListIncomingWebhooks() (*Result, *AppError) { } } +func (c *Client) GetAllPreferences() (*Result, *AppError) { + if r, err := c.DoApiGet("/preferences/", "", ""); err != nil { + return nil, err + } else { + preferences, _ := PreferencesFromJson(r.Body) + return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil + } +} + +func (c *Client) SetPreferences(preferences *Preferences) (*Result, *AppError) { + if r, err := c.DoApiPost("/preferences/save", preferences.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil + } +} + +func (c *Client) GetPreference(category string, name string) (*Result, *AppError) { + if r, err := c.DoApiGet("/preferences/"+category+"/"+name, "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), PreferenceFromJson(r.Body)}, nil + } +} + +func (c *Client) GetPreferenceCategory(category string) (*Result, *AppError) { + if r, err := c.DoApiGet("/preferences/"+category, "", ""); err != nil { + return nil, err + } else { + preferences, _ := PreferencesFromJson(r.Body) + return &Result{r.Header.Get(HEADER_REQUEST_ID), r.Header.Get(HEADER_ETAG_SERVER), preferences}, nil + } +} + +func (c *Client) CreateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppError) { + if r, err := c.DoApiPost("/hooks/outgoing/create", hook.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookFromJson(r.Body)}, nil + } +} + +func (c *Client) DeleteOutgoingWebhook(data map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/hooks/outgoing/delete", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) ListOutgoingWebhooks() (*Result, *AppError) { + if r, err := c.DoApiGet("/hooks/outgoing/list", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookListFromJson(r.Body)}, nil + } +} + +func (c *Client) RegenOutgoingWebhookToken(data map[string]string) (*Result, *AppError) { + if r, err := c.DoApiPost("/hooks/outgoing/regen_token", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookFromJson(r.Body)}, nil + } +} + func (c *Client) MockSession(sessionToken string) { c.AuthToken = sessionToken c.AuthType = HEADER_BEARER diff --git a/model/config.go b/model/config.go index 59caf0884..3a39df2f1 100644 --- a/model/config.go +++ b/model/config.go @@ -29,6 +29,7 @@ type ServiceSettings struct { GoogleDeveloperKey string EnableOAuthServiceProvider bool EnableIncomingWebhooks bool + EnableOutgoingWebhooks bool EnablePostUsernameOverride bool EnablePostIconOverride bool EnableTesting bool diff --git a/model/webhook.go b/model/incoming_webhook.go index 3bf034908..9b9969b96 100644 --- a/model/webhook.go +++ b/model/incoming_webhook.go @@ -8,6 +8,11 @@ import ( "io" ) +const ( + DEFAULT_WEBHOOK_USERNAME = "webhook" + DEFAULT_WEBHOOK_ICON = "/static/images/webhook_icon.jpg" +) + type IncomingWebhook struct { Id string `json:"id"` CreateAt int64 `json:"create_at"` diff --git a/model/webhook_test.go b/model/incoming_webhook_test.go index 5297d7d90..5297d7d90 100644 --- a/model/webhook_test.go +++ b/model/incoming_webhook_test.go diff --git a/model/message.go b/model/message.go index 122af4d9c..2725353ac 100644 --- a/model/message.go +++ b/model/message.go @@ -9,14 +9,14 @@ import ( ) const ( - ACTION_TYPING = "typing" - ACTION_POSTED = "posted" - ACTION_POST_EDITED = "post_edited" - ACTION_POST_DELETED = "post_deleted" - ACTION_VIEWED = "viewed" - ACTION_NEW_USER = "new_user" - ACTION_USER_ADDED = "user_added" - ACTION_USER_REMOVED = "user_removed" + ACTION_TYPING = "typing" + ACTION_POSTED = "posted" + ACTION_POST_EDITED = "post_edited" + ACTION_POST_DELETED = "post_deleted" + ACTION_CHANNEL_VIEWED = "channel_viewed" + ACTION_NEW_USER = "new_user" + ACTION_USER_ADDED = "user_added" + ACTION_USER_REMOVED = "user_removed" ) type Message struct { diff --git a/model/outgoing_webhook.go b/model/outgoing_webhook.go new file mode 100644 index 000000000..8958dd5b0 --- /dev/null +++ b/model/outgoing_webhook.go @@ -0,0 +1,135 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" +) + +type OutgoingWebhook struct { + Id string `json:"id"` + Token string `json:"token"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + CreatorId string `json:"creator_id"` + ChannelId string `json:"channel_id"` + TeamId string `json:"team_id"` + TriggerWords StringArray `json:"trigger_words"` + CallbackURLs StringArray `json:"callback_urls"` +} + +func (o *OutgoingWebhook) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func OutgoingWebhookFromJson(data io.Reader) *OutgoingWebhook { + decoder := json.NewDecoder(data) + var o OutgoingWebhook + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func OutgoingWebhookListToJson(l []*OutgoingWebhook) string { + b, err := json.Marshal(l) + if err != nil { + return "" + } else { + return string(b) + } +} + +func OutgoingWebhookListFromJson(data io.Reader) []*OutgoingWebhook { + decoder := json.NewDecoder(data) + var o []*OutgoingWebhook + err := decoder.Decode(&o) + if err == nil { + return o + } else { + return nil + } +} + +func (o *OutgoingWebhook) IsValid() *AppError { + + if len(o.Id) != 26 { + return NewAppError("OutgoingWebhook.IsValid", "Invalid Id", "") + } + + if len(o.Token) != 26 { + return NewAppError("OutgoingWebhook.IsValid", "Invalid token", "") + } + + if o.CreateAt == 0 { + return NewAppError("OutgoingWebhook.IsValid", "Create at must be a valid time", "id="+o.Id) + } + + if o.UpdateAt == 0 { + return NewAppError("OutgoingWebhook.IsValid", "Update at must be a valid time", "id="+o.Id) + } + + if len(o.CreatorId) != 26 { + return NewAppError("OutgoingWebhook.IsValid", "Invalid user id", "") + } + + if len(o.ChannelId) != 0 && len(o.ChannelId) != 26 { + return NewAppError("OutgoingWebhook.IsValid", "Invalid channel id", "") + } + + if len(o.TeamId) != 26 { + return NewAppError("OutgoingWebhook.IsValid", "Invalid team id", "") + } + + if len(fmt.Sprintf("%s", o.TriggerWords)) > 1024 { + return NewAppError("OutgoingWebhook.IsValid", "Invalid trigger words", "") + } + + if len(o.CallbackURLs) == 0 || len(fmt.Sprintf("%s", o.CallbackURLs)) > 1024 { + return NewAppError("OutgoingWebhook.IsValid", "Invalid callback urls", "") + } + + return nil +} + +func (o *OutgoingWebhook) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + if o.Token == "" { + o.Token = NewId() + } + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt +} + +func (o *OutgoingWebhook) PreUpdate() { + o.UpdateAt = GetMillis() +} + +func (o *OutgoingWebhook) HasTriggerWord(word string) bool { + if len(o.TriggerWords) == 0 || len(word) == 0 { + return false + } + + for _, trigger := range o.TriggerWords { + if trigger == word { + return true + } + } + + return false +} diff --git a/model/outgoing_webhook_test.go b/model/outgoing_webhook_test.go new file mode 100644 index 000000000..2ca48c291 --- /dev/null +++ b/model/outgoing_webhook_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestOutgoingWebhookJson(t *testing.T) { + o := OutgoingWebhook{Id: NewId()} + json := o.ToJson() + ro := OutgoingWebhookFromJson(strings.NewReader(json)) + + if o.Id != ro.Id { + t.Fatal("Ids do not match") + } +} + +func TestOutgoingWebhookIsValid(t *testing.T) { + o := OutgoingWebhook{} + + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Id = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.CreateAt = GetMillis() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.UpdateAt = GetMillis() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.CreatorId = "123" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.CreatorId = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Token = "123" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Token = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.ChannelId = "123" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.ChannelId = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.TeamId = "123" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.TeamId = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.CallbackURLs = []string{"http://nowhere.com/"} + if err := o.IsValid(); err != nil { + t.Fatal(err) + } +} + +func TestOutgoingWebhookPreSave(t *testing.T) { + o := OutgoingWebhook{} + o.PreSave() +} + +func TestOutgoingWebhookPreUpdate(t *testing.T) { + o := OutgoingWebhook{} + o.PreUpdate() +} diff --git a/model/preference.go b/model/preference.go new file mode 100644 index 000000000..44279f71a --- /dev/null +++ b/model/preference.go @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW = "direct_channel_show" +) + +type Preference struct { + UserId string `json:"user_id"` + Category string `json:"category"` + Name string `json:"name"` + Value string `json:"value"` +} + +func (o *Preference) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func PreferenceFromJson(data io.Reader) *Preference { + decoder := json.NewDecoder(data) + var o Preference + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *Preference) IsValid() *AppError { + if len(o.UserId) != 26 { + return NewAppError("Preference.IsValid", "Invalid user id", "user_id="+o.UserId) + } + + if len(o.Category) == 0 || len(o.Category) > 32 { + return NewAppError("Preference.IsValid", "Invalid category", "category="+o.Category) + } + + if len(o.Name) == 0 || len(o.Name) > 32 { + return NewAppError("Preference.IsValid", "Invalid name", "name="+o.Name) + } + + if len(o.Value) > 128 { + return NewAppError("Preference.IsValid", "Value is too long", "value="+o.Value) + } + + return nil +} diff --git a/model/preference_test.go b/model/preference_test.go new file mode 100644 index 000000000..66b7ac50b --- /dev/null +++ b/model/preference_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestPreferenceIsValid(t *testing.T) { + preference := Preference{ + UserId: "1234garbage", + Category: PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, + Name: NewId(), + } + + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.UserId = NewId() + if err := preference.IsValid(); err != nil { + t.Fatal(err) + } + + preference.Category = strings.Repeat("01234567890", 20) + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.Category = PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW + if err := preference.IsValid(); err != nil { + t.Fatal() + } + + preference.Name = strings.Repeat("01234567890", 20) + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.Name = NewId() + if err := preference.IsValid(); err != nil { + t.Fatal() + } + + preference.Value = strings.Repeat("01234567890", 20) + if err := preference.IsValid(); err == nil { + t.Fatal() + } + + preference.Value = "1234garbage" + if err := preference.IsValid(); err != nil { + t.Fatal() + } +} diff --git a/model/preferences.go b/model/preferences.go new file mode 100644 index 000000000..1ef16151f --- /dev/null +++ b/model/preferences.go @@ -0,0 +1,31 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type Preferences []Preference + +func (o *Preferences) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func PreferencesFromJson(data io.Reader) (Preferences, error) { + decoder := json.NewDecoder(data) + var o Preferences + err := decoder.Decode(&o) + if err == nil { + return o, nil + } else { + return nil, err + } +} diff --git a/model/search_params.go b/model/search_params.go new file mode 100644 index 000000000..7eeeed10f --- /dev/null +++ b/model/search_params.go @@ -0,0 +1,130 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" +) + +type SearchParams struct { + Terms string + IsHashtag bool + InChannel string + FromUser string +} + +var searchFlags = [...]string{"from", "channel", "in"} + +func splitWords(text string) []string { + words := []string{} + + for _, word := range strings.Fields(text) { + word = puncStart.ReplaceAllString(word, "") + word = puncEnd.ReplaceAllString(word, "") + + if len(word) != 0 { + words = append(words, word) + } + } + + return words +} + +func parseSearchFlags(input []string) ([]string, map[string]string) { + words := []string{} + flags := make(map[string]string) + + skipNextWord := false + for i, word := range input { + if skipNextWord { + skipNextWord = false + continue + } + + isFlag := false + + if colon := strings.Index(word, ":"); colon != -1 { + flag := word[:colon] + value := word[colon+1:] + + for _, searchFlag := range searchFlags { + // check for case insensitive equality + if strings.EqualFold(flag, searchFlag) { + if value != "" { + flags[searchFlag] = value + isFlag = true + } else if i < len(input)-1 { + flags[searchFlag] = input[i+1] + skipNextWord = true + isFlag = true + } + + if isFlag { + break + } + } + } + } + + if !isFlag { + words = append(words, word) + } + } + + return words, flags +} + +func ParseSearchParams(text string) (*SearchParams, *SearchParams) { + words, flags := parseSearchFlags(splitWords(text)) + + hashtagTerms := []string{} + plainTerms := []string{} + + for _, word := range words { + if validHashtag.MatchString(word) { + hashtagTerms = append(hashtagTerms, word) + } else { + plainTerms = append(plainTerms, word) + } + } + + inChannel := flags["channel"] + if inChannel == "" { + inChannel = flags["in"] + } + + fromUser := flags["from"] + + var plainParams *SearchParams + if len(plainTerms) > 0 { + plainParams = &SearchParams{ + Terms: strings.Join(plainTerms, " "), + IsHashtag: false, + InChannel: inChannel, + FromUser: fromUser, + } + } + + var hashtagParams *SearchParams + if len(hashtagTerms) > 0 { + hashtagParams = &SearchParams{ + Terms: strings.Join(hashtagTerms, " "), + IsHashtag: true, + InChannel: inChannel, + FromUser: fromUser, + } + } + + // special case for when no terms are specified but we still have a filter + if plainParams == nil && hashtagParams == nil && (inChannel != "" || fromUser != "") { + plainParams = &SearchParams{ + Terms: "", + IsHashtag: false, + InChannel: inChannel, + FromUser: fromUser, + } + } + + return plainParams, hashtagParams +} diff --git a/model/search_params_test.go b/model/search_params_test.go new file mode 100644 index 000000000..2eba20f4c --- /dev/null +++ b/model/search_params_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "testing" +) + +func TestParseSearchFlags(t *testing.T) { + if words, flags := parseSearchFlags(splitWords("")); len(words) != 0 { + t.Fatal("got words from empty input") + } else if len(flags) != 0 { + t.Fatal("got flags from empty input") + } + + if words, flags := parseSearchFlags(splitWords("word")); len(words) != 1 || words[0] != "word" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("apple banana cherry")); len(words) != 3 || words[0] != "apple" || words[1] != "banana" || words[2] != "cherry" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("apple banana from:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 1 || flags["from"] != "chan" { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("apple banana from: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 1 || flags["from"] != "chan" { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("apple banana in: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 1 || flags["in"] != "chan" { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("apple banana channel:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 1 || flags["channel"] != "chan" { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("fruit: cherry")); len(words) != 2 || words[0] != "fruit:" || words[1] != "cherry" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("channel:")); len(words) != 1 || words[0] != "channel:" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 0 { + t.Fatalf("got incorrect flags %v", flags) + } + + if words, flags := parseSearchFlags(splitWords("channel: first in: second from:")); len(words) != 1 || words[0] != "from:" { + t.Fatalf("got incorrect words %v", words) + } else if len(flags) != 2 || flags["channel"] != "first" || flags["in"] != "second" { + t.Fatalf("got incorrect flags %v", flags) + } +} diff --git a/model/system.go b/model/system.go index 033f660b3..70db529d5 100644 --- a/model/system.go +++ b/model/system.go @@ -8,6 +8,12 @@ import ( "io" ) +const ( + SYSTEM_DIAGNOSTIC_ID = "DiagnosticId" + SYSTEM_RAN_UNIT_TESTS = "RanUnitTests" + SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime" +) + type System struct { Name string `json:"name"` Value string `json:"value"` diff --git a/model/team.go b/model/team.go index c0f6524cd..584c78f8d 100644 --- a/model/team.go +++ b/model/team.go @@ -219,3 +219,9 @@ func CleanTeamName(s string) string { func (o *Team) PreExport() { } + +func (o *Team) Sanitize() { + o.Email = "" + o.Type = "" + o.AllowedDomains = "" +} diff --git a/model/utils.go b/model/utils.go index 269144afc..bb0669df7 100644 --- a/model/utils.go +++ b/model/utils.go @@ -242,10 +242,10 @@ func Etag(parts ...interface{}) string { var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$`) var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`) -var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`) +var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"';\\]+$`) func ParseHashtags(text string) (string, string) { - words := strings.Split(strings.Replace(text, "\n", " ", -1), " ") + words := strings.Fields(text) hashtagString := "" plainString := "" |