A => DiscordGo/LICENSE +28 -0
@@ 1,28 @@
+Copyright (c) 2015, Bruce Marriner
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of GoDiscord nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
A => DiscordGo/README.md +128 -0
@@ 1,128 @@
+<img align="right" src="http://bwmarrin.github.io/discordgo/img/discordgo.png">
+Discordgo
+====
+>If for some reason you stumbled upon this repo, use the official one: https://github.com/bwmarrin/discordgo
+This is merely a master fork for stability reasons of discord-cli.
+Since their highly unstable develop branch is their default one this can give problems with continious integration tools or even the dependency errors. When 1.0 arrives, and becomes default, this repo gets removed.
+
+[](https://godoc.org/github.com/bwmarrin/discordgo) [](http://goreportcard.com/report/bwmarrin/discordgo) [](https://travis-ci.org/bwmarrin/discordgo)
+
+Discordgo is a [Go](https://golang.org/) package that provides low level
+bindings to the [Discord](https://discordapp.com/) chat client API. Discordgo
+has nearly complete support for all of the Discord JSON-API endpoints, websocket
+interface, and voice interface.
+
+* See [dgVoice](https://github.com/bwmarrin/dgvoice) package to extend Discordgo
+with additional voice helper functions and features.
+
+* See [dca](https://github.com/bwmarrin/dca) for an **experimental** stand alone
+tool that wraps `ffmpeg` to create opus encoded audio appropriate for use with
+Discord (and Discordgo)
+
+Join [#go_discordgo](https://discord.gg/0SBTUU1wZTWT6sqd) Discord chat channel
+for support.
+
+## Getting Started
+
+### master vs develop Branch
+* The master branch represents the latest released version of Discordgo. This
+branch will always have a stable and tested version of the library. Each release
+is tagged and you can easily download a specific release and view release notes
+on the github [releases](https://github.com/bwmarrin/discordgo/releases) page.
+
+* The develop branch is where all development happens and almost always has
+new features over the master branch. However breaking changes are frequently
+added to develop and even sometimes bugs are introduced. Bugs get fixed and
+the breaking changes get documented before pushing to master.
+
+*So, what should you use?*
+
+If you can accept the constant changing nature of *develop* then it is the
+recommended branch to use. Otherwise, if you want to tail behind development
+slightly and have a more stable package with documented releases then use *master*
+
+### Installing
+
+Discordgo has been tested to compile on Debian 8 (Go 1.3.3),
+FreeBSD 10 (Go 1.5.1), and Windows 7 (Go 1.5.2).
+
+This assumes you already have a working Go environment, if not please see
+[this page](https://golang.org/doc/install) first.
+
+`go get` *will always pull the latest released version from the master branch.*
+
+```sh
+go get github.com/bwmarrin/discordgo
+```
+
+If you want to use the develop branch, follow these steps next.
+
+```sh
+cd $GOPATH/src/github.com/bwmarrin/discordgo
+git checkout develop
+```
+
+
+
+### Usage
+
+Import the package into your project.
+
+```go
+import "github.com/bwmarrin/discordgo"
+```
+
+Construct a new Discord client which can be used to access the variety of
+Discord API functions and to set callback functions for Discord events.
+
+```go
+discord, err := discordgo.New("username", "password")
+```
+
+See Documentation and Examples below for more detailed information.
+
+
+## Documentation
+
+**NOTICE** : This library and the Discord API are unfinished.
+Because of that there may be major changes to library functions, constants,
+and structures.
+
+The Discordgo code is fairly well documented at this point and is currently
+the only documentation available. Both GoDoc and GoWalker (below) present
+that information in a nice format.
+
+- [](https://godoc.org/github.com/bwmarrin/discordgo)
+- [](https://gowalker.org/github.com/bwmarrin/discordgo)
+- [Unofficial Discord API Documentation](https://discordapi.readthedocs.org/en/latest/)
+- Hand crafted documentation coming eventually.
+
+
+## Examples
+
+Below is a list of examples and other projects using Discordgo. Please submit
+an issue if you would like your project added or removed from this list
+
+- [Basic - New](https://github.com/bwmarrin/discordgo/tree/develop/examples/new_basic) A basic example using the easy New() helper function
+- [Basic - API](https://github.com/bwmarrin/discordgo/tree/develop/examples/api_basic) A basic example using the low level API functions.
+- [Bruxism](https://github.com/iopred/bruxism) A chat bot for YouTube and Discord
+- [GoGerard](https://github.com/GoGerard/GoGerard) A modern bot for Discord
+- [Digo](https://github.com/sethdmoore/digo) A pluggable bot for your Discord server
+
+## Contributing
+Contributions are very welcomed, however please follow the below guidelines.
+
+- First open an issue describing the bug or enhancement so it can be
+discussed.
+- Fork the develop branch and make your changes.
+- Try to match current naming conventions as closely as possible.
+- This package is intended to be a low level direct mapping of the Discord API
+so please avoid adding enhancements outside of that scope without first
+discussing it.
+- Create a Pull Request with your changes against the develop branch.
+
+
+## List of Discord APIs
+
+See [this chart](https://abal.moe/Discord/Libraries.html) for a feature
+comparison and list of other Discord API libraries.
A => DiscordGo/discord.go +247 -0
@@ 1,247 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains high level helper functions and easy entry points for the
+// entire discordgo package. These functions are beling developed and are very
+// experimental at this point. They will most likley change so please use the
+// low level functions if that's a problem.
+
+// Package discordgo provides Discord binding for Go
+package discordgo
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/)
+const VERSION = "0.11.0"
+
+// New creates a new Discord session and will automate some startup
+// tasks if given enough information to do so. Currently you can pass zero
+// arguments and it will return an empty Discord session.
+// There are 3 ways to call New:
+// With a single auth token - All requests will use the token blindly,
+// no verification of the token will be done and requests may fail.
+// With an email and password - Discord will sign in with the provided
+// credentials.
+// With an email, password and auth token - Discord will verify the auth
+// token, if it is invalid it will sign in with the provided
+// credentials. This is the Discord recommended way to sign in.
+func New(args ...interface{}) (s *Session, err error) {
+
+ // Create an empty Session interface.
+ s = &Session{
+ State: NewState(),
+ StateEnabled: true,
+ Compress: true,
+ ShouldReconnectOnError: true,
+ }
+
+ // If no arguments are passed return the empty Session interface.
+ // Later I will add default values, if appropriate.
+ if args == nil {
+ return
+ }
+
+ // Variables used below when parsing func arguments
+ var auth, pass string
+
+ // Parse passed arguments
+ for _, arg := range args {
+
+ switch v := arg.(type) {
+
+ case []string:
+ if len(v) > 3 {
+ err = fmt.Errorf("Too many string parameters provided.")
+ return
+ }
+
+ // First string is either token or username
+ if len(v) > 0 {
+ auth = v[0]
+ }
+
+ // If second string exists, it must be a password.
+ if len(v) > 1 {
+ pass = v[1]
+ }
+
+ // If third string exists, it must be an auth token.
+ if len(v) > 2 {
+ s.Token = v[2]
+ }
+
+ case string:
+ // First string must be either auth token or username.
+ // Second string must be a password.
+ // Only 2 input strings are supported.
+
+ if auth == "" {
+ auth = v
+ } else if pass == "" {
+ pass = v
+ } else if s.Token == "" {
+ s.Token = v
+ } else {
+ err = fmt.Errorf("Too many string parameters provided.")
+ return
+ }
+
+ // case Config:
+ // TODO: Parse configuration
+
+ default:
+ err = fmt.Errorf("Unsupported parameter type provided.")
+ return
+ }
+ }
+
+ // If only one string was provided, assume it is an auth token.
+ // Otherwise get auth token from Discord, if a token was specified
+ // Discord will verify it for free, or log the user in if it is
+ // invalid.
+ if pass == "" {
+ s.Token = auth
+ } else {
+ err = s.Login(auth, pass)
+ if err != nil || s.Token == "" {
+ err = fmt.Errorf("Unable to fetch discord authentication token. %v", err)
+ return
+ }
+ }
+
+ // The Session is now able to have RestAPI methods called on it.
+ // It is recommended that you now call Open() so that events will trigger.
+
+ return
+}
+
+// validateHandler takes an event handler func, and returns the type of event.
+// eg.
+// Session.validateHandler(func (s *discordgo.Session, m *discordgo.MessageCreate))
+// will return the reflect.Type of *discordgo.MessageCreate
+func (s *Session) validateHandler(handler interface{}) reflect.Type {
+ handlerType := reflect.TypeOf(handler)
+
+ if handlerType.NumIn() != 2 {
+ panic("Unable to add event handler, handler must be of the type func(*discordgo.Session, *discordgo.EventType).")
+ }
+
+ if handlerType.In(0) != reflect.TypeOf(s) {
+ panic("Unable to add event handler, first argument must be of type *discordgo.Session.")
+ }
+
+ eventType := handlerType.In(1)
+
+ // Support handlers of type interface{}, this is a special handler, which is triggered on every event.
+ if eventType.Kind() == reflect.Interface {
+ eventType = nil
+ }
+
+ return eventType
+}
+
+// AddHandler allows you to add an event handler that will be fired anytime
+// the Discord WSAPI event that matches the interface fires.
+// eventToInterface in events.go has a list of all the Discord WSAPI events
+// and their respective interface.
+// eg:
+// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
+// })
+//
+// or:
+// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) {
+// })
+// The return value of this method is a function, that when called will remove the
+// event handler.
+func (s *Session) AddHandler(handler interface{}) func() {
+ s.initialize()
+
+ eventType := s.validateHandler(handler)
+
+ s.handlersMu.Lock()
+ defer s.handlersMu.Unlock()
+
+ h := reflect.ValueOf(handler)
+
+ handlers := s.handlers[eventType]
+ if handlers == nil {
+ handlers = []reflect.Value{}
+ }
+ s.handlers[eventType] = append(handlers, h)
+
+ // This must be done as we need a consistent reference to the
+ // reflected value, otherwise a RemoveHandler method would have
+ // been nice.
+ return func() {
+ s.handlersMu.Lock()
+ defer s.handlersMu.Unlock()
+
+ handlers := s.handlers[eventType]
+ for i, v := range handlers {
+ if h == v {
+ s.handlers[eventType] = append(handlers[:i], handlers[i+1:]...)
+ return
+ }
+ }
+ }
+}
+
+// handle calls any handlers that match the event type and any handlers of
+// interface{}.
+func (s *Session) handle(event interface{}) {
+ s.handlersMu.RLock()
+ defer s.handlersMu.RUnlock()
+
+ if s.handlers == nil {
+ return
+ }
+
+ handlerParameters := []reflect.Value{reflect.ValueOf(s), reflect.ValueOf(event)}
+
+ if handlers, ok := s.handlers[reflect.TypeOf(event)]; ok {
+ for _, handler := range handlers {
+ handler.Call(handlerParameters)
+ }
+ }
+
+ if handlers, ok := s.handlers[nil]; ok {
+ for _, handler := range handlers {
+ handler.Call(handlerParameters)
+ }
+ }
+}
+
+// initialize adds all internal handlers and state tracking handlers.
+func (s *Session) initialize() {
+ s.handlersMu.Lock()
+ if s.handlers != nil {
+ s.handlersMu.Unlock()
+ return
+ }
+
+ s.handlers = map[interface{}][]reflect.Value{}
+ s.handlersMu.Unlock()
+
+ s.AddHandler(s.onEvent)
+ s.AddHandler(s.onReady)
+ s.AddHandler(s.onVoiceServerUpdate)
+ s.AddHandler(s.onVoiceStateUpdate)
+ s.AddHandler(s.State.onInterface)
+}
+
+// onEvent handles events that are unhandled or errored while unmarshalling
+func (s *Session) onEvent(se *Session, e *Event) {
+ printEvent(e)
+}
+
+// onReady handles the ready event.
+func (s *Session) onReady(se *Session, r *Ready) {
+ go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval)
+}
A => DiscordGo/discord_test.go +288 -0
@@ 1,288 @@
+package discordgo
+
+import (
+ "os"
+ "runtime"
+ "testing"
+ "time"
+)
+
+//////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////// VARS NEEDED FOR TESTING
+var (
+ dg *Session // Stores global discordgo session
+
+ envToken = os.Getenv("DG_TOKEN") // Token to use when authenticating
+ envEmail = os.Getenv("DG_EMAIL") // Email to use when authenticating
+ envPassword = os.Getenv("DG_PASSWORD") // Password to use when authenticating
+ // envGuild = os.Getenv("DG_GUILD") // Guild ID to use for tests
+ envChannel = os.Getenv("DG_CHANNEL") // Channel ID to use for tests
+ // envUser = os.Getenv("DG_USER") // User ID to use for tests
+ envAdmin = os.Getenv("DG_ADMIN") // User ID of admin user to use for tests
+)
+
+func init() {
+ if envEmail == "" || envPassword == "" || envToken == "" {
+ return
+ }
+
+ if d, err := New(envEmail, envPassword, envToken); err == nil {
+ dg = d
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//////////////////////////////////////////// HELPER FUNCTIONS USED FOR TESTING
+
+// This waits x time for the check bool to be the want bool
+func waitBoolEqual(timeout time.Duration, check *bool, want bool) bool {
+
+ start := time.Now()
+ for {
+ if *check == want {
+ return true
+ }
+
+ if time.Since(start) > timeout {
+ return false
+ }
+
+ runtime.Gosched()
+ }
+}
+
+// Checks if we're connected to Discord
+func isConnected() bool {
+
+ if dg == nil {
+ return false
+ }
+
+ if dg.Token == "" {
+ return false
+ }
+
+ // Need a way to see if the ws connection is nil
+
+ if !waitBoolEqual(10*time.Second, &dg.DataReady, true) {
+ return false
+ }
+
+ return true
+}
+
+//////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////// START OF TESTS
+
+// TestNew tests the New() function without any arguments. This should return
+// a valid Session{} struct and no errors.
+func TestNew(t *testing.T) {
+
+ _, err := New()
+ if err != nil {
+ t.Errorf("New() returned error: %+v", err)
+ }
+}
+
+// TestInvalidToken tests the New() function with an invalid token
+func TestInvalidToken(t *testing.T) {
+ d, err := New("asjkldhflkjasdh")
+ if err != nil {
+ t.Fatalf("New(InvalidToken) returned error: %+v", err)
+ }
+
+ // New with just a token does not do any communication, so attempt an api call.
+ _, err = d.UserSettings()
+ if err == nil {
+ t.Errorf("New(InvalidToken), d.UserSettings returned nil error.")
+ }
+}
+
+// TestInvalidUserPass tests the New() function with an invalid Email and Pass
+func TestInvalidEmailPass(t *testing.T) {
+
+ _, err := New("invalidemail", "invalidpassword")
+ if err == nil {
+ t.Errorf("New(InvalidEmail, InvalidPass) returned nil error.")
+ }
+
+}
+
+// TestInvalidPass tests the New() function with an invalid Password
+func TestInvalidPass(t *testing.T) {
+
+ if envEmail == "" {
+ t.Skip("Skipping New(username,InvalidPass), DG_EMAIL not set")
+ return
+ }
+ _, err := New(envEmail, "invalidpassword")
+ if err == nil {
+ t.Errorf("New(Email, InvalidPass) returned nil error.")
+ }
+}
+
+// TestNewUserPass tests the New() function with a username and password.
+// This should return a valid Session{}, a valid Session.Token.
+func TestNewUserPass(t *testing.T) {
+
+ if envEmail == "" || envPassword == "" {
+ t.Skip("Skipping New(username,password), DG_EMAIL or DG_PASSWORD not set")
+ return
+ }
+
+ d, err := New(envEmail, envPassword)
+ if err != nil {
+ t.Fatalf("New(user,pass) returned error: %+v", err)
+ }
+
+ if d == nil {
+ t.Fatal("New(user,pass), d is nil, should be Session{}")
+ }
+
+ if d.Token == "" {
+ t.Fatal("New(user,pass), d.Token is empty, should be a valid Token.")
+ }
+}
+
+// TestNewToken tests the New() function with a Token. This should return
+// the same as the TestNewUserPass function.
+func TestNewToken(t *testing.T) {
+
+ if envToken == "" {
+ t.Skip("Skipping New(token), DG_TOKEN not set")
+ }
+
+ d, err := New(envToken)
+ if err != nil {
+ t.Fatalf("New(envToken) returned error: %+v", err)
+ }
+
+ if d == nil {
+ t.Fatal("New(envToken), d is nil, should be Session{}")
+ }
+
+ if d.Token == "" {
+ t.Fatal("New(envToken), d.Token is empty, should be a valid Token.")
+ }
+}
+
+// TestNewUserPassToken tests the New() function with a username, password and token.
+// This should return the same as the TestNewUserPass function.
+func TestNewUserPassToken(t *testing.T) {
+
+ if envEmail == "" || envPassword == "" || envToken == "" {
+ t.Skip("Skipping New(username,password,token), DG_EMAIL, DG_PASSWORD or DG_TOKEN not set")
+ return
+ }
+
+ d, err := New(envEmail, envPassword, envToken)
+ if err != nil {
+ t.Fatalf("New(user,pass,token) returned error: %+v", err)
+ }
+
+ if d == nil {
+ t.Fatal("New(user,pass,token), d is nil, should be Session{}")
+ }
+
+ if d.Token == "" {
+ t.Fatal("New(user,pass,token), d.Token is empty, should be a valid Token.")
+ }
+}
+
+func TestOpenClose(t *testing.T) {
+ if envToken == "" {
+ t.Skip("Skipping TestClose, DG_TOKEN not set")
+ }
+
+ d, err := New(envToken)
+ if err != nil {
+ t.Fatalf("TestClose, New(envToken) returned error: %+v", err)
+ }
+
+ if err = d.Open(); err != nil {
+ t.Fatalf("TestClose, d.Open failed: %+v", err)
+ }
+
+ if !waitBoolEqual(10*time.Second, &d.DataReady, true) {
+ t.Fatal("DataReady never became true.")
+ }
+
+ // TODO find a better way
+ // Add a small sleep here to make sure heartbeat and other events
+ // have enough time to get fired. Need a way to actually check
+ // those events.
+ time.Sleep(2 * time.Second)
+
+ // UpdateStatus - maybe we move this into wsapi_test.go but the websocket
+ // created here is needed. This helps tests that the websocket was setup
+ // and it is working.
+ if err = d.UpdateStatus(0, time.Now().String()); err != nil {
+ t.Errorf("UpdateStatus error: %+v", err)
+ }
+
+ if err = d.Close(); err != nil {
+ t.Fatalf("TestClose, d.Close failed: %+v", err)
+ }
+}
+
+func TestAddHandler(t *testing.T) {
+ testHandlerCalled := 0
+ testHandler := func(s *Session, m *MessageCreate) {
+ testHandlerCalled++
+ }
+
+ interfaceHandlerCalled := 0
+ interfaceHandler := func(s *Session, i interface{}) {
+ interfaceHandlerCalled++
+ }
+
+ bogusHandlerCalled := false
+ bogusHandler := func(s *Session, se *Session) {
+ bogusHandlerCalled = true
+ }
+
+ d := Session{}
+ d.AddHandler(testHandler)
+ d.AddHandler(testHandler)
+
+ d.AddHandler(interfaceHandler)
+ d.AddHandler(bogusHandler)
+
+ d.handle(&MessageCreate{})
+ d.handle(&MessageDelete{})
+
+ // testHandler will be called twice because it was added twice.
+ if testHandlerCalled != 2 {
+ t.Fatalf("testHandler was not called twice.")
+ }
+
+ // interfaceHandler will be called twice, once for each event.
+ if interfaceHandlerCalled != 2 {
+ t.Fatalf("interfaceHandler was not called twice.")
+ }
+
+ if bogusHandlerCalled {
+ t.Fatalf("bogusHandler was called.")
+ }
+}
+
+func TestRemoveHandler(t *testing.T) {
+ testHandlerCalled := 0
+ testHandler := func(s *Session, m *MessageCreate) {
+ testHandlerCalled++
+ }
+
+ d := Session{}
+ r := d.AddHandler(testHandler)
+
+ d.handle(&MessageCreate{})
+
+ r()
+
+ d.handle(&MessageCreate{})
+
+ // testHandler will be called once, as it was removed in between calls.
+ if testHandlerCalled != 1 {
+ t.Fatalf("testHandler was not called once.")
+ }
+}
A => DiscordGo/endpoints.go +88 -0
@@ 1,88 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains variables for all known Discord end points. All functions
+// throughout the Discordgo package use these variables for all connections
+// to Discord. These are all exported and you may modify them if needed.
+
+package discordgo
+
+// Known Discord API Endpoints.
+var (
+ STATUS = "https://status.discordapp.com/api/v2/"
+ SM = STATUS + "scheduled-maintenances/"
+ SM_ACTIVE = SM + "active.json"
+ SM_UPCOMING = SM + "upcoming.json"
+
+ DISCORD = "https://discordapp.com" // TODO consider removing
+ API = DISCORD + "/api/"
+ GUILDS = API + "guilds/"
+ CHANNELS = API + "channels/"
+ USERS = API + "users/"
+ GATEWAY = API + "gateway"
+
+ AUTH = API + "auth/"
+ LOGIN = AUTH + "login"
+ LOGOUT = AUTH + "logout"
+ VERIFY = AUTH + "verify"
+ VERIFY_RESEND = AUTH + "verify/resend"
+ FORGOT_PASSWORD = AUTH + "forgot"
+ RESET_PASSWORD = AUTH + "reset"
+ REGISTER = AUTH + "register"
+
+ VOICE = API + "/voice/"
+ VOICE_REGIONS = VOICE + "regions"
+ VOICE_ICE = VOICE + "ice"
+
+ TUTORIAL = API + "tutorial/"
+ TUTORIAL_INDICATORS = TUTORIAL + "indicators"
+
+ TRACK = API + "track"
+ SSO = API + "sso"
+ REPORT = API + "report"
+ INTEGRATIONS = API + "integrations"
+
+ USER = func(uID string) string { return USERS + uID }
+ USER_AVATAR = func(uID, aID string) string { return USERS + uID + "/avatars/" + aID + ".jpg" }
+ USER_SETTINGS = func(uID string) string { return USERS + uID + "/settings" }
+ USER_GUILDS = func(uID string) string { return USERS + uID + "/guilds" }
+ USER_GUILD = func(uID, gID string) string { return USERS + uID + "/guilds/" + gID }
+ USER_CHANNELS = func(uID string) string { return USERS + uID + "/channels" }
+ USER_DEVICES = func(uID string) string { return USERS + uID + "/devices" }
+ USER_CONNECTIONS = func(uID string) string { return USERS + uID + "/connections" }
+
+ GUILD = func(gID string) string { return GUILDS + gID }
+ GUILD_INIVTES = func(gID string) string { return GUILDS + gID + "/invites" }
+ GUILD_CHANNELS = func(gID string) string { return GUILDS + gID + "/channels" }
+ GUILD_MEMBERS = func(gID string) string { return GUILDS + gID + "/members" }
+ GUILD_MEMBER = func(gID, uID string) string { return GUILDS + gID + "/members/" + uID }
+ GUILD_BANS = func(gID string) string { return GUILDS + gID + "/bans" }
+ GUILD_BAN = func(gID, uID string) string { return GUILDS + gID + "/bans/" + uID }
+ GUILD_INTEGRATIONS = func(gID string) string { return GUILDS + gID + "/integrations" }
+ GUILD_ROLES = func(gID string) string { return GUILDS + gID + "/roles" }
+ GUILD_ROLE = func(gID, rID string) string { return GUILDS + gID + "/roles/" + rID }
+ GUILD_INVITES = func(gID string) string { return GUILDS + gID + "/invites" }
+ GUILD_EMBED = func(gID string) string { return GUILDS + gID + "/embed" }
+ GUILD_PRUNE = func(gID string) string { return GUILDS + gID + "/prune" }
+ GUILD_ICON = func(gID, hash string) string { return GUILDS + gID + "/icons/" + hash + ".jpg" }
+ GUILD_SPLASH = func(gID, hash string) string { return GUILDS + gID + "/splashes/" + hash + ".jpg" }
+
+ CHANNEL = func(cID string) string { return CHANNELS + cID }
+ CHANNEL_PERMISSIONS = func(cID string) string { return CHANNELS + cID + "/permissions" }
+ CHANNEL_PERMISSION = func(cID, tID string) string { return CHANNELS + cID + "/permissions/" + tID }
+ CHANNEL_INVITES = func(cID string) string { return CHANNELS + cID + "/invites" }
+ CHANNEL_TYPING = func(cID string) string { return CHANNELS + cID + "/typing" }
+ CHANNEL_MESSAGES = func(cID string) string { return CHANNELS + cID + "/messages" }
+ CHANNEL_MESSAGE = func(cID, mID string) string { return CHANNELS + cID + "/messages/" + mID }
+ CHANNEL_MESSAGE_ACK = func(cID, mID string) string { return CHANNELS + cID + "/messages/" + mID + "/ack" }
+
+ INVITE = func(iID string) string { return API + "invite/" + iID }
+
+ INTEGRATIONS_JOIN = func(iID string) string { return API + "integrations/" + iID + "/join" }
+
+ EMOJI = func(eID string) string { return API + "emojis/" + eID + ".png" }
+)
A => DiscordGo/events.go +150 -0
@@ 1,150 @@
+package discordgo
+
+// eventToInterface is a mapping of Discord WSAPI events to their
+// DiscordGo event container.
+// Each Discord WSAPI event maps to a unique interface.
+// Use Session.AddHandler with one of these types to handle that
+// type of event.
+// eg:
+// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
+// })
+//
+// or:
+// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) {
+// })
+var eventToInterface = map[string]interface{}{
+ "CHANNEL_CREATE": ChannelCreate{},
+ "CHANNEL_UPDATE": ChannelUpdate{},
+ "CHANNEL_DELETE": ChannelDelete{},
+ "GUILD_CREATE": GuildCreate{},
+ "GUILD_UPDATE": GuildUpdate{},
+ "GUILD_DELETE": GuildDelete{},
+ "GUILD_BAN_ADD": GuildBanAdd{},
+ "GUILD_BAN_REMOVE": GuildBanRemove{},
+ "GUILD_MEMBER_ADD": GuildMemberAdd{},
+ "GUILD_MEMBER_UPDATE": GuildMemberUpdate{},
+ "GUILD_MEMBER_REMOVE": GuildMemberRemove{},
+ "GUILD_ROLE_CREATE": GuildRoleCreate{},
+ "GUILD_ROLE_UPDATE": GuildRoleUpdate{},
+ "GUILD_ROLE_DELETE": GuildRoleDelete{},
+ "GUILD_INTEGRATIONS_UPDATE": GuildIntegrationsUpdate{},
+ "GUILD_EMOJIS_UPDATE": GuildEmojisUpdate{},
+ "MESSAGE_ACK": MessageAck{},
+ "MESSAGE_CREATE": MessageCreate{},
+ "MESSAGE_UPDATE": MessageUpdate{},
+ "MESSAGE_DELETE": MessageDelete{},
+ "PRESENCE_UPDATE": PresenceUpdate{},
+ "READY": Ready{},
+ "USER_UPDATE": UserUpdate{},
+ "USER_SETTINGS_UPDATE": UserSettingsUpdate{},
+ "TYPING_START": TypingStart{},
+ "VOICE_SERVER_UPDATE": VoiceServerUpdate{},
+ "VOICE_STATE_UPDATE": VoiceStateUpdate{},
+ "MESSAGE_REACTION_ADD": ReactionRemove{},
+ "MESSAGE_REACTION_REMOVE": ReactionAdd{},
+}
+
+// ReactionRemove is an empty struct for an event.
+type ReactionRemove struct{}
+
+// ReactionAdd is an empty struct for an event.
+type ReactionAdd struct{}
+
+// Connect is an empty struct for an event.
+type Connect struct{}
+
+// Disconnect is an empty struct for an event.
+type Disconnect struct{}
+
+// MessageCreate is a wrapper struct for an event.
+type MessageCreate struct {
+ *Message
+}
+
+// MessageUpdate is a wrapper struct for an event.
+type MessageUpdate struct {
+ *Message
+}
+
+// MessageDelete is a wrapper struct for an event.
+type MessageDelete struct {
+ *Message
+}
+
+// ChannelCreate is a wrapper struct for an event.
+type ChannelCreate struct {
+ *Channel
+}
+
+// ChannelUpdate is a wrapper struct for an event.
+type ChannelUpdate struct {
+ *Channel
+}
+
+// ChannelDelete is a wrapper struct for an event.
+type ChannelDelete struct {
+ *Channel
+}
+
+// GuildCreate is a wrapper struct for an event.
+type GuildCreate struct {
+ *Guild
+}
+
+// GuildUpdate is a wrapper struct for an event.
+type GuildUpdate struct {
+ *Guild
+}
+
+// GuildDelete is a wrapper struct for an event.
+type GuildDelete struct {
+ *Guild
+}
+
+// GuildBanAdd is a wrapper struct for an event.
+type GuildBanAdd struct {
+ *GuildBan
+}
+
+// GuildBanRemove is a wrapper struct for an event.
+type GuildBanRemove struct {
+ *GuildBan
+}
+
+// GuildMemberAdd is a wrapper struct for an event.
+type GuildMemberAdd struct {
+ *Member
+}
+
+// GuildMemberUpdate is a wrapper struct for an event.
+type GuildMemberUpdate struct {
+ *Member
+}
+
+// GuildMemberRemove is a wrapper struct for an event.
+type GuildMemberRemove struct {
+ *Member
+}
+
+// GuildRoleCreate is a wrapper struct for an event.
+type GuildRoleCreate struct {
+ *GuildRole
+}
+
+// GuildRoleUpdate is a wrapper struct for an event.
+type GuildRoleUpdate struct {
+ *GuildRole
+}
+
+// VoiceStateUpdate is a wrapper struct for an event.
+type VoiceStateUpdate struct {
+ *VoiceState
+}
+
+// UserUpdate is a wrapper struct for an event.
+type UserUpdate struct {
+ *UserUpdate
+}
+
+// UserSettingsUpdate is a map for an event.
+type UserSettingsUpdate map[string]interface{}
A => DiscordGo/examples/api_basic/api_basic.go +56 -0
@@ 1,56 @@
+// This file provides a basic "quick start" example of using the Discordgo
+// package to connect to Discord using the low level API functions.
+package main
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/bwmarrin/discordgo"
+)
+
+func main() {
+
+ var err error
+
+ // Check for Username and Password CLI arguments.
+ if len(os.Args) != 3 {
+ fmt.Println("You must provide username and password as arguments. See below example.")
+ fmt.Println(os.Args[0], " [username] [password]")
+ return
+ }
+
+ // Create a new Discord Session interface and set a handler for the
+ // OnMessageCreate event that happens for every new message on any channel
+ dg := discordgo.Session{}
+
+ // Register messageCreate as a callback for the messageCreate events.
+ dg.AddHandler(messageCreate)
+
+ // Login to the Discord server and store the authentication token
+ err = dg.Login(os.Args[1], os.Args[2])
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ // Open websocket connection
+ err = dg.Open()
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ // Simple way to keep program running until any key press.
+ var input string
+ fmt.Scanln(&input)
+ return
+}
+
+// This function will be called (due to AddHandler above) every time a new
+// message is created on any channel that the autenticated user has access to.
+func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
+
+ // Print message to stdout.
+ fmt.Printf("%20s %20s %20s > %s\n", m.ChannelID, time.Now().Format(time.Stamp), m.Author.Username, m.Content)
+}
A => DiscordGo/examples/new_basic/new_basic.go +49 -0
@@ 1,49 @@
+// This file provides a basic "quick start" example of using the Discordgo
+// package to connect to Discord using the New() helper function.
+package main
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/bwmarrin/discordgo"
+)
+
+func main() {
+
+ // Check for Username and Password CLI arguments.
+ if len(os.Args) != 3 {
+ fmt.Println("You must provide username and password as arguments. See below example.")
+ fmt.Println(os.Args[0], " [username] [password]")
+ return
+ }
+
+ // Call the helper function New() passing username and password command
+ // line arguments. This returns a new Discord session, authenticates,
+ // connects to the Discord data websocket, and listens for events.
+ dg, err := discordgo.New(os.Args[1], os.Args[2])
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ // Register messageCreate as a callback for the messageCreate events.
+ dg.AddHandler(messageCreate)
+
+ // Open the websocket and begin listening.
+ dg.Open()
+
+ // Simple way to keep program running until any key press.
+ var input string
+ fmt.Scanln(&input)
+ return
+}
+
+// This function will be called (due to AddHandler above) every time a new
+// message is created on any channel that the autenticated user has access to.
+func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
+
+ // Print message to stdout.
+ fmt.Printf("%20s %20s %20s > %s\n", m.ChannelID, time.Now().Format(time.Stamp), m.Author.Username, m.Content)
+}
A => DiscordGo/message.go +82 -0
@@ 1,82 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains code related to the Message struct
+
+package discordgo
+
+import (
+ "fmt"
+ "strings"
+)
+
+// A Message stores all data related to a specific Discord message.
+type Message struct {
+ ID string `json:"id"`
+ ChannelID string `json:"channel_id"`
+ Content string `json:"content"`
+ Timestamp string `json:"timestamp"`
+ EditedTimestamp string `json:"edited_timestamp"`
+ Tts bool `json:"tts"`
+ MentionEveryone bool `json:"mention_everyone"`
+ Author *User `json:"author"`
+ Attachments []*Attachment `json:"attachments"`
+ Embeds []*Embed `json:"embeds"`
+ Mentions []*User `json:"mentions"`
+}
+
+// An Attachment stores data for message attachments.
+type Attachment struct {
+ ID string `json:"id"`
+ URL string `json:"url"`
+ ProxyURL string `json:"proxy_url"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Filename string `json:"filename"`
+ Size int `json:"size"`
+}
+
+// An Embed stores data for message embeds.
+type Embed struct {
+ URL string `json:"url"`
+ Type string `json:"type"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Thumbnail *struct {
+ URL string `json:"url"`
+ ProxyURL string `json:"proxy_url"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ } `json:"thumbnail"`
+ Provider *struct {
+ URL string `json:"url"`
+ Name string `json:"name"`
+ } `json:"provider"`
+ Author *struct {
+ URL string `json:"url"`
+ Name string `json:"name"`
+ } `json:"author"`
+ Video *struct {
+ URL string `json:"url"`
+ Width int `json:"width"`
+ Height int `json:"height"`
+ } `json:"video"`
+}
+
+// ContentWithMentionsReplaced will replace all @<id> mentions with the
+// username of the mention.
+func (m *Message) ContentWithMentionsReplaced() string {
+ if m.Mentions == nil {
+ return m.Content
+ }
+ content := m.Content
+ for _, user := range m.Mentions {
+ content = strings.Replace(content, fmt.Sprintf("<@%s>", user.ID),
+ fmt.Sprintf("@%s", user.Username), -1)
+ }
+ return content
+}
A => DiscordGo/restapi.go +1060 -0
@@ 1,1060 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains functions for interacting with the Discord REST/JSON API
+// at the lowest level.
+
+package discordgo
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "image"
+ _ "image/jpeg" // For JPEG decoding
+ _ "image/png" // For PNG decoding
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "strconv"
+ "time"
+)
+
+// ErrJSONUnmarshal is returned for JSON Unmarshall errors.
+var ErrJSONUnmarshal = errors.New("json unmarshal")
+
+// Request makes a (GET/POST/...) Requests to Discord REST API with JSON data.
+// All the other Discord REST Calls in this file use this function.
+func (s *Session) Request(method, urlStr string, data interface{}) (response []byte, err error) {
+
+ if s.Debug {
+ fmt.Println("API REQUEST PAYLOAD :: [" + fmt.Sprintf("%+v", data) + "]")
+ }
+
+ var body []byte
+ if data != nil {
+ body, err = json.Marshal(data)
+ if err != nil {
+ return
+ }
+ }
+
+ return s.request(method, urlStr, "application/json", body)
+}
+
+// request makes a (GET/POST/...) Requests to Discord REST API.
+func (s *Session) request(method, urlStr, contentType string, b []byte) (response []byte, err error) {
+
+ if s.Debug {
+ fmt.Printf("API REQUEST %8s :: %s\n", method, urlStr)
+ }
+
+ req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(b))
+ if err != nil {
+ return
+ }
+
+ // Not used on initial login..
+ // TODO: Verify if a login, otherwise complain about no-token
+ if s.Token != "" {
+ req.Header.Set("authorization", s.Token)
+ }
+
+ req.Header.Set("Content-Type", contentType)
+ // TODO: Make a configurable static variable.
+ req.Header.Set("User-Agent", fmt.Sprintf("DiscordBot (https://github.com/bwmarrin/discordgo, v%s)", VERSION))
+
+ if s.Debug {
+ for k, v := range req.Header {
+ fmt.Printf("API REQUEST HEADER :: [%s] = %+v\n", k, v)
+ }
+ }
+
+ client := &http.Client{Timeout: (20 * time.Second)}
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return
+ }
+ defer func() {
+ err := resp.Body.Close()
+ if err != nil {
+ fmt.Println("error closing resp body")
+ }
+ }()
+
+ response, err = ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return
+ }
+
+ if s.Debug {
+
+ fmt.Printf("API RESPONSE STATUS :: %s\n", resp.Status)
+ for k, v := range resp.Header {
+ fmt.Printf("API RESPONSE HEADER :: [%s] = %+v\n", k, v)
+ }
+ fmt.Printf("API RESPONSE BODY :: [%s]\n", response)
+ }
+
+ // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+ switch resp.StatusCode {
+
+ case 200: // OK
+ case 204: // No Content
+
+ // TODO check for 401 response, invalidate token if we get one.
+
+ case 429: // TOO MANY REQUESTS - Rate limiting
+ rl := RateLimit{}
+ err = json.Unmarshal(response, &rl)
+ if err != nil {
+ err = fmt.Errorf("Request unmarshal rate limit error : %+v", err)
+ return
+ }
+ time.Sleep(rl.RetryAfter)
+ response, err = s.request(method, urlStr, contentType, b)
+
+ default: // Error condition
+ err = fmt.Errorf("HTTP %s, %s", resp.Status, response)
+ }
+
+ return
+}
+
+func unmarshal(data []byte, v interface{}) error {
+ err := json.Unmarshal(data, v)
+ if err != nil {
+ return ErrJSONUnmarshal
+ }
+
+ return nil
+}
+
+// ------------------------------------------------------------------------------------------------
+// Functions specific to Discord Sessions
+// ------------------------------------------------------------------------------------------------
+
+// Login asks the Discord server for an authentication token.
+func (s *Session) Login(email, password string) (err error) {
+
+ data := struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+ }{email, password}
+
+ response, err := s.Request("POST", LOGIN, data)
+ if err != nil {
+ return
+ }
+
+ temp := struct {
+ Token string `json:"token"`
+ }{}
+
+ err = unmarshal(response, &temp)
+ if err != nil {
+ return
+ }
+
+ s.Token = temp.Token
+ return
+}
+
+// Register sends a Register request to Discord, and returns the authentication token
+// Note that this account is temporary and should be verified for future use.
+// Another option is to save the authentication token external, but this isn't recommended.
+func (s *Session) Register(username string) (token string, err error) {
+
+ data := struct {
+ Username string `json:"username"`
+ }{username}
+
+ response, err := s.Request("POST", REGISTER, data)
+ if err != nil {
+ return
+ }
+
+ temp := struct {
+ Token string `json:"token"`
+ }{}
+
+ err = unmarshal(response, &temp)
+ if err != nil {
+ return
+ }
+
+ token = temp.Token
+ return
+}
+
+// Logout sends a logout request to Discord.
+// This does not seem to actually invalidate the token. So you can still
+// make API calls even after a Logout. So, it seems almost pointless to
+// even use.
+func (s *Session) Logout() (err error) {
+
+ // _, err = s.Request("POST", LOGOUT, fmt.Sprintf(`{"token": "%s"}`, s.Token))
+
+ if s.Token == "" {
+ return
+ }
+
+ data := struct {
+ Token string `json:"token"`
+ }{s.Token}
+
+ _, err = s.Request("POST", LOGOUT, data)
+ return
+}
+
+// ------------------------------------------------------------------------------------------------
+// Functions specific to Discord Users
+// ------------------------------------------------------------------------------------------------
+
+// User returns the user details of the given userID
+// userID : A user ID or "@me" which is a shortcut of current user ID
+func (s *Session) User(userID string) (st *User, err error) {
+
+ body, err := s.Request("GET", USER(userID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// UserAvatar returns an image.Image of a users Avatar.
+// userID : A user ID or "@me" which is a shortcut of current user ID
+func (s *Session) UserAvatar(userID string) (img image.Image, err error) {
+ u, err := s.User(userID)
+ if err != nil {
+ return
+ }
+
+ body, err := s.Request("GET", USER_AVATAR(userID, u.Avatar), nil)
+ if err != nil {
+ return
+ }
+
+ img, _, err = image.Decode(bytes.NewReader(body))
+ return
+}
+
+// UserUpdate updates a users settings.
+func (s *Session) UserUpdate(email, password, username, avatar, newPassword string) (st *User, err error) {
+
+ // NOTE: Avatar must be either the hash/id of existing Avatar or
+ // data:image/png;base64,BASE64_STRING_OF_NEW_AVATAR_PNG
+ // to set a new avatar.
+ // If left blank, avatar will be set to null/blank
+
+ data := struct {
+ Email string `json:"email"`
+ Password string `json:"password"`
+ Username string `json:"username"`
+ Avatar string `json:"avatar,omitempty"`
+ NewPassword string `json:"new_password,omitempty"`
+ }{email, password, username, avatar, newPassword}
+
+ body, err := s.Request("PATCH", USER("@me"), data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// UserSettings returns the settings for a given user
+func (s *Session) UserSettings() (st *Settings, err error) {
+
+ body, err := s.Request("GET", USER_SETTINGS("@me"), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// UserChannels returns an array of Channel structures for all private
+// channels.
+func (s *Session) UserChannels() (st []*Channel, err error) {
+
+ body, err := s.Request("GET", USER_CHANNELS("@me"), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// UserChannelCreate creates a new User (Private) Channel with another User
+// recipientID : A user ID for the user to which this channel is opened with.
+func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) {
+
+ data := struct {
+ RecipientID string `json:"recipient_id"`
+ }{recipientID}
+
+ body, err := s.Request("POST", USER_CHANNELS("@me"), data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// UserGuilds returns an array of Guild structures for all guilds.
+func (s *Session) UserGuilds() (st []*Guild, err error) {
+
+ body, err := s.Request("GET", USER_GUILDS("@me"), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// ------------------------------------------------------------------------------------------------
+// Functions specific to Discord Guilds
+// ------------------------------------------------------------------------------------------------
+
+// Guild returns a Guild structure of a specific Guild.
+// guildID : The ID of a Guild
+func (s *Session) Guild(guildID string) (st *Guild, err error) {
+ if s.StateEnabled {
+ // Attempt to grab the guild from State first.
+ st, err = s.State.Guild(guildID)
+ if err == nil {
+ return
+ }
+ }
+
+ body, err := s.Request("GET", GUILD(guildID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// GuildCreate creates a new Guild
+// name : A name for the Guild (2-100 characters)
+func (s *Session) GuildCreate(name string) (st *Guild, err error) {
+
+ data := struct {
+ Name string `json:"name"`
+ }{name}
+
+ body, err := s.Request("POST", GUILDS, data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// GuildEdit edits a new Guild
+// guildID : The ID of a Guild
+// name : A name for the Guild (2-100 characters)
+func (s *Session) GuildEdit(guildID, name string) (st *Guild, err error) {
+
+ data := struct {
+ Name string `json:"name"`
+ }{name}
+
+ body, err := s.Request("POST", GUILD(guildID), data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// GuildDelete deletes a Guild.
+// guildID : The ID of a Guild
+func (s *Session) GuildDelete(guildID string) (st *Guild, err error) {
+
+ body, err := s.Request("DELETE", GUILD(guildID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// GuildLeave leaves a Guild.
+// guildID : The ID of a Guild
+func (s *Session) GuildLeave(guildID string) (err error) {
+
+ _, err = s.Request("DELETE", USER_GUILD("@me", guildID), nil)
+ return
+}
+
+// GuildBans returns an array of User structures for all bans of a
+// given guild.
+// guildID : The ID of a Guild.
+func (s *Session) GuildBans(guildID string) (st []*User, err error) {
+
+ body, err := s.Request("GET", GUILD_BANS(guildID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+
+ return
+}
+
+// GuildBanCreate bans the given user from the given guild.
+// guildID : The ID of a Guild.
+// userID : The ID of a User
+// days : The number of days of previous comments to delete.
+func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) {
+
+ uri := GUILD_BAN(guildID, userID)
+
+ if days > 0 {
+ uri = fmt.Sprintf("%s?delete-message-days=%d", uri, days)
+ }
+
+ _, err = s.Request("PUT", uri, nil)
+ return
+}
+
+// GuildBanDelete removes the given user from the guild bans
+// guildID : The ID of a Guild.
+// userID : The ID of a User
+func (s *Session) GuildBanDelete(guildID, userID string) (err error) {
+
+ _, err = s.Request("DELETE", GUILD_BAN(guildID, userID), nil)
+ return
+}
+
+// GuildMembers returns a list of members for a guild.
+// guildID : The ID of a Guild.
+// offset : A number of members to skip
+// limit : max number of members to return
+func (s *Session) GuildMembers(guildID string, offset, limit int) (st []*Member, err error) {
+
+ uri := GUILD_MEMBERS(guildID)
+
+ v := url.Values{}
+
+ if offset > 0 {
+ v.Set("offset", strconv.Itoa(offset))
+ }
+
+ if limit > 0 {
+ v.Set("limit", strconv.Itoa(limit))
+ }
+
+ if len(v) > 0 {
+ uri = fmt.Sprintf("%s?%s", uri, v.Encode())
+ }
+
+ body, err := s.Request("GET", uri, nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// GuildMember returns a member of a guild.
+// guildID : The ID of a Guild.
+// userID : The ID of a User
+func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) {
+
+ body, err := s.Request("GET", GUILD_MEMBER(guildID, userID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// GuildMemberDelete removes the given user from the given guild.
+// guildID : The ID of a Guild.
+// userID : The ID of a User
+func (s *Session) GuildMemberDelete(guildID, userID string) (err error) {
+
+ _, err = s.Request("DELETE", GUILD_MEMBER(guildID, userID), nil)
+ return
+}
+
+// GuildMemberEdit edits the roles of a member.
+// guildID : The ID of a Guild.
+// userID : The ID of a User.
+// roles : A list of role ID's to set on the member.
+func (s *Session) GuildMemberEdit(guildID, userID string, roles []string) (err error) {
+
+ data := struct {
+ Roles []string `json:"roles"`
+ }{roles}
+
+ _, err = s.Request("PATCH", GUILD_MEMBER(guildID, userID), data)
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+// GuildMemberMove moves a guild member from one voice channel to another/none
+// guildID : The ID of a Guild.
+// userID : The ID of a User.
+// channelID : The ID of a channel to move user to, or null?
+// NOTE : I am not entirely set on the name of this function and it may change
+// prior to the final 1.0.0 release of Discordgo
+func (s *Session) GuildMemberMove(guildID, userID, channelID string) (err error) {
+
+ data := struct {
+ ChannelID string `json:"channel_id"`
+ }{channelID}
+
+ _, err = s.Request("PATCH", GUILD_MEMBER(guildID, userID), data)
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+// GuildChannels returns an array of Channel structures for all channels of a
+// given guild.
+// guildID : The ID of a Guild.
+func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) {
+
+ body, err := s.Request("GET", GUILD_CHANNELS(guildID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+
+ return
+}
+
+// GuildChannelCreate creates a new channel in the given guild
+// guildID : The ID of a Guild.
+// name : Name of the channel (2-100 chars length)
+// ctype : Tpye of the channel (voice or text)
+func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, err error) {
+
+ data := struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ }{name, ctype}
+
+ body, err := s.Request("POST", GUILD_CHANNELS(guildID), data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// GuildInvites returns an array of Invite structures for the given guild
+// guildID : The ID of a Guild.
+func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) {
+ body, err := s.Request("GET", GUILD_INVITES(guildID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// GuildRoles returns all roles for a given guild.
+// guildID : The ID of a Guild.
+func (s *Session) GuildRoles(guildID string) (st []*Role, err error) {
+
+ body, err := s.Request("GET", GUILD_ROLES(guildID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+
+ return // TODO return pointer
+}
+
+// GuildRoleCreate returns a new Guild Role.
+// guildID: The ID of a Guild.
+func (s *Session) GuildRoleCreate(guildID string) (st *Role, err error) {
+
+ body, err := s.Request("POST", GUILD_ROLES(guildID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+
+ return
+}
+
+// GuildRoleEdit updates an existing Guild Role with new values
+// guildID : The ID of a Guild.
+// roleID : The ID of a Role.
+// name : The name of the Role.
+// color : The color of the role (decimal, not hex).
+// hoist : Whether to display the role's users separately.
+// perm : The permissions for the role.
+func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist bool, perm int) (st *Role, err error) {
+
+ data := struct {
+ Name string `json:"name"` // The color the role should have (as a decimal, not hex)
+ Color int `json:"color"` // Whether to display the role's users separately
+ Hoist bool `json:"hoist"` // The role's name (overwrites existing)
+ Permissions int `json:"permissions"` // The overall permissions number of the role (overwrites existing)
+ }{name, color, hoist, perm}
+
+ body, err := s.Request("PATCH", GUILD_ROLE(guildID, roleID), data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+
+ return
+}
+
+// GuildRoleReorder reoders guild roles
+// guildID : The ID of a Guild.
+// roles : A list of ordered roles.
+func (s *Session) GuildRoleReorder(guildID string, roles []*Role) (st []*Role, err error) {
+
+ body, err := s.Request("PATCH", GUILD_ROLES(guildID), roles)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+
+ return
+}
+
+// GuildRoleDelete deletes an existing role.
+// guildID : The ID of a Guild.
+// roleID : The ID of a Role.
+func (s *Session) GuildRoleDelete(guildID, roleID string) (err error) {
+
+ _, err = s.Request("DELETE", GUILD_ROLE(guildID, roleID), nil)
+
+ return
+}
+
+// GuildIcon returns an image.Image of a guild icon.
+// guildID : The ID of a Guild.
+func (s *Session) GuildIcon(guildID string) (img image.Image, err error) {
+ g, err := s.Guild(guildID)
+ if err != nil {
+ return
+ }
+
+ if g.Icon == "" {
+ err = errors.New("Guild does not have an icon set.")
+ return
+ }
+
+ body, err := s.Request("GET", GUILD_ICON(guildID, g.Icon), nil)
+ if err != nil {
+ return
+ }
+
+ img, _, err = image.Decode(bytes.NewReader(body))
+ return
+}
+
+// GuildSplash returns an image.Image of a guild splash image.
+// guildID : The ID of a Guild.
+func (s *Session) GuildSplash(guildID string) (img image.Image, err error) {
+ g, err := s.Guild(guildID)
+ if err != nil {
+ return
+ }
+
+ if g.Splash == "" {
+ err = errors.New("Guild does not have a splash set.")
+ return
+ }
+
+ body, err := s.Request("GET", GUILD_SPLASH(guildID, g.Splash), nil)
+ if err != nil {
+ return
+ }
+
+ img, _, err = image.Decode(bytes.NewReader(body))
+ return
+}
+
+// ------------------------------------------------------------------------------------------------
+// Functions specific to Discord Channels
+// ------------------------------------------------------------------------------------------------
+
+// Channel returns a Channel strucutre of a specific Channel.
+// channelID : The ID of the Channel you want returned.
+func (s *Session) Channel(channelID string) (st *Channel, err error) {
+ body, err := s.Request("GET", CHANNEL(channelID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// ChannelEdit edits the given channel
+// channelID : The ID of a Channel
+// name : The new name to assign the channel.
+func (s *Session) ChannelEdit(channelID, name string) (st *Channel, err error) {
+
+ data := struct {
+ Name string `json:"name"`
+ }{name}
+
+ body, err := s.Request("PATCH", CHANNEL(channelID), data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// ChannelDelete deletes the given channel
+// channelID : The ID of a Channel
+func (s *Session) ChannelDelete(channelID string) (st *Channel, err error) {
+
+ body, err := s.Request("DELETE", CHANNEL(channelID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// ChannelTyping broadcasts to all members that authenticated user is typing in
+// the given channel.
+// channelID : The ID of a Channel
+func (s *Session) ChannelTyping(channelID string) (err error) {
+
+ _, err = s.Request("POST", CHANNEL_TYPING(channelID), nil)
+ return
+}
+
+// ChannelMessages returns an array of Message structures for messages within
+// a given channel.
+// channelID : The ID of a Channel.
+// limit : The number messages that can be returned.
+// beforeID : If provided all messages returned will be before given ID.
+// afterID : If provided all messages returned will be after given ID.
+func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID string) (st []*Message, err error) {
+
+ uri := CHANNEL_MESSAGES(channelID)
+
+ v := url.Values{}
+ if limit > 0 {
+ v.Set("limit", strconv.Itoa(limit))
+ }
+ if afterID != "" {
+ v.Set("after", afterID)
+ }
+ if beforeID != "" {
+ v.Set("before", beforeID)
+ }
+ if len(v) > 0 {
+ uri = fmt.Sprintf("%s?%s", uri, v.Encode())
+ }
+
+ body, err := s.Request("GET", uri, nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// ChannelMessageAck acknowledges and marks the given message as read
+// channeld : The ID of a Channel
+// messageID : the ID of a Message
+func (s *Session) ChannelMessageAck(channelID, messageID string) (err error) {
+
+ _, err = s.Request("POST", CHANNEL_MESSAGE_ACK(channelID, messageID), nil)
+ return
+}
+
+// channelMessageSend sends a message to the given channel.
+// channelID : The ID of a Channel.
+// content : The message to send.
+// tts : Whether to send the message with TTS.
+func (s *Session) channelMessageSend(channelID, content string, tts bool) (st *Message, err error) {
+
+ // TODO: nonce string ?
+ data := struct {
+ Content string `json:"content"`
+ TTS bool `json:"tts"`
+ }{content, tts}
+
+ // Send the message to the given channel
+ response, err := s.Request("POST", CHANNEL_MESSAGES(channelID), data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(response, &st)
+ return
+}
+
+// ChannelMessageSend sends a message to the given channel.
+// channelID : The ID of a Channel.
+// content : The message to send.
+func (s *Session) ChannelMessageSend(channelID string, content string) (st *Message, err error) {
+
+ return s.channelMessageSend(channelID, content, false)
+}
+
+// ChannelMessageSendTTS sends a message to the given channel with Text to Speech.
+// channelID : The ID of a Channel.
+// content : The message to send.
+func (s *Session) ChannelMessageSendTTS(channelID string, content string) (st *Message, err error) {
+
+ return s.channelMessageSend(channelID, content, true)
+}
+
+// ChannelMessageEdit edits an existing message, replacing it entirely with
+// the given content.
+// channeld : The ID of a Channel
+// messageID : the ID of a Message
+func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st *Message, err error) {
+
+ data := struct {
+ Content string `json:"content"`
+ }{content}
+
+ response, err := s.Request("PATCH", CHANNEL_MESSAGE(channelID, messageID), data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(response, &st)
+ return
+}
+
+// ChannelMessageDelete deletes a message from the Channel.
+func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error) {
+
+ _, err = s.Request("DELETE", CHANNEL_MESSAGE(channelID, messageID), nil)
+ return
+}
+
+// ChannelFileSend sends a file to the given channel.
+// channelID : The ID of a Channel.
+// io.Reader : A reader for the file contents.
+func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (st *Message, err error) {
+
+ body := &bytes.Buffer{}
+ bodywriter := multipart.NewWriter(body)
+
+ writer, err := bodywriter.CreateFormFile("file", name)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = io.Copy(writer, r)
+ if err != nil {
+ return
+ }
+
+ err = bodywriter.Close()
+ if err != nil {
+ return
+ }
+
+ response, err := s.request("POST", CHANNEL_MESSAGES(channelID), bodywriter.FormDataContentType(), body.Bytes())
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(response, &st)
+ return
+}
+
+// ChannelInvites returns an array of Invite structures for the given channel
+// channelID : The ID of a Channel
+func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) {
+
+ body, err := s.Request("GET", CHANNEL_INVITES(channelID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// ChannelInviteCreate creates a new invite for the given channel.
+// channelID : The ID of a Channel
+// i : An Invite struct with the values MaxAge, MaxUses, Temporary,
+// and XkcdPass defined.
+func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) {
+
+ data := struct {
+ MaxAge int `json:"max_age"`
+ MaxUses int `json:"max_uses"`
+ Temporary bool `json:"temporary"`
+ XKCDPass bool `json:"xkcdpass"`
+ }{i.MaxAge, i.MaxUses, i.Temporary, i.XkcdPass}
+
+ body, err := s.Request("POST", CHANNEL_INVITES(channelID), data)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// ChannelPermissionSet creates a Permission Override for the given channel.
+// NOTE: This func name may changed. Using Set instead of Create because
+// you can both create a new override or update an override with this function.
+func (s *Session) ChannelPermissionSet(channelID, targetID, targetType string, allow, deny int) (err error) {
+
+ data := struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ Allow int `json:"allow"`
+ Deny int `json:"deny"`
+ }{targetID, targetType, allow, deny}
+
+ _, err = s.Request("PUT", CHANNEL_PERMISSION(channelID, targetID), data)
+ return
+}
+
+// ChannelPermissionDelete deletes a specific permission override for the given channel.
+// NOTE: Name of this func may change.
+func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error) {
+
+ _, err = s.Request("DELETE", CHANNEL_PERMISSION(channelID, targetID), nil)
+ return
+}
+
+// ------------------------------------------------------------------------------------------------
+// Functions specific to Discord Invites
+// ------------------------------------------------------------------------------------------------
+
+// Invite returns an Invite structure of the given invite
+// inviteID : The invite code (or maybe xkcdpass?)
+func (s *Session) Invite(inviteID string) (st *Invite, err error) {
+
+ body, err := s.Request("GET", INVITE(inviteID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// InviteDelete deletes an existing invite
+// inviteID : the code (or maybe xkcdpass?) of an invite
+func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) {
+
+ body, err := s.Request("DELETE", INVITE(inviteID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// InviteAccept accepts an Invite to a Guild or Channel
+// inviteID : The invite code (or maybe xkcdpass?)
+func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) {
+
+ body, err := s.Request("POST", INVITE(inviteID), nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// ------------------------------------------------------------------------------------------------
+// Functions specific to Discord Voice
+// ------------------------------------------------------------------------------------------------
+
+// VoiceRegions returns the voice server regions
+func (s *Session) VoiceRegions() (st []*VoiceRegion, err error) {
+
+ body, err := s.Request("GET", VOICE_REGIONS, nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// VoiceICE returns the voice server ICE information
+func (s *Session) VoiceICE() (st *VoiceICE, err error) {
+
+ body, err := s.Request("GET", VOICE_ICE, nil)
+ if err != nil {
+ return
+ }
+
+ err = unmarshal(body, &st)
+ return
+}
+
+// ------------------------------------------------------------------------------------------------
+// Functions specific to Discord Websockets
+// ------------------------------------------------------------------------------------------------
+
+// Gateway returns the a websocket Gateway address
+func (s *Session) Gateway() (gateway string, err error) {
+
+ response, err := s.Request("GET", GATEWAY, nil)
+ if err != nil {
+ return
+ }
+
+ temp := struct {
+ URL string `json:"url"`
+ }{}
+
+ err = unmarshal(response, &temp)
+ if err != nil {
+ return
+ }
+
+ gateway = temp.URL
+ return
+}
A => DiscordGo/restapi_test.go +151 -0
@@ 1,151 @@
+package discordgo
+
+import (
+ "testing"
+)
+
+//////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////// START OF TESTS
+
+// TestChannelMessageSend tests the ChannelMessageSend() function. This should not return an error.
+func TestChannelMessageSend(t *testing.T) {
+
+ if envChannel == "" {
+ t.Skip("Skipping, DG_CHANNEL not set.")
+ }
+
+ if dg == nil {
+ t.Skip("Skipping, dg not set.")
+ }
+
+ _, err := dg.ChannelMessageSend(envChannel, "Running REST API Tests!")
+ if err != nil {
+ t.Errorf("ChannelMessageSend returned error: %+v", err)
+ }
+}
+
+func TestUserAvatar(t *testing.T) {
+ if dg == nil {
+ t.Skip("Cannot TestUserAvatar, dg not set.")
+ }
+
+ a, err := dg.UserAvatar("@me")
+ if err != nil {
+ if err.Error() == `HTTP 404 NOT FOUND, {"message": ""}` {
+ t.Skip("Skipped, @me doesn't have an Avatar")
+ }
+ t.Errorf(err.Error())
+ }
+
+ if a == nil {
+ t.Errorf("a == nil, should be image.Image")
+ }
+}
+
+func TestUserUpdate(t *testing.T) {
+ if dg == nil {
+ t.Skip("Cannot test logout, dg not set.")
+ }
+
+ u, err := dg.User("@me")
+ if err != nil {
+ t.Errorf(err.Error())
+ }
+
+ s, err := dg.UserUpdate(envEmail, envPassword, "testname", u.Avatar, "")
+ if err != nil {
+ t.Error(err.Error())
+ }
+ if s.Username != "testname" {
+ t.Error("Username != testname")
+ }
+ s, err = dg.UserUpdate(envEmail, envPassword, u.Username, u.Avatar, "")
+ if err != nil {
+ t.Error(err.Error())
+ }
+ if s.Username != u.Username {
+ t.Error("Username != " + u.Username)
+ }
+}
+
+//func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) {
+
+func TestUserChannelCreate(t *testing.T) {
+ if dg == nil {
+ t.Skip("Cannot TestUserChannelCreate, dg not set.")
+ }
+
+ if envAdmin == "" {
+ t.Skip("Skipped, DG_ADMIN not set.")
+ }
+
+ _, err := dg.UserChannelCreate(envAdmin)
+ if err != nil {
+ t.Errorf(err.Error())
+ }
+
+ // TODO make sure the channel was added
+}
+
+func TestUserChannels(t *testing.T) {
+ if dg == nil {
+ t.Skip("Cannot TestUserChannels, dg not set.")
+ }
+
+ _, err := dg.UserChannels()
+ if err != nil {
+ t.Errorf(err.Error())
+ }
+}
+
+func TestUserGuilds(t *testing.T) {
+ if dg == nil {
+ t.Skip("Cannot TestUserGuilds, dg not set.")
+ }
+
+ _, err := dg.UserGuilds()
+ if err != nil {
+ t.Errorf(err.Error())
+ }
+}
+
+func TestUserSettings(t *testing.T) {
+ if dg == nil {
+ t.Skip("Cannot TestUserSettings, dg not set.")
+ }
+
+ _, err := dg.UserSettings()
+ if err != nil {
+ t.Errorf(err.Error())
+ }
+}
+
+// TestLogout tests the Logout() function. This should not return an error.
+func TestLogout(t *testing.T) {
+
+ if dg == nil {
+ t.Skip("Cannot TestLogout, dg not set.")
+ }
+
+ err := dg.Logout()
+ if err != nil {
+ t.Errorf("Logout() returned error: %+v", err)
+ }
+}
+
+// TestChannelMessageSend2 tests the ChannelMessageSend() function. This should not return an error.
+func TestChannelMessageSend2(t *testing.T) {
+
+ if envChannel == "" {
+ t.Skip("Skipping, DG_CHANNEL not set.")
+ }
+
+ if dg == nil {
+ t.Skip("Skipping, dg not set.")
+ }
+
+ _, err := dg.ChannelMessageSend(envChannel, "All done running REST API Tests!")
+ if err != nil {
+ t.Errorf("ChannelMessageSend returned error: %+v", err)
+ }
+}
A => DiscordGo/state.go +520 -0
@@ 1,520 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains code related to state tracking. If enabled, state
+// tracking will capture the initial READY packet and many other websocket
+// events and maintain an in-memory state of of guilds, channels, users, and
+// so forth. This information can be accessed through the Session.State struct.
+
+package discordgo
+
+import (
+ "errors"
+ "fmt"
+)
+
+// ErrNilState is returned when the state is nil.
+var ErrNilState = errors.New("State not instantiated, please use discordgo.New() or assign Session.State.")
+
+// NewState creates an empty state.
+func NewState() *State {
+ return &State{
+ Ready: Ready{
+ PrivateChannels: []*Channel{},
+ Guilds: []*Guild{},
+ },
+ }
+}
+
+// OnReady takes a Ready event and updates all internal state.
+func (s *State) OnReady(r *Ready) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ s.Ready = *r
+ return nil
+}
+
+// GuildAdd adds a guild to the current world state, or
+// updates it if it already exists.
+func (s *State) GuildAdd(guild *Guild) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ // If the guild exists, replace it.
+ for i, g := range s.Guilds {
+ if g.ID == guild.ID {
+ // Don't stomp on properties that don't come in updates.
+ guild.Members = g.Members
+ guild.Presences = g.Presences
+ guild.Channels = g.Channels
+ guild.VoiceStates = g.VoiceStates
+ s.Guilds[i] = guild
+ return nil
+ }
+ }
+
+ s.Guilds = append(s.Guilds, guild)
+ return nil
+}
+
+// GuildRemove removes a guild from current world state.
+func (s *State) GuildRemove(guild *Guild) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ for i, g := range s.Guilds {
+ if g.ID == guild.ID {
+ s.Guilds = append(s.Guilds[:i], s.Guilds[i+1:]...)
+ return nil
+ }
+ }
+
+ return errors.New("Guild not found.")
+}
+
+// Guild gets a guild by ID.
+// Useful for querying if @me is in a guild:
+// _, err := discordgo.Session.State.Guild(guildID)
+// isInGuild := err == nil
+func (s *State) Guild(guildID string) (*Guild, error) {
+ if s == nil {
+ return nil, ErrNilState
+ }
+
+ s.RLock()
+ defer s.RUnlock()
+
+ for _, g := range s.Guilds {
+ if g.ID == guildID {
+ return g, nil
+ }
+ }
+
+ return nil, errors.New("Guild not found.")
+}
+
+// TODO: Consider moving Guild state update methods onto *Guild.
+
+// MemberAdd adds a member to the current world state, or
+// updates it if it already exists.
+func (s *State) MemberAdd(member *Member) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ guild, err := s.Guild(member.GuildID)
+ if err != nil {
+ return err
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ for i, m := range guild.Members {
+ if m.User.ID == member.User.ID {
+ guild.Members[i] = member
+ return nil
+ }
+ }
+
+ guild.Members = append(guild.Members, member)
+ return nil
+}
+
+// MemberRemove removes a member from current world state.
+func (s *State) MemberRemove(member *Member) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ guild, err := s.Guild(member.GuildID)
+ if err != nil {
+ return err
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ for i, m := range guild.Members {
+ if m.User.ID == member.User.ID {
+ guild.Members = append(guild.Members[:i], guild.Members[i+1:]...)
+ return nil
+ }
+ }
+
+ return errors.New("Member not found.")
+}
+
+// Member gets a member by ID from a guild.
+func (s *State) Member(guildID, userID string) (*Member, error) {
+ if s == nil {
+ return nil, ErrNilState
+ }
+
+ guild, err := s.Guild(guildID)
+ if err != nil {
+ return nil, err
+ }
+
+ s.RLock()
+ defer s.RUnlock()
+
+ for _, m := range guild.Members {
+ if m.User.ID == userID {
+ return m, nil
+ }
+ }
+
+ return nil, errors.New("Member not found.")
+}
+
+// ChannelAdd adds a guild to the current world state, or
+// updates it if it already exists.
+// Channels may exist either as PrivateChannels or inside
+// a guild.
+func (s *State) ChannelAdd(channel *Channel) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ if channel.IsPrivate {
+ s.Lock()
+ defer s.Unlock()
+
+ // If the channel exists, replace it.
+ for i, c := range s.PrivateChannels {
+ if c.ID == channel.ID {
+ // Don't stomp on messages.
+ channel.Messages = c.Messages
+ s.PrivateChannels[i] = channel
+ return nil
+ }
+ }
+
+ s.PrivateChannels = append(s.PrivateChannels, channel)
+ } else {
+ guild, err := s.Guild(channel.GuildID)
+ if err != nil {
+ return err
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ // If the channel exists, replace it.
+ for i, c := range guild.Channels {
+ if c.ID == channel.ID {
+ // Don't stomp on messages.
+ channel.Messages = c.Messages
+ guild.Channels[i] = channel
+ return nil
+ }
+ }
+
+ guild.Channels = append(guild.Channels, channel)
+ }
+
+ return nil
+}
+
+// ChannelRemove removes a channel from current world state.
+func (s *State) ChannelRemove(channel *Channel) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ if channel.IsPrivate {
+ s.Lock()
+ defer s.Unlock()
+
+ for i, c := range s.PrivateChannels {
+ if c.ID == channel.ID {
+ s.PrivateChannels = append(s.PrivateChannels[:i], s.PrivateChannels[i+1:]...)
+ return nil
+ }
+ }
+ } else {
+ guild, err := s.Guild(channel.GuildID)
+ if err != nil {
+ return err
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ for i, c := range guild.Channels {
+ if c.ID == channel.ID {
+ guild.Channels = append(guild.Channels[:i], guild.Channels[i+1:]...)
+ return nil
+ }
+ }
+ }
+
+ return errors.New("Channel not found.")
+}
+
+// GuildChannel gets a channel by ID from a guild.
+func (s *State) GuildChannel(guildID, channelID string) (*Channel, error) {
+ if s == nil {
+ return nil, ErrNilState
+ }
+
+ guild, err := s.Guild(guildID)
+ if err != nil {
+ return nil, err
+ }
+
+ s.RLock()
+ defer s.RUnlock()
+
+ for _, c := range guild.Channels {
+ if c.ID == channelID {
+ return c, nil
+ }
+ }
+
+ return nil, errors.New("Channel not found.")
+}
+
+// PrivateChannel gets a private channel by ID.
+func (s *State) PrivateChannel(channelID string) (*Channel, error) {
+ if s == nil {
+ return nil, ErrNilState
+ }
+
+ s.RLock()
+ defer s.RUnlock()
+
+ for _, c := range s.PrivateChannels {
+ if c.ID == channelID {
+ return c, nil
+ }
+ }
+
+ return nil, errors.New("Channel not found.")
+}
+
+// Channel gets a channel by ID, it will look in all guilds an private channels.
+func (s *State) Channel(channelID string) (*Channel, error) {
+ if s == nil {
+ return nil, ErrNilState
+ }
+
+ c, err := s.PrivateChannel(channelID)
+ if err == nil {
+ return c, nil
+ }
+
+ for _, g := range s.Guilds {
+ c, err := s.GuildChannel(g.ID, channelID)
+ if err == nil {
+ return c, nil
+ }
+ }
+
+ return nil, errors.New("Channel not found.")
+}
+
+// Emoji returns an emoji for a guild and emoji id.
+func (s *State) Emoji(guildID, emojiID string) (*Emoji, error) {
+ if s == nil {
+ return nil, ErrNilState
+ }
+
+ guild, err := s.Guild(guildID)
+ if err != nil {
+ return nil, err
+ }
+
+ s.RLock()
+ defer s.RUnlock()
+
+ for _, e := range guild.Emojis {
+ if e.ID == emojiID {
+ return e, nil
+ }
+ }
+
+ return nil, errors.New("Emoji not found.")
+}
+
+// EmojiAdd adds an emoji to the current world state.
+func (s *State) EmojiAdd(guildID string, emoji *Emoji) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ guild, err := s.Guild(guildID)
+ if err != nil {
+ return err
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ for i, e := range guild.Emojis {
+ if e.ID == emoji.ID {
+ guild.Emojis[i] = emoji
+ return nil
+ }
+ }
+
+ guild.Emojis = append(guild.Emojis, emoji)
+ return nil
+}
+
+// EmojisAdd adds multiple emojis to the world state.
+func (s *State) EmojisAdd(guildID string, emojis []*Emoji) error {
+ for _, e := range emojis {
+ if err := s.EmojiAdd(guildID, e); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// MessageAdd adds a message to the current world state, or updates it if it exists.
+// If the channel cannot be found, the message is discarded.
+// Messages are kept in state up to s.MaxMessageCount
+func (s *State) MessageAdd(message *Message) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ c, err := s.Channel(message.ChannelID)
+ if err != nil {
+ return err
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ // If the message exists, replace it.
+ for i, m := range c.Messages {
+ if m.ID == message.ID {
+ c.Messages[i] = message
+ return nil
+ }
+ }
+
+ c.Messages = append(c.Messages, message)
+
+ if len(c.Messages) > s.MaxMessageCount {
+ s.Unlock()
+ for len(c.Messages) > s.MaxMessageCount {
+ err := s.MessageRemove(c.Messages[0])
+ if err != nil {
+ fmt.Println("message remove error: ", err)
+ }
+ }
+ s.Lock()
+ }
+ return nil
+}
+
+// MessageRemove removes a message from the world state.
+func (s *State) MessageRemove(message *Message) error {
+ if s == nil {
+ return ErrNilState
+ }
+
+ c, err := s.Channel(message.ChannelID)
+ if err != nil {
+ return err
+ }
+
+ s.Lock()
+ defer s.Unlock()
+
+ for i, m := range c.Messages {
+ if m.ID == message.ID {
+ c.Messages = append(c.Messages[:i], c.Messages[i+1:]...)
+ return nil
+ }
+ }
+
+ return errors.New("Message not found.")
+}
+
+// Message gets a message by channel and message ID.
+func (s *State) Message(channelID, messageID string) (*Message, error) {
+ if s == nil {
+ return nil, ErrNilState
+ }
+
+ c, err := s.Channel(channelID)
+ if err != nil {
+ return nil, err
+ }
+
+ s.RLock()
+ defer s.RUnlock()
+
+ for _, m := range c.Messages {
+ if m.ID == messageID {
+ return m, nil
+ }
+ }
+
+ return nil, errors.New("Message not found.")
+}
+
+// onInterface handles all events related to states.
+func (s *State) onInterface(se *Session, i interface{}) (err error) {
+ if s == nil {
+ return ErrNilState
+ }
+ if !se.StateEnabled {
+ return nil
+ }
+
+ switch t := i.(type) {
+ case *Ready:
+ err = s.OnReady(t)
+ case *GuildCreate:
+ err = s.GuildAdd(t.Guild)
+ case *GuildUpdate:
+ err = s.GuildAdd(t.Guild)
+ case *GuildDelete:
+ err = s.GuildRemove(t.Guild)
+ case *GuildMemberAdd:
+ err = s.MemberAdd(t.Member)
+ case *GuildMemberUpdate:
+ err = s.MemberAdd(t.Member)
+ case *GuildMemberRemove:
+ err = s.MemberRemove(t.Member)
+ case *GuildEmojisUpdate:
+ err = s.EmojisAdd(t.GuildID, t.Emojis)
+ case *ChannelCreate:
+ err = s.ChannelAdd(t.Channel)
+ case *ChannelUpdate:
+ err = s.ChannelAdd(t.Channel)
+ case *ChannelDelete:
+ err = s.ChannelRemove(t.Channel)
+ case *MessageCreate:
+ err = s.MessageAdd(t.Message)
+ case *MessageUpdate:
+ err = s.MessageAdd(t.Message)
+ case *MessageDelete:
+ err = s.MessageRemove(t.Message)
+ }
+
+ return
+}
A => DiscordGo/structs.go +341 -0
@@ 1,341 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains all structures for the discordgo package. These
+// may be moved about later into separate files but I find it easier to have
+// them all located together.
+
+package discordgo
+
+import (
+ "encoding/json"
+ "reflect"
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+// A Session represents a connection to the Discord API.
+type Session struct {
+ sync.RWMutex
+
+ // General configurable settings.
+
+ // Authentication token for this session
+ Token string
+
+ // Debug for printing JSON request/responses
+ Debug bool
+
+ // Should the session reconnect the websocket on errors.
+ ShouldReconnectOnError bool
+
+ // Should the session request compressed websocket data.
+ Compress bool
+
+ // Should state tracking be enabled.
+ // State tracking is the best way for getting the the users
+ // active guilds and the members of the guilds.
+ StateEnabled bool
+
+ // Exposed but should not be modified by User.
+
+ // Whether the Data Websocket is ready
+ DataReady bool
+
+ // Whether the Voice Websocket is ready
+ VoiceReady bool
+
+ // Whether the UDP Connection is ready
+ UDPReady bool
+
+ // Stores all details related to voice connections
+ Voice *Voice
+
+ // Managed state object, updated internally with events when
+ // StateEnabled is true.
+ State *State
+
+ handlersMu sync.RWMutex
+ // This is a mapping of event struct to a reflected value
+ // for event handlers.
+ // We store the reflected value instead of the function
+ // reference as it is more performant, instead of re-reflecting
+ // the function each event.
+ handlers map[interface{}][]reflect.Value
+
+ // The websocket connection.
+ wsConn *websocket.Conn
+
+ // When nil, the session is not listening.
+ listening chan interface{}
+}
+
+// A VoiceRegion stores data for a specific voice region server.
+type VoiceRegion struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Hostname string `json:"sample_hostname"`
+ Port int `json:"sample_port"`
+}
+
+// A VoiceICE stores data for voice ICE servers.
+type VoiceICE struct {
+ TTL string `json:"ttl"`
+ Servers []*ICEServer `json:"servers"`
+}
+
+// A ICEServer stores data for a specific voice ICE server.
+type ICEServer struct {
+ URL string `json:"url"`
+ Username string `json:"username"`
+ Credential string `json:"credential"`
+}
+
+// A Invite stores all data related to a specific Discord Guild or Channel invite.
+type Invite struct {
+ Guild *Guild `json:"guild"`
+ Channel *Channel `json:"channel"`
+ Inviter *User `json:"inviter"`
+ Code string `json:"code"`
+ CreatedAt string `json:"created_at"` // TODO make timestamp
+ MaxAge int `json:"max_age"`
+ Uses int `json:"uses"`
+ MaxUses int `json:"max_uses"`
+ XkcdPass bool `json:"xkcdpass"`
+ Revoked bool `json:"revoked"`
+ Temporary bool `json:"temporary"`
+}
+
+// A Channel holds all data related to an individual Discord channel.
+type Channel struct {
+ ID string `json:"id"`
+ GuildID string `json:"guild_id"`
+ Name string `json:"name"`
+ Topic string `json:"topic"`
+ Position int `json:"position"`
+ Bitrate int `json:"bitrate"`
+ Type string `json:"type"`
+ PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"`
+ IsPrivate bool `json:"is_private"`
+ LastMessageID string `json:"last_message_id"`
+ Recipient *User `json:"recipient"`
+ Messages []*Message `json:"-"`
+}
+
+// A PermissionOverwrite holds permission overwrite data for a Channel
+type PermissionOverwrite struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ Deny int `json:"deny"`
+ Allow int `json:"allow"`
+}
+
+// Emoji struct holds data related to Emoji's
+type Emoji struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Roles []string `json:"roles"`
+ Managed bool `json:"managed"`
+ RequireColons bool `json:"require_colons"`
+}
+
+// A Guild holds all data related to a specific Discord Guild. Guilds are also
+// sometimes referred to as Servers in the Discord client.
+type Guild struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Icon string `json:"icon"`
+ Region string `json:"region"`
+ AfkChannelID string `json:"afk_channel_id"`
+ EmbedChannelID string `json:"embed_channel_id"`
+ OwnerID string `json:"owner_id"`
+ JoinedAt string `json:"joined_at"` // make this a timestamp
+ Splash string `json:"splash"`
+ AfkTimeout int `json:"afk_timeout"`
+ EmbedEnabled bool `json:"embed_enabled"`
+ Large bool `json:"large"` // ??
+ Roles []*Role `json:"roles"`
+ Emojis []*Emoji `json:"emojis"`
+ Members []*Member `json:"members"`
+ Presences []*Presence `json:"presences"`
+ Channels []*Channel `json:"channels"`
+ VoiceStates []*VoiceState `json:"voice_states"`
+}
+
+// A Role stores information about Discord guild member roles.
+type Role struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Managed bool `json:"managed"`
+ Hoist bool `json:"hoist"`
+ Color int `json:"color"`
+ Position int `json:"position"`
+ Permissions int `json:"permissions"`
+}
+
+// A VoiceState stores the voice states of Guilds
+type VoiceState struct {
+ UserID string `json:"user_id"`
+ SessionID string `json:"session_id"`
+ ChannelID string `json:"channel_id"`
+ Suppress bool `json:"suppress"`
+ SelfMute bool `json:"self_mute"`
+ SelfDeaf bool `json:"self_deaf"`
+ Mute bool `json:"mute"`
+ Deaf bool `json:"deaf"`
+}
+
+// A Presence stores the online, offline, or idle and game status of Guild members.
+type Presence struct {
+ User *User `json:"user"`
+ Status string `json:"status"`
+ Game *Game `json:"game"`
+}
+
+// A Game struct holds the name of the "playing .." game for a user
+type Game struct {
+ Name string `json:"name"`
+}
+
+// A Member stores user information for Guild members.
+type Member struct {
+ GuildID string `json:"guild_id"`
+ JoinedAt string `json:"joined_at"`
+ Deaf bool `json:"deaf"`
+ Mute bool `json:"mute"`
+ User *User `json:"user"`
+ Roles []string `json:"roles"`
+}
+
+// A User stores all data for an individual Discord user.
+type User struct {
+ ID string `json:"id"`
+ Email string `json:"email"`
+ Username string `json:"username"`
+ Avatar string `json:"Avatar"`
+ Verified bool `json:"verified"`
+ //Discriminator int `json:"discriminator,string"` // TODO: See below
+}
+
+// TODO: Research issue.
+// Discriminator sometimes comes as a string
+// and sometimes it comes as a int. Weird.
+// to avoid errors I've just commented it out
+// but it doesn't seem to just kill the whole
+// program. Heartbeat is taken on READY even
+// with error and the system continues to read
+// it just doesn't seem able to handle this one
+// field correctly. Need to research this more.
+
+// A Settings stores data for a specific users Discord client settings.
+type Settings struct {
+ RenderEmbeds bool `json:"render_embeds"`
+ InlineEmbedMedia bool `json:"inline_embed_media"`
+ EnableTtsCommand bool `json:"enable_tts_command"`
+ MessageDisplayCompact bool `json:"message_display_compact"`
+ ShowCurrentGame bool `json:"show_current_game"`
+ Locale string `json:"locale"`
+ Theme string `json:"theme"`
+ MutedChannels []string `json:"muted_channels"`
+}
+
+// An Event provides a basic initial struct for all websocket event.
+type Event struct {
+ Type string `json:"t"`
+ State int `json:"s"`
+ Operation int `json:"op"`
+ Direction int `json:"dir"`
+ RawData json.RawMessage `json:"d"`
+}
+
+// A Ready stores all data for the websocket READY event.
+type Ready struct {
+ Version int `json:"v"`
+ SessionID string `json:"session_id"`
+ HeartbeatInterval time.Duration `json:"heartbeat_interval"`
+ User *User `json:"user"`
+ ReadState []*ReadState `json:"read_state"`
+ PrivateChannels []*Channel `json:"private_channels"`
+ Guilds []*Guild `json:"guilds"`
+}
+
+// A RateLimit struct holds information related to a specific rate limit.
+type RateLimit struct {
+ Bucket string `json:"bucket"`
+ Message string `json:"message"`
+ RetryAfter time.Duration `json:"retry_after"`
+}
+
+// A ReadState stores data on the read state of channels.
+type ReadState struct {
+ MentionCount int
+ LastMessageID string `json:"last_message_id"`
+ ID string `json:"id"`
+}
+
+// A TypingStart stores data for the typing start websocket event.
+type TypingStart struct {
+ UserID string `json:"user_id"`
+ ChannelID string `json:"channel_id"`
+ Timestamp int `json:"timestamp"`
+}
+
+// A PresenceUpdate stores data for the pressence update websocket event.
+type PresenceUpdate struct {
+ User *User `json:"user"`
+ Status string `json:"status"`
+ Roles []string `json:"roles"`
+ GuildID string `json:"guild_id"`
+ Game *Game `json:"game"`
+}
+
+// A MessageAck stores data for the message ack websocket event.
+type MessageAck struct {
+ MessageID string `json:"message_id"`
+ ChannelID string `json:"channel_id"`
+}
+
+// A GuildIntegrationsUpdate stores data for the guild integrations update
+// websocket event.
+type GuildIntegrationsUpdate struct {
+ GuildID string `json:"guild_id"`
+}
+
+// A GuildRole stores data for guild role websocket events.
+type GuildRole struct {
+ Role *Role `json:"role"`
+ GuildID string `json:"guild_id"`
+}
+
+// A GuildRoleDelete stores data for the guild role delete websocket event.
+type GuildRoleDelete struct {
+ RoleID string `json:"role_id"`
+ GuildID string `json:"guild_id"`
+}
+
+// A GuildBan stores data for a guild ban.
+type GuildBan struct {
+ User *User `json:"user"`
+ GuildID string `json:"guild_id"`
+}
+
+// A GuildEmojisUpdate stores data for a guild emoji update event.
+type GuildEmojisUpdate struct {
+ GuildID string `json:"guild_id"`
+ Emojis []*Emoji `json:"emojis"`
+}
+
+// A State contains the current known state.
+// As discord sends this in a READY blob, it seems reasonable to simply
+// use that struct as the data store.
+type State struct {
+ sync.RWMutex
+ Ready
+ MaxMessageCount int
+}
A => DiscordGo/util.go +34 -0
@@ 1,34 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains utility functions for the discordgo package. These
+// functions are not exported and are likely to change substantially in
+// the future to match specific needs of the discordgo package itself.
+
+package discordgo
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+)
+
+// printEvent prints out a WSAPI event.
+func printEvent(e *Event) {
+ //fmt.Println(fmt.Sprintf("Event. Type: %s, State: %d Operation: %d Direction: %d", e.Type, e.State, e.Operation, e.Direction))
+ //printJSON(e.RawData)
+}
+
+// printJSON is a helper function to display JSON data in a easy to read format.
+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(string(prettyJSON.Bytes()))
+}
A => DiscordGo/voice.go +590 -0
@@ 1,590 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains code related to Discord voice suppport
+
+package discordgo
+
+import (
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "net"
+ "runtime"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+// ------------------------------------------------------------------------------------------------
+// Code related to both Voice Websocket and UDP connections.
+// ------------------------------------------------------------------------------------------------
+
+// A Voice struct holds all data and functions related to Discord Voice support.
+type Voice struct {
+ sync.Mutex // future use
+ Ready bool // If true, voice is ready to send/receive audio
+ Debug bool // If true, print extra logging
+ OP2 *voiceOP2 // exported for dgvoice, may change.
+ OpusSend chan []byte // Chan for sending opus audio
+ OpusRecv chan *Packet // Chan for receiving opus audio
+ // FrameRate int // This can be used to set the FrameRate of Opus data
+ // FrameSize int // This can be used to set the FrameSize of Opus data
+
+ wsConn *websocket.Conn
+ UDPConn *net.UDPConn // this will become unexported soon.
+
+ sessionID string
+ token string
+ endpoint string
+ guildID string
+ channelID string
+ userID string
+
+ // Used to send a close signal to goroutines
+ close chan struct{}
+}
+
+// ------------------------------------------------------------------------------------------------
+// Code related to the Voice websocket connection
+// ------------------------------------------------------------------------------------------------
+
+// A voiceOP2 stores the data for the voice operation 2 websocket event
+// which is sort of like the voice READY packet
+type voiceOP2 struct {
+ SSRC uint32 `json:"ssrc"`
+ Port int `json:"port"`
+ Modes []string `json:"modes"`
+ HeartbeatInterval time.Duration `json:"heartbeat_interval"`
+}
+
+type voiceHandshakeData struct {
+ ServerID string `json:"server_id"`
+ UserID string `json:"user_id"`
+ SessionID string `json:"session_id"`
+ Token string `json:"token"`
+}
+
+type voiceHandshakeOp struct {
+ Op int `json:"op"` // Always 0
+ Data voiceHandshakeData `json:"d"`
+}
+
+// Open opens a voice connection. This should be called
+// after VoiceChannelJoin is used and the data VOICE websocket events
+// are captured.
+func (v *Voice) Open() (err error) {
+
+ v.Lock()
+ defer v.Unlock()
+
+ // Don't open a websocket if one is already open
+ if v.wsConn != nil {
+ return
+ }
+
+ // Connect to Voice Websocket
+ vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80"))
+ v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil)
+ if err != nil {
+ fmt.Println("VOICE error opening websocket:", err)
+ return
+ }
+
+ data := voiceHandshakeOp{0, voiceHandshakeData{v.guildID, v.userID, v.sessionID, v.token}}
+
+ err = v.wsConn.WriteJSON(data)
+ if err != nil {
+ fmt.Println("VOICE error sending init packet:", err)
+ return
+ }
+
+ // Start a listening for voice websocket events
+ // TODO add a check here to make sure Listen worked by monitoring
+ // a chan or bool?
+ v.close = make(chan struct{})
+ go v.wsListen(v.wsConn, v.close)
+
+ return
+}
+
+// wsListen listens on the voice websocket for messages and passes them
+// to the voice event handler. This is automatically called by the Open func
+func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) {
+
+ for {
+ messageType, message, err := v.wsConn.ReadMessage()
+ if err != nil {
+ // TODO: add reconnect, matching wsapi.go:listen()
+ // TODO: Handle this problem better.
+ // TODO: needs proper logging
+ fmt.Println("Voice Listen Error:", err)
+ return
+ }
+
+ // Pass received message to voice event handler
+ select {
+ case <-close:
+ return
+ default:
+ go v.wsEvent(messageType, message)
+ }
+ }
+}
+
+// wsEvent handles any voice websocket events. This is only called by the
+// wsListen() function.
+func (v *Voice) wsEvent(messageType int, message []byte) {
+
+ if v.Debug {
+ fmt.Println("wsEvent received: ", messageType)
+ printJSON(message)
+ }
+
+ var e Event
+ if err := json.Unmarshal(message, &e); err != nil {
+ fmt.Println("wsEvent Unmarshall error: ", err)
+ return
+ }
+
+ switch e.Operation {
+
+ case 2: // READY
+
+ v.OP2 = &voiceOP2{}
+ if err := json.Unmarshal(e.RawData, v.OP2); err != nil {
+ fmt.Println("voiceWS.onEvent OP2 Unmarshall error: ", err)
+ printJSON(e.RawData) // TODO: Better error logging
+ return
+ }
+
+ // Start the voice websocket heartbeat to keep the connection alive
+ go v.wsHeartbeat(v.wsConn, v.close, v.OP2.HeartbeatInterval)
+ // TODO monitor a chan/bool to verify this was successful
+
+ // Start the UDP connection
+ err := v.udpOpen()
+ if err != nil {
+ fmt.Println("Error opening udp connection: ", err)
+ return
+ }
+
+ // Start the opusSender.
+ // TODO: Should we allow 48000/960 values to be user defined?
+ if v.OpusSend == nil {
+ v.OpusSend = make(chan []byte, 2)
+ }
+ go v.opusSender(v.UDPConn, v.close, v.OpusSend, 48000, 960)
+
+ // Start the opusReceiver
+ if v.OpusRecv == nil {
+ v.OpusRecv = make(chan *Packet, 2)
+ }
+ go v.opusReceiver(v.UDPConn, v.close, v.OpusRecv)
+ return
+
+ case 3: // HEARTBEAT response
+ // add code to use this to track latency?
+ return
+
+ case 4:
+ // TODO
+
+ case 5:
+ // SPEAKING TRUE/FALSE NOTIFICATION
+ /*
+ {
+ "user_id": "1238921738912",
+ "ssrc": 2,
+ "speaking": false
+ }
+ */
+
+ default:
+ fmt.Println("UNKNOWN VOICE OP: ", e.Operation)
+ printJSON(e.RawData)
+ }
+
+ return
+}
+
+type voiceHeartbeatOp struct {
+ Op int `json:"op"` // Always 3
+ Data int `json:"d"`
+}
+
+// NOTE :: When a guild voice server changes how do we shut this down
+// properly, so a new connection can be setup without fuss?
+//
+// wsHeartbeat sends regular heartbeats to voice Discord so it knows the client
+// is still connected. If you do not send these heartbeats Discord will
+// disconnect the websocket connection after a few seconds.
+func (v *Voice) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i time.Duration) {
+
+ if close == nil || wsConn == nil {
+ return
+ }
+
+ var err error
+ ticker := time.NewTicker(i * time.Millisecond)
+ for {
+ err = wsConn.WriteJSON(voiceHeartbeatOp{3, int(time.Now().Unix())})
+ if err != nil {
+ fmt.Println("wsHeartbeat send error: ", err)
+ return
+ }
+
+ select {
+ case <-ticker.C:
+ // continue loop and send heartbeat
+ case <-close:
+ return
+ }
+ }
+}
+
+type voiceSpeakingData struct {
+ Speaking bool `json:"speaking"`
+ Delay int `json:"delay"`
+}
+
+type voiceSpeakingOp struct {
+ Op int `json:"op"` // Always 5
+ Data voiceSpeakingData `json:"d"`
+}
+
+// Speaking sends a speaking notification to Discord over the voice websocket.
+// This must be sent as true prior to sending audio and should be set to false
+// once finished sending audio.
+// b : Send true if speaking, false if not.
+func (v *Voice) Speaking(b bool) (err error) {
+
+ if v.wsConn == nil {
+ return fmt.Errorf("No Voice websocket.")
+ }
+
+ data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}}
+ err = v.wsConn.WriteJSON(data)
+ if err != nil {
+ fmt.Println("Speaking() write json error:", err)
+ return
+ }
+
+ return
+}
+
+// ------------------------------------------------------------------------------------------------
+// Code related to the Voice UDP connection
+// ------------------------------------------------------------------------------------------------
+
+type voiceUDPData struct {
+ Address string `json:"address"` // Public IP of machine running this code
+ Port uint16 `json:"port"` // UDP Port of machine running this code
+ Mode string `json:"mode"` // plain or ? (plain or encrypted)
+}
+
+type voiceUDPD struct {
+ Protocol string `json:"protocol"` // Always "udp" ?
+ Data voiceUDPData `json:"data"`
+}
+
+type voiceUDPOp struct {
+ Op int `json:"op"` // Always 1
+ Data voiceUDPD `json:"d"`
+}
+
+// udpOpen opens a UDP connection to the voice server and completes the
+// initial required handshake. This connection is left open in the session
+// and can be used to send or receive audio. This should only be called
+// from voice.wsEvent OP2
+func (v *Voice) udpOpen() (err error) {
+
+ v.Lock()
+ defer v.Unlock()
+
+ if v.wsConn == nil {
+ return fmt.Errorf("nil voice websocket")
+ }
+
+ if v.UDPConn != nil {
+ return fmt.Errorf("udp connection already open")
+ }
+
+ if v.close == nil {
+ return fmt.Errorf("nil close channel")
+ }
+
+ if v.endpoint == "" {
+ return fmt.Errorf("empty endpoint")
+ }
+
+ host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.OP2.Port)
+ addr, err := net.ResolveUDPAddr("udp", host)
+ if err != nil {
+ fmt.Println("udpOpen resolve addr error: ", err)
+ // TODO better logging
+ return
+ }
+
+ v.UDPConn, err = net.DialUDP("udp", nil, addr)
+ if err != nil {
+ fmt.Println("udpOpen dial udp error: ", err)
+ // TODO better logging
+ return
+ }
+
+ // Create a 70 byte array and put the SSRC code from the Op 2 Voice event
+ // into it. Then send that over the UDP connection to Discord
+ sb := make([]byte, 70)
+ binary.BigEndian.PutUint32(sb, v.OP2.SSRC)
+ _, err = v.UDPConn.Write(sb)
+ if err != nil {
+ fmt.Println("udpOpen udp write error : ", err)
+ // TODO better logging
+ return
+ }
+
+ // Create a 70 byte array and listen for the initial handshake response
+ // from Discord. Once we get it parse the IP and PORT information out
+ // of the response. This should be our public IP and PORT as Discord
+ // saw us.
+ rb := make([]byte, 70)
+ rlen, _, err := v.UDPConn.ReadFromUDP(rb)
+ if err != nil {
+ fmt.Println("udpOpen udp read error : ", err)
+ // TODO better logging
+ return
+ }
+ if rlen < 70 {
+ fmt.Println("Voice RLEN should be 70 but isn't")
+ }
+
+ // Loop over position 4 though 20 to grab the IP address
+ // Should never be beyond position 20.
+ var ip string
+ for i := 4; i < 20; i++ {
+ if rb[i] == 0 {
+ break
+ }
+ ip += string(rb[i])
+ }
+
+ // Grab port from position 68 and 69
+ port := binary.LittleEndian.Uint16(rb[68:70])
+
+ // Take the data from above and send it back to Discord to finalize
+ // the UDP connection handshake.
+ data := voiceUDPOp{1, voiceUDPD{"udp", voiceUDPData{ip, port, "plain"}}}
+
+ err = v.wsConn.WriteJSON(data)
+ if err != nil {
+ fmt.Println("udpOpen write json error:", err)
+ return
+ }
+
+ // start udpKeepAlive
+ go v.udpKeepAlive(v.UDPConn, v.close, 5*time.Second)
+ // TODO: find a way to check that it fired off okay
+
+ return
+}
+
+// udpKeepAlive sends a udp packet to keep the udp connection open
+// This is still a bit of a "proof of concept"
+func (v *Voice) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time.Duration) {
+
+ if UDPConn == nil || close == nil {
+ return
+ }
+
+ var err error
+ var sequence uint64
+
+ packet := make([]byte, 8)
+
+ ticker := time.NewTicker(i)
+ for {
+
+ binary.LittleEndian.PutUint64(packet, sequence)
+ sequence++
+
+ _, err = UDPConn.Write(packet)
+ if err != nil {
+ fmt.Println("udpKeepAlive udp write error : ", err)
+ return
+ }
+
+ select {
+ case <-ticker.C:
+ // continue loop and send keepalive
+ case <-close:
+ return
+ }
+ }
+}
+
+// opusSender will listen on the given channel and send any
+// pre-encoded opus audio to Discord. Supposedly.
+func (v *Voice) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-chan []byte, rate, size int) {
+
+ if UDPConn == nil || close == nil {
+ return
+ }
+
+ runtime.LockOSThread()
+
+ // Voice is now ready to receive audio packets
+ // TODO: this needs reviewed as I think there must be a better way.
+ v.Ready = true
+ defer func() { v.Ready = false }()
+
+ var sequence uint16
+ var timestamp uint32
+ var recvbuf []byte
+ var ok bool
+ udpHeader := make([]byte, 12)
+
+ // build the parts that don't change in the udpHeader
+ udpHeader[0] = 0x80
+ udpHeader[1] = 0x78
+ binary.BigEndian.PutUint32(udpHeader[8:], v.OP2.SSRC)
+
+ // start a send loop that loops until buf chan is closed
+ ticker := time.NewTicker(time.Millisecond * time.Duration(size/(rate/1000)))
+ for {
+
+ // Get data from chan. If chan is closed, return.
+ select {
+ case <-close:
+ return
+ case recvbuf, ok = <-opus:
+ if !ok {
+ return
+ }
+ // else, continue loop
+ }
+
+ // Add sequence and timestamp to udpPacket
+ binary.BigEndian.PutUint16(udpHeader[2:], sequence)
+ binary.BigEndian.PutUint32(udpHeader[4:], timestamp)
+
+ // Combine the UDP Header and the opus data
+ sendbuf := append(udpHeader, recvbuf...)
+
+ // block here until we're exactly at the right time :)
+ // Then send rtp audio packet to Discord over UDP
+ select {
+ case <-close:
+ return
+ case <-ticker.C:
+ // continue
+ }
+ _, err := UDPConn.Write(sendbuf)
+
+ if err != nil {
+ fmt.Println("error writing to udp connection: ", err)
+ return
+ }
+
+ if (sequence) == 0xFFFF {
+ sequence = 0
+ } else {
+ sequence++
+ }
+
+ if (timestamp + uint32(size)) >= 0xFFFFFFFF {
+ timestamp = 0
+ } else {
+ timestamp += uint32(size)
+ }
+ }
+}
+
+// A Packet contains the headers and content of a received voice packet.
+type Packet struct {
+ SSRC uint32
+ Sequence uint16
+ Timestamp uint32
+ Type []byte
+ Opus []byte
+ PCM []int16
+}
+
+// opusReceiver listens on the UDP socket for incoming packets
+// and sends them across the given channel
+// NOTE :: This function may change names later.
+func (v *Voice) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan *Packet) {
+
+ if UDPConn == nil || close == nil {
+ return
+ }
+
+ p := Packet{}
+ recvbuf := make([]byte, 1024)
+
+ for {
+ rlen, err := UDPConn.Read(recvbuf)
+ if err != nil {
+ fmt.Println("opusReceiver UDP Read error:", err)
+ return
+ }
+
+ select {
+ case <-close:
+ return
+ default:
+ // continue loop
+ }
+
+ // For now, skip anything except audio.
+ if rlen < 12 || recvbuf[0] != 0x80 {
+ continue
+ }
+
+ p.Type = recvbuf[0:2]
+ p.Sequence = binary.BigEndian.Uint16(recvbuf[2:4])
+ p.Timestamp = binary.BigEndian.Uint32(recvbuf[4:8])
+ p.SSRC = binary.BigEndian.Uint32(recvbuf[8:12])
+ p.Opus = recvbuf[12:rlen]
+
+ if c != nil {
+ c <- &p
+ }
+ }
+}
+
+// Close closes the voice ws and udp connections
+func (v *Voice) Close() {
+
+ v.Lock()
+ defer v.Unlock()
+
+ v.Ready = false
+
+ if v.close != nil {
+ close(v.close)
+ v.close = nil
+ }
+
+ if v.UDPConn != nil {
+ err := v.UDPConn.Close()
+ if err != nil {
+ fmt.Println("error closing udp connection: ", err)
+ }
+ v.UDPConn = nil
+ }
+
+ if v.wsConn != nil {
+ err := v.wsConn.Close()
+ if err != nil {
+ fmt.Println("error closing websocket connection: ", err)
+ }
+ v.wsConn = nil
+ }
+}
A => DiscordGo/wsapi.go +434 -0
@@ 1,434 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains low level functions for interacting with the Discord
+// data websocket interface.
+
+package discordgo
+
+import (
+ "bytes"
+ "compress/zlib"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "reflect"
+ "runtime"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+type handshakeProperties struct {
+ OS string `json:"$os"`
+ Browser string `json:"$browser"`
+ Device string `json:"$device"`
+ Referer string `json:"$referer"`
+ ReferringDomain string `json:"$referring_domain"`
+}
+
+type handshakeData struct {
+ Version int `json:"v"`
+ Token string `json:"token"`
+ Properties handshakeProperties `json:"properties"`
+ LargeThreshold int `json:"large_threshold"`
+ Compress bool `json:"compress"`
+}
+
+type handshakeOp struct {
+ Op int `json:"op"`
+ Data handshakeData `json:"d"`
+}
+
+// Open opens a websocket connection to Discord.
+func (s *Session) Open() (err error) {
+ s.Lock()
+ defer func() {
+ if err != nil {
+ s.Unlock()
+ }
+ }()
+
+ if s.wsConn != nil {
+ err = errors.New("Web socket already opened.")
+ return
+ }
+
+ // Get the gateway to use for the Websocket connection
+ g, err := s.Gateway()
+ if err != nil {
+ return
+ }
+
+ header := http.Header{}
+ header.Add("accept-encoding", "zlib")
+
+ // TODO: See if there's a use for the http response.
+ // conn, response, err := websocket.DefaultDialer.Dial(session.Gateway, nil)
+ s.wsConn, _, err = websocket.DefaultDialer.Dial(g, header)
+ if err != nil {
+ return
+ }
+
+ err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{3, s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}, 250, s.Compress}})
+ if err != nil {
+ return
+ }
+
+ // Create listening outside of listen, as it needs to happen inside the mutex
+ // lock.
+ s.listening = make(chan interface{})
+ go s.listen(s.wsConn, s.listening)
+
+ s.Unlock()
+
+ s.initialize()
+ s.handle(&Connect{})
+
+ return
+}
+
+// Close closes a websocket and stops all listening/heartbeat goroutines.
+// TODO: Add support for Voice WS/UDP connections
+func (s *Session) Close() (err error) {
+ s.Lock()
+
+ s.DataReady = false
+
+ if s.listening != nil {
+ close(s.listening)
+ s.listening = nil
+ }
+
+ if s.wsConn != nil {
+ err = s.wsConn.Close()
+ s.wsConn = nil
+ }
+
+ s.Unlock()
+
+ s.handle(&Disconnect{})
+
+ return
+}
+
+// listen polls the websocket connection for events, it will stop when
+// the listening channel is closed, or an error occurs.
+func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) {
+ for {
+ messageType, message, err := wsConn.ReadMessage()
+ if err != nil {
+ // Detect if we have been closed manually. If a Close() has already
+ // happened, the websocket we are listening on will be different to the
+ // current session.
+ s.RLock()
+ sameConnection := s.wsConn == wsConn
+ s.RUnlock()
+ if sameConnection {
+ // There has been an error reading, Close() the websocket so that
+ // OnDisconnect is fired.
+ err := s.Close()
+ if err != nil {
+ fmt.Println("error closing session connection: ", err)
+ }
+
+ // Attempt to reconnect, with expenonential backoff up to 10 minutes.
+ if s.ShouldReconnectOnError {
+ wait := time.Duration(1)
+ for {
+ if s.Open() == nil {
+ return
+ }
+ <-time.After(wait * time.Second)
+ wait *= 2
+ if wait > 600 {
+ wait = 600
+ }
+ }
+ }
+ }
+ return
+ }
+
+ select {
+ case <-listening:
+ return
+ default:
+ go s.event(messageType, message)
+ }
+ }
+}
+
+type heartbeatOp struct {
+ Op int `json:"op"`
+ Data int `json:"d"`
+}
+
+// heartbeat sends regular heartbeats to Discord so it knows the client
+// is still connected. If you do not send these heartbeats Discord will
+// disconnect the websocket connection after a few seconds.
+func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) {
+
+ if listening == nil || wsConn == nil {
+ return
+ }
+
+ s.Lock()
+ s.DataReady = true
+ s.Unlock()
+
+ var err error
+ ticker := time.NewTicker(i * time.Millisecond)
+ for {
+ err = wsConn.WriteJSON(heartbeatOp{1, int(time.Now().Unix())})
+ if err != nil {
+ fmt.Println("Error sending heartbeat:", err)
+ return
+ }
+
+ select {
+ case <-ticker.C:
+ // continue loop and send heartbeat
+ case <-listening:
+ return
+ }
+ }
+}
+
+type updateStatusGame struct {
+ Name string `json:"name"`
+}
+
+type updateStatusData struct {
+ IdleSince *int `json:"idle_since"`
+ Game *updateStatusGame `json:"game"`
+}
+
+type updateStatusOp struct {
+ Op int `json:"op"`
+ Data updateStatusData `json:"d"`
+}
+
+// UpdateStatus is used to update the authenticated user's status.
+// If idle>0 then set status to idle. If game>0 then set game.
+// if otherwise, set status to active, and no game.
+func (s *Session) UpdateStatus(idle int, game string) (err error) {
+ s.RLock()
+ defer s.RUnlock()
+ if s.wsConn == nil {
+ return errors.New("No websocket connection exists.")
+ }
+
+ var usd updateStatusData
+ if idle > 0 {
+ usd.IdleSince = &idle
+ }
+ if game != "" {
+ usd.Game = &updateStatusGame{game}
+ }
+
+ err = s.wsConn.WriteJSON(updateStatusOp{3, usd})
+
+ return
+}
+
+// Front line handler for all Websocket Events. Determines the
+// event type and passes the message along to the next handler.
+
+// event is the front line handler for all events. This needs to be
+// broken up into smaller functions to be more idiomatic Go.
+// Events will be handled by any implemented handler in Session.
+// All unhandled events will then be handled by OnEvent.
+func (s *Session) event(messageType int, message []byte) {
+ var err error
+ var reader io.Reader
+ reader = bytes.NewBuffer(message)
+
+ if messageType == 2 {
+ z, err1 := zlib.NewReader(reader)
+ if err1 != nil {
+ fmt.Println(err1)
+ return
+ }
+ defer func() {
+ err := z.Close()
+ if err != nil {
+ fmt.Println("error closing zlib:", err)
+ }
+ }()
+ reader = z
+ }
+
+ var e *Event
+ decoder := json.NewDecoder(reader)
+ if err = decoder.Decode(&e); err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ if s.Debug {
+ printEvent(e)
+ }
+
+ i := eventToInterface[e.Type]
+ if i != nil {
+ // Create a new instance of the event type.
+ i = reflect.New(reflect.TypeOf(i)).Interface()
+
+ // Attempt to unmarshal our event.
+ // If there is an error we should handle the event itself.
+ if err = unmarshal(e.RawData, i); err != nil {
+ fmt.Println("Unable to unmarshal event data.")
+ i = e
+ }
+ } else {
+ //fmt.Println("Unknown event.")
+ i = e
+ }
+
+ s.handle(i)
+
+ return
+}
+
+// ------------------------------------------------------------------------------------------------
+// Code related to voice connections that initiate over the data websocket
+// ------------------------------------------------------------------------------------------------
+
+// A VoiceServerUpdate stores the data received during the Voice Server Update
+// data websocket event. This data is used during the initial Voice Channel
+// join handshaking.
+type VoiceServerUpdate struct {
+ Token string `json:"token"`
+ GuildID string `json:"guild_id"`
+ Endpoint string `json:"endpoint"`
+}
+
+type voiceChannelJoinData struct {
+ GuildID *string `json:"guild_id"`
+ ChannelID *string `json:"channel_id"`
+ SelfMute bool `json:"self_mute"`
+ SelfDeaf bool `json:"self_deaf"`
+}
+
+type voiceChannelJoinOp struct {
+ Op int `json:"op"`
+ Data voiceChannelJoinData `json:"d"`
+}
+
+// ChannelVoiceJoin joins the session user to a voice channel. After calling
+// this func please monitor the Session.Voice.Ready bool to determine when
+// it is ready and able to send/receive audio, that should happen quickly.
+//
+// gID : Guild ID of the channel to join.
+// cID : Channel ID of the channel to join.
+// mute : If true, you will be set to muted upon joining.
+// deaf : If true, you will be set to deafened upon joining.
+func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (err error) {
+
+ // Create new voice{} struct if one does not exist.
+ // If you create this prior to calling this func then you can manually
+ // set some variables if needed, such as to enable debugging.
+ if s.Voice == nil {
+ s.Voice = &Voice{}
+ }
+
+ // Send the request to Discord that we want to join the voice channel
+ data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}}
+ err = s.wsConn.WriteJSON(data)
+ if err != nil {
+ return
+ }
+
+ // Store gID and cID for later use
+ s.Voice.guildID = gID
+ s.Voice.channelID = cID
+
+ return
+}
+
+// ChannelVoiceLeave disconnects from the currently connected
+// voice channel.
+func (s *Session) ChannelVoiceLeave() (err error) {
+
+ if s.Voice == nil {
+ return
+ }
+
+ // Send the request to Discord that we want to leave voice
+ data := voiceChannelJoinOp{4, voiceChannelJoinData{nil, nil, true, true}}
+ err = s.wsConn.WriteJSON(data)
+ if err != nil {
+ return
+ }
+
+ // Close voice and nil data struct
+ s.Voice.Close()
+ s.Voice = nil
+
+ return
+}
+
+// onVoiceStateUpdate handles Voice State Update events on the data
+// websocket. This comes immediately after the call to VoiceChannelJoin
+// for the session user.
+func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) {
+
+ // Ignore if Voice is nil
+ if s.Voice == nil {
+ return
+ }
+
+ // Need to have this happen at login and store it in the Session
+ // TODO : This should be done upon connecting to Discord, or
+ // be moved to a small helper function
+ self, err := s.User("@me") // TODO: move to Login/New
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ // This event comes for all users, if it's not for the session
+ // user just ignore it.
+ // TODO Move this IF to the event() func
+ if st.UserID != self.ID {
+ return
+ }
+
+ // Store the SessionID for later use.
+ s.Voice.userID = self.ID // TODO: Review
+ s.Voice.sessionID = st.SessionID
+}
+
+// onVoiceServerUpdate handles the Voice Server Update data websocket event.
+// This event tells us the information needed to open a voice websocket
+// connection and should happen after the VOICE_STATE event.
+//
+// This is also fired if the Guild's voice region changes while connected
+// to a voice channel. In that case, need to re-establish connection to
+// the new region endpoint.
+func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) {
+
+ // Store values for later use
+ s.Voice.token = st.Token
+ s.Voice.endpoint = st.Endpoint
+ s.Voice.guildID = st.GuildID
+
+ // If currently connected to voice ws/udp, then disconnect.
+ // Has no effect if not connected.
+ s.Voice.Close()
+
+ // We now have enough information to open a voice websocket conenction
+ // so, that's what the next call does.
+ err := s.Voice.Open()
+ if err != nil {
+ fmt.Println("onVoiceServerUpdate Voice.Open error: ", err)
+ // TODO better logging
+ }
+}
A => DiscordState/session.go +126 -0
@@ 1,126 @@
+//Package DiscordState is an abstraction layer that gives proper structs and functions to get and set the current state of the cli server
+package DiscordState
+
+import (
+ "fmt"
+
+ "bitbucket.org/henesy/disco/DiscordGo"
+)
+
+//!----- Session -----!//
+
+//NewSession Creates a new Session
+func NewSession(Username, Password string) *Session {
+ Session := new(Session)
+ Session.Username = Username
+ Session.Password = Password
+
+ return Session
+}
+
+//Start attaches a discordgo listener to the Sessions and fills it.
+func (Session *Session) Start() error {
+
+ fmt.Printf("Connecting...")
+
+ dg, err := discordgo.New(Session.Username, Session.Password)
+ if err != nil {
+ return err
+ }
+
+ // Open the websocket and begin listening.
+ dg.Open()
+
+ //Retrieve GuildID's from current User
+ UserGuilds, err := dg.UserGuilds()
+ if err != nil {
+ return err
+ }
+
+ Session.Guilds = UserGuilds
+
+ Session.DiscordGo = dg
+
+ Session.User, _ = Session.DiscordGo.User("@me")
+
+ fmt.Printf(" PASSED!\n")
+
+ return nil
+}
+
+//NewState (constructor) attaches a new state to the Guild inside a Session, and fills it.
+func (Session *Session) NewState(GuildID string, MessageAmount int) (*State, error) {
+ State := new(State)
+
+ //Disable Event Handling
+ State.Enabled = false
+
+ //Set Session
+ State.Session = Session
+
+ //Set Guild
+ for _, guildID := range Session.Guilds {
+ if guildID.ID == GuildID {
+ Guild, err := State.Session.DiscordGo.Guild(guildID.ID)
+ if err != nil {
+ return nil, err
+ }
+
+ State.Guild = Guild
+ }
+ }
+
+ //Retrieve Members
+
+ State.Members = make(map[string]*discordgo.Member)
+
+ for _, Member := range State.Guild.Members {
+ State.Members[Member.User.Username] = Member
+ }
+
+ //RetrieveMemberRoles
+ State.MemberRole = make(map[string]*discordgo.Role)
+
+ for _, Member := range State.Guild.Members {
+ var MemberRole string
+
+ if len(Member.Roles) > 0 {
+ MemberRole = Member.Roles[0]
+ } else {
+ break
+ }
+
+ for _, Role := range State.Guild.Roles {
+ if Role.ID == MemberRole {
+ State.MemberRole[Member.User.Username] = Role
+ break
+ }
+ }
+ }
+
+ //Set MessageAmount
+ State.MessageAmount = MessageAmount
+
+ //Init Messages
+ State.Messages = []*discordgo.Message{}
+
+ //Retrieve Channels
+
+ State.Channels = State.Guild.Channels
+
+ //Set User Channels
+ //State.Chan = Session.DiscordGo.UserChannels()
+
+ return State, nil
+}
+
+//Update updates the session, this reloads the Guild list
+func (Session *Session) Update() error {
+ UserGuilds, err := Session.DiscordGo.UserGuilds()
+ if err != nil {
+ return err
+ }
+
+ Session.Guilds = UserGuilds
+ return nil
+}
A => DiscordState/state.go +70 -0
@@ 1,70 @@
+package DiscordState
+
+import "bitbucket.org/henesy/disco/DiscordGo"
+
+//SetChannel sets the channel of the current State
+func (State *State) SetChannel(ID string) {
+ for _, Channel := range State.Channels {
+ if Channel.ID == ID {
+ State.Channel = Channel
+ }
+ }
+}
+
+//AddMember adds Member to State
+func (State *State) AddMember(Member *discordgo.Member) {
+ State.Members[Member.User.ID] = Member
+}
+
+//DelMember deletes Member from State
+func (State *State) DelMember(Member *discordgo.Member) {
+ delete(State.Members, Member.User.ID)
+}
+
+//AddMessage adds Message to State
+func (State *State) AddMessage(Message *discordgo.Message) {
+ //Do not add if Amount <= 0
+ if State.MessageAmount <= 0 {
+ return
+ }
+
+ //Remove First Message if next message is going to increase length past MessageAmount
+ if len(State.Messages) == State.MessageAmount {
+ State.Messages = append(State.Messages[:0], State.Messages[1:]...)
+ }
+
+ State.Messages = append(State.Messages, Message)
+}
+
+//EditMessage edits Message inside State
+func (State *State) EditMessage(Message *discordgo.Message) {
+ for Index, StateMessage := range State.Messages {
+ if StateMessage.ID == Message.ID {
+ State.Messages[Index] = Message
+ }
+ }
+}
+
+//DelMessage deletes Message from State
+func (State *State) DelMessage(Message *discordgo.Message) {
+ for Index, StateMessage := range State.Messages {
+ if StateMessage.ID == Message.ID {
+ State.Messages = append(State.Messages[:Index], State.Messages[Index+1:]...)
+ }
+ }
+}
+
+//RetrieveMessages retrieves last N Messages and puts it in state
+func (State *State) RetrieveMessages(Amount int) error {
+ Messages, err := State.Session.DiscordGo.ChannelMessages(State.Channel.ID, Amount, "", "")
+ if err != nil {
+ return err
+ }
+
+ //Reverse insert Messages
+ for i := 0; i < len(Messages); i++ {
+ State.AddMessage(Messages[len(Messages)-i-1])
+ }
+
+ return nil
+}
A => DiscordState/struct.go +26 -0
@@ 1,26 @@
+package DiscordState
+
+import "bitbucket.org/henesy/disco/DiscordGo"
+
+//State is the current state of the attached client
+type State struct {
+ Guild *discordgo.Guild
+ Channel *discordgo.Channel
+ Channels []*discordgo.Channel
+ UserChannels []*discordgo.Channel
+ Members map[string]*discordgo.Member
+ MemberRole map[string]*discordgo.Role
+ Messages []*discordgo.Message
+ Session *Session
+ MessageAmount int //Amount of Messages to keep in State
+ Enabled bool //Toggles State for Event handling
+}
+
+//Session contains the 'state' of the attached server
+type Session struct {
+ Username string
+ User *discordgo.User
+ Password string
+ DiscordGo *discordgo.Session
+ Guilds []*discordgo.Guild
+}
A => README.md +7 -0
@@ 1,7 @@
+# Disco
+
+Hacked up version of theboxmage's DiscordCli (see LICENSE.theboxmage README.theboxmage.md)
+
+JSON config is in `$home/lib/disco-cfg.json` for setting password.
+
+
A => README.theboxmage.md +55 -0
@@ 1,55 @@
+ # discord-cli
+Minimalistic Command-Line Interface for Discord
+
+I haven't tried to mess with the AUR yet, if I ever will, so it is unlikely that anyone has found this.
+
+Regardless, most of this isn't my work. Most of it was done by a github user that goes by Rivalo.
+All I have done is implemented private messaging, and while I plan to do more, that does not change
+how little work done here that is mine.
+
+Questions can be answered at my discord server, which at the time of editing is empty:
+
+https://discord.gg/qp2Q8jB
+
+## Current build status
+[](https://gitlab.com/chamunks/discordcli/commits/master)
+
+
+## Screenshots
+
+What does chat look like with 256 color sweg.
+
+
+Pressing ```:G + ENTER``` opens the guild[Server] selector.
+
+
+Pressing ```:C + ENTER``` opens the Channel selector.
+
+
+### How to Install the Master branch?
+Currently the easiest working way to install is to use the Go tools. I'm looking at using GCCGO and makefiles to reduce installation steps, and make setting PATHS unnecessary.
+* Install the Go Tools and setup the `$GOPATH` (There are loads of tutorial for this part)
+* `$ go get -u github.com/theboxmage/discordcli`
+* Go to the `bin` folder inside your `$GOPATH`
+* `./discord-cli`
+
+### (Master) Configuration Settings
+Configuration files are being stored in JSON format and are automatically created when you first run discord-cli. Do not change the 'key' value inside `{"key":"value"}`, this is the part that discord-cli uses for parsing, missing keys will definitely return errors.
+
+| Setting | Function |
+| ------------- |-------------|
+| username | Discord Username (emailaddress) |
+| password | Discord Password |
+| messagedefault| (true or false) Display messages automatically|
+| messages | Amount of Messages kept in memory |
+
+### (Master) Chat Commands
+When inside a text channel, the following commands are available:
+
+| Command | Function |
+| ------------- |-------------|
+| :q | Quits discord-cli |
+| :g | Change listening Guild|
+| :c | Change listening Channel inside Guild |
+| :m [n] | Display last [n] messages: ex. `:m 2` displays last two messages |
+| :p | Pulls up the private channel menu |
A => READMEOLD.md +41 -0
@@ 1,41 @@
+# discord-cli
+Minimalistic Command-Line Interface for Discord
+
+Master (Semi-Stable): [](https://travis-ci.org/Rivalo/discord-cli), Develop (Default Git Branch): [](https://travis-ci.org/Rivalo/discord-cli)
+
+Join our Discord Chat! https://discord.gg/0pXWCo5RQbVuFHDM
+
+
+
+<sub>Disclaimer: Currently only tested on Linux.</sub>
+
+### How to Install the Master branch?
+Currently the easiest working way to install is to use the Go tools. I'm looking at using GCCGO and makefiles to reduce installation steps, and make setting PATHS unnecessary.
+* Install the Go Tools and setup the `$GOPATH` (There are loads of tutorial for this part)
+* `$ go get -u github.com/Rivalo/discord-cli`
+* Go to the `bin` folder inside your `$GOPATH`
+* `./discord-cli`
+
+For trying the develop branch, do a git checkout and reinstall the application.
+
+### (Master) Configuration Settings
+Configuration files are being stored in JSON format and are automatically created when you first run discord-cli. Do not change the 'key' value inside `{"key":"value"}`, this is the part that discord-cli uses for parsing, missing keys will definitely return errors.
+
+| Setting | Function |
+| ------------- |-------------|
+| username | Discord Username (emailaddress) |
+| password | Discord Password |
+| messagedefault| (true or false) Display messages automatically|
+| messages | Amount of Messages kept in memory |
+
+NOTE: The Configuration settings are likely to change. Breaking updates are stated in the release section. To solve problems, delete `~/.config/discord-cli/config.json` and restart discord-cli.
+
+### (Master) Chat Commands
+When inside a text channel, the following commands are available:
+
+| Command | Function |
+| ------------- |-------------|
+| :q | Quits discord-cli |
+| :g | Change listening Guild|
+| :c | Change listening Channel inside Guild |
+| :m [n] | Display last [n] messages: ex. `:m 2` displays last two messages |
A => color/LICENSE.md +20 -0
@@ 1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Fatih Arslan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
A => commands.go +90 -0
@@ 1,90 @@
+package main
+
+import (
+ "strconv"
+ "strings"
+)
+
+//ParseForCommands parses input for Commands, returns message if no command specified, else return is empty
+func ParseForCommands(line string) string {
+ //One Key Commands
+ switch line {
+ case ":g":
+ SelectGuild()
+ line = ""
+ case ":c":
+ SelectChannel()
+ line = ""
+ case ":p":
+ SelectPrivate()
+ line = ""
+ default:
+ // Nothing
+ }
+
+ //Argument Commands
+ if strings.HasPrefix(line, ":m") {
+ AmountStr := strings.Split(line, " ")
+ if len(AmountStr) < 2 {
+ Msg(ErrorMsg, "[:m] No Arguments \n")
+ return ""
+ }
+
+ Amount, err := strconv.Atoi(AmountStr[1])
+ if err != nil {
+ Msg(ErrorMsg, "[:m] Argument Error: %s \n", err)
+ return ""
+ }
+
+ Msg(InfoMsg, "Printing last %d messages!\n", Amount)
+ State.RetrieveMessages(Amount)
+ PrintMessages(Amount)
+ line = ""
+ }
+
+ return line
+}
+
+//SelectGuild selects a new Guild
+func SelectGuild() {
+ State.Enabled = false
+ SelectGuildMenu()
+ if !State.Channel.IsPrivate {
+ SelectChannelMenu()
+ }
+ State.Enabled = true
+ ShowContent()
+}
+
+//AddUserChannel moves a user to a private channel with another user.
+func AddUserChannel() {
+ State.Enabled = false
+ AddUserChannelMenu()
+ State.Enabled = true
+ ShowContent()
+}
+
+//SelectChannel selects a new Channel
+func SelectChannel() {
+ State.Enabled = false
+ SelectChannelMenu()
+ State.Enabled = true
+ ShowContent()
+}
+
+//SelectPrivate a private channel
+func SelectPrivate() {
+ State.Enabled = false
+ SelectPrivateMenu()
+ State.Enabled = true
+}
+
+//SelectDeletePrivate a private channel
+func SelectDeletePrivate() {
+ State.Enabled = false
+ SelectDeletePrivateMenu()
+ State.Enabled = true
+ if State.Channel != nil {
+ ShowContent()
+ }
+}
A => config.go +115 -0
@@ 1,115 @@
+package main
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "os/user"
+
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+//Configuration is a struct that contains all configuration fields
+type Configuration struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ MessageDefault bool `json:"messagedefault"`
+ Messages int `json:"messages"`
+}
+
+// Config is the global configuration of discord-cli
+var Config Configuration
+
+//GetConfig retrieves configuration file from ~./config/discord-cli, if it doesn't exist it calls CreateConfig()
+func GetConfig() {
+ //Get User
+Start:
+ usr, err := user.Current()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ //Get File
+ file, err := os.Open(usr.HomeDir + "/lib/disco-cfg.json")
+ if err != nil {
+ log.Println("Creating new config file")
+ CreateConfig()
+ goto Start
+ }
+
+ //Decode File
+ decoder := json.NewDecoder(file)
+ err = decoder.Decode(&Config)
+ if err != nil {
+ log.Println("Failed to decode configuration file")
+ log.Fatalf("Error: %s", err)
+ }
+}
+
+//CreateConfig creates folder inside $HOME and makes a new empty configuration file
+func CreateConfig() {
+ //Get User
+ usr, err := user.Current()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var EmptyStruct Configuration
+ //Set Default values
+ fmt.Println("Input your email")
+ scan := bufio.NewScanner(os.Stdin)
+ scan.Scan()
+
+ EmptyStruct.Username = scan.Text()
+ fmt.Println("Input your password")
+ password, err := terminal.ReadPassword(0)
+ EmptyStruct.Password = string(password)
+ EmptyStruct.Messages = 10
+ EmptyStruct.MessageDefault = true
+
+ //Create Folder
+ err = os.MkdirAll(usr.HomeDir+"/.config/discord-cli/", os.ModePerm)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ //Create File
+ file, err := os.Create(usr.HomeDir + "/.config/discord-cli/config.json")
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ //Marshall EmptyStruct
+ raw, err := json.Marshal(EmptyStruct)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ //PrintToFile
+ _, err = file.Write(raw)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ file.Close()
+}
+
+//CheckState checks the current state for essential missing information, errors will fail the program
+func CheckState() {
+ //Get User
+ usr, err := user.Current()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if Config.Username == "" {
+ log.Fatalln("No Username Specified, please edit " + usr.HomeDir + "/.config/discord-cli/config.json")
+ }
+
+ if Config.Password == "" {
+ log.Fatalln("No Password Specified, please edit " + usr.HomeDir + "/.config/discord-cli/config.json")
+ }
+
+}
A => config.json.example +1 -0
@@ 1,1 @@
+{"username":"REPLACEME@example.com","password":"SDSFSDF879aFDSSKLFjkflsj234IREALLYHOPETHISISAUNIQUEPASSWORD","messagedefault":true,"messages":10}
A => events.go +41 -0
@@ 1,41 @@
+package main
+
+import (
+ "strings"
+
+ "bitbucket.org/henesy/disco/DiscordGo"
+)
+
+func removeReaction(s *discordgo.Session, r *discordgo.ReactionRemove) {
+
+}
+
+func newReaction(s *discordgo.Session, m *discordgo.ReactionAdd) {
+}
+
+// This function will be called (due to AddHandler above) every time a new
+// message is created on any channel that the autenticated user has access to.
+func newMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
+ //Global Mentions
+ Mention := "@" + State.Session.User.Username
+ if strings.Contains(m.ContentWithMentionsReplaced(), Mention) {
+ go Notify(m.Message)
+ }
+
+ // Do nothing when State is disabled
+ if !State.Enabled {
+ return
+ }
+
+ //State Messages
+ if m.ChannelID == State.Channel.ID {
+ State.AddMessage(m.Message)
+
+ Messages := ReceivingMessageParser(m.Message)
+
+ for _, Msg := range Messages {
+ MessagePrint(m.Timestamp, m.Author.Username, Msg)
+ //log.Printf("> %s > %s\n", UserName(m.Author.Username), Msg)
+ }
+ }
+}
A => helper.go +166 -0
@@ 1,166 @@
+package main
+
+import (
+ "encoding/binary"
+ "log"
+ "math"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "bitbucket.org/henesy/disco/color"
+ "bitbucket.org/henesy/disco/DiscordGo"
+)
+
+//HexColor is a struct gives RGB values
+type HexColor struct {
+ Color color.Attribute
+ R int
+ G int
+ B int
+}
+
+//Msg is a composition of Color.New printf functions
+func Msg(MsgType, format string, a ...interface{}) {
+
+ // TODO: Add support for changing color by configuration
+
+ Error := color.New(color.FgRed, color.Bold)
+ Info := color.New(color.FgYellow, color.Bold)
+ Head := color.New(color.FgCyan, color.Bold)
+ Text := color.New(color.FgWhite)
+
+ switch MsgType {
+ case "Error":
+ Error.Printf(format, a...)
+ case "Info":
+ Info.Printf(format, a...)
+ case "Head":
+ Head.Printf(format, a...)
+ case "Text":
+ Text.Printf(format, a...)
+ default:
+ Text.Printf(format, a...)
+ }
+}
+
+//Clear clears the terminal => This barely works, please fix
+func Clear() {
+
+ // TODO: ADD support for multiple operating systems and terminals. Linux = clear, Windows = cls, have to do research for OSX and BSD.
+
+ c := exec.Command("clear")
+ c.Stdout = os.Stdout
+ c.Run()
+}
+
+//Header simply prints a header containing state/session information
+func Header() {
+ Msg(InfoMsg, "Welcome, %s!\n\n", State.Session.User.Username)
+ if State.Channel.IsPrivate {
+ Msg(InfoMsg, "Channel: %s\n", State.Channel.Recipient.Username)
+ } else {
+ Msg(InfoMsg, "Guild: %s, Channel: %s\n", State.Guild.Name, State.Channel.Name)
+ }
+}
+
+//ReceivingMessageParser parses receiving message for mentions, images and MultiLine and returns string array
+func ReceivingMessageParser(m *discordgo.Message) []string {
+ Message := m.ContentWithMentionsReplaced()
+
+ //Parse images
+ for _, Attachment := range m.Attachments {
+ Message = Message + " " + Attachment.URL
+ }
+
+ // MultiLine comment parsing
+ Messages := strings.Split(Message, "\n")
+
+ return Messages
+}
+
+//PrintMessages prints amount of Messages to CLI
+func PrintMessages(Amount int) {
+ for Key, m := range State.Messages {
+ if Key >= len(State.Messages)-Amount {
+ Messages := ReceivingMessageParser(m)
+
+ for _, Msg := range Messages {
+ //log.Printf("> %s > %s\n", UserName(m.Author.Username), Msg)
+ MessagePrint(m.Timestamp, m.Author.Username, Msg)
+
+ }
+ }
+ }
+}
+
+//Notify uses Notify-Send from libnotify to send a notification when a mention arrives.
+func Notify(m *discordgo.Message) {
+ Channel, err := State.Session.DiscordGo.Channel(m.ChannelID)
+ if err != nil {
+ Msg(ErrorMsg, "(NOT) Channel Error: %s\n", err)
+ }
+ Guild, err := State.Session.DiscordGo.Guild(Channel.GuildID)
+ if err != nil {
+ Msg(ErrorMsg, "(NOT) Guild Error: %s\n", err)
+ }
+ Title := "@" + m.Author.Username + " : " + Guild.Name + "/" + Channel.Name
+ cmd := exec.Command("notify-send", Title, m.ContentWithMentionsReplaced())
+ err = cmd.Start()
+ if err != nil {
+ Msg(ErrorMsg, "(NOT) Check if libnotify is installed, or disable notifications.\n")
+ }
+
+}
+
+//MessagePrint prints one correctly formatted Message to stdout
+func MessagePrint(Time, Username, Content string) {
+ var Color color.Attribute
+ TimeStamp, _ := time.Parse(time.RFC3339, Time)
+ LocalTime := TimeStamp.Local().Format("2006/01/02 15:04:05")
+ if val, ok := State.MemberRole[Username]; ok {
+ Color = ColorMatch(val.Color)
+ }
+ UserName := color.New(Color).SprintFunc()
+
+ log.SetFlags(0)
+ log.Printf("%s > %s > %s\n", LocalTime, UserName(Username), Content)
+ log.SetFlags(log.LstdFlags)
+}
+
+//ColorMatch compares HEX->DEC colorcoding and returns the closest ANSI color
+func ColorMatch(colorinput int) color.Attribute {
+ var Result float64
+ var ColorResult color.Attribute
+ Result = 10000
+
+ log.Println(colorinput)
+
+ var ANSIColors []HexColor
+ ANSIColors = append(ANSIColors, HexColor{color.FgRed, 255, 0, 0})
+ ANSIColors = append(ANSIColors, HexColor{color.FgGreen, 0, 128, 0})
+ ANSIColors = append(ANSIColors, HexColor{color.FgYellow, 255, 255, 0})
+ ANSIColors = append(ANSIColors, HexColor{color.FgBlue, 0, 0, 255})
+ ANSIColors = append(ANSIColors, HexColor{color.FgMagenta, 255, 0, 255})
+ ANSIColors = append(ANSIColors, HexColor{color.FgCyan, 0, 255, 255})
+ ANSIColors = append(ANSIColors, HexColor{color.FgWhite, 255, 255, 255})
+ HexNumber := [4]byte{}
+ binary.BigEndian.PutUint32(HexNumber[:], uint32(colorinput))
+ InputStruct := HexColor{color.FgBlack, int(HexNumber[1]), int(HexNumber[2]), int(HexNumber[3])}
+
+ for _, acolor := range ANSIColors {
+ DiffSum := dis(acolor.R, InputStruct.R) + dis(acolor.G, InputStruct.G) + dis(acolor.B, InputStruct.B)
+ TestResult := math.Sqrt(DiffSum)
+ if TestResult < Result {
+ Result = TestResult
+ ColorResult = acolor.Color
+ }
+ }
+
+ return ColorResult
+}
+
+func dis(a, b int) float64 {
+ return float64((a - b) * (a - b))
+}
A => main.go +145 -0
@@ 1,145 @@
+// This file provides a basic "quick start" example of using the Discordgo
+// package to connect to Discord using the New() helper function.
+package main
+
+import (
+ "log"
+ "regexp"
+ "bitbucket.org/henesy/disco/DiscordState"
+ "fmt"
+ "bufio"
+ "os"
+)
+
+//Global Message Types
+const (
+ ErrorMsg = "Error"
+ InfoMsg = "Info"
+ HeaderMsg = "Head"
+ TextMsg = "Text"
+)
+
+//Version is current version const
+const Version = "v1.4.3 - Box Develop~"
+
+//Session is global Session
+var Session *DiscordState.Session
+
+//State is global State
+var State *DiscordState.State
+
+//UserChannels is global User Channels
+
+//MsgType is a string containing global message type
+type MsgType string
+
+func main() {
+ //Initialize Config
+ GetConfig()
+ CheckState()
+ Clear()
+ Msg(HeaderMsg, "discord-cli - version: %s\n\n", Version)
+
+ //NewSession
+ Session = DiscordState.NewSession(Config.Username, Config.Password) //Please don't abuse
+ err := Session.Start()
+ if err != nil {
+ log.Println("Session Failed")
+ log.Fatalln(err)
+ }
+ //Attach New Window
+ InitWindow()
+
+ //Attach Even Handlers
+ State.Session.DiscordGo.AddHandler(newMessage)
+ //State.Session.DiscordGo.AddHandler(newReaction)
+ //Setup Readline
+ /*
+ rl, err := readline.NewEx(&readline.Config{
+ Prompt: "> ",
+ UniqueEditLine: true,
+ })
+ */
+
+ //defer rl.Close()
+ //log.SetOutput(rl.Stderr()) // let "log" write to l.Stderr instead of os.Stderr
+ State.Session.DiscordGo.UpdateStatus(0, "discord-cli")
+
+ //Start Listening
+ reader := bufio.NewReader(os.Stdin)
+ for {
+ fmt.Print("> ")
+ //line, _ := rl.Readline()
+ line, _ := reader.ReadString('\n')
+
+
+ //QUIT
+ if line == ":q" {
+ break
+ }
+
+ //Parse Commands
+ line = ParseForCommands(line)
+
+ line = ParseForMentions(line)
+
+ if line != "" {
+ State.Session.DiscordGo.ChannelMessageSend(State.Channel.ID, line)
+ }
+ }
+
+ return
+}
+
+//InitWindow creates a New CLI Window
+func InitWindow() {
+ SelectGuildMenu()
+ if State.Channel == nil {
+ SelectChannelMenu()
+ }
+ State.Enabled = true
+ ShowContent()
+}
+
+//ShowContent shows defaulth Channel content
+func ShowContent() {
+ Clear()
+ Header()
+ if Config.MessageDefault {
+ State.RetrieveMessages(Config.Messages)
+ PrintMessages(Config.Messages)
+ }
+}
+
+//ShowEmptyContent shows an empty channel
+func ShowEmptyContent() {
+ Clear()
+ Header()
+}
+
+//ParseForMentions parses input string for mentions
+func ParseForMentions(line string) string {
+ r, err := regexp.Compile("\\@\\w+")
+ if err != nil {
+ Msg(ErrorMsg, "Regex Error: ", err)
+ }
+
+ lineByte := r.ReplaceAllFunc([]byte(line), ReplaceMentions)
+
+ return string(lineByte[:])
+}
+
+//ReplaceMentions replaces mentions to ID
+func ReplaceMentions(input []byte) []byte {
+ var OutputString string
+
+ SizeByte := len(input)
+ InputString := string(input[1:SizeByte])
+
+ if Member, ok := State.Members[InputString]; ok {
+ OutputString = "<@" + Member.User.ID + ">"
+ } else {
+ OutputString = "@" + InputString
+ }
+ return []byte(OutputString)
+}
A => +368 -0
@@ 1,368 @@
package main
import (
"fmt"
"log"
"strconv"
)
//SelectPrivateMenu is a menu item that changes to a private channel
func SelectPrivateMenu() {
Start:
Msg(InfoMsg, "Select a Member:\n")
UserChannels, err := Session.DiscordGo.UserChannels()
if err != nil {
Msg(ErrorMsg, "No Private Channels\n")
}
UserMap := make(map[int]string)
SelectID := 0
for _, user := range UserChannels {
UserMap[SelectID] = user.ID
Msg(TextMsg, "[%d] %s\n", SelectID, UserChannels[SelectID].Recipient.Username)
SelectID++
}
Msg(TextMsg, "[b] Extra Options\n")
var response string
fmt.Scanf("%s\n", &response)
ResponseInteger, err := strconv.Atoi(response)
if response == "b" {
New:
Msg(InfoMsg, "Extra Options:\n")
Msg(TextMsg, "[n] Join New User Channel\n")
Msg(TextMsg, "[d] Leave User Channel\n")
Msg(TextMsg, "[b] Go Back\n")
var response string
fmt.Scanf("%s\n", &response)
switch response {
case "n":
if State.Channel != nil {
AddUserChannel()
ShowEmptyContent()
goto End
} else {
Msg(ErrorMsg, "Join a guild before attempting to join a user channel\n")
goto New
}
case "d":
SelectDeletePrivate()
goto Start
case "b":
goto Start
default:
goto New
}
}
if err != nil {
Msg(ErrorMsg, "(GU) Conversion Error: %s\n", err)
goto Start
}
if ResponseInteger > SelectID-1 || ResponseInteger < 0 {
Msg(ErrorMsg, "(GU) Error: ID is out of bounds\n")
goto Start
}
State.Channel = UserChannels[ResponseInteger]
ShowContent()
End:
}
//SelectDeletePrivateMenu deletes a user channel
func SelectDeletePrivateMenu() {
Start:
Msg(InfoMsg, "Select a Member:\n")
UserChannels, err := Session.DiscordGo.UserChannels()
if err != nil {
Msg(ErrorMsg, "No Private Channels\n")
}
UserMap := make(map[int]string)
SelectID := 0
for _, user := range UserChannels {
UserMap[SelectID] = user.ID
Msg(TextMsg, "[%d] %s\n", SelectID, UserChannels[SelectID].Recipient.Username)
SelectID++
}
var response string
fmt.Scanf("%s\n", &response)
ResponseInteger, err := strconv.Atoi(response)
if err != nil {
Msg(ErrorMsg, "(GU) Conversion Error: %s\n", err)
goto Start
}
if ResponseInteger > SelectID-1 || ResponseInteger < 0 {
Msg(ErrorMsg, "(GU) Error: ID is out of bounds\n")
goto Start
}
Session.DiscordGo.ChannelDelete(UserChannels[ResponseInteger].ID)
}
//SelectGuildMenu is a menu item that creates a new State on basis of Guild selection
func SelectGuildMenu() {
var err error
Start:
Msg(InfoMsg, "Select a Guild:\n")
SelectMap := make(map[int]string)
SelectID := 0
for _, guild := range Session.Guilds {
SelectMap[SelectID] = guild.ID
Msg(TextMsg, "[%d] %s\n", SelectID, guild.Name)
SelectID++
}
Msg(TextMsg, "[b] Extra Options\n")
Msg(TextMsg, "[p] Private Channels\n")
var response string
fmt.Scanf("%s\n", &response)
ResponseInteger, err := strconv.Atoi(response)
if response == "b" {
ExtraGuildMenuOptions()
goto Start
}
if response == "p" {
if State != nil {
SelectPrivate()
} else {
State, err = Session.NewState(SelectMap[0], Config.Messages)
if err != nil {
log.Fatal(err)
}
SelectPrivate()
}
} else {
if err != nil {
Msg(ErrorMsg, "(GU) Conversion Error: %s\n", err)
goto Start
}
if ResponseInteger > SelectID-1 || ResponseInteger < 0 {
Msg(ErrorMsg, "(GU) Error: ID is out of bounds\n")
goto Start
}
State, err = Session.NewState(SelectMap[ResponseInteger], Config.Messages)
if err != nil {
log.Fatal(err)
}
}
}
//SelectChannelMenu is a menu item that sets the current channel
func SelectChannelMenu() {
Start:
Msg(InfoMsg, "Select a Channel:\n")
SelectMap := make(map[int]string)
SelectID := 0
for _, channel := range State.Channels {
if channel.Type == "text" {
SelectMap[SelectID] = channel.ID
Msg(TextMsg, "[%d] %s\n", SelectID, channel.Name)
SelectID++
}
}
Msg(TextMsg, "[b] Go Back\n")
var response string
fmt.Scanf("%s\n", &response)
if response == "b" {
SelectGuildMenu()
goto Start
}
ResponseInteger, err := strconv.Atoi(response)
if err != nil {
Msg(ErrorMsg, "(CH) Conversion Error: %s\n", err)
goto Start
}
if ResponseInteger > SelectID-1 || ResponseInteger < 0 {
Msg(ErrorMsg, "(CH) Error: ID is out of bound\n")
goto Start
}
State.SetChannel(SelectMap[ResponseInteger])
}
//ExtraGuildMenuOptions prints and handles extra options for SelectGuildMenu
func ExtraGuildMenuOptions() {
Start:
Msg(InfoMsg, "Extra Options:\n")
Msg(TextMsg, "[n] Join New Server\n")
Msg(TextMsg, "[d] Leave Server\n")
Msg(TextMsg, "[o] Join Official discord-cli Server\n")
Msg(TextMsg, "[b] Go Back\n")
var response string
fmt.Scanf("%s\n", &response)
switch response {
case "n":
New:
Msg(TextMsg, "Please input invite number ([b] back):\n")
fmt.Scanf("%s\n", &response)
if response == "b" {
goto Start
}
Invite, err := Session.DiscordGo.Invite(response)
if err != nil {
Msg(ErrorMsg, "Invalid Invite\n")
goto New
}
Msg(TextMsg, "Join %s ? [y/n]:\n", Invite.Guild.Name)
fmt.Scanf("%s\n", &response)
if response == "y" {
Session.DiscordGo.InviteAccept(Invite.Code)
err := Session.Update()
if err != nil {
Msg(ErrorMsg, "Session Update Failed: %s\n", err)
}
} else {
goto Start
}
case "o":
_, err := Session.DiscordGo.InviteAccept("0pXWCo5RQbVuFHDM")
if err != nil {
Msg(ErrorMsg, "Joining Official discord-cli Server failed\n")
goto Start
}
Msg(InfoMsg, "Joined Official discord-cli Server!\n")
case "d":
LeaveServerMenu()
goto Start
default:
return
}
return
}
//ExtraPrivateMenuOptions adds functionality to UserChannels.
func ExtraPrivateMenuOptions() {
return
}
//AddUserChannelMenu takes a user from the current guild and adds them to a private message. WILL RETURN ERROR IF IN USER CHANNEL.
func AddUserChannelMenu() {
if State.Channel.IsPrivate {
Msg(ErrorMsg, "Currently in a user channel, move to a guild with :g\n")
} else {
SelectMap := make(map[int]string)
Start:
SelectID := 0
for _, Member := range State.Members {
SelectMap[SelectID] = Member.User.ID
Msg(TextMsg, "[%d] %s\n", SelectID, Member.User.Username)
SelectID++
}
var response string
fmt.Scanf("%s\n", &response)
if response == "b" {
return
}
ResponseInteger, err := strconv.Atoi(response)
if err != nil {
Msg(ErrorMsg, "(CH) Conversion Error: %s\n", err)
goto Start
}
if ResponseInteger > SelectID-1 || ResponseInteger < 0 {
Msg(ErrorMsg, "(CH) Error: ID is out of bound\n")
goto Start
}
Chan, err := Session.DiscordGo.UserChannelCreate(SelectMap[ResponseInteger])
if Chan.LastMessageID == "" {
var firstMessage string
fmt.Scanf("%s\n", &firstMessage)
Session.DiscordGo.ChannelMessageSend(Chan.ID, "Test")
}
State.Channel = Chan
}
}
//LeaveServerMenu is a copy of SelectGuildMenu that leaves instead of selects
func LeaveServerMenu() {
var err error
Start:
Msg(InfoMsg, "Leave a Guild:\n")
SelectMap := make(map[int]string)
SelectID := 0
for _, guild := range Session.Guilds {
SelectMap[SelectID] = guild.ID
Msg(TextMsg, "[%d] %s\n", SelectID, guild.Name)
SelectID++
}
Msg(TextMsg, "[b] Go Back\n")
var response string
fmt.Scanf("%s\n", &response)
if response == "b" {
return
}
ResponseInteger, err := strconv.Atoi(response)
if err != nil {
Msg(ErrorMsg, "(GUD) Conversion Error: %s\n", err)
goto Start
}
if ResponseInteger > SelectID-1 || ResponseInteger < 0 {
Msg(ErrorMsg, "(GUD) Error: ID is out of bounds\n")
goto Start
}
Guild, err := Session.DiscordGo.Guild(SelectMap[ResponseInteger])
if err != nil {
Msg(ErrorMsg, "(GUD) Unknown Error: %s\n", err)
goto Start
}
Msg(TextMsg, "Leave %s ? [y/n]:\n", Guild.Name)
fmt.Scanf("%s\n", &response)
if response == "y" {
Session.DiscordGo.GuildLeave(Guild.ID)
err := Session.Update()
if err != nil {
Msg(ErrorMsg, "Session Update Failed: %s\n", err)
}
} else {
goto Start
}
}