discordrb 1.1.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of discordrb might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b0dcdf2ba14487766e026041d7e00e0e49f565d7
4
- data.tar.gz: 2630df655c054d3a00520302bcf8e164f199cddd
3
+ metadata.gz: 002c1c37653e0e75bbe66c5727030a39f5eae9d8
4
+ data.tar.gz: e703d694a84d3fe9b738403aee0517fa250ae549
5
5
  SHA512:
6
- metadata.gz: 6641d9e376c4bacb632a7e9796914974fe3e8225aa9f32604ad9bdd7d84bcc63a8f4dcf5e66b9a1c4af4bebcf4da5b4c7a64f641fc0c6f4793fdc98205de2e24
7
- data.tar.gz: f06c2d54af5b4572ebe1a74e108252880e469ecfba206b9e6c5b7dfe55493c9480474b47a0f178ca2e1e537a78e151c877845e66f5229f0bb82b9401ec3a521b
6
+ metadata.gz: e43742e8e297e34d20892936267e1e60464945ee020b6c69c4b6d6f8277fb7256b32e3050a5af3696f0926f168e1e889e2dc9b0a7517d4ba04c62f38f1f1b24c
7
+ data.tar.gz: ce006992dd5bf162e19c32f0bca74980982023954ee448ec7b04916d153bf7bade1d5f6b182a108bb05e298160d3f8b73f25f84d7cc71dff28cdb88a7300813e
data/.gitignore CHANGED
@@ -1,9 +1,9 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml CHANGED
@@ -1,4 +1,4 @@
1
- language: ruby
2
- rvm:
3
- - 2.2.2
4
- before_install: gem install bundler -v 1.10.6
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in discordrb.gemspec
4
- gemspec
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in discordrb.gemspec
4
+ gemspec
data/LICENSE.txt CHANGED
@@ -1,21 +1,21 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2015 meew0
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 meew0
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -1,78 +1,78 @@
1
- [![Build Status](https://travis-ci.org/meew0/discordrb.svg?branch=master)](https://travis-ci.org/meew0/discordrb)
2
-
3
- # discordrb
4
-
5
- An implementation of the [Discord](https://discordapp.com/) API using Ruby.
6
-
7
- ## Installation
8
-
9
- ### Linux
10
-
11
- On Linux, it should be as simple as running:
12
-
13
- $ gem install discordrb
14
-
15
- ### Windows
16
-
17
- On Windows, to install discordrb, run this in a shell:
18
-
19
- $ gem install discordrb
20
-
21
- Run the [ping example](https://github.com/meew0/discordrb/blob/master/examples/ping.rb) to verify that the installation works:
22
-
23
- $ ruby ping.rb
24
-
25
- #### Troubleshooting
26
-
27
- **If you get an error like this when installing the gem**:
28
-
29
- ERROR: Error installing discordrb:
30
- The 'websocket-driver' native gem requires installed build tools.
31
-
32
- You're missing the development kit required to build native extensions. Follow [these instructions](https://github.com/oneclick/rubyinstaller/wiki/Development-Kit#installation-instructions) and reinstall discordrb:
33
-
34
- $ gem uninstall discordrb
35
- $ gem install discordrb
36
-
37
- **If you get an error like this when running the example**:
38
-
39
- terminate called after throwing an instance of 'std::runtime_error'
40
- what(): Encryption not available on this event-machine
41
-
42
- You're missing the OpenSSL libraries that EventMachine, a dependency of discordrb, needs to be built with to use encrypted connections (which Discord requires). Download the OpenSSL libraries from [here](http://slproweb.com/download/Win32OpenSSL-1_0_2d.exe), install them to their default location and reinstall EventMachine using these libraries:
43
-
44
- $ gem uninstall eventmachine
45
- $ gem install eventmachine -- --with-ssl-dir=C:/OpenSSL-Win32
46
-
47
- ## Usage
48
-
49
- You can make a simple bot like this:
50
-
51
- ```ruby
52
- require 'discordrb'
53
-
54
- bot = Discordrb::Bot.new "email@example.com", "hunter2"
55
-
56
- bot.message(with_text: "Ping!") do |event|
57
- event.respond "Pong!"
58
- end
59
-
60
- bot.run
61
- ```
62
-
63
- This bot responds to every "Ping!" with a "Pong!".
64
-
65
- ## Development
66
-
67
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake false` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
68
-
69
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
70
-
71
- ## Contributing
72
-
73
- Bug reports and pull requests are welcome on GitHub at https://github.com/meew0/discordrb.
74
-
75
-
76
- ## License
77
-
78
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
1
+ [![Build Status](https://travis-ci.org/meew0/discordrb.svg?branch=master)](https://travis-ci.org/meew0/discordrb)
2
+
3
+ # discordrb
4
+
5
+ An implementation of the [Discord](https://discordapp.com/) API using Ruby.
6
+
7
+ ## Installation
8
+
9
+ ### Linux
10
+
11
+ On Linux, it should be as simple as running:
12
+
13
+ $ gem install discordrb
14
+
15
+ ### Windows
16
+
17
+ On Windows, to install discordrb, run this in a shell:
18
+
19
+ $ gem install discordrb
20
+
21
+ Run the [ping example](https://github.com/meew0/discordrb/blob/master/examples/ping.rb) to verify that the installation works (make sure to replace the username and password in there with your own or your bots'!):
22
+
23
+ $ ruby ping.rb
24
+
25
+ #### Troubleshooting
26
+
27
+ **If you get an error like this when installing the gem**:
28
+
29
+ ERROR: Error installing discordrb:
30
+ The 'websocket-driver' native gem requires installed build tools.
31
+
32
+ You're missing the development kit required to build native extensions. Follow [these instructions](https://github.com/oneclick/rubyinstaller/wiki/Development-Kit#installation-instructions) and reinstall discordrb:
33
+
34
+ $ gem uninstall discordrb
35
+ $ gem install discordrb
36
+
37
+ **If you get an error like this when running the example**:
38
+
39
+ terminate called after throwing an instance of 'std::runtime_error'
40
+ what(): Encryption not available on this event-machine
41
+
42
+ You're missing the OpenSSL libraries that EventMachine, a dependency of discordrb, needs to be built with to use encrypted connections (which Discord requires). Download the OpenSSL libraries from [here](http://slproweb.com/download/Win32OpenSSL-1_0_2d.exe), install them to their default location and reinstall EventMachine using these libraries:
43
+
44
+ $ gem uninstall eventmachine
45
+ $ gem install eventmachine -- --with-ssl-dir=C:/OpenSSL-Win32
46
+
47
+ ## Usage
48
+
49
+ You can make a simple bot like this:
50
+
51
+ ```ruby
52
+ require 'discordrb'
53
+
54
+ bot = Discordrb::Bot.new "email@example.com", "hunter2"
55
+
56
+ bot.message(with_text: "Ping!") do |event|
57
+ event.respond "Pong!"
58
+ end
59
+
60
+ bot.run
61
+ ```
62
+
63
+ This bot responds to every "Ping!" with a "Pong!".
64
+
65
+ ## Development
66
+
67
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake false` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
68
+
69
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
70
+
71
+ ## Contributing
72
+
73
+ Bug reports and pull requests are welcome on GitHub at https://github.com/meew0/discordrb.
74
+
75
+
76
+ ## License
77
+
78
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
- require "bundler/gem_tasks"
2
-
3
- # Make "build" the default task
4
- task :default => :build
1
+ require "bundler/gem_tasks"
2
+
3
+ # Make "build" the default task
4
+ task :default => :build
data/bin/console CHANGED
@@ -1,14 +1,14 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "discordrb"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "discordrb"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup CHANGED
@@ -1,7 +1,7 @@
1
- #!/bin/bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
-
5
- bundle install
6
-
7
- # Do any other automated setup that you need to do here
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/discordrb.gemspec CHANGED
@@ -1,28 +1,28 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'discordrb/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "discordrb"
8
- spec.version = Discordrb::VERSION
9
- spec.authors = ["meew0"]
10
- spec.email = [""]
11
-
12
- spec.summary = "Discord API for Ruby"
13
- spec.description = "A Ruby implementation of the Discord (https://discordapp.com) API."
14
- spec.homepage = "https://github.com/meew0/discordrb"
15
- spec.license = "MIT"
16
-
17
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
- spec.bindir = "exe"
19
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
- spec.require_paths = ["lib"]
21
-
22
- spec.add_dependency "faye-websocket"
23
- spec.add_dependency "rest-client"
24
- spec.add_dependency "activesupport"
25
-
26
- spec.add_development_dependency "bundler", "~> 1.10"
27
- spec.add_development_dependency "rake", "~> 10.0"
28
- end
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'discordrb/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "discordrb"
8
+ spec.version = Discordrb::VERSION
9
+ spec.authors = ["meew0"]
10
+ spec.email = [""]
11
+
12
+ spec.summary = "Discord API for Ruby"
13
+ spec.description = "A Ruby implementation of the Discord (https://discordapp.com) API."
14
+ spec.homepage = "https://github.com/meew0/discordrb"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "faye-websocket"
23
+ spec.add_dependency "rest-client"
24
+ spec.add_dependency "activesupport"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.10"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ end
data/examples/ping.rb CHANGED
@@ -1,11 +1,11 @@
1
- # This simple bot responds to every "Ping!" message with a "Pong!"
2
-
3
- require 'discordrb'
4
-
5
- bot = Discordrb::Bot.new "email@example.com", "hunter2"
6
-
7
- bot.message(with_text: "Ping!") do |event|
8
- event.respond "Pong!"
9
- end
10
-
11
- bot.run
1
+ # This simple bot responds to every "Ping!" message with a "Pong!"
2
+
3
+ require 'discordrb'
4
+
5
+ bot = Discordrb::Bot.new "email@example.com", "hunter2"
6
+
7
+ bot.message(with_text: "Ping!") do |event|
8
+ event.respond "Pong!"
9
+ end
10
+
11
+ bot.run
data/examples/pm.rb CHANGED
@@ -1,11 +1,11 @@
1
- # This bot shows off PM functionality by sending a PM every time the bot is mentioned.
2
-
3
- require 'discordrb'
4
-
5
- bot = Discordrb::Bot.new "email@example.com", "hunter2"
6
-
7
- bot.mention do |event|
8
- event.user.pm("You have mentioned me!")
9
- end
10
-
11
- bot.run
1
+ # This bot shows off PM functionality by sending a PM every time the bot is mentioned.
2
+
3
+ require 'discordrb'
4
+
5
+ bot = Discordrb::Bot.new "email@example.com", "hunter2"
6
+
7
+ bot.mention do |event|
8
+ event.user.pm("You have mentioned me!")
9
+ end
10
+
11
+ bot.run
data/lib/discordrb.rb CHANGED
@@ -1,6 +1,6 @@
1
- require "discordrb/version"
2
- require "discordrb/bot"
3
-
4
- module Discordrb
5
- # Your code goes here...
6
- end
1
+ require "discordrb/version"
2
+ require "discordrb/bot"
3
+
4
+ module Discordrb
5
+ # Your code goes here...
6
+ end
data/lib/discordrb/bot.rb CHANGED
@@ -1,342 +1,525 @@
1
- require 'rest-client'
2
- require 'faye/websocket'
3
- require 'eventmachine'
4
-
5
- require 'discordrb/endpoints/endpoints'
6
-
7
- require 'discordrb/events/message'
8
- require 'discordrb/events/typing'
9
- require 'discordrb/events/lifetime'
10
- require 'discordrb/events/presence'
11
-
12
- require 'discordrb/exceptions'
13
- require 'discordrb/data'
14
-
15
- module Discordrb
16
- class Bot
17
- include Discordrb::Events
18
- def initialize(email, password, debug = false)
19
- @debug = debug
20
-
21
- @email = email
22
- @password = password
23
-
24
- @token = login
25
-
26
- @event_handlers = {}
27
-
28
- @channels = {}
29
- @users = {}
30
-
31
- add_default_handlers
32
- end
33
-
34
- def run
35
- # Handle heartbeats
36
- @heartbeat_interval = 1
37
- @heartbeat_active = false
38
- @heartbeat_thread = Thread.new do
39
- while true do
40
- sleep @heartbeat_interval
41
- send_heartbeat if @heartbeat_active
42
- end
43
- end
44
-
45
- while true do
46
- websocket_connect
47
- debug("Disconnected! Attempting to reconnect in 5 seconds.")
48
- sleep 5
49
- @token = login
50
- end
51
- end
52
-
53
- def channel(id)
54
- debug("Obtaining data for channel with id #{id}")
55
- return @channels[id] if @channels[id]
56
-
57
- response = RestClient.get Discordrb::Endpoints::CHANNELS + "/#{id}", {:Authorization => @token}
58
- channel = Channel.new(JSON.parse(response), self)
59
- @channels[id] = channel
60
- end
61
-
62
- def private_channel(id)
63
- debug("Creating private channel with user id #{id}")
64
- return @private_channels[id] if @private_channels[id]
65
-
66
- data = {
67
- 'recipient_id' => id
68
- }
69
-
70
- response = RestClient.post Discordrb::Endpoints::USERS + "/#{@bot_user.id}/channels", data.to_json, {:Authorization => @token, :content_type => :json}
71
- channel = Channel.new(JSON.parse(response), self)
72
- @private_channels[id] = channel
73
- end
74
-
75
- def user(id)
76
- @users[id]
77
- end
78
-
79
- def server(id)
80
- @servers[id]
81
- end
82
-
83
- def send_message(channel_id, content)
84
- debug("Sending message to #{channel_id} with content '#{content}'")
85
- data = {
86
- 'content' => content.to_s,
87
- 'mentions' => []
88
- }
89
- RestClient.post Discordrb::Endpoints::CHANNELS + "/#{channel_id}/messages", data.to_json, {:Authorization => @token, :content_type => :json}
90
- end
91
-
92
- def debug=(debug)
93
- @debug = debug
94
- end
95
-
96
- def message(attributes = {}, &block)
97
- register_event(MessageEvent, attributes, block)
98
- end
99
-
100
- def ready(attributes = {}, &block)
101
- register_event(ReadyEvent, attributes, block)
102
- end
103
-
104
- def disconnected(attributes = {}, &block)
105
- register_event(DisconnectEvent, attributes, block)
106
- end
107
-
108
- def typing(attributes = {}, &block)
109
- register_event(TypingEvent, attributes, block)
110
- end
111
-
112
- def presence(attributes = {}, &block)
113
- register_event(PresenceEvent, attributes, block)
114
- end
115
-
116
- def mention(attributes = {}, &block)
117
- register_event(MentionEvent, attributes, block)
118
- end
119
-
120
- def remove_handler(handler)
121
- clazz = event_class(handler.class)
122
- @event_handlers[clazz].delete(handler)
123
- end
124
-
125
- def add_handler(handler)
126
- clazz = event_class(handler.class)
127
- @event_handlers[clazz] << handler
128
- end
129
-
130
- alias_method :<<, :add_handler
131
-
132
- private
133
-
134
- # Register any default event handlers that the end user shouldn't
135
- # need to worry about implementing
136
- def add_default_handlers
137
- # Update a user's status
138
- presence do |event|
139
- user_id = event.user.id
140
- cached_user = @users[user_id]
141
- if !cached_user
142
- # If this is a user we've never seen before, add them
143
- @users[user_id] = event.user
144
- cached_user = event.user
145
- end
146
- cached_user.instance_exec(event.status) do |status|
147
- @status = status
148
- end
149
- end
150
- end
151
-
152
- def debug(message)
153
- puts "[DEBUG @ #{Time.now.to_s}] #{message}" if @debug
154
- end
155
-
156
- def login
157
- debug("Logging in")
158
- login_attempts = login_attempts || 0
159
-
160
- # Login
161
- login_response = RestClient.post Discordrb::Endpoints::LOGIN, :email => @email, :password => @password
162
- raise HTTPStatusException.new(login_response.code) if login_response.code >= 400
163
-
164
- # Parse response
165
- login_response_object = JSON.parse(login_response)
166
- raise InvalidAuthenticationException unless login_response_object['token']
167
-
168
- debug("Received token: #{login_response_object['token']}")
169
- login_response_object['token']
170
- rescue Exception => e
171
- response_code = login_response.nil? ? 0 : login_response.code ######## mackmm145
172
- if login_attempts < 100 && (e.inspect.include?("No such host is known.") || response_code == 523)
173
- debug("Login failed! Reattempting in 5 seconds. #{100 - login_attempts} attempts remaining.")
174
- debug("Error was: #{e.inspect}")
175
- sleep 5
176
- login_attempts += 1
177
- retry
178
- else
179
- debug("Login failed permanently after #{login_attempts + 1} attempts")
180
-
181
- # Apparently we get a 400 if the password or username is incorrect. In that case, tell the user
182
- debug("Are you sure you're using the correct username and password?") if e.class == RestClient::BadRequest
183
- raise $!
184
- end
185
- end
186
-
187
- def get_gateway
188
- # Get updated websocket_hub
189
- response = RestClient.get Discordrb::Endpoints::GATEWAY, :authorization => @token
190
- JSON.parse(response)["url"]
191
- end
192
-
193
- def websocket_connect
194
- debug("Attempting to get gateway URL...")
195
- websocket_hub = get_gateway
196
- debug("Success! Gateway URL is #{websocket_hub}.")
197
- debug("Now running bot")
198
-
199
- EM.run {
200
- @ws = Faye::WebSocket::Client.new(websocket_hub)
201
-
202
- @ws.on :open do |event|; websocket_open(event); end
203
- @ws.on :message do |event|; websocket_message(event); end
204
- @ws.on :error do |event|; debug(event.message); end
205
- @ws.on :close do |event|; websocket_close(event); @ws = nil; end
206
- }
207
- end
208
-
209
- def websocket_message(event)
210
- debug("Received packet #{event.data}")
211
-
212
- # Parse packet
213
- packet = JSON.parse(event.data)
214
-
215
- raise "Invalid Packet" unless packet['op'] == 0 # TODO
216
-
217
- data = packet['d']
218
- case packet['t']
219
- when "READY"
220
- # Activate the heartbeats
221
- @heartbeat_interval = data['heartbeat_interval'].to_f / 1000.0
222
- @heartbeat_active = true
223
- debug("Desired heartbeat_interval: #{@heartbeat_interval}")
224
-
225
- # Initialize the bot user
226
- @bot_user = User.new(data['user'], self)
227
-
228
- # Initialize servers
229
- @servers = {}
230
- data['guilds'].each do |element|
231
- server = Server.new(element, self)
232
- @servers[server.id] = server
233
-
234
- # Initialize users
235
- server.members.each do |element|
236
- @users[element.id] = element
237
- end
238
- end
239
-
240
- # Add private channels
241
- @private_channels = {}
242
- data['private_channels'].each do |element|
243
- channel = Channel.new(element, self)
244
- @channels[channel.id] = channel
245
- @private_channels[channel.recipient.id] = channel
246
- end
247
-
248
- # Make sure to raise the event
249
- raise_event(ReadyEvent.new)
250
- when "MESSAGE_CREATE"
251
- message = Message.new(data, self)
252
- event = MessageEvent.new(message, self)
253
- raise_event(event)
254
-
255
- if message.mentions.any? { |user| user.id == @bot_user.id }
256
- event = MentionEvent.new(message, self)
257
- raise_event(event)
258
- end
259
- when "TYPING_START"
260
- event = TypingEvent.new(data, self)
261
- raise_event(event)
262
- when "PRESENCE_UPDATE"
263
- event = PresenceEvent.new(data, self)
264
- raise_event(event)
265
- end
266
- end
267
-
268
- def websocket_close(event)
269
- debug("Disconnected from WebSocket!")
270
- debug(" (Reason: #{event.reason})")
271
- debug(" (Code: #{event.code})")
272
- raise_event(DisconnectEvent.new)
273
- EM.stop
274
- end
275
-
276
- def websocket_open(event)
277
- # Send the initial packet
278
- packet = {
279
- "op" => 2, # Packet identifier
280
- "d" => { # Packet data
281
- "v" => 2, # Another identifier
282
- "token" => @token,
283
- "properties" => { # I'm unsure what these values are for exactly, but they don't appear to impact bot functionality in any way.
284
- "$os" => "Linux",
285
- "$browser" => "",
286
- "$device" => "discordrb",
287
- "$referrer" => "",
288
- "$referring_domain" => ""
289
- }
290
- }
291
- }
292
-
293
- @ws.send(packet.to_json)
294
- end
295
-
296
- def raise_event(event)
297
- debug("Raised a #{event.class}")
298
- handlers = @event_handlers[event.class]
299
- (handlers || []).each do |handler|
300
- handler.match(event)
301
- end
302
- end
303
-
304
- def register_event(clazz, attributes, block)
305
- handler = handler_class(clazz).new(attributes, block)
306
-
307
- @event_handlers[clazz] ||= []
308
- @event_handlers[clazz] << handler
309
-
310
- # Return the handler so it can be removed later
311
- handler
312
- end
313
-
314
- def send_heartbeat
315
- millis = Time.now.strftime("%s%L").to_i
316
- debug("Sending heartbeat at #{millis}")
317
- data = {
318
- 'op' => 1,
319
- 'd' => millis
320
- }
321
-
322
- @ws.send(data.to_json)
323
- end
324
-
325
- def class_from_string(str)
326
- str.split('::').inject(Object) do |mod, class_name|
327
- mod.const_get(class_name)
328
- end
329
- end
330
-
331
- def event_class(handler_class)
332
- class_name = handler_class.to_s
333
- return nil unless class_name.end_with? "Handler"
334
-
335
- class_from_string(class_name[0..-8])
336
- end
337
-
338
- def handler_class(event_class)
339
- class_from_string(event_class.to_s + "Handler")
340
- end
341
- end
342
- end
1
+ require 'rest-client'
2
+ require 'faye/websocket'
3
+ require 'eventmachine'
4
+
5
+ require 'discordrb/endpoints/endpoints'
6
+
7
+ require 'discordrb/events/message'
8
+ require 'discordrb/events/typing'
9
+ require 'discordrb/events/lifetime'
10
+ require 'discordrb/events/presence'
11
+ require 'discordrb/events/voice-state-update'
12
+ require 'discordrb/events/channel-create'
13
+ require 'discordrb/events/channel-update'
14
+ require 'discordrb/events/channel-delete'
15
+ require 'discordrb/events/guild-member-update'
16
+ require 'discordrb/events/guild-role-create'
17
+ require 'discordrb/events/guild-role-delete'
18
+ require 'discordrb/events/guild-role-update'
19
+
20
+ require 'discordrb/exceptions'
21
+ require 'discordrb/data'
22
+
23
+ module Discordrb
24
+ class Bot
25
+ include Discordrb::Events
26
+ def initialize(email, password, debug = false)
27
+ # Make sure people replace the login details in the example files...
28
+ if email.end_with? "example.com"
29
+ puts "You have to replace the login details in the example files with your own!"
30
+ exit
31
+ end
32
+
33
+ @debug = debug
34
+
35
+ @email = email
36
+ @password = password
37
+
38
+ @token = login
39
+
40
+ @event_handlers = {}
41
+
42
+ @channels = {}
43
+ @users = {}
44
+ end
45
+
46
+ def run
47
+ # Handle heartbeats
48
+ @heartbeat_interval = 1
49
+ @heartbeat_active = false
50
+ @heartbeat_thread = Thread.new do
51
+ while true do
52
+ sleep @heartbeat_interval
53
+ send_heartbeat if @heartbeat_active
54
+ end
55
+ end
56
+
57
+ while true do
58
+ websocket_connect
59
+ debug("Disconnected! Attempting to reconnect in 5 seconds.")
60
+ sleep 5
61
+ @token = login
62
+ end
63
+ end
64
+
65
+ def channel(id)
66
+ debug("Obtaining data for channel with id #{id}")
67
+ return @channels[id] if @channels[id]
68
+
69
+ response = RestClient.get Discordrb::Endpoints::CHANNELS + "/#{id}", {:Authorization => @token}
70
+ channel = Channel.new(JSON.parse(response), self)
71
+ @channels[id] = channel
72
+ end
73
+
74
+ def private_channel(id)
75
+ debug("Creating private channel with user id #{id}")
76
+ return @private_channels[id] if @private_channels[id]
77
+
78
+ data = {
79
+ 'recipient_id' => id
80
+ }
81
+
82
+ response = RestClient.post Discordrb::Endpoints::USERS + "/#{@bot_user.id}/channels", data.to_json, {:Authorization => @token, :content_type => :json}
83
+ channel = Channel.new(JSON.parse(response), self)
84
+ @private_channels[id] = channel
85
+ end
86
+
87
+ def user(id)
88
+ @users[id]
89
+ end
90
+
91
+ def server(id)
92
+ @servers[id]
93
+ end
94
+
95
+ def send_message(channel_id, content)
96
+ debug("Sending message to #{channel_id} with content '#{content}'")
97
+ data = {
98
+ 'content' => content.to_s,
99
+ 'mentions' => []
100
+ }
101
+ RestClient.post Discordrb::Endpoints::CHANNELS + "/#{channel_id}/messages", data.to_json, {:Authorization => @token, :content_type => :json}
102
+ end
103
+
104
+ def debug=(debug)
105
+ @debug = debug
106
+ end
107
+
108
+ def message(attributes = {}, &block)
109
+ register_event(MessageEvent, attributes, block)
110
+ end
111
+
112
+ def ready(attributes = {}, &block)
113
+ register_event(ReadyEvent, attributes, block)
114
+ end
115
+
116
+ def disconnected(attributes = {}, &block)
117
+ register_event(DisconnectEvent, attributes, block)
118
+ end
119
+
120
+ def typing(attributes = {}, &block)
121
+ register_event(TypingEvent, attributes, block)
122
+ end
123
+
124
+ def presence(attributes = {}, &block)
125
+ register_event(PresenceEvent, attributes, block)
126
+ end
127
+
128
+ def mention(attributes = {}, &block)
129
+ register_event(MentionEvent, attributes, block)
130
+ end
131
+
132
+ # Handle channel creation
133
+ # Attributes:
134
+ # * type: Channel type ('text' or 'voice')
135
+ # * name: Channel name
136
+ def channel_create(attributes = {}, &block)
137
+ register_event(ChannelCreateEvent, attributes, block)
138
+ end
139
+
140
+ # Handle channel update
141
+ # Attributes:
142
+ # * type: Channel type ('text' or 'voice')
143
+ # * name: Channel name
144
+ def channel_update(attributes = {}, &block)
145
+ register_event(ChannelUpdateEvent, attributes, block)
146
+ end
147
+
148
+ # Handle channel deletion
149
+ # Attributes:
150
+ # * type: Channel type ('text' or 'voice')
151
+ # * name: Channel name
152
+ def channel_delete(attributes = {}, &block)
153
+ register_event(ChannelDeleteEvent, attributes, block)
154
+ end
155
+
156
+ # Handle a change to a voice state.
157
+ # This includes joining a voice channel or changing mute or deaf state.
158
+ # Attributes:
159
+ # * from: User whose voice state changed
160
+ # * mute: server mute status
161
+ # * deaf: server deaf status
162
+ # * self_mute: self mute status
163
+ # * self_deaf: self deaf status
164
+ # * channel: channel the user joined
165
+ def voice_state_update(attributes = {}, &block)
166
+ register_event(VoiceStateUpdateEvent, attributes, block)
167
+ end
168
+
169
+ def remove_handler(handler)
170
+ clazz = event_class(handler.class)
171
+ @event_handlers[clazz].delete(handler)
172
+ end
173
+
174
+ def add_handler(handler)
175
+ clazz = event_class(handler.class)
176
+ @event_handlers[clazz] << handler
177
+ end
178
+
179
+ alias_method :<<, :add_handler
180
+
181
+ private
182
+
183
+ # Internal handler for PRESENCE_UPDATE
184
+ def update_presence(data)
185
+ user_id = data['user']['id'].to_i
186
+ server_id = data['guild_id'].to_i
187
+ server = @servers[server_id]
188
+ return if !server
189
+
190
+ user = @users[user_id]
191
+ if !user
192
+ user = User.new(data['user'], self)
193
+ @users[user_id] = user
194
+ end
195
+
196
+ status = data['status'].to_sym
197
+ if status != :offline
198
+ if !(server.members.find {|u| u.id == user.id })
199
+ server.members << user
200
+ end
201
+ end
202
+ user.status = status
203
+ user.game_id = data['game_id']
204
+ end
205
+
206
+ # Internal handler for VOICE_STATUS_UPDATE
207
+ def update_voice_state(data)
208
+ user_id = data['user_id'].to_i
209
+ server_id = data['guild_id'].to_i
210
+ server = @servers[server_id]
211
+ return if !server
212
+
213
+ user = @users[user_id]
214
+ user.server_mute = data['mute']
215
+ user.server_deaf = data['deaf']
216
+ user.self_mute = data['self_mute']
217
+ user.self_deaf = data['self_deaf']
218
+
219
+ channel_id = data['channel_id']
220
+ channel = nil
221
+ if channel_id
222
+ channel = @channels[channel_id.to_i]
223
+ end
224
+ user.move(channel)
225
+ end
226
+
227
+ # Internal handler for CHANNEL_CREATE
228
+ def create_channel(data)
229
+ channel = Channel.new(data, self)
230
+ server = channel.server
231
+ server.channels << channel
232
+ @channels[channel.id] = channel
233
+ end
234
+
235
+ # Internal handler for CHANNEL_UPDATE
236
+ def update_channel(data)
237
+ channel = Channel.new(data, self)
238
+ server = channel.server
239
+ old_channel = @channels[channel.id]
240
+ return if !old_channel
241
+ old_channel.update_from(channel)
242
+ end
243
+
244
+ # Internal handler for CHANNEL_DELETE
245
+ def delete_channel(data)
246
+ channel = Channel.new(data, self)
247
+ server = channel.server
248
+ @channels[channel.id] = nil
249
+ server.channels.reject! {|c| c.id == channel.id}
250
+ end
251
+
252
+ # Internal handler for GUILD_MEMBER_UPDATE
253
+ def update_guild_member(data)
254
+ user_data = data['user']
255
+ server_id = data['guild_id'].to_i
256
+ roles = []
257
+ data['roles'].each do |element|
258
+ role_id = element.to_i
259
+ roles << @servers[server_id].roles.find {|r| r.id == role_id}
260
+ end
261
+ user_id = user_data['id'].to_i
262
+ user = @users[user_id]
263
+ user.update_roles(roles)
264
+ end
265
+
266
+ # Internal handler for GUILD_ROLE_UPDATE
267
+ def update_guild_role(data)
268
+ role_data = data['role']
269
+ server_id = data['guild_id'].to_i
270
+ server = @servers[server_id]
271
+ new_role = Role.new(role_data, self, server)
272
+ role_id = role_data['id'].to_i
273
+ old_role = server.roles.find {|r| r.id == role_id}
274
+ old_role.update_from(new_role)
275
+ end
276
+
277
+ # Internal handler for GUILD_ROLE_CREATE
278
+ def create_guild_role(data)
279
+ role_data = data['role']
280
+ server_id = data['guild_id'].to_i
281
+ server = @servers[server_id]
282
+ new_role = Role.new(role_data, self, server)
283
+ server.add_role(new_role)
284
+ end
285
+
286
+ # Internal handler for GUILD_ROLE_DELETE
287
+ def delete_guild_role(data)
288
+ role_data = data['role']
289
+ role_id = role_data['id'].to_i
290
+ server_id = data['guild_id'].to_i
291
+ server = @servers[server_id]
292
+ server.delete_role(role_id)
293
+ end
294
+
295
+ def debug(message)
296
+ puts "[DEBUG @ #{Time.now.to_s}] #{message}" if @debug
297
+ end
298
+
299
+ def login
300
+ debug("Logging in")
301
+ login_attempts = login_attempts || 0
302
+
303
+ # Login
304
+ login_response = RestClient.post Discordrb::Endpoints::LOGIN, :email => @email, :password => @password
305
+ raise HTTPStatusException.new(login_response.code) if login_response.code >= 400
306
+
307
+ # Parse response
308
+ login_response_object = JSON.parse(login_response)
309
+ raise InvalidAuthenticationException unless login_response_object['token']
310
+
311
+ debug("Received token: #{login_response_object['token']}")
312
+ login_response_object['token']
313
+ rescue Exception => e
314
+ response_code = login_response.nil? ? 0 : login_response.code ######## mackmm145
315
+ if login_attempts < 100 && (e.inspect.include?("No such host is known.") || response_code == 523)
316
+ debug("Login failed! Reattempting in 5 seconds. #{100 - login_attempts} attempts remaining.")
317
+ debug("Error was: #{e.inspect}")
318
+ sleep 5
319
+ login_attempts += 1
320
+ retry
321
+ else
322
+ debug("Login failed permanently after #{login_attempts + 1} attempts")
323
+
324
+ # Apparently we get a 400 if the password or username is incorrect. In that case, tell the user
325
+ debug("Are you sure you're using the correct username and password?") if e.class == RestClient::BadRequest
326
+ raise $!
327
+ end
328
+ end
329
+
330
+ def get_gateway
331
+ # Get updated websocket_hub
332
+ response = RestClient.get Discordrb::Endpoints::GATEWAY, :authorization => @token
333
+ JSON.parse(response)["url"]
334
+ end
335
+
336
+ def websocket_connect
337
+ debug("Attempting to get gateway URL...")
338
+ websocket_hub = get_gateway
339
+ debug("Success! Gateway URL is #{websocket_hub}.")
340
+ debug("Now running bot")
341
+
342
+ EM.run {
343
+ @ws = Faye::WebSocket::Client.new(websocket_hub)
344
+
345
+ @ws.on :open do |event|; websocket_open(event); end
346
+ @ws.on :message do |event|; websocket_message(event); end
347
+ @ws.on :error do |event|; debug(event.message); end
348
+ @ws.on :close do |event|; websocket_close(event); @ws = nil; end
349
+ }
350
+ end
351
+
352
+ def websocket_message(event)
353
+ begin
354
+ debug("Received packet #{event.data}")
355
+
356
+ # Parse packet
357
+ packet = JSON.parse(event.data)
358
+
359
+ raise "Invalid Packet" unless packet['op'] == 0 # TODO
360
+
361
+ data = packet['d']
362
+ case packet['t']
363
+ when "READY"
364
+ # Activate the heartbeats
365
+ @heartbeat_interval = data['heartbeat_interval'].to_f / 1000.0
366
+ @heartbeat_active = true
367
+ debug("Desired heartbeat_interval: #{@heartbeat_interval}")
368
+
369
+ bot_user_id = data['user']['id'].to_i
370
+
371
+ # Initialize servers
372
+ @servers = {}
373
+ data['guilds'].each do |element|
374
+ server = Server.new(element, self)
375
+ @servers[server.id] = server
376
+
377
+ # Initialize users
378
+ server.members.each do |element|
379
+ @users[element.id] = element
380
+ end
381
+
382
+ # Save the bot user
383
+ @bot_user = @users[bot_user_id]
384
+ end
385
+
386
+ # Add private channels
387
+ @private_channels = {}
388
+ data['private_channels'].each do |element|
389
+ channel = Channel.new(element, self)
390
+ @channels[channel.id] = channel
391
+ @private_channels[channel.recipient.id] = channel
392
+ end
393
+
394
+ # Make sure to raise the event
395
+ raise_event(ReadyEvent.new)
396
+ when "MESSAGE_CREATE"
397
+ message = Message.new(data, self)
398
+ event = MessageEvent.new(message, self)
399
+ raise_event(event)
400
+
401
+ if message.mentions.any? { |user| user.id == @bot_user.id }
402
+ event = MentionEvent.new(message, self)
403
+ raise_event(event)
404
+ end
405
+ when "TYPING_START"
406
+ event = TypingEvent.new(data, self)
407
+ raise_event(event)
408
+ when "PRESENCE_UPDATE"
409
+ update_presence(data)
410
+ event = PresenceEvent.new(data, self)
411
+ raise_event(event)
412
+ when "VOICE_STATE_UPDATE"
413
+ update_voice_state(data)
414
+ event = VoiceStateUpdateEvent.new(data, self)
415
+ raise_event(event)
416
+ when "CHANNEL_CREATE"
417
+ create_channel(data)
418
+ event = ChannelCreateEvent.new(data, self)
419
+ raise_event(event)
420
+ when "CHANNEL_UPDATE"
421
+ update_channel(data)
422
+ event = ChannelUpdateEvent.new(data, self)
423
+ raise_event(event)
424
+ when "CHANNEL_DELETE"
425
+ delete_channel(data)
426
+ event = ChannelDeleteEvent.new(data, self)
427
+ raise_event(event)
428
+ when "GUILD_MEMBER_UPDATE"
429
+ update_guild_member(data)
430
+ event = GuildMemberUpdateEvent.new(data, self)
431
+ raise_event(event)
432
+ when "GUILD_ROLE_UPDATE"
433
+ update_guild_role(data)
434
+ event = GuildRoleUpdateEvent.new(data, self)
435
+ raise_event(event)
436
+ when "GUILD_ROLE_CREATE"
437
+ create_guild_role(data)
438
+ event = GuildRoleCreateEvent.new(data, self)
439
+ raise_event(event)
440
+ when "GUILD_ROLE_DELETE"
441
+ delete_guild_role(data)
442
+ event = GuildRoleDeleteEvent.new(data, self)
443
+ raise_event(event)
444
+ end
445
+ rescue Exception => e
446
+ debug("Exception: #{e.inspect}")
447
+ e.backtrace.each {|line| debug(line) }
448
+ end
449
+ end
450
+
451
+ def websocket_close(event)
452
+ debug("Disconnected from WebSocket!")
453
+ debug(" (Reason: #{event.reason})")
454
+ debug(" (Code: #{event.code})")
455
+ raise_event(DisconnectEvent.new)
456
+ EM.stop
457
+ end
458
+
459
+ def websocket_open(event)
460
+ # Send the initial packet
461
+ packet = {
462
+ "op" => 2, # Packet identifier
463
+ "d" => { # Packet data
464
+ "v" => 2, # Another identifier
465
+ "token" => @token,
466
+ "properties" => { # I'm unsure what these values are for exactly, but they don't appear to impact bot functionality in any way.
467
+ "$os" => "Linux",
468
+ "$browser" => "",
469
+ "$device" => "discordrb",
470
+ "$referrer" => "",
471
+ "$referring_domain" => ""
472
+ }
473
+ }
474
+ }
475
+
476
+ @ws.send(packet.to_json)
477
+ end
478
+
479
+ def raise_event(event)
480
+ debug("Raised a #{event.class}")
481
+ handlers = @event_handlers[event.class]
482
+ (handlers || []).each do |handler|
483
+ handler.match(event)
484
+ end
485
+ end
486
+
487
+ def register_event(clazz, attributes, block)
488
+ handler = handler_class(clazz).new(attributes, block)
489
+
490
+ @event_handlers[clazz] ||= []
491
+ @event_handlers[clazz] << handler
492
+
493
+ # Return the handler so it can be removed later
494
+ handler
495
+ end
496
+
497
+ def send_heartbeat
498
+ millis = Time.now.strftime("%s%L").to_i
499
+ debug("Sending heartbeat at #{millis}")
500
+ data = {
501
+ 'op' => 1,
502
+ 'd' => millis
503
+ }
504
+
505
+ @ws.send(data.to_json)
506
+ end
507
+
508
+ def class_from_string(str)
509
+ str.split('::').inject(Object) do |mod, class_name|
510
+ mod.const_get(class_name)
511
+ end
512
+ end
513
+
514
+ def event_class(handler_class)
515
+ class_name = handler_class.to_s
516
+ return nil unless class_name.end_with? "Handler"
517
+
518
+ class_from_string(class_name[0..-8])
519
+ end
520
+
521
+ def handler_class(event_class)
522
+ class_from_string(event_class.to_s + "Handler")
523
+ end
524
+ end
525
+ end