twitch-bot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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