cinch 0.3.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +192 -0
- data/Rakefile +53 -43
- 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 +38 -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 +40 -0
- data/examples/plugins/custom_prefix.rb +23 -0
- data/examples/plugins/google.rb +37 -0
- data/examples/plugins/hello.rb +22 -0
- data/examples/plugins/join_part.rb +42 -0
- data/examples/plugins/memo.rb +50 -0
- data/examples/plugins/msg.rb +22 -0
- data/examples/plugins/multiple_matches.rb +41 -0
- data/examples/plugins/seen.rb +45 -0
- data/examples/plugins/urban_dict.rb +30 -0
- data/examples/plugins/url_shorten.rb +32 -0
- data/lib/cinch.rb +7 -20
- data/lib/cinch/ban.rb +41 -0
- data/lib/cinch/bot.rb +479 -0
- data/lib/cinch/callback.rb +11 -0
- data/lib/cinch/channel.rb +419 -0
- data/lib/cinch/constants.rb +369 -0
- data/lib/cinch/exceptions.rb +25 -0
- data/lib/cinch/helpers.rb +21 -0
- data/lib/cinch/irc.rb +344 -38
- data/lib/cinch/isupport.rb +96 -0
- data/lib/cinch/logger/formatted_logger.rb +80 -0
- data/lib/cinch/logger/logger.rb +44 -0
- data/lib/cinch/logger/null_logger.rb +18 -0
- data/lib/cinch/mask.rb +46 -0
- data/lib/cinch/message.rb +183 -0
- data/lib/cinch/message_queue.rb +62 -0
- data/lib/cinch/plugin.rb +205 -0
- data/lib/cinch/rubyext/infinity.rb +1 -0
- data/lib/cinch/rubyext/module.rb +18 -0
- data/lib/cinch/rubyext/queue.rb +19 -0
- data/lib/cinch/rubyext/string.rb +24 -0
- data/lib/cinch/syncable.rb +55 -0
- data/lib/cinch/user.rb +325 -0
- data/spec/bot_spec.rb +5 -0
- data/spec/channel_spec.rb +5 -0
- data/spec/cinch_spec.rb +5 -0
- data/spec/irc_spec.rb +5 -0
- data/spec/message_spec.rb +5 -0
- data/spec/plugin_spec.rb +5 -0
- data/spec/{helper.rb → spec_helper.rb} +0 -0
- data/spec/user_spec.rb +5 -0
- metadata +69 -51
- data/README.rdoc +0 -195
- data/examples/autovoice.rb +0 -32
- data/examples/custom_patterns.rb +0 -19
- data/examples/custom_prefix.rb +0 -25
- data/examples/google.rb +0 -31
- data/examples/hello.rb +0 -13
- data/examples/join_part.rb +0 -26
- data/examples/memo.rb +0 -40
- data/examples/msg.rb +0 -14
- data/examples/named-param-types.rb +0 -19
- data/examples/seen.rb +0 -41
- data/examples/urban_dict.rb +0 -31
- data/examples/url_shorten.rb +0 -34
- data/lib/cinch/base.rb +0 -368
- data/lib/cinch/irc/message.rb +0 -135
- data/lib/cinch/irc/parser.rb +0 -141
- data/lib/cinch/irc/socket.rb +0 -329
- data/lib/cinch/names.rb +0 -54
- data/lib/cinch/rules.rb +0 -171
- data/spec/base_spec.rb +0 -94
- data/spec/irc/helper.rb +0 -8
- data/spec/irc/message_spec.rb +0 -61
- data/spec/irc/parser_spec.rb +0 -103
- data/spec/irc/socket_spec.rb +0 -90
- data/spec/names_spec.rb +0 -393
- data/spec/options_spec.rb +0 -45
- data/spec/rules_spec.rb +0 -109
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'cinch'
|
2
|
+
|
3
|
+
class Seen
|
4
|
+
class SeenStruct < Struct.new(:who, :where, :what, :time)
|
5
|
+
def to_s
|
6
|
+
"[#{time.asctime}] #{who} was seen in #{where} saying #{what}"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
include Cinch::Plugin
|
11
|
+
listen_to :channel
|
12
|
+
match /seen (.+)/
|
13
|
+
|
14
|
+
def initialize(*args)
|
15
|
+
super
|
16
|
+
@users = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def listen(m)
|
20
|
+
@users[m.user.nick] = SeenStruct.new(m.user, m.channel, m.message, Time.now)
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute(m, nick)
|
24
|
+
if nick == @bot.nick
|
25
|
+
m.reply "That's me!"
|
26
|
+
elsif nick == m.user.nick
|
27
|
+
m.reply "That's you!"
|
28
|
+
elsif @users.key?(nick)
|
29
|
+
m.reply @users[nick].to_s
|
30
|
+
else
|
31
|
+
m.reply "I haven't seen #{nick}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
bot = Cinch::Bot.new do
|
37
|
+
configure do |c|
|
38
|
+
c.server = 'irc.freenode.org'
|
39
|
+
c.channels = ["#cinch-bots"]
|
40
|
+
c.plugins.plugins = [Seen]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
bot.start
|
45
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'cinch'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'cgi'
|
5
|
+
|
6
|
+
class UrbanDictionary
|
7
|
+
include Cinch::Plugin
|
8
|
+
|
9
|
+
match /urban (.+)/
|
10
|
+
def lookup(word)
|
11
|
+
url = "http://www.urbandictionary.com/define.php?term=#{CGI.escape(word)}"
|
12
|
+
CGI.unescape_html Nokogiri::HTML(open(url)).at("div.definition").text.gsub(/\s+/, ' ') rescue nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(m, word)
|
16
|
+
m.reply(lookup(word) || "No results found", true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
bot = Cinch::Bot.new do
|
21
|
+
configure do |c|
|
22
|
+
c.server = "irc.freenode.net"
|
23
|
+
c.nick = "MrCinch"
|
24
|
+
c.channels = ["#cinch-bots"]
|
25
|
+
c.plugins.plugins = [UrbanDictionary]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
bot.start
|
30
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'cinch'
|
3
|
+
|
4
|
+
class TinyURL
|
5
|
+
include Cinch::Plugin
|
6
|
+
|
7
|
+
listen_to :channel
|
8
|
+
|
9
|
+
def shorten(url)
|
10
|
+
url = open("http://tinyurl.com/api-create.php?url=#{URI.escape(url)}").read
|
11
|
+
url == "Error" ? nil : url
|
12
|
+
rescue OpenURI::HTTPError
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def listen(m)
|
17
|
+
urls = URI.extract(m.message, "http")
|
18
|
+
short_urls = urls.map { |url| shorten(url) }.compact
|
19
|
+
unless short_urls.empty?
|
20
|
+
m.reply short_urls.join(", ")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
bot = Cinch::Bot.new do
|
26
|
+
configure do |c|
|
27
|
+
c.server = "irc.freenode.org"
|
28
|
+
c.channels = ["#cinch"]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
bot.start
|
data/lib/cinch.rb
CHANGED
@@ -1,25 +1,12 @@
|
|
1
|
-
|
2
|
-
$LOAD_PATH.unshift(dir) unless $LOAD_PATH.include? dir
|
3
|
-
|
4
|
-
require 'ostruct'
|
5
|
-
require 'optparse'
|
6
|
-
|
7
|
-
require 'cinch/irc'
|
8
|
-
require 'cinch/rules'
|
9
|
-
require 'cinch/base'
|
10
|
-
require 'cinch/names'
|
1
|
+
require 'cinch/bot'
|
11
2
|
|
12
3
|
module Cinch
|
13
|
-
VERSION = '0.
|
14
|
-
|
15
|
-
class << self
|
4
|
+
VERSION = '1.0.0'
|
16
5
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
6
|
+
# @return [String]
|
7
|
+
# @todo Handle mIRC color codes more gracefully.
|
8
|
+
# @api private
|
9
|
+
def self.filter_string(string)
|
10
|
+
string.gsub(/[\x00-\x1f]/, '')
|
22
11
|
end
|
23
|
-
|
24
12
|
end
|
25
|
-
|
data/lib/cinch/ban.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "cinch/mask"
|
2
|
+
module Cinch
|
3
|
+
class Ban
|
4
|
+
# @return [Mask, String]
|
5
|
+
attr_reader :mask
|
6
|
+
|
7
|
+
# @return [String]
|
8
|
+
attr_reader :by
|
9
|
+
|
10
|
+
# @return [Time]
|
11
|
+
attr_reader :created_at
|
12
|
+
|
13
|
+
# @return [Boolean]
|
14
|
+
attr_reader :extended
|
15
|
+
|
16
|
+
def initialize(mask, by, at)
|
17
|
+
@by, @created_at = by, at
|
18
|
+
if mask =~ /^\$/
|
19
|
+
@extended = true
|
20
|
+
@mask = mask
|
21
|
+
else
|
22
|
+
@extended = false
|
23
|
+
@mask = Mask.new(mask)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Boolean] true if the ban matches `user`
|
28
|
+
# @raise [Exceptions::UnsupportedFeature] Cinch does not support
|
29
|
+
# Freenode's extended bans
|
30
|
+
def match(user)
|
31
|
+
raise UnsupportedFeature, "extended bans (freenode) are not supported yet" if @extended
|
32
|
+
@mask =~ user
|
33
|
+
end
|
34
|
+
alias_method :=~, :match
|
35
|
+
|
36
|
+
# @return [String]
|
37
|
+
def to_s
|
38
|
+
@mask.to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/cinch/bot.rb
ADDED
@@ -0,0 +1,479 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'socket'
|
3
|
+
require "thread"
|
4
|
+
require "ostruct"
|
5
|
+
require "cinch/rubyext/module"
|
6
|
+
require "cinch/rubyext/queue"
|
7
|
+
require "cinch/rubyext/string"
|
8
|
+
require "cinch/rubyext/infinity"
|
9
|
+
|
10
|
+
require "cinch/exceptions"
|
11
|
+
|
12
|
+
require "cinch/helpers"
|
13
|
+
require "cinch/logger/logger"
|
14
|
+
require "cinch/logger/null_logger"
|
15
|
+
require "cinch/logger/formatted_logger"
|
16
|
+
require "cinch/syncable"
|
17
|
+
require "cinch/message"
|
18
|
+
require "cinch/message_queue"
|
19
|
+
require "cinch/irc"
|
20
|
+
require "cinch/channel"
|
21
|
+
require "cinch/user"
|
22
|
+
require "cinch/constants"
|
23
|
+
require "cinch/callback"
|
24
|
+
require "cinch/ban"
|
25
|
+
require "cinch/mask"
|
26
|
+
require "cinch/isupport"
|
27
|
+
require "cinch/plugin"
|
28
|
+
|
29
|
+
module Cinch
|
30
|
+
|
31
|
+
class Bot
|
32
|
+
# @return [Config]
|
33
|
+
attr_accessor :config
|
34
|
+
# @return [IRC]
|
35
|
+
attr_accessor :irc
|
36
|
+
# @return [Logger]
|
37
|
+
attr_accessor :logger
|
38
|
+
# @return [Array<Channel>] All channels the bot currently is in
|
39
|
+
attr_reader :channels
|
40
|
+
# @return [String]
|
41
|
+
attr_reader :host
|
42
|
+
# @return [Mask]
|
43
|
+
attr_reader :mask
|
44
|
+
# @return [String]
|
45
|
+
attr_reader :user
|
46
|
+
# @return [String]
|
47
|
+
attr_reader :realname
|
48
|
+
# @return [Time]
|
49
|
+
attr_reader :signed_on_at
|
50
|
+
|
51
|
+
# Helper method for turning a String into a {Channel} object.
|
52
|
+
#
|
53
|
+
# @param [String] channel a channel name
|
54
|
+
# @return [Channel] a {Channel} object
|
55
|
+
# @example
|
56
|
+
# on :message, /^please join (#.+)$/ do |target|
|
57
|
+
# Channel(target).join
|
58
|
+
# end
|
59
|
+
def Channel(channel)
|
60
|
+
return channel if channel.is_a?(Channel)
|
61
|
+
Channel.find_ensured(channel, self)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Helper method for turning a String into an {User} object.
|
65
|
+
#
|
66
|
+
# @param [String] user a user's nickname
|
67
|
+
# @return [User] an {User} object
|
68
|
+
# @example
|
69
|
+
# on :message, /^tell me everything about (.+)$/ do |target|
|
70
|
+
# user = User(target)
|
71
|
+
# reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
|
72
|
+
# end
|
73
|
+
def User(user)
|
74
|
+
return user if user.is_a?(User)
|
75
|
+
User.find_ensured(user, self)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [void]
|
79
|
+
# @see Logger#debug
|
80
|
+
def debug(msg)
|
81
|
+
@logger.debug(msg)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [Boolean]
|
85
|
+
def strict?
|
86
|
+
@config.strictness == :strict
|
87
|
+
end
|
88
|
+
|
89
|
+
# @yield
|
90
|
+
def initialize(&b)
|
91
|
+
@logger = Logger::FormattedLogger.new($stderr)
|
92
|
+
@events = {}
|
93
|
+
@config = OpenStruct.new({
|
94
|
+
:server => "localhost",
|
95
|
+
:port => 6667,
|
96
|
+
:ssl => false,
|
97
|
+
:password => nil,
|
98
|
+
:nick => "cinch",
|
99
|
+
:realname => "cinch",
|
100
|
+
:verbose => true,
|
101
|
+
:messages_per_second => 0.5,
|
102
|
+
:server_queue_size => 10,
|
103
|
+
:strictness => :forgiving,
|
104
|
+
:message_split_start => '... ',
|
105
|
+
:message_split_end => ' ...',
|
106
|
+
:max_messages => nil,
|
107
|
+
:plugins => OpenStruct.new({
|
108
|
+
:plugins => [],
|
109
|
+
:prefix => "!",
|
110
|
+
:options => Hash.new {|h,k| h[k] = {}},
|
111
|
+
}),
|
112
|
+
:channels => [],
|
113
|
+
:encoding => nil,
|
114
|
+
})
|
115
|
+
|
116
|
+
@semaphores_mutex = Mutex.new
|
117
|
+
@semaphores = Hash.new { |h,k| h[k] = Mutex.new }
|
118
|
+
@plugins = []
|
119
|
+
@callback = Callback.new(self)
|
120
|
+
@channels = []
|
121
|
+
|
122
|
+
on :connect do
|
123
|
+
bot.config.channels.each do |channel|
|
124
|
+
bot.join channel
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
instance_eval(&b) if block_given?
|
129
|
+
end
|
130
|
+
|
131
|
+
# This method is used to set a bot's options. It indeed does
|
132
|
+
# nothing else but yielding {Bot#config}, but it makes for a nice DSL.
|
133
|
+
#
|
134
|
+
# @yieldparam [Struct] config the bot's config
|
135
|
+
# @return [void]
|
136
|
+
def configure(&block)
|
137
|
+
@callback.instance_exec(@config, &block)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Since Cinch uses threads, all handlers can be run
|
141
|
+
# simultaneously, even the same handler multiple times. This also
|
142
|
+
# means, that your code has to be thread-safe. Most of the time,
|
143
|
+
# this is not a problem, but if you are accessing stored data, you
|
144
|
+
# will most likely have to synchronize access to it. Instead of
|
145
|
+
# managing all mutexes yourself, Cinch provides a synchronize
|
146
|
+
# method, which takes a name and block.
|
147
|
+
#
|
148
|
+
# Synchronize blocks with the same name share the same mutex,
|
149
|
+
# which means that only one of them will be executed at a time.
|
150
|
+
#
|
151
|
+
# @param [String, Symbol] name a name for the synchronize block.
|
152
|
+
# @return [void]
|
153
|
+
# @yield
|
154
|
+
#
|
155
|
+
# @example
|
156
|
+
# configure do |c|
|
157
|
+
# …
|
158
|
+
# @i = 0
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# on :channel, /^start counting!/ do
|
162
|
+
# synchronize(:my_counter) do
|
163
|
+
# 10.times do
|
164
|
+
# val = @i
|
165
|
+
# # at this point, another thread might've incremented :i already.
|
166
|
+
# # this thread wouldn't know about it, though.
|
167
|
+
# @i = val + 1
|
168
|
+
# end
|
169
|
+
# end
|
170
|
+
# end
|
171
|
+
def synchronize(name, &block)
|
172
|
+
# Must run the default block +/ fetch in a thread safe way in order to
|
173
|
+
# ensure we always get the same mutex for a given name.
|
174
|
+
semaphore = @semaphores_mutex.synchronize { @semaphores[name] }
|
175
|
+
semaphore.synchronize(&block)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Registers a handler.
|
179
|
+
#
|
180
|
+
# @param [String, Symbol, Integer] event the event to match. Available
|
181
|
+
# events are all IRC commands in lowercase as symbols, all numeric
|
182
|
+
# replies, and the following:
|
183
|
+
#
|
184
|
+
# - :channel (a channel message)
|
185
|
+
# - :private (a private message)
|
186
|
+
# - :message (both channel and private messages)
|
187
|
+
# - :error (handling errors, use a numeric error code as `match`)
|
188
|
+
# - :ctcp (ctcp requests, use a ctcp command as `match`)
|
189
|
+
#
|
190
|
+
# @param [Regexp, String, Integer] match every message of the
|
191
|
+
# right event will be checked against this argument and the event
|
192
|
+
# will only be called if it matches
|
193
|
+
#
|
194
|
+
# @yieldparam [String] *args each capture group of the regex will
|
195
|
+
# be one argument to the block. It is optional to accept them,
|
196
|
+
# though
|
197
|
+
#
|
198
|
+
# @return [void]
|
199
|
+
def on(event, regexps = [], *args, &block)
|
200
|
+
regexps = [*regexps]
|
201
|
+
regexps = [//] if regexps.empty?
|
202
|
+
|
203
|
+
event = event.to_sym
|
204
|
+
|
205
|
+
regexps.map! do |regexp|
|
206
|
+
case regexp
|
207
|
+
when String, Integer
|
208
|
+
if event == :ctcp
|
209
|
+
/^#{Regexp.escape(regexp.to_s)}(?:$| .+)/
|
210
|
+
else
|
211
|
+
/^#{Regexp.escape(regexp.to_s)}$/
|
212
|
+
end
|
213
|
+
else
|
214
|
+
regexp
|
215
|
+
end
|
216
|
+
end
|
217
|
+
(@events[event] ||= []) << [regexps, args, block]
|
218
|
+
end
|
219
|
+
|
220
|
+
# Define helper methods in the context of the bot.
|
221
|
+
#
|
222
|
+
# @yield Expects a block containing method definitions
|
223
|
+
# @return [void]
|
224
|
+
def helpers(&b)
|
225
|
+
Callback.class_eval(&b)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Stop execution of the current {#on} handler.
|
229
|
+
#
|
230
|
+
# @return [void]
|
231
|
+
def halt
|
232
|
+
throw :halt
|
233
|
+
end
|
234
|
+
|
235
|
+
# Sends a raw message to the server.
|
236
|
+
#
|
237
|
+
# @param [String] command The message to send.
|
238
|
+
# @return [void]
|
239
|
+
# @see IRC#message
|
240
|
+
def raw(command)
|
241
|
+
@irc.message(command)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Sends a PRIVMSG to a recipient (a channel or user).
|
245
|
+
# You should be using {Channel#send} and {User#send} instead.
|
246
|
+
#
|
247
|
+
# @param [String] recipient the recipient
|
248
|
+
# @param [String] text the message to send
|
249
|
+
# @return [void]
|
250
|
+
# @see Channel#send
|
251
|
+
# @see User#send
|
252
|
+
# @see #safe_msg
|
253
|
+
def msg(recipient, text)
|
254
|
+
text = text.to_s
|
255
|
+
split_start = @config.message_split_start || ""
|
256
|
+
split_end = @config.message_split_end || ""
|
257
|
+
|
258
|
+
text.split(/\r\n|\r|\n/).each do |line|
|
259
|
+
# 498 = 510 - length(":" . " PRIVMSG " . " :");
|
260
|
+
maxlength = 498 - self.mask.to_s.length - recipient.to_s.length
|
261
|
+
maxlength_without_end = maxlength - split_end.bytesize
|
262
|
+
|
263
|
+
if line.bytesize > maxlength
|
264
|
+
splitted = []
|
265
|
+
|
266
|
+
while line.bytesize > maxlength_without_end
|
267
|
+
pos = line.rindex(/\s/, maxlength_without_end)
|
268
|
+
r = pos || maxlength_without_end
|
269
|
+
splitted << line.slice!(0, r) + split_end.tr(" ", "\u00A0")
|
270
|
+
line = split_start.tr(" ", "\u00A0") + line.lstrip
|
271
|
+
end
|
272
|
+
|
273
|
+
splitted << line
|
274
|
+
splitted[0, (@config.max_messages || splitted.size)].each do |string|
|
275
|
+
string.tr!("\u00A0", " ") # clean string from any non-breaking spaces
|
276
|
+
raw("PRIVMSG #{recipient} :#{string}")
|
277
|
+
end
|
278
|
+
else
|
279
|
+
raw("PRIVMSG #{recipient} :#{line}")
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
alias_method :privmsg, :msg
|
284
|
+
alias_method :send, :msg
|
285
|
+
|
286
|
+
# Like {#msg}, but remove any non-printable characters from
|
287
|
+
# `text`. The purpose of this method is to send text of untrusted
|
288
|
+
# sources, like other users or feeds.
|
289
|
+
#
|
290
|
+
# Note: this will **break** any mIRC color codes embedded in the
|
291
|
+
# string.
|
292
|
+
#
|
293
|
+
# @return (see #msg)
|
294
|
+
# @param (see #msg)
|
295
|
+
# @see #msg
|
296
|
+
# @see User#safe_send
|
297
|
+
# @see Channel#safe_send
|
298
|
+
# @todo Handle mIRC color codes more gracefully.
|
299
|
+
def safe_msg(recipient, text)
|
300
|
+
msg(recipient, Cinch.filter_string(text))
|
301
|
+
end
|
302
|
+
alias_method :safe_privmsg, :safe_msg
|
303
|
+
alias_method :safe_send, :safe_msg
|
304
|
+
|
305
|
+
# Invoke an action (/me) in/to a recipient (a channel or user).
|
306
|
+
# You should be using {Channel#action} and {User#action} instead.
|
307
|
+
#
|
308
|
+
# @param [String] recipient the recipient
|
309
|
+
# @param [String] text the message to send
|
310
|
+
# @return [void]
|
311
|
+
# @see Channel#action
|
312
|
+
# @see User#action
|
313
|
+
# @see #safe_action
|
314
|
+
def action(recipient, text)
|
315
|
+
raw("PRIVMSG #{recipient} :\001ACTION #{text}\001")
|
316
|
+
end
|
317
|
+
|
318
|
+
# Like {#action}, but remove any non-printable characters from
|
319
|
+
# `text`. The purpose of this method is to send text from
|
320
|
+
# untrusted sources, like other users or feeds.
|
321
|
+
#
|
322
|
+
# Note: this will **break** any mIRC color codes embedded in the
|
323
|
+
# string.
|
324
|
+
#
|
325
|
+
# @param (see #action)
|
326
|
+
# @return (see #action)
|
327
|
+
# @see #action
|
328
|
+
# @see Channel#safe_action
|
329
|
+
# @see User#safe_action
|
330
|
+
# @todo Handle mIRC color codes more gracefully.
|
331
|
+
def safe_action(recipient, text)
|
332
|
+
action(recipient, Cinch.filter_string(text))
|
333
|
+
end
|
334
|
+
|
335
|
+
# Join a channel.
|
336
|
+
#
|
337
|
+
# @param [String, Channel] channel either the name of a channel or a {Channel} object
|
338
|
+
# @param [String] key optionally the key of the channel
|
339
|
+
# @return [void]
|
340
|
+
# @see Channel#join
|
341
|
+
def join(channel, key = nil)
|
342
|
+
Channel(channel).join(key)
|
343
|
+
end
|
344
|
+
|
345
|
+
# Part a channel.
|
346
|
+
#
|
347
|
+
# @param [String, Channel] channel either the name of a channel or a {Channel} object
|
348
|
+
# @param [String] reason an optional reason/part message
|
349
|
+
# @return [void]
|
350
|
+
# @see Channel#part
|
351
|
+
def part(channel, reason = nil)
|
352
|
+
Channel(channel).part(reason)
|
353
|
+
end
|
354
|
+
|
355
|
+
# @return [String]
|
356
|
+
attr_accessor :nick
|
357
|
+
def nick
|
358
|
+
@config.nick
|
359
|
+
end
|
360
|
+
|
361
|
+
def secure?
|
362
|
+
@config[:ssl]
|
363
|
+
end
|
364
|
+
|
365
|
+
def unknown?
|
366
|
+
false
|
367
|
+
end
|
368
|
+
|
369
|
+
[:host, :mask, :user, :realname, :signed_on_at, :secure?].each do |attr|
|
370
|
+
define_method(attr) do
|
371
|
+
User(nick).__send__(attr)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# Sets the bot's nick.
|
376
|
+
#
|
377
|
+
# @param [String] new_nick
|
378
|
+
# @raise [Exceptions::NickTooLong]
|
379
|
+
def nick=(new_nick)
|
380
|
+
if new_nick.size > @irc.isupport["NICKLEN"] && strict?
|
381
|
+
raise Exceptions::NickTooLong, new_nick
|
382
|
+
end
|
383
|
+
@config.nick = new_nick
|
384
|
+
raw "NICK #{new_nick}"
|
385
|
+
end
|
386
|
+
|
387
|
+
# Disconnects from the server.
|
388
|
+
#
|
389
|
+
# @return [void]
|
390
|
+
def quit(message = nil)
|
391
|
+
command = message ? "QUIT :#{message}" : "QUIT"
|
392
|
+
raw command
|
393
|
+
end
|
394
|
+
|
395
|
+
# Connects the bot to a server.
|
396
|
+
#
|
397
|
+
# @param [Boolean] plugins Automatically register plugins from
|
398
|
+
# `@config.plugins.plugins`?
|
399
|
+
# @return [void]
|
400
|
+
def start(plugins = true)
|
401
|
+
register_plugins if plugins
|
402
|
+
@logger.debug "Connecting to #{@config.server}:#{@config.port}"
|
403
|
+
@irc = IRC.new(self, @config)
|
404
|
+
@irc.connect
|
405
|
+
end
|
406
|
+
|
407
|
+
# Register all plugins from `@config.plugins.plugins`.
|
408
|
+
#
|
409
|
+
# @return [void]
|
410
|
+
def register_plugins
|
411
|
+
@config.plugins.plugins.each do |plugin|
|
412
|
+
register_plugin(plugin)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
# Registers a plugin.
|
417
|
+
#
|
418
|
+
# @param [Class<Plugin>] plugin The plugin class to register
|
419
|
+
# @return [void]
|
420
|
+
def register_plugin(plugin)
|
421
|
+
@plugins << plugin.new(self)
|
422
|
+
end
|
423
|
+
|
424
|
+
# @api private
|
425
|
+
# @return [void]
|
426
|
+
def dispatch(event, msg = nil)
|
427
|
+
if handlers = find(event, msg)
|
428
|
+
handlers.each do |handler|
|
429
|
+
regexps, args, block = *handler
|
430
|
+
# calling Message#match multiple times is not a problem
|
431
|
+
# because we cache the result
|
432
|
+
if msg
|
433
|
+
regexp = regexps.find { |rx| msg.match(rx, event) }
|
434
|
+
captures = msg.match(regexp, event).captures
|
435
|
+
else
|
436
|
+
captures = []
|
437
|
+
end
|
438
|
+
|
439
|
+
invoke(block, args, msg, captures)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
private
|
445
|
+
def find(type, msg = nil)
|
446
|
+
if events = @events[type]
|
447
|
+
if msg.nil?
|
448
|
+
return events
|
449
|
+
end
|
450
|
+
|
451
|
+
events.select { |regexps|
|
452
|
+
regexps.first.any? { |regexp|
|
453
|
+
msg.match(regexp, type)
|
454
|
+
}
|
455
|
+
}
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def invoke(block, args, msg, match)
|
460
|
+
# -1 splat arg, send everything
|
461
|
+
# 0 no args, send nothing
|
462
|
+
# 1 defined number of args, send only those
|
463
|
+
bargs = case block.arity <=> 0
|
464
|
+
when -1; match
|
465
|
+
when 0; []
|
466
|
+
when 1; match[0..block.arity-1 - args.size]
|
467
|
+
end
|
468
|
+
Thread.new do
|
469
|
+
begin
|
470
|
+
catch(:halt) do
|
471
|
+
@callback.instance_exec(msg, *args, *bargs, &block)
|
472
|
+
end
|
473
|
+
rescue => e
|
474
|
+
@logger.log_exception(e)
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|