cinch 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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