cinch 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +120 -0
- data/Rakefile +65 -0
- data/lib/cinch.rb +19 -0
- data/lib/cinch/base.rb +236 -0
- data/lib/cinch/irc.rb +44 -0
- data/lib/cinch/irc/message.rb +121 -0
- data/lib/cinch/irc/parser.rb +125 -0
- data/lib/cinch/irc/socket.rb +291 -0
- data/spec/helper.rb +8 -0
- data/spec/irc/helper.rb +8 -0
- data/spec/irc/message_spec.rb +61 -0
- data/spec/irc/parser_spec.rb +107 -0
- data/spec/irc/socket_spec.rb +90 -0
- data/spec/options_spec.rb +40 -0
- metadata +93 -0
data/README.rdoc
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
= Cinch: The IRC Microframework
|
2
|
+
|
3
|
+
== Description
|
4
|
+
|
5
|
+
Cinch is an IRC Microframework for quickly creating IRC bots
|
6
|
+
in Ruby with minimal effort.
|
7
|
+
It provides a minimal interface based on plugins and rules. It's as simple as creating a
|
8
|
+
plugin, defining a rule, and watching your profits flourish.
|
9
|
+
|
10
|
+
Cinch will do all of the hard work for you, so you can spend time creating cool plugins
|
11
|
+
and extensions to wow your internet peers.
|
12
|
+
|
13
|
+
== Installation
|
14
|
+
|
15
|
+
=== RubyGems
|
16
|
+
You can install the latest version of Cinch using RubyGems
|
17
|
+
gem install cinch
|
18
|
+
|
19
|
+
=== GitHub
|
20
|
+
Alternatively you can check out the latest code directly from Github
|
21
|
+
git clone http://github.com/injekt/cinch.git
|
22
|
+
|
23
|
+
== Example
|
24
|
+
|
25
|
+
Your typical <em>Hello, World</em> application would go something like this:
|
26
|
+
|
27
|
+
require 'cinch'
|
28
|
+
|
29
|
+
bot = Cinch.setup do
|
30
|
+
server "irc.freenode.org"
|
31
|
+
nick "Cinch"
|
32
|
+
end
|
33
|
+
|
34
|
+
bot.plugin "hello" do |m|
|
35
|
+
m.reply "Hello, #{m.nick}!"
|
36
|
+
end
|
37
|
+
|
38
|
+
bot.run
|
39
|
+
|
40
|
+
It doesn't take much to work out what's happening here, but I'll explain it anyway.
|
41
|
+
|
42
|
+
First we run the <em>Cinch::setup</em> block which is required in every application. Cinch is boxed
|
43
|
+
with a load of default values, so the <b>only</b> option required in this block is the <em>server</em>
|
44
|
+
option. We then define a plugin using the <em>plugin</em> method and pass it a rule (a String in this
|
45
|
+
case). Every plugin must be mapped to a rule. When the rule matches an IRC message its block is
|
46
|
+
invoked, the contents of which contains your plugin interface. The variable passed to the block is
|
47
|
+
an instance of IRC::Message. This provides us with a couple of helper methods which can be used to
|
48
|
+
reply to a message. See Cinch::IRC::Message#reply and Cinch::IRC::Message#answer
|
49
|
+
|
50
|
+
This example would provide the following response on IRC:
|
51
|
+
|
52
|
+
* Cinch has joined #cinch
|
53
|
+
injekt> !hello
|
54
|
+
Cinch> Hello, injekt!
|
55
|
+
|
56
|
+
Since Cinch doesn't provide a binary executable, running your application is as simple as you would any
|
57
|
+
other Ruby script.
|
58
|
+
|
59
|
+
ruby hello.rb
|
60
|
+
|
61
|
+
Cinch also parses the command line for options, to save you having to configure options within your script.
|
62
|
+
|
63
|
+
ruby hello.rb -s irc.freenode.org -n Coolbot
|
64
|
+
ruby hello.rb --channels foo,bar
|
65
|
+
|
66
|
+
Doing a <b>ruby hello.rb -h</b> provides all possible command line options. When using the <em>--channels</em>
|
67
|
+
option, the channel prefix is option, and if none is given the channel will be prefixed with a hash (#)
|
68
|
+
character
|
69
|
+
|
70
|
+
== Plugins
|
71
|
+
|
72
|
+
Plugins are invoked using the command prefix character (which by default is set to <b>!</b>). You can
|
73
|
+
also tell Cinch to ignore any command prefix and instead use the bots username. This would provide
|
74
|
+
a result similar to this:
|
75
|
+
|
76
|
+
injekt> Cinch: hello
|
77
|
+
Cinch> Hello, injekt!
|
78
|
+
|
79
|
+
Cinch also provides named parameters. This method of expression was inspired by the {Sinatra
|
80
|
+
Web Framework}[http://www.sinatrarb.com/] and although it doesn't quite follow the same pattern,
|
81
|
+
it's useful for naming parameters passed to plugins. These paramaters are available through the
|
82
|
+
Cinch::IRC::Message#args method which is passed to each plugin.
|
83
|
+
|
84
|
+
bot.plugin("say :text") do |m|
|
85
|
+
m.reply m.args[:text]
|
86
|
+
end
|
87
|
+
|
88
|
+
This plugin would provide the following output:
|
89
|
+
|
90
|
+
injekt> !say foo bar baz
|
91
|
+
Cinch> foo bar baz
|
92
|
+
|
93
|
+
Each plugin takes an optional hash of message specific options. These options provide an extension to
|
94
|
+
the rules given, for example if we want to reply only if the nick sending the message is injekt, we
|
95
|
+
could pass the 'nick' option to the hash.
|
96
|
+
|
97
|
+
bot.plugin("join :channel", :nick => 'injekt') do |m|
|
98
|
+
m.join #{m.args[:channel]}
|
99
|
+
end
|
100
|
+
|
101
|
+
== Authors
|
102
|
+
Just me at the moment, sad poor lonely me...
|
103
|
+
* {Lee Jarvis}[http://blog.injekt.net]
|
104
|
+
|
105
|
+
== Notes
|
106
|
+
|
107
|
+
* RDoc API documentation is available {here}[http://rdoc.injekt.net/cinch]
|
108
|
+
* Wiki is available {here}[https://github.com/injekt/cinch/wikis]
|
109
|
+
* Issue and feature tracking is available {here}[https://github.com/injekt/cinch/issues]
|
110
|
+
* Contribution in the form of bugfixes or feature requests is welcome and encouraged
|
111
|
+
|
112
|
+
If you'd like to contribute, fork the GitHub repository, make any changes, and send
|
113
|
+
{injekt}[http://github.com/injekt] a pull request. Collaborator access is available on
|
114
|
+
request once one patch has been submitted.
|
115
|
+
|
116
|
+
== TODO
|
117
|
+
* More specs
|
118
|
+
* More documentation
|
119
|
+
* More examples
|
120
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require "rake"
|
2
|
+
require "rake/clean"
|
3
|
+
require "rake/gempackagetask"
|
4
|
+
require "rake/rdoctask"
|
5
|
+
require "spec/rake/spectask"
|
6
|
+
|
7
|
+
require 'lib/cinch'
|
8
|
+
|
9
|
+
NAME = 'cinch'
|
10
|
+
VERSION = Cinch::VERSION
|
11
|
+
TITLE = "Cinch: The IRC Microframework"
|
12
|
+
CLEAN.include ["*.gem", "rdoc"]
|
13
|
+
RDOC_OPTS = [
|
14
|
+
"-U", "--title", TITLE,
|
15
|
+
"--op", "rdoc",
|
16
|
+
"--main", "README.rdoc"
|
17
|
+
]
|
18
|
+
|
19
|
+
Rake::RDocTask.new do |rdoc|
|
20
|
+
rdoc.rdoc_dir = "rdoc"
|
21
|
+
rdoc.options += RDOC_OPTS
|
22
|
+
rdoc.rdoc_files.add %w(README.rdoc lib/**/*.rb)
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Package"
|
26
|
+
task :package => [:clean] do |p|
|
27
|
+
sh "gem build #{NAME}.gemspec"
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Install gem"
|
31
|
+
task :install => [:package] do
|
32
|
+
sh "sudo gem install ./#{NAME}-#{VERSION} --local"
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Uninstall gem"
|
36
|
+
task :uninstall => [:clean] do
|
37
|
+
sh "sudo gem uninstall #{NAME}"
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Upload gem to gemcutter"
|
41
|
+
task :release => [:package] do
|
42
|
+
sh "gem push ./#{NAME}-#{VERSION}.gem"
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Upload rdoc to injekt.net"
|
46
|
+
task :upload => [:clean, :rdoc] do
|
47
|
+
sh("scp -r rdoc/* injekt@injekt.net:/var/www/injekt.net/rdoc/cinch")
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "Run all specs"
|
51
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
52
|
+
t.spec_files = Dir['spec/**/*_spec.rb']
|
53
|
+
end
|
54
|
+
|
55
|
+
namespace :spec do
|
56
|
+
desc "Print with specdoc formatting"
|
57
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
58
|
+
t.spec_opts = ["--format", "specdoc"]
|
59
|
+
t.spec_files = Dir['spec/**/*_spec.rb']
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
task :default => [:clean, :spec]
|
65
|
+
|
data/lib/cinch.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
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/base'
|
9
|
+
|
10
|
+
module Cinch
|
11
|
+
VERSION = '0.1'
|
12
|
+
|
13
|
+
# Setup bot options and return a new Cinch::Base instance
|
14
|
+
def self.setup(ops={}, &blk)
|
15
|
+
Cinch::Base.new(ops, &blk)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
data/lib/cinch/base.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
module Cinch
|
2
|
+
|
3
|
+
# == Author
|
4
|
+
# * Lee Jarvis - ljjarvis@gmail.com
|
5
|
+
#
|
6
|
+
# == Description
|
7
|
+
# The base for an IRC connection
|
8
|
+
# TODO: More documentation
|
9
|
+
#
|
10
|
+
# == Example
|
11
|
+
# bot = Cinch::Base.new :server => 'irc.freenode.org'
|
12
|
+
#
|
13
|
+
# bot.on :join do |m|
|
14
|
+
# m.reply "Welcome to #{m.channel}, #{m.nick}!" unless m.nick == bot.nick
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# bot.plugin "say :text" do |m|
|
18
|
+
# m.reply m.args[:text]
|
19
|
+
# end
|
20
|
+
class Base
|
21
|
+
|
22
|
+
# A Hash holding rules and attributes
|
23
|
+
attr_reader :rules
|
24
|
+
|
25
|
+
# A Hash holding listeners and reply Procs
|
26
|
+
attr_reader :listeners
|
27
|
+
|
28
|
+
# An OpenStruct holding all configuration options
|
29
|
+
attr_reader :options
|
30
|
+
|
31
|
+
# Default options hash
|
32
|
+
DEFAULTS = {
|
33
|
+
:port => 6667,
|
34
|
+
:nick => "Cinch",
|
35
|
+
:username => 'cinch',
|
36
|
+
:realname => "Cinch IRC Microframework",
|
37
|
+
:prefix => '!',
|
38
|
+
:usermode => 0,
|
39
|
+
}
|
40
|
+
|
41
|
+
# Options can be passed via a hash, a block, or on the instance
|
42
|
+
# independantly. Or of course via the command line
|
43
|
+
#
|
44
|
+
# == Example
|
45
|
+
# # With a Hash
|
46
|
+
# bot = Cinch::Base.new(:server => 'irc.freenode.org')
|
47
|
+
#
|
48
|
+
# # With a block
|
49
|
+
# bot = Cinch::Base.new do
|
50
|
+
# server "irc.freenode.org"
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# # After the instance is created
|
54
|
+
# bot = Cinch::Base.new
|
55
|
+
# bot.options.server = "irc.freenode.org"
|
56
|
+
#
|
57
|
+
# # Nothing, but invoked with "ruby foo.rb -s irc.freenode.org"
|
58
|
+
# bot = Cinch::Base.new
|
59
|
+
def initialize(ops={}, &blk)
|
60
|
+
options = DEFAULTS.merge(ops).merge(Options.new(&blk))
|
61
|
+
@options = OpenStruct.new(options.merge(cli_ops))
|
62
|
+
|
63
|
+
@rules = {}
|
64
|
+
@listeners = {}
|
65
|
+
|
66
|
+
@irc = IRC::Socket.new(options[:server], options[:port])
|
67
|
+
@parser = IRC::Parser.new
|
68
|
+
|
69
|
+
# Default listeners
|
70
|
+
on(:ping) {|m| @irc.pong(m.text) }
|
71
|
+
|
72
|
+
if @options.respond_to?(:channels)
|
73
|
+
on(376) { @options.channels.each {|c| @irc.join(c) } }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Parse command line options
|
78
|
+
def cli_ops
|
79
|
+
options = {}
|
80
|
+
if ARGV.any?
|
81
|
+
begin
|
82
|
+
OptionParser.new do |op|
|
83
|
+
op.on("-s server") {|v| options[:server] = v }
|
84
|
+
op.on("-p port") {|v| options[:port] = v.to_i }
|
85
|
+
op.on("-n nick") {|v| options[:nick] = v }
|
86
|
+
op.on("-c command_prefix") {|v| options[:prefix] = v }
|
87
|
+
op.on("-v", "--verbose", "Enable verbose mode") {|v| options[:verbose] = true }
|
88
|
+
op.on("-j", "--channels x,y,z", Array, "Autojoin channels") {|v|
|
89
|
+
options[:channels] = v.map {|c| %w(# + &).include?(c[0].chr) ? c : c.insert(0, '#') }
|
90
|
+
}
|
91
|
+
end.parse(ARGV)
|
92
|
+
rescue OptionParser::MissingArgument => err
|
93
|
+
warn "Missing values for options: #{err.args.join(', ')}\nFalling back to default"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
options
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add a new plugin
|
100
|
+
#
|
101
|
+
# == Example
|
102
|
+
# plugin('hello') do |m|
|
103
|
+
# m.reply "Hello, #{m.nick}!"
|
104
|
+
# end
|
105
|
+
def plugin(rule, options={}, &blk)
|
106
|
+
rule, keys = compile(rule)
|
107
|
+
add_rule(rule, keys, options, &blk)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Add new listeners
|
111
|
+
#
|
112
|
+
# == Example
|
113
|
+
# on(376) do |m|
|
114
|
+
# m.join "#mychan"
|
115
|
+
# end
|
116
|
+
def on(*commands, &blk)
|
117
|
+
commands.map {|x| x.to_s.downcase.to_sym }.each do |cmd|
|
118
|
+
if @listeners.key?(cmd)
|
119
|
+
@listeners[cmd] << blk
|
120
|
+
else
|
121
|
+
@listeners[cmd] = [blk]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Compile a rule string into regexp
|
127
|
+
def compile(rule)
|
128
|
+
return [rule []] if rule.is_a?(Regexp)
|
129
|
+
keys = []
|
130
|
+
special_chars = %w{. + ( )}
|
131
|
+
|
132
|
+
pattern = rule.to_s.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
|
133
|
+
case match
|
134
|
+
when *special_chars
|
135
|
+
Regexp.escape(match)
|
136
|
+
else
|
137
|
+
keys << $2[1..-1]
|
138
|
+
"([^\x00\r\n]+?)"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
["^#{pattern}$", keys]
|
142
|
+
end
|
143
|
+
|
144
|
+
# Add a new rule, or add to an existing one if it
|
145
|
+
# already exists
|
146
|
+
def add_rule(rule, keys, options={}, &blk)
|
147
|
+
unless @rules.key?(rule)
|
148
|
+
@rules[rule] = [rule, keys, options, blk]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Run run run
|
153
|
+
def run
|
154
|
+
@irc.connect
|
155
|
+
@irc.nick options.nick
|
156
|
+
@irc.user options.username, options.usermode, '*', options.realname
|
157
|
+
|
158
|
+
begin
|
159
|
+
process(@irc.read) while @irc.connected?
|
160
|
+
rescue Interrupt
|
161
|
+
@irc.quit("Interrupted")
|
162
|
+
puts "\nInterrupted. Shutting down.."
|
163
|
+
exit
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Process the next line read from the server
|
168
|
+
def process(line)
|
169
|
+
message = @parser.parse(line)
|
170
|
+
message.irc = @irc
|
171
|
+
puts message if options.verbose
|
172
|
+
|
173
|
+
if @listeners.key?(message.symbol)
|
174
|
+
@listeners[message.symbol].each {|l| l.call(message) }
|
175
|
+
end
|
176
|
+
|
177
|
+
if [:privmsg].include?(message.symbol)
|
178
|
+
rules.each_value do |attr|
|
179
|
+
rule, keys, ops, blk = attr
|
180
|
+
args = {}
|
181
|
+
|
182
|
+
unless ops.has_key?(:prefix) || options.prefix == false
|
183
|
+
rule.insert(1, options.prefix) unless rule[1].chr == options.prefix
|
184
|
+
end
|
185
|
+
|
186
|
+
if message.text && mdata = message.text.match(Regexp.new(rule))
|
187
|
+
unless keys.empty? || mdata.captures.empty?
|
188
|
+
args = Hash[keys.map {|k| k.to_sym}.zip(mdata.captures)]
|
189
|
+
message.args = args
|
190
|
+
end
|
191
|
+
execute_rule(message, ops, blk)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Execute a rule
|
198
|
+
def execute_rule(message, ops, blk)
|
199
|
+
ops.keys.each do |k|
|
200
|
+
case k
|
201
|
+
when :nick; return unless ops[:nick] == message.nick
|
202
|
+
when :user; return unless ops[:user] == message.user
|
203
|
+
when :host; return unless ops[:host] == message.host
|
204
|
+
when :channel
|
205
|
+
if message.channel
|
206
|
+
return unless ops[:channel] == message.channel
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
blk.call(message)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Catch methods
|
215
|
+
def method_missing(meth, *args, &blk) # :nodoc:
|
216
|
+
if options.respond_to?(meth)
|
217
|
+
options.send(meth)
|
218
|
+
elsif @irc.respond_to?(meth)
|
219
|
+
@irc.send(meth, *args)
|
220
|
+
else
|
221
|
+
super
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Option management
|
226
|
+
class Options < Hash # :nodoc:
|
227
|
+
def initialize(&blk)
|
228
|
+
instance_eval(&blk) if block_given?
|
229
|
+
end
|
230
|
+
def method_missing(meth, *args, &blk)
|
231
|
+
self[meth] = args.first unless args.empty?
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
data/lib/cinch/irc.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
lib = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'socket'
|
5
|
+
|
6
|
+
require 'irc/parser'
|
7
|
+
require 'irc/message'
|
8
|
+
require 'irc/socket'
|
9
|
+
|
10
|
+
module Cinch
|
11
|
+
# == Author
|
12
|
+
# * Lee Jarvis - ljjarvis@gmail.com
|
13
|
+
#
|
14
|
+
# == Description
|
15
|
+
# Cinch::IRC provides tools to interact with an IRC server, this
|
16
|
+
# includes reading/writing/parsing and building a message response.
|
17
|
+
#
|
18
|
+
# You can use these tools through Cinch or include them directly and use
|
19
|
+
# them on their own.
|
20
|
+
#
|
21
|
+
# Each class inside of this module can be used direcly as they contain
|
22
|
+
# no references to higher level classes inside Cinch
|
23
|
+
#
|
24
|
+
# == Example
|
25
|
+
# require 'cinch/irc'
|
26
|
+
# require 'pp'
|
27
|
+
#
|
28
|
+
# parser = Cinch::IRC::Parser.new
|
29
|
+
#
|
30
|
+
# Cinch::IRC::Socket.new('irc.2600.net') do |irc|
|
31
|
+
# irc.nick "Cinch"
|
32
|
+
# irc.user "Cinch", 0, '*', "Cinch IRC bot"
|
33
|
+
#
|
34
|
+
# while line = irc.read
|
35
|
+
# message = parser.parse(line)
|
36
|
+
#
|
37
|
+
# pp message
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
module IRC
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|