Sutto-marvin 0.1.0.20081014
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/marvin +60 -0
- data/config/settings.yml.sample +13 -0
- data/config/setup.rb +15 -0
- data/handlers/hello_world.rb +14 -0
- data/handlers/logging_handler.rb +87 -0
- data/handlers/tweet_tweet.rb +19 -0
- data/lib/marvin/abstract_client.rb +250 -0
- data/lib/marvin/abstract_parser.rb +19 -0
- data/lib/marvin/base.rb +121 -0
- data/lib/marvin/command_handler.rb +62 -0
- data/lib/marvin/core_ext.rb +11 -0
- data/lib/marvin/data_store.rb +73 -0
- data/lib/marvin/drb_handler.rb +7 -0
- data/lib/marvin/exception_tracker.rb +16 -0
- data/lib/marvin/exceptions.rb +8 -0
- data/lib/marvin/irc/abstract_server.rb +4 -0
- data/lib/marvin/irc/client.rb +107 -0
- data/lib/marvin/irc/event.rb +29 -0
- data/lib/marvin/irc/socket_client.rb +63 -0
- data/lib/marvin/irc.rb +8 -0
- data/lib/marvin/loader.rb +55 -0
- data/lib/marvin/logger.rb +22 -0
- data/lib/marvin/middle_man.rb +105 -0
- data/lib/marvin/parsers/regexp_parser.rb +94 -0
- data/lib/marvin/parsers.rb +7 -0
- data/lib/marvin/settings.rb +73 -0
- data/lib/marvin/test_client.rb +60 -0
- data/lib/marvin/util.rb +30 -0
- data/lib/marvin.rb +33 -0
- data/script/run +18 -0
- metadata +85 -0
data/bin/marvin
ADDED
@@ -0,0 +1,60 @@
|
|
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
|
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
|
+
FileUtils.chmod 0755, j(DEST, "script/run")
|
48
|
+
|
49
|
+
puts "Copying example handler"
|
50
|
+
copy "handlers/hello_world.rb"
|
51
|
+
|
52
|
+
puts "Done!"
|
53
|
+
|
54
|
+
else
|
55
|
+
if !File.exist?("script/run")
|
56
|
+
puts "Woops! This isn't a marvin directory."
|
57
|
+
exit(1)
|
58
|
+
end
|
59
|
+
exec "script/run"
|
60
|
+
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,15 @@
|
|
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
|
+
# Register using Marvin::MiddleMan.
|
13
|
+
#HelloWorld.register!(Marvin::MiddleMan)
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class HelloWorld < Marvin::CommandHandler
|
2
|
+
|
3
|
+
exposes :hello
|
4
|
+
|
5
|
+
uses_datastore "hello-count", :counts
|
6
|
+
|
7
|
+
def hello(data)
|
8
|
+
self.counts ||= {}
|
9
|
+
self.counts[options.nick] ||= 0
|
10
|
+
self.counts[options.nick] += 1
|
11
|
+
reply "Oh hai there - This is hello ##{self.counts[options.nick]} from you!"
|
12
|
+
end
|
13
|
+
|
14
|
+
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,19 @@
|
|
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
|
+
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'active_support'
|
3
|
+
require "marvin/irc/event"
|
4
|
+
|
5
|
+
module Marvin
|
6
|
+
class AbstractClient
|
7
|
+
|
8
|
+
cattr_accessor :events, :handlers, :configuration, :logger, :is_setup, :connections
|
9
|
+
attr_accessor :channels, :nickname
|
10
|
+
|
11
|
+
# Set the default values for the variables
|
12
|
+
self.handlers = []
|
13
|
+
self.events = []
|
14
|
+
self.configuration = OpenStruct.new
|
15
|
+
self.configuration.channels = []
|
16
|
+
self.connections = []
|
17
|
+
|
18
|
+
# Initializes the instance variables used for the
|
19
|
+
# current connection, dispatching a :client_connected event
|
20
|
+
# once it has finished. During this process, it will
|
21
|
+
# call #client= on each handler if they respond to it.
|
22
|
+
def process_connect
|
23
|
+
self.class.setup
|
24
|
+
logger.debug "Initializing the current instance"
|
25
|
+
self.channels = []
|
26
|
+
(self.connections ||= []) << self
|
27
|
+
logger.debug "Setting the client for each handler"
|
28
|
+
self.handlers.each { |h| h.client = self if h.respond_to?(:client=) }
|
29
|
+
logger.debug "Dispatching the default :client_connected event"
|
30
|
+
dispatch_event :client_connected
|
31
|
+
end
|
32
|
+
|
33
|
+
def process_disconnect
|
34
|
+
self.connections.delete(self) if self.connections.include?(self)
|
35
|
+
dispatch_event :client_disconnected
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sets the current class-wide settings of this IRC Client
|
39
|
+
# to either an OpenStruct or the results of #to_hash on
|
40
|
+
# any other value that is passed in.
|
41
|
+
def self.configuration=(config)
|
42
|
+
@@configuration = config.is_a?(OpenStruct) ? config : OpenStruct.new(config.to_hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Initializes class-wide settings and those that
|
46
|
+
# are required such as the logger. by default, it
|
47
|
+
# will convert the channel option of the configuration
|
48
|
+
# to be channels - hence normalising it into a format
|
49
|
+
# that is more widely used throughout the client.
|
50
|
+
def self.setup
|
51
|
+
return if self.is_setup
|
52
|
+
# Default the logger back to a new one.
|
53
|
+
self.configuration.channels ||= []
|
54
|
+
unless self.configuration.channel.blank? || self.configuration.channels.include?(self.configuration.channel)
|
55
|
+
self.configuration.channels.unshift(self.configuration.channel)
|
56
|
+
end
|
57
|
+
if configuration.logger.blank?
|
58
|
+
require 'logger'
|
59
|
+
configuration.logger = Marvin::Logger.logger
|
60
|
+
end
|
61
|
+
self.logger = self.configuration.logger
|
62
|
+
self.is_setup = true
|
63
|
+
end
|
64
|
+
|
65
|
+
## Handling all of the the actual client stuff.
|
66
|
+
|
67
|
+
# Appends a handler to the end of the handler callback
|
68
|
+
# chain. Note that they will be called in the order they
|
69
|
+
# are appended.
|
70
|
+
def self.register_handler(handler)
|
71
|
+
return if handler.blank?
|
72
|
+
self.handlers << handler
|
73
|
+
end
|
74
|
+
|
75
|
+
def receive_line(line)
|
76
|
+
dispatch_event :incoming_line, :line => line
|
77
|
+
event = Marvin::Settings.default_parser.parse(line)
|
78
|
+
dispatch_event(event.to_incoming_event_name, event.to_hash) unless event.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Handles the dispatch of an event and it's associated options
|
82
|
+
# / properties (defaulting to an empty hash) to both the client
|
83
|
+
# (used for things such as responding to PING) and each of the
|
84
|
+
# registered handlers.
|
85
|
+
def dispatch_event(name, opts = {})
|
86
|
+
# The full handler name is simply what is used to handle
|
87
|
+
# a single event (e.g. handle_incoming_message)
|
88
|
+
full_handler_name = "handle_#{name}"
|
89
|
+
|
90
|
+
# If the current handle_name method is defined on this
|
91
|
+
# class, we dispatch to that first. We use this to provide
|
92
|
+
# functionality such as responding to PING's and handling
|
93
|
+
# required stuff on connections.
|
94
|
+
self.send(full_handler_name, opts) if respond_to?(full_handler_name)
|
95
|
+
|
96
|
+
begin
|
97
|
+
# For each of the handlers, check first if they respond to
|
98
|
+
# the full handler name (e.g. handle_incoming_message) - calling
|
99
|
+
# that if it exists - otherwise falling back to the handle method.
|
100
|
+
# if that doesn't exist, nothing is done.
|
101
|
+
self.handlers.each do |handler|
|
102
|
+
if handler.respond_to?(full_handler_name)
|
103
|
+
handler.send(full_handler_name, opts)
|
104
|
+
elsif handler.respond_to?(:handle)
|
105
|
+
handler.handle name, opts
|
106
|
+
end
|
107
|
+
end
|
108
|
+
# Raise an exception in order to stop the flow
|
109
|
+
# of the control. Ths enables handlers to prevent
|
110
|
+
# responses from happening multiple times.
|
111
|
+
rescue HaltHandlerProcessing
|
112
|
+
logger.debug "Handler Progress halted; Continuing on."
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Default handlers
|
117
|
+
|
118
|
+
# The default handler for all things initialization-related
|
119
|
+
# on the client. Usually, this will send the user command,
|
120
|
+
# set out nick, join all of the channels / rooms we wish
|
121
|
+
# to be in and if a password is specified in the configuration,
|
122
|
+
# it will also attempt to identify us.
|
123
|
+
def handle_client_connected(opts = {})
|
124
|
+
logger.debug "About to handle post init"
|
125
|
+
# IRC Connection is establish so we send all the required commands to the server.
|
126
|
+
logger.debug "sending user command"
|
127
|
+
command :user, self.configuration.user, "0", "*", Marvin::Util.last_param(self.configuration.name)
|
128
|
+
default_nickname = self.configuration.nick || self.configuration.nicknames.shift
|
129
|
+
logger.debug "Setting default nickname"
|
130
|
+
nick default_nickname
|
131
|
+
# If a password is specified, we will attempt to message
|
132
|
+
# NickServ to identify ourselves.
|
133
|
+
say ":IDENTIFY #{self.configuration.password}", "NickServ" unless self.configuration.password.blank?
|
134
|
+
# Join the default channels
|
135
|
+
self.configuration.channels.each { |c| self.join c }
|
136
|
+
end
|
137
|
+
|
138
|
+
# The default handler for when a users nickname is taken on
|
139
|
+
# on the server. It will attempt to get the nicknickname from
|
140
|
+
# the nicknames part of the configuration (if available) and
|
141
|
+
# will then call #nick to change the nickname.
|
142
|
+
def handle_incoming_nick_taken(opts = {})
|
143
|
+
logger.info "Nick Is Taken"
|
144
|
+
logger.debug "Available Nicknames: #{self.configuration.nicknames.to_a.join(", ")}"
|
145
|
+
available_nicknames = self.configuration.nicknames.to_a
|
146
|
+
if available_nicknames.length > 0
|
147
|
+
logger.debug "Getting next nickname to switch"
|
148
|
+
next_nick = available_nicknames.shift # Get the next nickname
|
149
|
+
self.configuration.nicknames = available_nicknames
|
150
|
+
logger.info "Attemping to set nickname to #{new_nick}"
|
151
|
+
nick next_nick
|
152
|
+
else
|
153
|
+
logger.info "No Nicknames available - QUITTING"
|
154
|
+
quit
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# The default response for PING's - it simply replies
|
159
|
+
# with a PONG.
|
160
|
+
def handle_incoming_ping(opts = {})
|
161
|
+
logger.info "Received Incoming Ping - Handling with a PONG"
|
162
|
+
pong(opts[:data])
|
163
|
+
end
|
164
|
+
|
165
|
+
# TODO: Get the correct mapping for a given
|
166
|
+
# Code.
|
167
|
+
def handle_incoming_numeric(opts = {})
|
168
|
+
code = opts[:code].to_i
|
169
|
+
args = Marvin::Util.arguments(opts[:data])
|
170
|
+
logger.debug "Dispatching processed numeric - #{code}"
|
171
|
+
dispatch_event :incoming_numeric_processed, {:code => code, :data => args}
|
172
|
+
end
|
173
|
+
|
174
|
+
## General IRC Functions
|
175
|
+
|
176
|
+
# Sends a specified command to the server.
|
177
|
+
# Takes name (e.g. :privmsg) and all of the args.
|
178
|
+
# Very simply formats them as a string correctly
|
179
|
+
# and calls send_data with the results.
|
180
|
+
def command(name, *args)
|
181
|
+
# First, get the appropriate command
|
182
|
+
name = name.to_s.upcase
|
183
|
+
args = args.flatten.compact
|
184
|
+
irc_command = "#{name} #{args.join(" ").strip} \r\n"
|
185
|
+
send_line irc_command
|
186
|
+
end
|
187
|
+
|
188
|
+
def join(channel)
|
189
|
+
channel = Marvin::Util.channel_name(channel)
|
190
|
+
# Record the fact we're entering the room.
|
191
|
+
self.channels << channel
|
192
|
+
command :JOIN, channel
|
193
|
+
logger.info "Joined channel #{channel}"
|
194
|
+
dispatch_event :outgoing_join, :target => channel
|
195
|
+
end
|
196
|
+
|
197
|
+
def part(channel, reason = nil)
|
198
|
+
channel = Marvin::Util.channel_name(channel)
|
199
|
+
if self.channels.include?(channel)
|
200
|
+
command :part, channel, Marvin::Util.last_param(reason)
|
201
|
+
dispatch_event :outgoing_part, :target => channel, :reason => reason
|
202
|
+
logger.info "Parted from room #{channel}#{reason ? " - #{reason}" : ""}"
|
203
|
+
else
|
204
|
+
logger.warn "Tried to disconnect from #{channel} - which you aren't a part of"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def quit(reason = nil)
|
209
|
+
logger.debug "Preparing to part from #{self.channels.size} channels"
|
210
|
+
self.channels.to_a.each do |chan|
|
211
|
+
logger.debug "Parting from #{chan}"
|
212
|
+
self.part chan, reason
|
213
|
+
end
|
214
|
+
logger.debug "Parted from all channels, quitting"
|
215
|
+
command :quit
|
216
|
+
dispatch_event :quit
|
217
|
+
# Remove the connections from the pool
|
218
|
+
self.connections.delete(self)
|
219
|
+
logger.info "Quit from server"
|
220
|
+
end
|
221
|
+
|
222
|
+
def msg(target, message)
|
223
|
+
command :privmsg, target, Marvin::Util.last_param(message)
|
224
|
+
logger.info "Message sent to #{target} - #{message}"
|
225
|
+
dispatch_event :outgoing_message, :target => target, :message => message
|
226
|
+
end
|
227
|
+
|
228
|
+
def action(target, message)
|
229
|
+
action_text = Marvin::Util.last_param "\01ACTION #{message.strip}\01"
|
230
|
+
command :privmsg, target, action_text
|
231
|
+
dispatch_event :outgoing_action, :target => target, :message => message
|
232
|
+
logger.info "Action sent to #{target} - #{message}"
|
233
|
+
end
|
234
|
+
|
235
|
+
def pong(data)
|
236
|
+
command :pong, data
|
237
|
+
dispatch_event :outgoing_pong
|
238
|
+
logger.info "PONG sent to #{data}"
|
239
|
+
end
|
240
|
+
|
241
|
+
def nick(new_nick)
|
242
|
+
logger.info "Changing nickname to #{new_nick}"
|
243
|
+
command :nick, new_nick
|
244
|
+
self.nickname = new_nick
|
245
|
+
dispatch_event :outgoing_nick, :new_nick => new_nick
|
246
|
+
logger.info "Nickname changed to #{new_nick}"
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
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
|
data/lib/marvin/base.rb
ADDED
@@ -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..0] == "#"
|
101
|
+
end
|
102
|
+
|
103
|
+
def addressed?
|
104
|
+
self.from_user? || options.message.split(" ").first == "#{self.client.nickname}:"
|
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
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Marvin
|
2
|
+
|
3
|
+
# A Simple Marvin handler based on processing
|
4
|
+
# commands, similar in design to MatzBot.
|
5
|
+
class CommandHandler < Base
|
6
|
+
|
7
|
+
class_inheritable_accessor :exposed_methods, :command_prefix
|
8
|
+
|
9
|
+
self.command_prefix = ""
|
10
|
+
self.exposed_methods = []
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
def exposes(*args)
|
15
|
+
self.exposed_methods ||= []
|
16
|
+
self.exposed_methods += args.map { |a| a.to_sym }.flatten
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
on_event :incoming_message do
|
22
|
+
logger.debug "Incoming message"
|
23
|
+
check_for_commands
|
24
|
+
end
|
25
|
+
|
26
|
+
def check_for_commands
|
27
|
+
data, command = nil, nil
|
28
|
+
if self.from_channel?
|
29
|
+
logger.debug "Processing command in channel"
|
30
|
+
split_message = options.message.split(" ", 3)
|
31
|
+
prefix = split_message.shift
|
32
|
+
# Return if in channel and it isn't address to the user.
|
33
|
+
return unless prefix == "#{self.client.nickname}:"
|
34
|
+
command, data = split_message # Set remaining.
|
35
|
+
else
|
36
|
+
command, data = options.message.split(" ", 2)
|
37
|
+
end
|
38
|
+
# Double check for sanity
|
39
|
+
return if command.blank?
|
40
|
+
command_name = extract_command_name(command)
|
41
|
+
unless command_name.nil?
|
42
|
+
logger.debug "Command Exists - processing"
|
43
|
+
# Dispatch the command.
|
44
|
+
self.send(command_name, data.to_a) if self.respond_to?(command_name)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def extract_command_name(command)
|
49
|
+
prefix_length = self.command_prefix.to_s.length
|
50
|
+
has_prefix = command[0...prefix_length] == self.command_prefix.to_s
|
51
|
+
logger.debug "Debugging, prefix is #{prefix_length} characters, has prefix? = #{has_prefix}"
|
52
|
+
if has_prefix
|
53
|
+
# Normalize the method name
|
54
|
+
method_name = command[prefix_length..-1].to_s.underscore.to_sym
|
55
|
+
logger.debug "Computed method name is #{method_name.inspect}"
|
56
|
+
return method_name if self.exposed_methods.to_a.include?(method_name)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|