Sutto-marvin 0.1.20081120 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/README.textile +8 -0
  2. data/VERSION.yml +2 -2
  3. data/bin/marvin +88 -55
  4. data/config/settings.yml.sample +6 -1
  5. data/config/setup.rb +19 -4
  6. data/handlers/debug_handler.rb +2 -2
  7. data/handlers/hello_world.rb +1 -1
  8. data/lib/marvin.rb +14 -9
  9. data/lib/marvin/abstract_client.rb +10 -2
  10. data/lib/marvin/console.rb +54 -0
  11. data/lib/marvin/daemon.rb +71 -0
  12. data/lib/marvin/distributed.rb +14 -0
  13. data/lib/marvin/distributed/dispatch_handler.rb +82 -0
  14. data/lib/marvin/distributed/drb_client.rb +78 -0
  15. data/lib/marvin/distributed/ring_server.rb +41 -0
  16. data/lib/marvin/exception_tracker.rb +1 -1
  17. data/lib/marvin/irc.rb +4 -5
  18. data/lib/marvin/irc/client.rb +13 -2
  19. data/lib/marvin/irc/server.rb +57 -0
  20. data/lib/marvin/irc/server/abstract_connection.rb +79 -0
  21. data/lib/marvin/irc/server/base_connection.rb +66 -0
  22. data/lib/marvin/irc/server/channel.rb +111 -0
  23. data/lib/marvin/irc/server/named_store.rb +14 -0
  24. data/lib/marvin/irc/server/user.rb +5 -0
  25. data/lib/marvin/irc/server/user/handle_mixin.rb +138 -0
  26. data/lib/marvin/irc/server/user_connection.rb +127 -0
  27. data/lib/marvin/loader.rb +111 -36
  28. data/lib/marvin/logger.rb +2 -2
  29. data/lib/marvin/options.rb +11 -2
  30. data/lib/marvin/parsers/command.rb +43 -11
  31. data/lib/marvin/settings.rb +7 -6
  32. data/lib/marvin/status.rb +72 -0
  33. data/lib/marvin/test_client.rb +5 -1
  34. data/lib/marvin/util.rb +18 -1
  35. data/script/client +2 -18
  36. data/script/console +9 -0
  37. data/script/distributed_client +9 -0
  38. data/script/ring_server +12 -0
  39. data/script/server +12 -0
  40. data/script/status +11 -0
  41. data/test/parser_comparison.rb +27 -1
  42. data/test/parser_test.rb +254 -10
  43. data/test/test_helper.rb +1 -1
  44. metadata +62 -8
  45. data/lib/marvin/drb_handler.rb +0 -12
  46. data/lib/marvin/irc/abstract_server.rb +0 -4
  47. data/lib/marvin/irc/base_server.rb +0 -11
  48. data/script/daemon-runner +0 -12
@@ -0,0 +1,111 @@
1
+ module Marvin::IRC::Server
2
+ class Channel
3
+ include Marvin::Dispatchable
4
+
5
+ cattr_accessor :logger
6
+ self.logger = Marvin::Logger
7
+
8
+ attr_accessor :name, :members, :name, :topic, :operators, :mode
9
+
10
+ def inspect
11
+ "#<Marvin::IRC::Server::Channel name='#{name}' topic='#{@topic}' members=[#{self.members.map { |m| m.nick.inspect}.join(", ")}]>"
12
+ end
13
+
14
+ def initialize(name)
15
+ @name = name
16
+ @members = []
17
+ @operators = []
18
+ @topic = ""
19
+ @mode = ""
20
+ logger.info "Created the channel #{name}"
21
+ dispatch :channel_created, :channel => self
22
+ end
23
+
24
+ def member?(user)
25
+ @members.include?(user)
26
+ end
27
+
28
+ def each_member(&blk)
29
+ members.each(&blk)
30
+ end
31
+
32
+ def add(user)
33
+ logger.info "Adding user #{user.nick} to #{@name}"
34
+ @operators << user if needs_op?
35
+ @members << user
36
+ end
37
+
38
+ def remove(user)
39
+ @members.delete(user)
40
+ end
41
+
42
+ # Ind. operations on the room
43
+
44
+ def join(user)
45
+ return false if member?(user)
46
+ # Otherwise, we add a user
47
+ add user
48
+ @members.each { |m| m.notify :join, @name, :prefix => user.prefix }
49
+ dispatch :outgoing_join, :target => @name, :nick => user.nick
50
+ return true
51
+ end
52
+
53
+ def part(user, message = nil)
54
+ logger.debug "Getting part from #{user.inspect} w/ #{message}"
55
+ return false if !member?(user)
56
+ @members.each { |m| m.notify :part, @name, user.nick, message, :prefix => user.prefix }
57
+ remove user
58
+ # TODO: Remove channel from ChannelStore if it's empty.
59
+ dispatch :outgoing_part, :target => @name, :user => user, :message => message
60
+ check_emptyness!
61
+ return true
62
+ end
63
+
64
+ def quit(user, message = nil)
65
+ remove user
66
+ @members.each { |m| m.notify :quit, @name, message, :prefix => user.prefix }
67
+ # TODO: Remove channel from the store if it's empty
68
+ dispatch :outgoing_quit, :target => @name, :user => user, :message => message
69
+ check_emptyness!
70
+ end
71
+
72
+ def message(user, message)
73
+ @members.each { |m| m.notify :privmsg, @name, ":#{message}", :prefix => user.prefix unless user == m }
74
+ dispatch :outgoing_message, :target => self, :user => user, :message => message
75
+ end
76
+
77
+ def notice(user, message)
78
+ @members.each { |m| m.notify :notice, @name, ":#{message}", :prefix => user.prefix unless user == m }
79
+ dispatch :outgoing_notice, :target => self, :user => user, :message => message
80
+ end
81
+
82
+ def topic(user = nil, t = nil)
83
+ return @topic if t.blank?
84
+ logger.info "Getting topic for '#{@name}' - #{t.inspect}"
85
+ @topic = t
86
+ @members.each { |m| m.notify :topic, @name, ":#{t}", :prefix => user.prefix }
87
+ dispatch :outgoing_topic, :target => @name, :user => user, :topic => t
88
+ return @topic
89
+ end
90
+
91
+ def mode(user)
92
+ @operators.include?(user) ? "@" : ""
93
+ end
94
+
95
+ private
96
+
97
+ def needs_op?
98
+ @operators.empty? && @members.empty?
99
+ end
100
+
101
+ def check_emptyness!
102
+ destroy if @members.empty?
103
+ end
104
+
105
+ def destroy
106
+ Marvin::IRC::Server::ChannelStore.delete(@name)
107
+ dispatch :channel_destroyed, :channel => self
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,14 @@
1
+ module Marvin::IRC::Server
2
+ class NamedStore
3
+
4
+ def self.new(key_plural, ref_value, &blk)
5
+ klass = Class.new(Hash) do
6
+ alias_method :"each_#{ref_value}", :each_value
7
+ alias_method key_plural.to_sym, :keys
8
+ end
9
+ klass.class_eval(&blk) unless blk.blank?
10
+ return klass.new
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module Marvin::IRC::Server::User
2
+
3
+ autoload :HandleMixin, 'marvin/irc/server/user/handle_mixin'
4
+
5
+ end
@@ -0,0 +1,138 @@
1
+ module Marvin::IRC::Server::User::HandleMixin
2
+
3
+ def handle_incoming_pass(opts = {})
4
+ @password = opts[:password]
5
+ end
6
+
7
+ def handle_incoming_user(opts = {})
8
+ @user = opts[:user]
9
+ @mode = opts[:mode]
10
+ @real_name = opts[:real_name]
11
+ welcome_if_complete!
12
+ end
13
+
14
+ def handle_incoming_nick(opts = {})
15
+ nick = opts[:new_nick]
16
+ if !nick.blank? && !Marvin::IRC::Server::UserStore.nick_taken?(nick.downcase)
17
+ if !(new_nick = @nick.nil?)
18
+ logger.debug "Notifying all users of nick change: #{@nick} => #{nick}"
19
+ # Get all users and let them now we've changed nick from @nick to nick
20
+ users = [self]
21
+ @channels.each do |c|
22
+ users += c.members.values
23
+ end
24
+ users.uniq.each { |u| u.notify :NICK, nick, :prefix => prefix }
25
+ dispatch :outgoing_nick, :nick => @nick, :new_nick => nick
26
+ end
27
+ # Update the store values
28
+ Marvin::IRC::Server::UserStore.delete(@nick.downcase) unless @nick.blank?
29
+ Marvin::IRC::Server::UserStore[nick.downcase] = self
30
+ # Change the nick and reset the number of attempts
31
+ @nick = nick
32
+ @nick_attempts = 0
33
+ # Finally, update the prefix.
34
+ update_prefix!
35
+ welcome_if_complete! if new_nick
36
+ elsif Marvin::IRC::Server::UserStore[nick.downcase] != self
37
+ # The nick is taken
38
+ # TODO: Remove hard coded nick attempts limit
39
+ if @nick_attempts > 5
40
+ # Handle abort here.
41
+ logger.debug "User has gone over nick attempt limits - Killing connection."
42
+ kill_connection!
43
+ dispatch :outgoing_nick_killed, :client => self
44
+ else
45
+ logger.debug "Noting users nick is taken, warning"
46
+ err :NICKNAMEINUSE, "*", nick, ":Nickname is already in use."
47
+ @nick_attempts += 1
48
+ end
49
+ end
50
+ end
51
+
52
+ def handle_incoming_join(opts = {})
53
+ return if @prefix.blank?
54
+ opts[:target].split(",").each do |channel|
55
+ # If the channel name is invalud, let the user known and dispatch
56
+ # the correct event.
57
+ if channel !~ Marvin::IRC::Server::UserConnection::CHANNEL
58
+ logger.debug "Attempted to join invalid channel name '#{channel}'"
59
+ err :NOSUCHCHANNEL, channel, ":That channel doesn't exist"
60
+ dispatch :invalid_channel_name, :channel => channel, :client => self
61
+ return
62
+ end
63
+ chan = (Marvin::IRC::Server::ChannelStore[channel.downcase] ||= Marvin::IRC::Server::Channel.new(channel))
64
+ if chan.join(self)
65
+ rpl :TOPIC, channel, ":#{chan.topic.blank? ? "There is no topic" : nchan.topic}"
66
+ rpl :NAMREPLY, "=", channel, ":#{chan.members.map { |m| m.nick }.join(" ")}"
67
+ rpl :ENDOFNAMES, channel, ":End of /NAMES list."
68
+ @channels << chan
69
+ else
70
+ logger.debug "Couldn't join channel '#{channel}'"
71
+ end
72
+ end
73
+ end
74
+
75
+ def handle_incoming_ping(opts = {})
76
+ command :PONG, ":#{opts[:data]}"
77
+ end
78
+
79
+ def handle_incoming_pong(opts = {})
80
+ # Decrease the ping count.
81
+ @ping_count -= 1
82
+ @ping_count = 0 if @ping_count < 0
83
+ logger.debug "Got pong: #{opts[:data]}"
84
+ end
85
+
86
+ def handle_incoming_message(opts = {})
87
+ return if @prefix.blank?
88
+ unless (t = target_from(opts[:target])).blank?
89
+ t.message self, opts[:message]
90
+ end
91
+ end
92
+
93
+ def handle_incoming_notice(opts = {})
94
+ return if @prefix.blank?
95
+ unless (t = target_from(opts[:target])).blank?
96
+ t.notice self, opts[:message]
97
+ end
98
+ end
99
+
100
+ def handle_incoming_part(opts = {})
101
+ t = opts[:target].downcase
102
+ if !(chan = Marvin::IRC::Server::ChannelStore[t]).blank?
103
+ if chan.part(self, opts[:message])
104
+ @channels.delete(chan)
105
+ else
106
+ err :NOTONCHANNEL, opts[:target], ":Not a member of that channel"
107
+ end
108
+ else
109
+ err :NOSUCHNICK, opts[:target], ":No such nick/channel"
110
+ end
111
+ end
112
+
113
+ def handle_incoming_quit(opts = {})
114
+ return unless @alive
115
+ @alive = false
116
+ @channels.each { |c| c.quit(self, opts[:message]) }
117
+ kill_connection!
118
+ end
119
+
120
+ def handle_incoming_topic(opts = {})
121
+ return if @prefix.blank? || opts[:target].blank?
122
+ c = Marvin::IRC::Server::ChannelStore[opts[:target].downcase]
123
+ return if c.blank?
124
+ if !@channels.include?(c)
125
+ err :NOTONCHANNEL, opts[:target], ":Not a member of that channel"
126
+ elsif opts[:message].nil?
127
+ t = c.topic
128
+ if t.blank?
129
+ rpl :NOTOPIC, c.name, ":No topic is set"
130
+ else
131
+ rpl :TOPIC, c.name, ":#{t}"
132
+ end
133
+ else
134
+ c.topic self, opts[:message].strip
135
+ end
136
+ end
137
+
138
+ end
@@ -0,0 +1,127 @@
1
+ module Marvin::IRC::Server
2
+ class UserConnection < AbstractConnection
3
+ USER_MODES = "aAbBcCdDeEfFGhHiIjkKlLmMnNopPQrRsStUvVwWxXyYzZ0123459*@"
4
+ CHANNEL_MODES = "bcdefFhiIklmnoPqstv"
5
+ CHANNEL = /^[\&\#]+/
6
+ include User::HandleMixin
7
+
8
+ attr_accessor :nick, :host, :user, :prefix, :password, :mode,
9
+ :real_name, :nick_attempts, :channels, :ping_count
10
+
11
+ def inspect
12
+ "#<Marvin::IRC::Server::UserConnection nick='#{@nick}' host='#{@host}' real_name='#{@real_name}' channels=[#{self.channels.map { |c| c.name.inspect}.join(", ")}]>"
13
+ end
14
+
15
+ def initialize(base, buffer = [])
16
+ super
17
+ @nick_attempts = 0
18
+ @channels = []
19
+ @ping_count = 0
20
+ end
21
+
22
+ # Notify is essentially command BUT it
23
+ # requires that the prefix is set.
24
+ def notify(command, *args)
25
+ opts = args.extract_options!
26
+ return if opts[:prefix].blank?
27
+ command command, *(args << opts)
28
+ end
29
+
30
+ # Notification messages
31
+
32
+ def message(user, message)
33
+ notify :PRIVMSG, @nick, ":#{message}", :prefix => user.prefix
34
+ dispatch :outgoing_message, :user => user, :message => message, :target => self
35
+ end
36
+
37
+ def notice(user, message)
38
+ notify :NOTICE, @nick, ":#{message}", :prefix => user.prefix
39
+ dispatch :outgoing_notice, :user => user, :message => message, :target => self
40
+ end
41
+
42
+ # Get the user / channel targeted by a particular request.
43
+
44
+ def target_from(target)
45
+ case target
46
+ when CHANNEL
47
+ chan = Marvin::IRC::Server::ChannelStore[target.downcase]
48
+ if chan.nil?
49
+ rpl :NOSUCHNICK, target, ":No such nick/channel"
50
+ elsif !chan.member?(self)
51
+ err :CANNOTSENDTOCHAN, target, ":Cannot send to channel"
52
+ else
53
+ return chan
54
+ end
55
+ else
56
+ user = Marvin::IRC::Server::UserStore[target.downcase]
57
+ if user.nil?
58
+ err :NOSUCHNICK, target, ":No suck nick/channel"
59
+ else
60
+ return user
61
+ end
62
+ end
63
+ end
64
+
65
+ # Implementations for connect / disconnect
66
+
67
+ def process_connect
68
+ super
69
+ end
70
+
71
+ def process_disconnect
72
+ super
73
+ end
74
+
75
+ def kill_connection!
76
+ @connection.kill_connection!
77
+ end
78
+
79
+ protected
80
+
81
+ def welcome_if_complete!
82
+ update_prefix!
83
+ # Next, send the MOTD and other misc. stuff
84
+ return if @welcomed || @prefix.blank?
85
+ rpl :WELCOME, @nick, ":Welcome to Marvin Server - #{@prefix}"
86
+ rpl :YOURHOST, @nick, ":Your host is #{server_host}, running version #{Marvin.version}"
87
+ rpl :CREATED, @nick, ":This server was created #{@connection.started_at}"
88
+ rpl :MYINFO, @nick, ":#{server_host} #{Marvin.version} #{USER_MODES} #{CHANNEL_MODES}"
89
+ rpl :MOTDSTART, @nick, ":- MOTD"
90
+ rpl :MOTD, @nick, ":- Welcome to Marvin Server, a Ruby + EventMachine ircd."
91
+ rpl :ENDOFMOTD, @nick, ":- End of /MOTD command."
92
+ @welcomed = true
93
+ end
94
+
95
+ def update_prefix!
96
+ @prefix = "#{@nick}!~#{@user}@#{peer_name}" if details_complete?
97
+ end
98
+
99
+ def details_complete?
100
+ !@nick.nil? && !@user.nil?
101
+ end
102
+
103
+ def server_host
104
+ @connection.host
105
+ end
106
+
107
+ def server_port
108
+ @connection.port
109
+ end
110
+
111
+ def rpl(number, *args)
112
+ numeric Marvin::IRC::Replies["RPL_#{number.to_s.upcase}"], *args
113
+ end
114
+
115
+ def err(number, *args)
116
+ numeric Marvin::IRC::Replies["ERR_#{number.to_s.upcase}"], *args
117
+ end
118
+
119
+ def numeric(number, *args)
120
+ args << {} unless args[-1].is_a?(Hash)
121
+ args[-1][:prefix] ||= server_host
122
+ args.unshift(@nick) unless args.first == @nick
123
+ command(number.to_s, *args)
124
+ end
125
+
126
+ end
127
+ end
data/lib/marvin/loader.rb CHANGED
@@ -1,69 +1,144 @@
1
+ require 'fileutils'
2
+ require 'singleton'
3
+
1
4
  module Marvin
2
5
  class Loader
6
+ include Singleton
3
7
 
4
- cattr_accessor :setup_block
8
+ # A Controller is any class e.g. a client / server
9
+ # which is provides the main functionality of the
10
+ # current client.
11
+ CONTROLLERS = {
12
+ :client => Marvin::Settings.default_client,
13
+ :server => Marvin::IRC::Server,
14
+ :ring_server => Marvin::Distributed::RingServer,
15
+ :distributed_client => Marvin::Distributed::DRbClient,
16
+ :console => nil
17
+ }
5
18
 
6
- cattr_accessor :start_hooks, :stop_hooks
7
- self.stop_hooks, self.start_hooks = [], []
19
+ # For each of the known types, define a method
20
+ # as Marvin::Loader.type? so we can easily do
21
+ # things like conditional registers.
22
+ class << self
23
+ CONTROLLERS.keys.each do |type|
24
+ define_method(:"#{type}?") { Marvin::Loader.type == type }
25
+ end
26
+ end
8
27
 
28
+ cattr_accessor :hooks, :boot, :type
29
+ self.hooks = {}
30
+ self.type = :client
31
+
32
+ # Old style of registering a block to be run on startup
33
+ # for doing setup etc. now replaced by before_run
9
34
  def self.before_connecting(&blk)
10
- self.setup_block = blk
35
+ Marvin::Logger.warn "Marvin::Loader.before_connecting is deprecated, please use before_run instead."
36
+ before_run(&blk)
11
37
  end
12
38
 
13
- def setup_defaults
14
- Marvin::Logger.setup
39
+ # Append a hook for a given type of hook in order
40
+ # to be called later on via invoke_hooks!
41
+ def self.append_hook(type, &blk)
42
+ self.hooks_for(type) << blk unless blk.blank?
15
43
  end
16
44
 
17
- def self.before_run(&blk)
18
- self.start_hooks << blk unless blk.blank?
45
+ # Return all of the existing hooks or an empty
46
+ # for a given hook type.
47
+ def self.hooks_for(type)
48
+ (self.hooks[type.to_sym] ||= [])
19
49
  end
20
50
 
21
- def self.after_stop(&blk)
22
- self.stop_hooks << blk unless blk.blank?
51
+ # Invoke (call) all of the hooks for a given
52
+ # type.
53
+ def self.invoke_hooks!(type)
54
+ hooks_for(type).each { |hook| hook.call }
23
55
  end
24
56
 
25
- def load_handlers
26
- handlers = Dir[Marvin::Settings.root / "handlers/**/*.rb"].map { |h| h[0..-4] }
27
- handlers.each do |handler|
28
- require handler
29
- end
57
+ # Append a call back to be run at a specific stage.
58
+
59
+ # Register a hook to be run before the controller
60
+ # has started running.
61
+ def self.before_run(&blk)
62
+ append_hook(:before_run, &blk)
30
63
  end
31
64
 
32
- def load_settings
33
- Marvin::Settings.setup
34
- Marvin::Settings.default_client.configuration = Marvin::Settings.to_hash
35
- Marvin::Settings.default_client.setup
65
+ # Register a hook to be run after the controller
66
+ # has stopped. Note that this will not guarantee
67
+ # all processing has completed.
68
+ def self.after_stop(&blk)
69
+ append_hook(:after_stop, &blk)
36
70
  end
37
71
 
38
- def pre_connect_setup
39
- Marvin::DataStore.load!
40
- require(Marvin::Settings.root / "config/setup")
41
- self.setup_block.call unless self.setup_block.blank?
72
+ def self.run!(type = :client)
73
+ self.type = type.to_sym
74
+ self.instance.run!
75
+ end
76
+
77
+ def self.stop!(force = false)
78
+ self.instance.stop!(force)
42
79
  end
43
80
 
44
81
  def run!
45
- Marvin::Options.parse!
46
- self.setup_defaults
82
+ self.register_signals
83
+ Marvin::Options.parse! unless self.type == :console
84
+ Marvin::Daemon.daemonize! if Marvin::Settings.daemon?
85
+ Marvin::Logger.setup
47
86
  self.load_settings
87
+ require(Marvin::Settings.root / "config/setup")
48
88
  self.load_handlers
49
- self.pre_connect_setup
50
- self.start_hooks.each { |h| h.call }
51
- Marvin::Settings.default_client.run
89
+ self.class.invoke_hooks! :before_run
90
+ attempt_controller_action! :run
52
91
  end
53
92
 
54
- def stop!
55
- Marvin::Settings.default_client.stop
56
- self.stop_hooks.each { |h| h.call }
57
- Marvin::DataStore.dump!
93
+ def stop!(force = false)
94
+ if force || !@attempted_stop
95
+ self.class.invoke_hooks! :before_stop
96
+ attempt_controller_action! :stop
97
+ self.class.invoke_hooks! :after_stop
98
+ @attempted_stop = true
99
+ end
100
+ Marvin::Daemon.cleanup! if Marvin::Settings.daemon?
58
101
  end
59
102
 
60
- def self.run!
61
- self.new.run!
103
+ protected
104
+
105
+ # Get the controller for the current type if
106
+ # it exists and attempt to class a given action.
107
+ def attempt_controller_action!(action)
108
+ klass = CONTROLLERS[self.type]
109
+ klass.send(action) unless klass.blank? || !klass.respond_to?(action, true)
62
110
  end
63
111
 
64
- def self.stop!
65
- self.new.stop!
112
+ # Load all of the handler's in the handlers subfolder
113
+ # of a marvin installation.
114
+ def load_handlers
115
+ Dir[Marvin::Settings.root / "handlers/**/*.rb"].each { |handler| require handler }
66
116
  end
67
117
 
118
+ def load_settings
119
+ Marvin::Settings.setup
120
+ case Marvin::Loader.type
121
+ # Perform client / type specific setup.
122
+ when :client
123
+ Marvin::Settings.default_client.configuration = Marvin::Settings.to_hash
124
+ Marvin::Settings.default_client.setup
125
+ when :distributed_client
126
+ Marvin::Settings.default_client = Marvin::Distributed::DRbClient
127
+ end
128
+ end
129
+
130
+ def register_signals
131
+ ["INT", "TERM"].each do |sig|
132
+ trap sig do
133
+ Marvin::Loader.stop!
134
+ exit
135
+ end
136
+ end
137
+ end
138
+
139
+ # Register to the Marvin::DataStore methods
140
+ before_run { Marvin::DataStore.load! if Marvin::Loader.type == :client }
141
+ after_stop { Marvin::DataStore.dump! if Marvin::Loader.type == :client }
142
+
68
143
  end
69
144
  end