grinch 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 +7 -0
- data/.yardopts +1 -0
- data/LICENSE +22 -0
- data/README.md +180 -0
- data/docs/bot_options.md +454 -0
- data/docs/changes.md +541 -0
- data/docs/common_mistakes.md +60 -0
- data/docs/common_tasks.md +57 -0
- data/docs/encodings.md +69 -0
- data/docs/events.md +273 -0
- data/docs/getting_started.md +184 -0
- data/docs/logging.md +90 -0
- data/docs/migrating.md +267 -0
- data/docs/plugins.md +4 -0
- data/docs/readme.md +20 -0
- data/examples/basic/autovoice.rb +32 -0
- data/examples/basic/google.rb +35 -0
- data/examples/basic/hello.rb +15 -0
- data/examples/basic/join_part.rb +34 -0
- data/examples/basic/memo.rb +39 -0
- data/examples/basic/msg.rb +16 -0
- data/examples/basic/seen.rb +36 -0
- data/examples/basic/urban_dict.rb +35 -0
- data/examples/basic/url_shorten.rb +35 -0
- data/examples/plugins/autovoice.rb +37 -0
- data/examples/plugins/custom_prefix.rb +23 -0
- data/examples/plugins/dice_roll.rb +38 -0
- data/examples/plugins/google.rb +36 -0
- data/examples/plugins/hello.rb +22 -0
- data/examples/plugins/hooks.rb +36 -0
- data/examples/plugins/join_part.rb +42 -0
- data/examples/plugins/lambdas.rb +35 -0
- data/examples/plugins/last_nick.rb +24 -0
- data/examples/plugins/msg.rb +22 -0
- data/examples/plugins/multiple_matches.rb +32 -0
- data/examples/plugins/own_events.rb +37 -0
- data/examples/plugins/seen.rb +45 -0
- data/examples/plugins/timer.rb +22 -0
- data/examples/plugins/url_shorten.rb +33 -0
- data/lib/cinch.rb +5 -0
- data/lib/cinch/ban.rb +50 -0
- data/lib/cinch/bot.rb +479 -0
- data/lib/cinch/cached_list.rb +19 -0
- data/lib/cinch/callback.rb +20 -0
- data/lib/cinch/channel.rb +463 -0
- data/lib/cinch/channel_list.rb +29 -0
- data/lib/cinch/configuration.rb +73 -0
- data/lib/cinch/configuration/bot.rb +48 -0
- data/lib/cinch/configuration/dcc.rb +16 -0
- data/lib/cinch/configuration/plugins.rb +41 -0
- data/lib/cinch/configuration/sasl.rb +19 -0
- data/lib/cinch/configuration/ssl.rb +19 -0
- data/lib/cinch/configuration/timeouts.rb +14 -0
- data/lib/cinch/constants.rb +533 -0
- data/lib/cinch/dcc.rb +12 -0
- data/lib/cinch/dcc/dccable_object.rb +37 -0
- data/lib/cinch/dcc/incoming.rb +1 -0
- data/lib/cinch/dcc/incoming/send.rb +147 -0
- data/lib/cinch/dcc/outgoing.rb +1 -0
- data/lib/cinch/dcc/outgoing/send.rb +122 -0
- data/lib/cinch/exceptions.rb +46 -0
- data/lib/cinch/formatting.rb +125 -0
- data/lib/cinch/handler.rb +118 -0
- data/lib/cinch/handler_list.rb +90 -0
- data/lib/cinch/helpers.rb +231 -0
- data/lib/cinch/irc.rb +924 -0
- data/lib/cinch/isupport.rb +98 -0
- data/lib/cinch/log_filter.rb +21 -0
- data/lib/cinch/logger.rb +168 -0
- data/lib/cinch/logger/formatted_logger.rb +97 -0
- data/lib/cinch/logger/zcbot_logger.rb +22 -0
- data/lib/cinch/logger_list.rb +85 -0
- data/lib/cinch/mask.rb +69 -0
- data/lib/cinch/message.rb +392 -0
- data/lib/cinch/message_queue.rb +107 -0
- data/lib/cinch/mode_parser.rb +76 -0
- data/lib/cinch/network.rb +104 -0
- data/lib/cinch/open_ended_queue.rb +26 -0
- data/lib/cinch/pattern.rb +65 -0
- data/lib/cinch/plugin.rb +515 -0
- data/lib/cinch/plugin_list.rb +38 -0
- data/lib/cinch/rubyext/float.rb +3 -0
- data/lib/cinch/rubyext/module.rb +26 -0
- data/lib/cinch/rubyext/string.rb +33 -0
- data/lib/cinch/sasl.rb +34 -0
- data/lib/cinch/sasl/dh_blowfish.rb +71 -0
- data/lib/cinch/sasl/diffie_hellman.rb +47 -0
- data/lib/cinch/sasl/mechanism.rb +6 -0
- data/lib/cinch/sasl/plain.rb +26 -0
- data/lib/cinch/syncable.rb +83 -0
- data/lib/cinch/target.rb +199 -0
- data/lib/cinch/timer.rb +145 -0
- data/lib/cinch/user.rb +488 -0
- data/lib/cinch/user_list.rb +87 -0
- data/lib/cinch/utilities/deprecation.rb +16 -0
- data/lib/cinch/utilities/encoding.rb +37 -0
- data/lib/cinch/utilities/kernel.rb +13 -0
- data/lib/cinch/version.rb +4 -0
- metadata +140 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
module Cinch
|
2
|
+
# @since 2.0.0
|
3
|
+
class Handler
|
4
|
+
# @return [Bot]
|
5
|
+
attr_reader :bot
|
6
|
+
|
7
|
+
# @return [Symbol]
|
8
|
+
attr_reader :event
|
9
|
+
|
10
|
+
# @return [Pattern]
|
11
|
+
attr_reader :pattern
|
12
|
+
|
13
|
+
# @return [Array]
|
14
|
+
attr_reader :args
|
15
|
+
|
16
|
+
# @return [Proc]
|
17
|
+
attr_reader :block
|
18
|
+
|
19
|
+
# @return [Symbol]
|
20
|
+
attr_reader :group
|
21
|
+
|
22
|
+
# @return [Boolean]
|
23
|
+
attr_reader :strip_colors
|
24
|
+
|
25
|
+
# @return [ThreadGroup]
|
26
|
+
# @api private
|
27
|
+
attr_reader :thread_group
|
28
|
+
|
29
|
+
# @param [Bot] bot
|
30
|
+
# @param [Symbol] event
|
31
|
+
# @param [Pattern] pattern
|
32
|
+
# @param [Hash] options
|
33
|
+
# @option options [Symbol] :group (nil) Match group the h belongs
|
34
|
+
# to.
|
35
|
+
# @option options [Boolean] :execute_in_callback (false) Whether
|
36
|
+
# to execute the handler in an instance of {Callback}
|
37
|
+
# @option options [Boolean] :strip_colors (false) Strip colors
|
38
|
+
# from message before attemping match
|
39
|
+
# @option options [Array] :args ([]) Additional arguments to pass
|
40
|
+
# to the block
|
41
|
+
def initialize(bot, event, pattern, options = {}, &block)
|
42
|
+
options = {
|
43
|
+
:group => nil,
|
44
|
+
:execute_in_callback => false,
|
45
|
+
:strip_colors => false,
|
46
|
+
:args => []
|
47
|
+
}.merge(options)
|
48
|
+
@bot = bot
|
49
|
+
@event = event
|
50
|
+
@pattern = pattern
|
51
|
+
@group = options[:group]
|
52
|
+
@execute_in_callback = options[:execute_in_callback]
|
53
|
+
@strip_colors = options[:strip_colors]
|
54
|
+
@args = options[:args]
|
55
|
+
@block = block
|
56
|
+
|
57
|
+
@thread_group = ThreadGroup.new
|
58
|
+
end
|
59
|
+
|
60
|
+
# Unregisters the handler.
|
61
|
+
#
|
62
|
+
# @return [void]
|
63
|
+
def unregister
|
64
|
+
@bot.handlers.unregister(self)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Stops execution of the handler. This means stopping and killing
|
68
|
+
# all associated threads.
|
69
|
+
#
|
70
|
+
# @return [void]
|
71
|
+
def stop
|
72
|
+
@bot.loggers.debug "[Stopping handler] Stopping all threads of handler #{self}: #{@thread_group.list.size} threads..."
|
73
|
+
@thread_group.list.each do |thread|
|
74
|
+
Thread.new do
|
75
|
+
@bot.loggers.debug "[Ending thread] Waiting 10 seconds for #{thread} to finish..."
|
76
|
+
thread.join(10)
|
77
|
+
@bot.loggers.debug "[Killing thread] Killing #{thread}"
|
78
|
+
thread.kill
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Executes the handler.
|
84
|
+
#
|
85
|
+
# @param [Message] message Message that caused the invocation
|
86
|
+
# @param [Array] captures Capture groups of the pattern that are
|
87
|
+
# being passed as arguments
|
88
|
+
# @return [Thread]
|
89
|
+
def call(message, captures, arguments)
|
90
|
+
bargs = captures + arguments
|
91
|
+
|
92
|
+
thread = Thread.new {
|
93
|
+
@bot.loggers.debug "[New thread] For #{self}: #{Thread.current} -- #{@thread_group.list.size} in total."
|
94
|
+
|
95
|
+
begin
|
96
|
+
if @execute_in_callback
|
97
|
+
@bot.callback.instance_exec(message, *@args, *bargs, &@block)
|
98
|
+
else
|
99
|
+
@block.call(message, *@args, *bargs)
|
100
|
+
end
|
101
|
+
rescue => e
|
102
|
+
@bot.loggers.exception(e)
|
103
|
+
ensure
|
104
|
+
@bot.loggers.debug "[Thread done] For #{self}: #{Thread.current} -- #{@thread_group.list.size - 1} remaining."
|
105
|
+
end
|
106
|
+
}
|
107
|
+
|
108
|
+
@thread_group.add(thread)
|
109
|
+
thread
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [String]
|
113
|
+
def to_s
|
114
|
+
# TODO maybe add the number of running threads to the output?
|
115
|
+
"#<Cinch::Handler @event=#{@event.inspect} pattern=#{@pattern.inspect}>"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "thread"
|
2
|
+
require "set"
|
3
|
+
require "cinch/cached_list"
|
4
|
+
|
5
|
+
module Cinch
|
6
|
+
# @since 2.0.0
|
7
|
+
class HandlerList
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@handlers = Hash.new {|h,k| h[k] = []}
|
12
|
+
@mutex = Mutex.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def register(handler)
|
16
|
+
@mutex.synchronize do
|
17
|
+
handler.bot.loggers.debug "[on handler] Registering handler with pattern `#{handler.pattern.inspect}`, reacting on `#{handler.event}`"
|
18
|
+
@handlers[handler.event] << handler
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Handler, Array<Handler>] handlers The handlers to unregister
|
23
|
+
# @return [void]
|
24
|
+
# @see Handler#unregister
|
25
|
+
def unregister(*handlers)
|
26
|
+
@mutex.synchronize do
|
27
|
+
handlers.each do |handler|
|
28
|
+
@handlers[handler.event].delete(handler)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
# @return [Array<Handler>]
|
35
|
+
def find(type, msg = nil)
|
36
|
+
if handlers = @handlers[type]
|
37
|
+
if msg.nil?
|
38
|
+
return handlers
|
39
|
+
end
|
40
|
+
|
41
|
+
handlers = handlers.select { |handler|
|
42
|
+
msg.match(handler.pattern.to_r(msg), type, handler.strip_colors)
|
43
|
+
}.group_by {|handler| handler.group}
|
44
|
+
|
45
|
+
handlers.values_at(*(handlers.keys - [nil])).map(&:first) + (handlers[nil] || [])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param [Symbol] event The event type
|
50
|
+
# @param [Message, nil] msg The message which is responsible for
|
51
|
+
# and attached to the event, or nil.
|
52
|
+
# @param [Array] arguments A list of additional arguments to pass
|
53
|
+
# to event handlers
|
54
|
+
# @return [Array<Thread>]
|
55
|
+
def dispatch(event, msg = nil, *arguments)
|
56
|
+
threads = []
|
57
|
+
|
58
|
+
if handlers = find(event, msg)
|
59
|
+
already_run = Set.new
|
60
|
+
handlers.each do |handler|
|
61
|
+
next if already_run.include?(handler.block)
|
62
|
+
already_run << handler.block
|
63
|
+
# calling Message#match multiple times is not a problem
|
64
|
+
# because we cache the result
|
65
|
+
if msg
|
66
|
+
captures = msg.match(handler.pattern.to_r(msg), event, handler.strip_colors).captures
|
67
|
+
else
|
68
|
+
captures = []
|
69
|
+
end
|
70
|
+
|
71
|
+
threads << handler.call(msg, captures, arguments)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
threads
|
76
|
+
end
|
77
|
+
|
78
|
+
# @yield [handler] Yields all registered handlers
|
79
|
+
# @yieldparam [Handler] handler
|
80
|
+
# @return [void]
|
81
|
+
def each(&block)
|
82
|
+
@handlers.values.flatten.each(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @api private
|
86
|
+
def stop_all
|
87
|
+
each { |h| h.stop }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
module Cinch
|
3
|
+
# The Helpers module contains a number of methods whose purpose is
|
4
|
+
# to make writing plugins easier by hiding parts of the API. The
|
5
|
+
# {#Channel} helper, for example, provides an easier way for turning
|
6
|
+
# a String object into a {Channel} object than directly using
|
7
|
+
# {ChannelList}: Compare `Channel("#some_channel")` with
|
8
|
+
# `bot.channel_list.find_ensured("#some_channel")`.
|
9
|
+
#
|
10
|
+
# The Helpers module automatically gets included in all plugins.
|
11
|
+
module Helpers
|
12
|
+
# @group Type casts
|
13
|
+
|
14
|
+
# Helper method for turning a String into a {Target} object.
|
15
|
+
#
|
16
|
+
# @param [String] target a target name
|
17
|
+
# @return [Target] a {Target} object
|
18
|
+
# @example
|
19
|
+
# on :message, /^message (.+)$/ do |m, target|
|
20
|
+
# Target(target).send "hi!"
|
21
|
+
# end
|
22
|
+
# @since 2.0.0
|
23
|
+
def Target(target)
|
24
|
+
return target if target.is_a?(Target)
|
25
|
+
Target.new(target, bot)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Helper method for turning a String into a {Channel} object.
|
29
|
+
#
|
30
|
+
# @param [String] channel a channel name
|
31
|
+
# @return [Channel] a {Channel} object
|
32
|
+
# @example
|
33
|
+
# on :message, /^please join (#.+)$/ do |m, target|
|
34
|
+
# Channel(target).join
|
35
|
+
# end
|
36
|
+
# @since 1.0.0
|
37
|
+
def Channel(channel)
|
38
|
+
return channel if channel.is_a?(Channel)
|
39
|
+
bot.channel_list.find_ensured(channel)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Helper method for turning a String into an {User} object.
|
43
|
+
#
|
44
|
+
# @param [String] user a user's nickname
|
45
|
+
# @return [User] an {User} object
|
46
|
+
# @example
|
47
|
+
# on :message, /^tell me everything about (.+)$/ do |m, target|
|
48
|
+
# user = User(target)
|
49
|
+
# m.reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
|
50
|
+
# end
|
51
|
+
# @since 1.0.0
|
52
|
+
def User(user)
|
53
|
+
return user if user.is_a?(User)
|
54
|
+
if user == bot.nick
|
55
|
+
bot
|
56
|
+
else
|
57
|
+
bot.user_list.find_ensured(user)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @example Used as a class method in a plugin
|
62
|
+
# timer 5, method: :some_method
|
63
|
+
# def some_method
|
64
|
+
# Channel("#cinch-bots").send(Time.now.to_s)
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# @example Used as an instance method in a plugin
|
68
|
+
# match "start timer"
|
69
|
+
# def execute(m)
|
70
|
+
# Timer(5) { puts "timer fired" }
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# @example Used as an instance method in a traditional `on` handler
|
74
|
+
# on :message, "start timer" do
|
75
|
+
# Timer(5) { puts "timer fired" }
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# @param [Numeric] interval Interval in seconds
|
79
|
+
# @param [Proc] block A proc to execute
|
80
|
+
# @option options [Symbol] :method (:timer) Method to call (only
|
81
|
+
# if no proc is provided)
|
82
|
+
# @option options [Boolean] :threaded (true) Call method in a
|
83
|
+
# thread?
|
84
|
+
# @option options [Integer] :shots (Float::INFINITY) How often
|
85
|
+
# should the timer fire?
|
86
|
+
# @option options [Boolean] :start_automatically (true) If true,
|
87
|
+
# the timer will automatically start after the bot finished
|
88
|
+
# connecting.
|
89
|
+
# @option options [Boolean] :stop_automaticall (true) If true, the
|
90
|
+
# timer will automatically stop when the bot disconnects.
|
91
|
+
# @return [Timer]
|
92
|
+
# @since 2.0.0
|
93
|
+
def Timer(interval, options = {}, &block)
|
94
|
+
options = {:method => :timer, :threaded => true, :interval => interval}.merge(options)
|
95
|
+
block ||= self.method(options[:method])
|
96
|
+
timer = Cinch::Timer.new(bot, options, &block)
|
97
|
+
timer.start
|
98
|
+
|
99
|
+
if self.respond_to?(:timers)
|
100
|
+
timers << timer
|
101
|
+
end
|
102
|
+
|
103
|
+
timer
|
104
|
+
end
|
105
|
+
|
106
|
+
# @endgroup
|
107
|
+
|
108
|
+
# @group Logging
|
109
|
+
|
110
|
+
# Use this method to automatically log exceptions to the loggers.
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# def my_method
|
114
|
+
# rescue_exception do
|
115
|
+
# something_that_might_raise()
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# @return [void]
|
120
|
+
# @since 2.0.0
|
121
|
+
def rescue_exception
|
122
|
+
begin
|
123
|
+
yield
|
124
|
+
rescue => e
|
125
|
+
bot.loggers.exception(e)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# (see Logger#log)
|
130
|
+
def log(messages, event = :debug, level = event)
|
131
|
+
if self.is_a?(Cinch::Plugin)
|
132
|
+
messages = Array(messages).map {|m|
|
133
|
+
"[#{self.class}] " + m
|
134
|
+
}
|
135
|
+
end
|
136
|
+
@bot.loggers.log(messages, event, level)
|
137
|
+
end
|
138
|
+
|
139
|
+
# (see Logger#debug)
|
140
|
+
def debug(message)
|
141
|
+
log(message, :debug)
|
142
|
+
end
|
143
|
+
|
144
|
+
# (see Logger#error)
|
145
|
+
def error(message)
|
146
|
+
log(message, :error)
|
147
|
+
end
|
148
|
+
|
149
|
+
# (see Logger#fatal)
|
150
|
+
def fatal(message)
|
151
|
+
log(message, :fatal)
|
152
|
+
end
|
153
|
+
|
154
|
+
# (see Logger#info)
|
155
|
+
def info(message)
|
156
|
+
log(message, :info)
|
157
|
+
end
|
158
|
+
|
159
|
+
# (see Logger#warn)
|
160
|
+
def warn(message)
|
161
|
+
log(message, :warn)
|
162
|
+
end
|
163
|
+
|
164
|
+
# (see Logger#incoming)
|
165
|
+
def incoming(message)
|
166
|
+
log(message, :incoming, :log)
|
167
|
+
end
|
168
|
+
|
169
|
+
# (see Logger#outgoing)
|
170
|
+
def outgoing(message)
|
171
|
+
log(message, :outgoing, :log)
|
172
|
+
end
|
173
|
+
|
174
|
+
# (see Logger#exception)
|
175
|
+
def exception(e)
|
176
|
+
log(e.message, :exception, :error)
|
177
|
+
end
|
178
|
+
# @endgroup
|
179
|
+
|
180
|
+
# @group Formatting
|
181
|
+
|
182
|
+
# (see Formatting.format)
|
183
|
+
def Format(*settings, string)
|
184
|
+
Formatting.format(*settings, string)
|
185
|
+
end
|
186
|
+
alias_method :Color, :Format # deprecated
|
187
|
+
undef_method(:Color) # yardoc hack
|
188
|
+
|
189
|
+
def Color(*args)
|
190
|
+
Cinch::Utilities::Deprecation.print_deprecation("2.2.0", "Helpers.Color", "Helpers.Format")
|
191
|
+
Format(*args)
|
192
|
+
end
|
193
|
+
|
194
|
+
# (see .sanitize)
|
195
|
+
def Sanitize(string)
|
196
|
+
Cinch::Helpers.sanitize(string)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Deletes all characters in the ranges 0–8, 10–31 as well as the
|
200
|
+
# character 127, that is all non-printable characters and
|
201
|
+
# newlines.
|
202
|
+
#
|
203
|
+
# This method is useful for filtering text from external sources
|
204
|
+
# before sending it to IRC.
|
205
|
+
#
|
206
|
+
# Note that this method does not gracefully handle mIRC color
|
207
|
+
# codes, because it will leave the numeric arguments behind. If
|
208
|
+
# your text comes from IRC, you may want to filter it through
|
209
|
+
# {#Unformat} first. If you want to send sanitized input that
|
210
|
+
# includes your own formatting, first use this method, then add
|
211
|
+
# your formatting.
|
212
|
+
#
|
213
|
+
# There exist methods for sending messages that automatically
|
214
|
+
# call this method, namely {Target#safe_msg},
|
215
|
+
# {Target#safe_notice}, and {Target#safe_action}.
|
216
|
+
#
|
217
|
+
# @param [String] string The string to filter
|
218
|
+
# @return [String] The filtered string
|
219
|
+
# @since 2.2.0
|
220
|
+
def self.sanitize(string)
|
221
|
+
string.gsub(/[\x00-\x08\x0a-\x1f\x7f]/, '')
|
222
|
+
end
|
223
|
+
|
224
|
+
# (see Formatting.unformat)
|
225
|
+
def Unformat(string)
|
226
|
+
Formatting.unformat(string)
|
227
|
+
end
|
228
|
+
|
229
|
+
# @endgroup
|
230
|
+
end
|
231
|
+
end
|
data/lib/cinch/irc.rb
ADDED
@@ -0,0 +1,924 @@
|
|
1
|
+
require "timeout"
|
2
|
+
require "net/protocol"
|
3
|
+
require "cinch/network"
|
4
|
+
|
5
|
+
module Cinch
|
6
|
+
# This class manages the connection to the IRC server. That includes
|
7
|
+
# processing incoming and outgoing messages, creating Ruby objects
|
8
|
+
# and invoking plugins.
|
9
|
+
class IRC
|
10
|
+
include Helpers
|
11
|
+
|
12
|
+
# @return [ISupport]
|
13
|
+
attr_reader :isupport
|
14
|
+
|
15
|
+
# @return [Bot]
|
16
|
+
attr_reader :bot
|
17
|
+
|
18
|
+
# @return [Network] The detected network
|
19
|
+
attr_reader :network
|
20
|
+
|
21
|
+
def initialize(bot)
|
22
|
+
@bot = bot
|
23
|
+
@isupport = ISupport.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [TCPSocket]
|
27
|
+
# @api private
|
28
|
+
# @since 2.0.0
|
29
|
+
def socket
|
30
|
+
@socket.io
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
# @return [void]
|
35
|
+
# @since 2.0.0
|
36
|
+
def setup
|
37
|
+
@registration = []
|
38
|
+
@network = Network.new(:unknown, :unknown)
|
39
|
+
@whois_updates = {}
|
40
|
+
@in_lists = Set.new
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
# @return [Boolean] True if the connection could be established
|
45
|
+
def connect
|
46
|
+
tcp_socket = nil
|
47
|
+
|
48
|
+
begin
|
49
|
+
Timeout::timeout(@bot.config.timeouts.connect) do
|
50
|
+
tcp_socket = TCPSocket.new(@bot.config.server, @bot.config.port, @bot.config.local_host)
|
51
|
+
end
|
52
|
+
rescue Timeout::Error
|
53
|
+
@bot.loggers.warn("Timed out while connecting")
|
54
|
+
return false
|
55
|
+
rescue SocketError => e
|
56
|
+
@bot.loggers.warn("Could not connect to the IRC server. Please check your network: #{e.message}")
|
57
|
+
return false
|
58
|
+
rescue => e
|
59
|
+
@bot.loggers.exception(e)
|
60
|
+
return false
|
61
|
+
end
|
62
|
+
|
63
|
+
if @bot.config.ssl.use
|
64
|
+
setup_ssl(tcp_socket)
|
65
|
+
else
|
66
|
+
@socket = tcp_socket
|
67
|
+
end
|
68
|
+
|
69
|
+
@socket = Net::BufferedIO.new(@socket)
|
70
|
+
@socket.read_timeout = @bot.config.timeouts.read
|
71
|
+
@queue = MessageQueue.new(@socket, @bot)
|
72
|
+
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
|
76
|
+
# @api private
|
77
|
+
# @return [void]
|
78
|
+
# @since 2.0.0
|
79
|
+
def setup_ssl(socket)
|
80
|
+
# require openssl in this method so the bot doesn't break for
|
81
|
+
# people who don't have SSL but don't want to use SSL anyway.
|
82
|
+
require 'openssl'
|
83
|
+
|
84
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
85
|
+
|
86
|
+
if @bot.config.ssl.is_a?(Configuration::SSL)
|
87
|
+
if @bot.config.ssl.client_cert
|
88
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@bot.config.ssl.client_cert))
|
89
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@bot.config.ssl.client_cert))
|
90
|
+
end
|
91
|
+
|
92
|
+
ssl_context.ca_path = @bot.config.ssl.ca_path
|
93
|
+
ssl_context.verify_mode = @bot.config.ssl.verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
94
|
+
else
|
95
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
96
|
+
end
|
97
|
+
@bot.loggers.info "Using SSL with #{@bot.config.server}:#{@bot.config.port}"
|
98
|
+
|
99
|
+
@socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
|
100
|
+
@socket.sync = true
|
101
|
+
@socket.connect
|
102
|
+
end
|
103
|
+
|
104
|
+
# @api private
|
105
|
+
# @return [void]
|
106
|
+
# @since 2.0.0
|
107
|
+
def send_cap_ls
|
108
|
+
send "CAP LS"
|
109
|
+
end
|
110
|
+
|
111
|
+
# @api private
|
112
|
+
# @return [void]
|
113
|
+
# @since 2.0.0
|
114
|
+
def send_cap_req
|
115
|
+
caps = [:"away-notify", :"multi-prefix", :sasl, :"twitch.tv/tags"] & @network.capabilities
|
116
|
+
|
117
|
+
# InspIRCd doesn't respond to empty REQs, so send an END in that
|
118
|
+
# case.
|
119
|
+
if caps.size > 0
|
120
|
+
send "CAP REQ :" + caps.join(" ")
|
121
|
+
else
|
122
|
+
send_cap_end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# @since 2.0.0
|
127
|
+
# @api private
|
128
|
+
# @return [void]
|
129
|
+
def send_cap_end
|
130
|
+
send "CAP END"
|
131
|
+
end
|
132
|
+
|
133
|
+
# @api private
|
134
|
+
# @return [void]
|
135
|
+
# @since 2.0.0
|
136
|
+
def send_login
|
137
|
+
send "PASS #{@bot.config.password}" if @bot.config.password
|
138
|
+
send "NICK #{@bot.generate_next_nick!}"
|
139
|
+
send "USER #{@bot.config.user} 0 * :#{@bot.config.realname}"
|
140
|
+
end
|
141
|
+
|
142
|
+
# @api private
|
143
|
+
# @return [Thread] the reading thread
|
144
|
+
# @since 2.0.0
|
145
|
+
def start_reading_thread
|
146
|
+
Thread.new do
|
147
|
+
begin
|
148
|
+
while line = @socket.readline
|
149
|
+
rescue_exception do
|
150
|
+
line = Cinch::Utilities::Encoding.encode_incoming(line, @bot.config.encoding)
|
151
|
+
parse line
|
152
|
+
end
|
153
|
+
end
|
154
|
+
rescue Timeout::Error
|
155
|
+
@bot.loggers.warn "Connection timed out."
|
156
|
+
rescue EOFError
|
157
|
+
@bot.loggers.warn "Lost connection."
|
158
|
+
rescue => e
|
159
|
+
@bot.loggers.exception(e)
|
160
|
+
end
|
161
|
+
|
162
|
+
@socket.close
|
163
|
+
@bot.handlers.dispatch(:disconnect)
|
164
|
+
# FIXME won't we kill all :disconnect handlers here? prolly
|
165
|
+
# not, as they have 10 seconds to finish. that should be
|
166
|
+
# plenty of time
|
167
|
+
@bot.handlers.stop_all
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# @api private
|
172
|
+
# @return [Thread] the sending thread
|
173
|
+
# @since 2.0.0
|
174
|
+
def start_sending_thread
|
175
|
+
Thread.new do
|
176
|
+
rescue_exception do
|
177
|
+
@queue.process!
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# @api private
|
183
|
+
# @return [Thread] The ping thread.
|
184
|
+
# @since 2.0.0
|
185
|
+
def start_ping_thread
|
186
|
+
Thread.new do
|
187
|
+
while true
|
188
|
+
sleep @bot.config.ping_interval
|
189
|
+
# PING requires a single argument. In our case the value
|
190
|
+
# doesn't matter though.
|
191
|
+
send("PING 0")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# @since 2.0.0
|
197
|
+
def send_sasl
|
198
|
+
if @bot.config.sasl.username && @sasl_current_method = @sasl_remaining_methods.pop
|
199
|
+
@bot.loggers.info "[SASL] Trying to authenticate with #{@sasl_current_method.mechanism_name}"
|
200
|
+
send "AUTHENTICATE #{@sasl_current_method.mechanism_name}"
|
201
|
+
else
|
202
|
+
send_cap_end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Establish a connection.
|
207
|
+
#
|
208
|
+
# @return [void]
|
209
|
+
# @since 2.0.0
|
210
|
+
def start
|
211
|
+
setup
|
212
|
+
if connect
|
213
|
+
@sasl_remaining_methods = @bot.config.sasl.mechanisms.reverse
|
214
|
+
send_cap_ls
|
215
|
+
send_login
|
216
|
+
|
217
|
+
reading_thread = start_reading_thread
|
218
|
+
sending_thread = start_sending_thread
|
219
|
+
ping_thread = start_ping_thread
|
220
|
+
|
221
|
+
reading_thread.join
|
222
|
+
sending_thread.kill
|
223
|
+
ping_thread.kill
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# @api private
|
228
|
+
# @return [void]
|
229
|
+
def parse(input)
|
230
|
+
return if input.chomp.empty?
|
231
|
+
@bot.loggers.incoming(input)
|
232
|
+
|
233
|
+
msg = Message.new(input, @bot)
|
234
|
+
events = [[:catchall]]
|
235
|
+
|
236
|
+
if ["001", "002", "003", "004", "422"].include?(msg.command)
|
237
|
+
@registration << msg.command
|
238
|
+
if registered?
|
239
|
+
events << [:connect]
|
240
|
+
@bot.last_connection_was_successful = true
|
241
|
+
on_connect(msg, events)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
if ["PRIVMSG", "NOTICE"].include?(msg.command)
|
246
|
+
events << [:ctcp] if msg.ctcp?
|
247
|
+
if msg.channel?
|
248
|
+
events << [:channel]
|
249
|
+
else
|
250
|
+
events << [:private]
|
251
|
+
end
|
252
|
+
|
253
|
+
if msg.command == "PRIVMSG"
|
254
|
+
events << [:message]
|
255
|
+
end
|
256
|
+
|
257
|
+
if msg.action?
|
258
|
+
events << [:action]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
meth = "on_#{msg.command.downcase}"
|
263
|
+
__send__(meth, msg, events) if respond_to?(meth, true)
|
264
|
+
|
265
|
+
if msg.error?
|
266
|
+
events << [:error]
|
267
|
+
end
|
268
|
+
|
269
|
+
events << [msg.command.downcase.to_sym]
|
270
|
+
|
271
|
+
msg.events = events.map(&:first)
|
272
|
+
events.each do |event, *args|
|
273
|
+
@bot.handlers.dispatch(event, msg, *args)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# @return [Boolean] true if we successfully registered yet
|
278
|
+
def registered?
|
279
|
+
(("001".."004").to_a - @registration).empty? || @registration.include?("422")
|
280
|
+
end
|
281
|
+
|
282
|
+
# Send a message to the server.
|
283
|
+
# @param [String] msg
|
284
|
+
# @return [void]
|
285
|
+
def send(msg)
|
286
|
+
@queue.queue(msg)
|
287
|
+
end
|
288
|
+
|
289
|
+
private
|
290
|
+
def set_leaving_user(message, user, events)
|
291
|
+
events << [:leaving, user]
|
292
|
+
end
|
293
|
+
|
294
|
+
# @since 2.0.0
|
295
|
+
def detect_network(msg, event)
|
296
|
+
old_network = @network
|
297
|
+
new_network = nil
|
298
|
+
new_ircd = nil
|
299
|
+
case event
|
300
|
+
when "002"
|
301
|
+
if msg.params.last =~ /^Your host is .+?, running version (.+)$/
|
302
|
+
case $1
|
303
|
+
when /\+snircd\(/
|
304
|
+
new_ircd = :snircd
|
305
|
+
when /^u[\d\.]+$/
|
306
|
+
new_ircd = :ircu
|
307
|
+
when /^(.+?)-?\d+/
|
308
|
+
new_ircd = $1.downcase.to_sym
|
309
|
+
end
|
310
|
+
elsif msg.params.last == "Your host is jtvchat"
|
311
|
+
new_network = :jtv
|
312
|
+
new_ircd = :jtv
|
313
|
+
end
|
314
|
+
when "004"
|
315
|
+
if msg.params == %w{irc.tinyspeck.com IRC-SLACK gateway}
|
316
|
+
new_network = :slack
|
317
|
+
new_ircd = :slack
|
318
|
+
end
|
319
|
+
when "005"
|
320
|
+
case @isupport["NETWORK"]
|
321
|
+
when "NGameTV"
|
322
|
+
new_network = :ngametv
|
323
|
+
new_ircd = :ngametv
|
324
|
+
when nil
|
325
|
+
else
|
326
|
+
new_network = @isupport["NETWORK"].downcase.to_sym
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
new_network ||= old_network.name
|
331
|
+
new_ircd ||= old_network.ircd
|
332
|
+
|
333
|
+
if old_network.unknown_ircd? && new_ircd != :unknown
|
334
|
+
@bot.loggers.info "Detected IRCd: #{new_ircd}"
|
335
|
+
end
|
336
|
+
if !old_network.unknown_ircd? && new_ircd != old_network.ircd
|
337
|
+
@bot.loggers.info "Detected different IRCd: #{old_network.ircd} -> #{new_ircd}"
|
338
|
+
end
|
339
|
+
if old_network.unknown_network? && new_network != :unknown
|
340
|
+
@bot.loggers.info "Detected network: #{new_network}"
|
341
|
+
end
|
342
|
+
if !old_network.unknown_network? && new_network != old_network.name
|
343
|
+
@bot.loggers.info "Detected different network: #{old_network.name} -> #{new_network}"
|
344
|
+
end
|
345
|
+
|
346
|
+
@network.name = new_network
|
347
|
+
@network.ircd = new_ircd
|
348
|
+
end
|
349
|
+
|
350
|
+
def process_ban_mode(msg, events, param, direction)
|
351
|
+
mask = param
|
352
|
+
ban = Ban.new(mask, msg.user, Time.now)
|
353
|
+
|
354
|
+
if direction == :add
|
355
|
+
msg.channel.bans_unsynced << ban
|
356
|
+
events << [:ban, ban]
|
357
|
+
else
|
358
|
+
msg.channel.bans_unsynced.delete_if {|b| b.mask == ban.mask}
|
359
|
+
events << [:unban, ban]
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def process_owner_mode(msg, events, param, direction)
|
364
|
+
owner = User(param)
|
365
|
+
if direction == :add
|
366
|
+
msg.channel.owners_unsynced << owner unless msg.channel.owners_unsynced.include?(owner)
|
367
|
+
events << [:owner, owner]
|
368
|
+
else
|
369
|
+
msg.channel.owners_unsynced.delete(owner)
|
370
|
+
events << [:deowner, owner]
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def update_whois(user, data)
|
375
|
+
@whois_updates[user] ||= {}
|
376
|
+
@whois_updates[user].merge!(data)
|
377
|
+
end
|
378
|
+
|
379
|
+
# @since 2.0.0
|
380
|
+
def on_away(msg, events)
|
381
|
+
if msg.message.to_s.empty?
|
382
|
+
# unaway
|
383
|
+
msg.user.sync(:away, nil, true)
|
384
|
+
events << [:unaway]
|
385
|
+
else
|
386
|
+
# away
|
387
|
+
msg.user.sync(:away, msg.message, true)
|
388
|
+
events << [:away]
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# @since 2.0.0
|
393
|
+
def on_cap(msg, events)
|
394
|
+
case msg.params[1]
|
395
|
+
when "LS"
|
396
|
+
@network.capabilities.concat msg.message.split(" ").map(&:to_sym)
|
397
|
+
send_cap_req
|
398
|
+
when "ACK"
|
399
|
+
if @network.capabilities.include?(:sasl)
|
400
|
+
send_sasl
|
401
|
+
else
|
402
|
+
send_cap_end
|
403
|
+
end
|
404
|
+
when "NAK"
|
405
|
+
send_cap_end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# @since 2.0.0
|
410
|
+
def on_connect(msg, events)
|
411
|
+
@bot.modes = @bot.config.modes
|
412
|
+
end
|
413
|
+
|
414
|
+
def on_join(msg, events)
|
415
|
+
if msg.user == @bot
|
416
|
+
@bot.channels << msg.channel
|
417
|
+
msg.channel.sync_modes
|
418
|
+
end
|
419
|
+
msg.channel.add_user(msg.user)
|
420
|
+
msg.user.online = true
|
421
|
+
end
|
422
|
+
|
423
|
+
def on_kick(msg, events)
|
424
|
+
target = User(msg.params[1])
|
425
|
+
if target == @bot
|
426
|
+
@bot.channels.delete(msg.channel)
|
427
|
+
end
|
428
|
+
msg.channel.remove_user(target)
|
429
|
+
|
430
|
+
set_leaving_user(msg, target, events)
|
431
|
+
end
|
432
|
+
|
433
|
+
def on_kill(msg, events)
|
434
|
+
user = User(msg.params[1])
|
435
|
+
|
436
|
+
@bot.channel_list.each do |channel|
|
437
|
+
channel.remove_user(user)
|
438
|
+
end
|
439
|
+
|
440
|
+
user.unsync_all
|
441
|
+
user.online = false
|
442
|
+
|
443
|
+
set_leaving_user(msg, user, events)
|
444
|
+
end
|
445
|
+
|
446
|
+
# @version 1.1.0
|
447
|
+
def on_mode(msg, events)
|
448
|
+
if msg.channel?
|
449
|
+
parse_channel_modes(msg, events)
|
450
|
+
return
|
451
|
+
end
|
452
|
+
if msg.params.first == bot.nick
|
453
|
+
parse_bot_modes(msg)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def parse_channel_modes(msg, events)
|
458
|
+
add_and_remove = @bot.irc.isupport["CHANMODES"]["A"] + @bot.irc.isupport["CHANMODES"]["B"] + @bot.irc.isupport["PREFIX"].keys
|
459
|
+
|
460
|
+
param_modes = {
|
461
|
+
:add => @bot.irc.isupport["CHANMODES"]["C"] + add_and_remove,
|
462
|
+
:remove => add_and_remove
|
463
|
+
}
|
464
|
+
|
465
|
+
|
466
|
+
modes, err = ModeParser.parse_modes(msg.params[1], msg.params[2..-1], param_modes)
|
467
|
+
if err != nil
|
468
|
+
if @network.ircd != :slack || !err.is_a?(ModeParser::TooManyParametersError)
|
469
|
+
raise Exceptions::InvalidModeString, err
|
470
|
+
end
|
471
|
+
end
|
472
|
+
modes.each do |direction, mode, param|
|
473
|
+
if @bot.irc.isupport["PREFIX"].keys.include?(mode)
|
474
|
+
target = User(param)
|
475
|
+
|
476
|
+
# (un)set a user-mode
|
477
|
+
if direction == :add
|
478
|
+
msg.channel.users[target] << mode unless msg.channel.users[target].include?(mode)
|
479
|
+
else
|
480
|
+
msg.channel.users[target].delete mode
|
481
|
+
end
|
482
|
+
|
483
|
+
user_events = {
|
484
|
+
"o" => "op",
|
485
|
+
"v" => "voice",
|
486
|
+
"h" => "halfop"
|
487
|
+
}
|
488
|
+
if user_events.has_key?(mode)
|
489
|
+
event = (direction == :add ? "" : "de") + user_events[mode]
|
490
|
+
events << [event.to_sym, target]
|
491
|
+
end
|
492
|
+
elsif @bot.irc.isupport["CHANMODES"]["A"].include?(mode)
|
493
|
+
case mode
|
494
|
+
when "b"
|
495
|
+
process_ban_mode(msg, events, param, direction)
|
496
|
+
when "q"
|
497
|
+
process_owner_mode(msg, events, param, direction) if @network.owner_list_mode
|
498
|
+
else
|
499
|
+
raise Exceptions::UnsupportedMode, mode
|
500
|
+
end
|
501
|
+
else
|
502
|
+
# channel options
|
503
|
+
if direction == :add
|
504
|
+
msg.channel.modes_unsynced[mode] = param.nil? ? true : param
|
505
|
+
else
|
506
|
+
msg.channel.modes_unsynced.delete(mode)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
events << [:mode_change, modes]
|
512
|
+
end
|
513
|
+
|
514
|
+
def parse_bot_modes(msg)
|
515
|
+
modes, err = ModeParser.parse_modes(msg.params[1], msg.params[2..-1])
|
516
|
+
if err != nil
|
517
|
+
raise Exceptions::InvalidModeString, err
|
518
|
+
end
|
519
|
+
modes.each do |direction, mode, _|
|
520
|
+
if direction == :add
|
521
|
+
@bot.modes << mode unless @bot.modes.include?(mode)
|
522
|
+
else
|
523
|
+
@bot.modes.delete(mode)
|
524
|
+
end
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
def on_nick(msg, events)
|
529
|
+
if msg.user == @bot
|
530
|
+
# @bot.set_nick msg.params.last
|
531
|
+
target = @bot
|
532
|
+
else
|
533
|
+
target = msg.user
|
534
|
+
end
|
535
|
+
|
536
|
+
target.update_nick(msg.params.last)
|
537
|
+
target.online = true
|
538
|
+
end
|
539
|
+
|
540
|
+
def on_part(msg, events)
|
541
|
+
msg.channel.remove_user(msg.user)
|
542
|
+
msg.user.channels_unsynced.delete msg.channel
|
543
|
+
|
544
|
+
if msg.user == @bot
|
545
|
+
@bot.channels.delete(msg.channel)
|
546
|
+
end
|
547
|
+
|
548
|
+
set_leaving_user(msg, msg.user, events)
|
549
|
+
end
|
550
|
+
|
551
|
+
def on_ping(msg, events)
|
552
|
+
send "PONG :#{msg.params.first}"
|
553
|
+
end
|
554
|
+
|
555
|
+
def on_topic(msg, events)
|
556
|
+
msg.channel.sync(:topic, msg.params[1])
|
557
|
+
end
|
558
|
+
|
559
|
+
def on_quit(msg, events)
|
560
|
+
@bot.channel_list.each do |channel|
|
561
|
+
channel.remove_user(msg.user)
|
562
|
+
end
|
563
|
+
msg.user.unsync_all
|
564
|
+
msg.user.online = false
|
565
|
+
|
566
|
+
set_leaving_user(msg, msg.user, events)
|
567
|
+
|
568
|
+
if msg.message.downcase == "excess flood" && msg.user == @bot
|
569
|
+
@bot.warn ["Looks like your bot has been kicked because of excess flood.",
|
570
|
+
"If you haven't modified the throttling options manually, please file a bug report at https://github.com/cinchrb/cinch/issues and include the following information:",
|
571
|
+
"- Server: #{@bot.config.server}",
|
572
|
+
"- Messages per second: #{@bot.config.messages_per_second}",
|
573
|
+
"- Server queue size: #{@bot.config.server_queue_size}"]
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
# @since 2.0.0
|
578
|
+
def on_privmsg(msg, events)
|
579
|
+
if msg.user
|
580
|
+
msg.user.online = true
|
581
|
+
end
|
582
|
+
|
583
|
+
if msg.message =~ /^\001DCC SEND (?:"([^"]+)"|(\S+)) (\S+) (\d+)(?: (\d+))?\001$/
|
584
|
+
process_dcc_send($1 || $2, $3, $4, $5, msg, events)
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
# @since 2.0.0
|
589
|
+
def process_dcc_send(filename, ip, port, size, m, events)
|
590
|
+
if ip =~ /^\d+$/
|
591
|
+
# If ip is a single integer, assume it's a specification
|
592
|
+
# compliant IPv4 address in network byte order. If it's any
|
593
|
+
# other string, assume that it's a valid IPv4 or IPv6 address.
|
594
|
+
# If it's not valid, let someone higher up the chain notice
|
595
|
+
# that.
|
596
|
+
ip = ip.to_i
|
597
|
+
ip = [24, 16, 8, 0].collect {|b| (ip >> b) & 255}.join('.')
|
598
|
+
end
|
599
|
+
|
600
|
+
port = port.to_i
|
601
|
+
size = size.to_i
|
602
|
+
|
603
|
+
@bot.loggers.info "DCC: Incoming DCC SEND: File name: %s - Size: %dB - IP: %s - Port: %d" % [filename, size, ip, port]
|
604
|
+
|
605
|
+
dcc = DCC::Incoming::Send.new(user: m.user, filename: filename, size: size, ip: ip, port: port)
|
606
|
+
events << [:dcc_send, dcc]
|
607
|
+
end
|
608
|
+
|
609
|
+
# @since 2.0.0
|
610
|
+
def on_001(msg, events)
|
611
|
+
# Ensure that we know our real, possibly truncated or otherwise
|
612
|
+
# modified nick.
|
613
|
+
@bot.set_nick msg.params.first
|
614
|
+
end
|
615
|
+
|
616
|
+
# @since 2.0.0
|
617
|
+
def on_002(msg, events)
|
618
|
+
detect_network(msg, "002")
|
619
|
+
end
|
620
|
+
|
621
|
+
# @since 2.2.6
|
622
|
+
def on_004(msg, events)
|
623
|
+
detect_network(msg, "004")
|
624
|
+
end
|
625
|
+
|
626
|
+
def on_005(msg, events)
|
627
|
+
# ISUPPORT
|
628
|
+
@isupport.parse(*msg.params[1..-2].map {|v| v.split(" ")}.flatten)
|
629
|
+
detect_network(msg, "005")
|
630
|
+
end
|
631
|
+
|
632
|
+
# @since 2.0.0
|
633
|
+
def on_301(msg, events)
|
634
|
+
# RPL_AWAY
|
635
|
+
user = User(msg.params[1])
|
636
|
+
away = msg.params.last
|
637
|
+
|
638
|
+
if @whois_updates[user]
|
639
|
+
update_whois(user, {:away => away})
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
# @since 1.1.0
|
644
|
+
def on_307(msg, events)
|
645
|
+
# RPL_WHOISREGNICK
|
646
|
+
user = User(msg.params[1])
|
647
|
+
update_whois(user, {:registered => true})
|
648
|
+
end
|
649
|
+
|
650
|
+
def on_311(msg, events)
|
651
|
+
# RPL_WHOISUSER
|
652
|
+
user = User(msg.params[1])
|
653
|
+
update_whois(user, {
|
654
|
+
:user => msg.params[2],
|
655
|
+
:host => msg.params[3],
|
656
|
+
:realname => msg.params[5],
|
657
|
+
})
|
658
|
+
end
|
659
|
+
|
660
|
+
def on_313(msg, events)
|
661
|
+
# RPL_WHOISOPERATOR
|
662
|
+
user = User(msg.params[1])
|
663
|
+
update_whois(user, {:oper? => true})
|
664
|
+
end
|
665
|
+
|
666
|
+
def on_317(msg, events)
|
667
|
+
# RPL_WHOISIDLE
|
668
|
+
user = User(msg.params[1])
|
669
|
+
update_whois(user, {
|
670
|
+
:idle => msg.params[2].to_i,
|
671
|
+
:signed_on_at => Time.at(msg.params[3].to_i),
|
672
|
+
})
|
673
|
+
end
|
674
|
+
|
675
|
+
def on_318(msg, events)
|
676
|
+
# RPL_ENDOFWHOIS
|
677
|
+
user = User(msg.params[1])
|
678
|
+
user.end_of_whois(@whois_updates[user])
|
679
|
+
@whois_updates.delete user
|
680
|
+
end
|
681
|
+
|
682
|
+
def on_319(msg, events)
|
683
|
+
# RPL_WHOISCHANNELS
|
684
|
+
user = User(msg.params[1])
|
685
|
+
channels = msg.params[2].scan(/[#{@isupport["CHANTYPES"].join}][^ ]+/o).map {|c| Channel(c) }
|
686
|
+
update_whois(user, {:channels => channels})
|
687
|
+
end
|
688
|
+
|
689
|
+
def on_324(msg, events)
|
690
|
+
# RPL_CHANNELMODEIS
|
691
|
+
modes = {}
|
692
|
+
arguments = msg.params[3..-1]
|
693
|
+
|
694
|
+
msg.params[2][1..-1].split("").each do |mode|
|
695
|
+
if (@isupport["CHANMODES"]["B"] + @isupport["CHANMODES"]["C"]).include?(mode)
|
696
|
+
modes[mode] = arguments.shift
|
697
|
+
else
|
698
|
+
modes[mode] = true
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
msg.channel.sync(:modes, modes, false)
|
703
|
+
end
|
704
|
+
|
705
|
+
def on_330(msg, events)
|
706
|
+
# RPL_WHOISACCOUNT
|
707
|
+
user = User(msg.params[1])
|
708
|
+
authname = msg.params[2]
|
709
|
+
update_whois(user, {:authname => authname})
|
710
|
+
end
|
711
|
+
|
712
|
+
def on_331(msg, events)
|
713
|
+
# RPL_NOTOPIC
|
714
|
+
msg.channel.sync(:topic, "")
|
715
|
+
end
|
716
|
+
|
717
|
+
def on_332(msg, events)
|
718
|
+
# RPL_TOPIC
|
719
|
+
msg.channel.sync(:topic, msg.params[2])
|
720
|
+
end
|
721
|
+
|
722
|
+
def on_352(msg, events)
|
723
|
+
# RPL_WHOREPLY
|
724
|
+
# "<channel> <user> <host> <server> <nick> <H|G>[*][@|+] :<hopcount> <real name>"
|
725
|
+
_, channel, user, host, _, nick, _, hopsrealname = msg.params
|
726
|
+
_, realname = hopsrealname.split(" ", 2)
|
727
|
+
channel = Channel(channel)
|
728
|
+
user_object = User(nick)
|
729
|
+
user_object.sync(:user, user, true)
|
730
|
+
user_object.sync(:host, host, true)
|
731
|
+
user_object.sync(:realname, realname, true)
|
732
|
+
end
|
733
|
+
|
734
|
+
def on_354(msg, events)
|
735
|
+
# RPL_WHOSPCRPL
|
736
|
+
# We are using the following format: %acfhnru
|
737
|
+
|
738
|
+
# _ user host nick f account realame
|
739
|
+
# :leguin.freenode.net 354 dominikh_ ~a ip-88-152-125-117.unitymediagroup.de dominikh_ H 0 :d
|
740
|
+
# :leguin.freenode.net 354 dominikh_ ~FiXato fixato.net FiXato H FiXato :FiXato, using WeeChat -- More? See: http://twitter
|
741
|
+
# :leguin.freenode.net 354 dominikh_ ~dominikh cinch/developer/dominikh dominikh H DominikH :dominikh
|
742
|
+
# :leguin.freenode.net 354 dominikh_ ~oddmunds s21-04214.dsl.no.powertech.net oddmunds H 0 :oddmunds
|
743
|
+
|
744
|
+
_, channel, user, host, nick, _, account, realname = msg.params
|
745
|
+
channel = Channel(channel)
|
746
|
+
user_object = User(nick)
|
747
|
+
user_object.sync(:user, user, true)
|
748
|
+
user_object.sync(:host, host, true)
|
749
|
+
user_object.sync(:realname, realname, true)
|
750
|
+
user_object.sync(:authname, account == "0" ? nil : account, true)
|
751
|
+
end
|
752
|
+
|
753
|
+
def on_353(msg, events)
|
754
|
+
# RPL_NAMEREPLY
|
755
|
+
unless @in_lists.include?(:names)
|
756
|
+
msg.channel.clear_users
|
757
|
+
end
|
758
|
+
@in_lists << :names
|
759
|
+
|
760
|
+
msg.params[3].split(" ").each do |user|
|
761
|
+
m = user.match(/^([#{@isupport["PREFIX"].values.join}]+)/)
|
762
|
+
if m
|
763
|
+
prefixes = m[1].split("").map {|s| @isupport["PREFIX"].key(s)}
|
764
|
+
nick = user[prefixes.size..-1]
|
765
|
+
else
|
766
|
+
nick = user
|
767
|
+
prefixes = []
|
768
|
+
end
|
769
|
+
user = User(nick)
|
770
|
+
user.online = true
|
771
|
+
msg.channel.add_user(user, prefixes)
|
772
|
+
user.channels_unsynced << msg.channel unless user.channels_unsynced.include?(msg.channel)
|
773
|
+
end
|
774
|
+
end
|
775
|
+
|
776
|
+
def on_366(msg, events)
|
777
|
+
# RPL_ENDOFNAMES
|
778
|
+
@in_lists.delete :names
|
779
|
+
msg.channel.mark_as_synced(:users)
|
780
|
+
end
|
781
|
+
|
782
|
+
# @version 2.0.0
|
783
|
+
def on_367(msg, events)
|
784
|
+
# RPL_BANLIST
|
785
|
+
unless @in_lists.include?(:bans)
|
786
|
+
msg.channel.bans_unsynced.clear
|
787
|
+
end
|
788
|
+
@in_lists << :bans
|
789
|
+
|
790
|
+
mask = msg.params[2]
|
791
|
+
if @network.jtv?
|
792
|
+
# on the justin tv network, ban "masks" only consist of the
|
793
|
+
# nick/username
|
794
|
+
mask = "%s!%s@%s" % [mask, mask, mask + ".irc.justin.tv"]
|
795
|
+
end
|
796
|
+
|
797
|
+
if msg.params[3]
|
798
|
+
by = User(msg.params[3].split("!").first)
|
799
|
+
else
|
800
|
+
by = nil
|
801
|
+
end
|
802
|
+
|
803
|
+
at = Time.at(msg.params[4].to_i)
|
804
|
+
ban = Ban.new(mask, by, at)
|
805
|
+
msg.channel.bans_unsynced << ban
|
806
|
+
end
|
807
|
+
|
808
|
+
def on_368(msg, events)
|
809
|
+
# RPL_ENDOFBANLIST
|
810
|
+
if @in_lists.include?(:bans)
|
811
|
+
@in_lists.delete :bans
|
812
|
+
else
|
813
|
+
# we never received a ban, yet an end of list => no bans
|
814
|
+
msg.channel.bans_unsynced.clear
|
815
|
+
end
|
816
|
+
|
817
|
+
msg.channel.mark_as_synced(:bans)
|
818
|
+
end
|
819
|
+
|
820
|
+
def on_386(msg, events)
|
821
|
+
# RPL_QLIST
|
822
|
+
unless @in_lists.include?(:owners)
|
823
|
+
msg.channel.owners_unsynced.clear
|
824
|
+
end
|
825
|
+
@in_lists << :owners
|
826
|
+
|
827
|
+
owner = User(msg.params[2])
|
828
|
+
msg.channel.owners_unsynced << owner
|
829
|
+
end
|
830
|
+
|
831
|
+
def on_387(msg, events)
|
832
|
+
# RPL_ENDOFQLIST
|
833
|
+
if @in_lists.include?(:owners)
|
834
|
+
@in_lists.delete :owners
|
835
|
+
else
|
836
|
+
#we never received an owner, yet an end of list -> no owners
|
837
|
+
msg.channel.owners_unsynced.clear
|
838
|
+
end
|
839
|
+
|
840
|
+
msg.channel.mark_as_synced(:owners)
|
841
|
+
end
|
842
|
+
|
843
|
+
def on_396(msg, events)
|
844
|
+
# RPL_HOSTHIDDEN
|
845
|
+
# note: designed for freenode
|
846
|
+
User(msg.params[0]).sync(:host, msg.params[1], true)
|
847
|
+
end
|
848
|
+
|
849
|
+
def on_401(msg, events)
|
850
|
+
# ERR_NOSUCHNICK
|
851
|
+
if user = @bot.user_list.find(msg.params[1])
|
852
|
+
update_whois(user, {:unknown? => true})
|
853
|
+
end
|
854
|
+
end
|
855
|
+
|
856
|
+
def on_402(msg, events)
|
857
|
+
# ERR_NOSUCHSERVER
|
858
|
+
|
859
|
+
if user = @bot.user_list.find(msg.params[1]) # not _ensured, we only want a user that already exists
|
860
|
+
user.end_of_whois({:unknown? => true})
|
861
|
+
@whois_updates.delete user
|
862
|
+
# TODO freenode specific, test on other IRCd
|
863
|
+
end
|
864
|
+
end
|
865
|
+
|
866
|
+
def on_433(msg, events)
|
867
|
+
# ERR_NICKNAMEINUSE
|
868
|
+
@bot.nick = @bot.generate_next_nick!(msg.params[1])
|
869
|
+
end
|
870
|
+
|
871
|
+
def on_671(msg, events)
|
872
|
+
user = User(msg.params[1])
|
873
|
+
update_whois(user, {:secure? => true})
|
874
|
+
end
|
875
|
+
|
876
|
+
# @since 2.0.0
|
877
|
+
def on_730(msg, events)
|
878
|
+
# RPL_MONONLINE
|
879
|
+
msg.params.last.split(",").each do |mask|
|
880
|
+
user = User(Mask.new(mask).nick)
|
881
|
+
# User is responsible for emitting an event
|
882
|
+
user.online = true
|
883
|
+
end
|
884
|
+
end
|
885
|
+
|
886
|
+
# @since 2.0.0
|
887
|
+
def on_731(msg, events)
|
888
|
+
# RPL_MONOFFLINE
|
889
|
+
msg.params.last.split(",").each do |nick|
|
890
|
+
user = User(nick)
|
891
|
+
# User is responsible for emitting an event
|
892
|
+
user.online = false
|
893
|
+
end
|
894
|
+
end
|
895
|
+
|
896
|
+
# @since 2.0.0
|
897
|
+
def on_734(msg, events)
|
898
|
+
# ERR_MONLISTFULL
|
899
|
+
user = User(msg.params[2])
|
900
|
+
user.monitored = false
|
901
|
+
end
|
902
|
+
|
903
|
+
# @since 2.0.0
|
904
|
+
def on_903(msg, events)
|
905
|
+
# SASL authentication successful
|
906
|
+
@bot.loggers.info "[SASL] SASL authentication with #{@sasl_current_method.mechanism_name} successful"
|
907
|
+
send_cap_end
|
908
|
+
end
|
909
|
+
|
910
|
+
# @since 2.0.0
|
911
|
+
def on_904(msg, events)
|
912
|
+
# SASL authentication failed
|
913
|
+
@bot.loggers.info "[SASL] SASL authentication with #{@sasl_current_method.mechanism_name} failed"
|
914
|
+
send_sasl
|
915
|
+
end
|
916
|
+
|
917
|
+
# @since 2.0.0
|
918
|
+
def on_authenticate(msg, events)
|
919
|
+
send "AUTHENTICATE " + @sasl_current_method.generate(@bot.config.sasl.username,
|
920
|
+
@bot.config.sasl.password,
|
921
|
+
msg.params.last)
|
922
|
+
end
|
923
|
+
end
|
924
|
+
end
|