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.
- data/README.textile +110 -0
- data/VERSION.yml +4 -0
- data/bin/marvin +67 -0
- data/config/settings.yml.sample +13 -0
- data/config/setup.rb +14 -0
- data/handlers/hello_world.rb +9 -0
- data/handlers/logging_handler.rb +87 -0
- data/handlers/tweet_tweet.rb +21 -0
- data/lib/marvin/abstract_client.rb +210 -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/dispatchable.rb +94 -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/handler.rb +12 -0
- data/lib/marvin/irc/abstract_server.rb +4 -0
- data/lib/marvin/irc/base_server.rb +11 -0
- data/lib/marvin/irc/client.rb +105 -0
- data/lib/marvin/irc/event.rb +30 -0
- data/lib/marvin/irc/socket_client.rb +69 -0
- data/lib/marvin/irc.rb +9 -0
- data/lib/marvin/loader.rb +68 -0
- data/lib/marvin/logger.rb +23 -0
- data/lib/marvin/middle_man.rb +103 -0
- data/lib/marvin/parsers/regexp_parser.rb +96 -0
- data/lib/marvin/parsers/simple_parser/default_events.rb +37 -0
- data/lib/marvin/parsers/simple_parser/event_extensions.rb +14 -0
- data/lib/marvin/parsers/simple_parser/prefixes.rb +34 -0
- data/lib/marvin/parsers/simple_parser.rb +101 -0
- data/lib/marvin/parsers.rb +7 -0
- data/lib/marvin/settings.rb +77 -0
- data/lib/marvin/test_client.rb +60 -0
- data/lib/marvin/util.rb +30 -0
- data/lib/marvin.rb +44 -0
- data/script/daemon-runner +12 -0
- data/script/run +25 -0
- data/spec/marvin/abstract_client_test.rb +38 -0
- data/spec/spec_helper.rb +14 -0
- 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
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,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
|
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] == ?#
|
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
|