+ Long polling to get events

+ ParseCommand sugar
This commit is contained in:
Alexander Kiryukhin 2018-12-22 19:53:28 +03:00
parent 98399bacff
commit 11849c8c18
No known key found for this signature in database
GPG key ID: 5579837FDBF65965
11 changed files with 377 additions and 173 deletions

View file

@ -13,8 +13,9 @@ Methods:
* SendMessage * SendMessage
* UploadFile * UploadFile
* FetchEvents
Webhooks to get updates Webhooks workds but not recommends
## Example ## Example
@ -25,67 +26,60 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"net/http"
"os" "os"
"os/signal" "os/signal"
"time"
"gopkg.in/icq.v1" "gopkg.in/icq.v2"
) )
func main() { func main() {
// New API object // New API object
b := icq.NewAPI(os.Getenv("ICQ_TOKEN")) b := icq.NewAPI(os.Getenv("ICQ_TOKEN"))
// Send message ctx, cancel := context.WithCancel(context.Background())
r, err := b.SendMessage(icq.Message{To: "429950", Text: "Hello, world!"})
if err != nil {
log.Fatalln(err)
}
log.Println(r.State)
// Send file ch := make(chan interface{}) // Events channel
f, err := os.Open("./example/icq.png")
defer f.Close()
if err != nil {
log.Fatalln(err)
}
file, err := b.UploadFile("icq.png", f)
if err != nil {
log.Fatalln(err)
}
b.SendMessage(icq.Message{To: "429950", Text: file.StaticUrl})
// Webhook usage
updates := make(chan icq.Update)
errors := make(chan error)
osSignal := make(chan os.Signal, 1) osSignal := make(chan os.Signal, 1)
m := http.NewServeMux()
m.HandleFunc("/webhook", b.GetWebhookHandler(updates, errors)) // Webhook sets here
h := &http.Server{Addr: ":8080", Handler: m}
go func() {
log.Fatalln(h.ListenAndServe())
}()
signal.Notify(osSignal, os.Interrupt) signal.Notify(osSignal, os.Interrupt)
signal.Notify(osSignal, os.Kill) signal.Notify(osSignal, os.Kill)
go b.FetchEvents(ctx, ch) // Events fetch loop
for { for {
select { select {
case u := <-updates: case e := <-ch:
log.Println("Incomming message", u) handleEvent(b, e)
case <-osSignal:
cancel()
break
}
}
}
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{ b.SendMessage(icq.Message{
To: u.Update.Chat.ID, To: message.Data.Source.AimID,
Text: fmt.Sprintf("You sent me: %s", u.Update.Text), Text: "Message process fail",
}) })
// ... process ICQ updates ... }
case err := <-errors: default:
log.Fatalln(err) log.Printf("%#v", event)
case sig := <-osSignal:
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
h.Shutdown(ctx)
log.Fatalln("OS signal:", sig.String())
} }
} }
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
} }
``` ```

79
api.go
View file

@ -1,15 +1,11 @@
package icq package icq
import ( import (
"encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time"
) )
// HTTP Client interface // HTTP Client interface
@ -22,6 +18,7 @@ type API struct {
token string token string
baseUrl string baseUrl string
client Doer client Doer
fetchBase string
} }
// NewAPI constructor of API object // NewAPI constructor of API object
@ -33,80 +30,6 @@ func NewAPI(token string) *API {
} }
} }
// 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
}
// 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
}
// GetWebhookHandler returns http.HandleFunc that parses webhooks
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
}
}
}
func (a *API) send(path string, v url.Values) ([]byte, error) { func (a *API) send(path string, v url.Values) ([]byte, error) {
req, err := http.NewRequest(http.MethodPost, a.baseUrl+path, strings.NewReader(v.Encode())) req, err := http.NewRequest(http.MethodPost, a.baseUrl+path, strings.NewReader(v.Encode()))
if err != nil { if err != nil {

60
events.go Normal file
View file

@ -0,0 +1,60 @@
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"
)
type TypingEvent struct {
CommonEvent
Data struct {
AimID string `json:"aimId"`
TypingStatus TypingStatus `json:"typingStatus"`
} `json:"eventData"`
}
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"`
}

View file

@ -3,67 +3,59 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/go-icq/icq"
"log" "log"
"net/http"
"os" "os"
"os/signal" "os/signal"
"time"
"github.com/go-icq/icq"
) )
func main() { func main() {
// New API object // New API object
b := icq.NewAPI(os.Getenv("ICQ_TOKEN")) b := icq.NewAPI(os.Getenv("ICQ_TOKEN"))
// Send message ctx, cancel := context.WithCancel(context.Background())
r, err := b.SendMessage(icq.Message{To: "429950", Text: "Hello, world!"})
if err != nil {
log.Fatalln(err)
}
log.Println(r.State)
// Send file ch := make(chan interface{}) // Events channel
f, err := os.Open("./example/icq.png")
defer f.Close()
if err != nil {
log.Fatalln(err)
}
file, err := b.UploadFile("icq.png", f)
if err != nil {
log.Fatalln(err)
}
b.SendMessage(icq.Message{To: "429950", Text: file.StaticUrl})
// Webhook usage
updates := make(chan icq.Update)
errors := make(chan error)
osSignal := make(chan os.Signal, 1) osSignal := make(chan os.Signal, 1)
m := http.NewServeMux()
m.HandleFunc("/webhook", b.GetWebhookHandler(updates, errors)) // Webhook sets here
h := &http.Server{Addr: ":8080", Handler: m}
go func() {
log.Fatalln(h.ListenAndServe())
}()
signal.Notify(osSignal, os.Interrupt) signal.Notify(osSignal, os.Interrupt)
signal.Notify(osSignal, os.Kill) signal.Notify(osSignal, os.Kill)
go b.FetchEvents(ctx, ch) // Events fetch loop
for { for {
select { select {
case u := <-updates: case e := <-ch:
log.Printf("Incomming message %#v", u) handleEvent(b, e)
case <-osSignal:
cancel()
break
}
}
}
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{ b.SendMessage(icq.Message{
To: u.Update.Chat.ID, To: message.Data.Source.AimID,
Text: fmt.Sprintf("You sent me: %s", u.Update.Text), Text: "Message process fail",
}) })
// ... process ICQ updates ... }
case err := <-errors: default:
log.Fatalln(err) log.Printf("%#v", event)
case sig := <-osSignal:
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
h.Shutdown(ctx)
log.Fatalln("OS signal:", sig.String())
} }
} }
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
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 KiB

89
fetchEvents.go Normal file
View file

@ -0,0 +1,89 @@
package icq
import (
"context"
"encoding/json"
"io/ioutil"
"log"
"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:
log.Printf("Unknown event %s: %#v", ce.Type, e)
}
}
select {
case <-time.After(time.Duration(fetchResp.Response.Data.PollTime)):
case <-ctx.Done():
return nil
}
}
return nil
}

36
sendMessage.go Normal file
View file

@ -0,0 +1,36 @@
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

@ -8,6 +8,7 @@ type Response struct {
Data *MessageResponse `json:"data"` Data *MessageResponse `json:"data"`
} `json:"response"` } `json:"response"`
} }
type ParseType string type ParseType string
const ( const (
@ -66,3 +67,15 @@ type User struct {
ID string `json:"id"` ID string `json:"id"`
LanguageCode string `json:"language_code"` 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"`
}

33
uploadFile.go Normal file
View file

@ -0,0 +1,33 @@
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
}

31
utils.go Normal file
View file

@ -0,0 +1,31 @@
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
}
return &Command{
From: event.Data.Source.AimID,
Command: string(parts[0][1:]),
Arguments: parts[1:],
}, true
}

33
webhookHandler.go Normal file
View file

@ -0,0 +1,33 @@
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
}
}
}