From adac11495ab810eb17cfe51becb73cbb3f26bfde Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Sun, 8 Nov 2015 19:26:46 -0600 Subject: [PATCH] Inital addition of Websocket handling. Lots of moving things around. --- client.go => restapi.go | 0 users.go | 17 +++ util.go | 16 +++ wsapi.go | 230 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 263 insertions(+) rename client.go => restapi.go (100%) create mode 100644 users.go create mode 100644 util.go create mode 100644 wsapi.go diff --git a/client.go b/restapi.go similarity index 100% rename from client.go rename to restapi.go diff --git a/users.go b/users.go new file mode 100644 index 0000000..ca4ced9 --- /dev/null +++ b/users.go @@ -0,0 +1,17 @@ +package discordgo + +type User struct { + Id int `json:"id,string"` + Email string `json:"email"` + Username string `json:"username"` + Avatar string `json:"Avatar"` + Verified bool `json:"verified"` + Discriminator string `json:"discriminator"` +} + +type PrivateChannel struct { + Id int `json:"id,string"` + IsPrivate bool `json:"is_private"` + LastMessageId int `json:"last_message_id,string"` + Recipient User `json:"recipient"` +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..4338606 --- /dev/null +++ b/util.go @@ -0,0 +1,16 @@ +package discordgo + +import ( + "bytes" + "encoding/json" + "fmt" +) + +func printJSON(body []byte) { + var prettyJSON bytes.Buffer + error := json.Indent(&prettyJSON, body, "", "\t") + if error != nil { + fmt.Print("JSON parse error: ", error) + } + fmt.Println("RESPONSE ::\n" + string(prettyJSON.Bytes())) +} diff --git a/wsapi.go b/wsapi.go new file mode 100644 index 0000000..e5809d7 --- /dev/null +++ b/wsapi.go @@ -0,0 +1,230 @@ +/****************************************************************************** + * Discordgo by Bruce Marriner + * A Discord API for Golang. + * See discord.go for more information. + * + * This file contains functions low level functions for interacting + * with the Discord Websocket interface. + */ + +package discordgo + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/gorilla/websocket" +) + +// Basic struct for all Websocket Event messages +type Event struct { + Type string `json:"t"` + State int `json:"s"` + Operation int `json:"o"` + Direction int `json:"dir"` + //Direction of command, 0-received, 1-sent -- thanks Xackery/discord + + RawData json.RawMessage `json:"d"` +} + +// The Ready Event given after initial connection +type Ready struct { + Version int `json:"v"` + SessionID string `json:"session_id"` + HeartbeatInterval time.Duration `json:"heartbeat_interval"` + User User `json:"user"` + ReadState []ReadState + PrivateChannels []PrivateChannel + Servers []Server +} + +// ReadState might need to move? Gives me the read status +// of all my channels when first connecting. I think :) +type ReadState struct { + MentionCount int + LastMessageID int `json:"last_message_id,string"` + ID int `json:"id,string"` +} + +// Returns the a websocket Gateway address +// session : An active session connection to Discord +// put this here instead of restapi because it is used soley +// for the websocket stuff - but maybe I should move it back +// because it's part of the restapi... +func Gateway(session *Session) (gateway string, err error) { + + response, err := Request(session, "GET", fmt.Sprintf("%s/gateway", discordApi), ``) + + var temp map[string]interface{} + err = json.Unmarshal(response, &temp) + gateway = temp["url"].(string) + return +} + +// Open a websocket connection to Discord +func Open(session *Session) (conn *websocket.Conn, err error) { + + // TODO: See if there's a use for the http response. + //conn, response, err := websocket.DefaultDialer.Dial(session.Gateway, nil) + conn, _, err = websocket.DefaultDialer.Dial(session.Gateway, nil) + if err != nil { + return + } + + return +} + +// maybe this is SendOrigin? not sure the right name here +// also bson.M vs string interface map? Read about +// how to send JSON the right way. +func Handshake(conn *websocket.Conn, token string) (err error) { + + err = conn.WriteJSON(map[string]interface{}{ + "op": 2, + "d": map[string]interface{}{ + "v": 3, + "token": token, + "properties": map[string]string{ + "$os": "linux", // get from os package + "$browser": "Discordgo", + "$device": "Discordgo", + "$referer": "", + "$referring_domain": "", + }, + }, + }) + + return +} + +func UpdateStatus(conn *websocket.Conn, idleSince, gameId string) (err error) { + + err = conn.WriteJSON(map[string]interface{}{ + "op": 2, + "d": map[string]interface{}{ + "idle_since": idleSince, + "game_id": gameId, + }, + }) + + return +} + +// TODO: need a channel or something to communicate +// to this so I can tell it to stop listening +func Listen(conn *websocket.Conn) (err error) { + for { + messageType, message, err := conn.ReadMessage() + if err != nil { + fmt.Println(err) + break + } + go event(conn, messageType, message) + } + + return +} + +// Not sure how needed this is and where it would be best to call it. +// somewhere. +func Close(conn *websocket.Conn) { + conn.Close() +} + +// Front line handler for all Websocket Events. Determines the +// event type and passes the message along to the next handler. +func event(conn *websocket.Conn, messageType int, message []byte) { + + //printJSON(message) // TODO: wrap in debug if statement + + var event Event + err := json.Unmarshal(message, &event) + if err != nil { + fmt.Println(err) + return + } + + switch event.Type { + + case "READY": + ready(conn, &event) + case "TYPING_START": + // do stuff + case "MESSAGE_CREATE": + // do stuff + case "MESSAGE_ACK": + // do stuff + case "MESSAGE_UPDATE": + // do stuff + case "MESSAGE_DELETE": + // do stuff + case "PRESENCE_UPDATE": + // do stuff + case "CHANNEL_CREATE": + // do stuff + case "CHANNEL_UPDATE": + // do stuff + case "CHANNEL_DELETE": + // do stuff + case "GUILD_CREATE": + // do stuff + case "GUILD_DELETE": + // do stuff + case "GUILD_MEMBER_ADD": + // do stuff + case "GUILD_MEMBER_REMOVE": // which is it. + // do stuff + case "GUILD_MEMBER_DELETE": + // do stuff + case "GUILD_MEMBER_UPDATE": + // do stuff + case "GUILD_ROLE_CREATE": + // do stuff + case "GUILD_ROLE_DELETE": + // do stuff + case "GUILD_INTEGRATIONS_UPDATE": + // do stuff + + default: + fmt.Println("UNKNOWN EVENT: ", event.Type) + // learn the log package + // log.print type and JSON data + } + +} + +// handles the READY Websocket Event from Discord +// this is the motherload of detail provided at +// initial connection to the Websocket. +func ready(conn *websocket.Conn, event *Event) { + + var ready Ready + err := json.Unmarshal(event.RawData, &ready) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(ready) + + go heartbeat(conn, ready.HeartbeatInterval) + + // Start KeepAlive based on . + +} + +// This heartbeat is sent to keep the Websocket conenction +// to Discord alive. If not sent, Discord will close the +// connection. +func heartbeat(conn *websocket.Conn, interval time.Duration) { + + ticker := time.NewTicker(interval * time.Millisecond) + for range ticker.C { + timestamp := int(time.Now().Unix()) + conn.WriteJSON(map[string]int{ + "op": 1, + "d": timestamp, + }) + } +}