cinch 0.1
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/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
|