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 +}