diff --git a/README.md b/README.md index 8e836ad..2768b18 100644 --- a/README.md +++ b/README.md @@ -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 Кирюхин \ No newline at end of file diff --git a/api.go b/api.go deleted file mode 100644 index 6ef6548..0000000 --- a/api.go +++ /dev/null @@ -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 -} diff --git a/chats.go b/chats.go new file mode 100644 index 0000000..59ad802 --- /dev/null +++ b/chats.go @@ -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) +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..b99a768 --- /dev/null +++ b/client.go @@ -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 +} diff --git a/events.go b/events.go index fe682e0..598c576 100644 --- a/events.go +++ b/events.go @@ -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 } diff --git a/example/example.go b/example/example.go index 6c4e125..90781e8 100644 --- a/example/example.go +++ b/example/example.go @@ -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 -} diff --git a/example/example.jpg b/example/example.jpg new file mode 100644 index 0000000..c167a3e Binary files /dev/null and b/example/example.jpg differ diff --git a/fetchEvents.go b/fetchEvents.go deleted file mode 100644 index 946a4e6..0000000 --- a/fetchEvents.go +++ /dev/null @@ -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 -} diff --git a/files.go b/files.go new file mode 100644 index 0000000..eaf1731 --- /dev/null +++ b/files.go @@ -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) +} diff --git a/go.mod b/go.mod index f591069..5b774d0 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/go-icq/icq + +go 1.12 diff --git a/icq.go b/icq.go new file mode 100644 index 0000000..682014d --- /dev/null +++ b/icq.go @@ -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), + } +} diff --git a/messages.go b/messages.go new file mode 100644 index 0000000..9b8a70e --- /dev/null +++ b/messages.go @@ -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) +} diff --git a/schemas.go b/schemas.go new file mode 100644 index 0000000..82b1df3 --- /dev/null +++ b/schemas.go @@ -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" +) diff --git a/self.go b/self.go new file mode 100644 index 0000000..771b2d6 --- /dev/null +++ b/self.go @@ -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) +} diff --git a/sendMessage.go b/sendMessage.go deleted file mode 100644 index 4856218..0000000 --- a/sendMessage.go +++ /dev/null @@ -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 -} diff --git a/types.go b/types.go deleted file mode 100644 index f82b3b8..0000000 --- a/types.go +++ /dev/null @@ -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"` -} diff --git a/uploadFile.go b/uploadFile.go deleted file mode 100644 index 11dd76e..0000000 --- a/uploadFile.go +++ /dev/null @@ -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 -} diff --git a/utils.go b/utils.go deleted file mode 100644 index 292e029..0000000 --- a/utils.go +++ /dev/null @@ -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 -} diff --git a/webhookHandler.go b/webhookHandler.go deleted file mode 100644 index 65457dc..0000000 --- a/webhookHandler.go +++ /dev/null @@ -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 - } - } -}