├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ ├── build_v1.yml
│ └── build_v2.yml
├── .gitignore
├── .ignore
├── .markdownlint.json
├── .vimrc
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── README_zhCN.md
├── api.go
├── api_auth.go
├── api_auth_test.go
├── api_bot.go
├── api_bot_test.go
├── api_buzz_message.go
├── api_buzz_message_test.go
├── api_chat.go
├── api_chat_test.go
├── api_contact.go
├── api_contact_test.go
├── api_group.go
├── api_group_test.go
├── api_message.go
├── api_message_test.go
├── api_notification.go
├── api_notification_test.go
├── api_upload.go
├── api_upload_test.go
├── crypto.go
├── crypto_test.go
├── emoji.go
├── error.go
├── event.go
├── event_bot_added.go
├── event_bot_deleted.go
├── event_chat_disbanded.go
├── event_message_reaction_created.go
├── event_message_reaction_deleted.go
├── event_message_read.go
├── event_message_recalled.go
├── event_message_received.go
├── event_user_added.go
├── event_user_deleted.go
├── event_v1.go
├── event_v1_test.go
├── event_v2.go
├── event_v2_test.go
├── fixtures
├── test.jpg
└── test.pdf
├── go.mod
├── go.sum
├── http.go
├── http_test.go
├── http_wrapper.go
├── lark.go
├── lark_test.go
├── locale.go
├── logger.go
├── logger_test.go
├── message.go
├── message_v1.go
├── message_v2.go
├── msg_buf.go
├── msg_buf_test.go
├── msg_card_builder.go
├── msg_card_builder_test.go
├── msg_post_builder.go
├── msg_post_builder_test.go
├── msg_template_builder.go
├── msg_template_builder_test.go
├── msg_text_builder.go
├── msg_text_builder_test.go
├── scripts
├── test_v1.sh
└── test_v2.sh
├── user.go
├── user_test.go
├── util.go
├── util_test.go
└── v2
├── README.md
├── api.go
├── api_auth.go
├── api_auth_test.go
├── api_bot.go
├── api_bot_test.go
├── api_buzz_message.go
├── api_buzz_message_test.go
├── api_chat.go
├── api_chat_test.go
├── api_contact.go
├── api_contact_test.go
├── api_message.go
├── api_message_test.go
├── api_notification.go
├── api_notification_test.go
├── crypto.go
├── crypto_test.go
├── emoji.go
├── error.go
├── event.go
├── event_bot_added.go
├── event_bot_deleted.go
├── event_challenge.go
├── event_chat_disbanded.go
├── event_message_reaction_created.go
├── event_message_reaction_deleted.go
├── event_message_read.go
├── event_message_recalled.go
├── event_message_received.go
├── event_test.go
├── event_user_added.go
├── event_user_deleted.go
├── go.mod
├── go.sum
├── http.go
├── http_client.go
├── http_test.go
├── lark.go
├── lark_test.go
├── locale.go
├── logger.go
├── logger_test.go
├── message.go
├── message_builder.go
├── msg_buf.go
├── msg_buf_test.go
├── msg_card_builder.go
├── msg_card_builder_test.go
├── msg_post_builder.go
├── msg_post_builder_test.go
├── msg_template_builder.go
├── msg_template_builder_test.go
├── msg_text_builder.go
├── msg_text_builder_test.go
├── user.go
├── user_test.go
├── util.go
└── util_test.go
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | [*.go]
8 | charset = utf-8
9 | indent_style = tab
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: https://www.buymeacoffee.com/crispgm
2 |
--------------------------------------------------------------------------------
/.github/workflows/build_v1.yml:
--------------------------------------------------------------------------------
1 | name: build_v1
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths-ignore:
7 | - 'v2/**'
8 | pull_request:
9 | paths-ignore:
10 | - 'v2/**'
11 | jobs:
12 | checks:
13 | name: run
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: checkout
17 | uses: actions/checkout@v3
18 |
19 | - name: cache
20 | uses: actions/cache@v3
21 | with:
22 | path: |
23 | ~/.cache/go-build
24 | ~/go/pkg/mod
25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
26 | restore-keys: |
27 | ${{ runner.os }}-go-
28 |
29 | - name: setup
30 | uses: actions/setup-go@v3
31 | with:
32 | go-version: '1.13.0'
33 |
34 | - name: lint
35 | uses: morphy2k/revive-action@v2
36 |
37 | - name: build
38 | run: ./scripts/test_v1.sh
39 | env:
40 | LARK_APP_ID: ${{ secrets.LARK_APP_ID }}
41 | LARK_APP_SECRET: ${{ secrets.LARK_APP_SECRET }}
42 | LARK_USER_EMAIL: ${{ secrets.LARK_USER_EMAIL }}
43 | LARK_CHAT_ID: ${{ secrets.LARK_CHAT_ID }}
44 | LARK_OPEN_ID: ${{ secrets.LARK_OPEN_ID }}
45 | LARK_USER_ID: ${{ secrets.LARK_USER_ID }}
46 | LARK_UNION_ID: ${{ secrets.LARK_UNION_ID }}
47 | LARK_MESSAGE_ID: ${{ secrets.LARK_MESSAGE_ID }}
48 | LARK_WEBHOOK_V1: ${{ secrets.LARK_WEBHOOK_V1 }}
49 | LARK_WEBHOOK_V2: ${{ secrets.LARK_WEBHOOK_V2 }}
50 | LARK_WEBHOOK_V2_SIGNED: ${{ secrets.LARK_WEBHOOK_V2_SIGNED }}
51 |
52 | - name: codecov
53 | uses: codecov/codecov-action@v1
54 | with:
55 | token: ${{ secrets.CODECOV_TOKEN }}
56 |
--------------------------------------------------------------------------------
/.github/workflows/build_v2.yml:
--------------------------------------------------------------------------------
1 | name: build_v2
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - 'v2/**'
8 | pull_request:
9 | paths:
10 | - 'v2/**'
11 | jobs:
12 | checks:
13 | name: run
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: checkout
17 | uses: actions/checkout@v3
18 |
19 | - name: cache
20 | uses: actions/cache@v3
21 | with:
22 | path: |
23 | ~/.cache/go-build
24 | ~/go/pkg/mod
25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
26 | restore-keys: |
27 | ${{ runner.os }}-go-
28 |
29 | - name: setup
30 | uses: actions/setup-go@v3
31 | with:
32 | go-version: '1.24.0'
33 |
34 | - name: lint
35 | uses: morphy2k/revive-action@v2
36 |
37 | - name: build
38 | run: ./scripts/test_v2.sh
39 | env:
40 | LARK_APP_ID: ${{ secrets.LARK_APP_ID }}
41 | LARK_APP_SECRET: ${{ secrets.LARK_APP_SECRET }}
42 | LARK_USER_EMAIL: ${{ secrets.LARK_USER_EMAIL }}
43 | LARK_CHAT_ID: ${{ secrets.LARK_CHAT_ID }}
44 | LARK_OPEN_ID: ${{ secrets.LARK_OPEN_ID }}
45 | LARK_USER_ID: ${{ secrets.LARK_USER_ID }}
46 | LARK_UNION_ID: ${{ secrets.LARK_UNION_ID }}
47 | LARK_MESSAGE_ID: ${{ secrets.LARK_MESSAGE_ID }}
48 | LARK_WEBHOOK: ${{ secrets.LARK_WEBHOOK_V2 }}
49 | LARK_WEBHOOK_SIGNED: ${{ secrets.LARK_WEBHOOK_V2_SIGNED }}
50 |
51 | - name: codecov
52 | uses: codecov/codecov-action@v1
53 | with:
54 | token: ${{ secrets.CODECOV_TOKEN }}
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage.txt
2 | profile.out
3 | .env
4 | .idea
5 |
--------------------------------------------------------------------------------
/.ignore:
--------------------------------------------------------------------------------
1 | go.mod
2 | go.sum
3 | *.md
4 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "no-inline-html": false,
3 | "line-length": false
4 | }
5 |
--------------------------------------------------------------------------------
/.vimrc:
--------------------------------------------------------------------------------
1 | lua << EOF
2 | require('go').setup({
3 | test_flags = { '-v', '-count=1' },
4 | test_env = {GO_LARK_TEST_MODE = 'local'},
5 | test_popup_width = 120,
6 | test_open_cmd = 'tabedit',
7 | tags_options = {'-sort'},
8 | tags_transform = 'camelcase',
9 | })
10 | EOF
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "go.testFlags": [
3 | "-count=1",
4 | "-v"
5 | ],
6 | "go.testEnvVars": {
7 | "GO_LARK_TEST_MODE": "local",
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## v1.16.0
4 |
5 | - feat(base): add base error (#78)
6 | - feat(im): support buzz message (#77)
7 |
8 | ## v1.15.0
9 |
10 | - feat(im): support card template (#75)
11 |
12 | ## v1.14.1
13 |
14 | - feat(api): UploadFile support uploads binary file
15 |
16 | ## v1.14.0
17 |
18 | - refactor(im): build reply and update message with standalone methods
19 | - feat(im): UpdateMessage supports text and post in addition to card
20 | - feat(im): ReplyMessage supports reply in thread
21 |
22 | ## v1.13.3
23 |
24 | - feat(event): support message_recalled, message_reaction_created, and message_reaction_deleted
25 |
26 | ## v1.13.2
27 |
28 | - fix(event): chat_id and message_id
29 |
30 | ## v1.13.1
31 |
32 | - feat(event): support card callback (#68)
33 |
34 | ## v1.13.0
35 |
36 | - feat(contact): support user info (#67)
37 |
38 | ## v1.12.0
39 |
40 | - feat(im): support forward message
41 | - feat(im): change reaction API names (#66)
42 |
43 | ## v1.11.0
44 |
45 | - feat(im): support reactions (#62)
46 | - ci: run tests under private tenant to avoid applying permissions (#65)
47 |
48 | ## v1.10.2
49 |
50 | - fix(notification): remove chat_id and uid_type from outcoming message (#63)
51 | - fix(notification): timestamp should be string
52 | - feat(im): drop v1 update_multi
53 |
54 | ## v1.10.1
55 |
56 | - feat(chat): support chat list and chat (#60)
57 |
58 | ## v1.10.0
59 |
60 | - feat(im): support card with i18n (#59)
61 |
62 | ## v1.9.0
63 |
64 | - feat(im): support column set (#57)
65 |
66 | ## v1.8.0
67 |
68 | - feat(im): IMMessage use Sender rather than Sendor (#54)
69 | - feat(chat): set and delete notice (#53)
70 | - feat(im): pin and unpin message (#47)
71 | - fix: update chat avatar field (#46)
72 | - feat(event): add more events (#38)
73 | - fix(sign): fix missing timestamp in signed message
74 |
75 | ## v1.7.4
76 |
77 | - feat(message): support UUID (#40)
78 |
79 | ## v1.7.3
80 |
81 | - feat(notification): support sign
82 |
83 | ## v1.7.2
84 |
85 | - fix(http): remove shared context for requests
86 | - feat: improve heartbeat
87 | - feat(notification): allow update url
88 |
89 | ## v1.7.1
90 |
91 | - feat(card): support update_multi in config
92 |
93 | ## v1.7.0
94 |
95 | - feat(message): support update message card (#31)
96 | - feat(chat): add more chat APIs (#29)
97 | - feat: support event v2 (#4)
98 | - fix(chat): allow set user id type
99 | - feat: api im (#19)
100 |
101 | ## v1.6.1
102 |
103 | - docs: add extension guide [ci skip] (#18)
104 |
105 | ## v1.6.0
106 |
107 | - chore: recall message uses base response
108 | - feat: delete ephemeral message
109 | - feat: add ephemeral card
110 | - docs: add goreportcard [ci skip]
111 |
112 | ## v1.5.0
113 |
114 | - fix: div.text, option.text & tests (#13)
115 | - feat: card builder (#12)
116 | - feat(auth): make heartbeat thread safe (#10)
117 | - chore: add editorconfig [ci skip]
118 | - ci: switch to revive
119 |
120 | ## v1.4.2
121 |
122 | - feat: add domain method
123 |
124 | ## v1.4.1
125 |
126 | - fix: should pass http header
127 | - feat: add context to logger & client (#9)
128 |
129 | ## v1.3.0
130 |
131 | - refactor: better logger interface (#8)
132 |
133 | ## v1.2.1
134 |
135 | - fix: http custom client test
136 | - refactor: improve http wrapper (#7)
137 |
138 | ## v1.1.0
139 |
140 | - feat: open more api (#6)
141 | - feat(message/post): patch i18n support
142 | - docs: add godoc badge [ci skip]
143 | - ci: add codecov (#2)
144 | - chore: add .vimrc [ci skip]
145 | - feat: add alternative domains
146 |
147 | ## v1.0.1
148 |
149 | - feat: drop api user v4
150 |
151 | ## v1.0.0
152 |
153 | - feat: init go-lark/lark oss version
154 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2023 go-lark
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 |
--------------------------------------------------------------------------------
/api.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // BaseResponse of an API
4 | type BaseResponse struct {
5 | Code int `json:"code"`
6 | Msg string `json:"msg"`
7 | Error BaseError `json:"error"`
8 | }
9 |
10 | // BaseError returned by the platform
11 | type BaseError struct {
12 | LogID string `json:"log_id,omitempty"`
13 | }
14 |
15 | // I18NNames .
16 | type I18NNames struct {
17 | ZhCN string `json:"zh_cn,omitempty"`
18 | EnUS string `json:"en_us,omitempty"`
19 | JaJP string `json:"ja_jp,omitempty"`
20 | }
21 |
22 | // WithUserIDType .
23 | func (bot *Bot) WithUserIDType(userIDType string) *Bot {
24 | bot.userIDType = userIDType
25 | return bot
26 | }
27 |
--------------------------------------------------------------------------------
/api_auth.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "sync/atomic"
5 | "time"
6 | )
7 |
8 | // URLs for auth
9 | const (
10 | appAccessTokenInternalURL = "/open-apis/auth/v3/app_access_token/internal"
11 | tenantAppAccessTokenInternalURL = "/open-apis/auth/v3/tenant_access_token/internal/"
12 | )
13 |
14 | // AuthTokenInternalResponse .
15 | type AuthTokenInternalResponse struct {
16 | BaseResponse
17 | AppAccessToken string `json:"app_access_token"`
18 | Expire int `json:"expire"`
19 | }
20 |
21 | // TenantAuthTokenInternalResponse .
22 | type TenantAuthTokenInternalResponse struct {
23 | BaseResponse
24 | TenantAppAccessToken string `json:"tenant_access_token"`
25 | Expire int `json:"expire"`
26 | }
27 |
28 | // GetAccessTokenInternal gets AppAccessToken for internal use
29 | func (bot *Bot) GetAccessTokenInternal(updateToken bool) (*AuthTokenInternalResponse, error) {
30 | if !bot.requireType(ChatBot) {
31 | return nil, ErrBotTypeError
32 | }
33 |
34 | params := map[string]interface{}{
35 | "app_id": bot.appID,
36 | "app_secret": bot.appSecret,
37 | }
38 | var respData AuthTokenInternalResponse
39 | err := bot.PostAPIRequest("GetAccessTokenInternal", appAccessTokenInternalURL, false, params, &respData)
40 | if err == nil && updateToken {
41 | bot.accessToken.Store(respData.AppAccessToken)
42 | }
43 | return &respData, err
44 | }
45 |
46 | // GetTenantAccessTokenInternal gets AppAccessToken for internal use
47 | func (bot *Bot) GetTenantAccessTokenInternal(updateToken bool) (*TenantAuthTokenInternalResponse, error) {
48 | if !bot.requireType(ChatBot) {
49 | return nil, ErrBotTypeError
50 | }
51 |
52 | params := map[string]interface{}{
53 | "app_id": bot.appID,
54 | "app_secret": bot.appSecret,
55 | }
56 | var respData TenantAuthTokenInternalResponse
57 | err := bot.PostAPIRequest("GetTenantAccessTokenInternal", tenantAppAccessTokenInternalURL, false, params, &respData)
58 | if err == nil && updateToken {
59 | bot.tenantAccessToken.Store(respData.TenantAppAccessToken)
60 | }
61 | return &respData, err
62 | }
63 |
64 | // StopHeartbeat stop auto-renew
65 | func (bot *Bot) StopHeartbeat() {
66 | bot.heartbeat <- true
67 | }
68 |
69 | // StartHeartbeat renew auth token periodically
70 | func (bot *Bot) StartHeartbeat() error {
71 | return bot.startHeartbeat(10 * time.Second)
72 | }
73 |
74 | func (bot *Bot) startHeartbeat(defaultInterval time.Duration) error {
75 | if !bot.requireType(ChatBot) {
76 | return ErrBotTypeError
77 | }
78 |
79 | // First initialize the token in blocking mode
80 | _, err := bot.GetTenantAccessTokenInternal(true)
81 | if err != nil {
82 | bot.httpErrorLog("Heartbeat", "failed to get tenant access token", err)
83 | return err
84 | }
85 | atomic.AddInt64(&bot.heartbeatCounter, 1)
86 |
87 | interval := defaultInterval
88 | bot.heartbeat = make(chan bool)
89 | go func() {
90 | for {
91 | t := time.NewTimer(interval)
92 | select {
93 | case <-bot.heartbeat:
94 | return
95 | case <-t.C:
96 | interval = defaultInterval
97 | resp, err := bot.GetTenantAccessTokenInternal(true)
98 | if err != nil {
99 | bot.httpErrorLog("Heartbeat", "failed to get tenant access token", err)
100 | }
101 | atomic.AddInt64(&bot.heartbeatCounter, 1)
102 | if resp != nil && resp.Expire-20 > 0 {
103 | interval = time.Duration(resp.Expire-20) * time.Second
104 | }
105 | }
106 | }
107 | }()
108 | return nil
109 | }
110 |
--------------------------------------------------------------------------------
/api_auth_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestAuthAccessTokenInternal(t *testing.T) {
11 | bot := newTestBot()
12 | resp, err := bot.GetAccessTokenInternal(true)
13 | if assert.NoError(t, err) {
14 | assert.Equal(t, 0, resp.Code)
15 | assert.NotEmpty(t, resp.AppAccessToken)
16 | t.Log(resp.AppAccessToken)
17 | assert.NotEmpty(t, resp.Expire)
18 | }
19 | }
20 |
21 | func TestAuthTenantAccessTokenInternal(t *testing.T) {
22 | bot := newTestBot()
23 | resp, err := bot.GetTenantAccessTokenInternal(true)
24 | if assert.NoError(t, err) {
25 | assert.Equal(t, 0, resp.Code)
26 | assert.NotEmpty(t, resp.TenantAppAccessToken)
27 | t.Log(resp.TenantAppAccessToken)
28 | assert.NotEmpty(t, resp.Expire)
29 | }
30 | }
31 |
32 | func TestHeartbeat(t *testing.T) {
33 | bot := newTestBot()
34 | assert.Nil(t, bot.heartbeat)
35 | assert.Nil(t, bot.startHeartbeat(time.Second*1))
36 | assert.NotEmpty(t, bot.tenantAccessToken)
37 | assert.Equal(t, int64(1), bot.heartbeatCounter)
38 | time.Sleep(2 * time.Second)
39 | assert.Equal(t, int64(2), bot.heartbeatCounter)
40 | bot.StopHeartbeat()
41 | time.Sleep(2 * time.Second)
42 | assert.Equal(t, int64(2), bot.heartbeatCounter)
43 | // restart heartbeat
44 | assert.Nil(t, bot.startHeartbeat(time.Second*1))
45 | time.Sleep(2 * time.Second)
46 | assert.Equal(t, int64(4), bot.heartbeatCounter)
47 | }
48 |
49 | func TestInvalidHeartbeat(t *testing.T) {
50 | bot := NewNotificationBot("")
51 | err := bot.StartHeartbeat()
52 | assert.Error(t, err, ErrBotTypeError)
53 | }
54 |
--------------------------------------------------------------------------------
/api_bot.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | const (
4 | getBotInfoURL = "/open-apis/bot/v3/info/"
5 | )
6 |
7 | // GetBotInfoResponse .
8 | type GetBotInfoResponse struct {
9 | BaseResponse
10 | Bot struct {
11 | ActivateStatus int `json:"activate_status"`
12 | AppName string `json:"app_name"`
13 | AvatarURL string `json:"avatar_url"`
14 | IPWhiteList []string `json:"ip_white_list"`
15 | OpenID string `json:"open_id"`
16 | } `json:"bot"`
17 | }
18 |
19 | // GetBotInfo returns bot info
20 | func (bot *Bot) GetBotInfo() (*GetBotInfoResponse, error) {
21 | var respData GetBotInfoResponse
22 | err := bot.PostAPIRequest("GetBotInfo", getBotInfoURL, true, nil, &respData)
23 | return &respData, err
24 | }
25 |
--------------------------------------------------------------------------------
/api_bot_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGetBotInfo(t *testing.T) {
10 | resp, err := bot.GetBotInfo()
11 | if assert.NoError(t, err) {
12 | assert.Equal(t, 0, resp.Code)
13 | assert.Equal(t, "go-lark-bot", resp.Bot.AppName)
14 | t.Log(resp)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/api_buzz_message.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import "fmt"
4 |
5 | const (
6 | buzzInAppURL = "/open-apis/im/v1/messages/%s/urgent_app?user_id_type=%s"
7 | buzzSMSURL = "/open-apis/im/v1/messages/%s/urgent_sms?user_id_type=%s"
8 | buzzPhoneURL = "/open-apis/im/v1/messages/%s/urgent_phone?user_id_type=%s"
9 | )
10 |
11 | // Buzz types
12 | const (
13 | BuzzTypeInApp = "buzz_inapp"
14 | BuzzTypeSMS = "buzz_sms"
15 | BuzzTypePhone = "buzz_phone"
16 | )
17 |
18 | // BuzzMessageResponse .
19 | type BuzzMessageResponse struct {
20 | BaseResponse
21 |
22 | Data struct {
23 | InvalidUserIDList []string `json:"invalid_user_id_list,omitempty"`
24 | } `json:"data,omitempty"`
25 | }
26 |
27 | // BuzzMessage .
28 | func (bot Bot) BuzzMessage(buzzType string, messageID string, userIDList ...string) (*BuzzMessageResponse, error) {
29 | var respData BuzzMessageResponse
30 | url := buzzInAppURL
31 | switch buzzType {
32 | case BuzzTypeInApp:
33 | url = buzzInAppURL
34 | case BuzzTypeSMS:
35 | url = buzzSMSURL
36 | case BuzzTypePhone:
37 | url = buzzPhoneURL
38 | }
39 | req := map[string][]string{
40 | "user_id_list": userIDList,
41 | }
42 | err := bot.PatchAPIRequest("BuzzMessage", fmt.Sprintf(url, messageID, bot.userIDType), true, req, &respData)
43 | return &respData, err
44 | }
45 |
--------------------------------------------------------------------------------
/api_buzz_message_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestBuzzMessage(t *testing.T) {
10 | resp, err := bot.PostText("this text will be buzzed", WithEmail(testUserEmail))
11 | if assert.NoError(t, err) {
12 | bot.WithUserIDType(UIDOpenID)
13 | messageID := resp.Data.MessageID
14 | buzzResp, err := bot.BuzzMessage(BuzzTypeInApp, messageID, testUserOpenID)
15 | if assert.NoError(t, err) {
16 | assert.Equal(t, 0, buzzResp.Code)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/api_chat_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestChatInfo(t *testing.T) {
13 | bot.WithUserIDType(UIDOpenID)
14 | assert.Equal(t, UIDOpenID, bot.userIDType)
15 | resp, err := bot.GetChat(testGroupChatID)
16 | if assert.NoError(t, err) {
17 | assert.Equal(t, 0, resp.Code)
18 | assert.Equal(t, "go-lark-ci", resp.Data.Name)
19 | assert.Equal(t, "group", resp.Data.ChatMode)
20 | assert.Equal(t, testUserOpenID, resp.Data.OwnerID)
21 | t.Log(resp.Data)
22 | }
23 | }
24 |
25 | func TestChatList(t *testing.T) {
26 | bot.WithUserIDType(UIDOpenID)
27 | assert.Equal(t, UIDOpenID, bot.userIDType)
28 | resp, err := bot.ListChat("ByCreateTimeAsc", "", 10)
29 | if assert.NoError(t, err) {
30 | assert.Equal(t, 0, resp.Code)
31 | assert.NotEmpty(t, resp.Data.Items)
32 | t.Log(resp.Data.Items[0])
33 | }
34 | }
35 |
36 | func TestChatSearch(t *testing.T) {
37 | bot.WithUserIDType(UIDOpenID)
38 | assert.Equal(t, UIDOpenID, bot.userIDType)
39 | resp, err := bot.SearchChat("go-lark", "", 10)
40 | if assert.NoError(t, err) {
41 | assert.Equal(t, 0, resp.Code)
42 | if assert.NotEmpty(t, resp.Data.Items) {
43 | for _, item := range resp.Data.Items {
44 | if !strings.Contains(item.Name, "go-lark") {
45 | t.Error(item.Name, "does not contain go-lark")
46 | }
47 | }
48 | }
49 | t.Log(resp.Data.Items)
50 | }
51 | }
52 |
53 | func TestChatCRUD(t *testing.T) {
54 | bot.WithUserIDType(UIDOpenID)
55 | resp, err := bot.CreateChat(CreateChatRequest{
56 | Name: fmt.Sprintf("go-lark-ci-%d", time.Now().Unix()),
57 | ChatMode: "group",
58 | ChatType: "public",
59 | })
60 | if assert.NoError(t, err) {
61 | chatID := resp.Data.ChatID
62 | assert.NotEmpty(t, chatID)
63 | upResp, err := bot.UpdateChat(chatID, UpdateChatRequest{
64 | Description: "new description",
65 | })
66 | t.Log(upResp)
67 | if assert.NoError(t, err) {
68 | getResp, err := bot.GetChat(chatID)
69 | if assert.NoError(t, err) {
70 | assert.Equal(t, "new description", getResp.Data.Description)
71 | // join chat
72 | joinResp, err := bot.JoinChat(chatID)
73 | assert.Zero(t, joinResp.Code)
74 | assert.NoError(t, err)
75 |
76 | // add chat member
77 | addMemberResp, err := bot.AddChatMember(chatID, []string{testUserOpenID})
78 | if assert.NoError(t, err) {
79 | assert.Equal(t, 0, addMemberResp.Code)
80 | assert.Empty(t, addMemberResp.Data.InvalidIDList)
81 | }
82 | // remove chat member
83 | removeMemberResp, err := bot.RemoveChatMember(chatID, []string{testUserOpenID})
84 | if assert.NoError(t, err) {
85 | assert.Equal(t, 0, removeMemberResp.Code)
86 | assert.Empty(t, removeMemberResp.Data.InvalidIDList)
87 | }
88 |
89 | // delete
90 | _, err = bot.DeleteChat(chatID)
91 | assert.NoError(t, err)
92 | }
93 | }
94 | }
95 | }
96 |
97 | func TestIsInChat(t *testing.T) {
98 | resp, err := bot.IsInChat(testGroupChatID)
99 | if assert.NoError(t, err) {
100 | assert.Equal(t, 0, resp.Code)
101 | assert.True(t, resp.Data.IsInChat)
102 | }
103 | }
104 |
105 | func TestGetChatMembers(t *testing.T) {
106 | bot.WithUserIDType(UIDOpenID)
107 | resp, err := bot.GetChatMembers(testGroupChatID, "", 1)
108 | if assert.NoError(t, err) {
109 | assert.Equal(t, 0, resp.Code)
110 | assert.NotEmpty(t, resp.Data.Items)
111 | assert.Empty(t, resp.Data.PageToken)
112 | assert.NotEmpty(t, resp.Data.MemberTotal)
113 | assert.False(t, resp.Data.HasMore)
114 | }
115 | }
116 |
117 | func TestChatTopNotice(t *testing.T) {
118 | resp, err := bot.PostText("group notice", WithChatID(testGroupChatID))
119 | if assert.NoError(t, err) {
120 | setResp, _ := bot.SetTopNotice(testGroupChatID, "2", resp.Data.MessageID)
121 | assert.Equal(t, 0, setResp.Code)
122 | delResp, _ := bot.DeleteTopNotice(testGroupChatID)
123 | assert.Equal(t, 0, delResp.Code)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/api_contact.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | )
7 |
8 | const (
9 | getUserInfoURL = "/open-apis/contact/v3/users/%s?user_id_type=%s"
10 | batchGetUserInfoURL = "/open-apis/contact/v3/users/batch?%s"
11 | )
12 |
13 | // GetUserInfoResponse .
14 | type GetUserInfoResponse struct {
15 | BaseResponse
16 | Data struct {
17 | User UserInfo
18 | }
19 | }
20 |
21 | // BatchGetUserInfoResponse .
22 | type BatchGetUserInfoResponse struct {
23 | BaseResponse
24 | Data struct {
25 | Items []UserInfo
26 | }
27 | }
28 |
29 | // UserInfo .
30 | type UserInfo struct {
31 | OpenID string `json:"open_id,omitempty"`
32 | Email string `json:"email,omitempty"`
33 | UserID string `json:"user_id,omitempty"`
34 | ChatID string `json:"chat_id,omitempty"`
35 | UnionID string `json:"union_id,omitempty"`
36 | Name string `json:"name,omitempty"`
37 | EnglishName string `json:"en_name,omitempty"`
38 | NickName string `json:"nickname,omitempty"`
39 | Mobile string `json:"mobile,omitempty"`
40 | MobileVisible bool `json:"mobile_visible,omitempty"`
41 | Gender int `json:"gender,omitempty"`
42 | Avatar UserAvatar `json:"avatar,omitempty"`
43 | Status UserStatus `json:"status,omitempty"`
44 | City string `json:"city,omitempty"`
45 | Country string `json:"country,omitempty"`
46 | WorkStation string `json:"work_station,omitempty"`
47 | JoinTime int `json:"join_time,omitempty"`
48 | EmployeeNo string `json:"employee_no,omitempty"`
49 | EmployeeType int `json:"employee_type,omitempty"`
50 | EnterpriseEmail string `json:"enterprise_email,omitempty"`
51 | Geo string `json:"geo,omitempty"`
52 | JobTitle string `json:"job_title,omitempty"`
53 | JobLevelID string `json:"job_level_id,omitempty"`
54 | JobFamilyID string `json:"job_family_id,omitempty"`
55 | DepartmentIDs []string `json:"department_ids,omitempty"`
56 | LeaderUserID string `json:"leader_user_id,omitempty"`
57 | IsTenantManager bool `json:"is_tenant_manager,omitempty"`
58 | }
59 |
60 | // UserAvatar .
61 | type UserAvatar struct {
62 | Avatar72 string `json:"avatar_72,omitempty"`
63 | Avatar240 string `json:"avatar_240,omitempty"`
64 | Avatar640 string `json:"avatar_640,omitempty"`
65 | AvatarOrigin string `json:"avatar_origin,omitempty"`
66 | }
67 |
68 | // UserStatus .
69 | type UserStatus struct {
70 | IsFrozen bool
71 | IsResigned bool
72 | IsActivated bool
73 | IsExited bool
74 | IsUnjoin bool
75 | }
76 |
77 | // GetUserInfo gets contact info
78 | func (bot Bot) GetUserInfo(userID *OptionalUserID) (*GetUserInfoResponse, error) {
79 | url := fmt.Sprintf(getUserInfoURL, userID.RealID, userID.UIDType)
80 | var respData GetUserInfoResponse
81 | err := bot.GetAPIRequest("GetUserInfo", url, true, nil, &respData)
82 | return &respData, err
83 | }
84 |
85 | // BatchGetUserInfo gets contact info in batch
86 | func (bot Bot) BatchGetUserInfo(userIDType string, userIDs ...string) (*BatchGetUserInfoResponse, error) {
87 | if len(userIDs) == 0 || len(userIDs) > 50 {
88 | return nil, ErrParamExceedInputLimit
89 | }
90 | v := url.Values{}
91 | v.Set("user_id_type", userIDType)
92 | for _, userID := range userIDs {
93 | v.Add("user_ids", userID)
94 | }
95 | url := fmt.Sprintf(batchGetUserInfoURL, v.Encode())
96 | var respData BatchGetUserInfoResponse
97 | err := bot.GetAPIRequest("GetUserInfo", url, true, nil, &respData)
98 | return &respData, err
99 | }
100 |
--------------------------------------------------------------------------------
/api_contact_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGetUserInfo(t *testing.T) {
10 | resp, err := bot.GetUserInfo(WithUserID(testUserID))
11 | if assert.NoError(t, err) {
12 | assert.Equal(t, resp.Data.User.Name, "David")
13 | }
14 | bresp, err := bot.BatchGetUserInfo(UIDUserID, testUserID)
15 | if assert.NoError(t, err) {
16 | if assert.NotEmpty(t, bresp.Data.Items) {
17 | assert.Equal(t, bresp.Data.Items[0].Name, "David")
18 | }
19 | }
20 | _, err = bot.BatchGetUserInfo(UIDUserID)
21 | assert.ErrorIs(t, err, ErrParamExceedInputLimit)
22 | }
23 |
--------------------------------------------------------------------------------
/api_group_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestGetGroupList(t *testing.T) {
12 | resp, err := bot.GetGroupList(1, 10)
13 | if assert.NoError(t, err) {
14 | assert.Equal(t, 0, resp.Code)
15 | t.Log(resp.Chats)
16 | assert.NotEmpty(t, resp.Chats)
17 | }
18 | }
19 |
20 | func TestGetGroupInfo(t *testing.T) {
21 | resp, err := bot.GetGroupInfo(testGroupChatID)
22 | if assert.NoError(t, err) {
23 | assert.Equal(t, 0, resp.Code)
24 | assert.Equal(t, "go-lark-ci", resp.Data.Name)
25 | assert.NotEmpty(t, resp.Data.Members)
26 | }
27 | }
28 |
29 | func TestGroupCRUD(t *testing.T) {
30 | // create group
31 | createResp, err := bot.CreateGroup(
32 | fmt.Sprintf("go-lark-ci group %d", time.Now().Unix()),
33 | "group create",
34 | []string{testUserOpenID},
35 | )
36 | if assert.NoError(t, err) {
37 | assert.Equal(t, 0, createResp.Code)
38 | }
39 | groupChatID := createResp.OpenChatID
40 | // delete member
41 | delResp, err := bot.DeleteGroupMember(groupChatID, []string{testUserOpenID})
42 | if assert.NoError(t, err) {
43 | assert.Equal(t, 0, delResp.Code)
44 | }
45 | // add member
46 | addResp, err := bot.AddGroupMember(groupChatID, []string{testUserOpenID})
47 | if assert.NoError(t, err) {
48 | assert.Equal(t, 0, addResp.Code)
49 | }
50 | // delete again
51 | delResp, err = bot.DeleteGroupMember(groupChatID, []string{testUserOpenID})
52 | if assert.NoError(t, err) {
53 | assert.Equal(t, 0, delResp.Code)
54 | }
55 | // add by user id
56 | addByUserResp, err := bot.AddGroupMemberByUserID(groupChatID, []string{testUserID})
57 | if assert.NoError(t, err) {
58 | assert.Equal(t, 0, addByUserResp.Code)
59 | }
60 | // update info
61 | updateResp, err := bot.UpdateGroupInfo(&UpdateGroupInfoReq{
62 | OpenChatID: groupChatID,
63 | Name: "test 1",
64 | })
65 | if assert.NoError(t, err) {
66 | assert.Equal(t, 0, updateResp.Code)
67 | }
68 | // disband
69 | disbandResp, err := bot.DisbandGroup(groupChatID)
70 | if assert.NoError(t, err) {
71 | assert.Equal(t, 0, disbandResp.Code)
72 | }
73 | }
74 |
75 | func TestBotAddRemove(t *testing.T) {
76 | // rm bot
77 | rmBotResp, err := bot.RemoveBotFromGroup(testGroupChatID)
78 | if assert.NoError(t, err) {
79 | assert.Equal(t, 0, rmBotResp.Code)
80 | }
81 | // add bot
82 | addBotResp, err := bot.AddBotToGroup(testGroupChatID)
83 | if assert.NoError(t, err) {
84 | assert.Equal(t, 0, addBotResp.Code)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/api_notification.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // PostNotificationResp response of PostNotification
4 | type PostNotificationResp struct {
5 | Ok bool `json:"ok,omitempty"`
6 | }
7 |
8 | // PostNotificationV2Resp response of PostNotificationV2
9 | type PostNotificationV2Resp struct {
10 | Code int `json:"code"`
11 | Msg string `json:"msg"`
12 | StatusCode int `json:"StatusCode"`
13 | StatusMessage string `json:"StatusMessage"`
14 | }
15 |
16 | // PostNotification send message to a webhook
17 | // deprecated: legacy version, please use PostNotificationV2 instead
18 | func (bot *Bot) PostNotification(title, text string) (*PostNotificationResp, error) {
19 | if !bot.requireType(NotificationBot) {
20 | return nil, ErrBotTypeError
21 | }
22 |
23 | params := map[string]interface{}{
24 | "title": title,
25 | "text": text,
26 | }
27 | var respData PostNotificationResp
28 | err := bot.PostAPIRequest("PostNotification", bot.webhook, false, params, &respData)
29 | return &respData, err
30 | }
31 |
32 | // PostNotificationV2 support v2 format
33 | func (bot *Bot) PostNotificationV2(om OutcomingMessage) (*PostNotificationV2Resp, error) {
34 | if !bot.requireType(NotificationBot) {
35 | return nil, ErrBotTypeError
36 | }
37 |
38 | params := buildOutcomingNotification(om)
39 | var respData PostNotificationV2Resp
40 | err := bot.PostAPIRequest("PostNotificationV2", bot.webhook, false, params, &respData)
41 | return &respData, err
42 | }
43 |
--------------------------------------------------------------------------------
/api_notification_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | // A weird case which sends V2 message body with V1 URL
11 | func TestWebhookV1Error(t *testing.T) {
12 | bot := NewNotificationBot(testWebhookV1)
13 | mbText := NewMsgBuffer(MsgText)
14 | mbText.Text("hello")
15 | resp, err := bot.PostNotificationV2(mbText.Build())
16 | assert.NoError(t, err)
17 | assert.Zero(t, resp.StatusCode)
18 | }
19 |
20 | func TestWebhookV2(t *testing.T) {
21 | bot := NewNotificationBot(testWebhookV2)
22 |
23 | mbText := NewMsgBuffer(MsgText)
24 | mbText.Text("hello")
25 | resp, err := bot.PostNotificationV2(mbText.Build())
26 | assert.NoError(t, err)
27 | assert.Zero(t, resp.StatusCode)
28 | assert.Equal(t, "success", resp.StatusMessage)
29 |
30 | mbPost := NewMsgBuffer(MsgPost)
31 | mbPost.Post(NewPostBuilder().Title("hello").TextTag("world", 1, true).Render())
32 | resp, err = bot.PostNotificationV2(mbPost.Build())
33 | assert.NoError(t, err)
34 | assert.Zero(t, resp.StatusCode)
35 | assert.Equal(t, "success", resp.StatusMessage)
36 |
37 | mbImg := NewMsgBuffer(MsgImage)
38 | mbImg.Image("img_a97c1597-9c0a-47c1-9fb4-dd3e5e37ac9g")
39 | resp, err = bot.PostNotificationV2(mbImg.Build())
40 | assert.NoError(t, err)
41 | assert.Zero(t, resp.StatusCode)
42 | assert.Equal(t, "success", resp.StatusMessage)
43 |
44 | mbShareGroup := NewMsgBuffer(MsgShareCard)
45 | mbShareGroup.ShareChat(testGroupChatID)
46 | resp, err = bot.PostNotificationV2(mbShareGroup.Build())
47 | assert.NoError(t, err)
48 | assert.Zero(t, resp.StatusCode)
49 | assert.Equal(t, "success", resp.StatusMessage)
50 | }
51 |
52 | func TestWebhookV2CardMessage(t *testing.T) {
53 | bot := NewNotificationBot(testWebhookV2)
54 |
55 | b := NewCardBuilder()
56 | card := b.Card(
57 | b.Div(
58 | b.Field(b.Text("左侧内容")).Short(),
59 | b.Field(b.Text("右侧内容")).Short(),
60 | b.Field(b.Text("整排内容")),
61 | b.Field(b.Text("整排**Markdown**内容").LarkMd()),
62 | ),
63 | b.Div().
64 | Text(b.Text("Text Content")).
65 | Extra(b.Img("img_a7c6aa35-382a-48ad-839d-d0182a69b4dg")),
66 | b.Note().
67 | AddText(b.Text("Note **Text**").LarkMd()).
68 | AddImage(b.Img("img_a7c6aa35-382a-48ad-839d-d0182a69b4dg")),
69 | ).
70 | Wathet().
71 | Title("Notification Card")
72 | msgV4 := NewMsgBuffer(MsgInteractive)
73 | omV4 := msgV4.Card(card.String()).Build()
74 | resp, err := bot.PostNotificationV2(omV4)
75 |
76 | if assert.NoError(t, err) {
77 | assert.Equal(t, 0, resp.StatusCode)
78 | assert.NotEmpty(t, resp.StatusMessage)
79 | }
80 | }
81 |
82 | func TestWebhookV2Signed(t *testing.T) {
83 | bot := NewNotificationBot(testWebhookV2Signed)
84 |
85 | mbText := NewMsgBuffer(MsgText)
86 | mbText.Text("hello sign").WithSign("FT1dnAgPYYTcpafMTkhPjc", time.Now().Unix())
87 | resp, err := bot.PostNotificationV2(mbText.Build())
88 | assert.NoError(t, err)
89 | assert.Zero(t, resp.StatusCode)
90 | assert.Equal(t, "success", resp.StatusMessage)
91 | }
92 |
93 | func TestWebhookV2SignedError(t *testing.T) {
94 | bot := NewNotificationBot("https://open.feishu.cn/open-apis/bot/v2/hook/749be902-6eaa-4cc3-9325-be4126164b02")
95 |
96 | mbText := NewMsgBuffer(MsgText)
97 | mbText.Text("hello sign").WithSign("LIpnNexV7rwOyOebKoqSdb", time.Now().Unix())
98 | resp, err := bot.PostNotificationV2(mbText.Build())
99 | assert.NoError(t, err)
100 | assert.Zero(t, resp.StatusCode)
101 | assert.Equal(t, "sign match fail or timestamp is not within one hour from current time", resp.Msg)
102 | }
103 |
--------------------------------------------------------------------------------
/api_upload.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // Import from Lark API Go demo
4 | // with adaption to go-lark frame
5 |
6 | import (
7 | "bytes"
8 | "fmt"
9 | "image"
10 | "image/jpeg"
11 | "io"
12 | "mime/multipart"
13 | "net/http"
14 | "os"
15 | )
16 |
17 | const (
18 | uploadImageURL = "/open-apis/im/v1/images"
19 | uploadFileURL = "/open-apis/im/v1/files"
20 | )
21 |
22 | // UploadImageResponse .
23 | type UploadImageResponse struct {
24 | BaseResponse
25 | Data struct {
26 | ImageKey string `json:"image_key"`
27 | } `json:"data"`
28 | }
29 |
30 | // UploadFileRequest .
31 | type UploadFileRequest struct {
32 | FileType string `json:"-"`
33 | FileName string `json:"-"`
34 | Duration int `json:"-"`
35 | Path string `json:"-"`
36 | Reader io.Reader `json:"-"`
37 | }
38 |
39 | // UploadFileResponse .
40 | type UploadFileResponse struct {
41 | BaseResponse
42 | Data struct {
43 | FileKey string `json:"file_key"`
44 | } `json:"data"`
45 | }
46 |
47 | // UploadImage uploads image to Lark server
48 | func (bot Bot) UploadImage(path string) (*UploadImageResponse, error) {
49 | file, err := os.Open(path)
50 | if err != nil {
51 | return nil, err
52 | }
53 | defer file.Close()
54 |
55 | body := &bytes.Buffer{}
56 | writer := multipart.NewWriter(body)
57 | writer.WriteField("image_type", "message")
58 | part, err := writer.CreateFormFile("image", path)
59 | if err != nil {
60 | return nil, err
61 | }
62 | _, err = io.Copy(part, file)
63 | if err != nil {
64 | return nil, err
65 | }
66 | err = writer.Close()
67 | if err != nil {
68 | return nil, err
69 | }
70 | var respData UploadImageResponse
71 | header := make(http.Header)
72 | header.Set("Content-Type", writer.FormDataContentType())
73 | err = bot.DoAPIRequest("POST", "UploadImage", uploadImageURL, header, true, body, &respData)
74 | if err != nil {
75 | return nil, err
76 | }
77 | return &respData, err
78 | }
79 |
80 | // UploadImageObject uploads image to Lark server
81 | func (bot Bot) UploadImageObject(img image.Image) (*UploadImageResponse, error) {
82 | body := &bytes.Buffer{}
83 | writer := multipart.NewWriter(body)
84 | writer.WriteField("image_type", "message")
85 | part, err := writer.CreateFormFile("image", "temp_image")
86 | if err != nil {
87 | return nil, err
88 | }
89 | err = jpeg.Encode(part, img, nil)
90 | if err != nil {
91 | return nil, err
92 | }
93 | err = writer.Close()
94 | if err != nil {
95 | return nil, err
96 | }
97 | var respData UploadImageResponse
98 | header := make(http.Header)
99 | header.Set("Content-Type", writer.FormDataContentType())
100 | err = bot.DoAPIRequest("POST", "UploadImage", uploadImageURL, header, true, body, &respData)
101 | if err != nil {
102 | return nil, err
103 | }
104 | return &respData, err
105 | }
106 |
107 | // UploadFile uploads file to Lark server
108 | func (bot Bot) UploadFile(req UploadFileRequest) (*UploadFileResponse, error) {
109 | var content io.Reader
110 | if req.Reader == nil {
111 | file, err := os.Open(req.Path)
112 | if err != nil {
113 | return nil, err
114 | }
115 | defer file.Close()
116 | content = file
117 | } else {
118 | content = req.Reader
119 | req.Path = req.FileName
120 | }
121 |
122 | body := &bytes.Buffer{}
123 | writer := multipart.NewWriter(body)
124 | writer.WriteField("file_type", req.FileType)
125 | writer.WriteField("file_name", req.FileName)
126 | if req.FileType == "mp4" && req.Duration > 0 {
127 | writer.WriteField("duration", fmt.Sprintf("%d", req.Duration))
128 | }
129 | part, err := writer.CreateFormFile("file", req.Path)
130 | if err != nil {
131 | return nil, err
132 | }
133 | _, err = io.Copy(part, content)
134 | if err != nil {
135 | return nil, err
136 | }
137 | err = writer.Close()
138 | if err != nil {
139 | return nil, err
140 | }
141 | var respData UploadFileResponse
142 | header := make(http.Header)
143 | header.Set("Content-Type", writer.FormDataContentType())
144 | err = bot.DoAPIRequest("POST", "UploadFile", uploadFileURL, header, true, body, &respData)
145 | if err != nil {
146 | return nil, err
147 | }
148 | return &respData, err
149 | }
150 |
--------------------------------------------------------------------------------
/api_upload_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "image/jpeg"
5 | "os"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestUploadImage(t *testing.T) {
13 | resp, err := bot.UploadImage("./fixtures/test.jpg")
14 | if assert.NoError(t, err) {
15 | assert.Zero(t, resp.Code)
16 | t.Log(resp.Data.ImageKey)
17 | assert.NotEmpty(t, resp.Data.ImageKey)
18 | }
19 | }
20 |
21 | func TestUploadImageObject(t *testing.T) {
22 | file, _ := os.Open("./fixtures/test.jpg")
23 | img, _ := jpeg.Decode(file)
24 |
25 | resp, err := bot.UploadImageObject(img)
26 | if assert.NoError(t, err) {
27 | assert.Zero(t, resp.Code)
28 | t.Log(resp.Data.ImageKey)
29 | assert.NotEmpty(t, resp.Data.ImageKey)
30 | }
31 | }
32 |
33 | func TestUploadFile(t *testing.T) {
34 | resp, err := bot.UploadFile(UploadFileRequest{
35 | FileType: "pdf",
36 | FileName: "hello.pdf",
37 | Path: "./fixtures/test.pdf",
38 | })
39 | if assert.NoError(t, err) {
40 | assert.Zero(t, resp.Code)
41 | t.Log(resp.Data.FileKey)
42 | assert.NotEmpty(t, resp.Data.FileKey)
43 | }
44 | }
45 |
46 | func TestUploadFile_Binary(t *testing.T) {
47 | resp, err := bot.UploadFile(UploadFileRequest{
48 | FileType: "stream",
49 | FileName: "test-data.csv",
50 | Reader: strings.NewReader(`Name,Age,Location
51 | Foo,25,Sleman
52 | Bar,23,Sidoarjo
53 | Baz,27,Bantul`),
54 | })
55 | if assert.NoError(t, err) {
56 | assert.Zero(t, resp.Code)
57 | t.Log(resp.Data.FileKey)
58 | assert.NotEmpty(t, resp.Data.FileKey)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/crypto.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/hmac"
7 | "crypto/sha256"
8 | "encoding/base64"
9 | "errors"
10 | "fmt"
11 | )
12 |
13 | // EncryptKey .
14 | func EncryptKey(key string) []byte {
15 | sha256key := sha256.Sum256([]byte(key))
16 | return sha256key[:sha256.Size]
17 | }
18 |
19 | // Decrypt with AES Cipher
20 | func Decrypt(encryptedKey []byte, data string) ([]byte, error) {
21 | block, err := aes.NewCipher(encryptedKey)
22 | if err != nil {
23 | return nil, err
24 | }
25 | ciphertext, err := base64.StdEncoding.DecodeString(data)
26 | iv := encryptedKey[:aes.BlockSize]
27 | blockMode := cipher.NewCBCDecrypter(block, iv)
28 | decryptedData := make([]byte, len(data))
29 | blockMode.CryptBlocks(decryptedData, ciphertext)
30 | msg := unpad(decryptedData)
31 | if len(msg) < block.BlockSize() {
32 | return nil, errors.New("msg length is less than blocksize")
33 | }
34 | return msg[block.BlockSize():], err
35 | }
36 |
37 | func unpad(data []byte) []byte {
38 | length := len(data)
39 | var unpadding, unpaddingIdx int
40 | for i := length - 1; i > 0; i-- {
41 | if data[i] != 0 {
42 | unpadding = int(data[i])
43 | unpaddingIdx = length - 1 - i
44 | break
45 | }
46 | }
47 | return data[:(length - unpaddingIdx - unpadding)]
48 | }
49 |
50 | // GenSign generate sign for notification bot
51 | func GenSign(secret string, timestamp int64) (string, error) {
52 | stringToSign := fmt.Sprintf("%v", timestamp) + "\n" + secret
53 |
54 | var data []byte
55 | h := hmac.New(sha256.New, []byte(stringToSign))
56 | _, err := h.Write(data)
57 | if err != nil {
58 | return "", err
59 | }
60 |
61 | signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
62 | return signature, nil
63 | }
64 |
--------------------------------------------------------------------------------
/crypto_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestAESDecrypt(t *testing.T) {
10 | key := EncryptKey("test key")
11 | data, _ := Decrypt(key, "P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk=")
12 | assert.Equal(t, "hello world", string(data))
13 | assert.Equal(t, 11, len(data))
14 | }
15 |
16 | func TestGenSign(t *testing.T) {
17 | sign, err := GenSign("xxx", 1661860880)
18 | if assert.NoError(t, err) {
19 | assert.Equal(t, "QnWVTSBe6FmQDE0bG6X0mURbI+DnvVyu1h+j5dHOjrU=", sign)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/error.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import "errors"
4 |
5 | // Errors
6 | var (
7 | ErrBotTypeError = errors.New("Bot type error")
8 | ErrParamUserID = errors.New("Param error: UserID")
9 | ErrParamMessageID = errors.New("Param error: Message ID")
10 | ErrParamExceedInputLimit = errors.New("Param error: Exceed input limit")
11 | ErrMessageTypeNotSuppored = errors.New("Message type not supported")
12 | ErrEncryptionNotEnabled = errors.New("Encryption is not enabled")
13 | ErrCustomHTTPClientNotSet = errors.New("Custom HTTP client not set")
14 | ErrMessageNotBuild = errors.New("Message not build")
15 | ErrUnsupportedUIDType = errors.New("Unsupported UID type")
16 | ErrInvalidReceiveID = errors.New("Invalid receive ID")
17 | ErrEventTypeNotMatch = errors.New("Event type not match")
18 | ErrMessageType = errors.New("Message type error")
19 | )
20 |
--------------------------------------------------------------------------------
/event.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import "encoding/json"
4 |
5 | // EventChallenge request of add event hook
6 | type EventChallenge struct {
7 | Token string `json:"token,omitempty"`
8 | Challenge string `json:"challenge,omitempty"`
9 | Type string `json:"type,omitempty"`
10 | }
11 |
12 | // EventChallengeReq is deprecated. Keep for legacy versions.
13 | type EventChallengeReq = EventChallenge
14 |
15 | // EncryptedReq request of encrypted challenge
16 | type EncryptedReq struct {
17 | Encrypt string `json:"encrypt,omitempty"`
18 | }
19 |
20 | // EventCardCallback request of card
21 | type EventCardCallback struct {
22 | AppID string `json:"app_id,omitempty"`
23 | TenantKey string `json:"tenant_key,omitempty"`
24 | Token string `json:"token,omitempty"`
25 | OpenID string `json:"open_id,omitempty"`
26 | UserID string `json:"user_id,omitempty"`
27 | MessageID string `json:"open_message_id,omitempty"`
28 | ChatID string `json:"open_chat_id,omitempty"`
29 | Action EventCardAction `json:"action,omitempty"`
30 | }
31 |
32 | // EventCardAction .
33 | type EventCardAction struct {
34 | Tag string `json:"tag,omitempty"` // button, overflow, select_static, select_person, &datepicker
35 | Option string `json:"option,omitempty"` // only for Overflow and SelectMenu
36 | Timezone string `json:"timezone,omitempty"` // only for DatePicker
37 | Value json.RawMessage `json:"value,omitempty"` // for any elements with value
38 | }
39 |
--------------------------------------------------------------------------------
/event_bot_added.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2BotAdded .
4 | type EventV2BotAdded struct {
5 | ChatID string `json:"chat_id,omitempty"`
6 | OperatorID EventV2UserID `json:"operator_id,omitempty"`
7 | External bool `json:"external,omitempty"`
8 | OperatorTenantKey string `json:"operator_tenant_key,omitempty"`
9 | }
10 |
11 | // GetBotAdded .
12 | func (e EventV2) GetBotAdded() (*EventV2BotAdded, error) {
13 | var body EventV2BotAdded
14 | err := e.GetEvent(EventTypeBotAdded, &body)
15 | return &body, err
16 | }
17 |
--------------------------------------------------------------------------------
/event_bot_deleted.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2BotDeleted .
4 | type EventV2BotDeleted = EventV2BotAdded
5 |
6 | // GetBotDeleted .
7 | func (e EventV2) GetBotDeleted() (*EventV2BotDeleted, error) {
8 | var body EventV2BotDeleted
9 | err := e.GetEvent(EventTypeBotDeleted, &body)
10 | return &body, err
11 | }
12 |
--------------------------------------------------------------------------------
/event_chat_disbanded.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2ChatDisbanded .
4 | type EventV2ChatDisbanded struct {
5 | ChatID string `json:"chat_id,omitempty"`
6 | OperatorID EventV2UserID `json:"operator_id,omitempty"`
7 | External bool `json:"external,omitempty"`
8 | OperatorTenantKey string `json:"operator_tenant_key,omitempty"`
9 | }
10 |
11 | // GetChatDisbanded .
12 | func (e EventV2) GetChatDisbanded() (*EventV2ChatDisbanded, error) {
13 | var body EventV2ChatDisbanded
14 | err := e.GetEvent(EventTypeChatDisbanded, &body)
15 | return &body, err
16 | }
17 |
--------------------------------------------------------------------------------
/event_message_reaction_created.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2MessageReactionCreated .
4 | type EventV2MessageReactionCreated struct {
5 | MessageID string `json:"message_id,omitempty"`
6 | OperatorType string `json:"operator_type,omitempty"`
7 | UserID EventV2UserID `json:"user_id,omitempty"`
8 | AppID string `json:"app_id,omitempty"`
9 | ActionTime string `json:"action_time,omitempty"`
10 | ReactionType struct {
11 | EmojiType string `json:"emoji_type,omitempty"`
12 | } `json:"reaction_type,omitempty"`
13 | }
14 |
15 | // GetMessageReactionCreated .
16 | func (e EventV2) GetMessageReactionCreated() (*EventV2MessageReactionCreated, error) {
17 | var body EventV2MessageReactionCreated
18 | err := e.GetEvent(EventTypeMessageReactionCreated, &body)
19 | return &body, err
20 | }
21 |
--------------------------------------------------------------------------------
/event_message_reaction_deleted.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2MessageReactionDeleted .
4 | type EventV2MessageReactionDeleted struct {
5 | MessageID string `json:"message_id,omitempty"`
6 | OperatorType string `json:"operator_type,omitempty"`
7 | UserID EventV2UserID `json:"user_id,omitempty"`
8 | AppID string `json:"app_id,omitempty"`
9 | ActionTime string `json:"action_time,omitempty"`
10 | ReactionType struct {
11 | EmojiType string `json:"emoji_type,omitempty"`
12 | } `json:"reaction_type,omitempty"`
13 | }
14 |
15 | // GetMessageReactionDeleted .
16 | func (e EventV2) GetMessageReactionDeleted() (*EventV2MessageReactionDeleted, error) {
17 | var body EventV2MessageReactionDeleted
18 | err := e.GetEvent(EventTypeMessageReactionDeleted, &body)
19 | return &body, err
20 | }
21 |
--------------------------------------------------------------------------------
/event_message_read.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2MessageRead .
4 | type EventV2MessageRead struct {
5 | Reader struct {
6 | ReaderID EventV2UserID `json:"reader_id,omitempty"`
7 | ReadTime string `json:"read_time,omitempty"`
8 | TenantKey string `json:"tenant_key,omitempty"`
9 | } `json:"reader,omitempty"`
10 | MessageIDList []string `json:"message_id_list,omitempty"`
11 | }
12 |
13 | // GetMessageRead .
14 | func (e EventV2) GetMessageRead() (*EventV2MessageRead, error) {
15 | var body EventV2MessageRead
16 | err := e.GetEvent(EventTypeMessageRead, &body)
17 | return &body, err
18 | }
19 |
--------------------------------------------------------------------------------
/event_message_recalled.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2MessageRecalled .
4 | type EventV2MessageRecalled struct {
5 | MessageID string `json:"message_id,omitempty"`
6 | ChatID string `json:"chat_id,omitempty"`
7 | RecallTime string `json:"recall_time,omitempty"`
8 | RecallType string `json:"recall_type,omitempty"`
9 | }
10 |
11 | // GetMessageRecalled .
12 | func (e EventV2) GetMessageRecalled() (*EventV2MessageRecalled, error) {
13 | var body EventV2MessageRecalled
14 | err := e.GetEvent(EventTypeMessageRecalled, &body)
15 | return &body, err
16 | }
17 |
--------------------------------------------------------------------------------
/event_message_received.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2MessageReceived .
4 | type EventV2MessageReceived struct {
5 | Sender struct {
6 | SenderID EventV2UserID `json:"sender_id,omitempty"`
7 | SenderType string `json:"sender_type,omitempty"`
8 | TenantKey string `json:"tenant_key,omitempty"`
9 | } `json:"sender,omitempty"`
10 | Message struct {
11 | MessageID string `json:"message_id,omitempty"`
12 | RootID string `json:"root_id,omitempty"`
13 | ParentID string `json:"parent_id,omitempty"`
14 | CreateTime string `json:"create_time,omitempty"`
15 | UpdateTime string `json:"update_time,omitempty"`
16 | ChatID string `json:"chat_id,omitempty"`
17 | ChatType string `json:"chat_type,omitempty"`
18 | ThreadID string `json:"thread_id,omitempty"`
19 | MessageType string `json:"message_type,omitempty"`
20 | Content string `json:"content,omitempty"`
21 | Mentions []struct {
22 | Key string `json:"key,omitempty"`
23 | ID EventV2UserID `json:"id,omitempty"`
24 | Name string `json:"name,omitempty"`
25 | TenantKey string `json:"tenant_key,omitempty"`
26 | } `json:"mentions,omitempty"`
27 | UserAgent string `json:"user_agent,omitempty"`
28 | } `json:"message,omitempty"`
29 | }
30 |
31 | // GetMessageReceived .
32 | func (e EventV2) GetMessageReceived() (*EventV2MessageReceived, error) {
33 | var body EventV2MessageReceived
34 | err := e.GetEvent(EventTypeMessageReceived, &body)
35 | return &body, err
36 | }
37 |
--------------------------------------------------------------------------------
/event_user_added.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2UserAdded .
4 | type EventV2UserAdded struct {
5 | ChatID string `json:"chat_id,omitempty"`
6 | OperatorID EventV2UserID `json:"operator_id,omitempty"`
7 | External bool `json:"external,omitempty"`
8 | OperatorTenantKey string `json:"operator_tenant_key,omitempty"`
9 | Users []struct {
10 | Name string `json:"name,omitempty"`
11 | TenantKey string `json:"tenant_key,omitempty"`
12 | UserID EventV2UserID `json:"user_id,omitempty"`
13 | } `json:"users,omitempty"`
14 | }
15 |
16 | // GetUserAdded .
17 | func (e EventV2) GetUserAdded() (*EventV2UserAdded, error) {
18 | var body EventV2UserAdded
19 | err := e.GetEvent(EventTypeUserAdded, &body)
20 | return &body, err
21 | }
22 |
--------------------------------------------------------------------------------
/event_user_deleted.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventV2UserDeleted .
4 | type EventV2UserDeleted = EventV2UserAdded
5 |
6 | // GetUserDeleted .
7 | func (e EventV2) GetUserDeleted() (*EventV2UserDeleted, error) {
8 | var body EventV2UserDeleted
9 | err := e.GetEvent(EventTypeUserDeleted, &body)
10 | return &body, err
11 | }
12 |
--------------------------------------------------------------------------------
/event_v1.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "log"
7 | "net/http"
8 | )
9 |
10 | // EventMessage .
11 | type EventMessage struct {
12 | UUID string `json:"uuid"`
13 | Timestamp string `json:"ts"`
14 | // Token is shown by Lark to indicate it is not a fake message, check at your own need
15 | Token string `json:"token"`
16 | EventType string `json:"type"`
17 | Event EventBody `json:"event"`
18 | }
19 |
20 | // EventBody .
21 | type EventBody struct {
22 | Type string `json:"type"`
23 | AppID string `json:"app_id"`
24 | TenantKey string `json:"tenant_key"`
25 | ChatType string `json:"chat_type"`
26 | MsgType string `json:"msg_type"`
27 | RootID string `json:"root_id,omitempty"`
28 | ParentID string `json:"parent_id,omitempty"`
29 | OpenID string `json:"open_id,omitempty"`
30 | OpenChatID string `json:"open_chat_id,omitempty"`
31 | OpenMessageID string `json:"open_message_id,omitempty"`
32 | IsMention bool `json:"is_mention,omitempty"`
33 | Title string `json:"title,omitempty"`
34 | Text string `json:"text,omitempty"`
35 | RealText string `json:"text_without_at_bot,omitempty"`
36 | ImageKey string `json:"image_key,omitempty"`
37 | ImageURL string `json:"image_url,omitempty"`
38 | FileKey string `json:"file_key,omitempty"`
39 | }
40 |
41 | // PostEvent posts event
42 | // 1. help to develop and test ServeEvent callback func much easier
43 | // 2. otherwise, you may use it to forward event
44 | func PostEvent(client *http.Client, hookURL string, message EventMessage) (*http.Response, error) {
45 | buf := new(bytes.Buffer)
46 | err := json.NewEncoder(buf).Encode(message)
47 | if err != nil {
48 | log.Printf("Encode json failed: %+v\n", err)
49 | return nil, err
50 | }
51 | resp, err := client.Post(hookURL, "application/json; charset=utf-8", buf)
52 | return resp, err
53 | }
54 |
--------------------------------------------------------------------------------
/event_v1_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestPostEventMessage(t *testing.T) {
14 | message := EventMessage{
15 | Timestamp: "",
16 | Token: "",
17 | EventType: "event_callback",
18 | Event: EventBody{
19 | Type: "message",
20 | ChatType: "private",
21 | MsgType: "text",
22 | OpenID: testUserOpenID,
23 | Text: "private event",
24 | Title: "",
25 | OpenMessageID: "",
26 | ImageKey: "",
27 | ImageURL: "",
28 | },
29 | }
30 | w := performRequest(func(w http.ResponseWriter, r *http.Request) {
31 | var m EventMessage
32 | json.NewDecoder(r.Body).Decode(&m)
33 | w.Write([]byte(m.Event.Text))
34 | }, "POST", "/", message)
35 | assert.Equal(t, "private event", string(w.Body.Bytes()))
36 |
37 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
38 | m, _ := json.Marshal(message)
39 | w.Write([]byte(m))
40 | }))
41 | defer ts.Close()
42 | resp, err := PostEvent(http.DefaultClient, ts.URL, message)
43 | if assert.NoError(t, err) {
44 | var event EventMessage
45 | body, err := ioutil.ReadAll(resp.Body)
46 | if assert.NoError(t, err) {
47 | defer resp.Body.Close()
48 | _ = json.Unmarshal(body, &event)
49 | assert.Equal(t, "event_callback", event.EventType)
50 | assert.Equal(t, "message", event.Event.Type)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/event_v2.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "log"
7 | "net/http"
8 | )
9 |
10 | // EventType definitions
11 | const (
12 | EventTypeMessageReceived = "im.message.receive_v1"
13 | EventTypeMessageRead = "im.message.message_read_v1"
14 | EventTypeMessageRecalled = "im.message.recalled_v1"
15 | EventTypeMessageReactionCreated = "im.message.reaction.created_v1"
16 | EventTypeMessageReactionDeleted = "im.message.reaction.deleted_v1"
17 | EventTypeChatDisbanded = "im.chat.disbanded_v1"
18 | EventTypeUserAdded = "im.chat.member.user.added_v1"
19 | EventTypeUserDeleted = "im.chat.member.user.deleted_v1"
20 | EventTypeBotAdded = "im.chat.member.bot.added_v1"
21 | EventTypeBotDeleted = "im.chat.member.bot.deleted_v1"
22 | // not supported yet
23 | EventTypeChatUpdated = "im.chat.updated_v1"
24 | EventTypeUserWithdrawn = "im.chat.member.user.withdrawn_v1"
25 | )
26 |
27 | // EventV2 handles events with v2 schema
28 | type EventV2 struct {
29 | Schema string `json:"schema,omitempty"`
30 | Header EventV2Header `json:"header,omitempty"`
31 |
32 | EventRaw json.RawMessage `json:"event,omitempty"`
33 | Event interface{} `json:"-"`
34 | }
35 |
36 | // EventV2Header .
37 | type EventV2Header struct {
38 | EventID string `json:"event_id,omitempty"`
39 | EventType string `json:"event_type,omitempty"`
40 | CreateTime string `json:"create_time,omitempty"`
41 | Token string `json:"token,omitempty"`
42 | AppID string `json:"app_id,omitempty"`
43 | TenantKey string `json:"tenant_key,omitempty"`
44 | }
45 |
46 | // EventV2UserID .
47 | type EventV2UserID struct {
48 | UnionID string `json:"union_id,omitempty"`
49 | UserID string `json:"user_id,omitempty"`
50 | OpenID string `json:"open_id,omitempty"`
51 | }
52 |
53 | // PostEvent with event v2 format
54 | // and it's part of EventV2 instead of package method
55 | func (e EventV2) PostEvent(client *http.Client, hookURL string) (*http.Response, error) {
56 | buf := new(bytes.Buffer)
57 | err := json.NewEncoder(buf).Encode(e)
58 | if err != nil {
59 | log.Printf("Encode json failed: %+v\n", err)
60 | return nil, err
61 | }
62 | resp, err := client.Post(hookURL, "application/json; charset=utf-8", buf)
63 | return resp, err
64 | }
65 |
66 | // GetEvent .
67 | func (e EventV2) GetEvent(eventType string, body interface{}) error {
68 | if e.Header.EventType != eventType {
69 | return ErrEventTypeNotMatch
70 | }
71 | err := json.Unmarshal(e.EventRaw, &body)
72 | return err
73 | }
74 |
--------------------------------------------------------------------------------
/event_v2_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestPostEventV2(t *testing.T) {
14 | message := EventV2{
15 | Schema: "2.0",
16 | Header: EventV2Header{
17 | AppID: "666",
18 | },
19 | Event: EventBody{
20 | Type: "message",
21 | ChatType: "group",
22 | MsgType: "text",
23 | OpenID: testUserOpenID,
24 | Text: "public event",
25 | Title: "",
26 | OpenMessageID: "",
27 | ImageKey: "",
28 | ImageURL: "",
29 | },
30 | }
31 | w := performRequest(func(w http.ResponseWriter, r *http.Request) {
32 | var m EventV2
33 | json.NewDecoder(r.Body).Decode(&m)
34 | w.Write([]byte(m.Schema))
35 | }, "POST", "/", message)
36 | assert.Equal(t, "2.0", string(w.Body.Bytes()))
37 |
38 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39 | m, _ := json.Marshal(message)
40 | w.Write([]byte(m))
41 | }))
42 | defer ts.Close()
43 | resp, err := message.PostEvent(http.DefaultClient, ts.URL)
44 | if assert.NoError(t, err) {
45 | var event EventV2
46 | body, err := ioutil.ReadAll(resp.Body)
47 | if assert.NoError(t, err) {
48 | defer resp.Body.Close()
49 | _ = json.Unmarshal(body, &event)
50 | assert.Equal(t, "2.0", event.Schema)
51 | assert.Equal(t, "666", event.Header.AppID)
52 | }
53 | }
54 | }
55 |
56 | func TestEventTypes(t *testing.T) {
57 | event := EventV2{
58 | Header: EventV2Header{
59 | EventType: EventTypeChatDisbanded,
60 | },
61 | EventRaw: json.RawMessage(`{ "message": { "chat_id": "oc_ae7f3952a9b28588aeac46c9853d25d3", "chat_type": "p2p", "content": "{\"text\":\"333\"}", "create_time": "1641385820771", "message_id": "om_6ff2cff41a3e9248bbb19bf0e4762e6e", "message_type": "text" }, "sender": { "sender_id": { "open_id": "ou_4f75b532aff410181e93552ad0532072", "union_id": "on_2312aab89ab7c87beb9a443b2f3b1342", "user_id": "4gbb63af" }, "sender_type": "user", "tenant_key": "736588c9260f175d" } }`),
62 | }
63 | m, e := event.GetMessageReceived()
64 | assert.Error(t, e)
65 | event.Header.EventType = EventTypeMessageReceived
66 | m, e = event.GetMessageReceived()
67 | assert.NoError(t, e)
68 | assert.Equal(t, "p2p", m.Message.ChatType)
69 | }
70 |
71 | func TestGetEvent(t *testing.T) {
72 | event := EventV2{
73 | Header: EventV2Header{
74 | EventType: EventTypeMessageReceived,
75 | },
76 | EventRaw: json.RawMessage(`{ "message": { "chat_id": "oc_ae7f3952a9b28588aeac46c9853d25d3", "chat_type": "p2p", "content": "{\"text\":\"333\"}", "create_time": "1641385820771", "message_id": "om_6ff2cff41a3e9248bbb19bf0e4762e6e", "message_type": "text" }, "sender": { "sender_id": { "open_id": "ou_4f75b532aff410181e93552ad0532072", "union_id": "on_2312aab89ab7c87beb9a443b2f3b1342", "user_id": "4gbb63af" }, "sender_type": "user", "tenant_key": "736588c9260f175d" } }`),
77 | }
78 | var ev EventV2MessageReceived
79 | err := event.GetEvent(EventTypeMessageReceived, &ev)
80 | if assert.NoError(t, err) {
81 | assert.Equal(t, "oc_ae7f3952a9b28588aeac46c9853d25d3", ev.Message.ChatID)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/fixtures/test.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-lark/lark/392c494793c4d573ec1edbc934bb1b96d1227dbb/fixtures/test.jpg
--------------------------------------------------------------------------------
/fixtures/test.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-lark/lark/392c494793c4d573ec1edbc934bb1b96d1227dbb/fixtures/test.pdf
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-lark/lark
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/davecgh/go-spew v1.1.1 // indirect
7 | github.com/go-lark/card-builder v1.0.0-beta.2 // indirect
8 | github.com/joho/godotenv v1.3.0
9 | github.com/stretchr/testify v1.7.0
10 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/go-lark/card-builder v1.0.0-beta.1 h1:yxUGUTVxn3nPpwVTXVNCYTSMeVGJMASw5s+8CMymJs4=
5 | github.com/go-lark/card-builder v1.0.0-beta.1/go.mod h1:7O2nuzjG4Cc/ty0s7Q7roYXOVxt9Gzrw0jBHsuP3UaM=
6 | github.com/go-lark/card-builder v1.0.0-beta.2 h1:DfH/14fs+R/G8CC+a2WoIp2wpWoD2isDcBpYZoSLwaU=
7 | github.com/go-lark/card-builder v1.0.0-beta.2/go.mod h1:7O2nuzjG4Cc/ty0s7Q7roYXOVxt9Gzrw0jBHsuP3UaM=
8 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
9 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
14 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
18 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
19 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
20 |
--------------------------------------------------------------------------------
/http.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "net/http/httputil"
10 | )
11 |
12 | // ExpandURL expands url path to full url
13 | func (bot Bot) ExpandURL(urlPath string) string {
14 | url := fmt.Sprintf("%s%s", bot.domain, urlPath)
15 | return url
16 | }
17 |
18 | func (bot Bot) httpErrorLog(prefix, text string, err error) {
19 | bot.logger.Log(bot.ctx, LogLevelError, fmt.Sprintf("[%s] %s: %+v\n", prefix, text, err))
20 | }
21 |
22 | // DoAPIRequest builds http request
23 | func (bot Bot) DoAPIRequest(
24 | method string,
25 | prefix, urlPath string,
26 | header http.Header, auth bool,
27 | body io.Reader,
28 | output interface{},
29 | ) error {
30 | var (
31 | err error
32 | respBody io.ReadCloser
33 | url = bot.ExpandURL(urlPath)
34 | )
35 | if header == nil {
36 | header = make(http.Header)
37 | }
38 | if auth {
39 | header.Add("Authorization", fmt.Sprintf("Bearer %s", bot.TenantAccessToken()))
40 | }
41 | if bot.useCustomClient {
42 | if bot.customClient == nil {
43 | return ErrCustomHTTPClientNotSet
44 | }
45 | respBody, err = bot.customClient.Do(bot.ctx, method, url, header, body)
46 | if err != nil {
47 | bot.httpErrorLog(prefix, "call failed", err)
48 | return err
49 | }
50 | } else {
51 | req, err := http.NewRequest(method, url, body)
52 | if err != nil {
53 | bot.httpErrorLog(prefix, "init request failed", err)
54 | return err
55 | }
56 | req.Header = header
57 | resp, err := bot.client.Do(req)
58 | if err != nil {
59 | bot.httpErrorLog(prefix, "call failed", err)
60 | return err
61 | }
62 | if bot.debug {
63 | b, _ := httputil.DumpResponse(resp, true)
64 | bot.logger.Log(bot.ctx, LogLevelDebug, string(b))
65 | }
66 | respBody = resp.Body
67 | }
68 | defer respBody.Close()
69 | err = json.NewDecoder(respBody).Decode(&output)
70 | if err != nil {
71 | bot.httpErrorLog(prefix, "decode body failed", err)
72 | return err
73 | }
74 | return err
75 | }
76 |
77 | func (bot Bot) wrapAPIRequest(method, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
78 | buf := new(bytes.Buffer)
79 | err := json.NewEncoder(buf).Encode(params)
80 | if err != nil {
81 | bot.httpErrorLog(prefix, "encode JSON failed", err)
82 | return err
83 | }
84 |
85 | header := make(http.Header)
86 | header.Set("Content-Type", "application/json; charset=utf-8")
87 | err = bot.DoAPIRequest(method, prefix, urlPath, header, auth, buf, output)
88 | if err != nil {
89 | return err
90 | }
91 | return nil
92 | }
93 |
94 | // PostAPIRequest call Lark API
95 | func (bot Bot) PostAPIRequest(prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
96 | return bot.wrapAPIRequest(http.MethodPost, prefix, urlPath, auth, params, output)
97 | }
98 |
99 | // GetAPIRequest call Lark API
100 | func (bot Bot) GetAPIRequest(prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
101 | return bot.wrapAPIRequest(http.MethodGet, prefix, urlPath, auth, params, output)
102 | }
103 |
104 | // DeleteAPIRequest call Lark API
105 | func (bot Bot) DeleteAPIRequest(prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
106 | return bot.wrapAPIRequest(http.MethodDelete, prefix, urlPath, auth, params, output)
107 | }
108 |
109 | // PutAPIRequest call Lark API
110 | func (bot Bot) PutAPIRequest(prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
111 | return bot.wrapAPIRequest(http.MethodPut, prefix, urlPath, auth, params, output)
112 | }
113 |
114 | // PatchAPIRequest call Lark API
115 | func (bot Bot) PatchAPIRequest(prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
116 | return bot.wrapAPIRequest(http.MethodPatch, prefix, urlPath, auth, params, output)
117 | }
118 |
--------------------------------------------------------------------------------
/http_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestExpandURL(t *testing.T) {
10 | bot := NewChatBot("test-id", "test-secret")
11 | bot.SetDomain("http://localhost")
12 | assert.Equal(t, bot.ExpandURL("/test"),
13 | "http://localhost/test")
14 | }
15 |
--------------------------------------------------------------------------------
/http_wrapper.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "context"
5 | "io"
6 | "net/http"
7 | )
8 |
9 | // HTTPWrapper is a wrapper interface, which enables extension on HTTP part.
10 | // Typicall, we do not need this because default client is sufficient.
11 | type HTTPWrapper interface {
12 | Do(
13 | ctx context.Context,
14 | method, url string,
15 | header http.Header,
16 | body io.Reader) (io.ReadCloser, error)
17 | }
18 |
--------------------------------------------------------------------------------
/lark.go:
--------------------------------------------------------------------------------
1 | // Package lark is an easy-to-use SDK for Feishu and Lark Open Platform,
2 | // which implements messaging APIs, with full-fledged supports on building Chat Bot and Notification Bot.
3 | package lark
4 |
5 | import (
6 | "context"
7 | "net/http"
8 | "sync/atomic"
9 | "time"
10 | )
11 |
12 | const (
13 | // ChatBot should be created with NewChatBot
14 | // Create from https://open.feishu.cn/ or https://open.larksuite.com/
15 | ChatBot = iota
16 | // NotificationBot for webhook, behave as a simpler notification bot
17 | // Create from Lark group
18 | NotificationBot
19 | )
20 |
21 | // Bot definition
22 | type Bot struct {
23 | // bot type
24 | botType int
25 | // Auth info
26 | appID string
27 | appSecret string
28 | accessToken atomic.Value
29 | tenantAccessToken atomic.Value
30 |
31 | // user id type for api chat
32 | userIDType string
33 | // webhook for NotificationBot
34 | webhook string
35 | // API Domain
36 | domain string
37 | // http client
38 | client *http.Client
39 | // custom http client
40 | useCustomClient bool
41 | customClient HTTPWrapper
42 | // auth heartbeat
43 | heartbeat chan bool
44 | // auth heartbeat counter (for testing)
45 | heartbeatCounter int64
46 |
47 | ctx context.Context
48 | logger LogWrapper
49 | debug bool
50 | }
51 |
52 | // Domains
53 | const (
54 | DomainFeishu = "https://open.feishu.cn"
55 | DomainLark = "https://open.larksuite.com"
56 | )
57 |
58 | // NewChatBot with appID and appSecret
59 | func NewChatBot(appID, appSecret string) *Bot {
60 | bot := &Bot{
61 | botType: ChatBot,
62 | appID: appID,
63 | appSecret: appSecret,
64 | client: initClient(),
65 | domain: DomainFeishu,
66 | ctx: context.Background(),
67 | logger: initDefaultLogger(),
68 | }
69 | bot.accessToken.Store("")
70 | bot.tenantAccessToken.Store("")
71 |
72 | return bot
73 | }
74 |
75 | // NewNotificationBot with URL
76 | func NewNotificationBot(hookURL string) *Bot {
77 | bot := &Bot{
78 | botType: NotificationBot,
79 | webhook: hookURL,
80 | client: initClient(),
81 | ctx: context.Background(),
82 | logger: initDefaultLogger(),
83 | }
84 | bot.accessToken.Store("")
85 | bot.tenantAccessToken.Store("")
86 |
87 | return bot
88 | }
89 |
90 | // requireType checks whether the action is allowed in a list of bot types
91 | func (bot Bot) requireType(botType ...int) bool {
92 | for _, iterType := range botType {
93 | if bot.botType == iterType {
94 | return true
95 | }
96 | }
97 | return false
98 | }
99 |
100 | // SetClient assigns a new client to bot.client
101 | func (bot *Bot) SetClient(c *http.Client) {
102 | bot.client = c
103 | }
104 |
105 | func initClient() *http.Client {
106 | return &http.Client{
107 | Timeout: 5 * time.Second,
108 | }
109 | }
110 |
111 | // SetCustomClient .
112 | func (bot *Bot) SetCustomClient(c HTTPWrapper) {
113 | bot.useCustomClient = true
114 | bot.customClient = c
115 | }
116 |
117 | // UnsetCustomClient .
118 | func (bot *Bot) UnsetCustomClient() {
119 | bot.useCustomClient = false
120 | bot.customClient = nil
121 | }
122 |
123 | // SetDomain set domain of endpoint, so we could call Feishu/Lark
124 | // go-lark does not check your host, just use the right one or fail.
125 | func (bot *Bot) SetDomain(domain string) {
126 | bot.domain = domain
127 | }
128 |
129 | // Domain returns current domain
130 | func (bot Bot) Domain() string {
131 | return bot.domain
132 | }
133 |
134 | // AppID returns bot.appID for external use
135 | func (bot Bot) AppID() string {
136 | return bot.appID
137 | }
138 |
139 | // BotType returns bot.botType for external use
140 | func (bot Bot) BotType() int {
141 | return bot.botType
142 | }
143 |
144 | // AccessToken returns bot.accessToken for external use
145 | func (bot Bot) AccessToken() string {
146 | return bot.accessToken.Load().(string)
147 | }
148 |
149 | // TenantAccessToken returns bot.tenantAccessToken for external use
150 | func (bot Bot) TenantAccessToken() string {
151 | return bot.tenantAccessToken.Load().(string)
152 | }
153 |
154 | // SetWebhook updates webhook URL
155 | func (bot *Bot) SetWebhook(url string) {
156 | bot.webhook = url
157 | }
158 |
--------------------------------------------------------------------------------
/lark_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "io"
8 | "log"
9 | "net/http"
10 | "net/http/httptest"
11 | "os"
12 | "testing"
13 |
14 | "github.com/joho/godotenv"
15 | "github.com/stretchr/testify/assert"
16 | )
17 |
18 | // IDs for test use
19 | var (
20 | testAppID string
21 | testAppSecret string
22 | testUserEmail string
23 | testUserOpenID string
24 | testUserID string
25 | testUserUnionID string
26 | testGroupChatID string
27 | testWebhookV1 string
28 | testWebhookV2 string
29 | testWebhookV2Signed string
30 | )
31 |
32 | func newTestBot() *Bot {
33 | testMode := os.Getenv("GO_LARK_TEST_MODE")
34 | if testMode == "" {
35 | testMode = "testing"
36 | }
37 | if testMode == "local" {
38 | err := godotenv.Load(".env")
39 | if err != nil {
40 | panic(err)
41 | }
42 | }
43 | testAppID = os.Getenv("LARK_APP_ID")
44 | testAppSecret = os.Getenv("LARK_APP_SECRET")
45 | testUserEmail = os.Getenv("LARK_USER_EMAIL")
46 | testUserID = os.Getenv("LARK_USER_ID")
47 | testUserUnionID = os.Getenv("LARK_UNION_ID")
48 | testUserOpenID = os.Getenv("LARK_OPEN_ID")
49 | testGroupChatID = os.Getenv("LARK_CHAT_ID")
50 | testWebhookV1 = os.Getenv("LARK_WEBHOOK_V1")
51 | testWebhookV2 = os.Getenv("LARK_WEBHOOK_V2")
52 | testWebhookV2Signed = os.Getenv("LARK_WEBHOOK_V2_SIGNED")
53 | if len(testAppID) == 0 ||
54 | len(testAppSecret) == 0 ||
55 | len(testUserEmail) == 0 ||
56 | len(testUserID) == 0 ||
57 | len(testUserUnionID) == 0 ||
58 | len(testUserOpenID) == 0 ||
59 | len(testGroupChatID) == 0 {
60 | panic("insufficient test environment")
61 | }
62 | return NewChatBot(testAppID, testAppSecret)
63 | }
64 |
65 | func captureOutput(f func()) string {
66 | var buf bytes.Buffer
67 | log.SetOutput(&buf)
68 | f()
69 | log.SetOutput(os.Stderr)
70 | return buf.String()
71 | }
72 |
73 | func performRequest(r http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder {
74 | buf := new(bytes.Buffer)
75 | json.NewEncoder(buf).Encode(body)
76 | req := httptest.NewRequest(method, path, buf)
77 | w := httptest.NewRecorder()
78 | r.ServeHTTP(w, req)
79 | return w
80 | }
81 |
82 | // for general API test suites
83 | var bot *Bot
84 |
85 | func init() {
86 | bot = newTestBot()
87 | _, _ = bot.GetTenantAccessTokenInternal(true)
88 | }
89 |
90 | func TestBotProperties(t *testing.T) {
91 | chatBot := newTestBot()
92 | assert.NotEmpty(t, chatBot.appID)
93 | assert.NotEmpty(t, chatBot.appSecret)
94 | assert.Empty(t, chatBot.webhook)
95 | assert.Equal(t, DomainFeishu, chatBot.domain)
96 | assert.Equal(t, ChatBot, chatBot.botType)
97 | assert.NotNil(t, chatBot.client)
98 | assert.NotNil(t, chatBot.logger)
99 |
100 | notifyBot := NewNotificationBot(testWebhookV1)
101 | assert.Empty(t, notifyBot.appID)
102 | assert.Empty(t, notifyBot.appSecret)
103 | assert.NotEmpty(t, notifyBot.webhook)
104 | assert.Empty(t, notifyBot.domain)
105 | assert.Equal(t, NotificationBot, notifyBot.botType)
106 | assert.NotNil(t, notifyBot.client)
107 | assert.NotNil(t, notifyBot.logger)
108 | }
109 |
110 | func TestRequiredType(t *testing.T) {
111 | bot := newTestBot()
112 | assert.True(t, bot.requireType(ChatBot))
113 | assert.False(t, bot.requireType(NotificationBot))
114 | }
115 |
116 | func TestSetDomain(t *testing.T) {
117 | bot := newTestBot()
118 | assert.Equal(t, DomainFeishu, bot.domain)
119 | assert.Equal(t, DomainFeishu, bot.Domain())
120 | bot.SetDomain("https://test.test")
121 | assert.Equal(t, "https://test.test", bot.domain)
122 | assert.Equal(t, "https://test.test", bot.Domain())
123 | }
124 |
125 | func TestBotGetters(t *testing.T) {
126 | bot := newTestBot()
127 | assert.Equal(t, testAppID, bot.AppID())
128 | assert.Equal(t, ChatBot, bot.BotType())
129 | assert.Equal(t, "", bot.AccessToken())
130 | assert.Equal(t, "", bot.TenantAccessToken())
131 | }
132 |
133 | func TestSetClient(t *testing.T) {
134 | bot := &Bot{}
135 | assert.Nil(t, bot.client)
136 | bot.SetClient(&http.Client{})
137 | assert.NotNil(t, bot.client)
138 | }
139 |
140 | type customHTTPWrapper struct {
141 | client *http.Client
142 | }
143 |
144 | func (c customHTTPWrapper) Do(ctx context.Context, method, url string, header http.Header, body io.Reader) (io.ReadCloser, error) {
145 | return nil, nil
146 | }
147 |
148 | func TestCustomClient(t *testing.T) {
149 | bot := &Bot{}
150 | assert.Nil(t, bot.customClient)
151 | var c customHTTPWrapper
152 | bot.SetCustomClient(c)
153 | assert.NotNil(t, bot.customClient)
154 | }
155 |
156 | func TestUpdateWebhook(t *testing.T) {
157 | bot := NewNotificationBot("abc")
158 | assert.Equal(t, "abc", bot.webhook)
159 | bot.SetWebhook("def")
160 | assert.Equal(t, "def", bot.webhook)
161 | }
162 |
--------------------------------------------------------------------------------
/locale.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // Supported Lark locales
4 | const (
5 | LocaleZhCN = "zh_cn"
6 | LocaleZhHK = "zh_hk"
7 | LocaleZhTW = "zh_tw"
8 | LocaleEnUS = "en_us"
9 | LocaleJaJP = "ja_jp"
10 | )
11 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "context"
5 | "io"
6 | "log"
7 | "os"
8 | )
9 |
10 | // LogLevel defs
11 | type LogLevel int
12 |
13 | // LogLevels
14 | const (
15 | LogLevelTrace = iota + 1
16 | LogLevelDebug
17 | LogLevelInfo
18 | LogLevelWarn
19 | LogLevelError
20 | )
21 |
22 | // LogWrapper interface
23 | type LogWrapper interface {
24 | // for log print
25 | Log(context.Context, LogLevel, string)
26 | // for test redirection
27 | SetOutput(io.Writer)
28 | }
29 |
30 | // String .
31 | func (ll LogLevel) String() string {
32 | switch ll {
33 | case LogLevelTrace:
34 | return "TRACE"
35 | case LogLevelDebug:
36 | return "DEBUG"
37 | case LogLevelInfo:
38 | return "INFO"
39 | case LogLevelWarn:
40 | return "WARN"
41 | case LogLevelError:
42 | return "ERROR"
43 | }
44 | return ""
45 | }
46 |
47 | type stdLogger struct {
48 | *log.Logger
49 | }
50 |
51 | func (sl stdLogger) Log(_ context.Context, level LogLevel, msg string) {
52 | sl.Printf("[%s] %s\n", level, msg)
53 | }
54 |
55 | const logPrefix = "[go-lark] "
56 |
57 | func initDefaultLogger() LogWrapper {
58 | // create a default std logger
59 | logger := stdLogger{
60 | log.New(os.Stderr, logPrefix, log.LstdFlags),
61 | }
62 | return logger
63 | }
64 |
65 | // SetLogger set a new logger
66 | func (bot *Bot) SetLogger(logger LogWrapper) {
67 | bot.logger = logger
68 | }
69 |
70 | // Logger returns current logger
71 | func (bot Bot) Logger() LogWrapper {
72 | return bot.logger
73 | }
74 |
75 | // WithContext .
76 | func (bot *Bot) WithContext(ctx context.Context) *Bot {
77 | bot.ctx = ctx
78 | return bot
79 | }
80 |
--------------------------------------------------------------------------------
/logger_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestSetLogger(t *testing.T) {
12 | bot := newTestBot()
13 | newLogger := initDefaultLogger()
14 | bot.SetLogger(newLogger)
15 | assert.Equal(t, newLogger, bot.logger)
16 | assert.Equal(t, newLogger, bot.Logger())
17 | }
18 |
19 | func TestLogLevel(t *testing.T) {
20 | var logLevel LogLevel = LogLevelDebug
21 | assert.Equal(t, "DEBUG", logLevel.String())
22 | logLevel = LogLevelError
23 | assert.Equal(t, "ERROR", logLevel.String())
24 | logLevel = LogLevelTrace
25 | assert.Equal(t, "TRACE", logLevel.String())
26 | logLevel = LogLevelWarn
27 | assert.Equal(t, "WARN", logLevel.String())
28 | logLevel = LogLevelInfo
29 | assert.Equal(t, "INFO", logLevel.String())
30 | logLevel = 1000
31 | assert.Equal(t, "", logLevel.String())
32 | }
33 |
34 | func TestWithContext(t *testing.T) {
35 | bot := newTestBot()
36 | assert.Equal(t, "context.Background", fmt.Sprintf("%s", bot.ctx))
37 | bot.WithContext(context.TODO())
38 | assert.Equal(t, "context.TODO", fmt.Sprintf("%s", bot.ctx))
39 | }
40 |
--------------------------------------------------------------------------------
/message.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // Msg Types
4 | const (
5 | MsgText = "text"
6 | MsgPost = "post"
7 | MsgInteractive = "interactive"
8 | MsgImage = "image"
9 | MsgShareCard = "share_chat"
10 | MsgShareUser = "share_user"
11 | MsgAudio = "audio"
12 | MsgMedia = "media"
13 | MsgFile = "file"
14 | MsgSticker = "sticker"
15 | )
16 |
17 | // OutcomingMessage struct of an outcoming message
18 | type OutcomingMessage struct {
19 | MsgType string `json:"msg_type"`
20 | Content MessageContent `json:"content"`
21 | Card CardContent `json:"card"`
22 | // ID for user
23 | UIDType string `json:"-"`
24 | OpenID string `json:"open_id,omitempty"`
25 | Email string `json:"email,omitempty"`
26 | UserID string `json:"user_id,omitempty"`
27 | ChatID string `json:"chat_id,omitempty"`
28 | UnionID string `json:"-"`
29 | // For reply
30 | RootID string `json:"root_id,omitempty"`
31 | ReplyInThread bool `json:"reply_in_thread,omitempty"`
32 | // Sign for notification bot
33 | Sign string `json:"sign"`
34 | // Timestamp for sign
35 | Timestamp int64 `json:"timestamp"`
36 | // UUID for idempotency
37 | UUID string `json:"uuid"`
38 | }
39 |
40 | // CardContent struct of card content
41 | type CardContent map[string]interface{}
42 |
43 | // MessageContent struct of message content
44 | type MessageContent struct {
45 | Text *TextContent `json:"text,omitempty"`
46 | Image *ImageContent `json:"image,omitempty"`
47 | Post *PostContent `json:"post,omitempty"`
48 | Card *CardContent `json:"card,omitempty"`
49 | ShareChat *ShareChatContent `json:"share_chat,omitempty"`
50 | ShareUser *ShareUserContent `json:"share_user,omitempty"`
51 | Audio *AudioContent `json:"audio,omitempty"`
52 | Media *MediaContent `json:"media,omitempty"`
53 | File *FileContent `json:"file,omitempty"`
54 | Sticker *StickerContent `json:"sticker,omitempty"`
55 | Template *TemplateContent `json:"template,omitempty"`
56 | }
57 |
58 | // TextContent .
59 | type TextContent struct {
60 | Text string `json:"text"`
61 | }
62 |
63 | // ImageContent .
64 | type ImageContent struct {
65 | ImageKey string `json:"image_key"`
66 | }
67 |
68 | // ShareChatContent .
69 | type ShareChatContent struct {
70 | ChatID string `json:"chat_id"`
71 | }
72 |
73 | // ShareUserContent .
74 | type ShareUserContent struct {
75 | UserID string `json:"user_id"`
76 | }
77 |
78 | // AudioContent .
79 | type AudioContent struct {
80 | FileKey string `json:"file_key"`
81 | }
82 |
83 | // MediaContent .
84 | type MediaContent struct {
85 | FileName string `json:"file_name,omitempty"`
86 | FileKey string `json:"file_key"`
87 | ImageKey string `json:"image_key"`
88 | Duration int `json:"duration,omitempty"`
89 | }
90 |
91 | // FileContent .
92 | type FileContent struct {
93 | FileName string `json:"file_name,omitempty"`
94 | FileKey string `json:"file_key"`
95 | }
96 |
97 | // StickerContent .
98 | type StickerContent struct {
99 | FileKey string `json:"file_key"`
100 | }
101 |
102 | // TemplateContent .
103 | type TemplateContent struct {
104 | Type string `json:"type"`
105 | Data templateData `json:"data,omitempty"`
106 | }
107 |
108 | type templateData struct {
109 | TemplateID string `json:"template_id"`
110 | TemplateVersionName string `json:"template_version_name,omitempty"`
111 | TemplateVariable map[string]interface{} `json:"template_variable,omitempty"`
112 | }
113 |
--------------------------------------------------------------------------------
/message_v1.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import "strconv"
4 |
5 | // BuildOutcomingMessageReq for msg builder
6 | func BuildOutcomingMessageReq(om OutcomingMessage) map[string]interface{} {
7 | params := map[string]interface{}{
8 | "msg_type": om.MsgType,
9 | "chat_id": om.ChatID, // request must contain chat_id, even if it is empty
10 | }
11 | params[om.UIDType] = buildReceiveID(om)
12 | if len(om.RootID) > 0 {
13 | params["root_id"] = om.RootID
14 | }
15 | content := make(map[string]interface{})
16 | if om.Content.Text != nil {
17 | content["text"] = om.Content.Text.Text
18 | }
19 | if om.Content.Image != nil {
20 | content["image_key"] = om.Content.Image.ImageKey
21 | }
22 | if om.Content.ShareChat != nil {
23 | content["share_open_chat_id"] = om.Content.ShareChat.ChatID
24 | }
25 | if om.Content.Post != nil {
26 | content["post"] = *om.Content.Post
27 | }
28 | if om.MsgType == MsgInteractive && om.Content.Card != nil {
29 | params["card"] = *om.Content.Card
30 | }
31 | if len(om.Sign) > 0 {
32 | params["sign"] = om.Sign
33 | params["timestamp"] = strconv.FormatInt(om.Timestamp, 10)
34 | }
35 | params["content"] = content
36 | return params
37 | }
38 |
39 | func buildOutcomingNotification(om OutcomingMessage) map[string]interface{} {
40 | params := map[string]interface{}{
41 | "msg_type": om.MsgType,
42 | }
43 | if len(om.RootID) > 0 {
44 | params["root_id"] = om.RootID
45 | }
46 | content := make(map[string]interface{})
47 | if om.Content.Text != nil {
48 | content["text"] = om.Content.Text.Text
49 | }
50 | if om.Content.Image != nil {
51 | content["image_key"] = om.Content.Image.ImageKey
52 | }
53 | if om.Content.ShareChat != nil {
54 | content["share_open_chat_id"] = om.Content.ShareChat.ChatID
55 | }
56 | if om.Content.Post != nil {
57 | content["post"] = *om.Content.Post
58 | }
59 | if om.MsgType == MsgInteractive && om.Content.Card != nil {
60 | params["card"] = *om.Content.Card
61 | }
62 | if len(om.Sign) > 0 {
63 | params["sign"] = om.Sign
64 | params["timestamp"] = strconv.FormatInt(om.Timestamp, 10)
65 | }
66 | params["content"] = content
67 | return params
68 | }
69 |
--------------------------------------------------------------------------------
/message_v2.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | // BuildMessage .
8 | func BuildMessage(om OutcomingMessage) (*IMMessageRequest, error) {
9 | req := IMMessageRequest{
10 | MsgType: string(om.MsgType),
11 | Content: buildContent(om),
12 | ReceiveID: buildReceiveID(om),
13 | }
14 | if req.ReceiveID == "" {
15 | return nil, ErrInvalidReceiveID
16 | }
17 | if req.Content == "" {
18 | return nil, ErrMessageNotBuild
19 | }
20 | if om.UUID != "" {
21 | req.UUID = om.UUID
22 | }
23 | return &req, nil
24 | }
25 |
26 | func buildReplyMessage(om OutcomingMessage) (*IMMessageRequest, error) {
27 | req := IMMessageRequest{
28 | MsgType: string(om.MsgType),
29 | Content: buildContent(om),
30 | ReceiveID: buildReceiveID(om),
31 | }
32 | if req.Content == "" {
33 | return nil, ErrMessageNotBuild
34 | }
35 | if om.ReplyInThread {
36 | req.ReplyInThread = om.ReplyInThread
37 | }
38 | if om.UUID != "" {
39 | req.UUID = om.UUID
40 | }
41 |
42 | return &req, nil
43 | }
44 |
45 | func buildUpdateMessage(om OutcomingMessage) (*IMMessageRequest, error) {
46 | req := IMMessageRequest{
47 | Content: buildContent(om),
48 | }
49 | if om.MsgType != MsgInteractive {
50 | req.MsgType = om.MsgType
51 | }
52 | if req.Content == "" {
53 | return nil, ErrMessageNotBuild
54 | }
55 |
56 | return &req, nil
57 | }
58 |
59 | func buildContent(om OutcomingMessage) string {
60 | var (
61 | content = ""
62 | b []byte
63 | err error
64 | )
65 | switch om.MsgType {
66 | case MsgText:
67 | b, err = json.Marshal(om.Content.Text)
68 | case MsgImage:
69 | b, err = json.Marshal(om.Content.Image)
70 | case MsgFile:
71 | b, err = json.Marshal(om.Content.File)
72 | case MsgShareCard:
73 | b, err = json.Marshal(om.Content.ShareChat)
74 | case MsgShareUser:
75 | b, err = json.Marshal(om.Content.ShareUser)
76 | case MsgPost:
77 | b, err = json.Marshal(om.Content.Post)
78 | case MsgInteractive:
79 | if om.Content.Card != nil {
80 | b, err = json.Marshal(om.Content.Card)
81 | } else if om.Content.Template != nil {
82 | b, err = json.Marshal(om.Content.Template)
83 | }
84 | case MsgAudio:
85 | b, err = json.Marshal(om.Content.Audio)
86 | case MsgMedia:
87 | b, err = json.Marshal(om.Content.Media)
88 | case MsgSticker:
89 | b, err = json.Marshal(om.Content.Sticker)
90 | }
91 | if err != nil {
92 | return ""
93 | }
94 | content = string(b)
95 |
96 | return content
97 | }
98 |
99 | func buildReceiveID(om OutcomingMessage) string {
100 | switch om.UIDType {
101 | case UIDEmail:
102 | return om.Email
103 | case UIDUserID:
104 | return om.UserID
105 | case UIDOpenID:
106 | return om.OpenID
107 | case UIDChatID:
108 | return om.ChatID
109 | case UIDUnionID:
110 | return om.UnionID
111 | }
112 | return ""
113 | }
114 |
--------------------------------------------------------------------------------
/msg_buf.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | )
7 |
8 | // MsgBuffer stores all the messages attached
9 | // You can call every function, but some of which is only available for specific condition
10 | type MsgBuffer struct {
11 | // Message type
12 | msgType string
13 | // Output
14 | message OutcomingMessage
15 |
16 | err error
17 | }
18 |
19 | // NewMsgBuffer create a message buffer
20 | func NewMsgBuffer(newMsgType string) *MsgBuffer {
21 | msgBuffer := MsgBuffer{
22 | message: OutcomingMessage{
23 | MsgType: newMsgType,
24 | },
25 | msgType: newMsgType,
26 | }
27 | return &msgBuffer
28 | }
29 |
30 | // BindOpenID binds open_id
31 | func (m *MsgBuffer) BindOpenID(openID string) *MsgBuffer {
32 | m.message.OpenID = openID
33 | m.message.UIDType = UIDOpenID
34 | return m
35 | }
36 |
37 | // BindEmail binds email
38 | func (m *MsgBuffer) BindEmail(email string) *MsgBuffer {
39 | m.message.Email = email
40 | m.message.UIDType = UIDEmail
41 | return m
42 | }
43 |
44 | // BindChatID binds chat_id
45 | func (m *MsgBuffer) BindChatID(chatID string) *MsgBuffer {
46 | m.message.ChatID = chatID
47 | m.message.UIDType = UIDChatID
48 | return m
49 | }
50 |
51 | // BindOpenChatID binds open_chat_id
52 | func (m *MsgBuffer) BindOpenChatID(openChatID string) *MsgBuffer {
53 | m.BindChatID(openChatID)
54 | m.message.UIDType = UIDChatID
55 | return m
56 | }
57 |
58 | // BindUserID binds open_id
59 | func (m *MsgBuffer) BindUserID(userID string) *MsgBuffer {
60 | m.message.UserID = userID
61 | m.message.UIDType = UIDUserID
62 | return m
63 | }
64 |
65 | // BindUnionID binds union_id
66 | func (m *MsgBuffer) BindUnionID(unionID string) *MsgBuffer {
67 | m.message.UnionID = unionID
68 | m.message.UIDType = UIDUnionID
69 | return m
70 | }
71 |
72 | // BindReply binds root id for reply
73 | // rootID is OpenMessageID of the message you reply
74 | func (m *MsgBuffer) BindReply(rootID string) *MsgBuffer {
75 | m.message.RootID = rootID
76 | return m
77 | }
78 |
79 | // ReplyInThread replies message in thread
80 | func (m *MsgBuffer) ReplyInThread(replyInThread bool) *MsgBuffer {
81 | m.message.ReplyInThread = replyInThread
82 | return m
83 | }
84 |
85 | // WithSign generates sign for notification bot check
86 | func (m *MsgBuffer) WithSign(secret string, ts int64) *MsgBuffer {
87 | m.message.Sign, _ = GenSign(secret, ts)
88 | m.message.Timestamp = ts
89 | return m
90 | }
91 |
92 | // WithUUID add UUID to message for idempotency
93 | func (m *MsgBuffer) WithUUID(uuid string) *MsgBuffer {
94 | m.message.UUID = uuid
95 | return m
96 | }
97 |
98 | func (m MsgBuffer) typeError(funcName string, msgType string) error {
99 | return fmt.Errorf("`%s` is only available to `%s`", funcName, msgType)
100 | }
101 |
102 | // Text attaches text
103 | func (m *MsgBuffer) Text(text string) *MsgBuffer {
104 | if m.msgType != MsgText {
105 | m.err = m.typeError("Text", MsgText)
106 | return m
107 | }
108 | m.message.Content.Text = &TextContent{
109 | Text: text,
110 | }
111 | return m
112 | }
113 |
114 | // Image attaches image key
115 | // for MsgImage only
116 | func (m *MsgBuffer) Image(imageKey string) *MsgBuffer {
117 | if m.msgType != MsgImage {
118 | m.err = m.typeError("Image", MsgImage)
119 | return m
120 | }
121 | m.message.Content.Image = &ImageContent{
122 | ImageKey: imageKey,
123 | }
124 | return m
125 | }
126 |
127 | // ShareChat attaches chat id
128 | // for MsgShareChat only
129 | func (m *MsgBuffer) ShareChat(chatID string) *MsgBuffer {
130 | if m.msgType != MsgShareCard {
131 | m.err = m.typeError("ShareChat", MsgShareCard)
132 | return m
133 | }
134 | m.message.Content.ShareChat = &ShareChatContent{
135 | ChatID: chatID,
136 | }
137 | return m
138 | }
139 |
140 | // ShareUser attaches user id
141 | // for MsgShareUser only
142 | func (m *MsgBuffer) ShareUser(userID string) *MsgBuffer {
143 | if m.msgType != MsgShareUser {
144 | m.err = m.typeError("ShareUser", MsgShareUser)
145 | return m
146 | }
147 | m.message.Content.ShareUser = &ShareUserContent{
148 | UserID: userID,
149 | }
150 | return m
151 | }
152 |
153 | // File attaches file
154 | // for MsgFile only
155 | func (m *MsgBuffer) File(fileKey string) *MsgBuffer {
156 | if m.msgType != MsgFile {
157 | m.err = m.typeError("File", MsgFile)
158 | return m
159 | }
160 | m.message.Content.File = &FileContent{
161 | FileKey: fileKey,
162 | }
163 | return m
164 | }
165 |
166 | // Audio attaches audio
167 | // for MsgAudio only
168 | func (m *MsgBuffer) Audio(fileKey string) *MsgBuffer {
169 | if m.msgType != MsgAudio {
170 | m.err = m.typeError("Audio", MsgAudio)
171 | return m
172 | }
173 | m.message.Content.Audio = &AudioContent{
174 | FileKey: fileKey,
175 | }
176 | return m
177 | }
178 |
179 | // Media attaches media
180 | // for MsgMedia only
181 | func (m *MsgBuffer) Media(fileKey, imageKey string) *MsgBuffer {
182 | if m.msgType != MsgMedia {
183 | m.err = m.typeError("Media", MsgMedia)
184 | return m
185 | }
186 | m.message.Content.Media = &MediaContent{
187 | FileKey: fileKey,
188 | ImageKey: imageKey,
189 | }
190 | return m
191 | }
192 |
193 | // Sticker attaches sticker
194 | // for MsgSticker only
195 | func (m *MsgBuffer) Sticker(fileKey string) *MsgBuffer {
196 | if m.msgType != MsgSticker {
197 | m.err = m.typeError("Sticker", MsgSticker)
198 | return m
199 | }
200 | m.message.Content.Sticker = &StickerContent{
201 | FileKey: fileKey,
202 | }
203 | return m
204 | }
205 |
206 | // Post sets raw post content
207 | func (m *MsgBuffer) Post(postContent *PostContent) *MsgBuffer {
208 | if m.msgType != MsgPost {
209 | m.err = m.typeError("Post", MsgPost)
210 | return m
211 | }
212 | m.message.Content.Post = postContent
213 | return m
214 | }
215 |
216 | // Card binds card content with V4 format
217 | func (m *MsgBuffer) Card(cardContent string) *MsgBuffer {
218 | if m.msgType != MsgInteractive {
219 | m.err = m.typeError("Card", MsgInteractive)
220 | return m
221 | }
222 | card := make(CardContent)
223 | _ = json.Unmarshal([]byte(cardContent), &card)
224 | m.message.Content.Card = &card
225 | return m
226 | }
227 |
228 | // Template sets raw template content
229 | func (m *MsgBuffer) Template(tempateContent *TemplateContent) *MsgBuffer {
230 | if m.msgType != MsgInteractive {
231 | m.err = m.typeError("Template", MsgInteractive)
232 | return m
233 | }
234 | m.message.Content.Template = tempateContent
235 | return m
236 | }
237 |
238 | // Build message and return message body
239 | func (m *MsgBuffer) Build() OutcomingMessage {
240 | return m.message
241 | }
242 |
243 | // Error returns last error
244 | func (m *MsgBuffer) Error() error {
245 | return m.err
246 | }
247 |
248 | // Clear message in buffer
249 | func (m *MsgBuffer) Clear() *MsgBuffer {
250 | m.message = OutcomingMessage{
251 | MsgType: m.msgType,
252 | }
253 | m.err = nil
254 | return m
255 | }
256 |
--------------------------------------------------------------------------------
/msg_buf_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestAttachText(t *testing.T) {
10 | mb := NewMsgBuffer(MsgText)
11 | msg := mb.Text("hello").Build()
12 | assert.Equal(t, "hello", msg.Content.Text.Text)
13 | }
14 |
15 | func TestAttachImage(t *testing.T) {
16 | mb := NewMsgBuffer(MsgImage)
17 | msg := mb.Image("aaaaa").Build()
18 | assert.Equal(t, "aaaaa", msg.Content.Image.ImageKey)
19 | }
20 |
21 | func TestMsgTextBinding(t *testing.T) {
22 | mb := NewMsgBuffer(MsgText)
23 | msg := mb.Text("hello, world").BindEmail(testUserEmail).Build()
24 | assert.Equal(t, "hello, world", msg.Content.Text.Text)
25 | assert.Equal(t, testUserEmail, msg.Email)
26 | }
27 |
28 | func TestBindingUserIDs(t *testing.T) {
29 | mb := NewMsgBuffer(MsgText)
30 | msgEmail := mb.BindEmail(testUserEmail).Build()
31 | assert.Equal(t, testUserEmail, msgEmail.Email)
32 |
33 | mb.Clear()
34 | msgOpenChatID := mb.BindOpenChatID(testGroupChatID).Build()
35 | assert.Equal(t, testGroupChatID, msgOpenChatID.ChatID)
36 |
37 | mb.Clear()
38 | msgUserID := mb.BindUserID("333444").Build()
39 | assert.Equal(t, "333444", msgUserID.UserID)
40 |
41 | mb.Clear()
42 | msgReplyID := mb.BindReply("om_f779ffe0ffa3d1b94fc1ef5fcb6f1063").Build()
43 | assert.Equal(t, "om_f779ffe0ffa3d1b94fc1ef5fcb6f1063", msgReplyID.RootID)
44 | mb.ReplyInThread(true)
45 | assert.False(t, msgReplyID.ReplyInThread)
46 | }
47 |
48 | func TestMsgShareChat(t *testing.T) {
49 | mb := NewMsgBuffer(MsgShareCard)
50 | msg := mb.ShareChat("6559399282837815565").Build()
51 | assert.Equal(t, MsgShareCard, msg.MsgType)
52 | assert.Equal(t, "6559399282837815565", msg.Content.ShareChat.ChatID)
53 | }
54 |
55 | func TestMsgShareUser(t *testing.T) {
56 | mb := NewMsgBuffer(MsgShareUser)
57 | msg := mb.ShareUser("334455").Build()
58 | assert.Equal(t, MsgShareUser, msg.MsgType)
59 | assert.Equal(t, "334455", msg.Content.ShareUser.UserID)
60 | }
61 |
62 | func TestMsgFile(t *testing.T) {
63 | mb := NewMsgBuffer(MsgFile)
64 | msg := mb.File("file_v2_71cafb2c-137f-4bb0-8381-ffd4971dbecg").Build()
65 | assert.Equal(t, MsgFile, msg.MsgType)
66 | assert.Equal(t, "file_v2_71cafb2c-137f-4bb0-8381-ffd4971dbecg", msg.Content.File.FileKey)
67 | }
68 |
69 | func TestMsgAudio(t *testing.T) {
70 | mb := NewMsgBuffer(MsgAudio)
71 | msg := mb.Audio("file_v2_71cafb2c-137f-4bb0-8381-ffd4971dbecg").Build()
72 | assert.Equal(t, MsgAudio, msg.MsgType)
73 | assert.Equal(t, "file_v2_71cafb2c-137f-4bb0-8381-ffd4971dbecg", msg.Content.Audio.FileKey)
74 | }
75 |
76 | func TestMsgMedia(t *testing.T) {
77 | mb := NewMsgBuffer(MsgMedia)
78 | msg := mb.Media("file_v2_b53cd6cc-5327-4968-8bf6-4528deb3068g", "img_v2_b276195a-9ae0-4fec-bbfe-f74b4d5a994g").Build()
79 | assert.Equal(t, MsgMedia, msg.MsgType)
80 | assert.Equal(t, "file_v2_b53cd6cc-5327-4968-8bf6-4528deb3068g", msg.Content.Media.FileKey)
81 | assert.Equal(t, "img_v2_b276195a-9ae0-4fec-bbfe-f74b4d5a994g", msg.Content.Media.ImageKey)
82 | }
83 |
84 | func TestMsgSticker(t *testing.T) {
85 | mb := NewMsgBuffer(MsgSticker)
86 | msg := mb.Sticker("4ba009df-2453-47b3-a753-444b152217bg").Build()
87 | assert.Equal(t, MsgSticker, msg.MsgType)
88 | assert.Equal(t, "4ba009df-2453-47b3-a753-444b152217bg", msg.Content.Sticker.FileKey)
89 | }
90 |
91 | func TestMsgWithWrongType(t *testing.T) {
92 | mb := NewMsgBuffer(MsgText)
93 | mb.ShareChat("6559399282837815565")
94 | assert.Equal(t, mb.Error().Error(), "`ShareChat` is only available to `share_chat`")
95 | mb.ShareUser("334455")
96 | assert.Equal(t, mb.Error().Error(), "`ShareUser` is only available to `share_user`")
97 | mb.Image("aaa")
98 | assert.Equal(t, mb.Error().Error(), "`Image` is only available to `image`")
99 | mb.File("aaa")
100 | assert.Equal(t, mb.Error().Error(), "`File` is only available to `file`")
101 | mb.Audio("aaa")
102 | assert.Equal(t, mb.Error().Error(), "`Audio` is only available to `audio`")
103 | mb.Media("aaa", "bbb")
104 | assert.Equal(t, mb.Error().Error(), "`Media` is only available to `media`")
105 | mb.Sticker("aaa")
106 | assert.Equal(t, mb.Error().Error(), "`Sticker` is only available to `sticker`")
107 | mb.Post(nil)
108 | assert.Equal(t, mb.Error().Error(), "`Post` is only available to `post`")
109 | mb.Card("nil")
110 | assert.Equal(t, mb.Error().Error(), "`Card` is only available to `interactive`")
111 | mbp := NewMsgBuffer(MsgPost)
112 | mbp.Text("hello")
113 | assert.Equal(t, mbp.Error().Error(), "`Text` is only available to `text`")
114 | }
115 |
116 | func TestClearMessage(t *testing.T) {
117 | mb := NewMsgBuffer(MsgText)
118 | mb.Text("hello, world").Build()
119 | assert.Equal(t, "hello, world", mb.message.Content.Text.Text)
120 | mb.Clear()
121 | assert.Equal(t, MsgText, mb.msgType)
122 | assert.Empty(t, mb.message.Content)
123 | mb.Text("attach again").Build()
124 | assert.Equal(t, "attach again", mb.message.Content.Text.Text)
125 | }
126 |
127 | func TestWorkWithTextBuilder(t *testing.T) {
128 | mb := NewMsgBuffer(MsgText)
129 | mb.Text(NewTextBuilder().Textln("hello, world").Render()).Build()
130 | assert.Equal(t, "hello, world\n", mb.message.Content.Text.Text)
131 | }
132 |
133 | func TestWithSign(t *testing.T) {
134 | mb := NewMsgBuffer(MsgText)
135 | assert.Empty(t, mb.message.Sign)
136 | msg := mb.WithSign("xxx", 1661860880).Build()
137 | assert.NotEmpty(t, mb.message.Sign)
138 | assert.Equal(t, "QnWVTSBe6FmQDE0bG6X0mURbI+DnvVyu1h+j5dHOjrU=", msg.Sign)
139 | }
140 |
141 | func TestWithUUID(t *testing.T) {
142 | mb := NewMsgBuffer(MsgText)
143 | assert.Empty(t, mb.message.UUID)
144 | msg := mb.WithUUID("abc-def-0000").Build()
145 | assert.NotEmpty(t, mb.message.UUID)
146 | assert.Equal(t, "abc-def-0000", msg.UUID)
147 | }
148 |
--------------------------------------------------------------------------------
/msg_card_builder.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "github.com/go-lark/card-builder"
5 | "github.com/go-lark/card-builder/i18n"
6 | )
7 |
8 | type i18nCardBuilder struct{}
9 |
10 | // CardBuilder .
11 | type CardBuilder struct {
12 | I18N *i18nCardBuilder
13 | }
14 |
15 | // Card wraps i18n card
16 | func (i18nCardBuilder) Card(blocks ...*i18n.LocalizedBlock) *i18n.Block {
17 | return i18n.Card(blocks...)
18 | }
19 |
20 | func (i18nCardBuilder) WithLocale(locale string, elements ...card.Element) *i18n.LocalizedBlock {
21 | return i18n.WithLocale(locale, elements...)
22 | }
23 |
24 | // Title wraps i18n title block
25 | func (i18nCardBuilder) LocalizedText(locale, s string) *i18n.LocalizedTextBlock {
26 | return i18n.LocalizedText(locale, s)
27 | }
28 |
29 | // NewCardBuilder 新建卡片构造器
30 | func NewCardBuilder() *CardBuilder {
31 | return &CardBuilder{
32 | I18N: &i18nCardBuilder{},
33 | }
34 | }
35 |
36 | // Card 包裹了最外层的卡片结构
37 | func (CardBuilder) Card(elements ...card.Element) *card.Block {
38 | return card.Card(elements...)
39 | }
40 |
41 | // Action 交互元素,可添加 Button, SelectMenu, Overflow, DatePicker, TimePicker, DatetimePicker
42 | func (CardBuilder) Action(actions ...card.Element) *card.ActionBlock {
43 | return card.Action(actions...)
44 | }
45 |
46 | // Button 按钮交互元素
47 | func (CardBuilder) Button(text *card.TextBlock) *card.ButtonBlock {
48 | return card.Button(text)
49 | }
50 |
51 | // Confirm 用于交互元素的二次确认
52 | func (CardBuilder) Confirm(title, text string) *card.ConfirmBlock {
53 | return card.Confirm(title, text)
54 | }
55 |
56 | // DatePicker 日期选择器
57 | func (CardBuilder) DatePicker() *card.DatePickerBlock {
58 | return card.DatePicker()
59 | }
60 |
61 | // TimePicker 时间选择器
62 | func (CardBuilder) TimePicker() *card.TimePickerBlock {
63 | return card.TimePicker()
64 | }
65 |
66 | // DatetimePicker 日期时间选择器
67 | func (CardBuilder) DatetimePicker() *card.DatetimePickerBlock {
68 | return card.DatetimePicker()
69 | }
70 |
71 | // Div 内容模块
72 | func (CardBuilder) Div(fields ...*card.FieldBlock) *card.DivBlock {
73 | return card.Div(fields...)
74 | }
75 |
76 | // Field 内容模块的排版元素
77 | func (CardBuilder) Field(text *card.TextBlock) *card.FieldBlock {
78 | return card.Field(text)
79 | }
80 |
81 | // Hr 分割线模块
82 | func (CardBuilder) Hr() *card.HrBlock {
83 | return card.Hr()
84 | }
85 |
86 | // Img 图片展示模块
87 | func (CardBuilder) Img(key string) *card.ImgBlock {
88 | return card.Img(key)
89 | }
90 |
91 | // Note 备注模块
92 | func (CardBuilder) Note() *card.NoteBlock {
93 | return card.Note()
94 | }
95 |
96 | // Option 选项模块,可用于 SelectMenu 和 Overflow
97 | func (CardBuilder) Option(value string) *card.OptionBlock {
98 | return card.Option(value)
99 | }
100 |
101 | // Overflow 折叠按钮菜单组件
102 | func (CardBuilder) Overflow(options ...*card.OptionBlock) *card.OverflowBlock {
103 | return card.Overflow(options...)
104 | }
105 |
106 | // SelectMenu 菜单组件
107 | func (CardBuilder) SelectMenu(options ...*card.OptionBlock) *card.SelectMenuBlock {
108 | return card.SelectMenu(options...)
109 | }
110 |
111 | // Text 文本模块
112 | func (CardBuilder) Text(s string) *card.TextBlock {
113 | return card.Text(s)
114 | }
115 |
116 | // Markdown 单独使用的 Markdown 文本模块
117 | func (CardBuilder) Markdown(s string) *card.MarkdownBlock {
118 | return card.Markdown(s)
119 | }
120 |
121 | // URL 链接模块
122 | func (CardBuilder) URL() *card.URLBlock {
123 | return card.URL()
124 | }
125 |
126 | // ColumnSet column set module
127 | func (CardBuilder) ColumnSet(columns ...*card.ColumnBlock) *card.ColumnSetBlock {
128 | return card.ColumnSet(columns...)
129 | }
130 |
131 | // Column column module
132 | func (CardBuilder) Column(elements ...card.Element) *card.ColumnBlock {
133 | return card.Column(elements...)
134 | }
135 |
136 | // ColumnSetAction column action module
137 | func (CardBuilder) ColumnSetAction(url *card.URLBlock) *card.ColumnSetActionBlock {
138 | return card.ColumnSetAction(url)
139 | }
140 |
--------------------------------------------------------------------------------
/msg_post_builder.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // PostContent .
4 | type PostContent map[string]PostBody
5 |
6 | // PostBody .
7 | type PostBody struct {
8 | Title string `json:"title"`
9 | Content [][]PostElem `json:"content"`
10 | }
11 |
12 | // PostElem .
13 | type PostElem struct {
14 | Tag string `json:"tag"`
15 | // For Text
16 | UnEscape *bool `json:"un_escape,omitempty"`
17 | Text *string `json:"text,omitempty"`
18 | Lines *int `json:"lines,omitempty"`
19 | // For Link
20 | Href *string `json:"href,omitempty"`
21 | // For At
22 | UserID *string `json:"user_id,omitempty"`
23 | // For Image
24 | ImageKey *string `json:"image_key,omitempty"`
25 | ImageWidth *int `json:"width,omitempty"`
26 | ImageHeight *int `json:"height,omitempty"`
27 | }
28 |
29 | const (
30 | msgPostText = "text"
31 | msgPostLink = "a"
32 | msgPostAt = "at"
33 | msgPostImage = "img"
34 | )
35 |
36 | // PostBuf .
37 | type PostBuf struct {
38 | Title string `json:"title"`
39 | Content []PostElem `json:"content"`
40 | }
41 |
42 | // MsgPostBuilder for build text buf
43 | type MsgPostBuilder struct {
44 | buf map[string]*PostBuf
45 | curLocale string
46 | }
47 |
48 | const defaultLocale = LocaleZhCN
49 |
50 | // NewPostBuilder creates a text builder
51 | func NewPostBuilder() *MsgPostBuilder {
52 | return &MsgPostBuilder{
53 | buf: make(map[string]*PostBuf),
54 | curLocale: defaultLocale,
55 | }
56 | }
57 |
58 | // Locale renamed to WithLocale but still available
59 | func (pb *MsgPostBuilder) Locale(locale string) *MsgPostBuilder {
60 | return pb.WithLocale(locale)
61 | }
62 |
63 | // WithLocale switches to locale and returns self
64 | func (pb *MsgPostBuilder) WithLocale(locale string) *MsgPostBuilder {
65 | if _, ok := pb.buf[locale]; !ok {
66 | pb.buf[locale] = &PostBuf{}
67 | }
68 |
69 | pb.curLocale = locale
70 | return pb
71 | }
72 |
73 | // CurLocale switches to locale and returns the buffer of that locale
74 | func (pb *MsgPostBuilder) CurLocale() *PostBuf {
75 | return pb.WithLocale(pb.curLocale).buf[pb.curLocale]
76 | }
77 |
78 | // Title sets title
79 | func (pb *MsgPostBuilder) Title(title string) *MsgPostBuilder {
80 | pb.CurLocale().Title = title
81 | return pb
82 | }
83 |
84 | // TextTag creates a text tag
85 | func (pb *MsgPostBuilder) TextTag(text string, lines int, unescape bool) *MsgPostBuilder {
86 | pe := PostElem{
87 | Tag: msgPostText,
88 | Text: &text,
89 | Lines: &lines,
90 | UnEscape: &unescape,
91 | }
92 | pb.CurLocale().Content = append(pb.CurLocale().Content, pe)
93 | return pb
94 | }
95 |
96 | // LinkTag creates a link tag
97 | func (pb *MsgPostBuilder) LinkTag(text, href string) *MsgPostBuilder {
98 | pe := PostElem{
99 | Tag: msgPostLink,
100 | Text: &text,
101 | Href: &href,
102 | }
103 | pb.CurLocale().Content = append(pb.CurLocale().Content, pe)
104 | return pb
105 | }
106 |
107 | // AtTag creates an at tag
108 | func (pb *MsgPostBuilder) AtTag(text, userID string) *MsgPostBuilder {
109 | pe := PostElem{
110 | Tag: msgPostAt,
111 | Text: &text,
112 | UserID: &userID,
113 | }
114 | pb.CurLocale().Content = append(pb.CurLocale().Content, pe)
115 | return pb
116 | }
117 |
118 | // ImageTag creates an image tag
119 | func (pb *MsgPostBuilder) ImageTag(imageKey string, imageWidth, imageHeight int) *MsgPostBuilder {
120 | pe := PostElem{
121 | Tag: msgPostImage,
122 | ImageKey: &imageKey,
123 | ImageWidth: &imageWidth,
124 | ImageHeight: &imageHeight,
125 | }
126 | pb.CurLocale().Content = append(pb.CurLocale().Content, pe)
127 | return pb
128 | }
129 |
130 | // Clear all message
131 | func (pb *MsgPostBuilder) Clear() {
132 | pb.curLocale = defaultLocale
133 | pb.buf = make(map[string]*PostBuf)
134 | }
135 |
136 | // Render message
137 | func (pb *MsgPostBuilder) Render() *PostContent {
138 | content := make(PostContent)
139 | for locale, buf := range pb.buf {
140 | content[locale] = PostBody{
141 | Title: buf.Title,
142 | Content: [][]PostElem{buf.Content},
143 | }
144 | }
145 | return &content
146 | }
147 |
148 | // Len returns buf len
149 | func (pb MsgPostBuilder) Len() int {
150 | return len(pb.CurLocale().Content)
151 | }
152 |
--------------------------------------------------------------------------------
/msg_post_builder_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestPostLocale(t *testing.T) {
10 | pb := NewPostBuilder()
11 | assert.Equal(t, defaultLocale, pb.curLocale)
12 | pb.Locale(LocaleEnUS)
13 | assert.Equal(t, LocaleEnUS, pb.curLocale)
14 | pb.WithLocale(LocaleJaJP)
15 | assert.Equal(t, LocaleJaJP, pb.curLocale)
16 | }
17 |
18 | func TestPostTitle(t *testing.T) {
19 | pb := NewPostBuilder()
20 | pb.Title("title")
21 | assert.Equal(t, "title", pb.CurLocale().Title)
22 | }
23 |
24 | func TestPostTextTag(t *testing.T) {
25 | pb := NewPostBuilder()
26 | pb.TextTag("hello, world", 1, true)
27 | buf := pb.CurLocale().Content
28 | assert.Equal(t, "text", buf[0].Tag)
29 | assert.Equal(t, "hello, world", *(buf[0].Text))
30 | assert.Equal(t, 1, *(buf[0].Lines))
31 | assert.Equal(t, true, *(buf[0].UnEscape))
32 | }
33 |
34 | func TestPostLinkTag(t *testing.T) {
35 | pb := NewPostBuilder()
36 | pb.LinkTag("hello, world", "https://www.toutiao.com/")
37 | buf := pb.CurLocale().Content
38 | assert.Equal(t, "a", buf[0].Tag)
39 | assert.Equal(t, "hello, world", *(buf[0].Text))
40 | assert.Equal(t, "https://www.toutiao.com/", *(buf[0].Href))
41 | }
42 |
43 | func TestPostAtTag(t *testing.T) {
44 | pb := NewPostBuilder()
45 | pb.AtTag("www", "123456")
46 | buf := pb.CurLocale().Content
47 | assert.Equal(t, "at", buf[0].Tag)
48 | assert.Equal(t, "www", *(buf[0].Text))
49 | assert.Equal(t, "123456", *(buf[0].UserID))
50 | }
51 |
52 | func TestPostImgTag(t *testing.T) {
53 | pb := NewPostBuilder()
54 | pb.ImageTag("d9f7d37e-c47c-411b-8ec6-9861132e6986", 320, 240)
55 | buf := pb.CurLocale().Content
56 | assert.Equal(t, "img", buf[0].Tag)
57 | assert.Equal(t, "d9f7d37e-c47c-411b-8ec6-9861132e6986", *(buf[0].ImageKey))
58 | assert.Equal(t, 240, *(buf[0].ImageHeight))
59 | assert.Equal(t, 320, *(buf[0].ImageWidth))
60 | }
61 |
62 | func TestPostClearAndLen(t *testing.T) {
63 | pb := NewPostBuilder()
64 | pb.TextTag("hello, world", 1, true).LinkTag("link", "https://www.toutiao.com/")
65 | assert.Equal(t, 2, pb.Len())
66 | pb.Clear()
67 | assert.Empty(t, pb.buf)
68 | assert.Equal(t, 0, pb.Len())
69 | }
70 |
71 | func TestPostMultiLocaleContent(t *testing.T) {
72 | pb := NewPostBuilder()
73 | pb.Title("中文标题")
74 | assert.Equal(t, "中文标题", pb.CurLocale().Title)
75 | pb.TextTag("你好世界", 1, true).TextTag("其他内容", 1, true)
76 | assert.Equal(t, 2, pb.Len())
77 |
78 | pb.WithLocale(LocaleEnUS).Title("en title")
79 | pb.TextTag("hello, world", 1, true).LinkTag("link", "https://www.toutiao.com/")
80 | assert.Equal(t, 2, pb.Len())
81 | assert.Equal(t, "en title", pb.CurLocale().Title)
82 |
83 | content := pb.Render()
84 | t.Log(content)
85 | assert.Equal(t, "中文标题", (*content)[LocaleZhCN].Title)
86 | assert.Equal(t, "en title", (*content)[LocaleEnUS].Title)
87 | }
88 |
--------------------------------------------------------------------------------
/msg_template_builder.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // MsgTemplateBuilder for build template
4 | type MsgTemplateBuilder struct {
5 | id string
6 | versionName string
7 | data map[string]interface{}
8 | }
9 |
10 | // NewTemplateBuilder creates a text builder
11 | func NewTemplateBuilder() *MsgTemplateBuilder {
12 | return &MsgTemplateBuilder{}
13 | }
14 |
15 | // BindTemplate .
16 | func (tb *MsgTemplateBuilder) BindTemplate(id, versionName string, data map[string]interface{}) *TemplateContent {
17 | tb.id = id
18 | tb.versionName = versionName
19 | tb.data = data
20 |
21 | tc := &TemplateContent{
22 | Type: "template",
23 | Data: templateData{
24 | TemplateID: tb.id,
25 | TemplateVersionName: tb.versionName,
26 | },
27 | }
28 |
29 | if data != nil {
30 | tc.Data.TemplateVariable = tb.data
31 | }
32 |
33 | return tc
34 | }
35 |
--------------------------------------------------------------------------------
/msg_template_builder_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestBindTemplate(t *testing.T) {
10 | b := NewTemplateBuilder()
11 | assert.Empty(t, b.id)
12 | assert.Empty(t, b.versionName)
13 | assert.Nil(t, b.data)
14 |
15 | _ = b.BindTemplate("AAqCYI07MQWh1", "1.0.0", map[string]interface{}{
16 | "name": "志田千陽",
17 | })
18 | assert.Equal(t, "AAqCYI07MQWh1", b.id)
19 | assert.Equal(t, "1.0.0", b.versionName)
20 | assert.NotEmpty(t, b.data)
21 | }
22 |
--------------------------------------------------------------------------------
/msg_text_builder.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // textElemType of a text buf
8 | type textElemType int
9 |
10 | type textElem struct {
11 | elemType textElemType
12 | content string
13 | }
14 |
15 | const (
16 | // MsgText text only message
17 | msgText textElemType = iota
18 | // MsgAt @somebody
19 | msgAt
20 | // MsgAtAll @all
21 | msgAtAll
22 | // msgSpace space
23 | msgSpace
24 | )
25 |
26 | // MsgTextBuilder for build text buf
27 | type MsgTextBuilder struct {
28 | buf []textElem
29 | }
30 |
31 | // NewTextBuilder creates a text builder
32 | func NewTextBuilder() *MsgTextBuilder {
33 | return &MsgTextBuilder{
34 | buf: make([]textElem, 0),
35 | }
36 | }
37 |
38 | // Text add simple texts
39 | func (tb *MsgTextBuilder) Text(text ...interface{}) *MsgTextBuilder {
40 | elem := textElem{
41 | elemType: msgText,
42 | content: fmt.Sprint(text...),
43 | }
44 | tb.buf = append(tb.buf, elem)
45 | return tb
46 | }
47 |
48 | // Textln add simple texts with a newline
49 | func (tb *MsgTextBuilder) Textln(text ...interface{}) *MsgTextBuilder {
50 | elem := textElem{
51 | elemType: msgText,
52 | content: fmt.Sprintln(text...),
53 | }
54 | tb.buf = append(tb.buf, elem)
55 | return tb
56 | }
57 |
58 | // Textf add texts with format
59 | func (tb *MsgTextBuilder) Textf(textFmt string, text ...interface{}) *MsgTextBuilder {
60 | elem := textElem{
61 | elemType: msgText,
62 | content: fmt.Sprintf(textFmt, text...),
63 | }
64 | tb.buf = append(tb.buf, elem)
65 | return tb
66 | }
67 |
68 | // Mention @somebody
69 | func (tb *MsgTextBuilder) Mention(userID string) *MsgTextBuilder {
70 | elem := textElem{
71 | elemType: msgAt,
72 | content: fmt.Sprintf("@user", userID),
73 | }
74 | tb.buf = append(tb.buf, elem)
75 | return tb
76 | }
77 |
78 | // MentionAll @all
79 | func (tb *MsgTextBuilder) MentionAll() *MsgTextBuilder {
80 | elem := textElem{
81 | elemType: msgAtAll,
82 | content: "@all",
83 | }
84 | tb.buf = append(tb.buf, elem)
85 | return tb
86 | }
87 |
88 | // Clear all message
89 | func (tb *MsgTextBuilder) Clear() {
90 | tb.buf = make([]textElem, 0)
91 | }
92 |
93 | // Render message
94 | func (tb *MsgTextBuilder) Render() string {
95 | var text string
96 | for _, msg := range tb.buf {
97 | text += msg.content
98 | }
99 | return text
100 | }
101 |
102 | // Len returns buf len
103 | func (tb MsgTextBuilder) Len() int {
104 | return len(tb.buf)
105 | }
106 |
--------------------------------------------------------------------------------
/msg_text_builder_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestUseText(t *testing.T) {
10 | tb := NewTextBuilder()
11 | msg := tb.Text("hello, ", "world", 123).Render()
12 | assert.Equal(t, "hello, world123", tb.buf[0].content)
13 | assert.Equal(t, "hello, world123", msg)
14 | }
15 |
16 | func TestTextTextf(t *testing.T) {
17 | tb := NewTextBuilder()
18 | msg := tb.Textf("hello, %s: %d", "world", 1).Render()
19 | assert.Equal(t, "hello, world: 1", tb.buf[0].content)
20 | assert.Equal(t, "hello, world: 1", msg)
21 | }
22 |
23 | func TestTextTextln(t *testing.T) {
24 | tb := NewTextBuilder()
25 | msg := tb.Textln("hello", "world").Render()
26 | assert.Equal(t, "hello world\n", tb.buf[0].content)
27 | assert.Equal(t, "hello world\n", msg)
28 | }
29 |
30 | func TestTextMention(t *testing.T) {
31 | tb := NewTextBuilder()
32 | msg := tb.Text("hello, world").Mention("6454030812462448910").Render()
33 | assert.Equal(t, "hello, world@user", msg)
34 | }
35 |
36 | func TestTextMentionAll(t *testing.T) {
37 | tb := NewTextBuilder()
38 | msg := tb.Text("hello, world").MentionAll().Render()
39 | assert.Equal(t, "hello, world@all", msg)
40 | }
41 |
42 | func TestTextClearAndLen(t *testing.T) {
43 | tb := NewTextBuilder()
44 | tb.Text("hello, world").MentionAll()
45 | assert.Equal(t, 2, tb.Len())
46 | tb.Clear()
47 | assert.Empty(t, tb.buf)
48 | assert.Equal(t, 0, tb.Len())
49 | }
50 |
--------------------------------------------------------------------------------
/scripts/test_v1.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | echo "mode: atomic" >coverage.txt
5 |
6 | go test -coverprofile=profile.out -covermode=atomic ./...
7 | if [ -f profile.out ]; then
8 | tail -q -n +2 profile.out >>coverage.txt
9 | rm profile.out
10 | fi
11 |
12 | go tool cover -func coverage.txt
13 |
--------------------------------------------------------------------------------
/scripts/test_v2.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | cd ./v2
6 | echo "mode: atomic" >coverage.txt
7 | go test -coverprofile=profile.out -covermode=atomic ./...
8 | if [ -f profile.out ]; then
9 | tail -q -n +2 profile.out >>coverage.txt
10 | rm profile.out
11 | fi
12 |
13 | go tool cover -func coverage.txt
14 |
--------------------------------------------------------------------------------
/user.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // UID types
4 | const (
5 | UIDEmail = "email"
6 | UIDUserID = "user_id"
7 | UIDOpenID = "open_id"
8 | UIDChatID = "chat_id"
9 | UIDUnionID = "union_id"
10 | )
11 |
12 | // OptionalUserID to contain openID, chatID, userID, email
13 | type OptionalUserID struct {
14 | UIDType string
15 | RealID string
16 | }
17 |
18 | func withOneID(uidType, realID string) *OptionalUserID {
19 | return &OptionalUserID{
20 | UIDType: uidType,
21 | RealID: realID,
22 | }
23 | }
24 |
25 | // WithEmail uses email as userID
26 | func WithEmail(email string) *OptionalUserID {
27 | return withOneID(UIDEmail, email)
28 | }
29 |
30 | // WithUserID uses userID as userID
31 | func WithUserID(userID string) *OptionalUserID {
32 | return withOneID(UIDUserID, userID)
33 | }
34 |
35 | // WithOpenID uses openID as userID
36 | func WithOpenID(openID string) *OptionalUserID {
37 | return withOneID(UIDOpenID, openID)
38 | }
39 |
40 | // WithChatID uses chatID as userID
41 | func WithChatID(chatID string) *OptionalUserID {
42 | return withOneID(UIDChatID, chatID)
43 | }
44 |
45 | // WithUnionID uses chatID as userID
46 | func WithUnionID(unionID string) *OptionalUserID {
47 | return withOneID(UIDUnionID, unionID)
48 | }
49 |
--------------------------------------------------------------------------------
/user_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestWithFunctions(t *testing.T) {
10 | emailUID := WithEmail(testUserEmail)
11 | assert.Equal(t, "email", emailUID.UIDType)
12 | assert.Equal(t, testUserEmail, emailUID.RealID)
13 |
14 | openIDUID := WithOpenID(testUserOpenID)
15 | assert.Equal(t, "open_id", openIDUID.UIDType)
16 | assert.Equal(t, testUserOpenID, openIDUID.RealID)
17 |
18 | chatIDUID := WithChatID(testGroupChatID)
19 | assert.Equal(t, "chat_id", chatIDUID.UIDType)
20 | assert.Equal(t, testGroupChatID, chatIDUID.RealID)
21 |
22 | fakeUID := "6893390418998738946"
23 | userIDUID := WithUserID(fakeUID)
24 | assert.Equal(t, "user_id", userIDUID.UIDType)
25 | assert.Equal(t, fakeUID, userIDUID.RealID)
26 | }
27 |
--------------------------------------------------------------------------------
/util.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "os"
7 | )
8 |
9 | // DownloadFile downloads from a URL to local path
10 | func DownloadFile(path, url string) error {
11 | // Create the file
12 | out, err := os.Create(path)
13 | if err != nil {
14 | return err
15 | }
16 | defer out.Close()
17 |
18 | // Get the data
19 | resp, err := http.Get(url)
20 | if err != nil {
21 | return err
22 | }
23 | defer resp.Body.Close()
24 |
25 | // Write the body to file
26 | _, err = io.Copy(out, resp.Body)
27 | if err != nil {
28 | return err
29 | }
30 |
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/util_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestFileDownload(t *testing.T) {
12 | ts := time.Now().Unix()
13 | filename := fmt.Sprintf("/tmp/go-lark-ci-%d", ts)
14 | err := DownloadFile(filename, "https://s1-fs.pstatp.com/static-resource/v1/363e0009ef09d43d5a96~?image_size=72x72&cut_type=&quality=&format=png&sticker_format=.webp")
15 | if assert.NoError(t, err) {
16 | assert.FileExists(t, filename)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/v2/README.md:
--------------------------------------------------------------------------------
1 | # go-lark
2 |
3 | [](https://github.com/go-lark/lark/actions/workflows/build_v2.yml)
4 |
--------------------------------------------------------------------------------
/v2/api.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // BaseResponse of an API
4 | type BaseResponse struct {
5 | Code int `json:"code"`
6 | Msg string `json:"msg"`
7 | Error BaseError `json:"error"`
8 | }
9 |
10 | // DummyResponse is used to unmarshal from a complete JSON response but only to retrieve error
11 | type DummyResponse struct {
12 | BaseResponse
13 | }
14 |
15 | // BaseError is returned by the platform
16 | type BaseError struct {
17 | LogID string `json:"log_id,omitempty"`
18 | }
19 |
20 | // I18NNames structure of names in multiple locales
21 | type I18NNames struct {
22 | ZhCN string `json:"zh_cn,omitempty"`
23 | EnUS string `json:"en_us,omitempty"`
24 | JaJP string `json:"ja_jp,omitempty"`
25 | }
26 |
27 | // WithUserIDType assigns user ID type
28 | func (bot *Bot) WithUserIDType(userIDType string) *Bot {
29 | bot.userIDType = userIDType
30 | return bot
31 | }
32 |
--------------------------------------------------------------------------------
/v2/api_auth.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | // URLs for auth
8 | const (
9 | tenantAccessTokenInternalURL = "/open-apis/auth/v3/tenant_access_token/internal"
10 | )
11 |
12 | // TenantAccessTokenInternalResponse .
13 | type TenantAccessTokenInternalResponse struct {
14 | BaseResponse
15 | TenantAccessToken string `json:"tenant_access_token"`
16 | Expire int `json:"expire"`
17 | }
18 |
19 | // GetTenantAccessTokenInternal gets AppAccessToken for internal use
20 | func (bot *Bot) GetTenantAccessTokenInternal(ctx context.Context) (*TenantAccessTokenInternalResponse, error) {
21 | if !bot.requireType(ChatBot) {
22 | return nil, ErrBotTypeError
23 | }
24 |
25 | params := map[string]interface{}{
26 | "app_id": bot.appID,
27 | "app_secret": bot.appSecret,
28 | }
29 | var respData TenantAccessTokenInternalResponse
30 | err := bot.PostAPIRequest(ctx, "GetTenantAccessTokenInternal", tenantAccessTokenInternalURL, false, params, &respData)
31 | return &respData, err
32 | }
33 |
--------------------------------------------------------------------------------
/v2/api_auth_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGetTenantAccessTokenInternal(t *testing.T) {
10 | bot := newTestBot()
11 | resp, err := bot.GetTenantAccessTokenInternal(t.Context())
12 | if assert.NoError(t, err) {
13 | assert.Equal(t, 0, resp.Code)
14 | assert.NotEmpty(t, resp.TenantAccessToken)
15 | assert.NotEmpty(t, resp.Expire)
16 | t.Log(resp)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/v2/api_bot.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import "context"
4 |
5 | const (
6 | getBotInfoURL = "/open-apis/bot/v3/info/"
7 | )
8 |
9 | // GetBotInfoResponse .
10 | type GetBotInfoResponse struct {
11 | BaseResponse
12 | Bot struct {
13 | ActivateStatus int `json:"activate_status"`
14 | AppName string `json:"app_name"`
15 | AvatarURL string `json:"avatar_url"`
16 | IPWhiteList []string `json:"ip_white_list"`
17 | OpenID string `json:"open_id"`
18 | } `json:"bot"`
19 | }
20 |
21 | // GetBotInfo returns bot info
22 | func (bot Bot) GetBotInfo(ctx context.Context) (*GetBotInfoResponse, error) {
23 | var respData GetBotInfoResponse
24 | err := bot.PostAPIRequest(ctx, "GetBotInfo", getBotInfoURL, true, nil, &respData)
25 | return &respData, err
26 | }
27 |
--------------------------------------------------------------------------------
/v2/api_bot_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGetBotInfo(t *testing.T) {
10 | resp, err := bot.GetBotInfo(t.Context())
11 | if assert.NoError(t, err) {
12 | assert.Equal(t, 0, resp.Code)
13 | assert.Equal(t, "go-lark-bot", resp.Bot.AppName)
14 | t.Log(resp)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/v2/api_buzz_message.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | )
7 |
8 | const (
9 | buzzInAppURL = "/open-apis/im/v1/messages/%s/urgent_app?user_id_type=%s"
10 | buzzSMSURL = "/open-apis/im/v1/messages/%s/urgent_sms?user_id_type=%s"
11 | buzzPhoneURL = "/open-apis/im/v1/messages/%s/urgent_phone?user_id_type=%s"
12 | )
13 |
14 | // Buzz types
15 | const (
16 | BuzzTypeInApp = "buzz_inapp"
17 | BuzzTypeSMS = "buzz_sms"
18 | BuzzTypePhone = "buzz_phone"
19 | )
20 |
21 | // BuzzMessageResponse .
22 | type BuzzMessageResponse struct {
23 | BaseResponse
24 |
25 | Data struct {
26 | InvalidUserIDList []string `json:"invalid_user_id_list,omitempty"`
27 | } `json:"data,omitempty"`
28 | }
29 |
30 | // BuzzMessage .
31 | func (bot Bot) BuzzMessage(ctx context.Context, buzzType string, messageID string, userIDList ...string) (*BuzzMessageResponse, error) {
32 | var respData BuzzMessageResponse
33 | url := buzzInAppURL
34 | switch buzzType {
35 | case BuzzTypeInApp:
36 | url = buzzInAppURL
37 | case BuzzTypeSMS:
38 | url = buzzSMSURL
39 | case BuzzTypePhone:
40 | url = buzzPhoneURL
41 | }
42 | req := map[string][]string{
43 | "user_id_list": userIDList,
44 | }
45 | err := bot.PatchAPIRequest(ctx, "BuzzMessage", fmt.Sprintf(url, messageID, bot.userIDType), true, req, &respData)
46 | return &respData, err
47 | }
48 |
--------------------------------------------------------------------------------
/v2/api_buzz_message_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestBuzzMessage(t *testing.T) {
10 | resp, err := bot.PostText(t.Context(), "this text will be buzzed", WithEmail(testUserEmail))
11 | if assert.NoError(t, err) {
12 | bot.WithUserIDType(UIDOpenID)
13 | messageID := resp.Data.MessageID
14 | buzzResp, err := bot.BuzzMessage(t.Context(), BuzzTypeInApp, messageID, testUserOpenID)
15 | if assert.NoError(t, err) {
16 | assert.Equal(t, 0, buzzResp.Code)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/v2/api_chat_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestChatInfo(t *testing.T) {
13 | bot.WithUserIDType(UIDOpenID)
14 | assert.Equal(t, UIDOpenID, bot.userIDType)
15 | resp, err := bot.GetChat(t.Context(), testGroupChatID)
16 | if assert.NoError(t, err) {
17 | assert.Equal(t, 0, resp.Code)
18 | assert.Equal(t, "go-lark-ci", resp.Data.Name)
19 | assert.Equal(t, "group", resp.Data.ChatMode)
20 | assert.Equal(t, testUserOpenID, resp.Data.OwnerID)
21 | t.Log(resp.Data)
22 | }
23 | }
24 |
25 | func TestChatList(t *testing.T) {
26 | bot.WithUserIDType(UIDOpenID)
27 | assert.Equal(t, UIDOpenID, bot.userIDType)
28 | resp, err := bot.ListChat(t.Context(), "ByCreateTimeAsc", "", 10)
29 | if assert.NoError(t, err) {
30 | assert.Equal(t, 0, resp.Code)
31 | assert.NotEmpty(t, resp.Data.Items)
32 | t.Log(resp.Data.Items[0])
33 | }
34 | }
35 |
36 | func TestChatSearch(t *testing.T) {
37 | bot.WithUserIDType(UIDOpenID)
38 | assert.Equal(t, UIDOpenID, bot.userIDType)
39 | resp, err := bot.SearchChat(t.Context(), "go-lark", "", 10)
40 | if assert.NoError(t, err) {
41 | assert.Equal(t, 0, resp.Code)
42 | if assert.NotEmpty(t, resp.Data.Items) {
43 | for _, item := range resp.Data.Items {
44 | if !strings.Contains(item.Name, "go-lark") {
45 | t.Error(item.Name, "does not contain go-lark")
46 | }
47 | }
48 | }
49 | t.Log(resp.Data.Items)
50 | }
51 | }
52 |
53 | func TestChatCRUD(t *testing.T) {
54 | bot.WithUserIDType(UIDOpenID)
55 | resp, err := bot.CreateChat(
56 | t.Context(),
57 | CreateChatRequest{
58 | Name: fmt.Sprintf("go-lark-ci-%d", time.Now().Unix()),
59 | ChatMode: "group",
60 | ChatType: "public",
61 | })
62 | if assert.NoError(t, err) {
63 | chatID := resp.Data.ChatID
64 | assert.NotEmpty(t, chatID)
65 | upResp, err := bot.UpdateChat(
66 | t.Context(),
67 | chatID,
68 | UpdateChatRequest{
69 | Description: "new description",
70 | })
71 | t.Log(upResp)
72 | if assert.NoError(t, err) {
73 | getResp, err := bot.GetChat(t.Context(), chatID)
74 | if assert.NoError(t, err) {
75 | assert.Equal(t, "new description", getResp.Data.Description)
76 | // join chat
77 | joinResp, err := bot.JoinChat(t.Context(), chatID)
78 | assert.Zero(t, joinResp.Code)
79 | assert.NoError(t, err)
80 |
81 | // add chat member
82 | addMemberResp, err := bot.AddChatMember(
83 | t.Context(),
84 | chatID,
85 | []string{testUserOpenID})
86 | if assert.NoError(t, err) {
87 | assert.Equal(t, 0, addMemberResp.Code)
88 | assert.Empty(t, addMemberResp.Data.InvalidIDList)
89 | }
90 | // remove chat member
91 | removeMemberResp, err := bot.RemoveChatMember(t.Context(), chatID, []string{testUserOpenID})
92 | if assert.NoError(t, err) {
93 | assert.Equal(t, 0, removeMemberResp.Code)
94 | assert.Empty(t, removeMemberResp.Data.InvalidIDList)
95 | }
96 |
97 | // delete
98 | _, err = bot.DeleteChat(t.Context(), chatID)
99 | assert.NoError(t, err)
100 | }
101 | }
102 | }
103 | }
104 |
105 | func TestIsInChat(t *testing.T) {
106 | resp, err := bot.IsInChat(t.Context(), testGroupChatID)
107 | if assert.NoError(t, err) {
108 | assert.Equal(t, 0, resp.Code)
109 | assert.True(t, resp.Data.IsInChat)
110 | }
111 | }
112 |
113 | func TestGetChatMembers(t *testing.T) {
114 | bot.WithUserIDType(UIDOpenID)
115 | resp, err := bot.GetChatMembers(t.Context(), testGroupChatID, "", 1)
116 | if assert.NoError(t, err) {
117 | assert.Equal(t, 0, resp.Code)
118 | assert.NotEmpty(t, resp.Data.Items)
119 | assert.Empty(t, resp.Data.PageToken)
120 | assert.NotEmpty(t, resp.Data.MemberTotal)
121 | assert.False(t, resp.Data.HasMore)
122 | }
123 | }
124 |
125 | func TestChatTopNotice(t *testing.T) {
126 | resp, err := bot.PostText(t.Context(), "group notice", WithChatID(testGroupChatID))
127 | if assert.NoError(t, err) {
128 | setResp, _ := bot.SetTopNotice(t.Context(), testGroupChatID, "2", resp.Data.MessageID)
129 | assert.Equal(t, 0, setResp.Code)
130 | delResp, _ := bot.DeleteTopNotice(t.Context(), testGroupChatID)
131 | assert.Equal(t, 0, delResp.Code)
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/v2/api_contact.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/url"
7 | )
8 |
9 | const (
10 | getUserInfoURL = "/open-apis/contact/v3/users/%s?user_id_type=%s"
11 | batchGetUserInfoURL = "/open-apis/contact/v3/users/batch?%s"
12 | )
13 |
14 | // GetUserInfoResponse .
15 | type GetUserInfoResponse struct {
16 | BaseResponse
17 | Data struct {
18 | User UserInfo
19 | }
20 | }
21 |
22 | // BatchGetUserInfoResponse .
23 | type BatchGetUserInfoResponse struct {
24 | BaseResponse
25 | Data struct {
26 | Items []UserInfo
27 | }
28 | }
29 |
30 | // UserInfo .
31 | type UserInfo struct {
32 | OpenID string `json:"open_id,omitempty"`
33 | Email string `json:"email,omitempty"`
34 | UserID string `json:"user_id,omitempty"`
35 | ChatID string `json:"chat_id,omitempty"`
36 | UnionID string `json:"union_id,omitempty"`
37 | Name string `json:"name,omitempty"`
38 | EnglishName string `json:"en_name,omitempty"`
39 | NickName string `json:"nickname,omitempty"`
40 | Mobile string `json:"mobile,omitempty"`
41 | MobileVisible bool `json:"mobile_visible,omitempty"`
42 | Gender int `json:"gender,omitempty"`
43 | Avatar UserAvatar `json:"avatar,omitempty"`
44 | Status UserStatus `json:"status,omitempty"`
45 | City string `json:"city,omitempty"`
46 | Country string `json:"country,omitempty"`
47 | WorkStation string `json:"work_station,omitempty"`
48 | JoinTime int `json:"join_time,omitempty"`
49 | EmployeeNo string `json:"employee_no,omitempty"`
50 | EmployeeType int `json:"employee_type,omitempty"`
51 | EnterpriseEmail string `json:"enterprise_email,omitempty"`
52 | Geo string `json:"geo,omitempty"`
53 | JobTitle string `json:"job_title,omitempty"`
54 | JobLevelID string `json:"job_level_id,omitempty"`
55 | JobFamilyID string `json:"job_family_id,omitempty"`
56 | DepartmentIDs []string `json:"department_ids,omitempty"`
57 | LeaderUserID string `json:"leader_user_id,omitempty"`
58 | IsTenantManager bool `json:"is_tenant_manager,omitempty"`
59 | }
60 |
61 | // UserAvatar .
62 | type UserAvatar struct {
63 | Avatar72 string `json:"avatar_72,omitempty"`
64 | Avatar240 string `json:"avatar_240,omitempty"`
65 | Avatar640 string `json:"avatar_640,omitempty"`
66 | AvatarOrigin string `json:"avatar_origin,omitempty"`
67 | }
68 |
69 | // UserStatus .
70 | type UserStatus struct {
71 | IsFrozen bool
72 | IsResigned bool
73 | IsActivated bool
74 | IsExited bool
75 | IsUnjoin bool
76 | }
77 |
78 | // GetUserInfo gets contact info
79 | func (bot Bot) GetUserInfo(ctx context.Context, userID *OptionalUserID) (*GetUserInfoResponse, error) {
80 | url := fmt.Sprintf(getUserInfoURL, userID.RealID, userID.UIDType)
81 | var respData GetUserInfoResponse
82 | err := bot.GetAPIRequest(ctx, "GetUserInfo", url, true, nil, &respData)
83 | return &respData, err
84 | }
85 |
86 | // BatchGetUserInfo gets contact info in batch
87 | func (bot Bot) BatchGetUserInfo(ctx context.Context, userIDType string, userIDs ...string) (*BatchGetUserInfoResponse, error) {
88 | if len(userIDs) == 0 || len(userIDs) > 50 {
89 | return nil, ErrParamExceedInputLimit
90 | }
91 | v := url.Values{}
92 | v.Set("user_id_type", userIDType)
93 | for _, userID := range userIDs {
94 | v.Add("user_ids", userID)
95 | }
96 | url := fmt.Sprintf(batchGetUserInfoURL, v.Encode())
97 | var respData BatchGetUserInfoResponse
98 | err := bot.GetAPIRequest(ctx, "GetUserInfo", url, true, nil, &respData)
99 | return &respData, err
100 | }
101 |
--------------------------------------------------------------------------------
/v2/api_contact_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGetUserInfo(t *testing.T) {
10 | resp, err := bot.GetUserInfo(t.Context(), WithUserID(testUserID))
11 | if assert.NoError(t, err) {
12 | assert.Equal(t, resp.Data.User.Name, "David")
13 | }
14 | bresp, err := bot.BatchGetUserInfo(t.Context(), UIDUserID, testUserID)
15 | if assert.NoError(t, err) {
16 | if assert.NotEmpty(t, bresp.Data.Items) {
17 | assert.Equal(t, bresp.Data.Items[0].Name, "David")
18 | }
19 | }
20 | _, err = bot.BatchGetUserInfo(t.Context(), UIDUserID)
21 | assert.ErrorIs(t, err, ErrParamExceedInputLimit)
22 | }
23 |
--------------------------------------------------------------------------------
/v2/api_notification.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import "context"
4 |
5 | // PostNotificationResp .
6 | type PostNotificationResp struct {
7 | Code int `json:"code"`
8 | Msg string `json:"msg"`
9 | StatusCode int `json:"StatusCode"`
10 | StatusMessage string `json:"StatusMessage"`
11 | }
12 |
13 | // PostNotification posts nofication to a given webhook
14 | func (bot Bot) PostNotification(ctx context.Context, om OutcomingMessage) (*PostNotificationResp, error) {
15 | if !bot.requireType(NotificationBot) {
16 | return nil, ErrBotTypeError
17 | }
18 |
19 | params := buildNotification(om)
20 | var respData PostNotificationResp
21 | err := bot.PostAPIRequest(ctx, "PostNotification", bot.webhook, false, params, &respData)
22 | return &respData, err
23 | }
24 |
--------------------------------------------------------------------------------
/v2/api_notification_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestWebhook(t *testing.T) {
11 | bot := NewNotificationBot(testWebhook)
12 |
13 | mbText := NewMsgBuffer(MsgText)
14 | mbText.Text("hello")
15 | resp, err := bot.PostNotification(t.Context(), mbText.Build())
16 | assert.NoError(t, err)
17 | assert.Zero(t, resp.StatusCode)
18 | assert.Equal(t, "success", resp.StatusMessage)
19 |
20 | mbPost := NewMsgBuffer(MsgPost)
21 | mbPost.Post(NewPostBuilder().Title("hello").TextTag("world", 1, true).Render())
22 | resp, err = bot.PostNotification(t.Context(), mbPost.Build())
23 | assert.NoError(t, err)
24 | assert.Zero(t, resp.StatusCode)
25 | assert.Equal(t, "success", resp.StatusMessage)
26 |
27 | mbImg := NewMsgBuffer(MsgImage)
28 | mbImg.Image("img_a97c1597-9c0a-47c1-9fb4-dd3e5e37ac9g")
29 | resp, err = bot.PostNotification(t.Context(), mbImg.Build())
30 | assert.NoError(t, err)
31 | assert.Zero(t, resp.StatusCode)
32 | assert.Equal(t, "success", resp.StatusMessage)
33 |
34 | mbShareGroup := NewMsgBuffer(MsgShareCard)
35 | mbShareGroup.ShareChat(testGroupChatID)
36 | resp, err = bot.PostNotification(t.Context(), mbShareGroup.Build())
37 | assert.NoError(t, err)
38 | assert.Zero(t, resp.StatusCode)
39 | assert.Equal(t, "success", resp.StatusMessage)
40 | }
41 |
42 | func TestWebhookCardMessage(t *testing.T) {
43 | bot := NewNotificationBot(testWebhook)
44 |
45 | b := NewCardBuilder()
46 | card := b.Card(
47 | b.Div(
48 | b.Field(b.Text("左侧内容")).Short(),
49 | b.Field(b.Text("右侧内容")).Short(),
50 | b.Field(b.Text("整排内容")),
51 | b.Field(b.Text("整排**Markdown**内容").LarkMd()),
52 | ),
53 | b.Div().
54 | Text(b.Text("Text Content")).
55 | Extra(b.Img("img_a7c6aa35-382a-48ad-839d-d0182a69b4dg")),
56 | b.Note().
57 | AddText(b.Text("Note **Text**").LarkMd()).
58 | AddImage(b.Img("img_a7c6aa35-382a-48ad-839d-d0182a69b4dg")),
59 | ).
60 | Wathet().
61 | Title("Notification Card")
62 | msg := NewMsgBuffer(MsgInteractive)
63 | om := msg.Card(card.String()).Build()
64 | resp, err := bot.PostNotification(t.Context(), om)
65 |
66 | if assert.NoError(t, err) {
67 | assert.Equal(t, 0, resp.StatusCode)
68 | assert.NotEmpty(t, resp.StatusMessage)
69 | }
70 | }
71 |
72 | func TestWebhookSigned(t *testing.T) {
73 | bot := NewNotificationBot(testWebhookSigned)
74 |
75 | mbText := NewMsgBuffer(MsgText)
76 | mbText.Text("hello sign").WithSign("FT1dnAgPYYTcpafMTkhPjc", time.Now().Unix())
77 | resp, err := bot.PostNotification(t.Context(), mbText.Build())
78 | assert.NoError(t, err)
79 | assert.Zero(t, resp.StatusCode)
80 | assert.Equal(t, "success", resp.StatusMessage)
81 | }
82 |
83 | func TestWebhookSignedError(t *testing.T) {
84 | bot := NewNotificationBot("https://open.feishu.cn/open-apis/bot/v2/hook/749be902-6eaa-4cc3-9325-be4126164b02")
85 |
86 | mbText := NewMsgBuffer(MsgText)
87 | mbText.Text("hello sign").WithSign("LIpnNexV7rwOyOebKoqSdb", time.Now().Unix())
88 | resp, err := bot.PostNotification(t.Context(), mbText.Build())
89 | assert.Error(t, err)
90 | assert.Zero(t, resp.StatusCode)
91 | assert.Equal(t, "sign match fail or timestamp is not within one hour from current time", resp.Msg)
92 | }
93 |
--------------------------------------------------------------------------------
/v2/crypto.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/hmac"
7 | "crypto/sha256"
8 | "encoding/base64"
9 | "errors"
10 | "fmt"
11 | )
12 |
13 | // EncryptKey .
14 | func EncryptKey(key string) []byte {
15 | sha256key := sha256.Sum256([]byte(key))
16 | return sha256key[:sha256.Size]
17 | }
18 |
19 | // Decrypt with AES Cipher
20 | func Decrypt(encryptedKey []byte, data string) ([]byte, error) {
21 | block, err := aes.NewCipher(encryptedKey)
22 | if err != nil {
23 | return nil, err
24 | }
25 | ciphertext, err := base64.StdEncoding.DecodeString(data)
26 | iv := encryptedKey[:aes.BlockSize]
27 | blockMode := cipher.NewCBCDecrypter(block, iv)
28 | decryptedData := make([]byte, len(data))
29 | blockMode.CryptBlocks(decryptedData, ciphertext)
30 | msg := unpad(decryptedData)
31 | if len(msg) < block.BlockSize() {
32 | return nil, errors.New("msg length is less than blocksize")
33 | }
34 | return msg[block.BlockSize():], err
35 | }
36 |
37 | func unpad(data []byte) []byte {
38 | length := len(data)
39 | var unpadding, unpaddingIdx int
40 | for i := length - 1; i > 0; i-- {
41 | if data[i] != 0 {
42 | unpadding = int(data[i])
43 | unpaddingIdx = length - 1 - i
44 | break
45 | }
46 | }
47 | return data[:(length - unpaddingIdx - unpadding)]
48 | }
49 |
50 | // GenSign generates sign for notification bot
51 | func GenSign(secret string, timestamp int64) (string, error) {
52 | stringToSign := fmt.Sprintf("%v", timestamp) + "\n" + secret
53 |
54 | var data []byte
55 | h := hmac.New(sha256.New, []byte(stringToSign))
56 | _, err := h.Write(data)
57 | if err != nil {
58 | return "", err
59 | }
60 |
61 | signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
62 | return signature, nil
63 | }
64 |
--------------------------------------------------------------------------------
/v2/crypto_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestAESDecrypt(t *testing.T) {
10 | key := EncryptKey("test key")
11 | data, _ := Decrypt(key, "P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk=")
12 | assert.Equal(t, "hello world", string(data))
13 | assert.Equal(t, 11, len(data))
14 | }
15 |
16 | func TestGenSign(t *testing.T) {
17 | sign, err := GenSign("xxx", 1661860880)
18 | if assert.NoError(t, err) {
19 | assert.Equal(t, "QnWVTSBe6FmQDE0bG6X0mURbI+DnvVyu1h+j5dHOjrU=", sign)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/v2/error.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | )
7 |
8 | // Errors
9 | var (
10 | ErrBotTypeError = errors.New("Bot type error")
11 | ErrParamUserID = errors.New("Param error: UserID")
12 | ErrParamMessageID = errors.New("Param error: Message ID")
13 | ErrParamExceedInputLimit = errors.New("Param error: Exceed input limit")
14 | ErrMessageTypeNotSuppored = errors.New("Message type not supported")
15 | ErrEncryptionNotEnabled = errors.New("Encryption is not enabled")
16 | ErrMessageNotBuild = errors.New("Message not build")
17 | ErrUnsupportedUIDType = errors.New("Unsupported UID type")
18 | ErrInvalidReceiveID = errors.New("Invalid receive ID")
19 | ErrEventTypeNotMatch = errors.New("Event type not match")
20 | ErrMessageType = errors.New("Message type error")
21 | )
22 |
23 | // APIError constructs an error with given response
24 | func APIError(url string, resp BaseResponse) error {
25 | return fmt.Errorf(
26 | "Lark API server returned error: Code [%d] Message [%s] LogID [%s] Interface [%s]",
27 | resp.Code,
28 | resp.Msg,
29 | resp.Error.LogID,
30 | url,
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/v2/event.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "log"
7 | "net/http"
8 | )
9 |
10 | // EventType definitions
11 | const (
12 | EventTypeMessageReceived = "im.message.receive_v1"
13 | EventTypeMessageRead = "im.message.message_read_v1"
14 | EventTypeMessageRecalled = "im.message.recalled_v1"
15 | EventTypeMessageReactionCreated = "im.message.reaction.created_v1"
16 | EventTypeMessageReactionDeleted = "im.message.reaction.deleted_v1"
17 | EventTypeChatDisbanded = "im.chat.disbanded_v1"
18 | EventTypeUserAdded = "im.chat.member.user.added_v1"
19 | EventTypeUserDeleted = "im.chat.member.user.deleted_v1"
20 | EventTypeBotAdded = "im.chat.member.bot.added_v1"
21 | EventTypeBotDeleted = "im.chat.member.bot.deleted_v1"
22 | // not supported yet
23 | EventTypeChatUpdated = "im.chat.updated_v1"
24 | EventTypeUserWithdrawn = "im.chat.member.user.withdrawn_v1"
25 | )
26 |
27 | // Event handles events with v2 schema
28 | type Event struct {
29 | Schema string `json:"schema,omitempty"`
30 | Header EventHeader `json:"header,omitempty"`
31 |
32 | EventRaw json.RawMessage `json:"event,omitempty"`
33 | Event interface{} `json:"-"`
34 | }
35 |
36 | // EventHeader .
37 | type EventHeader struct {
38 | EventID string `json:"event_id,omitempty"`
39 | EventType string `json:"event_type,omitempty"`
40 | CreateTime string `json:"create_time,omitempty"`
41 | Token string `json:"token,omitempty"`
42 | AppID string `json:"app_id,omitempty"`
43 | TenantKey string `json:"tenant_key,omitempty"`
44 | }
45 |
46 | // EventBody .
47 | type EventBody struct {
48 | Type string `json:"type"`
49 | AppID string `json:"app_id"`
50 | TenantKey string `json:"tenant_key"`
51 | ChatType string `json:"chat_type"`
52 | MsgType string `json:"msg_type"`
53 | RootID string `json:"root_id,omitempty"`
54 | ParentID string `json:"parent_id,omitempty"`
55 | OpenID string `json:"open_id,omitempty"`
56 | OpenChatID string `json:"open_chat_id,omitempty"`
57 | OpenMessageID string `json:"open_message_id,omitempty"`
58 | IsMention bool `json:"is_mention,omitempty"`
59 | Title string `json:"title,omitempty"`
60 | Text string `json:"text,omitempty"`
61 | RealText string `json:"text_without_at_bot,omitempty"`
62 | ImageKey string `json:"image_key,omitempty"`
63 | ImageURL string `json:"image_url,omitempty"`
64 | FileKey string `json:"file_key,omitempty"`
65 | }
66 |
67 | // EventUserID .
68 | type EventUserID struct {
69 | UnionID string `json:"union_id,omitempty"`
70 | UserID string `json:"user_id,omitempty"`
71 | OpenID string `json:"open_id,omitempty"`
72 | }
73 |
74 | // PostEvent posts events
75 | func (e Event) PostEvent(client *http.Client, hookURL string) (*http.Response, error) {
76 | buf := new(bytes.Buffer)
77 | err := json.NewEncoder(buf).Encode(e)
78 | if err != nil {
79 | log.Printf("Encode json failed: %+v\n", err)
80 | return nil, err
81 | }
82 | resp, err := client.Post(hookURL, "application/json; charset=utf-8", buf)
83 | return resp, err
84 | }
85 |
86 | // GetEvent .
87 | func (e Event) GetEvent(eventType string, body interface{}) error {
88 | if e.Header.EventType != eventType {
89 | return ErrEventTypeNotMatch
90 | }
91 | err := json.Unmarshal(e.EventRaw, &body)
92 | return err
93 | }
94 |
--------------------------------------------------------------------------------
/v2/event_bot_added.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventBotAdded .
4 | type EventBotAdded struct {
5 | ChatID string `json:"chat_id,omitempty"`
6 | OperatorID EventUserID `json:"operator_id,omitempty"`
7 | External bool `json:"external,omitempty"`
8 | OperatorTenantKey string `json:"operator_tenant_key,omitempty"`
9 | }
10 |
11 | // GetBotAdded .
12 | func (e Event) GetBotAdded() (*EventBotAdded, error) {
13 | var body EventBotAdded
14 | err := e.GetEvent(EventTypeBotAdded, &body)
15 | return &body, err
16 | }
17 |
--------------------------------------------------------------------------------
/v2/event_bot_deleted.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventBotDeleted .
4 | type EventBotDeleted = EventBotAdded
5 |
6 | // GetBotDeleted .
7 | func (e Event) GetBotDeleted() (*EventBotDeleted, error) {
8 | var body EventBotDeleted
9 | err := e.GetEvent(EventTypeBotDeleted, &body)
10 | return &body, err
11 | }
12 |
--------------------------------------------------------------------------------
/v2/event_challenge.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import "encoding/json"
4 |
5 | // EventChallenge is request of add event hook
6 | type EventChallenge struct {
7 | Token string `json:"token,omitempty"`
8 | Challenge string `json:"challenge,omitempty"`
9 | Type string `json:"type,omitempty"`
10 | }
11 |
12 | // EncryptedReq is request of encrypted challenge
13 | type EncryptedReq struct {
14 | Encrypt string `json:"encrypt,omitempty"`
15 | }
16 |
17 | // EventCardCallback is request of card
18 | type EventCardCallback struct {
19 | AppID string `json:"app_id,omitempty"`
20 | TenantKey string `json:"tenant_key,omitempty"`
21 | Token string `json:"token,omitempty"`
22 | OpenID string `json:"open_id,omitempty"`
23 | UserID string `json:"user_id,omitempty"`
24 | MessageID string `json:"open_message_id,omitempty"`
25 | ChatID string `json:"open_chat_id,omitempty"`
26 | Action EventCardAction `json:"action,omitempty"`
27 | }
28 |
29 | // EventCardAction .
30 | type EventCardAction struct {
31 | Tag string `json:"tag,omitempty"` // button, overflow, select_static, select_person, &datepicker
32 | Option string `json:"option,omitempty"` // only for Overflow and SelectMenu
33 | Timezone string `json:"timezone,omitempty"` // only for DatePicker
34 | Value json.RawMessage `json:"value,omitempty"` // for any elements with value
35 | }
36 |
--------------------------------------------------------------------------------
/v2/event_chat_disbanded.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventChatDisbanded .
4 | type EventChatDisbanded struct {
5 | ChatID string `json:"chat_id,omitempty"`
6 | OperatorID EventUserID `json:"operator_id,omitempty"`
7 | External bool `json:"external,omitempty"`
8 | OperatorTenantKey string `json:"operator_tenant_key,omitempty"`
9 | }
10 |
11 | // GetChatDisbanded .
12 | func (e Event) GetChatDisbanded() (*EventChatDisbanded, error) {
13 | var body EventChatDisbanded
14 | err := e.GetEvent(EventTypeChatDisbanded, &body)
15 | return &body, err
16 | }
17 |
--------------------------------------------------------------------------------
/v2/event_message_reaction_created.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventMessageReactionCreated .
4 | type EventMessageReactionCreated struct {
5 | MessageID string `json:"message_id,omitempty"`
6 | OperatorType string `json:"operator_type,omitempty"`
7 | UserID EventUserID `json:"user_id,omitempty"`
8 | AppID string `json:"app_id,omitempty"`
9 | ActionTime string `json:"action_time,omitempty"`
10 | ReactionType struct {
11 | EmojiType string `json:"emoji_type,omitempty"`
12 | } `json:"reaction_type,omitempty"`
13 | }
14 |
15 | // GetMessageReactionCreated .
16 | func (e Event) GetMessageReactionCreated() (*EventMessageReactionCreated, error) {
17 | var body EventMessageReactionCreated
18 | err := e.GetEvent(EventTypeMessageReactionCreated, &body)
19 | return &body, err
20 | }
21 |
--------------------------------------------------------------------------------
/v2/event_message_reaction_deleted.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventMessageReactionDeleted .
4 | type EventMessageReactionDeleted struct {
5 | MessageID string `json:"message_id,omitempty"`
6 | OperatorType string `json:"operator_type,omitempty"`
7 | UserID EventUserID `json:"user_id,omitempty"`
8 | AppID string `json:"app_id,omitempty"`
9 | ActionTime string `json:"action_time,omitempty"`
10 | ReactionType struct {
11 | EmojiType string `json:"emoji_type,omitempty"`
12 | } `json:"reaction_type,omitempty"`
13 | }
14 |
15 | // GetMessageReactionDeleted .
16 | func (e Event) GetMessageReactionDeleted() (*EventMessageReactionDeleted, error) {
17 | var body EventMessageReactionDeleted
18 | err := e.GetEvent(EventTypeMessageReactionDeleted, &body)
19 | return &body, err
20 | }
21 |
--------------------------------------------------------------------------------
/v2/event_message_read.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventMessageRead .
4 | type EventMessageRead struct {
5 | Reader struct {
6 | ReaderID EventUserID `json:"reader_id,omitempty"`
7 | ReadTime string `json:"read_time,omitempty"`
8 | TenantKey string `json:"tenant_key,omitempty"`
9 | } `json:"reader,omitempty"`
10 | MessageIDList []string `json:"message_id_list,omitempty"`
11 | }
12 |
13 | // GetMessageRead .
14 | func (e Event) GetMessageRead() (*EventMessageRead, error) {
15 | var body EventMessageRead
16 | err := e.GetEvent(EventTypeMessageRead, &body)
17 | return &body, err
18 | }
19 |
--------------------------------------------------------------------------------
/v2/event_message_recalled.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventMessageRecalled .
4 | type EventMessageRecalled struct {
5 | MessageID string `json:"message_id,omitempty"`
6 | ChatID string `json:"chat_id,omitempty"`
7 | RecallTime string `json:"recall_time,omitempty"`
8 | RecallType string `json:"recall_type,omitempty"`
9 | }
10 |
11 | // GetMessageRecalled .
12 | func (e Event) GetMessageRecalled() (*EventMessageRecalled, error) {
13 | var body EventMessageRecalled
14 | err := e.GetEvent(EventTypeMessageRecalled, &body)
15 | return &body, err
16 | }
17 |
--------------------------------------------------------------------------------
/v2/event_message_received.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventMessageReceived .
4 | type EventMessageReceived struct {
5 | Sender struct {
6 | SenderID EventUserID `json:"sender_id,omitempty"`
7 | SenderType string `json:"sender_type,omitempty"`
8 | TenantKey string `json:"tenant_key,omitempty"`
9 | } `json:"sender,omitempty"`
10 | Message struct {
11 | MessageID string `json:"message_id,omitempty"`
12 | RootID string `json:"root_id,omitempty"`
13 | ParentID string `json:"parent_id,omitempty"`
14 | CreateTime string `json:"create_time,omitempty"`
15 | UpdateTime string `json:"update_time,omitempty"`
16 | ChatID string `json:"chat_id,omitempty"`
17 | ChatType string `json:"chat_type,omitempty"`
18 | ThreadID string `json:"thread_id,omitempty"`
19 | MessageType string `json:"message_type,omitempty"`
20 | Content string `json:"content,omitempty"`
21 | Mentions []struct {
22 | Key string `json:"key,omitempty"`
23 | ID EventUserID `json:"id,omitempty"`
24 | Name string `json:"name,omitempty"`
25 | TenantKey string `json:"tenant_key,omitempty"`
26 | } `json:"mentions,omitempty"`
27 | UserAgent string `json:"user_agent,omitempty"`
28 | } `json:"message,omitempty"`
29 | }
30 |
31 | // GetMessageReceived .
32 | func (e Event) GetMessageReceived() (*EventMessageReceived, error) {
33 | var body EventMessageReceived
34 | err := e.GetEvent(EventTypeMessageReceived, &body)
35 | return &body, err
36 | }
37 |
--------------------------------------------------------------------------------
/v2/event_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestPostEvent(t *testing.T) {
14 | message := Event{
15 | Schema: "2.0",
16 | Header: EventHeader{
17 | AppID: "666",
18 | },
19 | Event: EventBody{
20 | Type: "message",
21 | ChatType: "group",
22 | MsgType: "text",
23 | OpenID: testUserOpenID,
24 | Text: "public event",
25 | Title: "",
26 | OpenMessageID: "",
27 | ImageKey: "",
28 | ImageURL: "",
29 | },
30 | }
31 | w := performRequest(func(w http.ResponseWriter, r *http.Request) {
32 | var m Event
33 | json.NewDecoder(r.Body).Decode(&m)
34 | w.Write([]byte(m.Schema))
35 | }, "POST", "/", message)
36 | assert.Equal(t, "2.0", string(w.Body.Bytes()))
37 |
38 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39 | m, _ := json.Marshal(message)
40 | w.Write([]byte(m))
41 | }))
42 | defer ts.Close()
43 | resp, err := message.PostEvent(http.DefaultClient, ts.URL)
44 | if assert.NoError(t, err) {
45 | var event Event
46 | body, err := ioutil.ReadAll(resp.Body)
47 | if assert.NoError(t, err) {
48 | defer resp.Body.Close()
49 | _ = json.Unmarshal(body, &event)
50 | assert.Equal(t, "2.0", event.Schema)
51 | assert.Equal(t, "666", event.Header.AppID)
52 | }
53 | }
54 | }
55 |
56 | func TestEventTypes(t *testing.T) {
57 | event := Event{
58 | Header: EventHeader{
59 | EventType: EventTypeChatDisbanded,
60 | },
61 | EventRaw: json.RawMessage(`{ "message": { "chat_id": "oc_ae7f3952a9b28588aeac46c9853d25d3", "chat_type": "p2p", "content": "{\"text\":\"333\"}", "create_time": "1641385820771", "message_id": "om_6ff2cff41a3e9248bbb19bf0e4762e6e", "message_type": "text" }, "sender": { "sender_id": { "open_id": "ou_4f75b532aff410181e93552ad0532072", "union_id": "on_2312aab89ab7c87beb9a443b2f3b1342", "user_id": "4gbb63af" }, "sender_type": "user", "tenant_key": "736588c9260f175d" } }`),
62 | }
63 | m, e := event.GetMessageReceived()
64 | assert.Error(t, e)
65 | event.Header.EventType = EventTypeMessageReceived
66 | m, e = event.GetMessageReceived()
67 | assert.NoError(t, e)
68 | assert.Equal(t, "p2p", m.Message.ChatType)
69 | }
70 |
71 | func TestGetEvent(t *testing.T) {
72 | event := Event{
73 | Header: EventHeader{
74 | EventType: EventTypeMessageReceived,
75 | },
76 | EventRaw: json.RawMessage(`{ "message": { "chat_id": "oc_ae7f3952a9b28588aeac46c9853d25d3", "chat_type": "p2p", "content": "{\"text\":\"333\"}", "create_time": "1641385820771", "message_id": "om_6ff2cff41a3e9248bbb19bf0e4762e6e", "message_type": "text" }, "sender": { "sender_id": { "open_id": "ou_4f75b532aff410181e93552ad0532072", "union_id": "on_2312aab89ab7c87beb9a443b2f3b1342", "user_id": "4gbb63af" }, "sender_type": "user", "tenant_key": "736588c9260f175d" } }`),
77 | }
78 | var ev EventMessageReceived
79 | err := event.GetEvent(EventTypeMessageReceived, &ev)
80 | if assert.NoError(t, err) {
81 | assert.Equal(t, "oc_ae7f3952a9b28588aeac46c9853d25d3", ev.Message.ChatID)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/v2/event_user_added.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventUserAdded .
4 | type EventUserAdded struct {
5 | ChatID string `json:"chat_id,omitempty"`
6 | OperatorID EventUserID `json:"operator_id,omitempty"`
7 | External bool `json:"external,omitempty"`
8 | OperatorTenantKey string `json:"operator_tenant_key,omitempty"`
9 | Users []struct {
10 | Name string `json:"name,omitempty"`
11 | TenantKey string `json:"tenant_key,omitempty"`
12 | UserID EventUserID `json:"user_id,omitempty"`
13 | } `json:"users,omitempty"`
14 | }
15 |
16 | // GetUserAdded .
17 | func (e Event) GetUserAdded() (*EventUserAdded, error) {
18 | var body EventUserAdded
19 | err := e.GetEvent(EventTypeUserAdded, &body)
20 | return &body, err
21 | }
22 |
--------------------------------------------------------------------------------
/v2/event_user_deleted.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // EventUserDeleted .
4 | type EventUserDeleted = EventUserAdded
5 |
6 | // GetUserDeleted .
7 | func (e Event) GetUserDeleted() (*EventUserDeleted, error) {
8 | var body EventUserDeleted
9 | err := e.GetEvent(EventTypeUserDeleted, &body)
10 | return &body, err
11 | }
12 |
--------------------------------------------------------------------------------
/v2/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-lark/lark/v2
2 |
3 | go 1.24
4 |
5 | require (
6 | github.com/go-lark/card-builder v1.0.0-beta.2
7 | github.com/joho/godotenv v1.5.1
8 | github.com/stretchr/testify v1.10.0
9 | )
10 |
11 | require (
12 | github.com/davecgh/go-spew v1.1.1 // indirect
13 | github.com/pmezard/go-difflib v1.0.0 // indirect
14 | gopkg.in/yaml.v3 v3.0.1 // indirect
15 | )
16 |
--------------------------------------------------------------------------------
/v2/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/go-lark/card-builder v1.0.0-beta.2 h1:DfH/14fs+R/G8CC+a2WoIp2wpWoD2isDcBpYZoSLwaU=
4 | github.com/go-lark/card-builder v1.0.0-beta.2/go.mod h1:7O2nuzjG4Cc/ty0s7Q7roYXOVxt9Gzrw0jBHsuP3UaM=
5 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
6 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
10 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
15 |
--------------------------------------------------------------------------------
/v2/http.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "net/http"
10 | "net/http/httputil"
11 | "time"
12 | )
13 |
14 | // HTTPClient is an interface handling http requests
15 | type HTTPClient interface {
16 | Do(ctx context.Context, req *http.Request) (*http.Response, error)
17 | }
18 |
19 | // ExpandURL expands url path to full url
20 | func (bot Bot) ExpandURL(urlPath string) string {
21 | url := fmt.Sprintf("%s%s", bot.domain, urlPath)
22 | return url
23 | }
24 |
25 | func (bot Bot) httpErrorLog(ctx context.Context, prefix, text string, err error) {
26 | bot.logger.Log(ctx, LogLevelError, fmt.Sprintf("[%s] %s: %+v\n", prefix, text, err))
27 | }
28 |
29 | func (bot *Bot) loadAndRenewToken(ctx context.Context) (string, error) {
30 | now := time.Now()
31 | // check token
32 | token, ok := bot.tenantAccessToken.Load().(TenantAccessToken)
33 | tenantAccessToken := token.TenantAccessToken
34 | if !ok || token.TenantAccessToken == "" || (token.EstimatedExpireAt != nil && now.After(*token.EstimatedExpireAt)) {
35 | // renew token
36 | if bot.autoRenew {
37 | tacResp, err := bot.GetTenantAccessTokenInternal(ctx)
38 | if err != nil {
39 | return "", err
40 | }
41 | now := time.Now()
42 | expire := time.Duration(tacResp.Expire - 10)
43 | eta := now.Add(expire)
44 | token := TenantAccessToken{
45 | TenantAccessToken: tacResp.TenantAccessToken,
46 | Expire: expire,
47 | LastUpdatedAt: &now,
48 | EstimatedExpireAt: &eta,
49 | }
50 | bot.tenantAccessToken.Store(token)
51 | tenantAccessToken = tacResp.TenantAccessToken
52 | }
53 | }
54 | return tenantAccessToken, nil
55 | }
56 |
57 | // PerformAPIRequest performs API request
58 | func (bot Bot) PerformAPIRequest(
59 | ctx context.Context,
60 | method string,
61 | prefix, urlPath string,
62 | header http.Header, auth bool,
63 | body io.Reader,
64 | output interface{},
65 | ) error {
66 | var (
67 | err error
68 | respBody io.ReadCloser
69 | url = bot.ExpandURL(urlPath)
70 | )
71 | if header == nil {
72 | header = make(http.Header)
73 | }
74 | if auth {
75 | tenantAccessToken, err := bot.loadAndRenewToken(ctx)
76 | if err != nil {
77 | return err
78 | }
79 | header.Add("Authorization", fmt.Sprintf("Bearer %s", tenantAccessToken))
80 | }
81 | req, err := http.NewRequest(method, url, body)
82 | if err != nil {
83 | bot.httpErrorLog(ctx, prefix, "init request failed", err)
84 | return err
85 | }
86 | req.Header = header
87 | resp, err := bot.client.Do(ctx, req)
88 | if err != nil {
89 | bot.httpErrorLog(ctx, prefix, "call failed", err)
90 | return err
91 | }
92 | if bot.debug {
93 | b, _ := httputil.DumpResponse(resp, true)
94 | bot.logger.Log(ctx, LogLevelDebug, string(b))
95 | }
96 | respBody = resp.Body
97 | defer respBody.Close()
98 | buffer, err := io.ReadAll(respBody)
99 | if err != nil {
100 | bot.httpErrorLog(ctx, prefix, "read body failed", err)
101 | return err
102 | }
103 | // read response content
104 | err = json.Unmarshal(buffer, &output)
105 | if err != nil {
106 | bot.httpErrorLog(ctx, prefix, "decode body failed", err)
107 | return err
108 | }
109 | // read error code
110 | var dummyOutput DummyResponse
111 | err = json.Unmarshal(buffer, &dummyOutput)
112 | if err == nil && dummyOutput.Code != 0 {
113 | apiError := APIError(url, dummyOutput.BaseResponse)
114 | bot.logger.Log(ctx, LogLevelError, apiError.Error())
115 | return apiError
116 | }
117 | return err
118 | }
119 |
120 | func (bot Bot) wrapAPIRequest(ctx context.Context, method, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
121 | buf := new(bytes.Buffer)
122 | err := json.NewEncoder(buf).Encode(params)
123 | if err != nil {
124 | bot.httpErrorLog(ctx, prefix, "encode JSON failed", err)
125 | return err
126 | }
127 |
128 | header := make(http.Header)
129 | header.Set("Content-Type", "application/json; charset=utf-8")
130 | err = bot.PerformAPIRequest(ctx, method, prefix, urlPath, header, auth, buf, output)
131 | return err
132 | }
133 |
134 | // PostAPIRequest POSTs Lark API
135 | func (bot Bot) PostAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
136 | return bot.wrapAPIRequest(ctx, http.MethodPost, prefix, urlPath, auth, params, output)
137 | }
138 |
139 | // GetAPIRequest GETs Lark API
140 | func (bot Bot) GetAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
141 | return bot.wrapAPIRequest(ctx, http.MethodGet, prefix, urlPath, auth, params, output)
142 | }
143 |
144 | // DeleteAPIRequest DELETEs Lark API
145 | func (bot Bot) DeleteAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
146 | return bot.wrapAPIRequest(ctx, http.MethodDelete, prefix, urlPath, auth, params, output)
147 | }
148 |
149 | // PutAPIRequest PUTs Lark API
150 | func (bot Bot) PutAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
151 | return bot.wrapAPIRequest(ctx, http.MethodPut, prefix, urlPath, auth, params, output)
152 | }
153 |
154 | // PatchAPIRequest PATCHes Lark API
155 | func (bot Bot) PatchAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
156 | return bot.wrapAPIRequest(ctx, http.MethodPatch, prefix, urlPath, auth, params, output)
157 | }
158 |
--------------------------------------------------------------------------------
/v2/http_client.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | // defaultClient .
10 | type defaultClient struct {
11 | c *http.Client
12 | }
13 |
14 | // newDefaultClient .
15 | func newDefaultClient() *defaultClient {
16 | return &defaultClient{
17 | c: &http.Client{
18 | Timeout: 5 * time.Second,
19 | },
20 | }
21 | }
22 |
23 | // Do .
24 | func (dc defaultClient) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
25 | req.WithContext(ctx)
26 | return dc.c.Do(req)
27 | }
28 |
--------------------------------------------------------------------------------
/v2/http_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestExpandURL(t *testing.T) {
10 | bot := NewChatBot("test-id", "test-secret")
11 | bot.SetDomain("http://localhost")
12 | assert.Equal(t, bot.ExpandURL("/test"),
13 | "http://localhost/test")
14 | }
15 |
16 | func TestAPIError(t *testing.T) {
17 | resp, err := bot.PostText(t.Context(), "failing", WithChatID("1231"))
18 | t.Log(resp, err)
19 | assert.Error(t, err)
20 | assert.NotZero(t, resp.Code)
21 | }
22 |
--------------------------------------------------------------------------------
/v2/lark.go:
--------------------------------------------------------------------------------
1 | // Package lark is an easy-to-use SDK for Feishu and Lark Open Platform,
2 | // which implements messaging APIs, with full-fledged supports on building Chat Bot and Notification Bot.
3 | package lark
4 |
5 | import (
6 | "sync/atomic"
7 | "time"
8 | )
9 |
10 | const (
11 | // ChatBot should be created with NewChatBot
12 | // Create from https://open.feishu.cn/ or https://open.larksuite.com/
13 | ChatBot = iota
14 | // NotificationBot for webhook, behave as a simpler notification bot
15 | // Create from Lark group
16 | NotificationBot
17 | )
18 |
19 | // Bot definition
20 | type Bot struct {
21 | // bot type
22 | botType int
23 | // auth info
24 | appID string
25 | appSecret string
26 | // access token
27 | tenantAccessToken atomic.Value
28 | autoRenew bool
29 | userAccessToken atomic.Value
30 | // user id type for api chat
31 | userIDType string
32 | // webhook for NotificationBot
33 | webhook string
34 | // API Domain
35 | domain string
36 | // http client
37 | client HTTPClient
38 | // logger
39 | logger LogWrapper
40 | debug bool
41 | }
42 |
43 | // Domains
44 | const (
45 | DomainFeishu = "https://open.feishu.cn"
46 | DomainLark = "https://open.larksuite.com"
47 | )
48 |
49 | // TenantAccessToken .
50 | type TenantAccessToken struct {
51 | TenantAccessToken string
52 | Expire time.Duration
53 | LastUpdatedAt *time.Time
54 | EstimatedExpireAt *time.Time
55 | }
56 |
57 | // NewChatBot with appID and appSecret
58 | func NewChatBot(appID, appSecret string) *Bot {
59 | bot := &Bot{
60 | botType: ChatBot,
61 | appID: appID,
62 | appSecret: appSecret,
63 | client: newDefaultClient(),
64 | domain: DomainFeishu,
65 | logger: initDefaultLogger(),
66 | }
67 | bot.autoRenew = true
68 | bot.tenantAccessToken.Store(TenantAccessToken{})
69 |
70 | return bot
71 | }
72 |
73 | // NewNotificationBot with URL
74 | func NewNotificationBot(hookURL string) *Bot {
75 | bot := &Bot{
76 | botType: NotificationBot,
77 | webhook: hookURL,
78 | client: newDefaultClient(),
79 | logger: initDefaultLogger(),
80 | }
81 | bot.tenantAccessToken.Store(TenantAccessToken{})
82 |
83 | return bot
84 | }
85 |
86 | // requireType checks whether the action is allowed in a list of bot types
87 | func (bot Bot) requireType(botType ...int) bool {
88 | for _, iterType := range botType {
89 | if bot.botType == iterType {
90 | return true
91 | }
92 | }
93 | return false
94 | }
95 |
96 | // SetClient assigns a new client to bot.client
97 | func (bot *Bot) SetClient(c HTTPClient) {
98 | bot.client = c
99 | }
100 |
101 | // SetDomain sets domain of endpoint, so we could call Feishu/Lark
102 | // go-lark does not check your host, just use the right one or fail.
103 | func (bot *Bot) SetDomain(domain string) {
104 | bot.domain = domain
105 | }
106 |
107 | // Domain returns current domain
108 | func (bot Bot) Domain() string {
109 | return bot.domain
110 | }
111 |
112 | // AppID returns bot.appID for external use
113 | func (bot Bot) AppID() string {
114 | return bot.appID
115 | }
116 |
117 | // BotType returns bot.botType for external use
118 | func (bot Bot) BotType() int {
119 | return bot.botType
120 | }
121 |
122 | // TenantAccessToken returns tenant access token for external use
123 | func (bot Bot) TenantAccessToken() string {
124 | token := bot.tenantAccessToken.Load().(TenantAccessToken)
125 | return token.TenantAccessToken
126 | }
127 |
128 | // SetTenantAccessToken sets tenant access token
129 | func (bot *Bot) SetTenantAccessToken(t TenantAccessToken) {
130 | bot.tenantAccessToken.Store(t)
131 | }
132 |
133 | // SetAutoRenew sets autoRenew
134 | func (bot *Bot) SetAutoRenew(onOff bool) {
135 | bot.autoRenew = onOff
136 | }
137 |
138 | // SetWebhook sets webhook URL
139 | func (bot *Bot) SetWebhook(url string) {
140 | bot.webhook = url
141 | }
142 |
--------------------------------------------------------------------------------
/v2/lark_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "log"
7 | "net/http"
8 | "net/http/httptest"
9 | "os"
10 | "testing"
11 |
12 | "github.com/joho/godotenv"
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | // IDs for test use
17 | var (
18 | testAppID string
19 | testAppSecret string
20 | testUserEmail string
21 | testUserOpenID string
22 | testUserID string
23 | testUserUnionID string
24 | testGroupChatID string
25 | testWebhookV1 string
26 | testWebhook string
27 | testWebhookSigned string
28 | )
29 |
30 | func newTestBot() *Bot {
31 | testMode := os.Getenv("GO_LARK_TEST_MODE")
32 | if testMode == "" {
33 | testMode = "testing"
34 | }
35 | if testMode == "local" {
36 | err := godotenv.Load(".env")
37 | if err != nil {
38 | panic(err)
39 | }
40 | }
41 | testAppID = os.Getenv("LARK_APP_ID")
42 | testAppSecret = os.Getenv("LARK_APP_SECRET")
43 | testUserEmail = os.Getenv("LARK_USER_EMAIL")
44 | testUserID = os.Getenv("LARK_USER_ID")
45 | testUserUnionID = os.Getenv("LARK_UNION_ID")
46 | testUserOpenID = os.Getenv("LARK_OPEN_ID")
47 | testGroupChatID = os.Getenv("LARK_CHAT_ID")
48 | testWebhook = os.Getenv("LARK_WEBHOOK")
49 | testWebhookSigned = os.Getenv("LARK_WEBHOOK_SIGNED")
50 | if len(testAppID) == 0 ||
51 | len(testAppSecret) == 0 ||
52 | len(testUserEmail) == 0 ||
53 | len(testUserID) == 0 ||
54 | len(testUserUnionID) == 0 ||
55 | len(testUserOpenID) == 0 ||
56 | len(testGroupChatID) == 0 {
57 | panic("insufficient test environment")
58 | }
59 | return NewChatBot(testAppID, testAppSecret)
60 | }
61 |
62 | func captureOutput(f func()) string {
63 | var buf bytes.Buffer
64 | log.SetOutput(&buf)
65 | f()
66 | log.SetOutput(os.Stderr)
67 | return buf.String()
68 | }
69 |
70 | func performRequest(r http.HandlerFunc, method, path string, body interface{}) *httptest.ResponseRecorder {
71 | buf := new(bytes.Buffer)
72 | json.NewEncoder(buf).Encode(body)
73 | req := httptest.NewRequest(method, path, buf)
74 | w := httptest.NewRecorder()
75 | r.ServeHTTP(w, req)
76 | return w
77 | }
78 |
79 | // for general API test suites
80 | var bot *Bot
81 |
82 | func init() {
83 | bot = newTestBot()
84 | }
85 |
86 | func TestBotProperties(t *testing.T) {
87 | chatBot := newTestBot()
88 | assert.NotEmpty(t, chatBot.appID)
89 | assert.NotEmpty(t, chatBot.appSecret)
90 | assert.Empty(t, chatBot.webhook)
91 | assert.Equal(t, DomainFeishu, chatBot.domain)
92 | assert.Equal(t, ChatBot, chatBot.botType)
93 | assert.NotNil(t, chatBot.client)
94 | assert.NotNil(t, chatBot.logger)
95 |
96 | notifyBot := NewNotificationBot(testWebhook)
97 | assert.Empty(t, notifyBot.appID)
98 | assert.Empty(t, notifyBot.appSecret)
99 | assert.NotEmpty(t, notifyBot.webhook)
100 | assert.Empty(t, notifyBot.domain)
101 | assert.Equal(t, NotificationBot, notifyBot.botType)
102 | assert.NotNil(t, notifyBot.client)
103 | assert.NotNil(t, notifyBot.logger)
104 | }
105 |
106 | func TestRequiredType(t *testing.T) {
107 | bot := newTestBot()
108 | assert.True(t, bot.requireType(ChatBot))
109 | assert.False(t, bot.requireType(NotificationBot))
110 | }
111 |
112 | func TestSetDomain(t *testing.T) {
113 | bot := newTestBot()
114 | assert.Equal(t, DomainFeishu, bot.domain)
115 | assert.Equal(t, DomainFeishu, bot.Domain())
116 | bot.SetDomain("https://test.test")
117 | assert.Equal(t, "https://test.test", bot.domain)
118 | assert.Equal(t, "https://test.test", bot.Domain())
119 | }
120 |
121 | func TestBotGetters(t *testing.T) {
122 | bot := newTestBot()
123 | assert.Equal(t, testAppID, bot.AppID())
124 | assert.Equal(t, ChatBot, bot.BotType())
125 | }
126 |
127 | func TestSetClient(t *testing.T) {
128 | bot := &Bot{}
129 | assert.Nil(t, bot.client)
130 | bot.SetClient(newDefaultClient())
131 | assert.NotNil(t, bot.client)
132 | }
133 |
134 | func TestSetWebhook(t *testing.T) {
135 | bot := NewNotificationBot("abc")
136 | assert.Equal(t, "abc", bot.webhook)
137 | bot.SetWebhook("def")
138 | assert.Equal(t, "def", bot.webhook)
139 | }
140 |
141 | func TestSetAutoRenew(t *testing.T) {
142 | bot := newTestBot()
143 | assert.True(t, bot.autoRenew)
144 | bot.SetAutoRenew(false)
145 | assert.False(t, bot.autoRenew)
146 | bot.SetAutoRenew(true)
147 | assert.True(t, bot.autoRenew)
148 | }
149 |
150 | func TestTenantAccessToken(t *testing.T) {
151 | bot := newTestBot()
152 | assert.Equal(t, "", bot.TenantAccessToken())
153 | bot.SetTenantAccessToken(TenantAccessToken{
154 | TenantAccessToken: "test",
155 | })
156 | assert.Equal(t, "test", bot.TenantAccessToken())
157 | }
158 |
--------------------------------------------------------------------------------
/v2/locale.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // Supported Lark locales
4 | const (
5 | LocaleZhCN = "zh_cn"
6 | LocaleZhHK = "zh_hk"
7 | LocaleZhTW = "zh_tw"
8 | LocaleEnUS = "en_us"
9 | LocaleJaJP = "ja_jp"
10 | )
11 |
--------------------------------------------------------------------------------
/v2/logger.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "context"
5 | "io"
6 | "log"
7 | "os"
8 | )
9 |
10 | // LogLevel defs
11 | type LogLevel int
12 |
13 | // LogLevels
14 | const (
15 | LogLevelTrace = iota + 1
16 | LogLevelDebug
17 | LogLevelInfo
18 | LogLevelWarn
19 | LogLevelError
20 | )
21 |
22 | // LogWrapper interface
23 | type LogWrapper interface {
24 | // for log print
25 | Log(context.Context, LogLevel, string)
26 | // for test redirection
27 | SetOutput(io.Writer)
28 | }
29 |
30 | // String .
31 | func (ll LogLevel) String() string {
32 | switch ll {
33 | case LogLevelTrace:
34 | return "TRACE"
35 | case LogLevelDebug:
36 | return "DEBUG"
37 | case LogLevelInfo:
38 | return "INFO"
39 | case LogLevelWarn:
40 | return "WARN"
41 | case LogLevelError:
42 | return "ERROR"
43 | }
44 | return ""
45 | }
46 |
47 | type stdLogger struct {
48 | *log.Logger
49 | }
50 |
51 | // Log standard log func
52 | func (sl stdLogger) Log(_ context.Context, level LogLevel, msg string) {
53 | sl.Printf("[%s] %s\n", level, msg)
54 | }
55 |
56 | const logPrefix = "[go-lark] "
57 |
58 | func initDefaultLogger() LogWrapper {
59 | // create a default std logger
60 | logger := stdLogger{
61 | log.New(os.Stderr, logPrefix, log.LstdFlags),
62 | }
63 | return logger
64 | }
65 |
66 | // SetLogger sets a new logger
67 | func (bot *Bot) SetLogger(logger LogWrapper) {
68 | bot.logger = logger
69 | }
70 |
71 | // Logger returns current logger
72 | func (bot Bot) Logger() LogWrapper {
73 | return bot.logger
74 | }
75 |
--------------------------------------------------------------------------------
/v2/logger_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestSetLogger(t *testing.T) {
10 | bot := newTestBot()
11 | newLogger := initDefaultLogger()
12 | bot.SetLogger(newLogger)
13 | assert.Equal(t, newLogger, bot.logger)
14 | assert.Equal(t, newLogger, bot.Logger())
15 | }
16 |
17 | func TestLogLevel(t *testing.T) {
18 | var logLevel LogLevel = LogLevelDebug
19 | assert.Equal(t, "DEBUG", logLevel.String())
20 | logLevel = LogLevelError
21 | assert.Equal(t, "ERROR", logLevel.String())
22 | logLevel = LogLevelTrace
23 | assert.Equal(t, "TRACE", logLevel.String())
24 | logLevel = LogLevelWarn
25 | assert.Equal(t, "WARN", logLevel.String())
26 | logLevel = LogLevelInfo
27 | assert.Equal(t, "INFO", logLevel.String())
28 | logLevel = 1000
29 | assert.Equal(t, "", logLevel.String())
30 | }
31 |
--------------------------------------------------------------------------------
/v2/message.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // Msg Types
4 | const (
5 | MsgText = "text"
6 | MsgPost = "post"
7 | MsgInteractive = "interactive"
8 | MsgImage = "image"
9 | MsgShareCard = "share_chat"
10 | MsgShareUser = "share_user"
11 | MsgAudio = "audio"
12 | MsgMedia = "media"
13 | MsgFile = "file"
14 | MsgSticker = "sticker"
15 | )
16 |
17 | // OutcomingMessage struct of an outcoming message
18 | type OutcomingMessage struct {
19 | MsgType string `json:"msg_type"`
20 | Content MessageContent `json:"content"`
21 | Card CardContent `json:"card"`
22 | // ID for user
23 | UIDType string `json:"-"`
24 | OpenID string `json:"open_id,omitempty"`
25 | Email string `json:"email,omitempty"`
26 | UserID string `json:"user_id,omitempty"`
27 | ChatID string `json:"chat_id,omitempty"`
28 | UnionID string `json:"-"`
29 | // For reply
30 | RootID string `json:"root_id,omitempty"`
31 | ReplyInThread bool `json:"reply_in_thread,omitempty"`
32 | // Sign for notification bot
33 | Sign string `json:"sign"`
34 | // Timestamp for sign
35 | Timestamp int64 `json:"timestamp"`
36 | // UUID for idempotency
37 | UUID string `json:"uuid"`
38 | }
39 |
40 | // CardContent struct of card content
41 | type CardContent map[string]interface{}
42 |
43 | // MessageContent struct of message content
44 | type MessageContent struct {
45 | Text *TextContent `json:"text,omitempty"`
46 | Image *ImageContent `json:"image,omitempty"`
47 | Post *PostContent `json:"post,omitempty"`
48 | Card *CardContent `json:"card,omitempty"`
49 | ShareChat *ShareChatContent `json:"share_chat,omitempty"`
50 | ShareUser *ShareUserContent `json:"share_user,omitempty"`
51 | Audio *AudioContent `json:"audio,omitempty"`
52 | Media *MediaContent `json:"media,omitempty"`
53 | File *FileContent `json:"file,omitempty"`
54 | Sticker *StickerContent `json:"sticker,omitempty"`
55 | Template *TemplateContent `json:"template,omitempty"`
56 | }
57 |
58 | // TextContent .
59 | type TextContent struct {
60 | Text string `json:"text"`
61 | }
62 |
63 | // ImageContent .
64 | type ImageContent struct {
65 | ImageKey string `json:"image_key"`
66 | }
67 |
68 | // ShareChatContent .
69 | type ShareChatContent struct {
70 | ChatID string `json:"chat_id"`
71 | }
72 |
73 | // ShareUserContent .
74 | type ShareUserContent struct {
75 | UserID string `json:"user_id"`
76 | }
77 |
78 | // AudioContent .
79 | type AudioContent struct {
80 | FileKey string `json:"file_key"`
81 | }
82 |
83 | // MediaContent .
84 | type MediaContent struct {
85 | FileName string `json:"file_name,omitempty"`
86 | FileKey string `json:"file_key"`
87 | ImageKey string `json:"image_key"`
88 | Duration int `json:"duration,omitempty"`
89 | }
90 |
91 | // FileContent .
92 | type FileContent struct {
93 | FileName string `json:"file_name,omitempty"`
94 | FileKey string `json:"file_key"`
95 | }
96 |
97 | // StickerContent .
98 | type StickerContent struct {
99 | FileKey string `json:"file_key"`
100 | }
101 |
102 | // TemplateContent .
103 | type TemplateContent struct {
104 | Type string `json:"type"`
105 | Data templateData `json:"data,omitempty"`
106 | }
107 |
108 | type templateData struct {
109 | TemplateID string `json:"template_id"`
110 | TemplateVersionName string `json:"template_version_name,omitempty"`
111 | TemplateVariable map[string]interface{} `json:"template_variable,omitempty"`
112 | }
113 |
--------------------------------------------------------------------------------
/v2/message_builder.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "encoding/json"
5 | "strconv"
6 | )
7 |
8 | // BuildMessage .
9 | func BuildMessage(om OutcomingMessage) (*IMMessageRequest, error) {
10 | req := IMMessageRequest{
11 | MsgType: string(om.MsgType),
12 | Content: buildContent(om),
13 | ReceiveID: buildReceiveID(om),
14 | }
15 | if req.ReceiveID == "" {
16 | return nil, ErrInvalidReceiveID
17 | }
18 | if req.Content == "" {
19 | return nil, ErrMessageNotBuild
20 | }
21 | if om.UUID != "" {
22 | req.UUID = om.UUID
23 | }
24 | return &req, nil
25 | }
26 |
27 | func buildReplyMessage(om OutcomingMessage) (*IMMessageRequest, error) {
28 | req := IMMessageRequest{
29 | MsgType: string(om.MsgType),
30 | Content: buildContent(om),
31 | ReceiveID: buildReceiveID(om),
32 | }
33 | if req.Content == "" {
34 | return nil, ErrMessageNotBuild
35 | }
36 | if om.ReplyInThread {
37 | req.ReplyInThread = om.ReplyInThread
38 | }
39 | if om.UUID != "" {
40 | req.UUID = om.UUID
41 | }
42 |
43 | return &req, nil
44 | }
45 |
46 | func buildUpdateMessage(om OutcomingMessage) (*IMMessageRequest, error) {
47 | req := IMMessageRequest{
48 | Content: buildContent(om),
49 | }
50 | if om.MsgType != MsgInteractive {
51 | req.MsgType = om.MsgType
52 | }
53 | if req.Content == "" {
54 | return nil, ErrMessageNotBuild
55 | }
56 |
57 | return &req, nil
58 | }
59 |
60 | func buildContent(om OutcomingMessage) string {
61 | var (
62 | content = ""
63 | b []byte
64 | err error
65 | )
66 | switch om.MsgType {
67 | case MsgText:
68 | b, err = json.Marshal(om.Content.Text)
69 | case MsgImage:
70 | b, err = json.Marshal(om.Content.Image)
71 | case MsgFile:
72 | b, err = json.Marshal(om.Content.File)
73 | case MsgShareCard:
74 | b, err = json.Marshal(om.Content.ShareChat)
75 | case MsgShareUser:
76 | b, err = json.Marshal(om.Content.ShareUser)
77 | case MsgPost:
78 | b, err = json.Marshal(om.Content.Post)
79 | case MsgInteractive:
80 | if om.Content.Card != nil {
81 | b, err = json.Marshal(om.Content.Card)
82 | } else if om.Content.Template != nil {
83 | b, err = json.Marshal(om.Content.Template)
84 | }
85 | case MsgAudio:
86 | b, err = json.Marshal(om.Content.Audio)
87 | case MsgMedia:
88 | b, err = json.Marshal(om.Content.Media)
89 | case MsgSticker:
90 | b, err = json.Marshal(om.Content.Sticker)
91 | }
92 | if err != nil {
93 | return ""
94 | }
95 | content = string(b)
96 |
97 | return content
98 | }
99 |
100 | func buildReceiveID(om OutcomingMessage) string {
101 | switch om.UIDType {
102 | case UIDEmail:
103 | return om.Email
104 | case UIDUserID:
105 | return om.UserID
106 | case UIDOpenID:
107 | return om.OpenID
108 | case UIDChatID:
109 | return om.ChatID
110 | case UIDUnionID:
111 | return om.UnionID
112 | }
113 | return ""
114 | }
115 |
116 | func buildEphemeralCard(om OutcomingMessage) map[string]interface{} {
117 | params := map[string]interface{}{
118 | "msg_type": om.MsgType,
119 | "chat_id": om.ChatID, // request must contain chat_id, even if it is empty
120 | }
121 | params[om.UIDType] = buildReceiveID(om)
122 | if len(om.RootID) > 0 {
123 | params["root_id"] = om.RootID
124 | }
125 | content := make(map[string]interface{})
126 | if om.Content.Text != nil {
127 | content["text"] = om.Content.Text.Text
128 | }
129 | if om.Content.Image != nil {
130 | content["image_key"] = om.Content.Image.ImageKey
131 | }
132 | if om.Content.ShareChat != nil {
133 | content["share_open_chat_id"] = om.Content.ShareChat.ChatID
134 | }
135 | if om.Content.Post != nil {
136 | content["post"] = *om.Content.Post
137 | }
138 | if om.MsgType == MsgInteractive && om.Content.Card != nil {
139 | params["card"] = *om.Content.Card
140 | }
141 | if len(om.Sign) > 0 {
142 | params["sign"] = om.Sign
143 | params["timestamp"] = strconv.FormatInt(om.Timestamp, 10)
144 | }
145 | params["content"] = content
146 | return params
147 | }
148 |
149 | func buildNotification(om OutcomingMessage) map[string]interface{} {
150 | params := map[string]interface{}{
151 | "msg_type": om.MsgType,
152 | }
153 | if len(om.RootID) > 0 {
154 | params["root_id"] = om.RootID
155 | }
156 | content := make(map[string]interface{})
157 | if om.Content.Text != nil {
158 | content["text"] = om.Content.Text.Text
159 | }
160 | if om.Content.Image != nil {
161 | content["image_key"] = om.Content.Image.ImageKey
162 | }
163 | if om.Content.ShareChat != nil {
164 | content["share_open_chat_id"] = om.Content.ShareChat.ChatID
165 | }
166 | if om.Content.Post != nil {
167 | content["post"] = *om.Content.Post
168 | }
169 | if om.MsgType == MsgInteractive && om.Content.Card != nil {
170 | params["card"] = *om.Content.Card
171 | }
172 | if len(om.Sign) > 0 {
173 | params["sign"] = om.Sign
174 | params["timestamp"] = strconv.FormatInt(om.Timestamp, 10)
175 | }
176 | params["content"] = content
177 | return params
178 | }
179 |
--------------------------------------------------------------------------------
/v2/msg_buf.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | )
7 |
8 | // MsgBuffer stores all the messages attached
9 | // You can call every function, but some of which is only available for specific condition
10 | type MsgBuffer struct {
11 | // Message type
12 | msgType string
13 | // Output
14 | message OutcomingMessage
15 |
16 | err error
17 | }
18 |
19 | // NewMsgBuffer creates a message buffer
20 | func NewMsgBuffer(newMsgType string) *MsgBuffer {
21 | msgBuffer := MsgBuffer{
22 | message: OutcomingMessage{
23 | MsgType: newMsgType,
24 | },
25 | msgType: newMsgType,
26 | }
27 | return &msgBuffer
28 | }
29 |
30 | // BindOpenID binds open_id
31 | func (m *MsgBuffer) BindOpenID(openID string) *MsgBuffer {
32 | m.message.OpenID = openID
33 | m.message.UIDType = UIDOpenID
34 | return m
35 | }
36 |
37 | // BindEmail binds email
38 | func (m *MsgBuffer) BindEmail(email string) *MsgBuffer {
39 | m.message.Email = email
40 | m.message.UIDType = UIDEmail
41 | return m
42 | }
43 |
44 | // BindChatID binds chat_id
45 | func (m *MsgBuffer) BindChatID(chatID string) *MsgBuffer {
46 | m.message.ChatID = chatID
47 | m.message.UIDType = UIDChatID
48 | return m
49 | }
50 |
51 | // BindOpenChatID binds open_chat_id
52 | func (m *MsgBuffer) BindOpenChatID(openChatID string) *MsgBuffer {
53 | m.BindChatID(openChatID)
54 | m.message.UIDType = UIDChatID
55 | return m
56 | }
57 |
58 | // BindUserID binds open_id
59 | func (m *MsgBuffer) BindUserID(userID string) *MsgBuffer {
60 | m.message.UserID = userID
61 | m.message.UIDType = UIDUserID
62 | return m
63 | }
64 |
65 | // BindUnionID binds union_id
66 | func (m *MsgBuffer) BindUnionID(unionID string) *MsgBuffer {
67 | m.message.UnionID = unionID
68 | m.message.UIDType = UIDUnionID
69 | return m
70 | }
71 |
72 | // BindReply binds root id for reply
73 | // rootID is OpenMessageID of the message you reply
74 | func (m *MsgBuffer) BindReply(rootID string) *MsgBuffer {
75 | m.message.RootID = rootID
76 | return m
77 | }
78 |
79 | // ReplyInThread replies message in thread
80 | func (m *MsgBuffer) ReplyInThread(replyInThread bool) *MsgBuffer {
81 | m.message.ReplyInThread = replyInThread
82 | return m
83 | }
84 |
85 | // WithSign generates sign for notification bot check
86 | func (m *MsgBuffer) WithSign(secret string, ts int64) *MsgBuffer {
87 | m.message.Sign, _ = GenSign(secret, ts)
88 | m.message.Timestamp = ts
89 | return m
90 | }
91 |
92 | // WithUUID add UUID to message for idempotency
93 | func (m *MsgBuffer) WithUUID(uuid string) *MsgBuffer {
94 | m.message.UUID = uuid
95 | return m
96 | }
97 |
98 | func (m MsgBuffer) typeError(funcName string, msgType string) error {
99 | return fmt.Errorf("`%s` is only available to `%s`", funcName, msgType)
100 | }
101 |
102 | // Text attaches text
103 | func (m *MsgBuffer) Text(text string) *MsgBuffer {
104 | if m.msgType != MsgText {
105 | m.err = m.typeError("Text", MsgText)
106 | return m
107 | }
108 | m.message.Content.Text = &TextContent{
109 | Text: text,
110 | }
111 | return m
112 | }
113 |
114 | // Image attaches image key
115 | // for MsgImage only
116 | func (m *MsgBuffer) Image(imageKey string) *MsgBuffer {
117 | if m.msgType != MsgImage {
118 | m.err = m.typeError("Image", MsgImage)
119 | return m
120 | }
121 | m.message.Content.Image = &ImageContent{
122 | ImageKey: imageKey,
123 | }
124 | return m
125 | }
126 |
127 | // ShareChat attaches chat id
128 | // for MsgShareChat only
129 | func (m *MsgBuffer) ShareChat(chatID string) *MsgBuffer {
130 | if m.msgType != MsgShareCard {
131 | m.err = m.typeError("ShareChat", MsgShareCard)
132 | return m
133 | }
134 | m.message.Content.ShareChat = &ShareChatContent{
135 | ChatID: chatID,
136 | }
137 | return m
138 | }
139 |
140 | // ShareUser attaches user id
141 | // for MsgShareUser only
142 | func (m *MsgBuffer) ShareUser(userID string) *MsgBuffer {
143 | if m.msgType != MsgShareUser {
144 | m.err = m.typeError("ShareUser", MsgShareUser)
145 | return m
146 | }
147 | m.message.Content.ShareUser = &ShareUserContent{
148 | UserID: userID,
149 | }
150 | return m
151 | }
152 |
153 | // File attaches file
154 | // for MsgFile only
155 | func (m *MsgBuffer) File(fileKey string) *MsgBuffer {
156 | if m.msgType != MsgFile {
157 | m.err = m.typeError("File", MsgFile)
158 | return m
159 | }
160 | m.message.Content.File = &FileContent{
161 | FileKey: fileKey,
162 | }
163 | return m
164 | }
165 |
166 | // Audio attaches audio
167 | // for MsgAudio only
168 | func (m *MsgBuffer) Audio(fileKey string) *MsgBuffer {
169 | if m.msgType != MsgAudio {
170 | m.err = m.typeError("Audio", MsgAudio)
171 | return m
172 | }
173 | m.message.Content.Audio = &AudioContent{
174 | FileKey: fileKey,
175 | }
176 | return m
177 | }
178 |
179 | // Media attaches media
180 | // for MsgMedia only
181 | func (m *MsgBuffer) Media(fileKey, imageKey string) *MsgBuffer {
182 | if m.msgType != MsgMedia {
183 | m.err = m.typeError("Media", MsgMedia)
184 | return m
185 | }
186 | m.message.Content.Media = &MediaContent{
187 | FileKey: fileKey,
188 | ImageKey: imageKey,
189 | }
190 | return m
191 | }
192 |
193 | // Sticker attaches sticker
194 | // for MsgSticker only
195 | func (m *MsgBuffer) Sticker(fileKey string) *MsgBuffer {
196 | if m.msgType != MsgSticker {
197 | m.err = m.typeError("Sticker", MsgSticker)
198 | return m
199 | }
200 | m.message.Content.Sticker = &StickerContent{
201 | FileKey: fileKey,
202 | }
203 | return m
204 | }
205 |
206 | // Post sets raw post content
207 | func (m *MsgBuffer) Post(postContent *PostContent) *MsgBuffer {
208 | if m.msgType != MsgPost {
209 | m.err = m.typeError("Post", MsgPost)
210 | return m
211 | }
212 | m.message.Content.Post = postContent
213 | return m
214 | }
215 |
216 | // Card binds card content with V4 format
217 | func (m *MsgBuffer) Card(cardContent string) *MsgBuffer {
218 | if m.msgType != MsgInteractive {
219 | m.err = m.typeError("Card", MsgInteractive)
220 | return m
221 | }
222 | card := make(CardContent)
223 | _ = json.Unmarshal([]byte(cardContent), &card)
224 | m.message.Content.Card = &card
225 | return m
226 | }
227 |
228 | // Template sets raw template content
229 | func (m *MsgBuffer) Template(tempateContent *TemplateContent) *MsgBuffer {
230 | if m.msgType != MsgInteractive {
231 | m.err = m.typeError("Template", MsgInteractive)
232 | return m
233 | }
234 | m.message.Content.Template = tempateContent
235 | return m
236 | }
237 |
238 | // Build message and return message body
239 | func (m *MsgBuffer) Build() OutcomingMessage {
240 | return m.message
241 | }
242 |
243 | // Error returns last error
244 | func (m *MsgBuffer) Error() error {
245 | return m.err
246 | }
247 |
248 | // Clear message in buffer
249 | func (m *MsgBuffer) Clear() *MsgBuffer {
250 | m.message = OutcomingMessage{
251 | MsgType: m.msgType,
252 | }
253 | m.err = nil
254 | return m
255 | }
256 |
--------------------------------------------------------------------------------
/v2/msg_buf_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestAttachText(t *testing.T) {
10 | mb := NewMsgBuffer(MsgText)
11 | msg := mb.Text("hello").Build()
12 | assert.Equal(t, "hello", msg.Content.Text.Text)
13 | }
14 |
15 | func TestAttachImage(t *testing.T) {
16 | mb := NewMsgBuffer(MsgImage)
17 | msg := mb.Image("aaaaa").Build()
18 | assert.Equal(t, "aaaaa", msg.Content.Image.ImageKey)
19 | }
20 |
21 | func TestMsgTextBinding(t *testing.T) {
22 | mb := NewMsgBuffer(MsgText)
23 | msg := mb.Text("hello, world").BindEmail(testUserEmail).Build()
24 | assert.Equal(t, "hello, world", msg.Content.Text.Text)
25 | assert.Equal(t, testUserEmail, msg.Email)
26 | }
27 |
28 | func TestBindingUserIDs(t *testing.T) {
29 | mb := NewMsgBuffer(MsgText)
30 | msgEmail := mb.BindEmail(testUserEmail).Build()
31 | assert.Equal(t, testUserEmail, msgEmail.Email)
32 |
33 | mb.Clear()
34 | msgOpenChatID := mb.BindOpenChatID(testGroupChatID).Build()
35 | assert.Equal(t, testGroupChatID, msgOpenChatID.ChatID)
36 |
37 | mb.Clear()
38 | msgUserID := mb.BindUserID("333444").Build()
39 | assert.Equal(t, "333444", msgUserID.UserID)
40 |
41 | mb.Clear()
42 | msgReplyID := mb.BindReply("om_f779ffe0ffa3d1b94fc1ef5fcb6f1063").Build()
43 | assert.Equal(t, "om_f779ffe0ffa3d1b94fc1ef5fcb6f1063", msgReplyID.RootID)
44 | mb.ReplyInThread(true)
45 | assert.False(t, msgReplyID.ReplyInThread)
46 | }
47 |
48 | func TestMsgShareChat(t *testing.T) {
49 | mb := NewMsgBuffer(MsgShareCard)
50 | msg := mb.ShareChat("6559399282837815565").Build()
51 | assert.Equal(t, MsgShareCard, msg.MsgType)
52 | assert.Equal(t, "6559399282837815565", msg.Content.ShareChat.ChatID)
53 | }
54 |
55 | func TestMsgShareUser(t *testing.T) {
56 | mb := NewMsgBuffer(MsgShareUser)
57 | msg := mb.ShareUser("334455").Build()
58 | assert.Equal(t, MsgShareUser, msg.MsgType)
59 | assert.Equal(t, "334455", msg.Content.ShareUser.UserID)
60 | }
61 |
62 | func TestMsgFile(t *testing.T) {
63 | mb := NewMsgBuffer(MsgFile)
64 | msg := mb.File("file_v2_71cafb2c-137f-4bb0-8381-ffd4971dbecg").Build()
65 | assert.Equal(t, MsgFile, msg.MsgType)
66 | assert.Equal(t, "file_v2_71cafb2c-137f-4bb0-8381-ffd4971dbecg", msg.Content.File.FileKey)
67 | }
68 |
69 | func TestMsgAudio(t *testing.T) {
70 | mb := NewMsgBuffer(MsgAudio)
71 | msg := mb.Audio("file_v2_71cafb2c-137f-4bb0-8381-ffd4971dbecg").Build()
72 | assert.Equal(t, MsgAudio, msg.MsgType)
73 | assert.Equal(t, "file_v2_71cafb2c-137f-4bb0-8381-ffd4971dbecg", msg.Content.Audio.FileKey)
74 | }
75 |
76 | func TestMsgMedia(t *testing.T) {
77 | mb := NewMsgBuffer(MsgMedia)
78 | msg := mb.Media("file_v2_b53cd6cc-5327-4968-8bf6-4528deb3068g", "img_v2_b276195a-9ae0-4fec-bbfe-f74b4d5a994g").Build()
79 | assert.Equal(t, MsgMedia, msg.MsgType)
80 | assert.Equal(t, "file_v2_b53cd6cc-5327-4968-8bf6-4528deb3068g", msg.Content.Media.FileKey)
81 | assert.Equal(t, "img_v2_b276195a-9ae0-4fec-bbfe-f74b4d5a994g", msg.Content.Media.ImageKey)
82 | }
83 |
84 | func TestMsgSticker(t *testing.T) {
85 | mb := NewMsgBuffer(MsgSticker)
86 | msg := mb.Sticker("4ba009df-2453-47b3-a753-444b152217bg").Build()
87 | assert.Equal(t, MsgSticker, msg.MsgType)
88 | assert.Equal(t, "4ba009df-2453-47b3-a753-444b152217bg", msg.Content.Sticker.FileKey)
89 | }
90 |
91 | func TestMsgWithWrongType(t *testing.T) {
92 | mb := NewMsgBuffer(MsgText)
93 | mb.ShareChat("6559399282837815565")
94 | assert.Equal(t, mb.Error().Error(), "`ShareChat` is only available to `share_chat`")
95 | mb.ShareUser("334455")
96 | assert.Equal(t, mb.Error().Error(), "`ShareUser` is only available to `share_user`")
97 | mb.Image("aaa")
98 | assert.Equal(t, mb.Error().Error(), "`Image` is only available to `image`")
99 | mb.File("aaa")
100 | assert.Equal(t, mb.Error().Error(), "`File` is only available to `file`")
101 | mb.Audio("aaa")
102 | assert.Equal(t, mb.Error().Error(), "`Audio` is only available to `audio`")
103 | mb.Media("aaa", "bbb")
104 | assert.Equal(t, mb.Error().Error(), "`Media` is only available to `media`")
105 | mb.Sticker("aaa")
106 | assert.Equal(t, mb.Error().Error(), "`Sticker` is only available to `sticker`")
107 | mb.Post(nil)
108 | assert.Equal(t, mb.Error().Error(), "`Post` is only available to `post`")
109 | mb.Card("nil")
110 | assert.Equal(t, mb.Error().Error(), "`Card` is only available to `interactive`")
111 | mbp := NewMsgBuffer(MsgPost)
112 | mbp.Text("hello")
113 | assert.Equal(t, mbp.Error().Error(), "`Text` is only available to `text`")
114 | }
115 |
116 | func TestClearMessage(t *testing.T) {
117 | mb := NewMsgBuffer(MsgText)
118 | mb.Text("hello, world").Build()
119 | assert.Equal(t, "hello, world", mb.message.Content.Text.Text)
120 | mb.Clear()
121 | assert.Equal(t, MsgText, mb.msgType)
122 | assert.Empty(t, mb.message.Content)
123 | mb.Text("attach again").Build()
124 | assert.Equal(t, "attach again", mb.message.Content.Text.Text)
125 | }
126 |
127 | func TestWorkWithTextBuilder(t *testing.T) {
128 | mb := NewMsgBuffer(MsgText)
129 | mb.Text(NewTextBuilder().Textln("hello, world").Render()).Build()
130 | assert.Equal(t, "hello, world\n", mb.message.Content.Text.Text)
131 | }
132 |
133 | func TestWithSign(t *testing.T) {
134 | mb := NewMsgBuffer(MsgText)
135 | assert.Empty(t, mb.message.Sign)
136 | msg := mb.WithSign("xxx", 1661860880).Build()
137 | assert.NotEmpty(t, mb.message.Sign)
138 | assert.Equal(t, "QnWVTSBe6FmQDE0bG6X0mURbI+DnvVyu1h+j5dHOjrU=", msg.Sign)
139 | }
140 |
141 | func TestWithUUID(t *testing.T) {
142 | mb := NewMsgBuffer(MsgText)
143 | assert.Empty(t, mb.message.UUID)
144 | msg := mb.WithUUID("abc-def-0000").Build()
145 | assert.NotEmpty(t, mb.message.UUID)
146 | assert.Equal(t, "abc-def-0000", msg.UUID)
147 | }
148 |
--------------------------------------------------------------------------------
/v2/msg_card_builder.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "github.com/go-lark/card-builder"
5 | "github.com/go-lark/card-builder/i18n"
6 | )
7 |
8 | type i18nCardBuilder struct{}
9 |
10 | // CardBuilder .
11 | type CardBuilder struct {
12 | I18N *i18nCardBuilder
13 | }
14 |
15 | // Card wraps i18n card
16 | func (i18nCardBuilder) Card(blocks ...*i18n.LocalizedBlock) *i18n.Block {
17 | return i18n.Card(blocks...)
18 | }
19 |
20 | func (i18nCardBuilder) WithLocale(locale string, elements ...card.Element) *i18n.LocalizedBlock {
21 | return i18n.WithLocale(locale, elements...)
22 | }
23 |
24 | // Title wraps i18n title block
25 | func (i18nCardBuilder) LocalizedText(locale, s string) *i18n.LocalizedTextBlock {
26 | return i18n.LocalizedText(locale, s)
27 | }
28 |
29 | // NewCardBuilder .
30 | func NewCardBuilder() *CardBuilder {
31 | return &CardBuilder{
32 | I18N: &i18nCardBuilder{},
33 | }
34 | }
35 |
36 | // Card assigns elements
37 | func (CardBuilder) Card(elements ...card.Element) *card.Block {
38 | return card.Card(elements...)
39 | }
40 |
41 | // Action elements including Button, SelectMenu, Overflow, DatePicker, TimePicker, DatetimePicker
42 | func (CardBuilder) Action(actions ...card.Element) *card.ActionBlock {
43 | return card.Action(actions...)
44 | }
45 |
46 | // Button .
47 | func (CardBuilder) Button(text *card.TextBlock) *card.ButtonBlock {
48 | return card.Button(text)
49 | }
50 |
51 | // Confirm .
52 | func (CardBuilder) Confirm(title, text string) *card.ConfirmBlock {
53 | return card.Confirm(title, text)
54 | }
55 |
56 | // DatePicker .
57 | func (CardBuilder) DatePicker() *card.DatePickerBlock {
58 | return card.DatePicker()
59 | }
60 |
61 | // TimePicker .
62 | func (CardBuilder) TimePicker() *card.TimePickerBlock {
63 | return card.TimePicker()
64 | }
65 |
66 | // DatetimePicker .
67 | func (CardBuilder) DatetimePicker() *card.DatetimePickerBlock {
68 | return card.DatetimePicker()
69 | }
70 |
71 | // Div .
72 | func (CardBuilder) Div(fields ...*card.FieldBlock) *card.DivBlock {
73 | return card.Div(fields...)
74 | }
75 |
76 | // Field .
77 | func (CardBuilder) Field(text *card.TextBlock) *card.FieldBlock {
78 | return card.Field(text)
79 | }
80 |
81 | // Hr .
82 | func (CardBuilder) Hr() *card.HrBlock {
83 | return card.Hr()
84 | }
85 |
86 | // Img .
87 | func (CardBuilder) Img(key string) *card.ImgBlock {
88 | return card.Img(key)
89 | }
90 |
91 | // Note .
92 | func (CardBuilder) Note() *card.NoteBlock {
93 | return card.Note()
94 | }
95 |
96 | // Option .
97 | func (CardBuilder) Option(value string) *card.OptionBlock {
98 | return card.Option(value)
99 | }
100 |
101 | // Overflow .
102 | func (CardBuilder) Overflow(options ...*card.OptionBlock) *card.OverflowBlock {
103 | return card.Overflow(options...)
104 | }
105 |
106 | // SelectMenu .
107 | func (CardBuilder) SelectMenu(options ...*card.OptionBlock) *card.SelectMenuBlock {
108 | return card.SelectMenu(options...)
109 | }
110 |
111 | // Text .
112 | func (CardBuilder) Text(s string) *card.TextBlock {
113 | return card.Text(s)
114 | }
115 |
116 | // Markdown .
117 | func (CardBuilder) Markdown(s string) *card.MarkdownBlock {
118 | return card.Markdown(s)
119 | }
120 |
121 | // URL .
122 | func (CardBuilder) URL() *card.URLBlock {
123 | return card.URL()
124 | }
125 |
126 | // ColumnSet column set module
127 | func (CardBuilder) ColumnSet(columns ...*card.ColumnBlock) *card.ColumnSetBlock {
128 | return card.ColumnSet(columns...)
129 | }
130 |
131 | // Column column module
132 | func (CardBuilder) Column(elements ...card.Element) *card.ColumnBlock {
133 | return card.Column(elements...)
134 | }
135 |
136 | // ColumnSetAction column action module
137 | func (CardBuilder) ColumnSetAction(url *card.URLBlock) *card.ColumnSetActionBlock {
138 | return card.ColumnSetAction(url)
139 | }
140 |
--------------------------------------------------------------------------------
/v2/msg_post_builder.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // PostContent .
4 | type PostContent map[string]PostBody
5 |
6 | // PostBody .
7 | type PostBody struct {
8 | Title string `json:"title"`
9 | Content [][]PostElem `json:"content"`
10 | }
11 |
12 | // PostElem .
13 | type PostElem struct {
14 | Tag string `json:"tag"`
15 | // For Text
16 | UnEscape *bool `json:"un_escape,omitempty"`
17 | Text *string `json:"text,omitempty"`
18 | Lines *int `json:"lines,omitempty"`
19 | // For Link
20 | Href *string `json:"href,omitempty"`
21 | // For At
22 | UserID *string `json:"user_id,omitempty"`
23 | // For Image
24 | ImageKey *string `json:"image_key,omitempty"`
25 | ImageWidth *int `json:"width,omitempty"`
26 | ImageHeight *int `json:"height,omitempty"`
27 | }
28 |
29 | const (
30 | msgPostText = "text"
31 | msgPostLink = "a"
32 | msgPostAt = "at"
33 | msgPostImage = "img"
34 | )
35 |
36 | // PostBuf .
37 | type PostBuf struct {
38 | Title string `json:"title"`
39 | Content []PostElem `json:"content"`
40 | }
41 |
42 | // MsgPostBuilder for build text buf
43 | type MsgPostBuilder struct {
44 | buf map[string]*PostBuf
45 | curLocale string
46 | }
47 |
48 | const defaultLocale = LocaleZhCN
49 |
50 | // NewPostBuilder creates a text builder
51 | func NewPostBuilder() *MsgPostBuilder {
52 | return &MsgPostBuilder{
53 | buf: make(map[string]*PostBuf),
54 | curLocale: defaultLocale,
55 | }
56 | }
57 |
58 | // WithLocale switches to locale and returns self
59 | func (pb *MsgPostBuilder) WithLocale(locale string) *MsgPostBuilder {
60 | if _, ok := pb.buf[locale]; !ok {
61 | pb.buf[locale] = &PostBuf{}
62 | }
63 |
64 | pb.curLocale = locale
65 | return pb
66 | }
67 |
68 | // CurLocale switches to locale and returns the buffer of that locale
69 | func (pb *MsgPostBuilder) CurLocale() *PostBuf {
70 | return pb.WithLocale(pb.curLocale).buf[pb.curLocale]
71 | }
72 |
73 | // Title sets title
74 | func (pb *MsgPostBuilder) Title(title string) *MsgPostBuilder {
75 | pb.CurLocale().Title = title
76 | return pb
77 | }
78 |
79 | // TextTag creates a text tag
80 | func (pb *MsgPostBuilder) TextTag(text string, lines int, unescape bool) *MsgPostBuilder {
81 | pe := PostElem{
82 | Tag: msgPostText,
83 | Text: &text,
84 | Lines: &lines,
85 | UnEscape: &unescape,
86 | }
87 | pb.CurLocale().Content = append(pb.CurLocale().Content, pe)
88 | return pb
89 | }
90 |
91 | // LinkTag creates a link tag
92 | func (pb *MsgPostBuilder) LinkTag(text, href string) *MsgPostBuilder {
93 | pe := PostElem{
94 | Tag: msgPostLink,
95 | Text: &text,
96 | Href: &href,
97 | }
98 | pb.CurLocale().Content = append(pb.CurLocale().Content, pe)
99 | return pb
100 | }
101 |
102 | // AtTag creates an at tag
103 | func (pb *MsgPostBuilder) AtTag(text, userID string) *MsgPostBuilder {
104 | pe := PostElem{
105 | Tag: msgPostAt,
106 | Text: &text,
107 | UserID: &userID,
108 | }
109 | pb.CurLocale().Content = append(pb.CurLocale().Content, pe)
110 | return pb
111 | }
112 |
113 | // ImageTag creates an image tag
114 | func (pb *MsgPostBuilder) ImageTag(imageKey string, imageWidth, imageHeight int) *MsgPostBuilder {
115 | pe := PostElem{
116 | Tag: msgPostImage,
117 | ImageKey: &imageKey,
118 | ImageWidth: &imageWidth,
119 | ImageHeight: &imageHeight,
120 | }
121 | pb.CurLocale().Content = append(pb.CurLocale().Content, pe)
122 | return pb
123 | }
124 |
125 | // Clear all message
126 | func (pb *MsgPostBuilder) Clear() {
127 | pb.curLocale = defaultLocale
128 | pb.buf = make(map[string]*PostBuf)
129 | }
130 |
131 | // Render message
132 | func (pb *MsgPostBuilder) Render() *PostContent {
133 | content := make(PostContent)
134 | for locale, buf := range pb.buf {
135 | content[locale] = PostBody{
136 | Title: buf.Title,
137 | Content: [][]PostElem{buf.Content},
138 | }
139 | }
140 | return &content
141 | }
142 |
143 | // Len returns buf len
144 | func (pb MsgPostBuilder) Len() int {
145 | return len(pb.CurLocale().Content)
146 | }
147 |
--------------------------------------------------------------------------------
/v2/msg_post_builder_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestPostLocale(t *testing.T) {
10 | pb := NewPostBuilder()
11 | assert.Equal(t, defaultLocale, pb.curLocale)
12 | pb.WithLocale(LocaleEnUS)
13 | assert.Equal(t, LocaleEnUS, pb.curLocale)
14 | pb.WithLocale(LocaleJaJP)
15 | assert.Equal(t, LocaleJaJP, pb.curLocale)
16 | }
17 |
18 | func TestPostTitle(t *testing.T) {
19 | pb := NewPostBuilder()
20 | pb.Title("title")
21 | assert.Equal(t, "title", pb.CurLocale().Title)
22 | }
23 |
24 | func TestPostTextTag(t *testing.T) {
25 | pb := NewPostBuilder()
26 | pb.TextTag("hello, world", 1, true)
27 | buf := pb.CurLocale().Content
28 | assert.Equal(t, "text", buf[0].Tag)
29 | assert.Equal(t, "hello, world", *(buf[0].Text))
30 | assert.Equal(t, 1, *(buf[0].Lines))
31 | assert.Equal(t, true, *(buf[0].UnEscape))
32 | }
33 |
34 | func TestPostLinkTag(t *testing.T) {
35 | pb := NewPostBuilder()
36 | pb.LinkTag("hello, world", "https://www.toutiao.com/")
37 | buf := pb.CurLocale().Content
38 | assert.Equal(t, "a", buf[0].Tag)
39 | assert.Equal(t, "hello, world", *(buf[0].Text))
40 | assert.Equal(t, "https://www.toutiao.com/", *(buf[0].Href))
41 | }
42 |
43 | func TestPostAtTag(t *testing.T) {
44 | pb := NewPostBuilder()
45 | pb.AtTag("www", "123456")
46 | buf := pb.CurLocale().Content
47 | assert.Equal(t, "at", buf[0].Tag)
48 | assert.Equal(t, "www", *(buf[0].Text))
49 | assert.Equal(t, "123456", *(buf[0].UserID))
50 | }
51 |
52 | func TestPostImgTag(t *testing.T) {
53 | pb := NewPostBuilder()
54 | pb.ImageTag("d9f7d37e-c47c-411b-8ec6-9861132e6986", 320, 240)
55 | buf := pb.CurLocale().Content
56 | assert.Equal(t, "img", buf[0].Tag)
57 | assert.Equal(t, "d9f7d37e-c47c-411b-8ec6-9861132e6986", *(buf[0].ImageKey))
58 | assert.Equal(t, 240, *(buf[0].ImageHeight))
59 | assert.Equal(t, 320, *(buf[0].ImageWidth))
60 | }
61 |
62 | func TestPostClearAndLen(t *testing.T) {
63 | pb := NewPostBuilder()
64 | pb.TextTag("hello, world", 1, true).LinkTag("link", "https://www.toutiao.com/")
65 | assert.Equal(t, 2, pb.Len())
66 | pb.Clear()
67 | assert.Empty(t, pb.buf)
68 | assert.Equal(t, 0, pb.Len())
69 | }
70 |
71 | func TestPostMultiLocaleContent(t *testing.T) {
72 | pb := NewPostBuilder()
73 | pb.Title("中文标题")
74 | assert.Equal(t, "中文标题", pb.CurLocale().Title)
75 | pb.TextTag("你好世界", 1, true).TextTag("其他内容", 1, true)
76 | assert.Equal(t, 2, pb.Len())
77 |
78 | pb.WithLocale(LocaleEnUS).Title("en title")
79 | pb.TextTag("hello, world", 1, true).LinkTag("link", "https://www.toutiao.com/")
80 | assert.Equal(t, 2, pb.Len())
81 | assert.Equal(t, "en title", pb.CurLocale().Title)
82 |
83 | content := pb.Render()
84 | t.Log(content)
85 | assert.Equal(t, "中文标题", (*content)[LocaleZhCN].Title)
86 | assert.Equal(t, "en title", (*content)[LocaleEnUS].Title)
87 | }
88 |
--------------------------------------------------------------------------------
/v2/msg_template_builder.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // MsgTemplateBuilder for build template
4 | type MsgTemplateBuilder struct {
5 | id string
6 | versionName string
7 | data map[string]interface{}
8 | }
9 |
10 | // NewTemplateBuilder creates a text builder
11 | func NewTemplateBuilder() *MsgTemplateBuilder {
12 | return &MsgTemplateBuilder{}
13 | }
14 |
15 | // BindTemplate .
16 | func (tb *MsgTemplateBuilder) BindTemplate(id, versionName string, data map[string]interface{}) *TemplateContent {
17 | tb.id = id
18 | tb.versionName = versionName
19 | tb.data = data
20 |
21 | tc := &TemplateContent{
22 | Type: "template",
23 | Data: templateData{
24 | TemplateID: tb.id,
25 | TemplateVersionName: tb.versionName,
26 | },
27 | }
28 |
29 | if data != nil {
30 | tc.Data.TemplateVariable = tb.data
31 | }
32 |
33 | return tc
34 | }
35 |
--------------------------------------------------------------------------------
/v2/msg_template_builder_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestBindTemplate(t *testing.T) {
10 | b := NewTemplateBuilder()
11 | assert.Empty(t, b.id)
12 | assert.Empty(t, b.versionName)
13 | assert.Nil(t, b.data)
14 |
15 | _ = b.BindTemplate("AAqCYI07MQWh1", "1.0.0", map[string]interface{}{
16 | "name": "志田千陽",
17 | })
18 | assert.Equal(t, "AAqCYI07MQWh1", b.id)
19 | assert.Equal(t, "1.0.0", b.versionName)
20 | assert.NotEmpty(t, b.data)
21 | }
22 |
--------------------------------------------------------------------------------
/v2/msg_text_builder.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // textElemType of a text buf
8 | type textElemType int
9 |
10 | type textElem struct {
11 | elemType textElemType
12 | content string
13 | }
14 |
15 | const (
16 | // MsgText text only message
17 | msgText textElemType = iota
18 | // MsgAt @somebody
19 | msgAt
20 | // MsgAtAll @all
21 | msgAtAll
22 | // msgSpace space
23 | msgSpace
24 | )
25 |
26 | // MsgTextBuilder for build text buf
27 | type MsgTextBuilder struct {
28 | buf []textElem
29 | }
30 |
31 | // NewTextBuilder creates a text builder
32 | func NewTextBuilder() *MsgTextBuilder {
33 | return &MsgTextBuilder{
34 | buf: make([]textElem, 0),
35 | }
36 | }
37 |
38 | // Text adds simple texts
39 | func (tb *MsgTextBuilder) Text(text ...interface{}) *MsgTextBuilder {
40 | elem := textElem{
41 | elemType: msgText,
42 | content: fmt.Sprint(text...),
43 | }
44 | tb.buf = append(tb.buf, elem)
45 | return tb
46 | }
47 |
48 | // Textln adds simple texts with a newline
49 | func (tb *MsgTextBuilder) Textln(text ...interface{}) *MsgTextBuilder {
50 | elem := textElem{
51 | elemType: msgText,
52 | content: fmt.Sprintln(text...),
53 | }
54 | tb.buf = append(tb.buf, elem)
55 | return tb
56 | }
57 |
58 | // Textf adds texts with format
59 | func (tb *MsgTextBuilder) Textf(textFmt string, text ...interface{}) *MsgTextBuilder {
60 | elem := textElem{
61 | elemType: msgText,
62 | content: fmt.Sprintf(textFmt, text...),
63 | }
64 | tb.buf = append(tb.buf, elem)
65 | return tb
66 | }
67 |
68 | // Mention @somebody
69 | func (tb *MsgTextBuilder) Mention(userID string) *MsgTextBuilder {
70 | elem := textElem{
71 | elemType: msgAt,
72 | content: fmt.Sprintf("@user", userID),
73 | }
74 | tb.buf = append(tb.buf, elem)
75 | return tb
76 | }
77 |
78 | // MentionAll @all
79 | func (tb *MsgTextBuilder) MentionAll() *MsgTextBuilder {
80 | elem := textElem{
81 | elemType: msgAtAll,
82 | content: "@all",
83 | }
84 | tb.buf = append(tb.buf, elem)
85 | return tb
86 | }
87 |
88 | // Clear all message
89 | func (tb *MsgTextBuilder) Clear() {
90 | tb.buf = make([]textElem, 0)
91 | }
92 |
93 | // Render message
94 | func (tb *MsgTextBuilder) Render() string {
95 | var text string
96 | for _, msg := range tb.buf {
97 | text += msg.content
98 | }
99 | return text
100 | }
101 |
102 | // Len returns buf len
103 | func (tb MsgTextBuilder) Len() int {
104 | return len(tb.buf)
105 | }
106 |
--------------------------------------------------------------------------------
/v2/msg_text_builder_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestUseText(t *testing.T) {
10 | tb := NewTextBuilder()
11 | msg := tb.Text("hello, ", "world", 123).Render()
12 | assert.Equal(t, "hello, world123", tb.buf[0].content)
13 | assert.Equal(t, "hello, world123", msg)
14 | }
15 |
16 | func TestTextTextf(t *testing.T) {
17 | tb := NewTextBuilder()
18 | msg := tb.Textf("hello, %s: %d", "world", 1).Render()
19 | assert.Equal(t, "hello, world: 1", tb.buf[0].content)
20 | assert.Equal(t, "hello, world: 1", msg)
21 | }
22 |
23 | func TestTextTextln(t *testing.T) {
24 | tb := NewTextBuilder()
25 | msg := tb.Textln("hello", "world").Render()
26 | assert.Equal(t, "hello world\n", tb.buf[0].content)
27 | assert.Equal(t, "hello world\n", msg)
28 | }
29 |
30 | func TestTextMention(t *testing.T) {
31 | tb := NewTextBuilder()
32 | msg := tb.Text("hello, world").Mention("6454030812462448910").Render()
33 | assert.Equal(t, "hello, world@user", msg)
34 | }
35 |
36 | func TestTextMentionAll(t *testing.T) {
37 | tb := NewTextBuilder()
38 | msg := tb.Text("hello, world").MentionAll().Render()
39 | assert.Equal(t, "hello, world@all", msg)
40 | }
41 |
42 | func TestTextClearAndLen(t *testing.T) {
43 | tb := NewTextBuilder()
44 | tb.Text("hello, world").MentionAll()
45 | assert.Equal(t, 2, tb.Len())
46 | tb.Clear()
47 | assert.Empty(t, tb.buf)
48 | assert.Equal(t, 0, tb.Len())
49 | }
50 |
--------------------------------------------------------------------------------
/v2/user.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | // UID types
4 | const (
5 | UIDEmail = "email"
6 | UIDUserID = "user_id"
7 | UIDOpenID = "open_id"
8 | UIDChatID = "chat_id"
9 | UIDUnionID = "union_id"
10 | )
11 |
12 | // OptionalUserID contains either openID, chatID, userID, or email
13 | type OptionalUserID struct {
14 | UIDType string
15 | RealID string
16 | }
17 |
18 | func withOneID(uidType, realID string) *OptionalUserID {
19 | return &OptionalUserID{
20 | UIDType: uidType,
21 | RealID: realID,
22 | }
23 | }
24 |
25 | // WithEmail uses email as userID
26 | func WithEmail(email string) *OptionalUserID {
27 | return withOneID(UIDEmail, email)
28 | }
29 |
30 | // WithUserID uses userID as userID
31 | func WithUserID(userID string) *OptionalUserID {
32 | return withOneID(UIDUserID, userID)
33 | }
34 |
35 | // WithOpenID uses openID as userID
36 | func WithOpenID(openID string) *OptionalUserID {
37 | return withOneID(UIDOpenID, openID)
38 | }
39 |
40 | // WithChatID uses chatID as userID
41 | func WithChatID(chatID string) *OptionalUserID {
42 | return withOneID(UIDChatID, chatID)
43 | }
44 |
45 | // WithUnionID uses chatID as userID
46 | func WithUnionID(unionID string) *OptionalUserID {
47 | return withOneID(UIDUnionID, unionID)
48 | }
49 |
--------------------------------------------------------------------------------
/v2/user_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestWithFunctions(t *testing.T) {
10 | emailUID := WithEmail(testUserEmail)
11 | assert.Equal(t, "email", emailUID.UIDType)
12 | assert.Equal(t, testUserEmail, emailUID.RealID)
13 |
14 | openIDUID := WithOpenID(testUserOpenID)
15 | assert.Equal(t, "open_id", openIDUID.UIDType)
16 | assert.Equal(t, testUserOpenID, openIDUID.RealID)
17 |
18 | chatIDUID := WithChatID(testGroupChatID)
19 | assert.Equal(t, "chat_id", chatIDUID.UIDType)
20 | assert.Equal(t, testGroupChatID, chatIDUID.RealID)
21 |
22 | fakeUID := "6893390418998738946"
23 | userIDUID := WithUserID(fakeUID)
24 | assert.Equal(t, "user_id", userIDUID.UIDType)
25 | assert.Equal(t, fakeUID, userIDUID.RealID)
26 | }
27 |
--------------------------------------------------------------------------------
/v2/util.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "os"
7 | )
8 |
9 | // DownloadFile downloads from a URL to local path
10 | func DownloadFile(path, url string) error {
11 | // Create the file
12 | out, err := os.Create(path)
13 | if err != nil {
14 | return err
15 | }
16 | defer out.Close()
17 |
18 | // Get the data
19 | resp, err := http.Get(url)
20 | if err != nil {
21 | return err
22 | }
23 | defer resp.Body.Close()
24 |
25 | // Write the body to file
26 | _, err = io.Copy(out, resp.Body)
27 | if err != nil {
28 | return err
29 | }
30 |
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/v2/util_test.go:
--------------------------------------------------------------------------------
1 | package lark
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestFileDownload(t *testing.T) {
12 | ts := time.Now().Unix()
13 | filename := fmt.Sprintf("/tmp/go-lark-ci-%d", ts)
14 | err := DownloadFile(filename, "https://s1-fs.pstatp.com/static-resource/v1/363e0009ef09d43d5a96~?image_size=72x72&cut_type=&quality=&format=png&sticker_format=.webp")
15 | if assert.NoError(t, err) {
16 | assert.FileExists(t, filename)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------