Sutto-marvin 0.1.0.20081014
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/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
|