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 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