0.1.8 Release

This commit is contained in:
Alexander Kiryukhin 2019-08-09 02:02:05 +03:00
parent d13acd7da9
commit 94c96cef29
14 changed files with 1192 additions and 997 deletions

2
.gitignore vendored
View file

@ -24,3 +24,5 @@ _testmain.go
*.prof
.idea
tmp

View file

@ -1,22 +1,75 @@
# TamTam Go
Простая реализация клиента к TamTam Bot API на Go.
[![Sourcegraph](https://sourcegraph.com/github.com/neonxp/tamtam/-/badge.svg?style=flat-square)](https://sourcegraph.com/github.com/neonxp/tamtam?badge)
[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/neonxp/tamtam)
[![Go Report Card](https://goreportcard.com/badge/github.com/neonxp/tamtam?style=flat-square)](https://goreportcard.com/report/github.com/neonxp/tamtam)
[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/neonxp/tamtam/master/LICENSE)
На данном этапе представляет собой 1 в 1 реализованные методы API + HTTPHandler для веб-хуков + сгенерированные модели.
Простая реализация клиента к TamTam Bot API на Go. Поддерживается получение обновление как с помощью вебхуков, так и лонгполлингом.
Поддерживаемая версия API - 0.1.8
## Документация
В общем случае, методы повторяют такие из [официальной документации](https://dev.tamtam.chat/)
Так же добавлены хелпер для создания клавиатуры (`api.Messages.NewKeyboardBuilder()`) и для загрузки вложений (`api.Uploads.UploadMedia(uploadType UploadType, filename string)`). Пример создания клавиатуры см. ниже в примере.
Её пока нет :)
Но http://godoc.org/github.com/neonxp/tamtam/ может вам помочь.
## Статус
API пока крайне неставбильное и обязательно будет меняться в будущем. Учитывайте это!
Остальное описано тут http://godoc.org/github.com/neonxp/tamtam/ и в примерах из директории [examples](https://github.com/neonxp/tamtam/tree/master/examples)
## Пример
Простые примеры использования находятся в директории `examples`.
```go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/neonxp/tamtam"
)
func main() {
api := tamtam.New(os.Getenv("TOKEN"))
info, err := api.Bots.GetBot() // Простой метод
log.Printf("Get me: %#v %#v", info, err)
go api.UpdatesLoop(context.Background()) // Запуск цикла получения обновлений
for upd := range api.GetUpdates() { // Чтение из канала с обновлениями
log.Printf("Received: %#v", upd)
switch upd := upd.(type) { // Определение типа пришедшего обновления
case *tamtam.MessageCreatedUpdate:
// Создание клавиатуры
keyboard := api.Messages.NewKeyboardBuilder()
keyboard.
AddRow().
AddGeolocation("Прислать геолокацию", true).
AddContact("Прислать контакт")
keyboard.
AddRow().
AddLink("Библиотека", tamtam.POSITIVE, "https://github.com/neonxp/tamtam").
AddCallback("Колбек 1", tamtam.NEGATIVE, "callback_1").
AddCallback("Колбек 2", tamtam.NEGATIVE, "callback_2")
// Отправка сообщения с клавиатурой
res, err := api.Messages.SendMessage(0, upd.Message.Sender.UserId, &tamtam.NewMessageBody{
Text: fmt.Sprintf("Hello, %s! Your message: %s", upd.Message.Sender.Name, upd.Message.Body.Text),
Attachments: []interface{}{
tamtam.NewInlineKeyboardAttachmentRequest(keyboard.Build()),
},
})
log.Printf("Answer: %#v %#v", res, err)
case *tamtam.MessageCallbackUpdate:
res, err := api.Messages.SendMessage(0, upd.Callback.User.UserId, &tamtam.NewMessageBody{
Text: "Callback: " + upd.Callback.Payload,
})
log.Printf("Answer: %#v %#v", res, err)
default:
log.Printf("Unknown type: %#v", upd)
}
}
}
```
## Автор

572
api.go
View file

@ -1,499 +1,146 @@
/*
* TamTam Bot API
*/
// Package tamtam implements TamTam Bot API
// Copyright (c) 2019 Alexander Kiryukhin <a.kiryukhin@mail.ru>
package tamtam
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
type Api struct {
key string
version string
url *url.URL
Bots *bots
Chats *chats
Messages *messages
Subscriptions *subscriptions
Uploads *uploads
client *client
updates chan UpdateInterface
timeout int
pause int
logging bool
}
// New TamTam Api object
func New(key string) *Api {
u, _ := url.Parse("https://botapi.tamtam.chat/")
cl := newClient(key, "0.1.8", u)
return &Api{
key: key,
url: u,
version: "0.1.8",
Bots: newBots(cl),
Chats: newChats(cl),
Uploads: newUploads(cl),
Messages: newMessages(cl),
Subscriptions: newSubscriptions(cl),
client: cl,
updates: make(chan UpdateInterface),
timeout: 30,
pause: 1,
logging: false,
}
}
func (a *Api) EnableLogging() {
a.logging = true
}
// region Misc methods
func (a *Api) GetMe() (*UserWithPhoto, error) {
result := new(UserWithPhoto)
values := url.Values{}
body, err := a.request(http.MethodGet, "me", values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) GetUploadURL(uploadType UploadType) (*UploadEndpoint, error) {
result := new(UploadEndpoint)
values := url.Values{}
values.Set("type", string(uploadType))
body, err := a.request(http.MethodPost, "uploads", values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) UploadMedia(uploadType UploadType, filename string) (*UploadedInfo, error) {
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
// this step is very important
fileWriter, err := bodyWriter.CreateFormFile("data", filename)
if err != nil {
return nil, err
}
// open file handle
fh, err := os.Open(filename)
if err != nil {
return nil, err
}
defer fh.Close()
//iocopy
_, err = io.Copy(fileWriter, fh)
if err != nil {
return nil, err
}
if err := bodyWriter.Close(); err != nil {
return nil, err
}
target, err := a.GetUploadURL(uploadType)
if err != nil {
return nil, err
}
contentType := bodyWriter.FormDataContentType()
resp, err := http.Post(target.Url, contentType, bodyBuf)
if err != nil {
return nil, err
}
defer resp.Body.Close()
result := new(UploadedInfo)
return result, json.NewDecoder(resp.Body).Decode(result)
}
func (a *Api) GetUpdatesLoop(ctx context.Context, updates chan interface{}) error {
for {
select {
case <-ctx.Done():
return nil
case <-time.After(time.Duration(a.pause) * time.Second):
var marker int64
for {
upds, err := a.getUpdates(50, a.timeout, marker, []string{})
if err != nil {
return err
}
if len(upds.Updates) == 0 {
break
}
for _, u := range upds.Updates {
updates <- a.bytesToProperUpdate(u)
}
marker = upds.Marker
}
}
}
}
func (a *Api) GetHandler(updates chan interface{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
b, _ := ioutil.ReadAll(r.Body)
updates <- a.bytesToProperUpdate(b)
}
}
// endregion
// region Chat methods
func (a *Api) GetChats(count, marker int64) (*ChatList, error) {
result := new(ChatList)
values := url.Values{}
if count > 0 {
values.Set("count", strconv.Itoa(int(count)))
}
if marker > 0 {
values.Set("marker", strconv.Itoa(int(marker)))
}
body, err := a.request(http.MethodGet, "chats", values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) GetChat(chatID int64) (*Chat, error) {
result := new(Chat)
values := url.Values{}
body, err := a.request(http.MethodGet, fmt.Sprintf("chats/%d", chatID), values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) GetChatMembership(chatID int64) (*ChatMember, error) {
result := new(ChatMember)
values := url.Values{}
body, err := a.request(http.MethodGet, fmt.Sprintf("chats/%d/members/me", chatID), values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) GetChatMembers(chatID, count, marker int64) (*ChatMembersList, error) {
result := new(ChatMembersList)
values := url.Values{}
if count > 0 {
values.Set("count", strconv.Itoa(int(count)))
}
if marker > 0 {
values.Set("marker", strconv.Itoa(int(marker)))
}
body, err := a.request(http.MethodGet, fmt.Sprintf("chats/%d/members", chatID), values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) LeaveChat(chatID int64) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
body, err := a.request(http.MethodDelete, fmt.Sprintf("chats/%d/members/me", chatID), values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) EditChat(chatID int64, update *ChatPatch) (*Chat, error) {
result := new(Chat)
values := url.Values{}
body, err := a.request(http.MethodPatch, fmt.Sprintf("chats/%d", chatID), values, update)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) AddMember(chatID int64, users UserIdsList) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
body, err := a.request(http.MethodPost, fmt.Sprintf("chats/%d/members", chatID), values, users)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) RemoveMember(chatID int64, userID int64) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("user_id", strconv.Itoa(int(userID)))
body, err := a.request(http.MethodDelete, fmt.Sprintf("chats/%d/members", chatID), values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) SendAction(chatID int64, action SenderAction) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
body, err := a.request(http.MethodPost, fmt.Sprintf("chats/%d/actions", chatID), values, ActionRequestBody{Action: action})
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
// endregion
// region Message methods
func (a *Api) GetMessages(chatID int64, messageIDs []string, from int64, to int64, count int64) (*MessageList, error) {
result := new(MessageList)
values := url.Values{}
if chatID != 0 {
values.Set("chat_id", strconv.Itoa(int(chatID)))
}
if len(messageIDs) > 0 {
for _, mid := range messageIDs {
values.Add("message_ids", mid)
}
}
if from != 0 {
values.Set("from", strconv.Itoa(int(from)))
}
if to != 0 {
values.Set("to", strconv.Itoa(int(to)))
}
if count > 0 {
values.Set("count", strconv.Itoa(int(count)))
}
body, err := a.request(http.MethodGet, "messages", values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) SendMessage(chatID int64, userID int64, message *NewMessageBody) (*Message, error) {
result := new(Message)
values := url.Values{}
if chatID != 0 {
values.Set("chat_id", strconv.Itoa(int(chatID)))
}
if userID != 0 {
values.Set("user_id", strconv.Itoa(int(userID)))
}
body, err := a.request(http.MethodPost, "messages", values, message)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) EditMessage(messageID int64, message *NewMessageBody) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("message_id", strconv.Itoa(int(messageID)))
body, err := a.request(http.MethodPut, "messages", values, message)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) DeleteMessage(messageID int64) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("message_id", strconv.Itoa(int(messageID)))
body, err := a.request(http.MethodDelete, "messages", values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) AnswerOnCallback(callbackID int64, callback *CallbackAnswer) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("callback_id", strconv.Itoa(int(callbackID)))
body, err := a.request(http.MethodPost, "answers", values, callback)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
// endregion
// region Subscriptions
func (a *Api) GetSubscriptions() (*GetSubscriptionsResult, error) {
result := new(GetSubscriptionsResult)
values := url.Values{}
body, err := a.request(http.MethodGet, "subscriptions", values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) Subscribe(subscribeURL string, updateTypes []string) (*SimpleQueryResult, error) {
subscription := &SubscriptionRequestBody{
Url: subscribeURL,
UpdateTypes: updateTypes,
Version: a.version,
}
result := new(SimpleQueryResult)
values := url.Values{}
body, err := a.request(http.MethodPost, "subscriptions", values, subscription)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
func (a *Api) Unsubscribe(subscriptionURL string) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("url", subscriptionURL)
body, err := a.request(http.MethodDelete, "subscriptions", values, nil)
if err != nil {
return result, err
}
defer body.Close()
return result, json.NewDecoder(body).Decode(result)
}
// endregion
// region Internal
func (a *Api) bytesToProperUpdate(b []byte) interface{} {
func (a *Api) bytesToProperUpdate(b []byte) UpdateInterface {
u := new(Update)
_ = json.Unmarshal(b, u)
switch u.UpdateType {
case UpdateTypeMessageCallback:
upd := UpdateMessageCallback{}
_ = json.Unmarshal(b, &upd)
switch u.GetUpdateType() {
case TypeMessageCallback:
upd := new(MessageCallbackUpdate)
_ = json.Unmarshal(b, upd)
return upd
case UpdateTypeMessageCreated:
upd := UpdateMessageCreated{}
_ = json.Unmarshal(b, &upd)
for _, att := range upd.Message.Body.RawAttachments {
case TypeMessageCreated:
upd := new(MessageCreatedUpdate)
_ = json.Unmarshal(b, upd)
for _, att := range upd.Message.Body.rawAttachments {
upd.Message.Body.Attachments = append(upd.Message.Body.Attachments, a.bytesToProperAttachment(att))
}
return upd
case UpdateTypeMessageRemoved:
upd := UpdateMessageRemoved{}
_ = json.Unmarshal(b, &upd)
case TypeMessageRemoved:
upd := new(MessageRemovedUpdate)
_ = json.Unmarshal(b, upd)
return upd
case UpdateTypeMessageEdited:
upd := UpdateMessageEdited{}
_ = json.Unmarshal(b, &upd)
for _, att := range upd.Message.Body.RawAttachments {
case TypeMessageEdited:
upd := new(MessageEditedUpdate)
_ = json.Unmarshal(b, upd)
for _, att := range upd.Message.Body.rawAttachments {
upd.Message.Body.Attachments = append(upd.Message.Body.Attachments, a.bytesToProperAttachment(att))
}
return upd
case UpdateTypeMessageRestored:
upd := UpdateMessageRestored{}
_ = json.Unmarshal(b, &upd)
case TypeBotAdded:
upd := new(BotAddedToChatUpdate)
_ = json.Unmarshal(b, upd)
return upd
case UpdateTypeBotAdded:
upd := UpdateBotAdded{}
_ = json.Unmarshal(b, &upd)
case TypeBotRemoved:
upd := new(BotRemovedFromChatUpdate)
_ = json.Unmarshal(b, upd)
return upd
case UpdateTypeBotRemoved:
upd := UpdateBotRemoved{}
_ = json.Unmarshal(b, &upd)
case TypeUserAdded:
upd := new(UserAddedToChatUpdate)
_ = json.Unmarshal(b, upd)
return upd
case UpdateTypeUserAdded:
upd := UpdateUserAdded{}
_ = json.Unmarshal(b, &upd)
case TypeUserRemoved:
upd := new(UserRemovedFromChatUpdate)
_ = json.Unmarshal(b, upd)
return upd
case UpdateTypeUserRemoved:
upd := UpdateUserRemoved{}
_ = json.Unmarshal(b, &upd)
case TypeBotStarted:
upd := new(BotStartedUpdate)
_ = json.Unmarshal(b, upd)
return upd
case UpdateTypeBotStarted:
upd := UpdateBotStarted{}
_ = json.Unmarshal(b, &upd)
return upd
case UpdateTypeChatTitleChanged:
upd := UpdateChatTitleChanged{}
_ = json.Unmarshal(b, &upd)
case TypeChatTitleChanged:
upd := new(ChatTitleChangedUpdate)
_ = json.Unmarshal(b, upd)
return upd
}
return nil
}
func (a *Api) bytesToProperAttachment(b []byte) interface{} {
func (a *Api) bytesToProperAttachment(b []byte) AttachmentInterface {
attachment := new(Attachment)
_ = json.Unmarshal(b, attachment)
switch attachment.Type {
switch attachment.GetAttachmentType() {
case AttachmentAudio:
res := &AudioAttachment{}
_ = json.Unmarshal(b, &res)
res := new(AudioAttachment)
_ = json.Unmarshal(b, res)
return res
case AttachmentContact:
res := &ContactAttachment{}
_ = json.Unmarshal(b, &res)
res := new(ContactAttachment)
_ = json.Unmarshal(b, res)
return res
case AttachmentFile:
res := &FileAttachment{}
_ = json.Unmarshal(b, &res)
res := new(FileAttachment)
_ = json.Unmarshal(b, res)
return res
case AttachmentImage:
res := &PhotoAttachment{}
_ = json.Unmarshal(b, &res)
res := new(PhotoAttachment)
_ = json.Unmarshal(b, res)
return res
case AttachmentKeyboard:
res := &InlineKeyboardAttachment{}
_ = json.Unmarshal(b, &res)
res := new(InlineKeyboardAttachment)
_ = json.Unmarshal(b, res)
return res
case AttachmentLocation:
res := &LocationAttachment{}
_ = json.Unmarshal(b, &res)
res := new(LocationAttachment)
_ = json.Unmarshal(b, res)
return res
case AttachmentShare:
res := &ShareAttachment{}
_ = json.Unmarshal(b, &res)
res := new(ShareAttachment)
_ = json.Unmarshal(b, res)
return res
case AttachmentSticker:
res := &StickerAttachment{}
_ = json.Unmarshal(b, &res)
res := new(StickerAttachment)
_ = json.Unmarshal(b, res)
return res
case AttachmentVideo:
res := &VideoAttachment{}
_ = json.Unmarshal(b, &res)
res := new(VideoAttachment)
_ = json.Unmarshal(b, res)
return res
}
return attachment
}
func (a *Api) getUpdates(limit int, timeout int, marker int64, types []string) (*UpdateList, error) {
func (a *Api) getUpdates(limit int, timeout int, marker int, types []string) (*UpdateList, error) {
result := new(UpdateList)
values := url.Values{}
if limit > 0 {
@ -503,57 +150,62 @@ func (a *Api) getUpdates(limit int, timeout int, marker int64, types []string) (
values.Set("timeout", strconv.Itoa(timeout))
}
if marker > 0 {
values.Set("marker", strconv.Itoa(int(marker)))
values.Set("marker", strconv.Itoa(marker))
}
if len(types) > 0 {
for _, t := range types {
values.Add("types", t)
}
}
body, err := a.request(http.MethodGet, "updates", values, nil)
body, err := a.client.request(http.MethodGet, "updates", values, nil)
if err != nil {
return result, err
}
defer body.Close()
jb, _ := ioutil.ReadAll(body)
if a.logging {
log.Printf("Received: %s", string(jb))
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
jb, _ := ioutil.ReadAll(body)
return result, json.Unmarshal(jb, result)
}
func (a *Api) request(method, path string, query url.Values, body interface{}) (io.ReadCloser, error) {
j, err := json.Marshal(body)
func (a *Api) UpdatesLoop(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
case <-time.After(time.Duration(a.pause) * time.Second):
var marker int
for {
upds, err := a.getUpdates(50, a.timeout, marker, []string{})
if err != nil {
return nil, err
return err
}
if len(upds.Updates) == 0 {
break
}
for _, u := range upds.Updates {
a.updates <- a.bytesToProperUpdate(u)
}
marker = *upds.Marker
}
}
}
return a.requestReader(method, path, query, bytes.NewReader(j))
}
func (a *Api) requestReader(method, path string, query url.Values, body io.Reader) (io.ReadCloser, error) {
c := http.DefaultClient
u := *a.url
u.Path = path
query.Set("access_token", a.key)
query.Set("v", a.version)
u.RawQuery = query.Encode()
if a.logging {
log.Printf("Sent: [%s %s] Query: %#v", method, path, query)
}
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return nil, err
}
resp, err := c.Do(req)
if resp.StatusCode != http.StatusOK {
errObj := new(Error)
err = json.NewDecoder(resp.Body).Decode(errObj)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("code=%s message=%s error=%s", errObj.Code, errObj.Message, errObj.Error)
}
return resp.Body, err
func (a *Api) GetUpdates() chan UpdateInterface {
return a.updates
}
// endregion
func (a *Api) GetHandler(updates chan interface{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := r.Body.Close(); err != nil {
log.Println(err)
}
}()
b, _ := ioutil.ReadAll(r.Body)
updates <- a.bytesToProperUpdate(b)
}
}

48
bots.go Normal file
View file

@ -0,0 +1,48 @@
// Package tamtam implements TamTam Bot API
// Copyright (c) 2019 Alexander Kiryukhin <a.kiryukhin@mail.ru>
package tamtam
import (
"encoding/json"
"log"
"net/http"
"net/url"
)
type bots struct {
client *client
}
func newBots(client *client) *bots {
return &bots{client: client}
}
func (a *bots) GetBot() (*BotInfo, error) {
result := new(BotInfo)
values := url.Values{}
body, err := a.client.request(http.MethodGet, "me", values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *bots) PatchBot(patch *BotPatch) (*BotInfo, error) {
result := new(BotInfo)
values := url.Values{}
body, err := a.client.request(http.MethodPatch, "me", values, patch)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}

168
chats.go Normal file
View file

@ -0,0 +1,168 @@
// Package tamtam implements TamTam Bot API
// Copyright (c) 2019 Alexander Kiryukhin <a.kiryukhin@mail.ru>
package tamtam
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strconv"
)
type chats struct {
client *client
}
func newChats(client *client) *chats {
return &chats{client: client}
}
func (a *chats) GetChats(count, marker int) (*ChatList, error) {
result := new(ChatList)
values := url.Values{}
if count > 0 {
values.Set("count", strconv.Itoa(int(count)))
}
if marker > 0 {
values.Set("marker", strconv.Itoa(int(marker)))
}
body, err := a.client.request(http.MethodGet, "chats", values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *chats) GetChat(chatID int) (*Chat, error) {
result := new(Chat)
values := url.Values{}
body, err := a.client.request(http.MethodGet, fmt.Sprintf("chats/%d", chatID), values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *chats) GetChatMembership(chatID int) (*ChatMember, error) {
result := new(ChatMember)
values := url.Values{}
body, err := a.client.request(http.MethodGet, fmt.Sprintf("chats/%d/members/me", chatID), values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *chats) GetChatMembers(chatID, count, marker int) (*ChatMembersList, error) {
result := new(ChatMembersList)
values := url.Values{}
if count > 0 {
values.Set("count", strconv.Itoa(int(count)))
}
if marker > 0 {
values.Set("marker", strconv.Itoa(int(marker)))
}
body, err := a.client.request(http.MethodGet, fmt.Sprintf("chats/%d/members", chatID), values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *chats) LeaveChat(chatID int) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
body, err := a.client.request(http.MethodDelete, fmt.Sprintf("chats/%d/members/me", chatID), values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *chats) EditChat(chatID int, update *ChatPatch) (*Chat, error) {
result := new(Chat)
values := url.Values{}
body, err := a.client.request(http.MethodPatch, fmt.Sprintf("chats/%d", chatID), values, update)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *chats) AddMember(chatID int, users UserIdsList) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
body, err := a.client.request(http.MethodPost, fmt.Sprintf("chats/%d/members", chatID), values, users)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *chats) RemoveMember(chatID int, userID int) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("user_id", strconv.Itoa(int(userID)))
body, err := a.client.request(http.MethodDelete, fmt.Sprintf("chats/%d/members", chatID), values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *chats) SendAction(chatID int, action SenderAction) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
body, err := a.client.request(http.MethodPost, fmt.Sprintf("chats/%d/actions", chatID), values, ActionRequestBody{Action: action})
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}

56
client.go Normal file
View file

@ -0,0 +1,56 @@
// Package tamtam implements TamTam Bot API
// Copyright (c) 2019 Alexander Kiryukhin <a.kiryukhin@mail.ru>
package tamtam
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)
type client struct {
key string
version string
url *url.URL
}
func newClient(key string, version string, url *url.URL) *client {
return &client{key: key, version: version, url: url}
}
func (cl *client) request(method, path string, query url.Values, body interface{}) (io.ReadCloser, error) {
j, err := json.Marshal(body)
if err != nil {
return nil, err
}
return cl.requestReader(method, path, query, bytes.NewReader(j))
}
func (cl *client) requestReader(method, path string, query url.Values, body io.Reader) (io.ReadCloser, error) {
c := http.DefaultClient
u := *cl.url
u.Path = path
query.Set("access_token", cl.key)
query.Set("v", cl.version)
u.RawQuery = query.Encode()
req, err := http.NewRequest(method, u.String(), body)
if err != nil {
return nil, err
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
errObj := new(Error)
err = json.NewDecoder(resp.Body).Decode(errObj)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("code=%s message=%s error=%s", errObj.Code, errObj.Message, errObj.Error)
}
return resp.Body, err
}

53
examples/example.go Normal file
View file

@ -0,0 +1,53 @@
// +build ignore
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/neonxp/tamtam"
)
func main() {
api := tamtam.New(os.Getenv("TOKEN"))
info, err := api.Bots.GetBot() // Простой метод
log.Printf("Get me: %#v %#v", info, err)
go api.UpdatesLoop(context.Background()) // Запуск цикла получения обновлений
for upd := range api.GetUpdates() { // Чтение из канала с обновлениями
log.Printf("Received: %#v", upd)
switch upd := upd.(type) { // Определение типа пришедшего обновления
case *tamtam.MessageCreatedUpdate:
// Создание клавиатуры
keyboard := api.Messages.NewKeyboardBuilder()
keyboard.
AddRow().
AddGeolocation("Прислать геолокацию", true).
AddContact("Прислать контакт")
keyboard.
AddRow().
AddLink("Библиотека", tamtam.POSITIVE, "https://github.com/neonxp/tamtam").
AddCallback("Колбек 1", tamtam.NEGATIVE, "callback_1").
AddCallback("Колбек 2", tamtam.NEGATIVE, "callback_2")
// Отправка сообщения с клавиатурой
res, err := api.Messages.SendMessage(0, upd.Message.Sender.UserId, &tamtam.NewMessageBody{
Text: fmt.Sprintf("Hello, %s! Your message: %s", upd.Message.Sender.Name, upd.Message.Body.Text),
Attachments: []interface{}{
tamtam.NewInlineKeyboardAttachmentRequest(keyboard.Build()),
},
})
log.Printf("Answer: %#v %#v", res, err)
case *tamtam.MessageCallbackUpdate:
res, err := api.Messages.SendMessage(0, upd.Callback.User.UserId, &tamtam.NewMessageBody{
Text: "Callback: " + upd.Callback.Payload,
})
log.Printf("Answer: %#v %#v", res, err)
default:
log.Printf("Unknown type: %#v", upd)
}
}
}

View file

@ -8,10 +8,11 @@ package main
import (
"context"
"fmt"
"github.com/neonxp/tamtam"
"log"
"os"
"os/signal"
"github.com/neonxp/tamtam"
)
func main() {
@ -19,28 +20,18 @@ func main() {
api := tamtam.New(os.Getenv("TOKEN"))
// Some methods demo:
info, err := api.GetMe()
info, err := api.Bots.GetBot()
log.Printf("Get me: %#v %#v", info, err)
chats, err := api.GetChats(0, 0)
log.Printf("Get chats: %#v %#v", chats, err)
chat, err := api.GetChat(chats.Chats[0].ChatId)
log.Printf("Get chat: %#v %#v", chat, err)
subs, _ := api.GetSubscriptions()
for _, s := range subs.Subscriptions {
_, _ = api.Unsubscribe(s.Url)
}
ch := make(chan interface{}, 1) // Channel with updates from TamTam
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case upd := <-ch:
case upd := <-api.GetUpdates():
log.Printf("Received: %#v", upd)
switch upd := upd.(type) {
case tamtam.UpdateMessageCreated:
res, err := api.SendMessage(0, upd.Message.Sender.UserId, &tamtam.NewMessageBody{
case *tamtam.MessageCreatedUpdate:
res, err := api.Messages.SendMessage(0, upd.Message.Sender.UserId, &tamtam.NewMessageBody{
Text: fmt.Sprintf("Hello, %s! Your message: %s", upd.Message.Sender.Name, upd.Message.Body.Text),
})
log.Printf("Answer: %#v %#v", res, err)
@ -53,7 +44,6 @@ func main() {
}
}()
go func() {
exit := make(chan os.Signal)
signal.Notify(exit, os.Kill, os.Interrupt)
@ -65,7 +55,7 @@ func main() {
}
}()
if err := api.GetUpdatesLoop(ctx, ch); err != nil {
if err := api.UpdatesLoop(ctx); err != nil {
log.Fatalln(err)
}

View file

@ -1,3 +1,5 @@
// +build ignore
/**
* Webhook example
*/
@ -5,30 +7,27 @@ package main
import (
"fmt"
"github.com/neonxp/tamtam"
"log"
"net/http"
"os"
"github.com/neonxp/tamtam"
)
func main() {
// Initialisation
api := tamtam.New(os.Getenv("TOKEN"))
host := os.Getenv("HOST")
// Some methods demo:
info, err := api.GetMe()
info, err := api.Bots.GetBot()
log.Printf("Get me: %#v %#v", info, err)
chats, err := api.GetChats(0, 0)
log.Printf("Get chats: %#v %#v", chats, err)
chat, err := api.GetChat(chats.Chats[0].ChatId)
log.Printf("Get chat: %#v %#v", chat, err)
msgs, err := api.GetMessages(chats.Chats[0].ChatId, nil, 0, 0, 0)
log.Printf("Get messages: %#v %#v", msgs, err)
subs, _ := api.GetSubscriptions()
subs, _ := api.Subscriptions.GetSubscriptions()
for _, s := range subs.Subscriptions {
_, _ = api.Unsubscribe(s.Url)
_, _ = api.Subscriptions.Unsubscribe(s.Url)
}
subscriptionResp, err := api.Subscribe("https://576df2ec.ngrok.io/webhook", []string{})
subscriptionResp, err := api.Subscriptions.Subscribe(host+"/webhook", []string{})
log.Printf("Subscription: %#v %#v", subscriptionResp, err)
ch := make(chan interface{}) // Channel with updates from TamTam
@ -39,8 +38,8 @@ func main() {
upd := <-ch
log.Printf("Received: %#v", upd)
switch upd := upd.(type) {
case tamtam.UpdateMessageCreated:
res, err := api.SendMessage(0, upd.Message.Sender.UserId, &tamtam.NewMessageBody{
case tamtam.MessageCreatedUpdate:
res, err := api.Messages.SendMessage(0, upd.Message.Sender.UserId, &tamtam.NewMessageBody{
Text: fmt.Sprintf("Hello, %s! Your message: %s", upd.Message.Sender.Name, upd.Message.Body.Text),
})
log.Printf("Answer: %#v %#v", res, err)

View file

@ -1,15 +1,11 @@
// Package tamtam implements TamTam Bot API
// Copyright (c) 2019 Alexander Kiryukhin <a.kiryukhin@mail.ru>
package tamtam
type KeyboardBuilder struct {
rows []*KeyboardRow
}
func NewKeyboardBuilder() *KeyboardBuilder {
return &KeyboardBuilder{
rows: make([]*KeyboardRow, 0),
}
}
func (k *KeyboardBuilder) AddRow() *KeyboardRow {
kr := &KeyboardRow{}
k.rows = append(k.rows, kr)
@ -17,7 +13,7 @@ func (k *KeyboardBuilder) AddRow() *KeyboardRow {
}
func (k *KeyboardBuilder) Build() Keyboard {
buttons := make([][]interface{}, 0, len(k.rows))
buttons := make([][]ButtonInterface, 0, len(k.rows))
for _, r := range k.rows {
buttons = append(buttons, r.Build())
}
@ -25,19 +21,20 @@ func (k *KeyboardBuilder) Build() Keyboard {
}
type KeyboardRow struct {
cols []interface{}
cols []ButtonInterface
}
func (k *KeyboardRow) Build() []interface{} {
func (k *KeyboardRow) Build() []ButtonInterface {
return k.cols
}
func (k *KeyboardRow) AddLink(text string, intent Intent, url string) *KeyboardRow {
b := LinkButton{
Text: text,
Url: url,
Intent: intent,
Button: Button{
Text: text,
Type: LINK,
},
}
k.cols = append(k.cols, b)
return k
@ -45,31 +42,35 @@ func (k *KeyboardRow) AddLink(text string, intent Intent, url string) *KeyboardR
func (k *KeyboardRow) AddCallback(text string, intent Intent, payload string) *KeyboardRow {
b := CallbackButton{
Text: text,
Payload: payload,
Intent: intent,
Button: Button{
Text: text,
Type: CALLBACK,
},
}
k.cols = append(k.cols, b)
return k
}
func (k *KeyboardRow) AddContact(text string, intent Intent, url string) *KeyboardRow {
func (k *KeyboardRow) AddContact(text string) *KeyboardRow {
b := RequestContactButton{
Button: Button{
Text: text,
Intent: intent,
Type: CONTACT,
},
}
k.cols = append(k.cols, b)
return k
}
func (k *KeyboardRow) AddGeolocation(text string, intent Intent, quick bool) *KeyboardRow {
func (k *KeyboardRow) AddGeolocation(text string, quick bool) *KeyboardRow {
b := RequestGeoLocationButton{
Text: text,
Quick: quick,
Intent: intent,
Button: Button{
Text: text,
Type: GEOLOCATION,
},
}
k.cols = append(k.cols, b)
return k

126
messages.go Normal file
View file

@ -0,0 +1,126 @@
// Package tamtam implements TamTam Bot API
// Copyright (c) 2019 Alexander Kiryukhin <a.kiryukhin@mail.ru>
package tamtam
import (
"encoding/json"
"log"
"net/http"
"net/url"
"strconv"
)
type messages struct {
client *client
}
func newMessages(client *client) *messages {
return &messages{client: client}
}
func (a *messages) GetMessages(chatID int, messageIDs []string, from int, to int, count int) (*MessageList, error) {
result := new(MessageList)
values := url.Values{}
if chatID != 0 {
values.Set("chat_id", strconv.Itoa(int(chatID)))
}
if len(messageIDs) > 0 {
for _, mid := range messageIDs {
values.Add("message_ids", mid)
}
}
if from != 0 {
values.Set("from", strconv.Itoa(int(from)))
}
if to != 0 {
values.Set("to", strconv.Itoa(int(to)))
}
if count > 0 {
values.Set("count", strconv.Itoa(int(count)))
}
body, err := a.client.request(http.MethodGet, "messages", values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *messages) SendMessage(chatID int, userID int, message *NewMessageBody) (*Message, error) {
result := new(Message)
values := url.Values{}
if chatID != 0 {
values.Set("chat_id", strconv.Itoa(int(chatID)))
}
if userID != 0 {
values.Set("user_id", strconv.Itoa(int(userID)))
}
body, err := a.client.request(http.MethodPost, "messages", values, message)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *messages) EditMessage(messageID int, message *NewMessageBody) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("message_id", strconv.Itoa(int(messageID)))
body, err := a.client.request(http.MethodPut, "messages", values, message)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *messages) DeleteMessage(messageID int) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("message_id", strconv.Itoa(int(messageID)))
body, err := a.client.request(http.MethodDelete, "messages", values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *messages) AnswerOnCallback(callbackID int, callback *CallbackAnswer) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("callback_id", strconv.Itoa(int(callbackID)))
body, err := a.client.request(http.MethodPost, "answers", values, callback)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *messages) NewKeyboardBuilder() *KeyboardBuilder {
return &KeyboardBuilder{
rows: make([]*KeyboardRow, 0),
}
}

813
models.go

File diff suppressed because it is too large Load diff

68
subscriptions.go Normal file
View file

@ -0,0 +1,68 @@
// Package tamtam implements TamTam Bot API
// Copyright (c) 2019 Alexander Kiryukhin <a.kiryukhin@mail.ru>
package tamtam
import (
"encoding/json"
"log"
"net/http"
"net/url"
)
type subscriptions struct {
client *client
}
func newSubscriptions(client *client) *subscriptions {
return &subscriptions{client: client}
}
func (a *subscriptions) GetSubscriptions() (*GetSubscriptionsResult, error) {
result := new(GetSubscriptionsResult)
values := url.Values{}
body, err := a.client.request(http.MethodGet, "subscriptions", values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *subscriptions) Subscribe(subscribeURL string, updateTypes []string) (*SimpleQueryResult, error) {
subscription := &SubscriptionRequestBody{
Url: subscribeURL,
UpdateTypes: updateTypes,
Version: a.client.version,
}
result := new(SimpleQueryResult)
values := url.Values{}
body, err := a.client.request(http.MethodPost, "subscriptions", values, subscription)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *subscriptions) Unsubscribe(subscriptionURL string) (*SimpleQueryResult, error) {
result := new(SimpleQueryResult)
values := url.Values{}
values.Set("url", subscriptionURL)
body, err := a.client.request(http.MethodDelete, "subscriptions", values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}

84
uploads.go Normal file
View file

@ -0,0 +1,84 @@
// Package tamtam implements TamTam Bot API
// Copyright (c) 2019 Alexander Kiryukhin <a.kiryukhin@mail.ru>
package tamtam
import (
"bytes"
"encoding/json"
"io"
"log"
"mime/multipart"
"net/http"
"net/url"
"os"
)
type uploads struct {
client *client
}
func newUploads(client *client) *uploads {
return &uploads{client: client}
}
func (a *uploads) GetUploadURL(uploadType UploadType) (*UploadEndpoint, error) {
result := new(UploadEndpoint)
values := url.Values{}
values.Set("type", string(uploadType))
body, err := a.client.request(http.MethodPost, "uploads", values, nil)
if err != nil {
return result, err
}
defer func() {
if err := body.Close(); err != nil {
log.Println(err)
}
}()
return result, json.NewDecoder(body).Decode(result)
}
func (a *uploads) UploadMedia(uploadType UploadType, filename string) (*UploadedInfo, error) {
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
fileWriter, err := bodyWriter.CreateFormFile("data", filename)
if err != nil {
return nil, err
}
fh, err := os.Open(filename)
if err != nil {
return nil, err
}
defer func() {
if err := fh.Close(); err != nil {
log.Println(err)
}
}()
_, err = io.Copy(fileWriter, fh)
if err != nil {
return nil, err
}
if err := bodyWriter.Close(); err != nil {
return nil, err
}
target, err := a.GetUploadURL(uploadType)
if err != nil {
return nil, err
}
contentType := bodyWriter.FormDataContentType()
resp, err := http.Post(target.Url, contentType, bodyBuf)
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Println(err)
}
}()
result := new(UploadedInfo)
return result, json.NewDecoder(resp.Body).Decode(result)
}