jeffrafter-marvin 0.1.20081115

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.
Files changed (43) hide show
  1. data/README.textile +110 -0
  2. data/VERSION.yml +4 -0
  3. data/bin/marvin +67 -0
  4. data/config/settings.yml.sample +13 -0
  5. data/config/setup.rb +14 -0
  6. data/handlers/hello_world.rb +9 -0
  7. data/handlers/logging_handler.rb +87 -0
  8. data/handlers/tweet_tweet.rb +21 -0
  9. data/lib/marvin/abstract_client.rb +210 -0
  10. data/lib/marvin/abstract_parser.rb +19 -0
  11. data/lib/marvin/base.rb +121 -0
  12. data/lib/marvin/command_handler.rb +62 -0
  13. data/lib/marvin/core_ext.rb +11 -0
  14. data/lib/marvin/data_store.rb +73 -0
  15. data/lib/marvin/dispatchable.rb +94 -0
  16. data/lib/marvin/drb_handler.rb +7 -0
  17. data/lib/marvin/exception_tracker.rb +16 -0
  18. data/lib/marvin/exceptions.rb +8 -0
  19. data/lib/marvin/handler.rb +12 -0
  20. data/lib/marvin/irc/abstract_server.rb +4 -0
  21. data/lib/marvin/irc/base_server.rb +11 -0
  22. data/lib/marvin/irc/client.rb +105 -0
  23. data/lib/marvin/irc/event.rb +30 -0
  24. data/lib/marvin/irc/socket_client.rb +69 -0
  25. data/lib/marvin/irc.rb +9 -0
  26. data/lib/marvin/loader.rb +68 -0
  27. data/lib/marvin/logger.rb +23 -0
  28. data/lib/marvin/middle_man.rb +103 -0
  29. data/lib/marvin/parsers/regexp_parser.rb +96 -0
  30. data/lib/marvin/parsers/simple_parser/default_events.rb +37 -0
  31. data/lib/marvin/parsers/simple_parser/event_extensions.rb +14 -0
  32. data/lib/marvin/parsers/simple_parser/prefixes.rb +34 -0
  33. data/lib/marvin/parsers/simple_parser.rb +101 -0
  34. data/lib/marvin/parsers.rb +7 -0
  35. data/lib/marvin/settings.rb +77 -0
  36. data/lib/marvin/test_client.rb +60 -0
  37. data/lib/marvin/util.rb +30 -0
  38. data/lib/marvin.rb +44 -0
  39. data/script/daemon-runner +12 -0
  40. data/script/run +25 -0
  41. data/spec/marvin/abstract_client_test.rb +38 -0
  42. data/spec/spec_helper.rb +14 -0
  43. metadata +99 -0
data/README.textile ADDED
@@ -0,0 +1,110 @@
1
+ h1. Marvin
2
+
3
+ Marvin is a simple IRC Framework for Rails suitable for building things
4
+ such as simple IRC bots. Extracted from real use - we'd originally used
5
+ a heavily modified version of MatzBot - it's been built to service a
6
+ particular need.
7
+
8
+ h2. Background
9
+
10
+ Marvin is an event driven framework in two ways - for one, it uses
11
+ EventMachine for all networking purposes - as a result, it's both
12
+ relatively stable / reliable and also powerful.
13
+
14
+ Following on from this, the irc library is event driven. At the base
15
+ level, you choose a client (By Default, Marvin::IRC::Client.) and then you register
16
+ any number of handlers. Whenever an event happens e.g. an incoming message,
17
+ a connection unbinding or event just post_init, each handler is notified
18
+ and given a small set of details about the event.
19
+
20
+ Handlers are very simple - in fact, you could get away with registering
21
+ Object.new as a handler.
22
+
23
+ To function, handlers only require one method: handle - which takes
24
+ two options. an event name (e.g. :incoming_message) and a hash
25
+ of the aforementioned attributes / details. This data can then be processed.
26
+ Alternatively, if a handler has a "handle_[event_name]" method (e.g.
27
+ handle_incoming_message), it will instead be called. Also, if client=
28
+ is implemented this will be called when the client is setup containing
29
+ a reference to said client. This is used to that the handler can
30
+ respond to actions.
31
+
32
+ Like Rack for HTTP, Marvin provides a fair amount of example
33
+ handlers for simple stuff inside IRC.
34
+
35
+ h2. Marvin::Base - A handler starting point
36
+
37
+ The first, Marvin::Base provides a base set of methods (e.g. say,
38
+ reply etc etc.) which make writing a client easier. You can simply
39
+ inherit from Marvin::Base, write some logic and then use the class
40
+ method on_event to define responses to events. The passed in meta
41
+ data for each event is then usable via options.attribute_name - an
42
+ openstruct version of the details. e.g.
43
+
44
+ class NinjaStuff < Marvin::Base
45
+ on_event :incoming_message do
46
+ do_something
47
+ end
48
+ def do_something
49
+ reply options.message # Will echo back the message
50
+ end
51
+ end
52
+
53
+ Or the like. Also, the halt! method can be called in any subclass to
54
+ halt the handler callback chain.
55
+
56
+ h2. Marvin::CommandHandler - Ridiculously easy Bots
57
+
58
+ With Marvin::CommandHandler, you get to define seriously
59
+ simple classes which can act as a simple bot. It takes
60
+ great inspiration from "MatzBot":http://github.com/defunkt/matzbot/tree/master
61
+ which was actually one of the main inspirations for
62
+ creating marvin.
63
+
64
+ To write a CommandHandler, you simply create a subclass
65
+ (ala ActiveRecord::Base), define a few methods and then
66
+ just use the "exposes" class method. e.g.
67
+
68
+ class MySecondExample < Marvin::CommandHandler
69
+ exposes :hello
70
+ def hello(data)
71
+ reply "Hello!"
72
+ end
73
+ end
74
+
75
+ Where data is an array of parameters. exposed methods will be called
76
+ when they match the following pattern:
77
+
78
+ Botname: <exposed-method> <space-seperated-list-meaning-data>
79
+
80
+ h2. Marvin::MiddleMan - Introducing middleware
81
+
82
+ Marvin::MiddleMan lets you insert middleware between handlers
83
+ and you're client - letting you do things such as translating
84
+ all messages on the fly. It's build to be extensible and is
85
+ relatively simple to use. On any Marvin::Base subclass (baring
86
+ the MiddleMan itself), you can simple use the normal methods
87
+ of registering a handler with one exception - you now pass
88
+ one argument, the class reference to your middleman class.
89
+
90
+ h2. Marvin::DataStore - A dead simple persistent hash store
91
+
92
+ Want to save data between when you stop and start your IRC
93
+ Client? With Marvin, it's really, really simple - Marvin::DataStore
94
+ offers a simple syntax for persistent data stores.
95
+
96
+ New datastores can be created with Marvin::DataStore.new("global-store-key").
97
+ From there, you have a hash to do whatever the hell you want. Just
98
+ make sure the data you store is JSON-serializable.
99
+
100
+ When you start - stop a server (via Marvin::Loader.run! and Marvin::Loader.stop!)
101
+ you're data will be loaded from and written to disk accordingly.
102
+
103
+ If you're inside a Marvin::Base subclass it's even easier. You can get a cattr_access
104
+ style accessor for free - just use the "uses_datastore" method. e.g:
105
+
106
+ class X < Marvin::Base
107
+ uses_datastore "datastore-global-key", :cattr_name
108
+ end
109
+
110
+ Then, self.cattr_name will point to the data store instance.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ patch: 20081115
3
+ major: 0
4
+ minor: 1
data/bin/marvin ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'fileutils'
4
+
5
+ LOCATION_ROOT = File.join(File.dirname(__FILE__), "..")
6
+ DEST = ARGV[1] || "./marvin"
7
+
8
+ def j(*args); File.join(*args); end
9
+
10
+ def copy(f, t = nil)
11
+ t = f if t.nil?
12
+ File.open(j(DEST, t), "w+") do |file|
13
+ file.puts File.read(j(LOCATION_ROOT, f))
14
+ end
15
+ end
16
+
17
+ puts "Marvin - A Ruby IRC Library / Framework"
18
+ if ARGV.include?("-h") || ARGV.include?("--help")
19
+ puts "Usage: marvin create <name> - Creates a marvin directory at name or ./marvin"
20
+ puts " marvin (in a Marvin dir) - Starts it, equiv. to script/marvin"
21
+ exit
22
+ end
23
+
24
+ if ARGV.length >= 1 && !["start", "stop", "run", "restart"].include?(ARGV[0])
25
+ if ARGV[0].to_s.downcase != "create"
26
+ puts "'#{ARGV[0]}' isn't a valid command. - Please use #{__FILE__} --help"
27
+ exit(1)
28
+ end
29
+ if File.exist?(DEST) && File.directory?(DEST)
30
+ puts "The folder '#{DEST}' already exists."
31
+ exit(1)
32
+ end
33
+ # Generate it.
34
+ FileUtils.mkdir(DEST)
35
+ ["log", "tmp", "config", "handlers", "script"].each do |folder|
36
+ FileUtils.mkdir(j(DEST, folder))
37
+ end
38
+
39
+ puts "Writing Settings file"
40
+ copy "config/settings.yml.sample", "config/settings.yml"
41
+
42
+ puts "Writing setup.rb"
43
+ copy "config/setup.rb"
44
+
45
+ puts "Copying start script - script/run"
46
+ copy "script/run"
47
+ copy "script/daemon-runner"
48
+ FileUtils.chmod 0755, j(DEST, "script/run")
49
+ FileUtils.chmod 0755, j(DEST, "script/daemon-runner")
50
+
51
+ puts "Copying example handler"
52
+ copy "handlers/hello_world.rb"
53
+
54
+ puts "Done!"
55
+ elsif ARGV.length >= 1
56
+ if !File.exist?("script/daemon-runner")
57
+ puts "Woops! This isn't a marvin directory."
58
+ exit(1)
59
+ end
60
+ exec "script/daemon-runner #{ARGV.map {|a| a.include?(" ") ? "\"#{a}\"" : a }.join(" ")}"
61
+ else
62
+ if !File.exist?("script/run")
63
+ puts "Woops! This isn't a marvin directory."
64
+ exit(1)
65
+ end
66
+ exec "script/run"
67
+ end
@@ -0,0 +1,13 @@
1
+ default:
2
+ name: "My Marvin Bot"
3
+ server: irc.freenode.net
4
+ port: 6667
5
+ channel: "#marvin-testing"
6
+ use_logging: false
7
+ datastore_location: tmp/datastore.json
8
+ development:
9
+ user: MarvinBot
10
+ name: MarvinBot
11
+ nick: MarvinBot3000
12
+ production:
13
+ deployed: false
data/config/setup.rb ADDED
@@ -0,0 +1,14 @@
1
+ # Register all of the handlers you wish to use
2
+ # when the clien connects.
3
+ Marvin::Loader.before_connecting do
4
+
5
+ # E.G.
6
+ # MyHandler.register! (Marvin::Base subclass) or
7
+ # Marvin::Settings.default_client.register_handler my_handler (a handler instance)
8
+
9
+ # Example Handler use.
10
+ # LoggingHandler.register! if Marvin::Settings.use_logging
11
+
12
+ HelloWorld.register!
13
+
14
+ end
@@ -0,0 +1,9 @@
1
+ class HelloWorld < Marvin::CommandHandler
2
+
3
+ exposes :hello
4
+
5
+ def hello(data)
6
+ reply "Hola!" unless target == "#all"
7
+ end
8
+
9
+ end
@@ -0,0 +1,87 @@
1
+ # A Simple Channel Logger, built for the
2
+ # #offrails community. Please note that this
3
+ # relies on models etc. inside the Rails App.
4
+ # it's suited for modification of subclassing
5
+ # if you wish to write your own Channel Logger.
6
+ # I plan on open sourcing the app sometime in
7
+ # the near future.
8
+ class LoggingHandler < Marvin::CommandHandler
9
+
10
+ class_inheritable_accessor :connection, :setup
11
+ attr_accessor :listening, :users
12
+
13
+ def initialize
14
+ super
15
+ logger.debug "Setting up LoggingHandler"
16
+ self.setup!
17
+ self.users = {}
18
+ end
19
+
20
+ # Control
21
+
22
+ exposes :listen, :earmuffs
23
+
24
+ def listen(data)
25
+ unless listening?
26
+ @listening = true
27
+ reply "Busted! I heard _everything_ you said ;)"
28
+ else
29
+ reply "Uh, You never asked me to put my earmuffs on?"
30
+ end
31
+ end
32
+
33
+ def earmuffs(data)
34
+ if listening?
35
+ @listening = false
36
+ reply "Oh hai, I'm not listening anymore."
37
+ else
38
+ reply "I've already put the earmuffs on!"
39
+ end
40
+ end
41
+
42
+ def listening?
43
+ @listening
44
+ end
45
+
46
+ # The actual logging
47
+
48
+ on_event :incoming_message do
49
+ log_message(options.nick, options.target, options.message)
50
+ end
51
+
52
+ on_event :outgoing_message do
53
+ log_message(client.nickname, options.target, options.message)
54
+ end
55
+
56
+ on_event :incoming_action do
57
+ log_message(options.nick, options.target, "ACTION \01#{options.message}\01")
58
+ end
59
+
60
+ def log_message(from, to, message)
61
+ return unless listening?
62
+ ensure_connection_is_alive # Before Logging, ensure that the connection is alive.
63
+ self.users[from.strip] ||= IrcHandle.find_or_create_by_name(from.strip)
64
+ self.users[from.strip].messages.create :message => message, :target => to
65
+ end
66
+
67
+ # Our General Tasks
68
+
69
+ def setup!
70
+ return true if self.setup
71
+ load_prerequisites
72
+ self.setup = true
73
+ self.listening = true
74
+ end
75
+
76
+ def load_prerequisites
77
+ require File.join(Marvin::Settings.rails_root, "config/environment")
78
+ end
79
+
80
+ def ensure_connection_is_alive
81
+ unless ActiveRecord::Base.connection.active?
82
+ ActiveRecord::Base.connection.reconnect!
83
+ end
84
+ end
85
+
86
+
87
+ end
@@ -0,0 +1,21 @@
1
+ # Not Yet Complete: Twitter Client in Channel.
2
+ class TweetTweet < Marvin::Base
3
+
4
+ on_event :client_connected do
5
+ start_tweeting
6
+ end
7
+
8
+ def start_tweeting
9
+ client.periodically 180, :check_tweets
10
+ end
11
+
12
+ def handle_check_tweets
13
+ logger.debug ">> Check Tweets"
14
+ end
15
+
16
+ def show_tweet(tweet)
17
+ end
18
+
19
+
20
+
21
+ end
@@ -0,0 +1,210 @@
1
+ require 'ostruct'
2
+ require 'active_support'
3
+ require "marvin/irc/event"
4
+
5
+ module Marvin
6
+ class AbstractClient
7
+
8
+ include Marvin::Dispatchable
9
+
10
+ cattr_accessor :events, :configuration, :logger, :is_setup, :connections
11
+ attr_accessor :channels, :nickname
12
+
13
+ # Set the default values for the variables
14
+ self.events = []
15
+ self.configuration = OpenStruct.new
16
+ self.configuration.channels = []
17
+ self.connections = []
18
+
19
+ # Initializes the instance variables used for the
20
+ # current connection, dispatching a :client_connected event
21
+ # once it has finished. During this process, it will
22
+ # call #client= on each handler if they respond to it.
23
+ def process_connect
24
+ self.class.setup
25
+ logger.debug "Initializing the current instance"
26
+ self.channels = []
27
+ self.connections << self
28
+ logger.debug "Setting the client for each handler"
29
+ self.handlers.each { |h| h.client = self if h.respond_to?(:client=) }
30
+ logger.debug "Dispatching the default :client_connected event"
31
+ dispatch :client_connected
32
+ end
33
+
34
+ def process_disconnect
35
+ self.connections.delete(self) if self.connections.include?(self)
36
+ dispatch :client_disconnected
37
+ Marvin::Loader.stop! if self.connections.blank?
38
+ end
39
+
40
+ # Sets the current class-wide settings of this IRC Client
41
+ # to either an OpenStruct or the results of #to_hash on
42
+ # any other value that is passed in.
43
+ def self.configuration=(config)
44
+ @@configuration = config.is_a?(OpenStruct) ? config : OpenStruct.new(config.to_hash)
45
+ end
46
+
47
+ # Initializes class-wide settings and those that
48
+ # are required such as the logger. by default, it
49
+ # will convert the channel option of the configuration
50
+ # to be channels - hence normalising it into a format
51
+ # that is more widely used throughout the client.
52
+ def self.setup
53
+ return if self.is_setup
54
+ # Default the logger back to a new one.
55
+ self.configuration.channels ||= []
56
+ unless self.configuration.channel.blank? || self.configuration.channels.include?(self.configuration.channel)
57
+ self.configuration.channels.unshift(self.configuration.channel)
58
+ end
59
+ if configuration.logger.blank?
60
+ require 'logger'
61
+ configuration.logger = Marvin::Logger.logger
62
+ end
63
+ self.logger = self.configuration.logger
64
+ self.is_setup = true
65
+ end
66
+
67
+ ## Handling all of the the actual client stuff.
68
+
69
+ def receive_line(line)
70
+ dispatch :incoming_line, :line => line
71
+ event = Marvin::Settings.default_parser.parse(line)
72
+ dispatch(event.to_incoming_event_name, event.to_hash) unless event.nil?
73
+ end
74
+
75
+ # Default handlers
76
+
77
+ # The default handler for all things initialization-related
78
+ # on the client. Usually, this will send the user command,
79
+ # set out nick, join all of the channels / rooms we wish
80
+ # to be in and if a password is specified in the configuration,
81
+ # it will also attempt to identify us.
82
+ def handle_client_connected(opts = {})
83
+ logger.debug "About to handle post init"
84
+ # IRC Connection is establish so we send all the required commands to the server.
85
+ logger.debug "Setting default nickname"
86
+ default_nickname = self.configuration.nick || self.configuration.nicknames.shift
87
+ nick default_nickname
88
+ logger.debug "sending user command"
89
+ command :user, self.configuration.user, "0", "*", Marvin::Util.last_param(self.configuration.name)
90
+ # If a password is specified, we will attempt to message
91
+ # NickServ to identify ourselves.
92
+ say ":IDENTIFY #{self.configuration.password}", "NickServ" unless self.configuration.password.blank?
93
+ # Join the default channels
94
+ self.configuration.channels.each { |c| self.join c }
95
+ rescue Exception => e
96
+ Marvin::ExceptionTracker.log(e)
97
+ end
98
+
99
+ # The default handler for when a users nickname is taken on
100
+ # on the server. It will attempt to get the nicknickname from
101
+ # the nicknames part of the configuration (if available) and
102
+ # will then call #nick to change the nickname.
103
+ def handle_incoming_nick_taken(opts = {})
104
+ logger.info "Nick Is Taken"
105
+ logger.debug "Available Nicknames: #{self.configuration.nicknames.to_a.join(", ")}"
106
+ available_nicknames = self.configuration.nicknames.to_a
107
+ if available_nicknames.length > 0
108
+ logger.debug "Getting next nickname to switch"
109
+ next_nick = available_nicknames.shift # Get the next nickname
110
+ self.configuration.nicknames = available_nicknames
111
+ logger.info "Attemping to set nickname to #{new_nick}"
112
+ nick next_nick
113
+ else
114
+ logger.info "No Nicknames available - QUITTING"
115
+ quit
116
+ end
117
+ end
118
+
119
+ # The default response for PING's - it simply replies
120
+ # with a PONG.
121
+ def handle_incoming_ping(opts = {})
122
+ logger.info "Received Incoming Ping - Handling with a PONG"
123
+ pong(opts[:data])
124
+ end
125
+
126
+ # TODO: Get the correct mapping for a given
127
+ # Code.
128
+ def handle_incoming_numeric(opts = {})
129
+ code = opts[:code].to_i
130
+ args = Marvin::Util.arguments(opts[:data])
131
+ dispatch :incoming_numeric_processed, {:code => code, :data => args}
132
+ end
133
+
134
+ ## General IRC Functions
135
+
136
+ # Sends a specified command to the server.
137
+ # Takes name (e.g. :privmsg) and all of the args.
138
+ # Very simply formats them as a string correctly
139
+ # and calls send_data with the results.
140
+ def command(name, *args)
141
+ # First, get the appropriate command
142
+ name = name.to_s.upcase
143
+ args = args.flatten.compact
144
+ irc_command = "#{name} #{args.join(" ").strip} \r\n"
145
+ send_line irc_command
146
+ end
147
+
148
+ def join(channel)
149
+ channel = Marvin::Util.channel_name(channel)
150
+ # Record the fact we're entering the room.
151
+ self.channels << channel
152
+ command :JOIN, channel
153
+ logger.info "Joined channel #{channel}"
154
+ dispatch :outgoing_join, :target => channel
155
+ end
156
+
157
+ def part(channel, reason = nil)
158
+ channel = Marvin::Util.channel_name(channel)
159
+ if self.channels.include?(channel)
160
+ command :part, channel, Marvin::Util.last_param(reason)
161
+ dispatch :outgoing_part, :target => channel, :reason => reason
162
+ logger.info "Parted from room #{channel}#{reason ? " - #{reason}" : ""}"
163
+ else
164
+ logger.warn "Tried to disconnect from #{channel} - which you aren't a part of"
165
+ end
166
+ end
167
+
168
+ def quit(reason = nil)
169
+ logger.debug "Preparing to part from #{self.channels.size} channels"
170
+ self.channels.to_a.each do |chan|
171
+ logger.debug "Parting from #{chan}"
172
+ self.part chan, reason
173
+ end
174
+ logger.debug "Parted from all channels, quitting"
175
+ command :quit
176
+ dispatch :quit
177
+ # Remove the connections from the pool
178
+ self.connections.delete(self)
179
+ logger.info "Quit from server"
180
+ end
181
+
182
+ def msg(target, message)
183
+ command :privmsg, target, Marvin::Util.last_param(message)
184
+ logger.info "Message sent to #{target} - #{message}"
185
+ dispatch :outgoing_message, :target => target, :message => message
186
+ end
187
+
188
+ def action(target, message)
189
+ action_text = Marvin::Util.last_param "\01ACTION #{message.strip}\01"
190
+ command :privmsg, target, action_text
191
+ dispatch :outgoing_action, :target => target, :message => message
192
+ logger.info "Action sent to #{target} - #{message}"
193
+ end
194
+
195
+ def pong(data)
196
+ command :pong, data
197
+ dispatch :outgoing_pong
198
+ logger.info "PONG sent to #{data}"
199
+ end
200
+
201
+ def nick(new_nick)
202
+ logger.info "Changing nickname to #{new_nick}"
203
+ command :nick, new_nick
204
+ self.nickname = new_nick
205
+ dispatch :outgoing_nick, :new_nick => new_nick
206
+ logger.info "Nickname changed to #{new_nick}"
207
+ end
208
+
209
+ end
210
+ end
@@ -0,0 +1,19 @@
1
+ module Marvin
2
+ # An abstract class for an IRC protocol
3
+ # Parser. Used as a basis for expirimentation.
4
+ class AbstractParser
5
+
6
+ def self.parse(line)
7
+ return self.new(line.strip).to_event
8
+ end
9
+
10
+ def initialize(line)
11
+ raise NotImplementedError, "Not implemented in an abstract parser"
12
+ end
13
+
14
+ def to_event
15
+ raise NotImplementedError, "Not implemented in an abstract parser"
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,121 @@
1
+ require 'ostruct'
2
+
3
+ module Marvin
4
+ # A Client Handler
5
+ class Base
6
+
7
+ cattr_accessor :logger
8
+ # Set the default logger
9
+ self.logger ||= Marvin::Logger
10
+
11
+ attr_accessor :client, :target, :from, :options, :logger
12
+ class_inheritable_accessor :registered_handlers
13
+ self.registered_handlers = {}
14
+
15
+ def initialize
16
+ self.registered_handlers ||= {}
17
+ self.logger ||= Marvin::Logger
18
+ end
19
+
20
+ class << self
21
+
22
+ def event_handlers_for(message_name, direct = true)
23
+ return [] if self == Marvin::Base
24
+ rh = (self.registered_handlers ||= {})
25
+ rh[self.name] ||= {}
26
+ rh[self.name][message_name] ||= []
27
+ if direct
28
+ found_handlers = rh[self.name][message_name]
29
+ found_handlers += self.superclass.event_handlers_for(message_name)
30
+ return found_handlers
31
+ else
32
+ return rh[self.name][message_name]
33
+ end
34
+ end
35
+
36
+ def on_event(name, &blk)
37
+ self.event_handlers_for(name, false) << blk
38
+ end
39
+
40
+ # Register's in the IRC Client callback chain.
41
+ def register!(parent = Marvin::Settings.default_client)
42
+ return if self == Marvin::Base # Only do it for sub-classes.
43
+ parent.register_handler self.new
44
+ end
45
+
46
+ def uses_datastore(datastore_name, local_name)
47
+ cattr_accessor local_name.to_sym
48
+ self.send("#{local_name}=", Marvin::DataStore.new(datastore_name))
49
+ rescue Exception => e
50
+ logger.debug "Exception in datastore declaration - #{e.inspect}"
51
+ end
52
+
53
+ end
54
+
55
+ # Given an incoming message, handle it appropriatly.
56
+ def handle(message, options)
57
+ begin
58
+ self.setup_defaults(options)
59
+ h = self.class.event_handlers_for(message)
60
+ h.each do |handle|
61
+ self.instance_eval &handle
62
+ end
63
+ rescue Exception => e
64
+ logger.fatal "Exception processing handle #{message}"
65
+ Marvin::ExceptionTracker.log(e)
66
+ end
67
+ end
68
+
69
+ def say(message, target = self.target)
70
+ client.msg target, message
71
+ end
72
+
73
+ def pm(target, message)
74
+ say(target, message)
75
+ end
76
+
77
+ def reply(message)
78
+ if from_channel?
79
+ say "#{self.from}: #{message}"
80
+ else
81
+ say message, self.from # Default back to pm'ing the user
82
+ end
83
+ end
84
+
85
+ def ctcp(message)
86
+ return if from_channel? # Must be from user
87
+ say "\01#{message}\01", self.from
88
+ end
89
+
90
+ # Request information
91
+
92
+ # reflects whether or not the current message / previous message came
93
+ # from a user via pm.
94
+ def from_user?
95
+ self.target && !from_channel?
96
+ end
97
+
98
+ # Determines whether the previous message was inside a channel.
99
+ def from_channel?
100
+ self.target && self.target[0] == ?#
101
+ end
102
+
103
+ def addressed?
104
+ self.from_user? || options.message.split(" ").first.downcase == "#{self.client.nickname.downcase}:"
105
+ end
106
+
107
+ def setup_defaults(options)
108
+ self.options = options.is_a?(OpenStruct) ? options : OpenStruct.new(options)
109
+ self.target = options[:target] if options.has_key?(:target)
110
+ self.from = options[:nick] if options.has_key?(:nick)
111
+ end
112
+
113
+ # Halt's on the handler, used to prevent
114
+ # other handlers also responding to the same
115
+ # message more than once.
116
+ def halt!
117
+ raise HaltHandlerProcessing
118
+ end
119
+
120
+ end
121
+ end