├── vendor ├── .gitignore └── manifest ├── example ├── chaosmonkey │ ├── README.md │ └── main.go └── rtm │ └── main.go ├── .travis.yml ├── openapi ├── vchannel.go ├── doc.go ├── meta.go ├── rtm.go ├── emoji.go ├── sticker.go ├── team.go ├── client_test.go ├── p2p.go ├── message_query.go ├── user.go ├── message_pin.go ├── session_channel.go ├── client.go ├── channel.go └── message.go ├── CHANGELOG.md ├── rtm_user.go ├── rtm_channel.go ├── README.md ├── rtm_loop.go ├── rtm_current_team.go ├── rtm_context.go ├── LICENSE ├── incoming_test.go ├── rtm_client_test.go ├── rtm_loop_impl_test.go ├── webhook_client_test.go ├── model.go ├── webhook_client.go ├── incoming.go ├── rtm_message.go ├── rtm_loop_impl.go ├── rtm_client.go └── rtm_message_test.go /vendor/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !manifest 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /example/chaosmonkey/README.md: -------------------------------------------------------------------------------- 1 | # Chaos Monkey 2 | 3 | ## Usage 4 | 5 | ``` 6 | $ CM_RTM_TOKEN=xxx CM_VICTIMS=xxx,yyy go run main.go 7 | ``` 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | - 1.8 6 | - 1.7 7 | 8 | notifications: 9 | webhooks: https://hook.bearychat.com/=bw9bk/travis/99011000053f89bfdc98f8a3e08c4167 10 | -------------------------------------------------------------------------------- /openapi/vchannel.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // VChannelType defines chat channel/inbox types. 4 | type VChannelType string 5 | 6 | // VChannelTS represents unix timestamp type. 7 | type VChannelTS int64 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ---- 2 | - Name: bearychat.go 3 | ---- 4 | 5 | # 1.1.0 / 2017-06-02 6 | 7 | ## Added 8 | 9 | - Updated to [OpenAPI@3f86e39426](https://github.com/bearyinnovative/OpenAPI/commit/3f86e394269c6bf82f508307aac088e1bc1d8bb3) 10 | 11 | # 1.0.0 / 2017-03-31 12 | 13 | ## Added 14 | 15 | - Added OpenAPI package 16 | 17 | # 0.2.0 / 2017-01-16 18 | 19 | ## Added 20 | 21 | - Initial version 22 | -------------------------------------------------------------------------------- /rtm_user.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import "fmt" 4 | 5 | type RTMUserService struct { 6 | rtm *RTMClient 7 | } 8 | 9 | func newRTMUserService(rtm *RTMClient) error { 10 | rtm.User = &RTMUserService{rtm} 11 | return nil 12 | } 13 | 14 | func (s *RTMUserService) Info(userId string) (*User, error) { 15 | user := new(User) 16 | resource := fmt.Sprintf("v1/user.info?user_id=%s", userId) 17 | _, err := s.rtm.Get(resource, user) 18 | return user, err 19 | } 20 | -------------------------------------------------------------------------------- /rtm_channel.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import "fmt" 4 | 5 | type RTMChannelService struct { 6 | rtm *RTMClient 7 | } 8 | 9 | func newRTMChannelService(rtm *RTMClient) error { 10 | rtm.Channel = &RTMChannelService{rtm} 11 | return nil 12 | } 13 | 14 | func (s *RTMChannelService) Info(channelId string) (*Channel, error) { 15 | channel := new(Channel) 16 | resource := fmt.Sprintf("v1/channel.info?channel_id=%s", channelId) 17 | _, err := s.rtm.Get(resource, channel) 18 | return channel, err 19 | } 20 | -------------------------------------------------------------------------------- /openapi/doc.go: -------------------------------------------------------------------------------- 1 | // Package openapi implements BearyChat's OpenAPI methods. 2 | // 3 | // For full api doc, please visit https://github.com/bearyinnovative/OpenAPI 4 | // 5 | // Usage: 6 | // 7 | // import "github.com/bearyinnovative/bearychat-go/openapi" 8 | // 9 | // API methods are bound to a API client. To construct a new client: 10 | // 11 | // client := openapi.NewClient(token) 12 | // 13 | // API methods are grouped by "namespace": 14 | // 15 | // team, _, err := client.Team.Info() 16 | package openapi 17 | -------------------------------------------------------------------------------- /openapi/meta.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type Meta struct { 9 | Version *string `json:"version,omitempty"` 10 | } 11 | 12 | type MetaService service 13 | 14 | // Get implements `GET /meta` 15 | func (m *MetaService) Get(ctx context.Context) (*Meta, *http.Response, error) { 16 | req, err := m.client.newRequest("GET", "meta", nil) 17 | if err != nil { 18 | return nil, nil, err 19 | } 20 | 21 | var meta Meta 22 | resp, err := m.client.do(ctx, req, &meta) 23 | if err != nil { 24 | return nil, resp, err 25 | } 26 | return &meta, resp, nil 27 | } 28 | -------------------------------------------------------------------------------- /openapi/rtm.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type RTMStart struct { 9 | WebSocketHost *string `json:"ws_host,omitempty"` 10 | User *User `json:"user,omitempty"` 11 | } 12 | 13 | type RTMService service 14 | 15 | // Start implements `POST /rtm.start` 16 | func (r *RTMService) Start(ctx context.Context) (*RTMStart, *http.Response, error) { 17 | req, err := r.client.newRequest("POST", "rtm.start", nil) 18 | if err != nil { 19 | return nil, nil, err 20 | } 21 | 22 | var start RTMStart 23 | resp, err := r.client.do(ctx, req, &start) 24 | if err != nil { 25 | return nil, resp, err 26 | } 27 | return &start, resp, nil 28 | } 29 | -------------------------------------------------------------------------------- /vendor/manifest: -------------------------------------------------------------------------------- 1 | { 2 | "version": 0, 3 | "dependencies": [ 4 | { 5 | "importpath": "github.com/gorilla/websocket", 6 | "repository": "https://github.com/gorilla/websocket", 7 | "vcs": "git", 8 | "revision": "2d1e4548da234d9cb742cc3628556fef86aafbac", 9 | "branch": "master", 10 | "notests": true 11 | }, 12 | { 13 | "importpath": "github.com/pkg/errors", 14 | "repository": "https://github.com/pkg/errors", 15 | "vcs": "git", 16 | "revision": "17b591df37844cde689f4d5813e5cea0927d8dd2", 17 | "branch": "master", 18 | "notests": true 19 | }, 20 | { 21 | "importpath": "golang.org/x/net/context", 22 | "repository": "https://go.googlesource.com/net", 23 | "vcs": "git", 24 | "revision": "6b27048ae5e6ad1ef927e72e437531493de612fe", 25 | "branch": "master", 26 | "path": "/context", 27 | "notests": true 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bearychat-go 2 | 3 | [BearyChat][bearychat] API with go. 4 | 5 | [![Build Status](https://travis-ci.org/bearyinnovative/bearychat-go.svg)](https://travis-ci.org/bearyinnovative/bearychat-go) 6 | [![GoDoc](https://godoc.org/github.com/bearyinnovative/bearychat-go?status.svg)](https://godoc.org/github.com/bearyinnovative/bearychat-go) 7 | [![GoReport](https://goreportcard.com/badge/github.com/bearyinnovative/bearychat-go)](https://goreportcard.com/report/github.com/bearyinnovative/bearychat-go) 8 | ![Development Status](https://img.shields.io/badge/status-1.0.0-brightgreen.svg?style=flat-square) 9 | 10 | [bearychat]: https://bearychat.com 11 | 12 | ## Usage 13 | 14 | go 1.7+ 15 | 16 | ``` 17 | go get github.com/bearyinnovative/bearychat-go 18 | ``` 19 | 20 | ## Examples 21 | 22 | - [RTM](example/rtm/main.go) 23 | - [Chaos Monkey](example/chaosmonkey/main.go) 24 | 25 | ## LICENSE 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /openapi/emoji.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type Emoji struct { 9 | ID *string `json:"id,omitempty"` 10 | UserID *string `json:"uid,omitempty"` 11 | TeamID *string `json:"team_id,omitempty"` 12 | Name *string `json:"name,omitempty"` 13 | URL *string `json:"url,omitempty"` 14 | Created *Time `json:"created,omitempty"` 15 | Updated *Time `json:"updated,omitempty"` 16 | } 17 | 18 | type EmojiService service 19 | 20 | // List implements `GET /emoji.list` 21 | func (e *EmojiService) List(ctx context.Context) ([]*Emoji, *http.Response, error) { 22 | req, err := e.client.newRequest("GET", "emoji.list", nil) 23 | if err != nil { 24 | return nil, nil, err 25 | } 26 | 27 | var emojis []*Emoji 28 | resp, err := e.client.do(ctx, req, &emojis) 29 | if err != nil { 30 | return nil, resp, err 31 | } 32 | return emojis, resp, nil 33 | } 34 | -------------------------------------------------------------------------------- /openapi/sticker.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type Sticker struct { 9 | URL *string `json:"url,omitempty"` 10 | Name *string `json:"name,omitempty"` 11 | Width *int `json:"width,omitempty"` 12 | Height *int `json:"height,omitempty"` 13 | } 14 | 15 | type StickerPack struct { 16 | PackName *string `json:"pack,omitempty"` 17 | Stickers []*Sticker `json:"stickers,omitempty"` 18 | } 19 | 20 | type StickerService service 21 | 22 | // List implements `GET /sticker.list` 23 | func (s *StickerService) List(ctx context.Context) ([]*StickerPack, *http.Response, error) { 24 | req, err := s.client.newRequest("GET", "sticker.list", nil) 25 | if err != nil { 26 | return nil, nil, err 27 | } 28 | 29 | var stickers []*StickerPack 30 | resp, err := s.client.do(ctx, req, &stickers) 31 | if err != nil { 32 | return nil, resp, err 33 | } 34 | return stickers, resp, nil 35 | } 36 | -------------------------------------------------------------------------------- /rtm_loop.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | type RTMLoopState string 9 | 10 | const ( 11 | RTMLoopStateClosed RTMLoopState = "closed" 12 | RTMLoopStateOpen = "open" 13 | ) 14 | 15 | var ( 16 | ErrRTMLoopClosed = errors.New("rtm loop is closed") 17 | ) 18 | 19 | // RTMLoop is used to interactive with BearyChat's RTM websocket message protocol. 20 | type RTMLoop interface { 21 | // Connect to RTM, returns after connected 22 | Start() error 23 | // Stop the connection 24 | Stop() error 25 | // Get current state 26 | State() RTMLoopState 27 | // Send a ping message 28 | Ping() error 29 | // Keep connection alive. Closes ticker before return 30 | Keepalive(interval *time.Ticker) error 31 | // Send a message 32 | Send(m RTMMessage) error 33 | // Get message receiving channel 34 | ReadC() (chan RTMMessage, error) 35 | // Get error channel 36 | ErrC() chan error 37 | } 38 | -------------------------------------------------------------------------------- /rtm_current_team.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | type RTMCurrentTeamService struct { 4 | rtm *RTMClient 5 | } 6 | 7 | func newRTMCurrentTeamService(rtm *RTMClient) error { 8 | rtm.CurrentTeam = &RTMCurrentTeamService{rtm} 9 | 10 | return nil 11 | } 12 | 13 | // Retrieves current team's information. 14 | func (s *RTMCurrentTeamService) Info() (*Team, error) { 15 | team := new(Team) 16 | _, err := s.rtm.Get("v1/current_team.info", team) 17 | return team, err 18 | } 19 | 20 | // Retrieves current team's members. 21 | func (s *RTMCurrentTeamService) Members() ([]*User, error) { 22 | members := []*User{} 23 | _, err := s.rtm.Get("v1/current_team.members?all=true", &members) 24 | return members, err 25 | } 26 | 27 | // Retrieves current team's channels. 28 | func (s *RTMCurrentTeamService) Channels() ([]*Channel, error) { 29 | channels := []*Channel{} 30 | _, err := s.rtm.Get("v1/current_team.channels", &channels) 31 | return channels, err 32 | } 33 | -------------------------------------------------------------------------------- /openapi/team.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type TeamPlan string 9 | 10 | type Team struct { 11 | ID *string `json:"id,omitempty"` 12 | Subdomain *string `json:"subdomain,omitempty"` 13 | Name *string `json:"name,omitempty"` 14 | EmailDomain *string `json:"email_domain,omitempty"` 15 | LogoURL *string `json:"logo_url,omitempty"` 16 | Description *string `json:"description,omitempty"` 17 | Plan *TeamPlan `json:"plan,omitempty"` 18 | Created *Time `json:"created,omitempty"` 19 | } 20 | 21 | type TeamService service 22 | 23 | // Info implements `GET /team.info` 24 | func (t *TeamService) Info(ctx context.Context) (*Team, *http.Response, error) { 25 | req, err := t.client.newRequest("GET", "team.info", nil) 26 | if err != nil { 27 | return nil, nil, err 28 | } 29 | 30 | var team Team 31 | resp, err := t.client.do(ctx, req, &team) 32 | if err != nil { 33 | return nil, resp, err 34 | } 35 | return &team, resp, nil 36 | } 37 | -------------------------------------------------------------------------------- /rtm_context.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import "time" 4 | 5 | type RTMContext struct { 6 | Loop RTMLoop 7 | 8 | uid string 9 | } 10 | 11 | func (c *RTMContext) UID() string { 12 | return c.uid 13 | } 14 | 15 | func NewRTMContext(token string) (*RTMContext, error) { 16 | rtmClient, err := NewRTMClient(token) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | user, wsHost, err := rtmClient.Start() 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | rtmLoop, err := NewRTMLoop(wsHost) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &RTMContext{ 32 | Loop: rtmLoop, 33 | uid: user.Id, 34 | }, nil 35 | } 36 | 37 | func (c *RTMContext) Run() (error, chan RTMMessage, chan error) { 38 | err := c.Loop.Start() 39 | if err != nil { 40 | return err, nil, nil 41 | } 42 | defer c.Loop.Stop() 43 | 44 | go c.Loop.Keepalive(time.NewTicker(10 * time.Second)) 45 | 46 | errC := c.Loop.ErrC() 47 | messageC, err := c.Loop.ReadC() 48 | if err != nil { 49 | return err, nil, nil 50 | } 51 | 52 | return nil, messageC, errC 53 | } 54 | -------------------------------------------------------------------------------- /openapi/client_test.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | ) 8 | 9 | func TestNewClient(t *testing.T) { 10 | token := "foobar" 11 | client := NewClient(token) 12 | 13 | if client.httpClient != http.DefaultClient { 14 | t.Errorf("should use http.DefaultClient by default") 15 | } 16 | if client.BaseURL.String() != defaultBaseURL { 17 | t.Errorf("should use defaultBaseURL by default") 18 | } 19 | if client.Token != token { 20 | t.Errorf("unexpected token: %s", client.Token) 21 | } 22 | } 23 | 24 | func TestNewClient_NewClientWithBaseURL(t *testing.T) { 25 | u, _ := url.Parse("http://foobar/") 26 | client := NewClient("foobar", NewClientWithBaseURL(u)) 27 | 28 | if client.BaseURL != u { 29 | t.Errorf("unexpected BaseURL: %s", client.BaseURL) 30 | } 31 | } 32 | 33 | func TestNewClient_NewClientWithHTTPClient(t *testing.T) { 34 | httpClient := &http.Client{} 35 | client := NewClient("foobar", NewClientWithHTTPClient(httpClient)) 36 | 37 | if client.httpClient != httpClient { 38 | t.Errorf("unexpected httpClient: %+v", client.httpClient) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 hbc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /incoming_test.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import "testing" 4 | 5 | func TestValidateIncoming(t *testing.T) { 6 | var m Incoming 7 | 8 | m = Incoming{} 9 | if err := m.Validate(); err == nil { 10 | t.Errorf("text should not be empty: %+v", err) 11 | } 12 | 13 | m = Incoming{ 14 | Text: "test", 15 | Attachments: []IncomingAttachment{{}}, 16 | } 17 | if err := m.Validate(); err == nil { 18 | t.Errorf("title or text should not be empty: %+v", err) 19 | } 20 | 21 | m = Incoming{ 22 | Text: "test", 23 | Attachments: []IncomingAttachment{ 24 | { 25 | Text: "test", 26 | Images: []IncomingAttachmentImage{{}}, 27 | }, 28 | }, 29 | } 30 | if err := m.Validate(); err == nil { 31 | t.Errorf("image url should not be empty: %+v", err) 32 | } 33 | } 34 | 35 | func ExampleIncoming() { 36 | m := Incoming{ 37 | Text: "Hello, **BearyChat", 38 | Notification: "Hello, BearyChat in notification", 39 | Markdown: true, 40 | Channel: "#所有人", 41 | User: "@bearybot", 42 | Attachments: []IncomingAttachment{ 43 | {Text: "attachment 1", Color: "#cb3f20"}, 44 | {Title: "attachment 2", Color: "#ffa500"}, 45 | { 46 | Text: "愿原力与你同在", 47 | Images: []IncomingAttachmentImage{ 48 | {URL: "http://img3.douban.com/icon/ul15067564-30.jpg"}, 49 | }, 50 | }, 51 | }, 52 | } 53 | 54 | m.Build() 55 | } 56 | -------------------------------------------------------------------------------- /example/rtm/main.go: -------------------------------------------------------------------------------- 1 | // Demo bot built with BearyChat RTM 2 | // 3 | // ./rtm -rtmToken 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "log" 9 | 10 | bearychat "github.com/bearyinnovative/bearychat-go" 11 | ) 12 | 13 | var rtmToken string 14 | 15 | func init() { 16 | flag.StringVar(&rtmToken, "rtmToken", "", "BearyChat RTM token") 17 | } 18 | 19 | func main() { 20 | flag.Parse() 21 | 22 | context, err := bearychat.NewRTMContext(rtmToken) 23 | if err != nil { 24 | log.Fatal(err) 25 | return 26 | } 27 | 28 | err, messageC, errC := context.Run() 29 | if err != nil { 30 | log.Fatal(err) 31 | return 32 | } 33 | 34 | for { 35 | select { 36 | case err := <-errC: 37 | log.Printf("rtm loop error: %+v", err) 38 | if err := context.Loop.Stop(); err != nil { 39 | log.Fatal(err) 40 | } 41 | return 42 | case message := <-messageC: 43 | if !message.IsChatMessage() { 44 | continue 45 | } 46 | 47 | // from self 48 | if message.IsFromUID(context.UID()) { 49 | continue 50 | } 51 | 52 | log.Printf( 53 | "received: %s from %s", 54 | message["text"], 55 | message["uid"], 56 | ) 57 | 58 | // only reply mentioned myself 59 | if mentioned, content := message.ParseMentionUID(context.UID()); mentioned { 60 | if err := context.Loop.Send(message.Refer(content)); err != nil { 61 | log.Fatal(err) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rtm_client_test.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | testRTMToken = "foobar" 10 | ) 11 | 12 | func TestNewRTMClient(t *testing.T) { 13 | c, err := NewRTMClient(testRTMToken) 14 | if err != nil { 15 | t.Errorf("unexpected error: %+v", err) 16 | } 17 | 18 | if c.Token != testRTMToken { 19 | t.Errorf("unexpected token: %s", c.Token) 20 | } 21 | 22 | if c.APIBase != DEFAULT_RTM_API_BASE { 23 | t.Errorf("should use default rtm api base: %s", c.APIBase) 24 | } 25 | 26 | if c.CurrentTeam == nil { 27 | t.Errorf("should create current team service") 28 | } 29 | if c.User == nil { 30 | t.Errorf("should create user service") 31 | } 32 | } 33 | 34 | func TestNewRTMClient_error(t *testing.T) { 35 | newError := errors.New("error") 36 | _, err := NewRTMClient(testRTMToken, func(c *RTMClient) error { 37 | return newError 38 | }) 39 | if err != newError { 40 | t.Errorf("should return error: %+v", err) 41 | } 42 | } 43 | 44 | func TestNewRTMClient_WithRTMAPIBase(t *testing.T) { 45 | apiBase := "http://foo.bar" 46 | c, err := NewRTMClient(testRTMToken, WithRTMAPIBase(apiBase)) 47 | if err != nil { 48 | t.Errorf("unexpected error: %+v", err) 49 | } 50 | 51 | if c.APIBase != apiBase { 52 | t.Errorf("should set api base: %s", c.APIBase) 53 | } 54 | } 55 | 56 | func TestNewRTMClient_WithRTMHTTPClient(t *testing.T) { 57 | c, err := NewRTMClient(testRTMToken, WithRTMHTTPClient(nil)) 58 | if err != nil { 59 | t.Errorf("unexpected error: %+v", err) 60 | } 61 | 62 | if c.httpClient != nil { 63 | t.Errorf("should set http client: %+v", c.httpClient) 64 | } 65 | } 66 | 67 | func testAddTokenToResourceUri(t *testing.T) { 68 | u, err := addTokenToResourceUri("http://foobar.com", "foobar") 69 | if err != nil { 70 | t.Errorf("unexpected error: %+v", err) 71 | } 72 | if u != "http://foobar.com?token=foobar" { 73 | t.Errorf("unexpected resource uri: %s", u) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /rtm_loop_impl_test.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | testRTMWSHost = "foobar" 10 | ) 11 | 12 | func TestNewRTMLoop(t *testing.T) { 13 | l, err := NewRTMLoop(testRTMWSHost) 14 | if err != nil { 15 | t.Errorf("unexpected error: %+v", err) 16 | } 17 | if l.wsHost != testRTMWSHost { 18 | t.Errorf("unexpected wsHost: %s", l.wsHost) 19 | } 20 | if l.callId != 0 { 21 | t.Errorf("unexpected call id: %d", l.callId) 22 | } 23 | } 24 | 25 | func TestRTMLoop_ReadC_Closed(t *testing.T) { 26 | l, err := NewRTMLoop(testRTMWSHost) 27 | if err != nil { 28 | t.Errorf("unexpected error: %+v", err) 29 | } 30 | 31 | if _, err := l.ReadC(); err != ErrRTMLoopClosed { 32 | t.Errorf("unexpected error: %+v", err) 33 | } 34 | } 35 | 36 | func TestRTMLoop_ErrC_Closed(t *testing.T) { 37 | l, err := NewRTMLoop(testRTMWSHost) 38 | if err != nil { 39 | t.Errorf("unexpected error: %+v", err) 40 | } 41 | 42 | if c := l.ErrC(); c == nil { 43 | t.Errorf("expected error channel") 44 | } 45 | } 46 | 47 | func TestRTMLoop_advanceCallId(t *testing.T) { 48 | l, err := NewRTMLoop(testRTMWSHost) 49 | if err != nil { 50 | t.Errorf("unexpected error: %+v", err) 51 | } 52 | 53 | for i := uint64(1); i < 11; i = i + 1 { 54 | newCallId := l.advanceCallId() 55 | if l.callId != i || newCallId != l.callId { 56 | t.Errorf( 57 | "unexpected callId: %d, %d, %d", 58 | i, 59 | newCallId, 60 | l.callId, 61 | ) 62 | } 63 | } 64 | } 65 | 66 | func TestRTMLoop_advanceCallId_Race(t *testing.T) { 67 | l, err := NewRTMLoop(testRTMWSHost) 68 | if err != nil { 69 | t.Errorf("unexpected error: %+v", err) 70 | } 71 | 72 | per := 15 73 | times := 10 74 | 75 | var wg sync.WaitGroup 76 | advance := func() { 77 | for i := 0; i < per; i = i + 1 { 78 | l.advanceCallId() 79 | } 80 | wg.Done() 81 | } 82 | 83 | for i := 0; i < times; i = i + 1 { 84 | wg.Add(1) 85 | go advance() 86 | } 87 | 88 | wg.Wait() 89 | if l.callId != uint64(per*times) { 90 | t.Errorf("unexepcted call id after data race: %d", l.callId) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /webhook_client_test.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | testWebhook = "http://localhost:3927" 10 | ) 11 | 12 | func TestWebhookResponse_IsOk(t *testing.T) { 13 | var resp WebhookResponse 14 | 15 | resp = WebhookResponse{Code: 0} 16 | if !resp.IsOk() { 17 | t.Errorf("response should be ok when code is 0") 18 | } 19 | 20 | resp = WebhookResponse{Code: 1} 21 | if resp.IsOk() { 22 | t.Errorf("response should not be ok when code is not 0") 23 | } 24 | } 25 | 26 | func TestIncomingWebhookClient_SetWebhook(t *testing.T) { 27 | h := NewIncomingWebhookClient("") 28 | if h.SetWebhook(testWebhook) == nil { 29 | t.Errorf("should return webhook client") 30 | } 31 | 32 | if h.Webhook != testWebhook { 33 | t.Errorf("should set webhook") 34 | } 35 | } 36 | 37 | func TestIncomingWebhookClient_SetHTTPClient(t *testing.T) { 38 | h := NewIncomingWebhookClient(testWebhook) 39 | 40 | if h.httpClient != http.DefaultClient { 41 | t.Errorf("should use `http.DefaultClient` by default") 42 | } 43 | 44 | testHTTPClient := &http.Client{} 45 | if h.SetHTTPClient(testHTTPClient) == nil { 46 | t.Errorf("should return webhook client") 47 | } 48 | 49 | if h.httpClient != testHTTPClient { 50 | t.Errorf("should set http client") 51 | } 52 | } 53 | 54 | func TestIncomingWebhookClient_Send_WithoutWebhook(t *testing.T) { 55 | h := NewIncomingWebhookClient("") 56 | _, err := h.Send(nil) 57 | if err == nil { 58 | t.Errorf("should not send when webhook is not set") 59 | } 60 | } 61 | 62 | func TestIncomingWebhookClient_Send_WithoutHTTPClient(t *testing.T) { 63 | h := NewIncomingWebhookClient(testWebhook) 64 | h.SetHTTPClient(nil) 65 | _, err := h.Send(nil) 66 | if err == nil { 67 | t.Errorf("should not send when http client is not set") 68 | } 69 | } 70 | 71 | func ExampleNewIncomingWebhookClient() { 72 | m := Incoming{Text: "Hello, BearyChat"} 73 | payload, _ := m.Build() 74 | resp, _ := NewIncomingWebhookClient("YOUR WEBHOOK URL").Send(payload) 75 | if resp.IsOk() { 76 | // parse resp result 77 | } else { 78 | // parse resp error 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /openapi/p2p.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type P2P struct { 10 | ID *string `json:"id,omitempty"` 11 | TeamID *string `json:"team_id,omitempty"` 12 | VChannelID *string `json:"vchannel_id,omitempty"` 13 | Type *VChannelType `json:"type,omitempty"` 14 | IsActive *bool `json:"is_active"` 15 | IsMember *bool `json:"is_member,omitempty"` 16 | MemberUserIDs []string `json:"member_uids,omitempty"` 17 | LatestTS *VChannelTS `json:"latest_ts,omitempty"` 18 | } 19 | 20 | type P2PService service 21 | 22 | type P2PInfoOptions struct { 23 | ChannelID string 24 | } 25 | 26 | // Info implements `GET /p2p.info` 27 | func (p *P2PService) Info(ctx context.Context, opt *P2PInfoOptions) (*P2P, *http.Response, error) { 28 | endpoint := fmt.Sprintf("p2p.info?p2p_channel_id=%s", opt.ChannelID) 29 | req, err := p.client.newRequest("GET", endpoint, nil) 30 | if err != nil { 31 | return nil, nil, err 32 | } 33 | 34 | var p2p P2P 35 | resp, err := p.client.do(ctx, req, &p2p) 36 | if err != nil { 37 | return nil, resp, err 38 | } 39 | return &p2p, resp, nil 40 | } 41 | 42 | // List implements `GET /p2p.list` 43 | func (p *P2PService) List(ctx context.Context) ([]*P2P, *http.Response, error) { 44 | req, err := p.client.newRequest("GET", "p2p.list", nil) 45 | if err != nil { 46 | return nil, nil, err 47 | } 48 | 49 | var p2p []*P2P 50 | resp, err := p.client.do(ctx, req, &p2p) 51 | if err != nil { 52 | return nil, resp, err 53 | } 54 | return p2p, resp, nil 55 | } 56 | 57 | type P2PCreateOptions struct { 58 | UserID string `json:"user_id"` 59 | } 60 | 61 | // Create implements `POST /p2p.create` 62 | func (p *P2PService) Create(ctx context.Context, opt *P2PCreateOptions) (*P2P, *http.Response, error) { 63 | req, err := p.client.newRequest("POST", "p2p.create", opt) 64 | if err != nil { 65 | return nil, nil, err 66 | } 67 | 68 | var p2p P2P 69 | resp, err := p.client.do(ctx, req, &p2p) 70 | if err != nil { 71 | return nil, resp, err 72 | } 73 | return &p2p, resp, nil 74 | } 75 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | // Team information 4 | type Team struct { 5 | Id string `json:"id"` 6 | Subdomain string `json:"subdomain"` 7 | Name string `json:"name"` 8 | UserId string `json:"uid"` 9 | Description string `json:"description"` 10 | EmailDomain string `json:"email_domain"` 11 | Inactive bool `json:"inactive"` 12 | CreatedAt string `json:"created"` // TODO parse date 13 | UpdatedAt string `json:"updated"` // TODO parse date 14 | } 15 | 16 | const ( 17 | UserRoleOwner = "owner" 18 | UserRoleAdmin = "admin" 19 | UserRoleNormal = "normal" 20 | UserRoleVisitor = "visitor" 21 | ) 22 | 23 | const ( 24 | UserTypeNormal = "normal" 25 | UserTypeAssistant = "assistant" 26 | UserTypeHubot = "hubot" 27 | ) 28 | 29 | // User information 30 | type User struct { 31 | Id string `json:"id"` 32 | TeamId string `json:"team_id"` 33 | VChannelId string `json:"vchannel_id"` 34 | Name string `json:"name"` 35 | FullName string `json:"full_name"` 36 | Email string `json:"email"` 37 | AvatarUrl string `json:"avatar_url"` 38 | Role string `json:"role"` 39 | Type string `json:"type"` 40 | Conn string `json:"conn"` 41 | CreatedAt string `json:"created"` // TODO parse date 42 | UpdatedAt string `json:"updated"` // TODO parse date 43 | } 44 | 45 | // IsOnline tells user connection status. 46 | func (u User) IsOnline() bool { 47 | return u.Conn == "connected" 48 | } 49 | 50 | // IsNormal tells if this user a normal user (owner, admin or normal) 51 | func (u User) IsNormal() bool { 52 | return u.Type == UserTypeNormal && u.Role != UserRoleVisitor 53 | } 54 | 55 | // Channel information. 56 | type Channel struct { 57 | Id string `json:"id"` 58 | TeamId string `json:"team_id"` 59 | UserId string `json:"uid"` 60 | VChannelId string `json:"vchannel_id"` 61 | Name string `json:"name"` 62 | IsPrivate bool `json:"private"` 63 | IsGeneral bool `json:"general"` 64 | Topic string `json:"topic"` 65 | CreatedAt string `json:"created"` // TODO parse date 66 | UpdatedAt string `json:"updated"` // TODO parse date 67 | } 68 | -------------------------------------------------------------------------------- /openapi/message_query.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | func uintp(x uint) *uint { return &x } 9 | 10 | var ( 11 | // Shorthand function for passing limit value as pointer. 12 | MessageQueryWithLimit = uintp 13 | // Shorthand function for passing forward value as pointer. 14 | MessageQueryWithForward = uintp 15 | // Shorthand function for passing backward value as pointer. 16 | MessageQueryWithBackward = uintp 17 | ) 18 | 19 | // TODO(hbc): introduce a query builder or map literal 20 | type MessageQuery struct { 21 | Latest *MessageQueryByLatest `json:"latest,omitempty"` 22 | Since *MessageQueryBySince `json:"since,omitempty"` 23 | Window *MessageQueryByWindow `json:"window,omitempty"` 24 | } 25 | 26 | type MessageQueryByLatest struct { 27 | Limit *uint `json:"limit,omitempty"` 28 | } 29 | 30 | type MessageQueryBySince struct { 31 | SinceKey *MessageKey `json:"key,omitempty"` 32 | SinceTS *VChannelTS `json:"ts,omitempty"` 33 | Forward *uint `json:"forward,omitempty"` 34 | Backward *uint `json:"backward,omitempty"` 35 | } 36 | 37 | type MessageQueryByWindow struct { 38 | FromKey *MessageKey `json:"from_key,omitempty"` 39 | ToKey *MessageKey `json:"to_key,omitempty"` 40 | FromTS *VChannelTS `json:"from_ts,omitempty"` 41 | ToTS *VChannelTS `json:"to_ts,omitempty"` 42 | Forward *uint `json:"forward,omitempty"` 43 | Backward *uint `json:"backward,omitempty"` 44 | } 45 | 46 | type MessageQueryOptions struct { 47 | VChannelID string `json:"vchannel_id"` 48 | Query *MessageQuery `json:"query"` 49 | } 50 | 51 | type MessageQueryResult struct { 52 | Messages []*Message `json:"messages"` 53 | } 54 | 55 | // Query implements `POST /message.query` 56 | func (m *MessageService) Query(ctx context.Context, opt *MessageQueryOptions) (*MessageQueryResult, *http.Response, error) { 57 | req, err := m.client.newRequest("POST", "message.query", opt) 58 | if err != nil { 59 | return nil, nil, err 60 | } 61 | 62 | var rv MessageQueryResult 63 | resp, err := m.client.do(ctx, req, &rv) 64 | if err != nil { 65 | return nil, resp, err 66 | } 67 | return &rv, resp, nil 68 | } 69 | -------------------------------------------------------------------------------- /webhook_client.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | // WebhookResponse represents a response. 11 | type WebhookResponse struct { 12 | StatusCode int `json:"-"` 13 | Code int `json:"code"` 14 | Error string `json:"error,omitempty"` 15 | Result *json.RawMessage `json:"result"` 16 | } 17 | 18 | func (w WebhookResponse) IsOk() bool { 19 | return w.Code == 0 20 | } 21 | 22 | // WebhookClient represents any webhook client can send message to BearyChat. 23 | type WebhookClient interface { 24 | // Set webhook webhook. 25 | SetWebhook(webhook string) WebhookClient 26 | 27 | // Set http client. 28 | SetHTTPClient(client *http.Client) WebhookClient 29 | 30 | // Send webhook payload. 31 | Send(payload io.Reader) (*WebhookResponse, error) 32 | } 33 | 34 | type webhookClient struct { 35 | httpClient *http.Client 36 | 37 | Webhook string 38 | } 39 | 40 | // Creates a new incoming webhook client. 41 | // 42 | // For full documentation, visit https://bearychat.com/integrations/incoming . 43 | func NewIncomingWebhookClient(webhook string) *webhookClient { 44 | return &webhookClient{ 45 | httpClient: http.DefaultClient, 46 | 47 | Webhook: webhook, 48 | } 49 | } 50 | 51 | func (w *webhookClient) SetWebhook(webhook string) WebhookClient { 52 | w.Webhook = webhook 53 | return w 54 | } 55 | 56 | func (w *webhookClient) SetHTTPClient(c *http.Client) WebhookClient { 57 | w.httpClient = c 58 | return w 59 | } 60 | 61 | func (w *webhookClient) Send(payload io.Reader) (*WebhookResponse, error) { 62 | if w.Webhook == "" { 63 | return nil, errors.New("webhook url is required") 64 | } 65 | if w.httpClient == nil { 66 | return nil, errors.New("http client is required") 67 | } 68 | 69 | resp, err := w.httpClient.Post(w.Webhook, "application/json", payload) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | defer resp.Body.Close() 75 | 76 | webhookResponse := new(WebhookResponse) 77 | webhookResponse.StatusCode = resp.StatusCode 78 | if err := json.NewDecoder(resp.Body).Decode(webhookResponse); err != nil { 79 | return nil, err 80 | } 81 | 82 | return webhookResponse, nil 83 | } 84 | -------------------------------------------------------------------------------- /openapi/user.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type UserType string 10 | 11 | type UserRole string 12 | 13 | type UserAvatar struct { 14 | Small *string `json:"small,omitempty"` 15 | Medium *string `json:"medium,omitempty"` 16 | Large *string `json:"large,omitempty"` 17 | } 18 | 19 | type UserProfile struct { 20 | Bio *string `json:"bio,omitempty"` 21 | Position *string `json:"position,omitempty"` 22 | Skype *string `json:"sykpe,omitempty"` 23 | } 24 | 25 | type User struct { 26 | ID *string `json:"id,omitempty"` 27 | TeamID *string `json:"team_id,omitempty"` 28 | Email *string `json:"email,omitempty"` 29 | Name *string `json:"name,omitempty"` 30 | FullName *string `json:"full_name,omitempty"` 31 | Type *UserType `json:"type,omitempty"` 32 | Role *UserRole `json:"role,omitempty"` 33 | Avatars *UserAvatar `json:"avatars,omitempty"` 34 | Profile *UserProfile `json:"profile,omitempty"` 35 | Inactive *bool `json:"inactive,omitempty"` 36 | Created *Time `json:"created,omitempty"` 37 | } 38 | 39 | type UserService service 40 | 41 | type UserInfoOptions struct { 42 | UserID string 43 | } 44 | 45 | // Info implements `GET /user.info` 46 | func (u *UserService) Info(ctx context.Context, opt *UserInfoOptions) (*User, *http.Response, error) { 47 | endpoint := fmt.Sprintf("user.info?user_id=%s", opt.UserID) 48 | req, err := u.client.newRequest("GET", endpoint, nil) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | 53 | var user User 54 | resp, err := u.client.do(ctx, req, &user) 55 | if err != nil { 56 | return nil, resp, err 57 | } 58 | return &user, resp, nil 59 | } 60 | 61 | // List implements `GET /user.list` 62 | func (u *UserService) List(ctx context.Context) ([]*User, *http.Response, error) { 63 | req, err := u.client.newRequest("GET", "user.list", nil) 64 | if err != nil { 65 | return nil, nil, err 66 | } 67 | 68 | var users []*User 69 | resp, err := u.client.do(ctx, req, &users) 70 | if err != nil { 71 | return nil, resp, err 72 | } 73 | return users, resp, nil 74 | } 75 | 76 | // Me implements `GET /user.me` 77 | func (u *UserService) Me(ctx context.Context) (*User, *http.Response, error) { 78 | req, err := u.client.newRequest("GET", "user.me", nil) 79 | if err != nil { 80 | return nil, nil, err 81 | } 82 | 83 | var user User 84 | resp, err := u.client.do(ctx, req, &user) 85 | if err != nil { 86 | return nil, resp, err 87 | } 88 | return &user, resp, nil 89 | } 90 | -------------------------------------------------------------------------------- /openapi/message_pin.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type MessagePin struct { 10 | ID *string `json:"id,omitempty"` 11 | TeamID *string `json:"team_id,omitempty"` 12 | UID *string `json:"uid,omitempty"` 13 | VchannelID *string `json:"vchannel_id,omitempty"` 14 | MessageID *string `json:"message_id,omitempty"` 15 | MessageKey *MessageKey `json:"message_key,omitempty"` 16 | CreatedAt *Time `json:"create_at,omitempty"` 17 | UpdatedAt *Time `json:"update_at,omitempty"` 18 | } 19 | 20 | type MessagePinService service 21 | 22 | type MessagePinListOptions struct { 23 | VChannelID string `json:"vchannel_id"` 24 | } 25 | 26 | // List implements `GET /message_pin.list` 27 | func (m *MessagePinService) List(ctx context.Context, opt *MessagePinListOptions) ([]*MessagePin, *http.Response, error) { 28 | endpoint := fmt.Sprintf("message_pin.list?vchannel_id=%s", opt.VChannelID) 29 | req, err := m.client.newRequest("GET", endpoint, nil) 30 | if err != nil { 31 | return nil, nil, err 32 | } 33 | 34 | var messagePins []*MessagePin 35 | resp, err := m.client.do(ctx, req, &messagePins) 36 | if err != nil { 37 | return nil, resp, err 38 | } 39 | 40 | return messagePins, resp, nil 41 | } 42 | 43 | type MessagePinCreateOptions struct { 44 | VChannelID string `json:"vchannel_id"` 45 | MessageKey MessageKey `json:"message_key"` 46 | } 47 | 48 | // Create implements `POST /message_pin.create` 49 | func (m *MessagePinService) Create(ctx context.Context, opt *MessagePinCreateOptions) (*MessagePin, *http.Response, error) { 50 | req, err := m.client.newRequest("POST", "message_pin.create", opt) 51 | if err != nil { 52 | return nil, nil, err 53 | } 54 | 55 | var messagePin MessagePin 56 | resp, err := m.client.do(ctx, req, &messagePin) 57 | if err != nil { 58 | return nil, resp, err 59 | } 60 | return &messagePin, resp, nil 61 | } 62 | 63 | type MessagePinDeleteOptions struct { 64 | VChannelID string `json:"vchannel_id"` 65 | PinID string `json:"pin_id"` 66 | } 67 | 68 | // Delete implements `POST /message_pin.delete` 69 | func (m *MessagePinService) Delete(ctx context.Context, opt *MessagePinDeleteOptions) (*ResponseNoContent, *http.Response, error) { 70 | req, err := m.client.newRequest("POST", "message_pin.delete", opt) 71 | if err != nil { 72 | return nil, nil, err 73 | } 74 | 75 | resp, err := m.client.do(ctx, req, nil) 76 | if err != nil { 77 | return nil, resp, err 78 | } 79 | return &ResponseNoContent{}, resp, nil 80 | } 81 | -------------------------------------------------------------------------------- /incoming.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Incoming message builder. 13 | // 14 | // m := Incoming{ 15 | // Text: "Hello, **BearyChat**", 16 | // Markdown: true, 17 | // Notification: "Hello, BearyChat in Notification", 18 | // } 19 | // output, _ := m.Build() 20 | // http.Post("YOUR INCOMING HOOK URI HERE", "application/json", output) 21 | // 22 | // For full documentation, visit https://bearychat.com/integrations/incoming . 23 | type Incoming struct { 24 | Text string `json:"text"` 25 | Notification string `json:"notification,omitempty"` 26 | Markdown bool `json:"markdown,omitempty"` 27 | Channel string `json:"channel,omitempty"` 28 | User string `json:"user,omitempty"` 29 | Attachments []IncomingAttachment `json:"attachments,omitempty"` 30 | } 31 | 32 | // Build an incoming message. 33 | func (m Incoming) Build() (io.Reader, error) { 34 | if err := m.Validate(); err != nil { 35 | return nil, err 36 | } 37 | 38 | b, err := json.Marshal(m) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return bytes.NewBuffer(b), nil 43 | } 44 | 45 | // Validate fields. 46 | func (m Incoming) Validate() error { 47 | if m.Text == "" { 48 | return fmt.Errorf("`text` is required for incoming message") 49 | } 50 | 51 | for i, a := range m.Attachments { 52 | if err := a.Validate(); err != nil { 53 | return errors.Wrapf( 54 | err, 55 | "#%d incoming attachment validate failed", 56 | i, 57 | ) 58 | } 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // IncomingAttachment contains incoming attachment fields. 65 | type IncomingAttachment struct { 66 | Title string `json:"title,omitempty"` 67 | Text string `json:"text,omitempty"` 68 | Color string `json:"color,omitempty"` 69 | Images []IncomingAttachmentImage `json:"images,omitempty"` 70 | } 71 | 72 | // Validate fields. 73 | func (a IncomingAttachment) Validate() error { 74 | if a.Title == "" && a.Text == "" { 75 | return fmt.Errorf("`title`/`text` is required for incoming attachment") 76 | } 77 | 78 | for i, im := range a.Images { 79 | if err := im.Validate(); err != nil { 80 | return errors.Wrapf( 81 | err, 82 | "#%d incoming image validate failed", 83 | i, 84 | ) 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // IncomingAttachmentImage contains attachment image fields. 92 | type IncomingAttachmentImage struct { 93 | URL string `json:"url"` 94 | } 95 | 96 | // Validate fields. 97 | func (i IncomingAttachmentImage) Validate() error { 98 | if i.URL == "" { 99 | return fmt.Errorf("`url` is required for incoming image") 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /rtm_message.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import "regexp" 4 | 5 | type RTMMessageType string 6 | 7 | const ( 8 | RTMMessageTypeUnknown RTMMessageType = "unknown" 9 | RTMMessageTypePing = "ping" 10 | RTMMessageTypePong = "pong" 11 | RTMMessageTypeReply = "reply" 12 | RTMMessageTypeOk = "ok" 13 | RTMMessageTypeP2PMessage = "message" 14 | RTMMessageTypeP2PTyping = "typing" 15 | RTMMessageTypeChannelMessage = "channel_message" 16 | RTMMessageTypeChannelTyping = "channel_typing" 17 | RTMMessageTypeUpdateUserConnection = "update_user_connection" 18 | ) 19 | 20 | // RTMMessage represents a message entity send over RTM protocol. 21 | type RTMMessage map[string]interface{} 22 | 23 | func (m RTMMessage) Type() RTMMessageType { 24 | if t, present := m["type"]; present { 25 | if mtype, ok := t.(string); ok { 26 | return RTMMessageType(mtype) 27 | } 28 | if mtype, ok := t.(RTMMessageType); ok { 29 | return mtype 30 | } 31 | } 32 | 33 | return RTMMessageTypeUnknown 34 | } 35 | 36 | // Reply a message (with copying type, vchannel_id) 37 | func (m RTMMessage) Reply(text string) RTMMessage { 38 | reply := RTMMessage{ 39 | "text": text, 40 | "vchannel_id": m["vchannel_id"], 41 | } 42 | 43 | if m.IsP2P() { 44 | reply["type"] = RTMMessageTypeP2PMessage 45 | reply["to_uid"] = m["uid"] 46 | } else { 47 | reply["type"] = RTMMessageTypeChannelMessage 48 | reply["channel_id"] = m["channel_id"] 49 | } 50 | 51 | return reply 52 | } 53 | 54 | // Refer a message 55 | func (m RTMMessage) Refer(text string) RTMMessage { 56 | refer := m.Reply(text) 57 | refer["refer_key"] = m["key"] 58 | 59 | return refer 60 | } 61 | 62 | func (m RTMMessage) IsP2P() bool { 63 | mt := m.Type() 64 | if mt == RTMMessageTypeP2PMessage || mt == RTMMessageTypeP2PTyping { 65 | return true 66 | } 67 | 68 | return false 69 | } 70 | 71 | func (m RTMMessage) IsChatMessage() bool { 72 | mt := m.Type() 73 | if mt == RTMMessageTypeP2PMessage || mt == RTMMessageTypeChannelMessage { 74 | return true 75 | } 76 | 77 | return false 78 | } 79 | 80 | func (m RTMMessage) IsFromUser(u User) bool { 81 | return m.IsFromUID(u.Id) 82 | } 83 | 84 | func (m RTMMessage) IsFromUID(uid string) bool { 85 | return m["uid"] == uid 86 | } 87 | 88 | func (m RTMMessage) Text() string { 89 | if text, ok := m["text"].(string); ok { 90 | return text 91 | } 92 | 93 | return "" 94 | } 95 | 96 | func (m RTMMessage) ParseMentionUser(u User) (bool, string) { 97 | return m.ParseMentionUID(u.Id) 98 | } 99 | 100 | var mentionUserRegex = regexp.MustCompile("@<=(=[A-Za-z0-9]+)=> ") 101 | 102 | func (m RTMMessage) ParseMentionUID(uid string) (bool, string) { 103 | text := m.Text() 104 | 105 | if m.IsP2P() { 106 | return true, text 107 | } 108 | 109 | if text == "" { 110 | return false, text 111 | } 112 | 113 | locs := mentionUserRegex.FindAllStringSubmatchIndex(text, -1) 114 | 115 | if len(locs) == 0 { 116 | return false, text 117 | } 118 | 119 | for _, loc := range locs { 120 | // "@<==1=> xxx" -> [0 8 3 5] 121 | // [3:5] "=1" [8:] "xxx" 122 | if text[loc[2]:loc[3]] == uid { 123 | return true, text[loc[1]:] 124 | } 125 | } 126 | 127 | return false, text 128 | } 129 | -------------------------------------------------------------------------------- /example/chaosmonkey/main.go: -------------------------------------------------------------------------------- 1 | // Chaos Monkey will talk to you randomly 🙊 2 | // 3 | // Envvars: 4 | // 5 | // - `CM_RTM_TOKEN`: BearyChat RTM token 6 | // - `CM_VICTIMS`: user ids who will be talk with, 7 | // separates with comma: `=bw52O,=bw52P` 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "log" 13 | "math/rand" 14 | "os" 15 | "strings" 16 | "time" 17 | 18 | "github.com/bearyinnovative/bearychat-go" 19 | ) 20 | 21 | type Config struct { 22 | rtmToken string 23 | victims []string 24 | } 25 | 26 | func (c Config) isVictimUID(uid string) bool { 27 | for _, victimUID := range c.victims { 28 | if victimUID == uid { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | func (c Config) randomVictim() string { 36 | return c.victims[rand.Intn(len(c.victims))] 37 | } 38 | 39 | func (c Config) insultMessage(user *bearychat.User) bearychat.RTMMessage { 40 | messages := []string{ 41 | fmt.Sprintf("hi %s", user.Name), 42 | fmt.Sprintf("hey %s, are you chill?", user.Name), 43 | fmt.Sprintf("苟利国家生死以,岂因祸福避趋之,%s 识得唔识得啊?", user.Name), 44 | } 45 | 46 | return bearychat.RTMMessage{ 47 | "type": bearychat.RTMMessageTypeP2PMessage, 48 | "vchannel_id": user.VChannelId, 49 | "to_uid": user.Id, 50 | "text": messages[rand.Intn(len(messages))], 51 | "call_id": rand.Int(), // FIXME `call_id` sequence generator 52 | "refer_key": nil, 53 | } 54 | } 55 | 56 | func main() { 57 | config := mustLoadConfigFromEnv() 58 | 59 | rtmClient, err := bearychat.NewRTMClient(config.rtmToken) 60 | checkErr(err) 61 | 62 | user, wsHost, err := rtmClient.Start() 63 | checkErr(err) 64 | 65 | rtmLoop, err := bearychat.NewRTMLoop(wsHost) 66 | checkErr(err) 67 | 68 | checkErr(rtmLoop.Start()) 69 | defer rtmLoop.Stop() 70 | 71 | go rtmLoop.Keepalive(time.NewTicker(2 * time.Second)) 72 | 73 | errC := rtmLoop.ErrC() 74 | messageC, err := rtmLoop.ReadC() 75 | checkErr(err) 76 | 77 | tickTock := time.NewTicker(15 * time.Second) 78 | defer tickTock.Stop() 79 | 80 | for { 81 | select { 82 | case err := <-errC: 83 | checkErr(err) 84 | return 85 | case message := <-messageC: 86 | if !message.IsChatMessage() { 87 | continue 88 | } 89 | if !message.IsP2P() { 90 | continue 91 | } 92 | if message.IsFromUser(*user) { 93 | continue 94 | } 95 | uid := message["uid"].(string) 96 | if !config.isVictimUID(uid) { 97 | continue 98 | } 99 | 100 | log.Printf("user %s said: %s", uid, message["text"]) 101 | 102 | checkErr(rtmLoop.Send(message.Refer("🙊"))) 103 | case <-tickTock.C: 104 | user, err := rtmClient.User.Info(config.randomVictim()) 105 | checkErr(err) 106 | 107 | log.Printf("insulting user %s", user.Name) 108 | checkErr(rtmLoop.Send(config.insultMessage(user))) 109 | } 110 | } 111 | } 112 | 113 | func mustLoadConfigFromEnv() (config Config) { 114 | config.rtmToken = os.Getenv("CM_RTM_TOKEN") 115 | if config.rtmToken == "" { 116 | log.Fatalf("`CM_RTM_TOKEN` is required!") 117 | return 118 | } 119 | 120 | svictims := os.Getenv("CM_VICTIMS") 121 | if svictims == "" { 122 | log.Fatalf("`CM_VICTIMS` is required!") 123 | return 124 | } 125 | 126 | config.victims = strings.Split(svictims, ",") 127 | if len(config.victims) == 0 { 128 | log.Fatalf("`CM_VICTIMS` is required!") 129 | return 130 | } 131 | 132 | return 133 | } 134 | 135 | func checkErr(err error) { 136 | if err != nil { 137 | log.Fatal(err) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /rtm_loop_impl.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/gorilla/websocket" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type rtmLoop struct { 14 | wsHost string 15 | conn *websocket.Conn 16 | state RTMLoopState 17 | callId uint64 18 | llock *sync.RWMutex // lock for properties below 19 | 20 | rtmCBacklog int 21 | rtmC chan RTMMessage 22 | errC chan error 23 | } 24 | 25 | type rtmLoopSetter func(*rtmLoop) error 26 | 27 | // Set RTM message chan backlog. 28 | func WithRTMLoopBacklog(backlog int) rtmLoopSetter { 29 | return func(r *rtmLoop) error { 30 | r.rtmCBacklog = backlog 31 | return nil 32 | } 33 | } 34 | 35 | func NewRTMLoop(wsHost string, setters ...rtmLoopSetter) (*rtmLoop, error) { 36 | l := &rtmLoop{ 37 | wsHost: wsHost, 38 | state: RTMLoopStateClosed, 39 | callId: 0, 40 | llock: &sync.RWMutex{}, 41 | 42 | errC: make(chan error, 1024), 43 | } 44 | for _, setter := range setters { 45 | if err := setter(l); err != nil { 46 | return nil, err 47 | } 48 | } 49 | 50 | if l.rtmCBacklog <= 0 { 51 | l.rtmC = make(chan RTMMessage) 52 | } else { 53 | l.rtmC = make(chan RTMMessage, l.rtmCBacklog) 54 | } 55 | 56 | return l, nil 57 | } 58 | 59 | func (l *rtmLoop) Start() error { 60 | l.llock.Lock() 61 | defer l.llock.Unlock() 62 | 63 | conn, _, err := websocket.DefaultDialer.Dial(l.wsHost, nil) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | l.conn = conn 69 | l.state = RTMLoopStateOpen 70 | 71 | go l.readMessage() 72 | 73 | return nil 74 | } 75 | 76 | func (l *rtmLoop) Stop() error { 77 | return nil 78 | } 79 | 80 | func (l *rtmLoop) State() RTMLoopState { 81 | l.llock.RLock() 82 | defer l.llock.RUnlock() 83 | 84 | return l.state 85 | } 86 | 87 | func (l *rtmLoop) Ping() error { 88 | return l.Send(RTMMessage{"type": RTMMessageTypePing}) 89 | } 90 | 91 | func (l *rtmLoop) Keepalive(interval *time.Ticker) error { 92 | defer interval.Stop() 93 | for { 94 | select { 95 | case <-interval.C: 96 | if err := l.Ping(); err != nil { 97 | return errors.Wrap(err, "keepalive closed") 98 | } 99 | } 100 | } 101 | } 102 | 103 | func (l *rtmLoop) Send(m RTMMessage) error { 104 | if l.State() != RTMLoopStateOpen { 105 | return ErrRTMLoopClosed 106 | } 107 | 108 | if _, hasCallId := m["call_id"]; !hasCallId { 109 | m["call_id"] = l.advanceCallId() 110 | } 111 | 112 | rawMessage, err := json.Marshal(m) 113 | if err != nil { 114 | return errors.Wrap(err, "encode message failed") 115 | } 116 | 117 | if err := l.conn.WriteMessage(websocket.TextMessage, rawMessage); err != nil { 118 | return errors.Wrap(err, "write socket failed") 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (l *rtmLoop) ReadC() (chan RTMMessage, error) { 125 | if l.State() != RTMLoopStateOpen { 126 | return nil, ErrRTMLoopClosed 127 | } 128 | 129 | return l.rtmC, nil 130 | } 131 | 132 | func (l *rtmLoop) ErrC() chan error { 133 | return l.errC 134 | } 135 | 136 | // Listen & read message from BearyChat 137 | func (l *rtmLoop) readMessage() { 138 | for { 139 | if l.State() == RTMLoopStateClosed { 140 | return 141 | } 142 | 143 | _, rawMessage, err := l.conn.ReadMessage() 144 | if err != nil { 145 | l.errC <- errors.Wrap(err, "read socket failed") 146 | continue 147 | } 148 | 149 | message := RTMMessage{} 150 | if err = json.Unmarshal(rawMessage, &message); err != nil { 151 | l.errC <- errors.Wrap(err, "decode message failed") 152 | continue 153 | } 154 | 155 | l.rtmC <- message 156 | } 157 | } 158 | 159 | func (l *rtmLoop) advanceCallId() uint64 { 160 | return atomic.AddUint64(&l.callId, 1) 161 | } 162 | -------------------------------------------------------------------------------- /rtm_client.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | const ( 13 | DEFAULT_RTM_API_BASE = "https://rtm.bearychat.com" 14 | ) 15 | 16 | // RTMClient is used to interactive with BearyChat's RTM api 17 | // and websocket message protocol. 18 | type RTMClient struct { 19 | // rtm token 20 | Token string 21 | 22 | // rtm api base, defaults to `https://rtm.bearychat.com` 23 | APIBase string 24 | 25 | // services 26 | CurrentTeam *RTMCurrentTeamService 27 | User *RTMUserService 28 | Channel *RTMChannelService 29 | 30 | httpClient *http.Client 31 | } 32 | 33 | type rtmOptSetter func(*RTMClient) error 34 | 35 | // enabled services 36 | var services = []rtmOptSetter{ 37 | newRTMCurrentTeamService, 38 | newRTMUserService, 39 | newRTMChannelService, 40 | } 41 | 42 | // NewRTMClient creates a rtm client. 43 | // 44 | // client, _ := NewRTMClient( 45 | // "rtm-token", 46 | // WithRTMAPIBase("https://rtm.bearychat.com"), 47 | // ) 48 | func NewRTMClient(token string, setters ...rtmOptSetter) (*RTMClient, error) { 49 | c := &RTMClient{ 50 | Token: token, 51 | APIBase: DEFAULT_RTM_API_BASE, 52 | 53 | httpClient: http.DefaultClient, 54 | } 55 | 56 | for _, setter := range services { 57 | if err := setter(c); err != nil { 58 | return nil, err 59 | } 60 | } 61 | 62 | for _, setter := range setters { 63 | if err := setter(c); err != nil { 64 | return nil, err 65 | } 66 | } 67 | 68 | return c, nil 69 | } 70 | 71 | // WithRTMAPIBase can be used to set rtm client's base api. 72 | func WithRTMAPIBase(apiBase string) rtmOptSetter { 73 | return func(c *RTMClient) error { 74 | c.APIBase = apiBase 75 | return nil 76 | } 77 | } 78 | 79 | // WithRTMHTTPClient sets http client. 80 | func WithRTMHTTPClient(httpClient *http.Client) rtmOptSetter { 81 | return func(c *RTMClient) error { 82 | c.httpClient = httpClient 83 | return nil 84 | } 85 | } 86 | 87 | // Do performs an api request. 88 | func (c RTMClient) Do(resource, method string, in, result interface{}) (*http.Response, error) { 89 | uri, err := addTokenToResourceUri( 90 | fmt.Sprintf("%s/%s", c.APIBase, resource), 91 | c.Token, 92 | ) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | // build payload (if any) 98 | var buf io.ReadWriter 99 | if in != nil { 100 | buf = new(bytes.Buffer) 101 | err := json.NewEncoder(buf).Encode(in) 102 | if err != nil { 103 | return nil, err 104 | } 105 | } 106 | 107 | // build request 108 | req, err := http.NewRequest(method, uri, buf) 109 | if err != nil { 110 | return nil, err 111 | } 112 | if in != nil { 113 | req.Header.Set("Content-Type", "application/json") 114 | } 115 | req.Header.Set("Accept", "application/json") 116 | 117 | resp, err := c.httpClient.Do(req) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | // parse response 123 | defer resp.Body.Close() 124 | response := new(RTMAPIResponse) 125 | if err := json.NewDecoder(resp.Body).Decode(response); err != nil { 126 | return resp, err 127 | } 128 | 129 | // request failed 130 | if resp.StatusCode/100 != 2 || response.Code != 0 { 131 | return resp, response 132 | } 133 | 134 | // parse result (if any) 135 | if result != nil { 136 | return resp, json.Unmarshal(response.Result, result) 137 | } 138 | 139 | return resp, nil 140 | } 141 | 142 | func (c RTMClient) Get(resource string, result interface{}) (*http.Response, error) { 143 | return c.Do(resource, "GET", nil, result) 144 | } 145 | 146 | func (c RTMClient) Post(resource string, in, result interface{}) (*http.Response, error) { 147 | return c.Do(resource, "POST", in, result) 148 | } 149 | 150 | // Start performs rtm.start 151 | func (c RTMClient) Start() (*User, string, error) { 152 | userAndWSHost := new(struct { 153 | User *User `json:"user"` 154 | WSHost string `json:"ws_host"` 155 | }) 156 | _, err := c.Post("start", nil, userAndWSHost) 157 | 158 | return userAndWSHost.User, userAndWSHost.WSHost, err 159 | } 160 | 161 | // Incoming performs rtm.message 162 | func (c RTMClient) Incoming(m RTMIncoming) error { 163 | _, err := c.Post("message", m, nil) 164 | 165 | return err 166 | } 167 | 168 | // RTM api request response 169 | type RTMAPIResponse struct { 170 | Code int `json:"code"` 171 | Result json.RawMessage `json:"result,omitempty"` 172 | ErrorReason string `json:"error,omitempty"` 173 | } 174 | 175 | func (r *RTMAPIResponse) Error() string { 176 | return r.ErrorReason 177 | } 178 | 179 | func addTokenToResourceUri(resource, token string) (string, error) { 180 | uri, err := url.Parse(resource) 181 | if err != nil { 182 | return "", err 183 | } 184 | 185 | q := uri.Query() 186 | q.Set("token", token) 187 | uri.RawQuery = q.Encode() 188 | 189 | return uri.String(), nil 190 | } 191 | 192 | // RTMIncoming represents message sent vai `rtm.message` api 193 | type RTMIncoming struct { 194 | Text string `json:"text"` 195 | VChannelId string `json:"vchannel"` 196 | Markdown bool `json:"markdown,omitempty"` 197 | Attachments []IncomingAttachment `json:"attachments,omitempty"` 198 | } 199 | -------------------------------------------------------------------------------- /openapi/session_channel.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type SessionChannel struct { 10 | ID *string `json:"id,omitempty"` 11 | TeamID *string `json:"team_id,omitempty"` 12 | VChannelID *string `json:"vchannel_id,omitempty"` 13 | Name *string `json:"name,omitempty"` 14 | Type *VChannelType `json:"type,omitempty"` 15 | IsMember *bool `json:"is_member,omitempty"` 16 | IsActive *bool `json:"is_active,omitempty"` 17 | MemberUserIDs []string `json:"member_uids,omitempty"` 18 | LatestTS *VChannelTS `json:"latest_ts,omitempty"` 19 | } 20 | 21 | type SessionChannelService service 22 | 23 | type SessionChannelInfoOptions struct { 24 | ChannelID string 25 | } 26 | 27 | // Info implements `GET /session_channel.info` 28 | func (s *SessionChannelService) Info(ctx context.Context, opt *SessionChannelInfoOptions) (*SessionChannel, *http.Response, error) { 29 | endpoint := fmt.Sprintf("session_channel.info?session_channel_id=%s", opt.ChannelID) 30 | req, err := s.client.newRequest("GET", endpoint, nil) 31 | if err != nil { 32 | return nil, nil, err 33 | } 34 | 35 | var channel SessionChannel 36 | resp, err := s.client.do(ctx, req, &channel) 37 | if err != nil { 38 | return nil, resp, err 39 | } 40 | return &channel, resp, nil 41 | } 42 | 43 | // List implements `GET /session_channel.list` 44 | func (s *SessionChannelService) List(ctx context.Context) ([]*SessionChannel, *http.Response, error) { 45 | req, err := s.client.newRequest("GET", "session_channel.list", nil) 46 | if err != nil { 47 | return nil, nil, err 48 | } 49 | 50 | var channels []*SessionChannel 51 | resp, err := s.client.do(ctx, req, &channels) 52 | if err != nil { 53 | return nil, resp, err 54 | } 55 | return channels, resp, nil 56 | } 57 | 58 | type SessionChannelCreateOptions struct { 59 | Name *string `json:"name,omitempty"` 60 | MemberUserIDs []string `json:"member_uids"` 61 | } 62 | 63 | // Create implements `POST /session_channel.create` 64 | func (s *SessionChannelService) Create(ctx context.Context, opt *SessionChannelCreateOptions) (*SessionChannel, *http.Response, error) { 65 | req, err := s.client.newRequest("POST", "session_channel.create", opt) 66 | if err != nil { 67 | return nil, nil, err 68 | } 69 | 70 | var channel SessionChannel 71 | resp, err := s.client.do(ctx, req, &channel) 72 | if err != nil { 73 | return nil, resp, err 74 | } 75 | return &channel, resp, nil 76 | } 77 | 78 | type SessionChannelArchiveOptions struct { 79 | ChannelID string `json:"session_channel_id"` 80 | } 81 | 82 | // Archive implements `POST /session_channel.archive` 83 | func (s *SessionChannelService) Archive(ctx context.Context, opt *SessionChannelArchiveOptions) (*SessionChannel, *http.Response, error) { 84 | req, err := s.client.newRequest("POST", "session_channel.archive", opt) 85 | if err != nil { 86 | return nil, nil, err 87 | } 88 | 89 | var channel SessionChannel 90 | resp, err := s.client.do(ctx, req, &channel) 91 | if err != nil { 92 | return nil, resp, err 93 | } 94 | return &channel, resp, nil 95 | } 96 | 97 | type SessionChannelConvertOptions struct { 98 | ChannelID string `json:"session_channel_id"` 99 | Name string `json:"name"` 100 | Private *bool `json:"private,omitempty"` 101 | } 102 | 103 | // ConvertToChannel implements `POST /session_channel.convert_to_channel` 104 | func (s *SessionChannelService) ConvertToChannel(ctx context.Context, opt *SessionChannelConvertOptions) (*Channel, *http.Response, error) { 105 | req, err := s.client.newRequest("POST", "session_channel.convert_to_channel", opt) 106 | if err != nil { 107 | return nil, nil, err 108 | } 109 | 110 | var channel Channel 111 | resp, err := s.client.do(ctx, req, &channel) 112 | if err != nil { 113 | return nil, resp, err 114 | } 115 | return &channel, resp, nil 116 | } 117 | 118 | type SessionChannelLeaveOptions struct { 119 | ChannelID string `json:"session_channel_id"` 120 | } 121 | 122 | // Leave implements `POST /session_channel.leave` 123 | func (s *SessionChannelService) Leave(ctx context.Context, opt *SessionChannelLeaveOptions) (*ResponseNoContent, *http.Response, error) { 124 | req, err := s.client.newRequest("POST", "session_channel.leave", opt) 125 | if err != nil { 126 | return nil, nil, err 127 | } 128 | 129 | resp, err := s.client.do(ctx, req, nil) 130 | if err != nil { 131 | return nil, resp, err 132 | } 133 | return &ResponseNoContent{}, resp, nil 134 | } 135 | 136 | type SessionChannelInviteOptions struct { 137 | ChannelID string `json:"session_channel_id"` 138 | InviteUserID string `json:"invite_uid"` 139 | } 140 | 141 | // Invite implements `POST /session_channel.invite` 142 | func (s *SessionChannelService) Invite(ctx context.Context, opt *SessionChannelInviteOptions) (*ResponseNoContent, *http.Response, error) { 143 | req, err := s.client.newRequest("POST", "session_channel.invite", opt) 144 | if err != nil { 145 | return nil, nil, err 146 | } 147 | 148 | resp, err := s.client.do(ctx, req, nil) 149 | if err != nil { 150 | return nil, resp, err 151 | } 152 | return &ResponseNoContent{}, resp, nil 153 | } 154 | 155 | type SessionChannelKickOptions struct { 156 | ChannelID string `json:"session_channel_id"` 157 | KickUserID string `json:"kick_uid"` 158 | } 159 | 160 | // Kick implements `POST /session_channel.kick` 161 | func (s *SessionChannelService) Kick(ctx context.Context, opt *SessionChannelKickOptions) (*ResponseNoContent, *http.Response, error) { 162 | req, err := s.client.newRequest("POST", "session_channel.kick", opt) 163 | if err != nil { 164 | return nil, nil, err 165 | } 166 | 167 | resp, err := s.client.do(ctx, req, nil) 168 | if err != nil { 169 | return nil, resp, err 170 | } 171 | return &ResponseNoContent{}, resp, nil 172 | } 173 | -------------------------------------------------------------------------------- /openapi/client.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var defaultBaseURL = "https://api.bearychat.com/v1/" 17 | 18 | // Client interacts with BearyChat's API. 19 | type Client struct { 20 | // HTTP client for calling the API. 21 | // Use http.DefaultClient by default. 22 | httpClient *http.Client 23 | 24 | // Base URL for API requests. Defaults to BearyChat's OpenAPI host. 25 | // BaseURL should always be specified with a trailing slash. 26 | BaseURL *url.URL 27 | 28 | // Access token for the client. 29 | Token string 30 | 31 | // Shared services holder to reduce real service allocating. 32 | base service 33 | 34 | Meta *MetaService 35 | Team *TeamService 36 | User *UserService 37 | Channel *ChannelService 38 | SessionChannel *SessionChannelService 39 | Message *MessageService 40 | P2P *P2PService 41 | Emoji *EmojiService 42 | Sticker *StickerService 43 | RTM *RTMService 44 | MessagePin *MessagePinService 45 | } 46 | 47 | type service struct { 48 | client *Client 49 | } 50 | 51 | type clientOpt func(c *Client) 52 | 53 | // NewClientWithBaseURL binds BaseURL to client. 54 | func NewClientWithBaseURL(u *url.URL) clientOpt { 55 | return func(c *Client) { 56 | c.BaseURL = u 57 | } 58 | } 59 | 60 | // NewClientWithHTTPClient binds http client to client. 61 | func NewClientWithHTTPClient(httpClient *http.Client) clientOpt { 62 | return func(c *Client) { 63 | c.httpClient = httpClient 64 | } 65 | } 66 | 67 | // NewClient constructs a client with given access token. 68 | // Other settings can set via clientOpt functions. 69 | func NewClient(token string, opts ...clientOpt) *Client { 70 | c := &Client{ 71 | Token: token, 72 | } 73 | 74 | for _, o := range opts { 75 | o(c) 76 | } 77 | 78 | if c.httpClient == nil { 79 | c.httpClient = http.DefaultClient 80 | } 81 | 82 | if c.BaseURL == nil { 83 | baseURL, _ := url.Parse(defaultBaseURL) 84 | c.BaseURL = baseURL 85 | } 86 | 87 | c.base.client = c 88 | c.Meta = (*MetaService)(&c.base) 89 | c.Team = (*TeamService)(&c.base) 90 | c.User = (*UserService)(&c.base) 91 | c.Channel = (*ChannelService)(&c.base) 92 | c.SessionChannel = (*SessionChannelService)(&c.base) 93 | c.Message = (*MessageService)(&c.base) 94 | c.P2P = (*P2PService)(&c.base) 95 | c.Emoji = (*EmojiService)(&c.base) 96 | c.Sticker = (*StickerService)(&c.base) 97 | c.RTM = (*RTMService)(&c.base) 98 | c.MessagePin = (*MessagePinService)(&c.base) 99 | 100 | return c 101 | } 102 | 103 | // newRequest creates an API request. API method should specified without a leading slash. 104 | // If specified, the value pointed to body is JSON encoded and included as the request body. 105 | func (c *Client) newRequest(requestMethod, apiMethod string, body interface{}) (*http.Request, error) { 106 | m, err := url.Parse(apiMethod) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | u := c.BaseURL.ResolveReference(m) 112 | q := u.Query() 113 | q.Set("token", c.Token) 114 | u.RawQuery = q.Encode() 115 | 116 | var buf io.ReadWriter 117 | if body != nil { 118 | buf = &bytes.Buffer{} 119 | err := json.NewEncoder(buf).Encode(body) 120 | if err != nil { 121 | return nil, err 122 | } 123 | } 124 | 125 | req, err := http.NewRequest(requestMethod, u.String(), buf) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | if body != nil { 131 | req.Header.Set("Content-Type", "application/json") 132 | } 133 | 134 | return req, nil 135 | } 136 | 137 | // do sends an API request and returns the API response. The API response is JSON decoded and 138 | // stored in the value pointed to v. If v implements the io.Writer interface, the raw response body 139 | // will be written to v, without attempting to first decode it. 140 | // 141 | // The provided ctx must be non-nil. If it is canceled or times out, ctx.Err() will be returned. 142 | func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { 143 | req = req.WithContext(ctx) 144 | 145 | resp, err := c.httpClient.Do(req) 146 | if err != nil { 147 | // try to use context's error 148 | select { 149 | case <-ctx.Done(): 150 | return nil, ctx.Err() 151 | default: 152 | } 153 | 154 | return nil, err 155 | } 156 | 157 | defer resp.Body.Close() 158 | 159 | err = CheckResponse(resp) 160 | if err != nil { 161 | return resp, err 162 | } 163 | 164 | if v != nil { 165 | if w, ok := v.(io.Writer); ok { 166 | io.Copy(w, resp.Body) 167 | } else { 168 | err = json.NewDecoder(resp.Body).Decode(v) 169 | } 170 | } 171 | 172 | return resp, err 173 | } 174 | 175 | // ErrorResponse represents errors caused by an API request. 176 | type ErrorResponse struct { 177 | // HTTP response that caused this error 178 | Response *http.Response 179 | ErrorCode int `json:"code"` 180 | ErrorReason string `json:"error"` 181 | } 182 | 183 | func (r ErrorResponse) Error() string { 184 | return fmt.Sprintf( 185 | "%v %v: %d %d %s", 186 | r.Response.Request.Method, 187 | r.Response.Request.URL, 188 | r.Response.StatusCode, 189 | r.ErrorCode, 190 | r.ErrorReason, 191 | ) 192 | } 193 | 194 | // CheckResponse checks the API response for errors, and returns them if present. 195 | func CheckResponse(r *http.Response) error { 196 | if c := r.StatusCode; 200 <= c && c <= 299 { 197 | return nil 198 | } 199 | 200 | errResponse := &ErrorResponse{Response: r} 201 | data, err := ioutil.ReadAll(r.Body) 202 | if err == nil && data != nil { 203 | json.Unmarshal(data, errResponse) 204 | } 205 | 206 | // TODO(hbc): handle ratelimit error 207 | 208 | return errResponse 209 | } 210 | 211 | const timeLayout = "2006-01-02T15:04:05-0700" 212 | 213 | // Time with custom JSON format. 214 | type Time struct { 215 | time.Time 216 | } 217 | 218 | func (t Time) UnmarshalJSON(b []byte) (err error) { 219 | s := strings.Trim(string(b), "\"") 220 | t.Time, err = time.Parse(timeLayout, s) 221 | return 222 | } 223 | 224 | func (t Time) MarshalJSON() ([]byte, error) { 225 | return []byte(fmt.Sprintf("\"%s\"", t.Time.Format(timeLayout))), nil 226 | } 227 | 228 | type ResponseOK struct { 229 | Code *int `json:"code,omitempty"` 230 | } 231 | 232 | type ResponseNoContent struct{} 233 | -------------------------------------------------------------------------------- /openapi/channel.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type Channel struct { 10 | ID *string `json:"id,omitempty"` 11 | TeamID *string `json:"team_id,omitempty"` 12 | VChannelID *string `json:"vchannel_id,omitempty"` 13 | UserID *string `json:"uid,omitempty"` 14 | Name *string `json:"name,omitempty"` 15 | Type *VChannelType `json:"type,omitempty"` 16 | Private *bool `json:"private,omitempty"` 17 | General *bool `json:"general,omitempty"` 18 | Topic *string `json:"topic,omitempty"` 19 | IsMember *bool `json:"is_member,omitempty"` 20 | IsActive *bool `json:"is_active,omitempty"` 21 | MemberUserIDs []string `json:"member_uids,omitempty"` 22 | LatestTS *VChannelTS `json:"latest_ts,omitempty"` 23 | } 24 | 25 | type ChannelService service 26 | 27 | type ChannelInfoOptions struct { 28 | ChannelID string 29 | } 30 | 31 | // Info implements `GET /channel.info` 32 | func (c *ChannelService) Info(ctx context.Context, opt *ChannelInfoOptions) (*Channel, *http.Response, error) { 33 | endpoint := fmt.Sprintf("channel.info?channel_id=%s", opt.ChannelID) 34 | req, err := c.client.newRequest("GET", endpoint, nil) 35 | if err != nil { 36 | return nil, nil, err 37 | } 38 | 39 | var channel Channel 40 | resp, err := c.client.do(ctx, req, &channel) 41 | if err != nil { 42 | return nil, resp, err 43 | } 44 | return &channel, resp, nil 45 | } 46 | 47 | // List implements `GET /channel.list` 48 | func (c *ChannelService) List(ctx context.Context) ([]*Channel, *http.Response, error) { 49 | req, err := c.client.newRequest("GET", "channel.list", nil) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | var channels []*Channel 55 | resp, err := c.client.do(ctx, req, &channels) 56 | if err != nil { 57 | return nil, resp, err 58 | } 59 | return channels, resp, nil 60 | } 61 | 62 | type ChannelCreateOptions struct { 63 | Name string `json:"name"` 64 | Topic *string `json:"topic,omitempty"` 65 | Private *bool `json:"private,omitempty"` 66 | } 67 | 68 | // Create implements `POST /channel.create` 69 | func (c *ChannelService) Create(ctx context.Context, opt *ChannelCreateOptions) (*Channel, *http.Response, error) { 70 | req, err := c.client.newRequest("POST", "channel.create", opt) 71 | if err != nil { 72 | return nil, nil, err 73 | } 74 | 75 | var channel Channel 76 | resp, err := c.client.do(ctx, req, &channel) 77 | if err != nil { 78 | return nil, resp, err 79 | } 80 | return &channel, resp, nil 81 | } 82 | 83 | type ChannelArchiveOptions struct { 84 | ChannelID string `json:"channel_id"` 85 | } 86 | 87 | // Archive implements `POST /channel.archive` 88 | func (c *ChannelService) Archive(ctx context.Context, opt *ChannelArchiveOptions) (*Channel, *http.Response, error) { 89 | req, err := c.client.newRequest("POST", "channel.archive", opt) 90 | if err != nil { 91 | return nil, nil, err 92 | } 93 | 94 | var channel Channel 95 | resp, err := c.client.do(ctx, req, &channel) 96 | if err != nil { 97 | return nil, resp, err 98 | } 99 | return &channel, resp, nil 100 | } 101 | 102 | type ChannelUnarchiveOptions struct { 103 | ChannelID string `json:"channel_id"` 104 | } 105 | 106 | // Unarchive implements `POST /channel.unarchive` 107 | func (c *ChannelService) Unarchive(ctx context.Context, opt *ChannelUnarchiveOptions) (*Channel, *http.Response, error) { 108 | req, err := c.client.newRequest("POST", "channel.unarchive", opt) 109 | if err != nil { 110 | return nil, nil, err 111 | } 112 | 113 | var channel Channel 114 | resp, err := c.client.do(ctx, req, &channel) 115 | if err != nil { 116 | return nil, resp, err 117 | } 118 | return &channel, resp, nil 119 | } 120 | 121 | type ChannelLeaveOptions struct { 122 | ChannelID string `json:"channel_id"` 123 | } 124 | 125 | // Leave implements `POST /channel.leave` 126 | func (c *ChannelService) Leave(ctx context.Context, opt *ChannelLeaveOptions) (*ResponseNoContent, *http.Response, error) { 127 | req, err := c.client.newRequest("POST", "channel.leave", opt) 128 | if err != nil { 129 | return nil, nil, err 130 | } 131 | 132 | resp, err := c.client.do(ctx, req, nil) 133 | if err != nil { 134 | return nil, resp, err 135 | } 136 | return &ResponseNoContent{}, resp, nil 137 | } 138 | 139 | type ChannelJoinOptions struct { 140 | ChannelID string `json:"channel_id"` 141 | } 142 | 143 | // Join implements `POST /channel.join` 144 | func (c *ChannelService) Join(ctx context.Context, opt *ChannelJoinOptions) (*Channel, *http.Response, error) { 145 | req, err := c.client.newRequest("POST", "channel.join", opt) 146 | if err != nil { 147 | return nil, nil, err 148 | } 149 | 150 | var channel Channel 151 | resp, err := c.client.do(ctx, req, &channel) 152 | if err != nil { 153 | return nil, resp, err 154 | } 155 | return &channel, resp, nil 156 | } 157 | 158 | type ChannelInviteOptions struct { 159 | ChannelID string `json:"channel_id"` 160 | InviteUserID string `json:"invite_uid"` 161 | } 162 | 163 | // Invite implements `POST /channel.invite` 164 | func (c *ChannelService) Invite(ctx context.Context, opt *ChannelInviteOptions) (*ResponseNoContent, *http.Response, error) { 165 | req, err := c.client.newRequest("POST", "channel.invite", opt) 166 | if err != nil { 167 | return nil, nil, err 168 | } 169 | 170 | resp, err := c.client.do(ctx, req, nil) 171 | if err != nil { 172 | return nil, resp, err 173 | } 174 | return &ResponseNoContent{}, resp, nil 175 | } 176 | 177 | type ChannelKickOptions struct { 178 | ChannelID string `json:"channel_id"` 179 | KickUserID string `json:"kick_uid"` 180 | } 181 | 182 | // Kick implements `POST /channel.kick` 183 | func (c *ChannelService) Kick(ctx context.Context, opt *ChannelKickOptions) (*ResponseNoContent, *http.Response, error) { 184 | req, err := c.client.newRequest("POST", "channel.kick", opt) 185 | if err != nil { 186 | return nil, nil, err 187 | } 188 | 189 | resp, err := c.client.do(ctx, req, nil) 190 | if err != nil { 191 | return nil, resp, err 192 | } 193 | return &ResponseNoContent{}, resp, nil 194 | } 195 | 196 | // Kickout implements `POST /channel.kickout` 197 | func (c *ChannelService) Kickout(ctx context.Context, opt *ChannelKickOptions) (*ResponseNoContent, *http.Response, error) { 198 | req, err := c.client.newRequest("POST", "channel.kickout", opt) 199 | if err != nil { 200 | return nil, nil, err 201 | } 202 | 203 | resp, err := c.client.do(ctx, req, nil) 204 | if err != nil { 205 | return nil, resp, err 206 | } 207 | return &ResponseNoContent{}, resp, nil 208 | } 209 | -------------------------------------------------------------------------------- /openapi/message.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type MessageKey string 10 | 11 | type MessageSubtype string 12 | 13 | const ( 14 | MessageSubtypeNormal MessageSubtype = "normal" 15 | MessageSubtypeInfo = "info" 16 | ) 17 | 18 | type MessageAttachmentImage struct { 19 | Url *string `json:"url,omitempty"` 20 | } 21 | 22 | type MessageAttachment struct { 23 | Title *string `json:"title,omitempty"` 24 | Text *string `json:"text,omitempty"` 25 | Color *string `json:"color,omitempty"` 26 | Images []MessageAttachmentImage `json:"images,omitempty"` 27 | } 28 | 29 | type Reaction struct { 30 | CreatedTS *VChannelTS `json:"created_ts,omitempty"` 31 | Reaction *string `json:"reaction,omitempty"` 32 | UIDs []string `json:"uids,omitempty"` 33 | } 34 | 35 | type Repost struct { 36 | UID *string `json:"uid,omitempty"` 37 | VchannelID *string `json:"vchannel_id,omitempty"` 38 | RobotID *string `json:"robot_id,omitempty"` 39 | CreatedTS *VChannelTS `json:"created_ts,omitempty"` 40 | MessageKey *MessageKey `json:"message_key,omitempty"` 41 | ID *string `json:"id,omitempty"` 42 | TeamID *string `json:"team_id,omitempty"` 43 | Subtype *MessageSubtype `json:"subtype,omitempty"` 44 | Text *string `json:"text,omitempty"` 45 | } 46 | 47 | type File struct { 48 | Inactive *bool `json:"inactive,omitempty"` 49 | Description *string `json:"description,omitempty"` 50 | Category *string `json:"category,omitempty"` 51 | Deleted *bool `json:"deleted,omitempty"` 52 | StarID *string `json:"star_id,omitempty"` 53 | Key *MessageKey `json:"key,omitempty"` 54 | Updated *Time `json:"updated,omitempty"` 55 | UID *string `json:"uid,omitempty"` 56 | VCIDS []string `json:"vcids,omitempty"` 57 | Name *string `json:"name,omitempty"` 58 | Type *string `json:"type,omitempty"` 59 | Created *Time `json:"created,omitempty"` 60 | Source *string `json:"source,omitempty"` 61 | Mime *string `json:"mime,omitempty"` 62 | ImageURL *string `json:"image_url,omitempty"` 63 | PreviewURL *string `json:"preview_url,omitempty"` 64 | Title *string `json:"title,omitempty"` 65 | ChannelID *string `json:"channel_id,omitempty"` 66 | Summary *string `json:"summary,omitempty"` 67 | IsPublic *bool `json:"is_public,omitempty"` 68 | ID *string `json:"id,omitempty"` 69 | URL *string `json:"url,omitempty"` 70 | TeamID *string `json:"team_id,omitempty"` 71 | } 72 | 73 | type Message struct { 74 | Repost *Repost `json:"repost,omitempty"` 75 | Key *MessageKey `json:"key,omitempty"` 76 | Updated *Time `json:"updated,omitempty"` 77 | UID *string `json:"uid,omitempty"` 78 | Created *Time `json:"created,omitempty"` 79 | VchannelID *string `json:"vchannel_id,omitempty"` 80 | ReferKey *string `json:"refer_key,omitempty"` 81 | RobotID *string `json:"robot_id,omitempty"` 82 | Edited *bool `json:"edited,omitempty"` 83 | CreatedTS *VChannelTS `json:"created_ts,omitempty"` 84 | PinID *string `json:"pin_id,omitempty"` 85 | StarID *string `json:"star_id,omitempty"` 86 | ID *string `json:"id,omitempty"` 87 | TeamID *string `json:"team_id,omitempty"` 88 | TextI18n *map[string]string `json:"text_i18n,omitempty"` 89 | Reactions []Reaction `json:"reactions,omitempty"` 90 | Subtype *MessageSubtype `json:"subtype,omitempty"` 91 | Text *string `json:"text,omitempty"` 92 | DisableMarkdown *bool `json:"disable_markdown,omitempty"` 93 | File *File `json:"file,omitempty"` 94 | } 95 | 96 | type MessageService service 97 | 98 | type MessageInfoOptions struct { 99 | VChannelID string 100 | Key MessageKey 101 | } 102 | 103 | // Info implements `GET /message.info` 104 | func (m *MessageService) Info(ctx context.Context, opt *MessageInfoOptions) (*Message, *http.Response, error) { 105 | endpoint := fmt.Sprintf("message.info?vchannel_id=%s&message_key=%s", opt.VChannelID, opt.Key) 106 | req, err := m.client.newRequest("GET", endpoint, nil) 107 | if err != nil { 108 | return nil, nil, err 109 | } 110 | 111 | var message Message 112 | resp, err := m.client.do(ctx, req, &message) 113 | if err != nil { 114 | return nil, resp, err 115 | } 116 | return &message, resp, nil 117 | } 118 | 119 | type MessageCreateOptions struct { 120 | VChannelID string `json:"vchannel_id"` 121 | Text string `json:"text"` 122 | Attachments []MessageAttachment `json:"attachments"` 123 | } 124 | 125 | // Create implements `POST /message.create` 126 | func (m *MessageService) Create(ctx context.Context, opt *MessageCreateOptions) (*Message, *http.Response, error) { 127 | if opt.Attachments == nil { 128 | opt.Attachments = []MessageAttachment{} 129 | } 130 | req, err := m.client.newRequest("POST", "message.create", opt) 131 | if err != nil { 132 | return nil, nil, err 133 | } 134 | 135 | var message Message 136 | resp, err := m.client.do(ctx, req, &message) 137 | if err != nil { 138 | return nil, resp, err 139 | } 140 | return &message, resp, nil 141 | } 142 | 143 | type MessageDeleteOptions struct { 144 | VChannelID string `json:"vchannel_id"` 145 | Key MessageKey `json:"message_key"` 146 | } 147 | 148 | // Delete implements `POST /message.delete` 149 | func (m *MessageService) Delete(ctx context.Context, opt *MessageDeleteOptions) (*ResponseNoContent, *http.Response, error) { 150 | req, err := m.client.newRequest("POST", "message.delete", opt) 151 | if err != nil { 152 | return nil, nil, err 153 | } 154 | 155 | resp, err := m.client.do(ctx, req, nil) 156 | if err != nil { 157 | return nil, resp, err 158 | } 159 | return &ResponseNoContent{}, resp, nil 160 | } 161 | 162 | type MessageUpdateTextOptions struct { 163 | VChannelID string `json:"vchannel_id"` 164 | Key MessageKey `json:"message_key"` 165 | Text string `json:"text"` 166 | } 167 | 168 | //UpdateText implements `POST /message.update_text` 169 | func (m *MessageService) UpdateText(ctx context.Context, opt *MessageUpdateTextOptions) (*Message, *http.Response, error) { 170 | req, err := m.client.newRequest("PATCH", "message.update_text", opt) 171 | if err != nil { 172 | return nil, nil, err 173 | } 174 | 175 | var message Message 176 | resp, err := m.client.do(ctx, req, &message) 177 | if err != nil { 178 | return nil, resp, err 179 | } 180 | return &message, resp, nil 181 | } 182 | 183 | type MessageForwardOptions struct { 184 | VChannelID string `json:"vchannel_id"` 185 | Key MessageKey `json:"message_key"` 186 | ToVChannelID string `json:"to_vchannel_id"` 187 | } 188 | 189 | // Forward implements `POST /message.forward` 190 | func (m *MessageService) Forward(ctx context.Context, opt *MessageForwardOptions) (*Message, *http.Response, error) { 191 | req, err := m.client.newRequest("POST", "message.forward", opt) 192 | if err != nil { 193 | return nil, nil, err 194 | } 195 | 196 | var message Message 197 | resp, err := m.client.do(ctx, req, &message) 198 | if err != nil { 199 | return nil, resp, err 200 | } 201 | return &message, resp, nil 202 | } 203 | -------------------------------------------------------------------------------- /rtm_message_test.go: -------------------------------------------------------------------------------- 1 | package bearychat 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestRTMMessage_Type(t *testing.T) { 9 | cases := [][]RTMMessageType{ 10 | {RTMMessageTypeUnknown, RTMMessageTypeUnknown}, 11 | {RTMMessageTypePing, RTMMessageTypePing}, 12 | {RTMMessageTypePong, RTMMessageTypePong}, 13 | {RTMMessageTypeReply, RTMMessageTypeReply}, 14 | {RTMMessageTypeOk, RTMMessageTypeOk}, 15 | {RTMMessageTypeP2PMessage, RTMMessageTypeP2PMessage}, 16 | {RTMMessageTypeP2PTyping, RTMMessageTypeP2PTyping}, 17 | {RTMMessageTypeChannelMessage, RTMMessageTypeChannelMessage}, 18 | {RTMMessageTypeChannelTyping, RTMMessageTypeChannelTyping}, 19 | {RTMMessageTypeUpdateUserConnection, RTMMessageTypeUpdateUserConnection}, 20 | } 21 | 22 | for _, c := range cases { 23 | m := RTMMessage{"type": c[0]} 24 | if m.Type() != c[1] { 25 | t.Errorf("expected type: %s, got: %s", c[0], m.Type()) 26 | } 27 | } 28 | } 29 | 30 | func TestRTMMessage_IsP2P(t *testing.T) { 31 | cases := []struct { 32 | mt RTMMessageType 33 | expected bool 34 | }{ 35 | {RTMMessageTypeUnknown, false}, 36 | {RTMMessageTypePing, false}, 37 | {RTMMessageTypePong, false}, 38 | {RTMMessageTypeReply, false}, 39 | {RTMMessageTypeOk, false}, 40 | {RTMMessageTypeP2PMessage, true}, 41 | {RTMMessageTypeP2PTyping, true}, 42 | {RTMMessageTypeChannelMessage, false}, 43 | {RTMMessageTypeChannelTyping, false}, 44 | {RTMMessageTypeUpdateUserConnection, false}, 45 | } 46 | 47 | for _, c := range cases { 48 | m := RTMMessage{"type": c.mt} 49 | if m.IsP2P() != c.expected { 50 | t.Errorf("expected: %+v, got: %+v", c.expected, m.IsP2P()) 51 | } 52 | } 53 | } 54 | 55 | func TestRTMMessage_IsChatMessage(t *testing.T) { 56 | cases := []struct { 57 | mt RTMMessageType 58 | expected bool 59 | }{ 60 | {RTMMessageTypeUnknown, false}, 61 | {RTMMessageTypePing, false}, 62 | {RTMMessageTypePong, false}, 63 | {RTMMessageTypeReply, false}, 64 | {RTMMessageTypeOk, false}, 65 | {RTMMessageTypeP2PMessage, true}, 66 | {RTMMessageTypeP2PTyping, false}, 67 | {RTMMessageTypeChannelMessage, true}, 68 | {RTMMessageTypeChannelTyping, false}, 69 | {RTMMessageTypeUpdateUserConnection, false}, 70 | } 71 | 72 | for _, c := range cases { 73 | m := RTMMessage{"type": c.mt} 74 | if m.IsChatMessage() != c.expected { 75 | t.Errorf("expected: %+v, got: %+v", c.expected, m.IsChatMessage()) 76 | } 77 | } 78 | } 79 | 80 | func TestRTMMessage_IsFrom(t *testing.T) { 81 | uid := "1" 82 | user := User{Id: uid} 83 | var m RTMMessage 84 | 85 | m = RTMMessage{"uid": uid} 86 | if !m.IsFromUser(user) { 87 | t.Errorf("expected from user: %+v", m) 88 | } 89 | if !m.IsFromUID(uid) { 90 | t.Errorf("expected from uid: %+v", m) 91 | } 92 | 93 | m = RTMMessage{"uid": uid + "1"} 94 | if m.IsFromUser(user) { 95 | t.Errorf("unexpected from user: %+v", m) 96 | } 97 | if m.IsFromUID(uid) { 98 | t.Errorf("expected from uid: %+v", m) 99 | } 100 | } 101 | 102 | func TestRTMMessage_Refer_ChannelMessage(t *testing.T) { 103 | m := RTMMessage{ 104 | "type": RTMMessageTypeChannelMessage, 105 | "channel_id": "foobar", 106 | "vchannel_id": "foobar", 107 | "key": "foobar", 108 | } 109 | 110 | referText := "foobar" 111 | refer := m.Refer(referText) 112 | if refer["text"] != referText { 113 | t.Errorf("unexpected %s", refer["text"]) 114 | } 115 | if refer.Type() != RTMMessageTypeChannelMessage { 116 | t.Errorf("unexpected %s", refer.Type()) 117 | } 118 | if refer["channel_id"] != m["channel_id"] { 119 | t.Errorf("unexpected %s", refer["channel_id"]) 120 | } 121 | if refer["vchannel_id"] != m["vchannel_id"] { 122 | t.Errorf("unexpected %s", refer["vchannel_id"]) 123 | } 124 | if refer["refer_key"] != m["key"] { 125 | t.Errorf("unexpected %s", refer["refer_key"]) 126 | } 127 | } 128 | 129 | func TestRTMMessage_Refer_P2PMessage(t *testing.T) { 130 | m := RTMMessage{ 131 | "type": RTMMessageTypeP2PMessage, 132 | "uid": "foobar", 133 | "vchannel_id": "foobar", 134 | "key": "foobar", 135 | } 136 | 137 | referText := "foobar" 138 | refer := m.Refer(referText) 139 | if refer["text"] != referText { 140 | t.Errorf("unexpected %s", refer["text"]) 141 | } 142 | if refer.Type() != RTMMessageTypeP2PMessage { 143 | t.Errorf("unexpected %s", refer.Type()) 144 | } 145 | if refer["to_uid"] != m["uid"] { 146 | t.Errorf("unexpected %s", refer["to_uid"]) 147 | } 148 | if refer["vchannel_id"] != m["vchannel_id"] { 149 | t.Errorf("unexpected %s", refer["vchannel_id"]) 150 | } 151 | if refer["refer_key"] != m["key"] { 152 | t.Errorf("unexpected %s", refer["refer_key"]) 153 | } 154 | } 155 | 156 | func TestRTMMessage_Reply_ChannelMessage(t *testing.T) { 157 | m := RTMMessage{ 158 | "type": RTMMessageTypeChannelMessage, 159 | "channel_id": "foobar", 160 | "vchannel_id": "foobar", 161 | } 162 | 163 | replyText := "foobar" 164 | reply := m.Reply(replyText) 165 | if reply["text"] != replyText { 166 | t.Errorf("unexpected %s", reply["text"]) 167 | } 168 | if reply.Type() != RTMMessageTypeChannelMessage { 169 | t.Errorf("unexpected %s", reply.Type()) 170 | } 171 | if reply["channel_id"] != m["channel_id"] { 172 | t.Errorf("unexpected %s", reply["channel_id"]) 173 | } 174 | if reply["vchannel_id"] != m["vchannel_id"] { 175 | t.Errorf("unexpected %s", reply["vchannel_id"]) 176 | } 177 | } 178 | 179 | func TestRTMMessage_Reply_P2PMessage(t *testing.T) { 180 | m := RTMMessage{ 181 | "type": RTMMessageTypeChannelMessage, 182 | "channel_id": "foobar", 183 | "vchannel_id": "foobar", 184 | } 185 | 186 | replyText := "foobar" 187 | reply := m.Reply(replyText) 188 | if reply["text"] != replyText { 189 | t.Errorf("unexpected %s", reply["text"]) 190 | } 191 | if reply.Type() != RTMMessageTypeChannelMessage { 192 | t.Errorf("unexpected %s", reply.Type()) 193 | } 194 | if reply["to_uid"] != m["uid"] { 195 | t.Errorf("unexpected %s", reply["to_uid"]) 196 | } 197 | if reply["vchannel_id"] != m["vchannel_id"] { 198 | t.Errorf("unexpected %s", reply["vchannel_id"]) 199 | } 200 | } 201 | 202 | func TestRTMMessage_ParseMention(t *testing.T) { 203 | uid := "=1" 204 | text := "abc" 205 | user := User{Id: uid} 206 | 207 | m := RTMMessage{} 208 | var mentioned bool 209 | var content string 210 | 211 | expect := func(expectMentioned bool, expectContent string) { 212 | if mentioned != expectMentioned { 213 | t.Errorf("expected mentioned: '%v', got '%v', m: %+v", expectMentioned, mentioned, m) 214 | } 215 | if content != expectContent { 216 | t.Errorf("expected content: '%v', got '%v'", expectContent, content) 217 | } 218 | } 219 | 220 | m["text"] = text 221 | m["type"] = RTMMessageTypeP2PMessage 222 | mentioned, content = m.ParseMentionUser(user) 223 | expect(true, text) 224 | mentioned, content = m.ParseMentionUID(uid) 225 | expect(true, text) 226 | 227 | m["type"] = RTMMessageTypeChannelMessage 228 | mentioned, content = m.ParseMentionUser(user) 229 | expect(false, text) 230 | mentioned, content = m.ParseMentionUID(uid) 231 | expect(false, text) 232 | 233 | m["text"] = fmt.Sprintf("@<=%s=> %s", uid, text) 234 | mentioned, content = m.ParseMentionUser(user) 235 | expect(true, text) 236 | mentioned, content = m.ParseMentionUID(uid) 237 | expect(true, text) 238 | 239 | m["text"] = fmt.Sprintf("123123123 12312 123@<=%s=> %s", uid, text) 240 | mentioned, content = m.ParseMentionUser(user) 241 | expect(true, text) 242 | mentioned, content = m.ParseMentionUID(uid) 243 | expect(true, text) 244 | 245 | m["text"] = fmt.Sprintf("@<=%s=>", uid) 246 | mentioned, content = m.ParseMentionUser(user) 247 | expect(false, m.Text()) 248 | mentioned, content = m.ParseMentionUID(uid) 249 | expect(false, m.Text()) 250 | 251 | m["text"] = fmt.Sprintf("@<=%s=> ", uid) 252 | mentioned, content = m.ParseMentionUser(user) 253 | expect(true, "") 254 | mentioned, content = m.ParseMentionUID(uid) 255 | expect(true, "") 256 | 257 | m["text"] = fmt.Sprintf("@<=%s=> 你和 @<==bwOwr=> 谁聪明", uid) 258 | mentioned, content = m.ParseMentionUser(user) 259 | expect(true, "你和 @<==bwOwr=> 谁聪明") 260 | mentioned, content = m.ParseMentionUID(uid) 261 | expect(true, "你和 @<==bwOwr=> 谁聪明") 262 | 263 | m["text"] = fmt.Sprintf("@<==bwOwr=> @<=%s=> hello", uid) 264 | mentioned, content = m.ParseMentionUser(user) 265 | expect(true, "hello") 266 | mentioned, content = m.ParseMentionUID(uid) 267 | expect(true, "hello") 268 | } 269 | --------------------------------------------------------------------------------