comboy-autumn 3.1

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