cinch 0.3.5 → 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.
- 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
|