twitch-bot 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a10dd280ea4e2cf20e4d4066f119b13622aae30e7d03f9bd3c6407113b05e5b8
4
+ data.tar.gz: 58c80ab7b5c89b67e317986713e854d4797048bca8d5cde5c11da2966022a54f
5
+ SHA512:
6
+ metadata.gz: c8c987740eff97e5b02e8b0b3a9c996d43948de76cedfe3ba83464c9115f025b81691f0de167cdaa91a82a4499d5c1a88ac5d926d1260f7a76b7ea280b7b9d02
7
+ data.tar.gz: 00a30a330696adbbf506fc4858b52a8e1cb4670ad56a49606433ad05d185868f62bed7ca4e09b0758a40d8817cd513ce59c10cd099e979b64fbf04209eb707b4
data/.editorconfig ADDED
@@ -0,0 +1,13 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
13
+ indent_size = 2
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /bin/
11
+ *.bundle
12
+ *.so
13
+ *.o
14
+ *.a
15
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ inherit_gem:
2
+ freistil-rubocop:
3
+ - default.yml
4
+
5
+ AllCops:
6
+ Exclude:
7
+ - bin/rake
8
+ - bin/rspec
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.6.5
@@ -0,0 +1,28 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "name": "RSpec - active spec file only",
9
+ "type": "Ruby",
10
+ "request": "launch",
11
+ "cwd": "${workspaceRoot}",
12
+ "program": "${workspaceRoot}/bin/rspec",
13
+ "args": [
14
+ "${file}"
15
+ ]
16
+ },
17
+ {
18
+ "name": "Rspec - all spec files",
19
+ "type": "Ruby",
20
+ "request": "launch",
21
+ "cwd": "${workspaceRoot}",
22
+ "program": "${workspaceRoot}/bin/rspec",
23
+ "args": [
24
+ "spec"
25
+ ]
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at TODO: Write your email address. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: "bundle exec rspec" do
4
+ require "guard/rspec/dsl"
5
+ dsl = Guard::RSpec::Dsl.new(self)
6
+
7
+ rspec = dsl.rspec
8
+ watch(rspec.spec_helper) { rspec.spec_dir }
9
+ watch(rspec.spec_support) { rspec.spec_dir }
10
+ watch(rspec.spec_files)
11
+
12
+ ruby = dsl.ruby
13
+ dsl.watch_spec_files_for(ruby.lib_files)
14
+
15
+ # Rails files
16
+ rails = dsl.rails(view_extensions: %w[erb haml slim])
17
+ dsl.watch_spec_files_for(rails.app_files)
18
+ dsl.watch_spec_files_for(rails.views)
19
+
20
+ watch(rails.controllers) do |m|
21
+ [
22
+ rspec.spec.call("routing/#{m[1]}_routing"),
23
+ rspec.spec.call("controllers/#{m[1]}_controller"),
24
+ rspec.spec.call("acceptance/#{m[1]}"),
25
+ ]
26
+ end
27
+
28
+ # Rails config changes
29
+ watch(rails.spec_helper) { rspec.spec_dir }
30
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
31
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
32
+
33
+ # Capybara features specs
34
+ watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
35
+ watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
36
+
37
+ # Turnip features and steps
38
+ watch(%r{^spec/acceptance/(.+)\.feature$})
39
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
40
+ Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
41
+ end
42
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Pavel Astraukh
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # Twitch::Bot
2
+
3
+ `twitch-bot` provides a Twitch chat client object that can be used for building Twitch chat bots.
4
+
5
+ This gem is based on the `twitch-chat` gem by https://github.com/EnotPoloskun.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's `Gemfile`:
10
+
11
+ ```ruby
12
+ gem 'twitch-bot'
13
+ ```
14
+
15
+ Install all the dependencies:
16
+
17
+ ```
18
+ $ bundle
19
+ ```
20
+
21
+ Or install it manually via:
22
+
23
+ ```
24
+ $ gem install twitch-bot
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```ruby
30
+ require "twitch/bot"
31
+
32
+ class JoinHandler < Twitch::Bot::EventHandler
33
+ def call
34
+ client.send_message "Hi guys!"
35
+ end
36
+
37
+ def self.handled_events
38
+ [:join]
39
+ end
40
+ end
41
+
42
+ class SubscriptionHandler < Twitch::Bot::EventHandler
43
+ def call
44
+ client.send_message "Hi #{event.user}, thank you for your subscription"
45
+ end
46
+
47
+ def self.handled_events
48
+ [:subscription]
49
+ end
50
+ end
51
+
52
+ class TimeCommandHandler < Twitch::Bot::EventHandler
53
+ def call
54
+ if event.bot_command?("time")
55
+ client.send_message "Current time: #{Time.now.utc}"
56
+ end
57
+ end
58
+
59
+ def self.handled_events
60
+ [:chat_message]
61
+ end
62
+ end
63
+
64
+ connection = Twitch::Bot::Connection.new(
65
+ nickname: "test",
66
+ password: "secret",
67
+ )
68
+
69
+ client = Twitch::Bot::Client.new(
70
+ connection: connection,
71
+ channel: "test",
72
+ ) do
73
+ register_handler(JoinHandler)
74
+ register_handler(SubscriptionHandler)
75
+ register_handler(TimeCommandHandler)
76
+ end
77
+
78
+ client.run
79
+ ```
80
+
81
+ ## Supported event types
82
+
83
+ * ``:authenticated``
84
+ * ``:join``
85
+ * ``:message``
86
+ * ``:slow_mode``
87
+ * ``:r9k_mode``
88
+ * ``:followers_mode``
89
+ * ``:subscribers_mode``
90
+ * ``:subscribe``
91
+ * ``:stop``
92
+ * ``:not_supported``
93
+
94
+ ## Contributing
95
+
96
+ 1. Fork the repo (https://github.com/geewiz/twitch-bot/fork)
97
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
98
+ 3. Commit your changes (`git commit -a`)
99
+ 4. Push the branch (`git push origin my-new-feature`)
100
+ 5. Submit a Pull Request from your Github repository
101
+
102
+ Please take note of the Code Of Conduct in `CODE_OF_CONDUCT.md`.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task test: %i[rubocop spec]
4
+
5
+ begin
6
+ require "rspec/core/rake_task"
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ task default: :spec
9
+ rescue LoadError
10
+ puts "RSpec is unavailable"
11
+ end
12
+
13
+ require "rubocop/rake_task"
14
+ RuboCop::RakeTask.new
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twitch
4
+ module Bot
5
+ # Twitch channel management
6
+ class Channel
7
+ attr_reader :name, :moderators
8
+
9
+ def initialize(name)
10
+ @name = name.downcase
11
+ @moderators = []
12
+ end
13
+
14
+ def add_moderator(moderator)
15
+ @moderators << moderator
16
+ end
17
+
18
+ def remove_moderator(moderator)
19
+ @moderators.delete moderator
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "socket"
5
+
6
+ Thread.abort_on_exception = true
7
+
8
+ module Twitch
9
+ module Bot
10
+ # Twitch chat client object
11
+ class Client
12
+ MODERATOR_MESSAGES_COUNT = 100
13
+ USER_MESSAGES_COUNT = 20
14
+ TWITCH_PERIOD = 30.0
15
+
16
+ # Respond to a :ping event with a pong so we don't get disconnected.
17
+ class PingHandler < Twitch::Bot::EventHandler
18
+ def call
19
+ client.send_data "PONG :#{event.hostname}"
20
+ end
21
+
22
+ def self.handled_events
23
+ [:ping]
24
+ end
25
+ end
26
+
27
+ # Handle the :authenticated event required for joining our channel.
28
+ class AuthenticatedHandler < Twitch::Bot::EventHandler
29
+ def call
30
+ client.join_default_channel
31
+ end
32
+
33
+ def self.handled_events
34
+ [:authenticated]
35
+ end
36
+ end
37
+
38
+ # Handle a change in moderators on the channel.
39
+ class ModeHandler < Twitch::Bot::EventHandler
40
+ def call
41
+ user = event.user
42
+ case event.mode
43
+ when :add_moderator
44
+ client.add_moderator(user)
45
+ when :remove_moderator
46
+ client.remove_moderator(user)
47
+ end
48
+ end
49
+
50
+ def self.handled_events
51
+ [:mode]
52
+ end
53
+ end
54
+
55
+ # Represent the event triggered when quitting the client loop.
56
+ class StopEvent < Twitch::Bot::Event
57
+ def initialize
58
+ @type = :stop
59
+ end
60
+ end
61
+
62
+ attr_reader :connection
63
+
64
+ def initialize(
65
+ connection:, output: STDOUT, channel: nil, &block
66
+ )
67
+ @connection = connection
68
+ @logger = Logger.new(output)
69
+ @channel = Twitch::Bot::Channel.new(channel) if channel
70
+ @messages_queue = []
71
+ @running = false
72
+ @event_handlers = {}
73
+
74
+ execute_initialize_block block if block
75
+ register_default_handlers
76
+ end
77
+
78
+ def trigger(event)
79
+ type = event.type
80
+ logger.debug "Triggered #{type}"
81
+ (event_handlers[type] || []).each do |handler_class|
82
+ logger.debug "Calling #{handler_class}..."
83
+ handler_class.new(event: event, client: self).call
84
+ end
85
+ end
86
+
87
+ def register_handler(handler)
88
+ handler.handled_events.each do |event_type|
89
+ (event_handlers[event_type] ||= []) << handler
90
+ end
91
+ end
92
+
93
+ def run
94
+ raise "Already running" if running
95
+
96
+ @running = true
97
+
98
+ %w[TERM INT].each { |signal| trap(signal) { stop } }
99
+
100
+ connect
101
+ input_thread.join
102
+ messages_thread.join
103
+ logger.debug "Joined"
104
+ end
105
+
106
+ def join(channel)
107
+ @channel = Channel.new(channel)
108
+ send_data "JOIN ##{@channel.name}"
109
+ end
110
+
111
+ def part
112
+ send_data "PART ##{@channel.name}"
113
+ @channel = nil
114
+ @messages_queue = []
115
+ end
116
+
117
+ def send_message(message)
118
+ @messages_queue << message if @messages_queue.last != message
119
+ end
120
+
121
+ def max_messages_count
122
+ if @channel.moderators.include?(connection.nickname)
123
+ MODERATOR_MESSAGES_COUNT
124
+ else
125
+ USER_MESSAGES_COUNT
126
+ end
127
+ end
128
+
129
+ def message_delay
130
+ TWITCH_PERIOD / max_messages_count
131
+ end
132
+
133
+ def stop
134
+ trigger StopEvent.new
135
+ @running = false
136
+ part if @channel
137
+ end
138
+
139
+ def send_data(data)
140
+ log_data = data.gsub(/(PASS oauth:)(\w+)/) do
141
+ "#{Regexp.last_match(1)}#{'*' * Regexp.last_match(2).size}"
142
+ end
143
+
144
+ logger.info "< #{log_data}"
145
+ socket.puts(data)
146
+ end
147
+
148
+ def join_default_channel
149
+ join @channel.name if @channel
150
+ end
151
+
152
+ def add_moderator(user)
153
+ channel.add_moderator(user)
154
+ end
155
+
156
+ def remove_moderator(user)
157
+ channel.remove_moderator(user)
158
+ end
159
+
160
+ private
161
+
162
+ attr_reader :event_handlers, :running, :input_thread, :messages_thread,
163
+ :socket, :logger
164
+
165
+ def connect
166
+ @socket = ::TCPSocket.new(connection.hostname, connection.port)
167
+
168
+ start_input_thread
169
+ start_messages_thread
170
+ enable_twitch_capabilities
171
+ authenticate
172
+ end
173
+
174
+ def enable_twitch_capabilities
175
+ send_data <<~DATA
176
+ CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership
177
+ DATA
178
+ end
179
+
180
+ def authenticate
181
+ send_data "PASS #{connection.password}"
182
+ send_data "NICK #{connection.nickname}"
183
+ end
184
+
185
+ def start_input_thread
186
+ @input_thread = Thread.start do
187
+ while running
188
+ line = read_socket
189
+ logger.info "> #{line}"
190
+ irc_message = IrcMessage.new(line)
191
+ trigger(Twitch::Bot::MessageParser.new(irc_message).message)
192
+ end
193
+
194
+ logger.debug "End of input thread"
195
+ socket.close
196
+ end
197
+ end
198
+
199
+ # Acceptable :reek:NilCheck
200
+ def read_socket
201
+ line = ""
202
+ while line.empty?
203
+ line = socket.gets&.chomp
204
+ end
205
+ line
206
+ end
207
+
208
+ def start_messages_thread
209
+ @messages_thread = Thread.start do
210
+ while running
211
+ sleep message_delay
212
+
213
+ # TODO: Replace with core Queue
214
+ if (message = @messages_queue.pop)
215
+ send_data "PRIVMSG ##{@channel.name} :#{message}"
216
+ end
217
+ end
218
+
219
+ logger.debug "End of messages thread"
220
+ end
221
+ end
222
+
223
+ def execute_initialize_block(block)
224
+ if block.arity == 1
225
+ block.call self
226
+ else
227
+ instance_eval(&block)
228
+ end
229
+ end
230
+
231
+ def register_default_handlers
232
+ register_handler(Twitch::Bot::Client::PingHandler)
233
+ register_handler(Twitch::Bot::Client::AuthenticatedHandler)
234
+ register_handler(Twitch::Bot::Client::ModeHandler)
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twitch
4
+ module Bot
5
+ # This class stores the connection details for the client.
6
+ class Connection
7
+ attr_reader :nickname, :password, :hostname, :port
8
+
9
+ def initialize(
10
+ nickname:, password:, hostname: "irc.chat.twitch.tv", port: "6667"
11
+ )
12
+ @nickname = nickname
13
+ @password = password
14
+ @hostname = hostname
15
+ @port = port
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twitch
4
+ module Bot
5
+ # Represent a generic Twitch chat event/message
6
+ class Event
7
+ attr_reader :type
8
+
9
+ def initialize(type: :unknown)
10
+ @type = type
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twitch
4
+ module Bot
5
+ # Handles a message of a specific type
6
+ class EventHandler
7
+ attr_reader :event, :client
8
+
9
+ def initialize(event:, client:)
10
+ @event = event
11
+ @client = client
12
+ end
13
+
14
+ #
15
+ # Handle the event
16
+ #
17
+ # @return void
18
+ #
19
+ def call
20
+ raise "Unhandled #{event.type}"
21
+ end
22
+
23
+ #
24
+ # Return a list of event types this handler can handle
25
+ #
26
+ # @return [Array] event type list
27
+ #
28
+ def self.handled_events
29
+ []
30
+ end
31
+ end
32
+ end
33
+ end