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 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
@@ -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
@@ -0,0 +1,11 @@
1
+ class String
2
+ def /(other)
3
+ File.join(self, other)
4
+ end
5
+ end
6
+
7
+ class File
8
+ def self.present_dir
9
+ File.dirname(__FILE__)
10
+ end
11
+ end