New BotApi

This commit is contained in:
Alexander Kiryukhin 2019-08-13 15:26:05 +03:00
parent f9744b81d9
commit 9efc5ffd51
19 changed files with 962 additions and 513 deletions

109
README.md
View file

@ -1,85 +1,72 @@
# ICQ Bot API
# ICQ Bot Api Go
## Installation
[![Sourcegraph](https://sourcegraph.com/github.com/go-icq/icq/-/badge.svg?style=flat-square)](https://sourcegraph.com/github.com/go-icq/icq?badge)
[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/go-icq/icq)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-icq/icq?style=flat-square)](https://goreportcard.com/report/github.com/go-icq/icq)
[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/go-icq/icq/master/LICENSE)
Go get: `go get gopkg.in/icq.v2`
Основана на новом Bot Api (https://icq.com/botapi/)
Go mod / Go dep: `import "gopkg.in/icq.v2"`
Реализованы все методы и соответствуют документации.
## Working
Methods:
* SendMessage
* UploadFile
* FetchEvents
Webhooks workds but not recommends
## Example
## Пример
```go
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"time"
"gopkg.in/icq.v2"
"github.com/go-icq/icq"
)
func main() {
// New API object
b := icq.NewAPI(os.Getenv("ICQ_TOKEN"))
// Инициализация
b := icq.NewApi(os.Getenv("ICQ_TOKEN"), icq.ICQ) // or icq.Agent
ctx, cancel := context.WithCancel(context.Background())
// Получение информации о боте
log.Println(b.Self.Get())
ch := make(chan interface{}) // Events channel
osSignal := make(chan os.Signal, 1)
signal.Notify(osSignal, os.Interrupt)
signal.Notify(osSignal, os.Kill)
// Отправка сообщения
resultSend, err := b.Messages.SendText("429950", "Привет!", nil, "", "")
if err != nil {
log.Fatal(err)
}
go b.FetchEvents(ctx, ch) // Events fetch loop
// Отправка файла
resultFile, err := b.Messages.SendFile("429950", "./example/example.jpg", "коржик", []string{resultSend.MsgID}, "", "")
if err != nil {
log.Fatal(err)
}
for {
select {
case e := <-ch:
handleEvent(b, e)
case <-osSignal:
cancel()
break
// Отправка существующего файла по ID
_, err = b.Messages.SendExistsFile("429950", resultFile.FileID, "Существующий файл", nil, "", "")
if err != nil {
log.Fatal(err)
}
// Редактирование сообщения
_, err = b.Messages.EditText("429950", "Новый текст", resultSend.MsgID)
if err != nil {
log.Fatal(err)
}
// Будем слушать эвенты 5 минут. При закрытии контекста перестает работать цикл получения событий. В реальном мире контекст надо будет закрывать по сигналу ОС
ctx, _ := context.WithTimeout(context.Background(), 5*time.Minute)
for ev := range b.Events.Get(ctx) {
switch ev := ev.(type) {
case *icq.EventDataMessage:
b.Messages.SendText(ev.Payload.Chat.ChatID, "Echo: "+ev.Payload.Text, []string{ev.Payload.MsgID}, "", "")
default:
log.Println(ev)
}
}
}
func handleEvent(b *icq.API, event interface{}) {
switch event.(type) {
case *icq.IMEvent:
message := event.(*icq.IMEvent)
if err := handleMessage(b, message); err != nil {
b.SendMessage(icq.Message{
To: message.Data.Source.AimID,
Text: "Message process fail",
})
}
default:
log.Printf("%#v", event)
}
}
func handleMessage(b *icq.API, message *icq.IMEvent) error {
cmd, ok := icq.ParseCommand(message)
if !ok {
return nil
}
_, err := b.SendMessage(icq.Message{
To: cmd.From,
Text: fmt.Sprintf("Command: %s, Arguments: %v", cmd.Command, cmd.Arguments),
})
return err
}
```
## Автор
Александр NeonXP Кирюхин <a.kiryukhin@mail.ru>

52
api.go
View file

@ -1,52 +0,0 @@
package icq
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
// HTTP Client interface
type Doer interface {
Do(req *http.Request) (*http.Response, error)
}
// API
type API struct {
token string
baseUrl string
client Doer
fetchBase string
}
// NewAPI constructor of API object
func NewAPI(token string) *API {
return &API{
token: token,
baseUrl: "https://botapi.icq.net",
client: http.DefaultClient,
}
}
func (a *API) send(path string, v url.Values) ([]byte, error) {
req, err := http.NewRequest(http.MethodPost, a.baseUrl+path, strings.NewReader(v.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := a.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return b, fmt.Errorf("ICQ API error. Code=%d Message=%s", resp.StatusCode, string(b))
}
return b, nil
}

68
chats.go Normal file
View file

@ -0,0 +1,68 @@
package icq
import (
"encoding/json"
"net/http"
"net/url"
)
type chats struct {
client *client
}
func newChats(client *client) *chats {
return &chats{client: client}
}
func (s *chats) SendActions(chatID string, actions []ChatAction) (bool, error) {
acts := []string{}
for _, act := range actions {
acts = append(acts, string(act))
}
resp, err := s.client.request(
http.MethodGet,
"/chats/sendActions",
url.Values{
"chatId": []string{chatID},
"actions": acts,
},
nil,
)
if err != nil {
return false, err
}
result := new(OK)
return result.OK, json.NewDecoder(resp).Decode(result)
}
func (s *chats) GetInfo(chatID string) (*Chat, error) {
resp, err := s.client.request(
http.MethodGet,
"/chats/getInfo",
url.Values{
"chatId": []string{chatID},
},
nil,
)
if err != nil {
return nil, err
}
result := new(Chat)
return result, json.NewDecoder(resp).Decode(result)
}
func (s *chats) GetAdmins(chatID string) (*Admins, error) {
resp, err := s.client.request(
http.MethodGet,
"/chats/getAdmins",
url.Values{
"chatId": []string{chatID},
},
nil,
)
if err != nil {
return nil, err
}
result := new(Admins)
return result, json.NewDecoder(resp).Decode(result)
}

75
client.go Normal file
View file

@ -0,0 +1,75 @@
package icq
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"time"
)
type ApiType int
const (
ICQ ApiType = iota
Agent
)
var servers = map[ApiType]string{
ICQ: "https://api.icq.net/bot/v1/",
Agent: "https://agent.mail.ru/bot/v1/",
}
type client struct {
token string
apiType ApiType
client http.Client
}
func newClient(token string, apiType ApiType) *client {
return &client{token: token, apiType: apiType, client: http.Client{Timeout: 30 * time.Second}}
}
func (c *client) request(method string, methodPath string, query url.Values, body *bytes.Buffer) (io.Reader, error) {
return c.requestWithContentType(method, methodPath, query, body, "")
}
func (c *client) requestWithContentType(method string, methodPath string, query url.Values, body *bytes.Buffer, contentType string) (io.Reader, error) {
query.Set("token", c.token)
u, err := url.Parse(servers[c.apiType])
if err != nil {
return nil, err
}
u.Path = path.Join(u.Path, methodPath)
u.RawQuery = query.Encode()
req, err := http.NewRequest(method, u.String(), nil)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if body != nil {
rc := ioutil.NopCloser(body)
req.Body = rc
}
if err != nil {
return nil, err
}
resp, err := c.client.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("ok=%v message=%s", errObj.OK, errObj.Description)
}
return resp.Body, err
}

164
events.go
View file

@ -1,60 +1,118 @@
package icq
type CommonEvent struct {
Type string `json:"type"`
SeqNum int `json:"seqNum"`
}
type ServiceEvent struct {
CommonEvent
Data interface{} `json:"eventData"`
}
type BuddyListEvent struct {
CommonEvent
Data struct {
Groups []struct {
Name string `json:"name"`
ID int `json:"id"`
Buddies []Buddy `json:"buddies"`
} `json:"groups"`
} `json:"eventData"`
}
type MyInfoEvent struct {
CommonEvent
Data Buddy `json:"eventData"`
}
type TypingStatus string
const (
StartTyping TypingStatus = "typing"
StopTyping = "none"
import (
"context"
"encoding/json"
"log"
"net/http"
"net/url"
"strconv"
"time"
)
type TypingEvent struct {
CommonEvent
Data struct {
AimID string `json:"aimId"`
TypingStatus TypingStatus `json:"typingStatus"`
} `json:"eventData"`
type events struct {
client *client
}
type IMEvent struct {
CommonEvent
Data struct {
Autoresponse int `json:"autoresponse"`
Timestamp int `json:"timestamp"`
Notification string `json:"notification"`
MsgID string `json:"msgId"`
IMF string `json:"imf"`
Message string `json:"message"`
RawMessage struct {
IPCountry string `json:"ipCountry"`
ClientCountry string `json:"clientCountry"`
Base64Msg string `json:"base64Msg"`
} `json:"rawMsg"`
Source Buddy `json:"source"`
} `json:"eventData"`
func newEvents(client *client) *events {
return &events{client: client}
}
func (e *events) Get(ctx context.Context) <-chan EventInterface {
ch := make(chan EventInterface)
go func() {
lastEvent := 0
for {
if ctx.Err() != nil {
close(ch)
return
}
events, err := e.getEvents(lastEvent)
if err != nil {
log.Println(err)
<-time.After(5 * time.Second) // Retry after 5 seconds
continue
}
for _, e := range events.Events {
ch <- e
lastEvent = e.GetEventID()
}
}
}()
return ch
}
func (e *events) getEvents(lastEvent int) (*Events, error) {
resp, err := e.client.request(
http.MethodGet,
"/events/get",
url.Values{
"lastEventId": []string{strconv.Itoa(lastEvent)},
"pollTime": []string{"30"},
},
nil)
if err != nil {
return nil, err
}
tempResult := new(RawEvents)
if err := json.NewDecoder(resp).Decode(tempResult); err != nil {
return nil, err
}
result := new(Events)
for _, e := range tempResult.Events {
tempEvent := new(Event)
if err := json.Unmarshal(e, tempEvent); err != nil {
return nil, err
}
var ev EventInterface
switch tempEvent.GetType() {
case EventTypeDataMessage:
ev = new(EventDataMessage)
case EventTypeEditedMessage:
ev = new(EventEditedMessage)
case EventTypeDeletedMessage:
ev = new(EventDeletedMessage)
case EventTypePinnedMessage:
ev = new(EventPinnedMessage)
case EventTypeUnpinnedMessage:
ev = new(EventUnpinnedMessage)
case EventTypeNewChatMembers:
ev = new(EventNewChatMembers)
case EventTypeLeftChatMembers:
ev = new(EventLeftChatMembers)
}
if err := json.Unmarshal(e, ev); err != nil {
return nil, err
}
switch ev := ev.(type) {
case *EventDataMessage:
for _, ea := range ev.Payload.RawParts {
tempAttachment := new(Attachment)
if err := json.Unmarshal(ea, tempAttachment); err != nil {
return nil, err
}
var eav AttachmentInterface
switch tempAttachment.Type {
case AttachmentTypeSticker:
eav = new(AttachmentSticker)
case AttachmentTypeMention:
eav = new(AttachmentMention)
case AttachmentTypeVoice:
eav = new(AttachmentVoice)
case AttachmentTypeFile:
eav = new(AttachmentFile)
case AttachmentTypeForward:
eav = new(AttachmentForward)
case AttachmentTypeReply:
eav = new(AttachmentReply)
}
if err := json.Unmarshal(ea, eav); err != nil {
return nil, err
}
ev.Payload.Parts = append(ev.Payload.Parts, eav)
}
}
result.Events = append(result.Events, ev)
}
return result, nil
}

View file

@ -2,60 +2,52 @@ package main
import (
"context"
"fmt"
"github.com/go-icq/icq"
"log"
"os"
"os/signal"
"time"
"github.com/go-icq/icq"
)
func main() {
// New API object
b := icq.NewAPI(os.Getenv("ICQ_TOKEN"))
// Инициализация
b := icq.NewApi(os.Getenv("ICQ_TOKEN"), icq.ICQ) // or icq.Agent
ctx, cancel := context.WithCancel(context.Background())
// Получение информации о боте
log.Println(b.Self.Get())
ch := make(chan interface{}) // Events channel
osSignal := make(chan os.Signal, 1)
signal.Notify(osSignal, os.Interrupt)
signal.Notify(osSignal, os.Kill)
// Отправка сообщения
resultSend, err := b.Messages.SendText("429950", "Привет!", nil, "", "")
if err != nil {
log.Fatal(err)
}
go b.FetchEvents(ctx, ch) // Events fetch loop
// Отправка файла
resultFile, err := b.Messages.SendFile("429950", "./example/example.jpg", "коржик", []string{resultSend.MsgID}, "", "")
if err != nil {
log.Fatal(err)
}
for {
select {
case e := <-ch:
handleEvent(b, e)
case <-osSignal:
cancel()
break
// Отправка существующего файла по ID
_, err = b.Messages.SendExistsFile("429950", resultFile.FileID, "Существующий файл", nil, "", "")
if err != nil {
log.Fatal(err)
}
// Редактирование сообщения
_, err = b.Messages.EditText("429950", "Новый текст", resultSend.MsgID)
if err != nil {
log.Fatal(err)
}
// Будем слушать эвенты 5 минут. При закрытии контекста перестает работать цикл получения событий. В реальном мире контекст надо будет закрывать по сигналу ОС
ctx, _ := context.WithTimeout(context.Background(), 5*time.Minute)
for ev := range b.Events.Get(ctx) {
switch ev := ev.(type) {
case *icq.EventDataMessage:
b.Messages.SendText(ev.Payload.Chat.ChatID, "Echo: "+ev.Payload.Text, []string{ev.Payload.MsgID}, "", "")
default:
log.Println(ev)
}
}
}
func handleEvent(b *icq.API, event interface{}) {
switch event.(type) {
case *icq.IMEvent:
message := event.(*icq.IMEvent)
if err := handleMessage(b, message); err != nil {
b.SendMessage(icq.Message{
To: message.Data.Source.AimID,
Text: "Message process fail",
})
}
default:
log.Printf("%#v", event)
}
}
func handleMessage(b *icq.API, message *icq.IMEvent) error {
cmd, ok := icq.ParseCommand(message)
if !ok {
return nil
}
_, err := b.SendMessage(icq.Message{
To: cmd.From,
Text: fmt.Sprintf("Command: %s, Arguments: %v", cmd.Command, cmd.Arguments),
})
return err
}

BIN
example/example.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View file

@ -1,88 +0,0 @@
package icq
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"time"
)
func (a *API) FetchEvents(ctx context.Context, ch chan interface{}) error {
fetchResp := &struct {
Response struct {
Data struct {
FetchBase string `json:"fetchBaseURL"`
PollTime int `json:"pollTime"`
Events []json.RawMessage `json:"events"`
} `json:"data"`
} `json:"response"`
}{}
for {
b := []byte{}
u := a.fetchBase
if u == "" {
v := url.Values{}
v.Set("aimsid", a.token)
v.Set("first", "1")
u = a.baseUrl + "/fetchEvents?" + v.Encode()
}
req, err := http.Get(u)
if err != nil {
return err
}
b, err = ioutil.ReadAll(req.Body)
req.Body.Close()
if err := json.Unmarshal(b, fetchResp); err != nil {
return err
}
a.fetchBase = fetchResp.Response.Data.FetchBase
for _, e := range fetchResp.Response.Data.Events {
ce := &CommonEvent{}
if err := json.Unmarshal(e, ce); err != nil {
return err
}
switch ce.Type {
case "service":
ev := &ServiceEvent{}
if err := json.Unmarshal(e, ev); err != nil {
return err
}
ch <- ev
case "buddylist":
ev := &BuddyListEvent{}
if err := json.Unmarshal(e, ev); err != nil {
return err
}
ch <- ev
case "myInfo":
ev := &MyInfoEvent{}
if err := json.Unmarshal(e, ev); err != nil {
return err
}
ch <- ev
case "typing":
ev := &TypingEvent{}
if err := json.Unmarshal(e, ev); err != nil {
return err
}
ch <- ev
case "im":
ev := &IMEvent{}
if err := json.Unmarshal(e, ev); err != nil {
return err
}
ch <- ev
default:
ch <- ce
}
}
select {
case <-time.After(time.Duration(fetchResp.Response.Data.PollTime)):
case <-ctx.Done():
return nil
}
}
return nil
}

31
files.go Normal file
View file

@ -0,0 +1,31 @@
package icq
import (
"encoding/json"
"net/http"
"net/url"
)
type files struct {
client *client
}
func newFiles(client *client) *files {
return &files{client: client}
}
func (f *files) GetInfo(fileID string) (*FileInfo, error) {
resp, err := f.client.request(
http.MethodGet,
"/chats/getInfo",
url.Values{
"fileId": []string{fileID},
},
nil,
)
if err != nil {
return nil, err
}
result := new(FileInfo)
return result, json.NewDecoder(resp).Decode(result)
}

2
go.mod
View file

@ -1 +1,3 @@
module github.com/go-icq/icq
go 1.12

20
icq.go Normal file
View file

@ -0,0 +1,20 @@
package icq
type Api struct {
Self *self
Chats *chats
Files *files
Messages *messages
Events *events
}
func NewApi(token string, apiType ApiType) *Api {
client := newClient(token, apiType)
return &Api{
Self: newSelf(client),
Chats: newChats(client),
Files: newFiles(client),
Messages: newMessages(client),
Events: newEvents(client),
}
}

264
messages.go Normal file
View file

@ -0,0 +1,264 @@
package icq
import (
"bytes"
"encoding/json"
"io"
"log"
"mime/multipart"
"net/http"
"net/url"
"os"
)
type messages struct {
client *client
}
func newMessages(client *client) *messages {
return &messages{client: client}
}
func (f *messages) SendText(chatID string, text string, replyMsgID []string, forwardChatID string, forwardMsgID string) (*Msg, error) {
params := url.Values{
"chatId": []string{chatID},
"text": []string{text},
}
if replyMsgID != nil && len(replyMsgID) > 0 {
for _, msgID := range replyMsgID {
params.Add("replyMsgId", msgID)
}
}
if forwardChatID != "" {
params.Set("forwardChatId", forwardChatID)
}
if forwardMsgID != "" {
params.Set("forwardMsgId", forwardMsgID)
}
resp, err := f.client.request(
http.MethodGet,
"/messages/sendText",
params,
nil,
)
if err != nil {
return nil, err
}
result := new(Msg)
return result, json.NewDecoder(resp).Decode(result)
}
func (f *messages) SendExistsFile(chatID string, fileID string, caption string, replyMsgID []string, forwardChatID string, forwardMsgID string) (*Msg, error) {
params := url.Values{
"chatId": []string{chatID},
"fileId": []string{fileID},
"caption": []string{caption},
}
if replyMsgID != nil && len(replyMsgID) > 0 {
for _, msgID := range replyMsgID {
params.Add("replyMsgId", msgID)
}
}
if forwardChatID != "" {
params.Set("forwardChatId", forwardChatID)
}
if forwardMsgID != "" {
params.Set("forwardMsgId", forwardMsgID)
}
resp, err := f.client.request(
http.MethodGet,
"/messages/sendFile",
params,
nil,
)
if err != nil {
return nil, err
}
result := new(Msg)
return result, json.NewDecoder(resp).Decode(result)
}
func (f *messages) SendFile(chatID string, fileName string, caption string, replyMsgID []string, forwardChatID string, forwardMsgID string) (*MsgLoadFile, error) {
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
fileWriter, err := bodyWriter.CreateFormFile("file", 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
}
contentType := bodyWriter.FormDataContentType()
params := url.Values{
"chatId": []string{chatID},
"caption": []string{caption},
}
if replyMsgID != nil && len(replyMsgID) > 0 {
for _, msgID := range replyMsgID {
params.Add("replyMsgId", msgID)
}
}
if forwardChatID != "" {
params.Set("forwardChatId", forwardChatID)
}
if forwardMsgID != "" {
params.Set("forwardMsgId", forwardMsgID)
}
resp, err := f.client.requestWithContentType(
http.MethodPost,
"/messages/sendFile",
params,
bodyBuf,
contentType,
)
if err != nil {
return nil, err
}
result := new(MsgLoadFile)
return result, json.NewDecoder(resp).Decode(result)
}
func (f *messages) SendExistsVoice(chatID string, fileID string, replyMsgID []string, forwardChatID string, forwardMsgID string) (*Msg, error) {
params := url.Values{
"chatId": []string{chatID},
"fileId": []string{fileID},
}
if replyMsgID != nil && len(replyMsgID) > 0 {
for _, msgID := range replyMsgID {
params.Add("replyMsgId", msgID)
}
}
if forwardChatID != "" {
params.Set("forwardChatId", forwardChatID)
}
if forwardMsgID != "" {
params.Set("forwardMsgId", forwardMsgID)
}
resp, err := f.client.request(
http.MethodGet,
"/messages/sendVoice",
params,
nil,
)
if err != nil {
return nil, err
}
result := new(Msg)
return result, json.NewDecoder(resp).Decode(result)
}
func (f *messages) SendVoice(chatID string, fileName string, replyMsgID []string, forwardChatID string, forwardMsgID string) (*MsgLoadFile, error) {
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
fileWriter, err := bodyWriter.CreateFormFile("file", 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
}
contentType := bodyWriter.FormDataContentType()
params := url.Values{
"chatId": []string{chatID},
}
if replyMsgID != nil && len(replyMsgID) > 0 {
for _, msgID := range replyMsgID {
params.Add("replyMsgId", msgID)
}
}
if forwardChatID != "" {
params.Set("forwardChatId", forwardChatID)
}
if forwardMsgID != "" {
params.Set("forwardMsgId", forwardMsgID)
}
resp, err := f.client.requestWithContentType(
http.MethodPost,
"/messages/sendVoice",
params,
bodyBuf,
contentType,
)
if err != nil {
return nil, err
}
result := new(MsgLoadFile)
return result, json.NewDecoder(resp).Decode(result)
}
func (f *messages) EditText(chatID string, text string, msgID string) (bool, error) {
params := url.Values{
"msgId": []string{msgID},
"chatId": []string{chatID},
"text": []string{text},
}
resp, err := f.client.request(
http.MethodGet,
"/messages/editText",
params,
nil,
)
if err != nil {
return false, err
}
result := new(OK)
return result.OK, json.NewDecoder(resp).Decode(result)
}
func (f *messages) DeleteMessages(chatID string, msgIDs []string) (bool, error) {
params := url.Values{
"chatId": []string{chatID},
}
if msgIDs != nil && len(msgIDs) > 0 {
for _, msgID := range msgIDs {
params.Add("msgId", msgID)
}
}
resp, err := f.client.request(
http.MethodGet,
"/messages/deleteMessages",
params,
nil,
)
if err != nil {
return false, err
}
result := new(OK)
return result.OK, json.NewDecoder(resp).Decode(result)
}

282
schemas.go Normal file
View file

@ -0,0 +1,282 @@
package icq
import "encoding/json"
type Bot struct {
UserID string `json:"userId"` // уникальный идентификатор
Nick string `json:"nick"` // уникальный ник
FirstName string `json:"firstName"` // имя
About string `json:"about"` // описание бота
Photo []struct {
URL string `json:"url"` // url
} `json:"photo"` // аватар бота
OK bool `json:"ok"` // статус запроса
}
type Chat struct {
InviteLink string `json:"inviteLink"`
Public bool `json:"public"`
Title string `json:"title"`
Group string `json:"group"`
OK bool `json:"ok"` // статус запроса
}
type Admin struct {
UserID string `json:"user_id"`
Creator bool `json:"creator"`
}
type Admins struct {
Admins []Admin `json:"admins"`
}
type FileInfo struct {
Type string `json:"type"`
Size int `json:"size"`
Filename string `json:"filename"`
URL string `json:"url"`
}
type Msg struct {
MsgID string `json:"msgId"`
OK bool `json:"ok"` // статус запроса
}
type MsgLoadFile struct {
FileID string `json:"fileId"`
MsgID string `json:"msgId"`
OK bool `json:"ok"` // статус запроса
}
type User struct {
UserID string `json:"userId"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
}
type File struct {
FileID string `json:"fileId"`
}
type EventType string
const (
EventTypeDataMessage EventType = "newMessage"
EventTypeEditedMessage EventType = "editedMessage"
EventTypeDeletedMessage EventType = "deletedMessage"
EventTypePinnedMessage EventType = "pinnedMessage"
EventTypeUnpinnedMessage EventType = "unpinnedMessage"
EventTypeNewChatMembers EventType = "newChatMembers"
EventTypeLeftChatMembers EventType = "leftChatMembers"
)
type EventInterface interface {
GetEventID() int
GetType() EventType
}
type Events struct {
Events []EventInterface `json:"events"`
}
type RawEvents struct {
Events []json.RawMessage `json:"events"`
}
type Event struct {
EventID int `json:"eventId"`
Type EventType `json:"type"`
}
func (e Event) GetEventID() int {
return e.EventID
}
func (e Event) GetType() EventType {
return e.Type
}
type EventDataMessage struct {
Event
Payload struct {
MsgID string `json:"msgId"`
Chat struct {
ChatID string `json:"chatId"`
Type string `json:"type"`
Title string `json:"title"`
} `json:"chat"`
From User `json:"from"`
Timestamp int `json:"timestamp"`
Text string `json:"text"`
Parts []AttachmentInterface
RawParts []json.RawMessage `json:"parts"`
} `json:"payload"`
}
type EventEditedMessage struct {
Event
Payload struct {
MsgID string `json:"msgId"`
Chat struct {
ChatID string `json:"chatId"`
Type string `json:"type"`
Title string `json:"title"`
} `json:"chat"`
From User `json:"from"`
Timestamp int `json:"timestamp"`
Text string `json:"text"`
EditedTimestamp string `json:"editedTimestamp"`
} `json:"payload"`
}
type EventDeletedMessage struct {
Event
Payload struct {
MsgID string `json:"msgId"`
Chat struct {
ChatID string `json:"chatId"`
Type string `json:"type"`
Title string `json:"title"`
} `json:"chat"`
Timestamp int `json:"timestamp"`
} `json:"payload"`
}
type EventPinnedMessage struct {
Event
Payload struct {
MsgID string `json:"msgId"`
Chat struct {
ChatID string `json:"chatId"`
Type string `json:"type"`
Title string `json:"title"`
} `json:"chat"`
From User `json:"from"`
Timestamp int `json:"timestamp"`
Text string `json:"text"`
} `json:"payload"`
}
type EventUnpinnedMessage struct {
Event
Payload struct {
MsgID string `json:"msgId"`
Chat struct {
ChatID string `json:"chatId"`
Type string `json:"type"`
Title string `json:"title"`
} `json:"chat"`
Timestamp int `json:"timestamp"`
} `json:"payload"`
}
type EventNewChatMembers struct {
Event
Payload struct {
MsgID string `json:"msgId"`
Chat struct {
ChatID string `json:"chatId"`
NewMembers []User `json:"newMembers"`
AddedBy User `json:"addedBy"`
} `json:"chat"`
Timestamp int `json:"timestamp"`
} `json:"payload"`
}
type EventLeftChatMembers struct {
Event
Payload struct {
MsgID string `json:"msgId"`
Chat struct {
ChatID string `json:"chatId"`
LeftMembers []User `json:"leftMembers"`
RemovedBy User `json:"removedBy"`
} `json:"chat"`
Timestamp int `json:"timestamp"`
} `json:"payload"`
}
type AttachmentType string
const (
AttachmentTypeSticker AttachmentType = "sticker"
AttachmentTypeMention AttachmentType = "mention"
AttachmentTypeVoice AttachmentType = "voice"
AttachmentTypeFile AttachmentType = "file"
AttachmentTypeForward AttachmentType = "forward"
AttachmentTypeReply AttachmentType = "reply"
)
type AttachmentInterface interface {
GetType() AttachmentType
}
type Attachment struct {
Type AttachmentType `json:"type"`
}
func (a Attachment) GetType() AttachmentType {
return a.Type
}
type AttachmentSticker struct {
Attachment
Payload File `json:"payload"`
}
type AttachmentMention struct {
Attachment
Payload User `json:"payload"`
}
type AttachmentVoice struct {
Attachment
Payload File `json:"payload"`
}
type AttachmentFile struct {
Attachment
Payload struct {
FileID string `json:"fileId"`
Type AttachmentFileType `json:"type"`
Caption string `json:"caption"`
} `json:"payload"`
}
type AttachmentFileType string
const (
AttachmentFileTypeImage AttachmentFileType = "image"
AttachmentFileTypeAudio AttachmentFileType = "audio"
AttachmentFileTypeVideo AttachmentFileType = "video"
)
type AttachmentForward struct {
Attachment
Payload struct {
Message string `json:"message"`
} `json:"payload"`
}
type AttachmentReply struct {
Attachment
Payload struct {
Message string `json:"message"`
} `json:"payload"`
}
type Error struct {
OK bool `json:"ok"`
Description string `json:"description"`
}
type OK struct {
OK bool `json:"ok"`
}
type ChatAction string
const (
ChatActionLooking ChatAction = "looking"
ChatActionTyping ChatAction = "typing"
)

24
self.go Normal file
View file

@ -0,0 +1,24 @@
package icq
import (
"encoding/json"
"net/http"
"net/url"
)
type self struct {
client *client
}
func newSelf(client *client) *self {
return &self{client: client}
}
func (s *self) Get() (*Bot, error) {
resp, err := s.client.request(http.MethodGet, "/self/get", url.Values{}, nil)
if err != nil {
return nil, err
}
result := new(Bot)
return result, json.NewDecoder(resp).Decode(result)
}

View file

@ -1,36 +0,0 @@
package icq
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
// SendMessage with `message` text to `to` participant
func (a *API) SendMessage(message Message) (*MessageResponse, error) {
parse, _ := json.Marshal(message.Parse)
v := url.Values{}
v.Set("aimsid", a.token)
v.Set("r", strconv.FormatInt(time.Now().Unix(), 10))
v.Set("t", message.To)
v.Set("message", message.Text)
v.Set("mentions", strings.Join(message.Mentions, ","))
if len(message.Parse) > 0 {
v.Set("parse", string(parse))
}
b, err := a.send("/im/sendIM", v)
if err != nil {
return nil, err
}
r := &Response{}
if err := json.Unmarshal(b, r); err != nil {
return nil, err
}
if r.Response.StatusCode != 200 {
return nil, fmt.Errorf("failed to send message: %s", r.Response.StatusText)
}
return r.Response.Data, nil
}

View file

@ -1,81 +0,0 @@
package icq
type Response struct {
Response struct {
StatusCode int `json:"statusCode"`
StatusText string `json:"statusText"`
RequestId string `json:"requestId"`
Data *MessageResponse `json:"data"`
} `json:"response"`
}
type ParseType string
const (
ParseURL ParseType = "url"
ParseFilesharing = "filesharing"
)
type Message struct {
To string
Text string
Mentions []string
Parse []ParseType
}
type MessageResponse struct {
SubCode struct {
Error int `json:"error"`
} `json:"subCode"`
MessageID string `json:"msgId"`
HistoryMessageID int64 `json:"histMsgId"`
State string `json:"state"`
}
type FileResponse struct {
StaticUrl string `json:"static_url"`
MimeType string `json:"mime"`
SnapID string `json:"snapId"`
TtlID string `json:"ttl_id"`
IsPreviewable int `json:"is_previewable"`
FileID string `json:"fileid"`
FileSize int `json:"filesize"`
FileName string `json:"filename"`
ContentID string `json:"content_id"`
}
type WebhookRequest struct {
Token string `json:"aimsid"`
Updates []Update `json:"update"`
}
type Update struct {
Update struct {
Chat Chat `json:"chat"`
Date int `json:"date"`
From User `json:"from"`
Text string `json:"text"`
} `json:"update"`
UpdateID int `json:"update_id"`
}
type Chat struct {
ID string `json:"id"`
}
type User struct {
ID string `json:"id"`
LanguageCode string `json:"language_code"`
}
type Buddy struct {
AimID string `json:"aimId"`
DisplayID string `json:"displayId"`
FriendlyName string `json:"friendly"`
State string `json:"state"`
UserType string `json:"userType"`
UserAgreement []string `json:"userAgreement"`
Nick string `json:"nick"`
GlobalFlags int `json:"globalFlags"`
BuddyIcon string `json:"buddyIcon"`
}

View file

@ -1,33 +0,0 @@
package icq
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
)
// UploadFile to ICQ servers and returns URL to file
func (a *API) UploadFile(fileName string, r io.Reader) (*FileResponse, error) {
v := url.Values{}
v.Set("aimsid", a.token)
v.Set("filename", fileName)
req, err := http.NewRequest(http.MethodPost, a.baseUrl+"/im/sendFile?"+v.Encode(), r)
if err != nil {
return nil, err
}
resp, err := a.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
file := struct {
Data FileResponse `json:"data"`
}{}
if err := json.Unmarshal(b, &file); err != nil {
return nil, err
}
return &file.Data, nil
}

View file

@ -1,31 +0,0 @@
package icq
import (
"strings"
)
// Command is sugar on top of IMEvent that represented standard ICQ bot commands
type Command struct {
From string
Command string
Arguments []string
}
// ParseCommand from IMEvent
// Command must starts from '.' or '/'. Arguments separated by space (' ')
func ParseCommand(event *IMEvent) (*Command, bool) {
message := event.Data.Message
parts := strings.Split(message, " ")
if len(parts) == 0 {
return nil, false
}
if parts[0][0] != '.' && parts[0][0] != '/' {
return nil, false
}
cmd := string(parts[0][1:])
return &Command{
From: event.Data.Source.AimID,
Command: strings.ToLower(cmd),
Arguments: parts[1:],
}, true
}

View file

@ -1,33 +0,0 @@
package icq
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
// GetWebhookHandler returns http.HandleFunc that parses webhooks
// Warning! Not fully functional at ICQ now!
func (a *API) GetWebhookHandler(cu chan<- Update, e chan<- error) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if r.Method != http.MethodPost {
e <- fmt.Errorf("incorrect method: %s", r.Method)
return
}
wr := &WebhookRequest{}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
e <- err
return
}
if err := json.Unmarshal(b, wr); err != nil {
e <- err
return
}
for _, u := range wr.Updates {
cu <- u
}
}
}