comboy-autumn 3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/README.textile +1192 -0
  2. data/autumn.gemspec +25 -0
  3. data/bin/autumn +27 -0
  4. data/lib/autumn.rb +2 -0
  5. data/lib/autumn/authentication.rb +290 -0
  6. data/lib/autumn/channel_leaf.rb +107 -0
  7. data/lib/autumn/coder.rb +166 -0
  8. data/lib/autumn/console_boot.rb +9 -0
  9. data/lib/autumn/ctcp.rb +250 -0
  10. data/lib/autumn/daemon.rb +207 -0
  11. data/lib/autumn/datamapper_hacks.rb +290 -0
  12. data/lib/autumn/foliater.rb +231 -0
  13. data/lib/autumn/formatting.rb +236 -0
  14. data/lib/autumn/generator.rb +231 -0
  15. data/lib/autumn/genesis.rb +191 -0
  16. data/lib/autumn/inheritable_attributes.rb +162 -0
  17. data/lib/autumn/leaf.rb +738 -0
  18. data/lib/autumn/log_facade.rb +49 -0
  19. data/lib/autumn/misc.rb +87 -0
  20. data/lib/autumn/script.rb +74 -0
  21. data/lib/autumn/speciator.rb +165 -0
  22. data/lib/autumn/stem.rb +919 -0
  23. data/lib/autumn/stem_facade.rb +176 -0
  24. data/resources/daemons/Anothernet.yml +3 -0
  25. data/resources/daemons/AustHex.yml +29 -0
  26. data/resources/daemons/Bahamut.yml +67 -0
  27. data/resources/daemons/Dancer.yml +3 -0
  28. data/resources/daemons/GameSurge.yml +3 -0
  29. data/resources/daemons/IRCnet.yml +3 -0
  30. data/resources/daemons/Ithildin.yml +7 -0
  31. data/resources/daemons/KineIRCd.yml +56 -0
  32. data/resources/daemons/PTlink.yml +6 -0
  33. data/resources/daemons/QuakeNet.yml +20 -0
  34. data/resources/daemons/RFC1459.yml +158 -0
  35. data/resources/daemons/RFC2811.yml +16 -0
  36. data/resources/daemons/RFC2812.yml +36 -0
  37. data/resources/daemons/RatBox.yml +25 -0
  38. data/resources/daemons/Ultimate.yml +24 -0
  39. data/resources/daemons/Undernet.yml +6 -0
  40. data/resources/daemons/Unreal.yml +110 -0
  41. data/resources/daemons/_Other.yml +7 -0
  42. data/resources/daemons/aircd.yml +33 -0
  43. data/resources/daemons/bdq-ircd.yml +3 -0
  44. data/resources/daemons/hybrid.yml +38 -0
  45. data/resources/daemons/ircu.yml +67 -0
  46. data/resources/daemons/tr-ircd.yml +8 -0
  47. data/skel/Rakefile +135 -0
  48. data/skel/config/global.yml +2 -0
  49. data/skel/config/seasons/testing/database.yml +7 -0
  50. data/skel/config/seasons/testing/leaves.yml +7 -0
  51. data/skel/config/seasons/testing/season.yml +2 -0
  52. data/skel/config/seasons/testing/stems.yml +9 -0
  53. data/skel/leaves/administrator/README +20 -0
  54. data/skel/leaves/administrator/controller.rb +67 -0
  55. data/skel/leaves/administrator/views/autumn.txt.erb +1 -0
  56. data/skel/leaves/administrator/views/reload.txt.erb +11 -0
  57. data/skel/leaves/insulter/README +17 -0
  58. data/skel/leaves/insulter/controller.rb +65 -0
  59. data/skel/leaves/insulter/views/about.txt.erb +1 -0
  60. data/skel/leaves/insulter/views/help.txt.erb +1 -0
  61. data/skel/leaves/insulter/views/insult.txt.erb +1 -0
  62. data/skel/leaves/scorekeeper/README +34 -0
  63. data/skel/leaves/scorekeeper/config.yml +2 -0
  64. data/skel/leaves/scorekeeper/controller.rb +104 -0
  65. data/skel/leaves/scorekeeper/helpers/general.rb +64 -0
  66. data/skel/leaves/scorekeeper/models/channel.rb +12 -0
  67. data/skel/leaves/scorekeeper/models/person.rb +14 -0
  68. data/skel/leaves/scorekeeper/models/pseudonym.rb +11 -0
  69. data/skel/leaves/scorekeeper/models/score.rb +14 -0
  70. data/skel/leaves/scorekeeper/tasks/stats.rake +17 -0
  71. data/skel/leaves/scorekeeper/views/about.txt.erb +1 -0
  72. data/skel/leaves/scorekeeper/views/change.txt.erb +5 -0
  73. data/skel/leaves/scorekeeper/views/history.txt.erb +11 -0
  74. data/skel/leaves/scorekeeper/views/points.txt.erb +5 -0
  75. data/skel/leaves/scorekeeper/views/usage.txt.erb +1 -0
  76. data/skel/script/console +34 -0
  77. data/skel/script/daemon +29 -0
  78. data/skel/script/destroy +48 -0
  79. data/skel/script/generate +48 -0
  80. data/skel/script/server +15 -0
  81. metadata +170 -0
@@ -0,0 +1,191 @@
1
+ # Defines the Autumn::Genesis class, which bootstraps the Autumn environment
2
+ # and starts the Foliater.
3
+
4
+ require 'set'
5
+ require 'rubygems'
6
+ require 'yaml'
7
+ require 'logger'
8
+ require 'facets'
9
+ require 'facets/random'
10
+ require 'anise'
11
+ require 'autumn'
12
+ require 'autumn/misc'
13
+ require 'autumn/speciator'
14
+ require 'autumn/authentication'
15
+
16
+ AUTUMN_VERSION = "3.0 (7-4-08)"
17
+
18
+ module Autumn # :nodoc:
19
+
20
+ # Oversight class responsible for initializing the Autumn environment. To boot
21
+ # the Autumn environment start all configured leaves, you make an instance of
22
+ # this class and run the boot! method. Leaves will each run in their own
23
+ # thread, monitored by an oversight thread spawned by this class.
24
+
25
+ class Genesis # :nodoc:
26
+ # The Speciator singleton.
27
+ attr_reader :config
28
+
29
+ # Creates a new instance that can be used to boot Autumn.
30
+
31
+ def initialize
32
+ @config = Speciator.instance
33
+ end
34
+
35
+ # Bootstraps the Autumn environment, and begins the stems' execution threads
36
+ # if +invoke+ is set to true.
37
+
38
+ def boot!(invoke=true)
39
+ load_global_settings
40
+ load_season_settings
41
+ load_libraries
42
+ init_system_logger
43
+ load_daemon_info
44
+ load_shared_code
45
+ load_databases
46
+ invoke_foliater(invoke)
47
+ end
48
+
49
+ # Loads the settings in the global.yml file.
50
+ #
51
+ # PREREQS: None
52
+
53
+ def load_global_settings
54
+ begin
55
+ config.global YAML.load(File.open("#{APP_ROOT}/config/global.yml"))
56
+ rescue SystemCallError
57
+ raise "Couldn't find your global.yml file."
58
+ end
59
+ config.global :root => APP_ROOT
60
+ config.global :season => ENV['SEASON'] if ENV['SEASON']
61
+ end
62
+
63
+ # Loads the settings for the current season in its season.yml file.
64
+ #
65
+ # PREREQS: load_global_settings
66
+
67
+ def load_season_settings
68
+ @season_dir = "#{APP_ROOT}/config/seasons/#{config.global :season}"
69
+ raise "The current season doesn't have a directory." unless File.directory? @season_dir
70
+ begin
71
+ config.season YAML.load(File.open("#{@season_dir}/season.yml"))
72
+ rescue
73
+ # season.yml is optional
74
+ end
75
+ end
76
+
77
+ # Loads Autumn library objects.
78
+ #
79
+ # PREREQS: load_global_settings
80
+
81
+ def load_libraries
82
+ require 'autumn/inheritable_attributes'
83
+ require 'autumn/daemon'
84
+ require 'autumn/stem_facade'
85
+ require 'autumn/ctcp'
86
+ require 'autumn/stem'
87
+ require 'autumn/leaf'
88
+ require 'autumn/channel_leaf'
89
+ require 'autumn/foliater'
90
+ require 'autumn/log_facade'
91
+ end
92
+
93
+ # Initializes the system-level logger.
94
+ #
95
+ # PREREQS: load_libraries
96
+
97
+ def init_system_logger
98
+ config.global :logfile => Logger.new(log_name, config.global(:log_history) || 10, 1024*1024)
99
+ begin
100
+ config.global(:logfile).level = Logger.const_get(config.season(:logging).upcase)
101
+ rescue NameError
102
+ puts "The level #{config.season(:logging).inspect} was not understood; the log level has been raised to INFO."
103
+ config.global(:logfile).level = Logger::INFO
104
+ end
105
+ config.global :system_logger => LogFacade.new(config.global(:logfile), 'N/A', 'System')
106
+ @logger = config.global(:system_logger)
107
+ end
108
+
109
+ # Instantiates Daemons from YAML files in resources/daemons. The daemons are
110
+ # named after their YAML files.
111
+ #
112
+ # PREREQS: load_libraries
113
+
114
+ def load_daemon_info
115
+ Dir.glob("#{AUTUMN_LIB_DIR}/../resources/daemons/*.yml").each do |yml_file|
116
+ yml = YAML.load(File.open(yml_file, 'r'))
117
+ Daemon.new File.basename(yml_file, '.yml'), yml
118
+ end
119
+ end
120
+
121
+ # Loads Ruby code in the shared directory.
122
+
123
+ def load_shared_code
124
+ Dir.glob("#{AUTUMN_LIB_DIR}/shared/**/*.rb").each { |lib| load lib }
125
+ end
126
+
127
+ # Creates connections to databases using the DataMapper gem.
128
+ #
129
+ # PREREQS: load_season_settings
130
+
131
+ def load_databases
132
+ db_file = "#{@season_dir}/database.yml"
133
+ if not File.exist? db_file then
134
+ $NO_DATABASE = true
135
+ return
136
+ end
137
+
138
+ require 'dm-core'
139
+ require 'autumn/datamapper_hacks'
140
+
141
+ dbconfig = YAML.load(File.open(db_file, 'r'))
142
+ dbconfig.rekey(&:to_sym).each do |db, config|
143
+ DataMapper.setup(db, config.kind_of?(Hash) ? config.rekey(&:to_sym) : config)
144
+ end
145
+ end
146
+
147
+ # Invokes the Foliater.load method. Spawns a new thread to oversee the
148
+ # stems' threads. This thread will exit when all leaves have terminated.
149
+ # Stems will not be started if +invoke+ is set to false.
150
+ #
151
+ # PREREQS: load_databases, load_season_settings, load_libraries,
152
+ # init_system_logger
153
+
154
+ def invoke_foliater(invoke=true)
155
+ begin
156
+ begin
157
+ stem_config = YAML.load(File.open("#{@season_dir}/stems.yml", 'r'))
158
+ rescue Errno::ENOENT
159
+ raise "Couldn't find stems.yml file for season #{config.global :season}"
160
+ end
161
+ begin
162
+ leaf_config = YAML.load(File.open("#{@season_dir}/leaves.yml", 'r'))
163
+ rescue Errno::ENOENT
164
+ # build a default leaf config
165
+ leaf_config = Hash.new
166
+ Dir.entries("leaves").each do |dir|
167
+ next if not File.directory? "leaves/#{dir}" or dir[0,1] == '.'
168
+ leaf_name = dir.camelcase
169
+ leaf_config[leaf_name] = { 'class' => leaf_name }
170
+ end
171
+ end
172
+
173
+ Foliater.instance.load stem_config, leaf_config, invoke
174
+ if invoke then
175
+ # suspend execution of the master thread until all stems are dead
176
+ while Foliater.instance.alive?
177
+ Thread.stop
178
+ end
179
+ end
180
+ rescue
181
+ @logger.fatal $!
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def log_name
188
+ "#{APP_ROOT}/log/#{config.global(:season)}.log"
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,162 @@
1
+ # This source file, originating from Ruby on Rails, extends the +Class+ class to
2
+ # allows attributes to be shared within an inheritance hierarchy, but where each
3
+ # descendant gets a copy of their parents' attributes, instead of just a pointer
4
+ # to the same. This means that the child can add elements to, for example, an
5
+ # array without those additions being shared with either their parent, siblings,
6
+ # or children, which is unlike the regular class-level attributes that are
7
+ # shared across the entire hierarchy.
8
+ #
9
+ # This functionality is used by Leaf's filter features; if not for this
10
+ # extension, then when a subclass changed its filter chain, all of its
11
+ # superclasses' filter chains would change as well. This class allows a subclass
12
+ # to inherit a _copy_ of the superclass's filter chain, but independently change
13
+ # that copy without affecting the superclass's filter chain.
14
+ #
15
+ # Copyright (c)2004 David Heinemeier Hansson
16
+ #
17
+ # Permission is hereby granted, free of charge, to any person obtaining
18
+ # a copy of this software and associated documentation files (the
19
+ # "Software"), to deal in the Software without restriction, including
20
+ # without limitation the rights to use, copy, modify, merge, publish,
21
+ # distribute, sublicense, and/or sell copies of the Software, and to
22
+ # permit persons to whom the Software is furnished to do so, subject to
23
+ # the following conditions:
24
+ # The above copyright notice and this permission notice shall be
25
+ # included in all copies or substantial portions of the Software.
26
+
27
+ # Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
28
+ # their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
29
+ # to, for example, an array without those additions being shared with either their parent, siblings, or
30
+ # children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
31
+ class Class # :nodoc:
32
+ def class_inheritable_reader(*syms)
33
+ syms.each do |sym|
34
+ next if sym.is_a?(Hash)
35
+ class_eval <<-EOS
36
+ def self.#{sym}
37
+ read_inheritable_attribute(:#{sym})
38
+ end
39
+
40
+ def #{sym}
41
+ self.class.#{sym}
42
+ end
43
+ EOS
44
+ end
45
+ end
46
+
47
+ def class_inheritable_writer(*syms)
48
+ options = syms.last.is_a?(Hash) ? syms.pop : {}
49
+ syms.each do |sym|
50
+ class_eval <<-EOS
51
+ def self.#{sym}=(obj)
52
+ write_inheritable_attribute(:#{sym}, obj)
53
+ end
54
+
55
+ #{"
56
+ def #{sym}=(obj)
57
+ self.class.#{sym} = obj
58
+ end
59
+ " unless options[:instance_writer] == false }
60
+ EOS
61
+ end
62
+ end
63
+
64
+ def class_inheritable_array_writer(*syms)
65
+ options = syms.last.is_a?(Hash) ? syms.pop : {}
66
+ syms.each do |sym|
67
+ class_eval <<-EOS
68
+ def self.#{sym}=(obj)
69
+ write_inheritable_array(:#{sym}, obj)
70
+ end
71
+
72
+ #{"
73
+ def #{sym}=(obj)
74
+ self.class.#{sym} = obj
75
+ end
76
+ " unless options[:instance_writer] == false }
77
+ EOS
78
+ end
79
+ end
80
+
81
+ def class_inheritable_hash_writer(*syms)
82
+ options = syms.last.is_a?(Hash) ? syms.pop : {}
83
+ syms.each do |sym|
84
+ class_eval <<-EOS
85
+ def self.#{sym}=(obj)
86
+ write_inheritable_hash(:#{sym}, obj)
87
+ end
88
+
89
+ #{"
90
+ def #{sym}=(obj)
91
+ self.class.#{sym} = obj
92
+ end
93
+ " unless options[:instance_writer] == false }
94
+ EOS
95
+ end
96
+ end
97
+
98
+ def class_inheritable_accessor(*syms)
99
+ class_inheritable_reader(*syms)
100
+ class_inheritable_writer(*syms)
101
+ end
102
+
103
+ def class_inheritable_array(*syms)
104
+ class_inheritable_reader(*syms)
105
+ class_inheritable_array_writer(*syms)
106
+ end
107
+
108
+ def class_inheritable_hash(*syms)
109
+ class_inheritable_reader(*syms)
110
+ class_inheritable_hash_writer(*syms)
111
+ end
112
+
113
+ def inheritable_attributes
114
+ @inheritable_attributes ||= EMPTY_INHERITABLE_ATTRIBUTES
115
+ end
116
+
117
+ def write_inheritable_attribute(key, value)
118
+ if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
119
+ @inheritable_attributes = {}
120
+ end
121
+ inheritable_attributes[key] = value
122
+ end
123
+
124
+ def write_inheritable_array(key, elements)
125
+ write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
126
+ write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
127
+ end
128
+
129
+ def write_inheritable_hash(key, hash)
130
+ write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil?
131
+ write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash))
132
+ end
133
+
134
+ def read_inheritable_attribute(key)
135
+ inheritable_attributes[key]
136
+ end
137
+
138
+ def reset_inheritable_attributes
139
+ @inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
140
+ end
141
+
142
+ private
143
+ # Prevent this constant from being created multiple times
144
+ EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES)
145
+
146
+ def inherited_with_inheritable_attributes(child)
147
+ inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes)
148
+
149
+ if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
150
+ new_inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
151
+ else
152
+ new_inheritable_attributes = inheritable_attributes.inject({}) do |memo, (key, value)|
153
+ memo.update(key => (value.dup rescue value))
154
+ end
155
+ end
156
+
157
+ child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes)
158
+ end
159
+
160
+ alias inherited_without_inheritable_attributes inherited
161
+ alias inherited inherited_with_inheritable_attributes
162
+ end
@@ -0,0 +1,738 @@
1
+ # Defines the Autumn::Leaf class, a library on which robust IRC bots can be
2
+ # written.
3
+
4
+ require 'yaml'
5
+ require 'timeout'
6
+ require 'erb'
7
+ require 'autumn/formatting'
8
+
9
+ module Autumn
10
+
11
+ # This is the superclass that all Autumn leaves use. To write a leaf, sublcass
12
+ # this class and implement methods for each of your leaf's commands. Your
13
+ # leaf's repertoire of commands is derived from the names of the methods you
14
+ # write. For instance, to have your leaf respond to a "!hello" command in IRC,
15
+ # write a method like so:
16
+ #
17
+ # def hello_command(stem, sender, reply_to, msg)
18
+ # stem.message "Why hello there!", reply_to
19
+ # end
20
+ #
21
+ # You can also implement this method as:
22
+ #
23
+ # def hello_command(stem, sender, reply_to, msg)
24
+ # return "Why hello there!"
25
+ # end
26
+ #
27
+ # Methods of the form <tt>[word]_command</tt> tell the leaf to respond to
28
+ # commands in IRC of the form "![word]". They should accept four parameters:
29
+ #
30
+ # 1. the Stem that received the message,
31
+ # 2. the sender hash for the person who sent the message (see below),
32
+ # 3. the "reply-to" string (either the name of the channel that the command
33
+ # was typed on, or the nick of the person that whispered the message), and
34
+ # 4. any text following the command. For instance, if the person typed "!eat A
35
+ # tasty slice of pizza", the last parameter would be "A tasty slice of
36
+ # pizza". This is nil if no text was supplied with the command.
37
+ #
38
+ # <b>Sender hashes:</b> A "sender hash" is a hash with the following keys:
39
+ # +nick+ (the user's nickname), +user+ (the user's username), and +host+ (the
40
+ # user's hostname). Any of these fields except +nick+ could be nil. Sender
41
+ # hashes are used throughout the Stem and Leaf classes, as well as other
42
+ # classes; they always have the same keys.
43
+ #
44
+ # If your <tt>*_command</tt> method returns a string, it will be sent as an
45
+ # IRC message to "reply-to" parameter.If your leaf needs to respond to more
46
+ # complicated commands, you will have to override the
47
+ # did_receive_channel_message method (see below). If you like, you can remove
48
+ # the quit_command method in your subclass, for instance, to prevent the leaf
49
+ # from responding to !quit. You can also protect that method using filters
50
+ # (see "Filters").
51
+ #
52
+ # If you want to separate view logic from the controller, you can use ERb to
53
+ # template your views. See the render method for more information.
54
+ #
55
+ # = Hook Methods
56
+ #
57
+ # Aside from adding your own <tt>*_command</tt>-type methods, you should
58
+ # investigate overriding the "hook" methods, such as will_start_up,
59
+ # did_start_up, did_receive_private_message, did_receive_channel_message, etc.
60
+ # There's a laundry list of so-named methods you can override. Their default
61
+ # implementations do nothing, so there's no need to call +super+.
62
+ #
63
+ # = Stem Convenience Methods
64
+ #
65
+ # Most of the IRC actions (such as joining and leaving a channel, setting a
66
+ # topic, etc.) are part of a Stem object. If your leaf is only running off
67
+ # of one stem, you can call these stem methods directly, as if they were
68
+ # methods in the Leaf class. Otherwise, you will need to specify which stem
69
+ # to perform these IRC actions on. Usually, the stem is given to you, as a
70
+ # parameter for your <tt>*_command</tt> method, for instance.
71
+ #
72
+ # For the sake of convenience, you can make Stem method calls on the +stems+
73
+ # attribute; these calls will be forwarded to every stem in the +stems+
74
+ # attribute. For instance, to broadcast a message to all servers and all
75
+ # channels:
76
+ #
77
+ # stems.message "Ready for orders!"
78
+ #
79
+ # = Filters
80
+ #
81
+ # Like Ruby on Rails, you can add filters to each of your commands to be
82
+ # executed before or after the command is run. You can do this using the
83
+ # before_filter and after_filter methods, just like in Rails. Filters are run
84
+ # in the order they are added to the chain. Thus, if you wanted to run your
85
+ # preload filter before you ran your cache filter, you'd write the calls in
86
+ # this order:
87
+ #
88
+ # class MyLeaf < Leaf
89
+ # before_filter :my_preload
90
+ # before_filter :my_cache
91
+ # end
92
+ #
93
+ # See the documentation for the before_filter and after_filter methods and the
94
+ # README file for more information on filters.
95
+ #
96
+ # = Authentication
97
+ #
98
+ # If a leaf is initialized with a hash for the +authentication+ option, the
99
+ # values of that hash are used to choose an authenticator that will be run
100
+ # before each command. This authenticator will determine whether or not the
101
+ # user can run that command. The options that can be specified in this hash
102
+ # are:
103
+ #
104
+ # +type+:: The name of a class in the Autumn::Authentication module, in
105
+ # snake_case. Thus, if you wanted to use the
106
+ # Autumn::Authentication::Password class, which does password-based
107
+ # authentication, you'd set this value to +password+.
108
+ # +only+:: A list of protected commands for which authentication is required;
109
+ # all other commands are unprotected.
110
+ # +except+:: A list of unprotected commands; all other commands require
111
+ # authentication.
112
+ # +silent+:: Normally, when someone fails to authenticate himself before
113
+ # running a protected command, the leaf responds with an error
114
+ # message (e.g., "You have to authenticate with a password first").
115
+ # Set this to true to suppress this behaivor.
116
+ #
117
+ # In addition, you can also specify any custom options for your authenticator.
118
+ # These options are passed to the authenticator's initialize method. See the
119
+ # classes in the Autumn::Authentication module for such options.
120
+ #
121
+ # If you annotate a command method as protected, the authenticator will be run
122
+ # unconditionally, regardless of the +only+ or +except+ options:
123
+ #
124
+ # class Controller < Autumn::Leaf
125
+ # def destructive_command(stem, sender, reply_to, msg)
126
+ # # ...
127
+ # end
128
+ # ann :destructive_command, :protected => true
129
+ # end
130
+ #
131
+ # = Logging
132
+ #
133
+ # Autumn comes with a framework for logging as well. It's very similar to the
134
+ # Ruby on Rails logging framework. To log an error message:
135
+ #
136
+ # logger.error "Quiz data is missing!"
137
+ #
138
+ # By default the logger will only log +info+ events and above in production
139
+ # seasons, and will log all messages for debug seasons. (See the README for
140
+ # more on seasons.) To customize the logger, and for more information on
141
+ # logging, see the LogFacade class documentation.
142
+ #
143
+ # = Colorizing and Formatting Text
144
+ #
145
+ # The Autumn::Formatting module contains sub-modules which handle formatting
146
+ # for different clients (such as mIRC-style formatting, the most common). The
147
+ # specific formatting module that's included depends on the leaf's
148
+ # initialization options; see initialize.
149
+
150
+ class Leaf
151
+ include Anise::Annotation
152
+
153
+ # Default for the +command_prefix+ init option.
154
+ DEFAULT_COMMAND_PREFIX = '!'
155
+ @@view_alias = Hash.new { |h,k| k }
156
+
157
+ # The LogFacade instance for this leaf.
158
+ attr :logger
159
+ # The Stem instances running this leaf.
160
+ attr :stems
161
+ # The configuration for this leaf.
162
+ attr :options
163
+
164
+ # Instantiates a leaf. This is generally handled by the Foliater class.
165
+ # Valid options are:
166
+ #
167
+ # +command_prefix+:: The string that must precede all command names (default
168
+ # "!")
169
+ # +responds_to_private_messages+:: If true, the bot responds to known
170
+ # commands sent in private messages.
171
+ # +logger+:: The LogFacade instance for this leaf.
172
+ # +database+:: The name of a custom database connection to use.
173
+ # +formatter+:: The name of an Autumn::Formatting class to use as the
174
+ # formatter (chooses Autumn::Formatting::DEFAULT by default).
175
+ #
176
+ # As well as any user-defined options you want.
177
+
178
+ def initialize(opts={})
179
+ @port = opts[:port]
180
+ @options = opts
181
+ @options[:command_prefix] ||= DEFAULT_COMMAND_PREFIX
182
+ @break_flag = false
183
+ @logger = options[:logger]
184
+
185
+ @stems = Set.new
186
+ # Let the stems array respond to methods as if it were a single stem
187
+ class << @stems
188
+ def method_missing(meth, *args)
189
+ if all? { |stem| stem.respond_to? meth } then
190
+ collect { |stem| stem.send(meth, *args) }
191
+ else
192
+ super
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ def preconfigure # :nodoc:
199
+ if options[:authentication] then
200
+ @authenticator = Autumn::Authentication.const_get(options[:authentication]['type'].camelcase).new(options[:authentication].rekey(&:to_sym))
201
+ stems.add_listener @authenticator
202
+ end
203
+ end
204
+
205
+ # Simplifies method calls for one-stem leaves.
206
+
207
+ def method_missing(meth, *args) # :nodoc:
208
+ if stems.size == 1 and stems.only.respond_to? meth then
209
+ stems.only.send meth, *args
210
+ else
211
+ super
212
+ end
213
+ end
214
+
215
+ ########################## METHODS INVOKED BY STEM #########################
216
+
217
+ def stem_ready(stem) # :nodoc:
218
+ return unless Thread.exclusive { stems.ready?.all? }
219
+ database { startup_check }
220
+ end
221
+
222
+ def irc_privmsg_event(stem, sender, arguments) # :nodoc:
223
+ database do
224
+ if arguments[:channel] then
225
+ command_parse stem, sender, arguments
226
+ did_receive_channel_message stem, sender, arguments[:channel], arguments[:message]
227
+ else
228
+ command_parse stem, sender, arguments if options[:respond_to_private_messages]
229
+ did_receive_private_message stem, sender, arguments[:message]
230
+ end
231
+ end
232
+ end
233
+
234
+ def irc_join_event(stem, sender, arguments) # :nodoc:
235
+ database { someone_did_join_channel stem, sender, arguments[:channel] }
236
+ end
237
+
238
+ def irc_part_event(stem, sender, arguments) # :nodoc:
239
+ database { someone_did_leave_channel stem, sender, arguments[:channel] }
240
+ end
241
+
242
+ def irc_mode_event(stem, sender, arguments) # :nodoc:
243
+ database do
244
+ if arguments[:recipient] then
245
+ gained_usermodes(stem, arguments[:mode]) { |prop| someone_did_gain_usermode stem, arguments[:recipient], prop, arguments[:parameter], sender }
246
+ lost_usermodes(stem, arguments[:mode]) { |prop| someone_did_lose_usermode stem, arguments[:recipient], prop, arguments[:parameter], sender }
247
+ elsif arguments[:parameter] and stem.server_type.privilege_mode?(arguments[:mode]) then
248
+ gained_privileges(stem, arguments[:mode]) { |prop| someone_did_gain_privilege stem, arguments[:channel], arguments[:parameter], prop, sender }
249
+ lost_privileges(stem, arguments[:mode]) { |prop| someone_did_lose_privilege stem, arguments[:channel], arguments[:parameter], prop, sender }
250
+ else
251
+ gained_properties(stem, arguments[:mode]) { |prop| channel_did_gain_property stem, arguments[:channel], prop, arguments[:parameter], sender }
252
+ lost_properties(stem, arguments[:mode]) { |prop| channel_did_lose_property stem, arguments[:channel], prop, arguments[:parameter], sender }
253
+ end
254
+ end
255
+ end
256
+
257
+ def irc_topic_event(stem, sender, arguments) # :nodoc:
258
+ database { someone_did_change_topic stem, sender, arguments[:channel], arguments[:topic] }
259
+ end
260
+
261
+ def irc_invite_event(stem, sender, arguments) # :nodoc:
262
+ database { someone_did_invite stem, sender, arguments[:recipient], arguments[:channel] }
263
+ end
264
+
265
+ def irc_kick_event(stem, sender, arguments) # :nodoc:
266
+ database { someone_did_kick stem, sender, arguments[:channel], arguments[:recipient], arguments[:message] }
267
+ end
268
+
269
+ def irc_notice_event(stem, sender, arguments) # :nodoc:
270
+ database do
271
+ if arguments[:recipient] then
272
+ did_receive_notice stem, sender, arguments[:recipient], arguments[:message]
273
+ else
274
+ did_receive_notice stem, sender, arguments[:channel], arguments[:message]
275
+ end
276
+ end
277
+ end
278
+
279
+ def irc_nick_event(stem, sender, arguments) # :nodoc:
280
+ database { nick_did_change stem, sender, arguments[:nick] }
281
+ end
282
+
283
+ def irc_quit_event(stem, sender, arguments) # :nodoc:
284
+ database { someone_did_quit stem, sender, arguments[:message] }
285
+ end
286
+
287
+ ########################### OTHER PUBLIC METHODS ###########################
288
+
289
+ # Invoked just before the leaf starts up. Override this method to do any
290
+ # pre-startup tasks you need. The leaf is fully initialized and all methods
291
+ # and helper objects are available.
292
+
293
+ def will_start_up
294
+ end
295
+
296
+ # Performs the block in the context of a database, referenced by symbol. For
297
+ # instance, if you had defined in database.yml a connection named
298
+ # "scorekeeper", you could access that connection like so:
299
+ #
300
+ # database(:scorekeeper) do
301
+ # [...]
302
+ # end
303
+ #
304
+ # If your database is named after your leaf (as in the example above for a
305
+ # leaf named "Scorekeeper"), it will automatically be set as the database
306
+ # context for the scope of all hook, filter and command methods. However, if
307
+ # your database connection is named differently, or if you are working in a
308
+ # method not invoked by the Leaf class, you will need to set the connection
309
+ # using this method.
310
+ #
311
+ # If you omit the +dbname+ parameter, it will try to guess the name of your
312
+ # database connection using the leaf's name and the leaf's class name.
313
+ #
314
+ # If the database connection cannot be found, the block is executed with no
315
+ # database scope.
316
+
317
+ def database(dbname=nil, &block)
318
+ dbname ||= database_name
319
+ if dbname then
320
+ repository dbname, &block
321
+ else
322
+ yield
323
+ end
324
+ end
325
+
326
+ # Trues to guess the name of the database connection this leaf is using.
327
+ # Looks for database connections named after either this leaf's identifier
328
+ # or this leaf's class name. Returns nil if no suitable connection is found.
329
+
330
+ def database_name # :nodoc:
331
+ return nil unless Module.constants.include? 'DataMapper' or Module.constants.include? :DataMapper
332
+ raise "No such database connection #{options[:database]}" if options[:database] and DataMapper::Repository.adapters[options[:database]].nil?
333
+ # Custom database connection specified
334
+ return options[:database].to_sym if options[:database]
335
+ # Leaf config name
336
+ return leaf_name.to_sym if DataMapper::Repository.adapters[leaf_name.to_sym]
337
+ # Leaf config name, underscored
338
+ return leaf_name.methodize.to_sym if DataMapper::Repository.adapters[leaf_name.methodize.to_sym]
339
+ # Leaf class name
340
+ return self.class.to_s.to_sym if DataMapper::Repository.adapters[self.class.to_s.to_sym]
341
+ # Leaf class name, underscored
342
+ return self.class.to_s.methodize.to_sym if DataMapper::Repository.adapters[self.class.to_s.methodize.to_sym]
343
+ # I give up
344
+ return nil
345
+ end
346
+
347
+ def inspect # :nodoc:
348
+ "#<#{self.class.to_s} #{leaf_name}>"
349
+ end
350
+
351
+ protected
352
+
353
+ # Duplicates a command. This method aliases the command method and also
354
+ # ensures the correct view file is rendered if appropriate.
355
+ #
356
+ # alias_command :google, :g
357
+
358
+ def self.alias_command(old, new)
359
+ raise NoMethodError, "Unknown command #{old}" unless instance_methods.include?("#{old}_command")
360
+ alias_method "#{new}_command", "#{old}_command"
361
+ @@view_alias[new] = old
362
+ end
363
+
364
+ # Adds a filter to the end of the list of filters to be run before a command
365
+ # is executed. You can use these filters to perform tasks that prepare the
366
+ # leaf to respond to a command, or to determine whether or not a command
367
+ # should be run (e.g., authentication). Pass the name of your filter as a
368
+ # symbol, and an optional has of options:
369
+ #
370
+ # +only+:: Only run the filter for these commands
371
+ # +except+:: Do not run the filter for these commands
372
+ #
373
+ # Each option can refer to a single command or an Array of commands.
374
+ # Commands should be symbols such as <tt>:quit</tt> for the !quit command.
375
+ #
376
+ # Your method will be called with these parameters:
377
+ #
378
+ # 1. the Stem instance that received the command,
379
+ # 2. the name of the channel to which the command was sent (or nil if it was
380
+ # a private message),
381
+ # 3. the sender hash,
382
+ # 4. the name of the command that was typed, as a symbol,
383
+ # 5. any additional parameters after the command (same as the +msg+
384
+ # parameter in the <tt>*_command</tt> methods),
385
+ # 6. the custom options that were given to before_filter.
386
+ #
387
+ # If your filter returns either nil or false, the filter chain will be
388
+ # halted and the command will not be run. For example, if you create the
389
+ # filter:
390
+ #
391
+ # before_filter :read_files, :only => [ :quit, :reload ], :remote_files => true
392
+ #
393
+ # then any time the bot receives a "!quit" or "!reload" command, it will
394
+ # first evaluate:
395
+ #
396
+ # read_files_filter <stem>, <channel>, <sender hash>, <command>, <message>, { :remote_files => true }
397
+ #
398
+ # and if the result is not false or nil, the command will be executed.
399
+
400
+ def self.before_filter(filter, options={})
401
+ if options[:only] and not options[:only].kind_of? Array then
402
+ options[:only] = [ options[:only] ]
403
+ end
404
+ if options[:except] and not options[:except].kind_of? Array then
405
+ options[:except] = [ options[:except] ]
406
+ end
407
+ write_inheritable_array 'before_filters', [ [ filter.to_sym, options ] ]
408
+ end
409
+
410
+ # Adds a filter to the end of the list of filters to be run after a command
411
+ # is executed. You can use these filters to perform tasks that must be done
412
+ # after a command is run, such as cleaning up temporary files. Pass the name
413
+ # of your filter as a symbol, and an optional has of options. See the
414
+ # before_filter docs for more.
415
+ #
416
+ # Your method will be called with five parameters -- see the before_filter
417
+ # method for more information. Unlike before_filter filters, however, any
418
+ # return value is ignored. For example, if you create the filter:
419
+ #
420
+ # after_filter :clean_tmp, :only => :sendfile, :remove_symlinks => true
421
+ #
422
+ # then any time the bot receives a "!sendfile" command, after running the
423
+ # command it will evaluate:
424
+ #
425
+ # clean_tmp_filter <stem>, <channel>, <sender hash>, :sendfile, <message>, { :remove_symlinks => true }
426
+
427
+ def self.after_filter(filter, options={})
428
+ if options[:only] and not options[:only].kind_of? Array then
429
+ options[:only] = [ options[:only] ]
430
+ end
431
+ if options[:except] and not options[:except].kind_of? Array then
432
+ options[:except] = [ options[:except] ]
433
+ end
434
+ write_inheritable_array 'after_filters', [ [ filter.to_sym, options ] ]
435
+ end
436
+
437
+ # Invoked after the leaf is started up and is ready to accept commands.
438
+ # Override this method to do any post-startup tasks you need, such as
439
+ # displaying a greeting message.
440
+
441
+ def did_start_up
442
+ end
443
+
444
+ # Invoked just before the leaf exists. Override this method to perform any
445
+ # pre-shutdown tasks you need.
446
+
447
+ def will_quit
448
+ end
449
+
450
+ # Invoked when the leaf receives a private (whispered) message. +sender+ is
451
+ # a sender hash.
452
+
453
+ def did_receive_private_message(stem, sender, msg)
454
+ end
455
+
456
+ # Invoked when a message is sent to a channel the leaf is a member of (even
457
+ # if that message was a valid command). +sender+ is a sender hash.
458
+
459
+ def did_receive_channel_message(stem, sender, channel, msg)
460
+ end
461
+
462
+ # Invoked when someone joins a channel the leaf is a member of. +person+ is
463
+ # a sender hash.
464
+
465
+ def someone_did_join_channel(stem, person, channel)
466
+ end
467
+
468
+ # Invoked when someone leaves a channel the leaf is a member of. +person+ is
469
+ # a sender hash.
470
+
471
+ def someone_did_leave_channel(stem, person, channel)
472
+ end
473
+
474
+ # Invoked when someone gains a channel privilege. +privilege+ can be any
475
+ # value returned by the stem's Daemon. If the privilege is not in the hash,
476
+ # it will be a string (not a symbol) equal to the letter value for that
477
+ # privilege (e.g., 'v' for voice). +bestower+ is a sender hash.
478
+
479
+ def someone_did_gain_privilege(stem, channel, nick, privilege, bestower)
480
+ end
481
+
482
+ # Invoked when someone loses a channel privilege.
483
+
484
+ def someone_did_lose_privilege(stem, channel, nick, privilege, bestower)
485
+ end
486
+
487
+ # Invoked when a channel gains a property. +property+ can be any value
488
+ # returned by the stem's Daemon. If the peroperty is not in the hash, it
489
+ # will be a string (not a symbol) equal to the letter value for that
490
+ # property (e.g., 'k' for password). If the property takes an argument (such
491
+ # as user limit or password), it will be passed via +argument+ (which is
492
+ # otherwise nil). +bestower+ is a sender hash.
493
+
494
+ def channel_did_gain_property(stem, channel, property, argument, bestower)
495
+ end
496
+
497
+ # Invoked when a channel loses a property.
498
+
499
+ def channel_did_lose_property(stem, channel, property, argument, bestower)
500
+ end
501
+
502
+ # Invoked when someone gains a user mode. +mode+ can be an value returned by
503
+ # the stem's Daemon. If the mode is not in the hash, it will be a string
504
+ # (not a symbol) equal to the letter value for that mode (e.g., 'i' for
505
+ # invisible). +bestower+ is a sender hash.
506
+
507
+ def someone_did_gain_usermode(stem, nick, mode, argument, bestower)
508
+ end
509
+
510
+ # Invoked when someone loses a user mode.
511
+
512
+ def someone_did_lose_usermode(stem, nick, mode, argument, bestower)
513
+ end
514
+
515
+ # Invoked when someone changes a channel's topic. +topic+ is the new topic.
516
+ # +person+ is a sender hash.
517
+
518
+ def someone_did_change_topic(stem, person, channel, topic)
519
+ end
520
+
521
+ # Invoked when someone invites another person to a channel. For some IRC
522
+ # servers, this will only be invoked if the leaf itself is invited into a
523
+ # channel. +inviter+ is a sender hash; +invitee+ is a nick.
524
+
525
+ def someone_did_invite(stem, inviter, invitee, channel)
526
+ end
527
+
528
+ # Invoked when someone is kicked from a channel. Note that this is called
529
+ # when your leaf is kicked as well, so it may well be the case that
530
+ # +channel+ is a channel you are no longer in! +kicker+ is a sender hash;
531
+ # +victim+ is a nick.
532
+
533
+ def someone_did_kick(stem, kicker, channel, victim, msg)
534
+ end
535
+
536
+ # Invoked when a notice is received. Notices are like channel or pivate
537
+ # messages, except that leaves are expected _not_ to respond to them.
538
+ # +sender+ is a sender hash; +recipient+ is either a channel or a nick.
539
+
540
+ def did_receive_notice(stem, sender, recipient, msg)
541
+ end
542
+
543
+ # Invoked when a user changes his nick. +person+ is a sender hash containing
544
+ # the person's old nick, and +nick+ is their new nick.
545
+
546
+ def nick_did_change(stem, person, nick)
547
+ end
548
+
549
+ # Invoked when someone quits IRC. +person+ is a sender hash.
550
+
551
+ def someone_did_quit(stem, person, msg)
552
+ end
553
+
554
+ UNADVERTISED_COMMANDS = [ 'about', 'commands' ] # :nodoc:
555
+
556
+ # Typing this command displays a list of all commands for each leaf running
557
+ # off this stem.
558
+
559
+ def commands_command(stem, sender, reply_to, msg)
560
+ commands = self.class.instance_methods.select { |m| m =~ /^\w+_command$/ }
561
+ commands.map! { |m| m.match(/^(\w+)_command$/)[1] }
562
+ commands.reject! { |m| UNADVERTISED_COMMANDS.include? m }
563
+ return if commands.empty?
564
+ commands.map! { |c| "#{options[:command_prefix]}#{c}" }
565
+ "Commands for #{leaf_name}: #{commands.sort.join(', ')}"
566
+ end
567
+
568
+ # Sets a custom view name to render. The name doesn't have to correspond to
569
+ # an actual command, just an existing view file. Example:
570
+ #
571
+ # def my_command(stem, sender, reply_to, msg)
572
+ # render :help and return if msg.empty? # user doesn't know how to use the command
573
+ # [...]
574
+ # end
575
+ #
576
+ # Only one view is rendered per command. If this method is called multiple
577
+ # times, the last value set is used. This method has no effect outside of
578
+ # a <tt>*_command</tt> method.
579
+ #
580
+ # By default, the view named after the command will be rendered. If no such
581
+ # view exists, the value returned by the method will be used as the
582
+ # response.
583
+
584
+ def render(view)
585
+ # Since only one command is executed per thread, we can store the view to
586
+ # render as a thread-local variable.
587
+ raise "The render method should be called at most once per command" if Thread.current[:render_view]
588
+ Thread.current[:render_view] = view.to_s
589
+ return nil
590
+ end
591
+
592
+ # Gets or sets a variable for use in the view. Use this method in
593
+ # <tt>*_command</tt> methods to pass data to the view ERb file, and in the
594
+ # ERb file to retrieve these values. For example, in your controller.rb
595
+ # file:
596
+ #
597
+ # def my_command(stem, sender, reply_to, msg)
598
+ # var :num_lights => 4
599
+ # end
600
+ #
601
+ # And in your my.txt.erb file:
602
+ #
603
+ # THERE ARE <%= var :num_lights %> LIGHTS!
604
+
605
+ def var(vars)
606
+ return Thread.current[:vars][vars] if vars.kind_of? Symbol
607
+ return vars.each { |var, val| Thread.current[:vars][var] = val } if vars.kind_of? Hash
608
+ raise ArgumentError, "var must take a symbol or a hash"
609
+ end
610
+
611
+ private
612
+
613
+ def startup_check
614
+ return if @started_up
615
+ @started_up = true
616
+ did_start_up
617
+ end
618
+
619
+ def command_parse(stem, sender, arguments)
620
+ if arguments[:channel] or options[:respond_to_private_messages] then
621
+ reply_to = arguments[:channel] ? arguments[:channel] : sender[:nick]
622
+ matches = arguments[:message].match(/^#{Regexp.escape options[:command_prefix]}(\w+)\s*(.*)$/)
623
+ if matches then
624
+ name = matches[1].to_sym
625
+ msg = matches[2]
626
+ origin = sender.merge(:stem => stem)
627
+ command_exec name, stem, arguments[:channel], sender, msg, reply_to
628
+ end
629
+ end
630
+ end
631
+
632
+ def command_exec(name, stem, channel, sender, msg, reply_to)
633
+ cmd_sym = "#{name}_command".to_sym
634
+ return unless respond_to? cmd_sym
635
+ msg = nil if msg.empty?
636
+
637
+ return unless authenticated?(name, stem, channel, sender)
638
+ return unless run_before_filters(name, stem, channel, sender, name, msg)
639
+
640
+ Thread.current[:vars] = Hash.new
641
+ return_val = send(cmd_sym, stem, sender, reply_to, msg)
642
+ view = Thread.current[:render_view]
643
+ view ||= @@view_alias[name]
644
+ if return_val.kind_of? String then
645
+ stem.message return_val, reply_to
646
+ elsif options[:views][view.to_s] then
647
+ stem.message parse_view(view.to_s), reply_to
648
+ #else
649
+ # raise "You must either specify a view to render or return a string to send."
650
+ end
651
+ Thread.current[:vars] = nil
652
+ Thread.current[:render_view] = nil # Clear it out in case the command is synchronized
653
+ run_after_filters name, stem, channel, sender, name, msg
654
+ end
655
+
656
+ def parse_view(name)
657
+ return nil unless options[:views][name]
658
+ ERB.new(options[:views][name]).result(binding)
659
+ end
660
+
661
+ def leaf_name
662
+ Foliater.instance.leaves.index self
663
+ end
664
+
665
+ def run_before_filters(cmd, stem, channel, sender, command, msg)
666
+ command = cmd.to_sym
667
+ self.class.before_filters.each do |filter, options|
668
+ local_opts = options.dup
669
+ next if local_opts[:only] and not local_opts.delete(:only).include? command
670
+ next if local_opts[:except] and local_opts.delete(:except).include? command
671
+ return false unless method("#{filter}_filter")[stem, channel, sender, command, msg, local_opts]
672
+ end
673
+ return true
674
+ end
675
+
676
+ def run_after_filters(cmd, stem, channel, sender, command, msg)
677
+ command = cmd.to_sym
678
+ self.class.after_filters.each do |filter, options|
679
+ local_opts = options.dup
680
+ next if local_opts[:only] and not local_opts.delete(:only).include? command
681
+ next if local_opts[:except] and local_opts.delete(:except).include? command
682
+ method("#{filter}_filter")[stem, channel, sender, command, msg, local_opts]
683
+ end
684
+ end
685
+
686
+ def authenticated?(cmd, stem, channel, sender)
687
+ return true if @authenticator.nil?
688
+ # Any method annotated as protected is authenticated unconditionally
689
+ if not self.class.ann("#{cmd}_command".to_sym, :protected) then
690
+ return true
691
+ end
692
+ if @authenticator.authenticate(stem, channel, sender, self) then
693
+ return true
694
+ else
695
+ stem.message @authenticator.unauthorized, channel unless options[:authentication]['silent']
696
+ return false
697
+ end
698
+ end
699
+
700
+ def gained_privileges(stem, privstr)
701
+ return unless privstr[0,1] == '+'
702
+ privstr.except_first.each_char { |c| yield stem.server_type.privilege[c] }
703
+ end
704
+
705
+ def lost_privileges(stem, privstr)
706
+ return unless privstr[0,1] == '-'
707
+ privstr.except_first.each_char { |c| yield stem.server_type.privilege[c] }
708
+ end
709
+
710
+ def gained_properties(stem, propstr)
711
+ return unless propstr[0,1] == '+'
712
+ propstr.except_first.each_char { |c| yield stem.server_type.channel_mode[c] }
713
+ end
714
+
715
+ def lost_properties(stem, propstr)
716
+ return unless propstr[0,1] == '-'
717
+ propstr.except_first.each_char { |c| yield stem.server_type.channel_mode[c] }
718
+ end
719
+
720
+ def gained_usermodes(stem, modestr)
721
+ return unless modestr[0,1] == '+'
722
+ modestr.except_first.each_char { |c| yield stem.server_type.usermode[c] }
723
+ end
724
+
725
+ def lost_usermodes(stem, modestr)
726
+ return unless modestr[0,1] == '-'
727
+ modestr.except_first.each_char { |c| yield stem.server_type.usermode[c] }
728
+ end
729
+
730
+ def self.before_filters
731
+ read_inheritable_attribute('before_filters') or []
732
+ end
733
+
734
+ def self.after_filters
735
+ read_inheritable_attribute('after_filters') or []
736
+ end
737
+ end
738
+ end