+ Long polling to get events
+ ParseCommand sugar
This commit is contained in:
parent
98399bacff
commit
11849c8c18
11 changed files with 377 additions and 173 deletions
86
README.md
86
README.md
|
@ -13,8 +13,9 @@ Methods:
|
|||
|
||||
* SendMessage
|
||||
* UploadFile
|
||||
* FetchEvents
|
||||
|
||||
Webhooks to get updates
|
||||
Webhooks workds but not recommends
|
||||
|
||||
## Example
|
||||
|
||||
|
@ -25,67 +26,60 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"gopkg.in/icq.v1"
|
||||
"gopkg.in/icq.v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// New API object
|
||||
b := icq.NewAPI(os.Getenv("ICQ_TOKEN"))
|
||||
|
||||
// Send message
|
||||
r, err := b.SendMessage(icq.Message{To: "429950", Text: "Hello, world!"})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
log.Println(r.State)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Send file
|
||||
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)
|
||||
ch := make(chan interface{}) // Events channel
|
||||
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.Kill)
|
||||
|
||||
go b.FetchEvents(ctx, ch) // Events fetch loop
|
||||
|
||||
for {
|
||||
select {
|
||||
case u := <-updates:
|
||||
log.Println("Incomming message", u)
|
||||
b.SendMessage(icq.Message{
|
||||
To: u.Update.Chat.ID,
|
||||
Text: fmt.Sprintf("You sent me: %s", u.Update.Text),
|
||||
})
|
||||
// ... process ICQ updates ...
|
||||
case err := <-errors:
|
||||
log.Fatalln(err)
|
||||
case sig := <-osSignal:
|
||||
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
h.Shutdown(ctx)
|
||||
log.Fatalln("OS signal:", sig.String())
|
||||
case e := <-ch:
|
||||
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{
|
||||
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
|
||||
}
|
||||
```
|
||||
|
|
85
api.go
85
api.go
|
@ -1,15 +1,11 @@
|
|||
package icq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTP Client interface
|
||||
|
@ -19,9 +15,10 @@ type Doer interface {
|
|||
|
||||
// API
|
||||
type API struct {
|
||||
token string
|
||||
baseUrl string
|
||||
client Doer
|
||||
token string
|
||||
baseUrl string
|
||||
client Doer
|
||||
fetchBase string
|
||||
}
|
||||
|
||||
// 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) {
|
||||
req, err := http.NewRequest(http.MethodPost, a.baseUrl+path, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
|
|
60
events.go
Normal file
60
events.go
Normal 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"`
|
||||
}
|
|
@ -3,67 +3,59 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/go-icq/icq"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/go-icq/icq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// New API object
|
||||
b := icq.NewAPI(os.Getenv("ICQ_TOKEN"))
|
||||
|
||||
// Send message
|
||||
r, err := b.SendMessage(icq.Message{To: "429950", Text: "Hello, world!"})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
log.Println(r.State)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Send file
|
||||
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)
|
||||
ch := make(chan interface{}) // Events channel
|
||||
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.Kill)
|
||||
|
||||
go b.FetchEvents(ctx, ch) // Events fetch loop
|
||||
|
||||
for {
|
||||
select {
|
||||
case u := <-updates:
|
||||
log.Printf("Incomming message %#v", u)
|
||||
b.SendMessage(icq.Message{
|
||||
To: u.Update.Chat.ID,
|
||||
Text: fmt.Sprintf("You sent me: %s", u.Update.Text),
|
||||
})
|
||||
// ... process ICQ updates ...
|
||||
case err := <-errors:
|
||||
log.Fatalln(err)
|
||||
case sig := <-osSignal:
|
||||
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
h.Shutdown(ctx)
|
||||
log.Fatalln("OS signal:", sig.String())
|
||||
case e := <-ch:
|
||||
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{
|
||||
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/icq.png
BIN
example/icq.png
Binary file not shown.
Before Width: | Height: | Size: 619 KiB |
89
fetchEvents.go
Normal file
89
fetchEvents.go
Normal 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
36
sendMessage.go
Normal 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
|
||||
}
|
13
types.go
13
types.go
|
@ -8,6 +8,7 @@ type Response struct {
|
|||
Data *MessageResponse `json:"data"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type ParseType string
|
||||
|
||||
const (
|
||||
|
@ -66,3 +67,15 @@ 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"`
|
||||
}
|
||||
|
|
33
uploadFile.go
Normal file
33
uploadFile.go
Normal 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
31
utils.go
Normal 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
33
webhookHandler.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue