From 56e74239d6b34df8f30ef046f0b0ff4ff0866a71 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Sun, 14 Jun 2015 23:53:32 -0800 Subject: first commit --- model/audit.go | 39 +++ model/audit_test.go | 19 ++ model/audits.go | 39 +++ model/audits_test.go | 29 ++ model/channel.go | 104 +++++++ model/channel_extra.go | 51 ++++ model/channel_list.go | 71 +++++ model/channel_member.go | 75 +++++ model/channel_member_test.go | 60 ++++ model/channel_test.go | 81 ++++++ model/client.go | 652 ++++++++++++++++++++++++++++++++++++++++++ model/command.go | 53 ++++ model/command_test.go | 25 ++ model/file.go | 42 +++ model/message.go | 54 ++++ model/message_test.go | 24 ++ model/post.go | 147 ++++++++++ model/post_list.go | 91 ++++++ model/post_list_test.go | 36 +++ model/post_test.go | 87 ++++++ model/session.go | 119 ++++++++ model/session_test.go | 35 +++ model/suggest_command.go | 34 +++ model/suggest_command_test.go | 19 ++ model/team.go | 141 +++++++++ model/team_signup.go | 40 +++ model/team_signup_test.go | 20 ++ model/team_test.go | 76 +++++ model/user.go | 293 +++++++++++++++++++ model/user_test.go | 92 ++++++ model/utils.go | 320 +++++++++++++++++++++ model/utils_test.go | 118 ++++++++ 32 files changed, 3086 insertions(+) create mode 100644 model/audit.go create mode 100644 model/audit_test.go create mode 100644 model/audits.go create mode 100644 model/audits_test.go create mode 100644 model/channel.go create mode 100644 model/channel_extra.go create mode 100644 model/channel_list.go create mode 100644 model/channel_member.go create mode 100644 model/channel_member_test.go create mode 100644 model/channel_test.go create mode 100644 model/client.go create mode 100644 model/command.go create mode 100644 model/command_test.go create mode 100644 model/file.go create mode 100644 model/message.go create mode 100644 model/message_test.go create mode 100644 model/post.go create mode 100644 model/post_list.go create mode 100644 model/post_list_test.go create mode 100644 model/post_test.go create mode 100644 model/session.go create mode 100644 model/session_test.go create mode 100644 model/suggest_command.go create mode 100644 model/suggest_command_test.go create mode 100644 model/team.go create mode 100644 model/team_signup.go create mode 100644 model/team_signup_test.go create mode 100644 model/team_test.go create mode 100644 model/user.go create mode 100644 model/user_test.go create mode 100644 model/utils.go create mode 100644 model/utils_test.go (limited to 'model') diff --git a/model/audit.go b/model/audit.go new file mode 100644 index 000000000..9f3640350 --- /dev/null +++ b/model/audit.go @@ -0,0 +1,39 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type Audit struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UserId string `json:"user_id"` + Action string `json:"action"` + ExtraInfo string `json:"extra_info"` + IpAddress string `json:"ip_address"` + SessionId string `json:"session_id"` +} + +func (o *Audit) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func AuditFromJson(data io.Reader) *Audit { + decoder := json.NewDecoder(data) + var o Audit + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/audit_test.go b/model/audit_test.go new file mode 100644 index 000000000..a309f5c65 --- /dev/null +++ b/model/audit_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestAuditJson(t *testing.T) { + audit := Audit{Id: NewId(), UserId: NewId(), CreateAt: GetMillis()} + json := audit.ToJson() + result := AuditFromJson(strings.NewReader(json)) + + if audit.Id != result.Id { + t.Fatal("Ids do not match") + } +} diff --git a/model/audits.go b/model/audits.go new file mode 100644 index 000000000..9c88deef9 --- /dev/null +++ b/model/audits.go @@ -0,0 +1,39 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type Audits []Audit + +func (o Audits) Etag() string { + if len(o) > 0 { + // the first in the list is always the most current + return Etag(o[0].CreateAt) + } else { + return "" + } +} + +func (o Audits) ToJson() string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func AuditsFromJson(data io.Reader) Audits { + decoder := json.NewDecoder(data) + var o Audits + err := decoder.Decode(&o) + if err == nil { + return o + } else { + return nil + } +} diff --git a/model/audits_test.go b/model/audits_test.go new file mode 100644 index 000000000..59b510f54 --- /dev/null +++ b/model/audits_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestAuditsJson(t *testing.T) { + audit := Audit{Id: NewId(), UserId: NewId(), CreateAt: GetMillis()} + json := audit.ToJson() + result := AuditFromJson(strings.NewReader(json)) + + if audit.Id != result.Id { + t.Fatal("Ids do not match") + } + + var audits Audits = make([]Audit, 1) + audits[0] = audit + + ljson := audits.ToJson() + results := AuditsFromJson(strings.NewReader(ljson)) + + if audits[0].Id != results[0].Id { + t.Fatal("Ids do not match") + } +} diff --git a/model/channel.go b/model/channel.go new file mode 100644 index 000000000..ab36d46a2 --- /dev/null +++ b/model/channel.go @@ -0,0 +1,104 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + CHANNEL_OPEN = "O" + CHANNEL_PRIVATE = "P" + CHANNEL_DIRECT = "D" + DEFAULT_CHANNEL = "town-square" +) + +type Channel struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + TeamId string `json:"team_id"` + Type string `json:"type"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + Description string `json:"description"` + LastPostAt int64 `json:"last_post_at"` + TotalMsgCount int64 `json:"total_msg_count"` +} + +func (o *Channel) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ChannelFromJson(data io.Reader) *Channel { + decoder := json.NewDecoder(data) + var o Channel + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *Channel) Etag() string { + return Etag(o.Id, o.LastPostAt) +} + +func (o *Channel) IsValid() *AppError { + + if len(o.Id) != 26 { + return NewAppError("Channel.IsValid", "Invalid Id", "") + } + + if o.CreateAt == 0 { + return NewAppError("Channel.IsValid", "Create at must be a valid time", "id="+o.Id) + } + + if o.UpdateAt == 0 { + return NewAppError("Channel.IsValid", "Update at must be a valid time", "id="+o.Id) + } + + if len(o.DisplayName) > 64 { + return NewAppError("Channel.IsValid", "Invalid display name", "id="+o.Id) + } + + if len(o.Name) > 64 { + return NewAppError("Channel.IsValid", "Invalid name", "id="+o.Id) + } + + if !IsValidChannelIdentifier(o.Name) { + return NewAppError("Channel.IsValid", "Name must be 2 or more lowercase alphanumeric characters", "id="+o.Id) + } + + if !(o.Type == CHANNEL_OPEN || o.Type == CHANNEL_PRIVATE || o.Type == CHANNEL_DIRECT) { + return NewAppError("Channel.IsValid", "Invalid type", "id="+o.Id) + } + + if len(o.Description) > 1024 { + return NewAppError("Channel.IsValid", "Invalid description", "id="+o.Id) + } + + return nil +} + +func (o *Channel) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt +} + +func (o *Channel) PreUpdate() { + o.UpdateAt = GetMillis() +} diff --git a/model/channel_extra.go b/model/channel_extra.go new file mode 100644 index 000000000..a5c9acf71 --- /dev/null +++ b/model/channel_extra.go @@ -0,0 +1,51 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ExtraMember struct { + Id string `json:"id"` + FullName string `json:"full_name"` + Email string `json:"email"` + Roles string `json:"roles"` + Username string `json:"username"` +} + +func (o *ExtraMember) Sanitize(options map[string]bool) { + if len(options) == 0 || !options["email"] { + o.Email = "" + } + if len(options) == 0 || !options["fullname"] { + o.FullName = "" + } +} + +type ChannelExtra struct { + Id string `json:"id"` + Members []ExtraMember `json:"members"` +} + +func (o *ChannelExtra) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ChannelExtraFromJson(data io.Reader) *ChannelExtra { + decoder := json.NewDecoder(data) + var o ChannelExtra + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/channel_list.go b/model/channel_list.go new file mode 100644 index 000000000..088dbea2a --- /dev/null +++ b/model/channel_list.go @@ -0,0 +1,71 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type ChannelList struct { + Channels []*Channel `json:"channels"` + Members map[string]*ChannelMember `json:"members"` +} + +func (o *ChannelList) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func (o *ChannelList) Etag() string { + + id := "0" + var t int64 = 0 + var delta int64 = 0 + + for _, v := range o.Channels { + if v.LastPostAt > t { + t = v.LastPostAt + id = v.Id + } + + if v.UpdateAt > t { + t = v.UpdateAt + id = v.Id + } + + member := o.Members[v.Id] + + if member != nil { + max := v.LastPostAt + if v.UpdateAt > max { + max = v.UpdateAt + } + + delta += max - member.LastViewedAt + + if member.LastViewedAt > t { + t = member.LastViewedAt + id = v.Id + } + } + } + + return Etag(id, t, delta, len(o.Channels)) +} + +func ChannelListFromJson(data io.Reader) *ChannelList { + decoder := json.NewDecoder(data) + var o ChannelList + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/channel_member.go b/model/channel_member.go new file mode 100644 index 000000000..720ac4c42 --- /dev/null +++ b/model/channel_member.go @@ -0,0 +1,75 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" + "strings" +) + +const ( + CHANNEL_ROLE_ADMIN = "admin" + CHANNEL_NOTIFY_ALL = "all" + CHANNEL_NOTIFY_MENTION = "mention" + CHANNEL_NOTIFY_NONE = "none" + CHANNEL_NOTIFY_QUIET = "quiet" +) + +type ChannelMember struct { + ChannelId string `json:"channel_id"` + UserId string `json:"user_id"` + Roles string `json:"roles"` + LastViewedAt int64 `json:"last_viewed_at"` + MsgCount int64 `json:"msg_count"` + MentionCount int64 `json:"mention_count"` + NotifyLevel string `json:"notify_level"` +} + +func (o *ChannelMember) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ChannelMemberFromJson(data io.Reader) *ChannelMember { + decoder := json.NewDecoder(data) + var o ChannelMember + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *ChannelMember) IsValid() *AppError { + + if len(o.ChannelId) != 26 { + return NewAppError("ChannelMember.IsValid", "Invalid channel id", "") + } + + if len(o.UserId) != 26 { + return NewAppError("ChannelMember.IsValid", "Invalid user id", "") + } + + for _, role := range strings.Split(o.Roles, " ") { + if !(role == "" || role == CHANNEL_ROLE_ADMIN) { + return NewAppError("ChannelMember.IsValid", "Invalid role", "role="+role) + } + } + + if len(o.NotifyLevel) > 20 || !IsChannelNotifyLevelValid(o.NotifyLevel) { + return NewAppError("ChannelMember.IsValid", "Invalid notify level", "notify_level="+o.NotifyLevel) + } + + return nil +} + +func IsChannelNotifyLevelValid(notifyLevel string) bool { + return notifyLevel == CHANNEL_NOTIFY_ALL || notifyLevel == CHANNEL_NOTIFY_MENTION || notifyLevel == CHANNEL_NOTIFY_NONE || notifyLevel == CHANNEL_NOTIFY_QUIET +} diff --git a/model/channel_member_test.go b/model/channel_member_test.go new file mode 100644 index 000000000..3b64ffbf7 --- /dev/null +++ b/model/channel_member_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestChannelMemberJson(t *testing.T) { + o := ChannelMember{ChannelId: NewId(), UserId: NewId()} + json := o.ToJson() + ro := ChannelMemberFromJson(strings.NewReader(json)) + + if o.ChannelId != ro.ChannelId { + t.Fatal("Ids do not match") + } +} + +func TestChannelMemberIsValid(t *testing.T) { + o := ChannelMember{} + + 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.Roles = "missing" + o.NotifyLevel = CHANNEL_NOTIFY_ALL + o.UserId = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Roles = CHANNEL_ROLE_ADMIN + o.NotifyLevel = "junk" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.NotifyLevel = "123456789012345678901" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.NotifyLevel = CHANNEL_NOTIFY_ALL + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.Roles = "" + if err := o.IsValid(); err != nil { + t.Fatal(err) + } +} diff --git a/model/channel_test.go b/model/channel_test.go new file mode 100644 index 000000000..21fe71889 --- /dev/null +++ b/model/channel_test.go @@ -0,0 +1,81 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestChannelJson(t *testing.T) { + o := Channel{Id: NewId(), Name: NewId()} + json := o.ToJson() + ro := ChannelFromJson(strings.NewReader(json)) + + if o.Id != ro.Id { + t.Fatal("Ids do not match") + } +} + +func TestChannelIsValid(t *testing.T) { + o := Channel{} + + 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.DisplayName = strings.Repeat("01234567890", 20) + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.DisplayName = "1234" + o.Name = "ZZZZZZZ" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Name = "zzzzz" + + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Type = "U" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Type = "P" + + if err := o.IsValid(); err != nil { + t.Fatal(err) + } +} + +func TestChannelPreSave(t *testing.T) { + o := Channel{Name: "test"} + o.PreSave() + o.Etag() +} + +func TestChannelPreUpdate(t *testing.T) { + o := Channel{Name: "test"} + o.PreUpdate() +} diff --git a/model/client.go b/model/client.go new file mode 100644 index 000000000..0448828bb --- /dev/null +++ b/model/client.go @@ -0,0 +1,652 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" +) + +const ( + HEADER_REQUEST_ID = "X-Request-ID" + HEADER_VERSION_ID = "X-Version-ID" + HEADER_ETAG_SERVER = "ETag" + HEADER_ETAG_CLIENT = "If-None-Match" + HEADER_FORWARDED = "X-Forwarded-For" + HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" + HEADER_TOKEN = "token" + HEADER_AUTH = "Authorization" +) + +type Result struct { + RequestId string + Etag string + Data interface{} +} + +type Client struct { + Url string // The location of the server like "http://localhost/api/v1" + HttpClient *http.Client // The http client + AuthToken string +} + +// NewClient constructs a new client with convienence methods for talking to +// the server. +func NewClient(url string) *Client { + return &Client{url, &http.Client{}, ""} +} + +func (c *Client) DoPost(url string, data string) (*http.Response, *AppError) { + rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data)) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil { + return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + } else if rp.StatusCode >= 300 { + return nil, AppErrorFromJson(rp.Body) + } else { + return rp, nil + } +} + +func (c *Client) DoGet(url string, data string, etag string) (*http.Response, *AppError) { + rq, _ := http.NewRequest("GET", c.Url+url, strings.NewReader(data)) + + if len(etag) > 0 { + rq.Header.Set(HEADER_ETAG_CLIENT, etag) + } + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil { + return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + } else if rp.StatusCode == 304 { + return rp, nil + } else if rp.StatusCode >= 300 { + return rp, AppErrorFromJson(rp.Body) + } else { + return rp, nil + } +} + +func getCookie(name string, resp *http.Response) *http.Cookie { + for _, cookie := range resp.Cookies() { + if cookie.Name == name { + return cookie + } + } + + return nil +} + +func (c *Client) Must(result *Result, err *AppError) *Result { + if err != nil { + panic(err) + } + + return result +} + +func (c *Client) SignupTeam(email string, name string) (*Result, *AppError) { + m := make(map[string]string) + m["email"] = email + m["name"] = name + if r, err := c.DoPost("/teams/signup", MapToJson(m)); 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) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppError) { + if r, err := c.DoPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamSignupFromJson(r.Body)}, nil + } +} + +func (c *Client) CreateTeam(team *Team) (*Result, *AppError) { + if r, err := c.DoPost("/teams/create", team.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil + } +} + +func (c *Client) FindTeamByDomain(domain string, allServers bool) (*Result, *AppError) { + m := make(map[string]string) + m["domain"] = domain + m["all"] = fmt.Sprintf("%v", allServers) + if r, err := c.DoPost("/teams/find_team_by_domain", MapToJson(m)); err != nil { + return nil, err + } else { + val := false + if body, _ := ioutil.ReadAll(r.Body); string(body) == "true" { + val = true + } + + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), val}, nil + } +} + +func (c *Client) FindTeams(email string) (*Result, *AppError) { + m := make(map[string]string) + m["email"] = email + if r, err := c.DoPost("/teams/find_teams", MapToJson(m)); err != nil { + return nil, err + } else { + + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil + } +} + +func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) { + m := make(map[string]string) + m["email"] = email + if r, err := c.DoPost("/teams/email_teams", MapToJson(m)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil + } +} + +func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { + if r, err := c.DoPost("/teams/invite_members", invites.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), InvitesFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdateTeamName(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/teams/update_name", 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) CreateUser(user *User, hash string) (*Result, *AppError) { + if r, err := c.DoPost("/users/create", user.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Result, *AppError) { + if r, err := c.DoPost("/users/create?d="+data+"&h="+hash, user.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) GetUser(id string, etag string) (*Result, *AppError) { + if r, err := c.DoGet("/users/"+id, "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) GetMe(etag string) (*Result, *AppError) { + if r, err := c.DoGet("/users/me", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) { + if r, err := c.DoGet("/users/profiles", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserMapFromJson(r.Body)}, nil + } +} + +func (c *Client) LoginById(id string, password string) (*Result, *AppError) { + m := make(map[string]string) + m["id"] = id + m["password"] = password + return c.login(m) +} + +func (c *Client) LoginByEmail(domain string, email string, password string) (*Result, *AppError) { + m := make(map[string]string) + m["domain"] = domain + m["email"] = email + m["password"] = password + return c.login(m) +} + +func (c *Client) LoginByEmailWithDevice(domain string, email string, password string, deviceId string) (*Result, *AppError) { + m := make(map[string]string) + m["domain"] = domain + m["email"] = email + m["password"] = password + m["device_id"] = deviceId + return c.login(m) +} + +func (c *Client) login(m map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/users/login", MapToJson(m)); err != nil { + return nil, err + } else { + c.AuthToken = r.Header.Get(HEADER_TOKEN) + sessionId := getCookie(SESSION_TOKEN, r) + + if c.AuthToken != sessionId.Value { + NewAppError("/users/login", "Authentication tokens didn't match", "") + } + + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) Logout() (*Result, *AppError) { + if r, err := c.DoPost("/users/logout", ""); err != nil { + return nil, err + } else { + c.AuthToken = "" + + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) RevokeSession(sessionAltId string) (*Result, *AppError) { + m := make(map[string]string) + m["id"] = sessionAltId + + if r, err := c.DoPost("/users/revoke_session", MapToJson(m)); 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) GetSessions(id string) (*Result, *AppError) { + if r, err := c.DoGet("/users/"+id+"/sessions", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), SessionsFromJson(r.Body)}, nil + } +} + +func (c *Client) Command(channelId string, command string, suggest bool) (*Result, *AppError) { + m := make(map[string]string) + m["command"] = command + m["channelId"] = channelId + m["suggest"] = strconv.FormatBool(suggest) + if r, err := c.DoPost("/command", MapToJson(m)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), CommandFromJson(r.Body)}, nil + } +} + +func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) { + if r, err := c.DoGet("/users/"+id+"/audits", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), AuditsFromJson(r.Body)}, nil + } +} + +func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) { + if r, err := c.DoPost("/channels/create", channel.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil + } +} + +func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/channels/create_direct", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) { + if r, err := c.DoPost("/channels/update", channel.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/channels/update_desc", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/channels/update_notify_level", 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) GetChannels(etag string) (*Result, *AppError) { + if r, err := c.DoGet("/channels/", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil + } +} + +func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) { + if r, err := c.DoGet("/channels/more", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelListFromJson(r.Body)}, nil + } +} + +func (c *Client) JoinChannel(id string) (*Result, *AppError) { + if r, err := c.DoPost("/channels/"+id+"/join", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), nil}, nil + } +} + +func (c *Client) LeaveChannel(id string) (*Result, *AppError) { + if r, err := c.DoPost("/channels/"+id+"/leave", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), nil}, nil + } +} + +func (c *Client) DeleteChannel(id string) (*Result, *AppError) { + if r, err := c.DoPost("/channels/"+id+"/delete", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), nil}, nil + } +} + +func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) { + data := make(map[string]string) + data["user_id"] = user_id + if r, err := c.DoPost("/channels/"+id+"/add", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), nil}, nil + } +} + +func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) { + data := make(map[string]string) + data["user_id"] = user_id + if r, err := c.DoPost("/channels/"+id+"/remove", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), nil}, nil + } +} + +func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) { + if r, err := c.DoPost("/channels/"+channelId+"/update_last_viewed_at", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), nil}, nil + } +} + +func (c *Client) GetChannelExtraInfo(id string) (*Result, *AppError) { + if r, err := c.DoGet("/channels/"+id+"/extra_info", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), ChannelExtraFromJson(r.Body)}, nil + } +} + +func (c *Client) CreatePost(post *Post) (*Result, *AppError) { + if r, err := c.DoPost("/channels/"+post.ChannelId+"/create", post.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil + } +} + +func (c *Client) CreateValetPost(post *Post) (*Result, *AppError) { + if r, err := c.DoPost("/channels/"+post.ChannelId+"/valet_create", post.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdatePost(post *Post) (*Result, *AppError) { + if r, err := c.DoPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), PostFromJson(r.Body)}, nil + } +} + +func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) (*Result, *AppError) { + if r, err := c.DoGet(fmt.Sprintf("/channels/%v/posts/%v/%v", channelId, offset, limit), "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil + } +} + +func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) { + if r, err := c.DoGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil + } +} + +func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError) { + if r, err := c.DoPost(fmt.Sprintf("/channels/%v/post/%v/delete", channelId, postId), ""); 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) SearchPosts(terms string) (*Result, *AppError) { + if r, err := c.DoGet("/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil + } +} + +func (c *Client) UploadFile(url string, data []byte, contentType string) (*Result, *AppError) { + rq, _ := http.NewRequest("POST", c.Url+url, bytes.NewReader(data)) + rq.Header.Set("Content-Type", contentType) + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil { + return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + } else if rp.StatusCode >= 300 { + return nil, AppErrorFromJson(rp.Body) + } else { + return &Result{rp.Header.Get(HEADER_REQUEST_ID), + rp.Header.Get(HEADER_ETAG_SERVER), FileUploadResponseFromJson(rp.Body)}, nil + } +} + +func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) { + var rq *http.Request + if isFullUrl { + rq, _ = http.NewRequest("GET", url, nil) + } else { + rq, _ = http.NewRequest("GET", c.Url+url, nil) + } + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil { + return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + } else if rp.StatusCode >= 300 { + return nil, AppErrorFromJson(rp.Body) + } else { + return &Result{rp.Header.Get(HEADER_REQUEST_ID), + rp.Header.Get(HEADER_ETAG_SERVER), rp.Body}, nil + } +} + +func (c *Client) GetPublicLink(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/files/get_public_link", 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) UpdateUser(user *User) (*Result, *AppError) { + if r, err := c.DoPost("/users/update", user.ToJson()); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdateUserRoles(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/users/update_roles", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdateActive(userId string, active bool) (*Result, *AppError) { + data := make(map[string]string) + data["user_id"] = userId + data["active"] = strconv.FormatBool(active) + if r, err := c.DoPost("/users/update_active", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdateUserNotify(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/users/update_notify", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string) (*Result, *AppError) { + data := make(map[string]string) + data["current_password"] = currentPassword + data["new_password"] = newPassword + data["user_id"] = userId + + if r, err := c.DoPost("/users/newpassword", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), UserFromJson(r.Body)}, nil + } +} + +func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/users/send_password_reset", 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) ResetPassword(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/users/reset_password", 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) GetStatuses() (*Result, *AppError) { + if r, err := c.DoGet("/users/status", "", ""); 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) MockSession(sessionToken string) { + c.AuthToken = sessionToken +} diff --git a/model/command.go b/model/command.go new file mode 100644 index 000000000..23573205e --- /dev/null +++ b/model/command.go @@ -0,0 +1,53 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + RESP_EXECUTED = "executed" +) + +type Command struct { + Command string `json:"command"` + Response string `json:"reponse"` + GotoLocation string `json:"goto_location"` + ChannelId string `json:"channel_id"` + Suggest bool `json:"-"` + Suggestions []*SuggestCommand `json:"suggestions"` +} + +func (o *Command) AddSuggestion(suggest *SuggestCommand) { + + if o.Suggest { + if o.Suggestions == nil { + o.Suggestions = make([]*SuggestCommand, 0, 128) + } + + o.Suggestions = append(o.Suggestions, suggest) + } +} + +func (o *Command) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func CommandFromJson(data io.Reader) *Command { + decoder := json.NewDecoder(data) + var o Command + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/command_test.go b/model/command_test.go new file mode 100644 index 000000000..e3b732c02 --- /dev/null +++ b/model/command_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestCommandJson(t *testing.T) { + + command := &Command{Command: NewId(), Suggest: true} + command.AddSuggestion(&SuggestCommand{Suggestion: NewId()}) + json := command.ToJson() + result := CommandFromJson(strings.NewReader(json)) + + if command.Command != result.Command { + t.Fatal("Ids do not match") + } + + if command.Suggestions[0].Suggestion != result.Suggestions[0].Suggestion { + t.Fatal("Ids do not match") + } +} diff --git a/model/file.go b/model/file.go new file mode 100644 index 000000000..7f5a3f916 --- /dev/null +++ b/model/file.go @@ -0,0 +1,42 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + MAX_FILE_SIZE = 50000000 // 50 MB +) + +var ( + IMAGE_EXTENSIONS = [4]string{".jpg", ".gif", ".bmp", ".png"} + IMAGE_MIME_TYPES = map[string]string{".jpg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff"} +) + +type FileUploadResponse struct { + Filenames []string `json:"filenames"` +} + +func FileUploadResponseFromJson(data io.Reader) *FileUploadResponse { + decoder := json.NewDecoder(data) + var o FileUploadResponse + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *FileUploadResponse) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} diff --git a/model/message.go b/model/message.go new file mode 100644 index 000000000..47f598af8 --- /dev/null +++ b/model/message.go @@ -0,0 +1,54 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +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" +) + +type Message struct { + TeamId string `json:"team_id"` + ChannelId string `json:"channel_id"` + UserId string `json:"user_id"` + Action string `json:"action"` + Props map[string]string `json:"props"` +} + +func (m *Message) Add(key string, value string) { + m.Props[key] = value +} + +func NewMessage(teamId string, channekId string, userId string, action string) *Message { + return &Message{TeamId: teamId, ChannelId: channekId, UserId: userId, Action: action, Props: make(map[string]string)} +} + +func (o *Message) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func MessageFromJson(data io.Reader) *Message { + decoder := json.NewDecoder(data) + var o Message + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/message_test.go b/model/message_test.go new file mode 100644 index 000000000..eb0c75193 --- /dev/null +++ b/model/message_test.go @@ -0,0 +1,24 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestMessgaeJson(t *testing.T) { + m := NewMessage(NewId(), NewId(), NewId(), ACTION_TYPING) + m.Add("RootId", NewId()) + json := m.ToJson() + result := MessageFromJson(strings.NewReader(json)) + + if m.TeamId != result.TeamId { + t.Fatal("Ids do not match") + } + + if m.Props["RootId"] != result.Props["RootId"] { + t.Fatal("Ids do not match") + } +} diff --git a/model/post.go b/model/post.go new file mode 100644 index 000000000..d10a9f9fc --- /dev/null +++ b/model/post.go @@ -0,0 +1,147 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + POST_DEFAULT = "" +) + +type Post struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + RootId string `json:"root_id"` + ParentId string `json:"parent_id"` + OriginalId string `json:"original_id"` + Message string `json:"message"` + ImgCount int64 `json:"img_count"` + Type string `json:"type"` + Props StringMap `json:"props"` + Hashtags string `json:"hashtags"` + Filenames StringArray `json:"filenames"` +} + +func (o *Post) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func PostFromJson(data io.Reader) *Post { + decoder := json.NewDecoder(data) + var o Post + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *Post) Etag() string { + return Etag(o.Id, o.UpdateAt) +} + +func (o *Post) IsValid() *AppError { + + if len(o.Id) != 26 { + return NewAppError("Post.IsValid", "Invalid Id", "") + } + + if o.CreateAt == 0 { + return NewAppError("Post.IsValid", "Create at must be a valid time", "id="+o.Id) + } + + if o.UpdateAt == 0 { + return NewAppError("Post.IsValid", "Update at must be a valid time", "id="+o.Id) + } + + if len(o.UserId) != 26 { + return NewAppError("Post.IsValid", "Invalid user id", "") + } + + if len(o.ChannelId) != 26 { + return NewAppError("Post.IsValid", "Invalid channel id", "") + } + + if !(len(o.RootId) == 26 || len(o.RootId) == 0) { + return NewAppError("Post.IsValid", "Invalid root id", "") + } + + if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) { + return NewAppError("Post.IsValid", "Invalid parent id", "") + } + + if len(o.ParentId) == 26 && len(o.RootId) == 0 { + return NewAppError("Post.IsValid", "Invalid root id must be set if parent id set", "") + } + + if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) { + return NewAppError("Post.IsValid", "Invalid original id", "") + } + + if len(o.Message) > 4000 { + return NewAppError("Post.IsValid", "Invalid message", "id="+o.Id) + } + + if len(o.Hashtags) > 1000 { + return NewAppError("Post.IsValid", "Invalid hashtags", "id="+o.Id) + } + + if !(o.Type == POST_DEFAULT) { + return NewAppError("Post.IsValid", "Invalid type", "id="+o.Type) + } + + if len(ArrayToJson(o.Filenames)) > 4000 { + return NewAppError("Post.IsValid", "Invalid filenames", "id="+o.Id) + } + + return nil +} + +func (o *Post) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + o.OriginalId = "" + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt + + if o.Props == nil { + o.Props = make(map[string]string) + } + + if o.Filenames == nil { + o.Filenames = []string{} + } +} + +func (o *Post) MakeNonNil() { + if o.Props == nil { + o.Props = make(map[string]string) + } + if o.Filenames == nil { + o.Filenames = []string{} + } +} + +func (o *Post) AddProp(key string, value string) { + + o.MakeNonNil() + + o.Props[key] = value +} diff --git a/model/post_list.go b/model/post_list.go new file mode 100644 index 000000000..88e3a9193 --- /dev/null +++ b/model/post_list.go @@ -0,0 +1,91 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type PostList struct { + Order []string `json:"order"` + Posts map[string]*Post `json:"posts"` +} + +func (o *PostList) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func (o *PostList) MakeNonNil() { + if o.Order == nil { + o.Order = make([]string, 0) + } + + if o.Posts == nil { + o.Posts = make(map[string]*Post) + } + + for _, v := range o.Posts { + v.MakeNonNil() + } +} + +func (o *PostList) AddOrder(id string) { + + if o.Order == nil { + o.Order = make([]string, 0, 128) + } + + o.Order = append(o.Order, id) +} + +func (o *PostList) AddPost(post *Post) { + + if o.Posts == nil { + o.Posts = make(map[string]*Post) + } + + o.Posts[post.Id] = post +} + +func (o *PostList) Etag() string { + + id := "0" + var t int64 = 0 + + for _, v := range o.Posts { + if v.UpdateAt > t { + t = v.UpdateAt + id = v.Id + } + } + + return Etag(id, t) +} + +func (o *PostList) IsChannelId(channelId string) bool { + for _, v := range o.Posts { + if v.ChannelId != channelId { + return false + } + } + + return true +} + +func PostListFromJson(data io.Reader) *PostList { + decoder := json.NewDecoder(data) + var o PostList + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/post_list_test.go b/model/post_list_test.go new file mode 100644 index 000000000..7ce36c399 --- /dev/null +++ b/model/post_list_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestPostListJson(t *testing.T) { + + pl := PostList{} + p1 := &Post{Id: NewId(), Message: NewId()} + pl.AddPost(p1) + p2 := &Post{Id: NewId(), Message: NewId()} + pl.AddPost(p2) + + pl.AddOrder(p1.Id) + pl.AddOrder(p2.Id) + + json := pl.ToJson() + rpl := PostListFromJson(strings.NewReader(json)) + + if rpl.Posts[p1.Id].Message != p1.Message { + t.Fatal("failed to serialize") + } + + if rpl.Posts[p2.Id].Message != p2.Message { + t.Fatal("failed to serialize") + } + + if rpl.Order[1] != p2.Id { + t.Fatal("failed to serialize") + } +} diff --git a/model/post_test.go b/model/post_test.go new file mode 100644 index 000000000..38f4b4c98 --- /dev/null +++ b/model/post_test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestPostJson(t *testing.T) { + o := Post{Id: NewId(), Message: NewId()} + json := o.ToJson() + ro := PostFromJson(strings.NewReader(json)) + + if o.Id != ro.Id { + t.Fatal("Ids do not match") + } +} + +func TestPostIsValid(t *testing.T) { + o := Post{} + + 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.UserId = NewId() + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.ChannelId = NewId() + o.RootId = "123" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.RootId = "" + o.ParentId = "123" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.ParentId = NewId() + o.RootId = "" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.ParentId = "" + o.Message = strings.Repeat("0", 4001) + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Message = strings.Repeat("0", 4000) + if err := o.IsValid(); err != nil { + t.Fatal(err) + } + + o.Message = "test" + if err := o.IsValid(); err != nil { + t.Fatal(err) + } +} + +func TestPostPreSave(t *testing.T) { + o := Post{Message: "test"} + o.PreSave() + o.Etag() +} diff --git a/model/session.go b/model/session.go new file mode 100644 index 000000000..9fd3b9ec3 --- /dev/null +++ b/model/session.go @@ -0,0 +1,119 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + SESSION_TOKEN = "MMSID" + SESSION_TIME_WEB_IN_DAYS = 365 + SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS + SESSION_TIME_MOBILE_IN_DAYS = 365 + SESSION_TIME_MOBILE_IN_SECS = 60 * 60 * 24 * SESSION_TIME_MOBILE_IN_DAYS + SESSION_CACHE_IN_SECS = 60 * 10 + SESSION_CACHE_SIZE = 10000 + SESSION_PROP_PLATFORM = "platform" + SESSION_PROP_OS = "os" + SESSION_PROP_BROWSER = "browser" +) + +type Session struct { + Id string `json:"id"` + AltId string `json:"alt_id"` + CreateAt int64 `json:"create_at"` + ExpiresAt int64 `json:"expires_at"` + LastActivityAt int64 `json:"last_activity_at"` + UserId string `json:"user_id"` + TeamId string `json:"team_id"` + DeviceId string `json:"device_id"` + Roles string `json:"roles"` + Props StringMap `json:"props"` +} + +func (me *Session) ToJson() string { + b, err := json.Marshal(me) + if err != nil { + return "" + } else { + return string(b) + } +} + +func SessionFromJson(data io.Reader) *Session { + decoder := json.NewDecoder(data) + var me Session + err := decoder.Decode(&me) + if err == nil { + return &me + } else { + return nil + } +} + +func (me *Session) PreSave() { + if me.Id == "" { + me.Id = NewId() + } + + me.AltId = NewId() + + me.CreateAt = GetMillis() + me.LastActivityAt = me.CreateAt + + if me.Props == nil { + me.Props = make(map[string]string) + } +} + +func (me *Session) Sanitize() { + me.Id = "" +} + +func (me *Session) IsExpired() bool { + + if me.ExpiresAt <= 0 { + return false + } + + if GetMillis() > me.ExpiresAt { + return true + } + + return false +} + +func (me *Session) SetExpireInDays(days int64) { + me.ExpiresAt = GetMillis() + (1000 * 60 * 60 * 24 * days) +} + +func (me *Session) AddProp(key string, value string) { + + if me.Props == nil { + me.Props = make(map[string]string) + } + + me.Props[key] = value +} + +func SessionsToJson(o []*Session) string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func SessionsFromJson(data io.Reader) []*Session { + decoder := json.NewDecoder(data) + var o []*Session + err := decoder.Decode(&o) + if err == nil { + return o + } else { + return nil + } +} diff --git a/model/session_test.go b/model/session_test.go new file mode 100644 index 000000000..4df2a0d76 --- /dev/null +++ b/model/session_test.go @@ -0,0 +1,35 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" + "time" +) + +func TestSessionJson(t *testing.T) { + session := Session{} + session.PreSave() + json := session.ToJson() + rsession := SessionFromJson(strings.NewReader(json)) + + if rsession.Id != session.Id { + t.Fatal("Ids do not match") + } + + session.Sanitize() + + if session.IsExpired() { + t.Fatal("Shouldn't expire") + } + + session.ExpiresAt = GetMillis() + time.Sleep(10 * time.Millisecond) + if !session.IsExpired() { + t.Fatal("Should expire") + } + + session.SetExpireInDays(10) +} diff --git a/model/suggest_command.go b/model/suggest_command.go new file mode 100644 index 000000000..0c07ac58e --- /dev/null +++ b/model/suggest_command.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type SuggestCommand struct { + Suggestion string `json:"suggestion"` + Description string `json:"description"` +} + +func (o *SuggestCommand) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func SuggestCommandFromJson(data io.Reader) *SuggestCommand { + decoder := json.NewDecoder(data) + var o SuggestCommand + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/suggest_command_test.go b/model/suggest_command_test.go new file mode 100644 index 000000000..a92b676ad --- /dev/null +++ b/model/suggest_command_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestSuggestCommandJson(t *testing.T) { + command := &SuggestCommand{Suggestion: NewId()} + json := command.ToJson() + result := SuggestCommandFromJson(strings.NewReader(json)) + + if command.Suggestion != result.Suggestion { + t.Fatal("Ids do not match") + } +} diff --git a/model/team.go b/model/team.go new file mode 100644 index 000000000..a510cde78 --- /dev/null +++ b/model/team.go @@ -0,0 +1,141 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + TEAM_OPEN = "O" + TEAM_INVITE = "I" +) + +type Team struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + Name string `json:"name"` + Domain string `json:"domain"` + Email string `json:"email"` + Type string `json:"type"` + CompanyName string `json:"company_name"` + AllowedDomains string `json:"allowed_domains"` +} + +type Invites struct { + Invites []map[string]string `json:"invites"` +} + +func InvitesFromJson(data io.Reader) *Invites { + decoder := json.NewDecoder(data) + var o Invites + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *Invites) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func (o *Team) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func TeamFromJson(data io.Reader) *Team { + decoder := json.NewDecoder(data) + var o Team + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} + +func (o *Team) Etag() string { + return Etag(o.Id, o.UpdateAt) +} + +func (o *Team) IsValid() *AppError { + + if len(o.Id) != 26 { + return NewAppError("Team.IsValid", "Invalid Id", "") + } + + if o.CreateAt == 0 { + return NewAppError("Team.IsValid", "Create at must be a valid time", "id="+o.Id) + } + + if o.UpdateAt == 0 { + return NewAppError("Team.IsValid", "Update at must be a valid time", "id="+o.Id) + } + + if len(o.Email) > 128 { + return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id) + } + + if !IsValidEmail(o.Email) { + return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id) + } + + if len(o.Name) > 64 { + return NewAppError("Team.IsValid", "Invalid name", "id="+o.Id) + } + + if len(o.Domain) > 64 { + return NewAppError("Team.IsValid", "Invalid domain", "id="+o.Id) + } + + if IsReservedDomain(o.Domain) { + return NewAppError("Team.IsValid", "This URL is unavailable. Please try another.", "id="+o.Id) + } + + if !IsValidDomain(o.Domain) { + return NewAppError("Team.IsValid", "Domain must be 4 or more lowercase alphanumeric characters", "id="+o.Id) + } + + if !(o.Type == TEAM_OPEN || o.Type == TEAM_INVITE) { + return NewAppError("Team.IsValid", "Invalid type", "id="+o.Id) + } + + if len(o.CompanyName) > 64 { + return NewAppError("Team.IsValid", "Invalid company name", "id="+o.Id) + } + + if len(o.AllowedDomains) > 500 { + return NewAppError("Team.IsValid", "Invalid allowed domains", "id="+o.Id) + } + + return nil +} + +func (o *Team) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + o.CreateAt = GetMillis() + o.UpdateAt = o.CreateAt +} + +func (o *Team) PreUpdate() { + o.UpdateAt = GetMillis() +} diff --git a/model/team_signup.go b/model/team_signup.go new file mode 100644 index 000000000..143ba8db1 --- /dev/null +++ b/model/team_signup.go @@ -0,0 +1,40 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "fmt" + "io" +) + +type TeamSignup struct { + Team Team `json:"team"` + User User `json:"user"` + Invites []string `json:"invites"` + Data string `json:"data"` + Hash string `json:"hash"` +} + +func TeamSignupFromJson(data io.Reader) *TeamSignup { + decoder := json.NewDecoder(data) + var o TeamSignup + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + fmt.Println(err) + + return nil + } +} + +func (o *TeamSignup) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} diff --git a/model/team_signup_test.go b/model/team_signup_test.go new file mode 100644 index 000000000..f3f74470b --- /dev/null +++ b/model/team_signup_test.go @@ -0,0 +1,20 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestTeamSignupJson(t *testing.T) { + team := Team{Id: NewId(), Name: NewId()} + o := TeamSignup{Team: team, Data: "data"} + json := o.ToJson() + ro := TeamSignupFromJson(strings.NewReader(json)) + + if o.Team.Id != ro.Team.Id { + t.Fatal("Ids do not match") + } +} diff --git a/model/team_test.go b/model/team_test.go new file mode 100644 index 000000000..6261ed6bf --- /dev/null +++ b/model/team_test.go @@ -0,0 +1,76 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestTeamJson(t *testing.T) { + o := Team{Id: NewId(), Name: NewId()} + json := o.ToJson() + ro := TeamFromJson(strings.NewReader(json)) + + if o.Id != ro.Id { + t.Fatal("Ids do not match") + } +} + +func TestTeamIsValid(t *testing.T) { + o := Team{} + + 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.Email = strings.Repeat("01234567890", 20) + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Email = "corey@hulen.com" + o.Name = strings.Repeat("01234567890", 20) + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Name = "1234" + o.Domain = "ZZZZZZZ" + if err := o.IsValid(); err == nil { + t.Fatal("should be invalid") + } + + o.Domain = "zzzzz" + o.Type = TEAM_OPEN + if err := o.IsValid(); err != nil { + t.Fatal(err) + } +} + +func TestTeamPreSave(t *testing.T) { + o := Team{Name: "test"} + o.PreSave() + o.Etag() +} + +func TestTeamPreUpdate(t *testing.T) { + o := Team{Name: "test"} + o.PreUpdate() +} diff --git a/model/user.go b/model/user.go new file mode 100644 index 000000000..794adcad4 --- /dev/null +++ b/model/user.go @@ -0,0 +1,293 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "code.google.com/p/go.crypto/bcrypt" + "encoding/json" + "io" + "regexp" + "strings" +) + +const ( + ROLE_ADMIN = "admin" + ROLE_SYSTEM_ADMIN = "system_admin" + ROLE_SYSTEM_SUPPORT = "system_support" + USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes + USER_OFFLINE_TIMEOUT = 5 * 60 * 1000 // 5 minutes + USER_OFFLINE = "offline" + USER_AWAY = "away" + USER_ONLINE = "online" + USER_NOTIFY_ALL = "all" + USER_NOTIFY_MENTION = "mention" + USER_NOTIFY_NONE = "none" + BOT_USERNAME = "valet" +) + +type User struct { + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + TeamId string `json:"team_id"` + Username string `json:"username"` + Password string `json:"password"` + AuthData string `json:"auth_data"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + FullName string `json:"full_name"` + Roles string `json:"roles"` + LastActivityAt int64 `json:"last_activity_at"` + LastPingAt int64 `json:"last_ping_at"` + AllowMarketing bool `json:"allow_marketing"` + Props StringMap `json:"props"` + NotifyProps StringMap `json:"notify_props"` + LastPasswordUpdate int64 `json:"last_password_update"` +} + +// IsValid validates the user and returns an error if it isn't configured +// correctly. +func (u *User) IsValid() *AppError { + + if len(u.Id) != 26 { + return NewAppError("User.IsValid", "Invalid user id", "") + } + + if u.CreateAt == 0 { + return NewAppError("User.IsValid", "Create at must be a valid time", "user_id="+u.Id) + } + + if u.UpdateAt == 0 { + return NewAppError("User.IsValid", "Update at must be a valid time", "user_id="+u.Id) + } + + if len(u.TeamId) != 26 { + return NewAppError("User.IsValid", "Invalid team id", "") + } + + if len(u.Username) == 0 || len(u.Username) > 64 { + return NewAppError("User.IsValid", "Invalid username", "user_id="+u.Id) + } + + validChars, _ := regexp.Compile("^[a-z0-9\\.\\-\\_]+$") + + if !validChars.MatchString(u.Username) { + return NewAppError("User.IsValid", "Invalid username", "user_id="+u.Id) + } + + if len(u.Email) > 128 || len(u.Email) == 0 { + return NewAppError("User.IsValid", "Invalid email", "user_id="+u.Id) + } + + if len(u.FullName) > 64 { + return NewAppError("User.IsValid", "Invalid full name", "user_id="+u.Id) + } + + return nil +} + +// PreSave will set the Id and Username if missing. It will also fill +// in the CreateAt, UpdateAt times. It will also hash the password. It should +// be run before saving the user to the db. +func (u *User) PreSave() { + if u.Id == "" { + u.Id = NewId() + } + + if u.Username == "" { + u.Username = NewId() + } + + u.Username = strings.ToLower(u.Username) + u.Email = strings.ToLower(u.Email) + + u.CreateAt = GetMillis() + u.UpdateAt = u.CreateAt + + u.LastPasswordUpdate = u.CreateAt + + if u.Props == nil { + u.Props = make(map[string]string) + } + + if u.NotifyProps == nil || len(u.NotifyProps) == 0 { + u.SetDefaultNotifications() + } + + if len(u.Password) > 0 { + u.Password = HashPassword(u.Password) + } +} + +// PreUpdate should be run before updating the user in the db. +func (u *User) PreUpdate() { + u.Username = strings.ToLower(u.Username) + u.Email = strings.ToLower(u.Email) + u.UpdateAt = GetMillis() + + if u.NotifyProps == nil || len(u.NotifyProps) == 0 { + u.SetDefaultNotifications() + } else if _, ok := u.NotifyProps["mention_keys"]; ok { + // Remove any blank mention keys + splitKeys := strings.Split(u.NotifyProps["mention_keys"], ",") + goodKeys := []string{} + for _, key := range splitKeys { + if len(key) > 0 { + goodKeys = append(goodKeys, strings.ToLower(key)) + } + } + u.NotifyProps["mention_keys"] = strings.Join(goodKeys, ",") + } +} + +func (u *User) SetDefaultNotifications() { + u.NotifyProps = make(map[string]string) + u.NotifyProps["email"] = "true" + u.NotifyProps["desktop"] = USER_NOTIFY_ALL + u.NotifyProps["desktop_sound"] = "true" + u.NotifyProps["mention_keys"] = u.Username + u.NotifyProps["first_name"] = "true" + splitName := strings.Split(u.FullName, " ") + if len(splitName) > 0 && splitName[0] != "" { + u.NotifyProps["mention_keys"] += "," + splitName[0] + } +} + +// ToJson convert a User to a json string +func (u *User) ToJson() string { + b, err := json.Marshal(u) + if err != nil { + return "" + } else { + return string(b) + } +} + +// Generate a valid strong etag so the browser can cache the results +func (u *User) Etag() string { + return Etag(u.Id, u.UpdateAt) +} + +func (u *User) IsOffline() bool { + return (GetMillis()-u.LastPingAt) > USER_OFFLINE_TIMEOUT && (GetMillis()-u.LastActivityAt) > USER_OFFLINE_TIMEOUT +} + +func (u *User) IsAway() bool { + return (GetMillis() - u.LastActivityAt) > USER_AWAY_TIMEOUT +} + +// Remove any private data from the user object +func (u *User) Sanitize(options map[string]bool) { + u.Password = "" + u.AuthData = "" + + if len(options) != 0 && !options["email"] { + u.Email = "" + } + if len(options) != 0 && !options["fullname"] { + u.FullName = "" + } + if len(options) != 0 && !options["skypeid"] { + // TODO - fill in when SkypeId is added to user model + } + if len(options) != 0 && !options["phonenumber"] { + // TODO - fill in when PhoneNumber is added to user model + } + if len(options) != 0 && !options["passwordupadte"] { + u.LastPasswordUpdate = 0 + } +} + +func (u *User) MakeNonNil() { + if u.Props == nil { + u.Props = make(map[string]string) + } + + if u.NotifyProps == nil { + u.NotifyProps = make(map[string]string) + } +} + +func (u *User) AddProp(key string, value string) { + u.MakeNonNil() + + u.Props[key] = value +} + +func (u *User) AddNotifyProp(key string, value string) { + u.MakeNonNil() + + u.NotifyProps[key] = value +} + +// UserFromJson will decode the input and return a User +func UserFromJson(data io.Reader) *User { + decoder := json.NewDecoder(data) + var user User + err := decoder.Decode(&user) + if err == nil { + return &user + } else { + return nil + } +} + +func UserMapToJson(u map[string]*User) string { + b, err := json.Marshal(u) + if err != nil { + return "" + } else { + return string(b) + } +} + +func UserMapFromJson(data io.Reader) map[string]*User { + decoder := json.NewDecoder(data) + var users map[string]*User + err := decoder.Decode(&users) + if err == nil { + return users + } else { + return nil + } +} + +// HashPassword generates a hash using the bcrypt.GenerateFromPassword +func HashPassword(password string) string { + hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) + if err != nil { + panic(err) + } + + return string(hash) +} + +// ComparePassword compares the hash +func ComparePassword(hash string, password string) bool { + + if len(password) == 0 { + return false + } + + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +func IsUsernameValid(username string) bool { + + var restrictedUsernames = []string { + BOT_USERNAME, + "all", + "channel", + } + + for _,restrictedUsername := range restrictedUsernames { + if username == restrictedUsername { + return false + } + } + + return true +} diff --git a/model/user_test.go b/model/user_test.go new file mode 100644 index 000000000..df9ac19c2 --- /dev/null +++ b/model/user_test.go @@ -0,0 +1,92 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestPasswordHash(t *testing.T) { + hash := HashPassword("Test") + + if !ComparePassword(hash, "Test") { + t.Fatal("Passwords don't match") + } + + if ComparePassword(hash, "Test2") { + t.Fatal("Passwords should not have matched") + } +} + +func TestUserJson(t *testing.T) { + user := User{Id: NewId(), Username: NewId()} + json := user.ToJson() + ruser := UserFromJson(strings.NewReader(json)) + + if user.Id != ruser.Id { + t.Fatal("Ids do not match") + } +} + +func TestUserPreSave(t *testing.T) { + user := User{Password: "test"} + user.PreSave() + user.Etag() +} + +func TestUserPreUpdate(t *testing.T) { + user := User{Password: "test"} + user.PreUpdate() +} + +func TestUserIsValid(t *testing.T) { + user := User{} + + if err := user.IsValid(); err == nil { + t.Fatal() + } + + user.Id = NewId() + if err := user.IsValid(); err == nil { + t.Fatal() + } + + user.CreateAt = GetMillis() + if err := user.IsValid(); err == nil { + t.Fatal() + } + + user.UpdateAt = GetMillis() + if err := user.IsValid(); err == nil { + t.Fatal() + } + + user.TeamId = NewId() + if err := user.IsValid(); err == nil { + t.Fatal() + } + + user.Username = NewId() + "^hello#" + if err := user.IsValid(); err == nil { + t.Fatal() + } + + user.Username = NewId() + user.Email = strings.Repeat("01234567890", 20) + if err := user.IsValid(); err == nil { + t.Fatal() + } + + user.Email = "test@nowhere.com" + user.FullName = strings.Repeat("01234567890", 20) + if err := user.IsValid(); err == nil { + t.Fatal() + } + + user.FullName = "" + if err := user.IsValid(); err != nil { + t.Fatal(err) + } +} diff --git a/model/utils.go b/model/utils.go new file mode 100644 index 000000000..8b597000a --- /dev/null +++ b/model/utils.go @@ -0,0 +1,320 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "bytes" + "code.google.com/p/go-uuid/uuid" + "encoding/base32" + "encoding/json" + "fmt" + "io" + "net/mail" + "regexp" + "strings" + "time" +) + +const ( + ETAG_ROOT_VERSION = "10" +) + +type StringMap map[string]string +type StringArray []string +type EncryptStringMap map[string]string + +// AppError is returned for any http response that's not in the 200 range. +type AppError struct { + Message string `json:"message"` // Message to be display to the end user without debugging information + DetailedError string `json:"detailed_error"` // Internal error string to help the developer + RequestId string `json:"request_id"` // The RequestId that's also set in the header + StatusCode int `json:"status_code"` // The http status code + Where string `json:"-"` // The function where it happened in the form of Struct.Func +} + +func (er *AppError) Error() string { + return er.Where + ": " + er.Message + ", " + er.DetailedError +} + +func (er *AppError) ToJson() string { + b, err := json.Marshal(er) + if err != nil { + return "" + } else { + return string(b) + } +} + +// AppErrorFromJson will decode the input and return an AppError +func AppErrorFromJson(data io.Reader) *AppError { + decoder := json.NewDecoder(data) + var er AppError + err := decoder.Decode(&er) + if err == nil { + return &er + } else { + return nil + } +} + +func NewAppError(where string, message string, details string) *AppError { + ap := &AppError{} + ap.Message = message + ap.Where = where + ap.DetailedError = details + ap.StatusCode = 500 + return ap +} + +var encoding = base32.NewEncoding("ybndrfg8ejkmcpqxot1uwisza345h769") + +// NewId is a globally unique identifier. It is a [A-Z0-9] string 26 +// characters long. It is a UUID version 4 Guid that is zbased32 encoded +// with the padding stripped off. +func NewId() string { + var b bytes.Buffer + encoder := base32.NewEncoder(encoding, &b) + encoder.Write(uuid.NewRandom()) + encoder.Close() + b.Truncate(26) // removes the '==' padding + return b.String() +} + +// GetMillis is a convience method to get milliseconds since epoch. +func GetMillis() int64 { + return time.Now().UnixNano() / int64(time.Millisecond) +} + +// MapToJson converts a map to a json string +func MapToJson(objmap map[string]string) string { + if b, err := json.Marshal(objmap); err != nil { + return "" + } else { + return string(b) + } +} + +// MapFromJson will decode the key/value pair map +func MapFromJson(data io.Reader) map[string]string { + decoder := json.NewDecoder(data) + + var objmap map[string]string + if err := decoder.Decode(&objmap); err != nil { + return make(map[string]string) + } else { + return objmap + } +} + +func ArrayToJson(objmap []string) string { + if b, err := json.Marshal(objmap); err != nil { + return "" + } else { + return string(b) + } +} + +func ArrayFromJson(data io.Reader) []string { + decoder := json.NewDecoder(data) + + var objmap []string + if err := decoder.Decode(&objmap); err != nil { + return make([]string, 0) + } else { + return objmap + } +} + +func IsLower(s string) bool { + if strings.ToLower(s) == s { + return true + } + + return false +} + +func IsValidEmail(email string) bool { + + if !IsLower(email) { + return false + } + + if _, err := mail.ParseAddress(email); err == nil { + return true + } + + return false +} + +var reservedDomains = []string{ + "www", + "web", + "admin", + "support", + "notify", + "test", + "demo", + "mail", + "team", + "channel", + "internal", + "localhost", + "stag", + "post", + "cluster", + "api", +} + +func IsReservedDomain(s string) bool { + s = strings.ToLower(s) + + for _, value := range reservedDomains { + if strings.Index(s, value) == 0 { + return true + } + } + + return false +} + +func IsValidDomain(s string) bool { + + if !IsValidAlphaNum(s) { + return false + } + + if len(s) <= 3 { + return false + } + + return true +} + +var wwwStart = regexp.MustCompile(`^www`) +var betaStart = regexp.MustCompile(`^beta`) +var ciStart = regexp.MustCompile(`^ci`) + +func GetSubDomain(s string) (string, string) { + s = strings.Replace(s, "http://", "", 1) + s = strings.Replace(s, "https://", "", 1) + + match := wwwStart.MatchString(s) + if match { + return "", "" + } + + match = betaStart.MatchString(s) + if match { + return "", "" + } + + match = ciStart.MatchString(s) + if match { + return "", "" + } + + parts := strings.Split(s, ".") + + if len(parts) != 3 { + return "", "" + } + + return parts[0], parts[1] +} + +func IsValidChannelIdentifier(s string) bool { + + if !IsValidAlphaNum(s) { + return false + } + + if len(s) < 2 { + return false + } + + return true +} + +var validAlphaNum = regexp.MustCompile(`^[a-z0-9]+([a-z\-0-9]+|(__)?)[a-z0-9]+$`) + +func IsValidAlphaNum(s string) bool { + match := validAlphaNum.MatchString(s) + + if !match { + return false + } + + return true +} + +func Etag(parts ...interface{}) string { + + etag := ETAG_ROOT_VERSION + + for _, part := range parts { + etag += fmt.Sprintf(".%v", part) + } + + return etag +} + +var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_]*[A-Za-z0-9])$`) +var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`) +var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`) + +func ParseHashtags(text string) (string, string) { + words := strings.Split(strings.Replace(text, "\n", " ", -1), " ") + + hashtagString := "" + plainString := "" + for _, word := range words { + word = puncStart.ReplaceAllString(word, "") + word = puncEnd.ReplaceAllString(word, "") + if validHashtag.MatchString(word) { + hashtagString += " " + word + } else { + plainString += " " + word + } + } + + if len(hashtagString) > 1000 { + hashtagString = hashtagString[:999] + lastSpace := strings.LastIndex(hashtagString, " ") + if lastSpace > -1 { + hashtagString = hashtagString[:lastSpace] + } else { + hashtagString = "" + } + } + + return strings.TrimSpace(hashtagString), strings.TrimSpace(plainString) +} + +func IsFileExtImage(ext string) bool { + ext = strings.ToLower(ext) + for _, imgExt := range IMAGE_EXTENSIONS { + if ext == imgExt { + return true + } + } + return false +} + +func GetImageMimeType(ext string) string { + ext = strings.ToLower(ext) + if len(IMAGE_MIME_TYPES[ext]) == 0 { + return "image" + } else { + return IMAGE_MIME_TYPES[ext] + } +} + +func ClearMentionTags(post string) string { + post = strings.Replace(post, "", "", -1) + post = strings.Replace(post, "", "", -1) + return post +} + +var UrlRegex = regexp.MustCompile(`^((?:[a-z]+:\/\/)?(?:(?:[a-z0-9\-]+\.)+(?:[a-z]{2}|aero|arpa|biz|com|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|local|internal))(:[0-9]{1,5})?(?:\/[a-z0-9_\-\.~]+)*(\/([a-z0-9_\-\.]*)(?:\?[a-z0-9+_~\-\.%=&]*)?)?(?:#[a-zA-Z0-9!$&'()*+.=-_~:@/?]*)?)(?:\s+|$)$`) +var PartialUrlRegex = regexp.MustCompile(`/api/v1/files/(get|get_image)/([A-Za-z0-9]{26})/([A-Za-z0-9]{26})/(([A-Za-z0-9]+/)?.+\.[A-Za-z0-9]{3,})`) diff --git a/model/utils_test.go b/model/utils_test.go new file mode 100644 index 000000000..a9721042d --- /dev/null +++ b/model/utils_test.go @@ -0,0 +1,118 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestNewId(t *testing.T) { + for i := 0; i < 1000; i++ { + id := NewId() + if len(id) > 26 { + t.Fatal("ids shouldn't be longer than 26 chars") + } + } +} + +func TestAppError(t *testing.T) { + err := NewAppError("TestAppError", "message", "") + json := err.ToJson() + rerr := AppErrorFromJson(strings.NewReader(json)) + if err.Message != rerr.Message { + t.Fatal() + } + + err.Error() +} + +func TestMapJson(t *testing.T) { + + m := make(map[string]string) + m["id"] = "test_id" + json := MapToJson(m) + + rm := MapFromJson(strings.NewReader(json)) + + if rm["id"] != "test_id" { + t.Fatal("map should be valid") + } + + rm2 := MapFromJson(strings.NewReader("")) + if len(rm2) > 0 { + t.Fatal("make should be ivalid") + } +} + +func TestValidEmail(t *testing.T) { + if !IsValidEmail("corey@hulen.com") { + t.Error("email should be valid") + } + + if IsValidEmail("@corey@hulen.com") { + t.Error("should be invalid") + } +} + +func TestValidLower(t *testing.T) { + if !IsLower("corey@hulen.com") { + t.Error("should be valid") + } + + if IsLower("Corey@hulen.com") { + t.Error("should be invalid") + } +} + +var domains = []struct { + value string + expected bool +}{ + {"spin-punch", true}, + {"-spin-punch", false}, + {"spin-punch-", false}, + {"spin_punch", false}, + {"a", false}, + {"aa", false}, + {"aaa", false}, + {"aaa-999b", true}, + {"b00b", true}, + {"b))b", false}, + {"test", true}, +} + +func TestValidDomain(t *testing.T) { + for _, v := range domains { + if IsValidDomain(v.value) != v.expected { + t.Errorf("expect %v as %v", v.value, v.expected) + } + } +} + +var tReservedDomains = []struct { + value string + expected bool +}{ + {"test-hello", true}, + {"test", true}, + {"admin", true}, + {"Admin-punch", true}, + {"spin-punch-admin", false}, +} + +func TestReservedDomain(t *testing.T) { + for _, v := range tReservedDomains { + if IsReservedDomain(v.value) != v.expected { + t.Errorf("expect %v as %v", v.value, v.expected) + } + } +} + +func TestEtag(t *testing.T) { + etag := Etag("hello", 24) + if len(etag) <= 0 { + t.Fatal() + } +} -- cgit v1.2.3-1-g7c22