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.
@@ -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