From 9c0e52b3d30115ffaffe57efbeaf9875fc70d53f Mon Sep 17 00:00:00 2001 From: mileusna Date: Tue, 20 Jun 2017 20:16:24 +0200 Subject: [PATCH] Init --- account.go | 53 +++++++++ error.go | 22 ++++ message.go | 308 +++++++++++++++++++++++++++++++++++++++++++++++++++ request.go | 37 +++++++ timestamp.go | 29 +++++ user.go | 117 +++++++++++++++++++ viber.go | 105 ++++++++++++++++++ webhook.go | 56 ++++++++++ 8 files changed, 727 insertions(+) create mode 100644 account.go create mode 100644 error.go create mode 100644 message.go create mode 100644 request.go create mode 100644 timestamp.go create mode 100644 user.go create mode 100644 viber.go create mode 100644 webhook.go diff --git a/account.go b/account.go new file mode 100644 index 0000000..c2acb04 --- /dev/null +++ b/account.go @@ -0,0 +1,53 @@ +package viber + +import "encoding/json" + +// Member of account details +type Member struct { + ID string `json:"id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Role string `json:"role"` +} + +// Account details +type Account struct { + Status int `json:"status"` + StatusMessage string `json:"status_message"` + ID string `json:"id"` + Name string `json:"name"` + URI string `json:"uri"` + Icon string `json:"icon"` + Background string `json:"background"` + Category string `json:"category"` + Subcategory string `json:"subcategory"` + Location struct { + Lon float64 `json:"lon"` + Lat float64 `json:"lat"` + } `json:"location"` + Country string `json:"country"` + Webhook string `json:"webhook"` + EventTypes []string `json:"event_types"` + SubscribersCount int `json:"subscribers_count"` + Members []Member `json:"members"` +} + +// AccountInfo returns Public chat info +func (v *Viber) AccountInfo() (Account, error) { + var a Account + b, err := v.PostData("https://chatapi.viber.com/pa/get_account_info", struct{}{}) + if err != nil { + return a, err + } + + err = json.Unmarshal(b, &a) + if err != nil { + return a, err + } + + if a.Status != 0 { + return a, Error{Status: a.Status, StatusMessage: a.StatusMessage} + } + + return a, err +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..8dea603 --- /dev/null +++ b/error.go @@ -0,0 +1,22 @@ +package viber + +// Error from Viber +type Error struct { + Status int + StatusMessage string +} + +// Error interface function +func (e Error) Error() string { + //return fmt.Sprintf("Viber error, status ID: %d Status: %s", id, status) + return e.StatusMessage +} + +// ErrorStatus code of Viber error, returns -1 if e is not Viber error +func ErrorStatus(e interface{}) int { + switch e.(type) { + case Error: + return e.(Error).Status + } + return -1 +} diff --git a/message.go b/message.go new file mode 100644 index 0000000..df2258d --- /dev/null +++ b/message.go @@ -0,0 +1,308 @@ +package viber + +import ( + "encoding/json" + "fmt" +) + +/* +{ + "receiver": "01234567890A=", + "min_api_version": 1, + "sender": { + "name": "John McClane", + "avatar": "http://avatar.example.com" + }, + "tracking_data": "tracking data", + "type": "text", + "text": "a message from pa" +} +*/ + +// MessageType for viber messaging +type MessageType string + +type messageResponse struct { + Status int `json:"status"` + StatusMessage string `json:"status_message"` + MessageToken int64 `json:"message_token"` +} + +// Message interface for all types of viber messages +type Message interface { + SetReceiver(r string) + SetFrom(from string) +} + +// TextMessage for Viber +type TextMessage struct { + Receiver string `json:"receiver,omitempty"` + From string `json:"from,omitempty"` + MinAPIVersion uint `json:"min_api_version,omitempty"` + Sender Sender `json:"sender"` + Type MessageType `json:"type"` + TrackingData string `json:"tracking_data,omitempty"` + Text string `json:"text"` + // "media": "http://www.images.com/img.jpg", + // "thumbnail": "http://www.images.com/thumb.jpg" + // "size": 10000, + // "duration": 10 +} + +// URLMessage structure +type URLMessage struct { + TextMessage + Media string `json:"media"` +} + +// PictureMessage structure +type PictureMessage struct { + TextMessage + Media string `json:"media"` + Thumbnail string `json:"thumbnail,omitempty"` +} + +// VideoMessage structure +type VideoMessage struct { + TextMessage + Media string `json:"media"` + Thumbnail string `json:"thumbnail,omitempty"` + Size uint `json:"size"` + Duration uint `json:"duration,omitempty"` +} + +// Button for carousel +type Button struct { + Columns int `json:"Columns"` + Rows int `json:"Rows"` + ActionType string `json:"ActionType"` + ActionBody string `json:"ActionBody"` + Image string `json:"Image,omitempty"` + Text string `json:"Text,omitempty"` + TextSize string `json:"TextSize,omitempty"` + TextVAlign string `json:"TextVAlign,omitempty"` + TextHAlign string `json:"TextHAlign,omitempty"` +} + +// RichMedia for carousel +type RichMedia struct { + Type string `json:"Type"` + ButtonsGroupColumns int `json:"ButtonsGroupColumns"` + ButtonsGroupRows int `json:"ButtonsGroupRows"` + BgColor string `json:"BgColor"` + TrackingData string `json:"tracking_data,omitempty"` + Buttons []Button `json:"Buttons"` +} + +// RichMediaMessage / Carousel +type RichMediaMessage struct { + AuthToken string `json:"auth_token"` + Receiver string `json:"receiver"` + Type MessageType `json:"type"` + MinAPIVersion int `json:"min_api_version"` + RichMedia RichMedia `json:"rich_media"` +} + +const ( + TypeTextMessage = MessageType("text") + TypeURLMessage = MessageType("url") + TypePictureMessage = MessageType("picture") + TypeVideoMessage = MessageType("video") + TypeFileMessage = MessageType("file") + TypeLocationMessage = MessageType("location") + TypeContactMessage = MessageType("contact") + TypeStickerMessage = MessageType("sticker") + TypeRichMediaMessage = MessageType("rich_media") +) + +//video, file, location, contact, sticker, carousel content + +func parseMsgResponse(b []byte) (token int64, err error) { + var resp messageResponse + if err := json.Unmarshal(b, &resp); err != nil { + return 0, err + } + + if resp.Status != 0 { + return resp.MessageToken, Error{Status: resp.Status, StatusMessage: resp.StatusMessage} + } + + return resp.MessageToken, nil +} + +func (v *Viber) sendMessage(url string, m interface{}) (token int64, err error) { + b, err := v.PostData(url, m) + if err != nil { + return 0, err + } + fmt.Println(string(b)) + return parseMsgResponse(b) +} + +// NewTextMessage viber +func (v *Viber) NewTextMessage(msg string) *TextMessage { + return &TextMessage{ + Sender: v.Sender, + Type: TypeTextMessage, + Text: msg, + } +} + +// SendTextMessage to reciever, returns message token +func (v *Viber) SendTextMessage(receiver string, msg string) (token int64, err error) { + m := v.NewTextMessage(msg) + m.Receiver = receiver + return v.sendMessage("https://chatapi.viber.com/pa/send_message", m) +} + +func (v *Viber) NewURLMessage(msg string, url string) *URLMessage { + return &URLMessage{ + TextMessage: TextMessage{ + Sender: v.Sender, + Type: TypeURLMessage, + Text: msg, + }, + Media: url, + } +} + +// SendURLMessage to receiver, return message token +func (v *Viber) SendURLMessage(receiver string, s Sender, msg string, url string) (token int64, err error) { + m := v.NewURLMessage(msg, url) + return v.sendMessage("https://chatapi.viber.com/pa/send_message", m) +} + +// NewPictureMessage for viber +func (v *Viber) NewPictureMessage(msg string, url string, thumbURL string) *PictureMessage { + return &PictureMessage{ + TextMessage: TextMessage{ + Sender: v.Sender, + Type: TypePictureMessage, + Text: msg, + }, + Media: url, + Thumbnail: thumbURL, + } +} + +// SendPictureMessage to receiver, returns message token +// func (v *Viber) SendPictureMessage(receiver string, s Sender, msg string, url string, thumbURL string) (token int64, err error) { +// m := PictureMessage{ +// Message: Message{ +// Receiver: receiver, +// Sender: s, +// Type: TypePictureMessage, +// Text: msg, +// }, +// Media: url, +// Thumbnail: thumbURL, +// } + +// return v.sendMessage("https://chatapi.viber.com/pa/send_message", m) +// } + +func (v *Viber) SendCarousel(receiver string) { + r := RichMediaMessage{ + MinAPIVersion: 2, + Receiver: receiver, + AuthToken: v.AppKey, + Type: TypeRichMediaMessage, + RichMedia: RichMedia{ + Type: "rich_media", + ButtonsGroupColumns: 6, + ButtonsGroupRows: 6, + BgColor: "#FFFFFF", + }, + } + + b1 := Button{ + Columns: 6, + Rows: 3, + ActionType: "open-url", + ActionBody: "https://aviokarte.rs/", + Image: "http://nstatic.net/beta/2b5b3ff1972f61d9bcfaaddd061aa1b9.jpg", + } + r.RichMedia.Buttons = append(r.RichMedia.Buttons, b1) + + b1 = Button{ + Columns: 6, + Rows: 2, + ActionType: "open-url", + ActionBody: "https://aviokarte.rs/", + Text: "Košarkaši Crvene zvezde odbranili titulu šampiona Srbije", + } + r.RichMedia.Buttons = append(r.RichMedia.Buttons, b1) + + b1 = Button{ + Columns: 3, + Rows: 1, + ActionType: "reply", + ActionBody: "ID: 21432323", + Text: "Otvori", + TextSize: "large", + TextVAlign: "middle", + TextHAlign: "middle", + Image: "https://s14.postimg.org/4mmt4rw1t/Button.png", + } + r.RichMedia.Buttons = append(r.RichMedia.Buttons, b1) + + // b2 := Button{ + + // Columns: 6, + // Rows: 6, + // ActionType: "reply", + // ActionBody: "https://aviokarte.rs/", + // Image: "https://aviokarte.rs/images/logo.png", + // Text: "Drugi tekst", + // TextSize: "large", + // TextVAlign: "middle", + // TextHAlign: "left", + // } + // r.RichMedia.Buttons = append(r.RichMedia.Buttons, b2) + + resp, err := v.PostData("https://chatapi.viber.com/pa/send_message", r) + fmt.Println(string(resp), err) + +} + +func (v *Viber) SendPublicMessage(from string, m Message) (token int64, err error) { + m.SetFrom(from) + return v.sendMessage("https://chatapi.viber.com/pa/post", m) +} + +func (v *Viber) SendMessage(to string, m Message) (token int64, err error) { + m.SetReceiver(to) + return v.sendMessage("https://chatapi.viber.com/pa/send_message", m) +} + +func (m *TextMessage) SetReceiver(r string) { + m.Receiver = r +} + +func (m *URLMessage) SetReceiver(r string) { + m.Receiver = r +} + +func (m *PictureMessage) SetReceiver(r string) { + m.Receiver = r +} + +func (m *VideoMessage) SetReceiver(r string) { + m.Receiver = r +} + +func (m *TextMessage) SetFrom(from string) { + m.From = from +} + +func (m *URLMessage) SetFrom(from string) { + m.From = from +} + +func (m *PictureMessage) SetFrom(from string) { + m.From = from +} + +func (m *VideoMessage) SetFrom(from string) { + m.From = from +} diff --git a/request.go b/request.go new file mode 100644 index 0000000..d5af76e --- /dev/null +++ b/request.go @@ -0,0 +1,37 @@ +package viber + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "log" + "net/http" +) + +// PostData to viber API +func (v *Viber) PostData(url string, i interface{}) ([]byte, error) { + b, err := json.Marshal(i) + if err != nil { + return nil, err + } + + log.Println(string(b)) + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(b)) + req.Close = true + req.Header.Add("X-Viber-Auth-Token", v.AppKey) + + //http.DefaultClient.Timeout = time.Duration(Timeout * time.Second) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} diff --git a/timestamp.go b/timestamp.go new file mode 100644 index 0000000..78487cc --- /dev/null +++ b/timestamp.go @@ -0,0 +1,29 @@ +package viber + +import ( + "fmt" + "strconv" + "time" +) + +type Timestamp struct { + time.Time +} + +func (t *Timestamp) MarshalJSON() ([]byte, error) { + ts := t.Time.Unix() + stamp := fmt.Sprint(ts) + + return []byte(stamp), nil +} + +func (t *Timestamp) UnmarshalJSON(b []byte) error { + ts, err := strconv.Atoi(string(b)) + if err != nil { + return err + } + + t.Time = time.Unix(int64(ts), 0) + + return nil +} diff --git a/user.go b/user.go new file mode 100644 index 0000000..343894e --- /dev/null +++ b/user.go @@ -0,0 +1,117 @@ +package viber + +import "encoding/json" + +// User struct as part of UserDetails +type User struct { + ID string `json:"id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Country string `json:"country"` + Language string `json:"language"` + PrimaryDeviceOs string `json:"primary_device_os"` + APIVersion int `json:"api_version"` + ViberVersion string `json:"viber_version"` + Mcc int `json:"mcc"` + Mnc int `json:"mnc"` + DeviceType string `json:"device_type"` +} + +// UserDetails for Viber user +type UserDetails struct { + Status int `json:"status"` + StatusMessage string `json:"status_message"` + MessageToken int64 `json:"message_token"` + User `json:"user"` +} + +// Online status struct +type online struct { + Status int `json:"status"` + StatusMessage string `json:"status_message"` + Users []UserOnline `json:"users"` +} + +// UserOnline response struct +type UserOnline struct { + ID string `json:"id"` + OnlineStatus int `json:"online_status"` + OnlineStatusMessage string `json:"online_status_message"` + LastOnline int64 `json:"last_online,omitempty"` +} + +// UserDetails of user id +func (v *Viber) UserDetails(id string) (UserDetails, error) { + /* + b := []byte(`{ + "status": 0, + "status_message": "ok", + "message_token": 4912661846655238145, + "user": { + "id": "01234567890A=", + "name": "John McClane", + "avatar": "http://avatar.example.com", + "country": "UK", + "language": "en", + "primary_device_os": "android 7.1", + "api_version": 1, + "viber_version": "6.5.0", + "mcc": 1, + "mnc": 1 + } + }`) + + var u UserDetails + err := json.Unmarshal(b, &u) + return u, err + */ + + var u UserDetails + s := struct { + ID string `json:"id"` + }{ + ID: id, + } + + b, err := v.PostData("https://chatapi.viber.com/pa/get_user_details", s) + if err != nil { + return u, err + } + + if err := json.Unmarshal(b, &u); err != nil { + return u, err + } + + // viber error returned + if u.Status != 0 { + return u, Error{Status: u.Status, StatusMessage: u.StatusMessage} + } + + return u, err + +} + +// UserOnline status +func (v *Viber) UserOnline(ids []string) ([]UserOnline, error) { + var uo online + req := struct { + IDs []string `json:"ids"` + }{ + IDs: ids, + } + b, err := v.PostData("https://chatapi.viber.com/pa/get_online", req) + if err != nil { + return []UserOnline{}, err + } + + if err := json.Unmarshal(b, &uo); err != nil { + return []UserOnline{}, err + } + + // viber error + if uo.Status != 0 { + return []UserOnline{}, Error{Status: uo.Status, StatusMessage: uo.StatusMessage} + } + + return uo.Users, nil +} diff --git a/viber.go b/viber.go new file mode 100644 index 0000000..0d15220 --- /dev/null +++ b/viber.go @@ -0,0 +1,105 @@ +package viber + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/json" + "io/ioutil" + "net/http" + "time" +) + +// AppKey for you app provided by Viber +//const AppKey = "46160a5f87b294eb-9502de2bc1cf5ddb-5b70d84954155377" + +// Sender structure +type Sender struct { + Name string `json:"name"` + Avatar string `json:"avatar,omitempty"` +} + +type event struct { + Event string `json:"event"` + Timestamp Timestamp `json:"timestamp"` + UserID string `json:"user_id"` + MessageToken string `json:"message_token"` + Descr string `json:"descr"` +} + +// Viber app +type Viber struct { + AppKey string + Sender Sender + + Subscribed func(u User, msgToken string, t time.Time) + ConversationStarted func() + Message func() + Unsubscribed func(userID, msgToken string, t time.Time) + Delivered func(userID, msgToken string, t time.Time) + Seen func(userID, msgToken string, t time.Time) + Failed func(userID, msgToken, descr string, t time.Time) +} + +func (v *Viber) ServeHTTP(w http.ResponseWriter, r *http.Request) { + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return + } + r.Body.Close() + + if !v.checkMAC(body, []byte(r.Header.Get("X-Viber-Content-Signature"))) { + return + } + + var e event + if err := json.Unmarshal(body, &e); err != nil { + return + } + + switch e.Event { + case "subscribed": + if v.Subscribed != nil { + + } + + case "unsubscribed": + if v.Unsubscribed != nil { + v.Unsubscribed(e.UserID, e.MessageToken, e.Timestamp.Time) + } + + case "conversation_started": + if v.ConversationStarted != nil { + + } + + case "delivered": + if v.Delivered != nil { + v.Delivered(e.UserID, e.MessageToken, e.Timestamp.Time) + } + + case "seen": + if v.Seen != nil { + v.Seen(e.UserID, e.MessageToken, e.Timestamp.Time) + } + + case "failed": + if v.Failed != nil { + v.Failed(e.UserID, e.MessageToken, e.Descr, e.Timestamp.Time) + } + + case "message": + if v.Message != nil { + + } + + } +} + +// checkMAC reports whether messageMAC is a valid HMAC tag for message. +func (v *Viber) checkMAC(message, messageMAC []byte) bool { + mac := hmac.New(sha256.New, []byte(v.AppKey)) + mac.Write(message) + expectedMAC := mac.Sum(nil) + return hmac.Equal(messageMAC, expectedMAC) +} diff --git a/webhook.go b/webhook.go new file mode 100644 index 0000000..63b0dba --- /dev/null +++ b/webhook.go @@ -0,0 +1,56 @@ +package viber + +import ( + "encoding/json" + "fmt" +) + +// +//https://chatapi.viber.com/pa/set_webhook +// { +// "url": "https://my.host.com", +// "event_types": ["delivered", "seen", "failed", "subscribed", "unsubscribed", "conversation_started"] +// } + +// WebhookReq request +type WebhookReq struct { + URL string `json:"url"` + EventTypes []string `json:"event_types"` +} + +// { +// "status": 0, +// "status_message": "ok", +// "event_types": ["delivered", "seen", "failed", "subscribed", "unsubscribed", "conversation_started"] +// } + +//WebhookResp response +type WebhookResp struct { + Status int `json:"status"` + StatusMessage string `json:"status_message"` + EventTypes []string `json:"event_types"` +} + +// WebhookVerify response +type WebhookVerify struct { + Event string `json:"event"` + Timestamp uint64 `json:"timestamp"` + MessageToken uint64 `json:"message_token"` +} + +// SetWebhook for Viber callbacks +func (v *Viber) SetWebhook(url string, eventTypes []string) (WebhookResp, error) { + + req := WebhookReq{ + URL: url, + EventTypes: eventTypes, + } + + r, err := v.PostData("https://chatapi.viber.com/pa/set_webhook", req) + + fmt.Println(string(r)) + var resp WebhookResp + json.Unmarshal(r, &resp) + + return resp, err +}