autumn 3.1.8
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +11 -0
- data/CHANGELOG +567 -0
- data/MANIFEST +110 -0
- data/README +1114 -0
- data/README.textile +1153 -0
- data/Rakefile +75 -0
- data/autumn.gemspec +44 -0
- data/bin/autumn +11 -0
- data/lib/autumn.rb +8 -0
- data/lib/autumn/authentication.rb +238 -0
- data/lib/autumn/channel_leaf.rb +107 -0
- data/lib/autumn/coder.rb +166 -0
- data/lib/autumn/console_boot.rb +10 -0
- data/lib/autumn/ctcp.rb +250 -0
- data/lib/autumn/daemon.rb +207 -0
- data/lib/autumn/datamapper_hacks.rb +290 -0
- data/lib/autumn/foliater.rb +231 -0
- data/lib/autumn/formatting.rb +236 -0
- data/lib/autumn/generator.rb +231 -0
- data/lib/autumn/genesis.rb +190 -0
- data/lib/autumn/inheritable_attributes.rb +162 -0
- data/lib/autumn/leaf.rb +738 -0
- data/lib/autumn/log_facade.rb +49 -0
- data/lib/autumn/misc.rb +87 -0
- data/lib/autumn/resources/daemons/Anothernet.yml +3 -0
- data/lib/autumn/resources/daemons/AustHex.yml +29 -0
- data/lib/autumn/resources/daemons/Bahamut.yml +67 -0
- data/lib/autumn/resources/daemons/Dancer.yml +3 -0
- data/lib/autumn/resources/daemons/GameSurge.yml +3 -0
- data/lib/autumn/resources/daemons/IRCnet.yml +3 -0
- data/lib/autumn/resources/daemons/Ithildin.yml +7 -0
- data/lib/autumn/resources/daemons/KineIRCd.yml +56 -0
- data/lib/autumn/resources/daemons/PTlink.yml +6 -0
- data/lib/autumn/resources/daemons/QuakeNet.yml +20 -0
- data/lib/autumn/resources/daemons/RFC1459.yml +158 -0
- data/lib/autumn/resources/daemons/RFC2811.yml +16 -0
- data/lib/autumn/resources/daemons/RFC2812.yml +36 -0
- data/lib/autumn/resources/daemons/RatBox.yml +25 -0
- data/lib/autumn/resources/daemons/Ultimate.yml +24 -0
- data/lib/autumn/resources/daemons/Undernet.yml +6 -0
- data/lib/autumn/resources/daemons/Unreal.yml +110 -0
- data/lib/autumn/resources/daemons/_Other.yml +7 -0
- data/lib/autumn/resources/daemons/aircd.yml +33 -0
- data/lib/autumn/resources/daemons/bdq-ircd.yml +3 -0
- data/lib/autumn/resources/daemons/hybrid.yml +38 -0
- data/lib/autumn/resources/daemons/ircu.yml +67 -0
- data/lib/autumn/resources/daemons/tr-ircd.yml +8 -0
- data/lib/autumn/script.rb +74 -0
- data/lib/autumn/speciator.rb +165 -0
- data/lib/autumn/stem.rb +919 -0
- data/lib/autumn/stem_facade.rb +176 -0
- data/lib/autumn/tool/bin.rb +301 -0
- data/lib/autumn/tool/create.rb +48 -0
- data/lib/autumn/tool/project_creator.rb +110 -0
- data/lib/autumn/version.rb +3 -0
- data/lib/skel/Rakefile +163 -0
- data/lib/skel/config/global.yml +2 -0
- data/lib/skel/config/seasons/testing/database.yml +4 -0
- data/lib/skel/config/seasons/testing/leaves.yml +9 -0
- data/lib/skel/config/seasons/testing/season.yml +2 -0
- data/lib/skel/config/seasons/testing/stems.yml +10 -0
- data/lib/skel/leaves/administrator/README +20 -0
- data/lib/skel/leaves/administrator/controller.rb +67 -0
- data/lib/skel/leaves/administrator/views/autumn.txt.erb +1 -0
- data/lib/skel/leaves/administrator/views/reload.txt.erb +11 -0
- data/lib/skel/leaves/insulter/README +17 -0
- data/lib/skel/leaves/insulter/controller.rb +65 -0
- data/lib/skel/leaves/insulter/views/about.txt.erb +1 -0
- data/lib/skel/leaves/insulter/views/help.txt.erb +1 -0
- data/lib/skel/leaves/insulter/views/insult.txt.erb +1 -0
- data/lib/skel/leaves/scorekeeper/README +34 -0
- data/lib/skel/leaves/scorekeeper/config.yml +2 -0
- data/lib/skel/leaves/scorekeeper/controller.rb +104 -0
- data/lib/skel/leaves/scorekeeper/helpers/general.rb +64 -0
- data/lib/skel/leaves/scorekeeper/models/channel.rb +12 -0
- data/lib/skel/leaves/scorekeeper/models/person.rb +14 -0
- data/lib/skel/leaves/scorekeeper/models/pseudonym.rb +11 -0
- data/lib/skel/leaves/scorekeeper/models/score.rb +14 -0
- data/lib/skel/leaves/scorekeeper/tasks/stats.rake +17 -0
- data/lib/skel/leaves/scorekeeper/views/about.txt.erb +1 -0
- data/lib/skel/leaves/scorekeeper/views/change.txt.erb +5 -0
- data/lib/skel/leaves/scorekeeper/views/history.txt.erb +11 -0
- data/lib/skel/leaves/scorekeeper/views/points.txt.erb +5 -0
- data/lib/skel/leaves/scorekeeper/views/usage.txt.erb +1 -0
- data/lib/skel/log/README +1 -0
- data/lib/skel/script/console +28 -0
- data/lib/skel/script/destroy +48 -0
- data/lib/skel/script/generate +48 -0
- data/lib/skel/shared/README +1 -0
- data/lib/skel/tmp/README +1 -0
- data/spec/authentication_spec.rb +328 -0
- data/spec/channel_leaf_spec.rb +142 -0
- data/spec/coder_spec.rb +146 -0
- data/spec/ctcp_spec.rb +222 -0
- data/spec/daemon_spec.rb +202 -0
- data/spec/datamapper_hacks_spec.rb +164 -0
- data/tasks/authors.rake +30 -0
- data/tasks/changelog.rake +18 -0
- data/tasks/copyright.rake +21 -0
- data/tasks/doc.rake +7 -0
- data/tasks/gem.rake +23 -0
- data/tasks/gem_installer.rake +76 -0
- data/tasks/install_dependencies.rake +6 -0
- data/tasks/manifest.rake +4 -0
- data/tasks/rcov.rake +23 -0
- data/tasks/release.rake +52 -0
- data/tasks/reversion.rake +8 -0
- data/tasks/setup.rake +24 -0
- data/tasks/spec.rake +7 -0
- data/tasks/yard.rake +4 -0
- metadata +188 -0
@@ -0,0 +1,190 @@
|
|
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/misc'
|
12
|
+
require 'autumn/speciator'
|
13
|
+
require 'autumn/authentication'
|
14
|
+
|
15
|
+
AUTUMN_VERSION = "3.0 (7-4-08)"
|
16
|
+
|
17
|
+
module Autumn # :nodoc:
|
18
|
+
|
19
|
+
# Oversight class responsible for initializing the Autumn environment. To boot
|
20
|
+
# the Autumn environment start all configured leaves, you make an instance of
|
21
|
+
# this class and run the boot! method. Leaves will each run in their own
|
22
|
+
# thread, monitored by an oversight thread spawned by this class.
|
23
|
+
|
24
|
+
class Genesis # :nodoc:
|
25
|
+
# The Speciator singleton.
|
26
|
+
attr_reader :config
|
27
|
+
|
28
|
+
# Creates a new instance that can be used to boot Autumn.
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@config = Speciator.instance
|
32
|
+
end
|
33
|
+
|
34
|
+
# Bootstraps the Autumn environment, and begins the stems' execution threads
|
35
|
+
# if +invoke+ is set to true.
|
36
|
+
|
37
|
+
def boot!(invoke=true)
|
38
|
+
load_global_settings
|
39
|
+
load_season_settings
|
40
|
+
load_libraries
|
41
|
+
init_system_logger
|
42
|
+
load_daemon_info
|
43
|
+
load_shared_code
|
44
|
+
load_databases
|
45
|
+
invoke_foliater(invoke)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Loads the settings in the global.yml file.
|
49
|
+
#
|
50
|
+
# PREREQS: None
|
51
|
+
|
52
|
+
def load_global_settings
|
53
|
+
begin
|
54
|
+
config.global YAML.load(File.open("#{AL_ROOT}/config/global.yml"))
|
55
|
+
rescue SystemCallError
|
56
|
+
raise "Couldn't find your global.yml file."
|
57
|
+
end
|
58
|
+
config.global :root => AL_ROOT
|
59
|
+
config.global :season => ENV['SEASON'] if ENV['SEASON']
|
60
|
+
end
|
61
|
+
|
62
|
+
# Loads the settings for the current season in its season.yml file.
|
63
|
+
#
|
64
|
+
# PREREQS: load_global_settings
|
65
|
+
|
66
|
+
def load_season_settings
|
67
|
+
@season_dir = "#{AL_ROOT}/config/seasons/#{config.global :season}"
|
68
|
+
raise "The current season doesn't have a directory." unless File.directory? @season_dir
|
69
|
+
begin
|
70
|
+
config.season YAML.load(File.open("#{@season_dir}/season.yml"))
|
71
|
+
rescue
|
72
|
+
# season.yml is optional
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Loads Autumn library objects.
|
77
|
+
#
|
78
|
+
# PREREQS: load_global_settings
|
79
|
+
|
80
|
+
def load_libraries
|
81
|
+
require 'autumn/inheritable_attributes'
|
82
|
+
require 'autumn/daemon'
|
83
|
+
require 'autumn/stem_facade'
|
84
|
+
require 'autumn/ctcp'
|
85
|
+
require 'autumn/stem'
|
86
|
+
require 'autumn/leaf'
|
87
|
+
require 'autumn/channel_leaf'
|
88
|
+
require 'autumn/foliater'
|
89
|
+
require 'autumn/log_facade'
|
90
|
+
end
|
91
|
+
|
92
|
+
# Initializes the system-level logger.
|
93
|
+
#
|
94
|
+
# PREREQS: load_libraries
|
95
|
+
|
96
|
+
def init_system_logger
|
97
|
+
config.global :logfile => Logger.new(log_name, config.global(:log_history) || 10, 1024*1024)
|
98
|
+
begin
|
99
|
+
config.global(:logfile).level = Logger.const_get(config.season(:logging).upcase)
|
100
|
+
rescue NameError
|
101
|
+
puts "The level #{config.season(:logging).inspect} was not understood; the log level has been raised to INFO."
|
102
|
+
config.global(:logfile).level = Logger::INFO
|
103
|
+
end
|
104
|
+
config.global :system_logger => LogFacade.new(config.global(:logfile), 'N/A', 'System')
|
105
|
+
@logger = config.global(:system_logger)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Instantiates Daemons from YAML files in resources/daemons. The daemons are
|
109
|
+
# named after their YAML files.
|
110
|
+
#
|
111
|
+
# PREREQS: load_libraries
|
112
|
+
|
113
|
+
def load_daemon_info
|
114
|
+
Dir.glob("#{::Autumn::ROOT}/autumn/resources/daemons/*.yml").each do |yml_file|
|
115
|
+
yml = YAML.load(File.open(yml_file, 'r'))
|
116
|
+
Daemon.new File.basename(yml_file, '.yml'), yml
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Loads Ruby code in the shared directory.
|
121
|
+
|
122
|
+
def load_shared_code
|
123
|
+
Dir.glob("#{AL_ROOT}/shared/**/*.rb").each { |lib| load lib }
|
124
|
+
end
|
125
|
+
|
126
|
+
# Creates connections to databases using the DataMapper gem.
|
127
|
+
#
|
128
|
+
# PREREQS: load_season_settings
|
129
|
+
|
130
|
+
def load_databases
|
131
|
+
db_file = "#{@season_dir}/database.yml"
|
132
|
+
if not File.exist? db_file then
|
133
|
+
$NO_DATABASE = true
|
134
|
+
return
|
135
|
+
end
|
136
|
+
|
137
|
+
require 'dm-core'
|
138
|
+
require 'autumn/datamapper_hacks'
|
139
|
+
|
140
|
+
dbconfig = YAML.load(File.open(db_file, 'r'))
|
141
|
+
dbconfig.rekey(&:to_sym).each do |db, config|
|
142
|
+
DataMapper.setup(db, config.kind_of?(Hash) ? config.rekey(&:to_sym) : config)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Invokes the Foliater.load method. Spawns a new thread to oversee the
|
147
|
+
# stems' threads. This thread will exit when all leaves have terminated.
|
148
|
+
# Stems will not be started if +invoke+ is set to false.
|
149
|
+
#
|
150
|
+
# PREREQS: load_databases, load_season_settings, load_libraries,
|
151
|
+
# init_system_logger
|
152
|
+
|
153
|
+
def invoke_foliater(invoke=true)
|
154
|
+
begin
|
155
|
+
begin
|
156
|
+
stem_config = YAML.load(File.open("#{@season_dir}/stems.yml", 'r'))
|
157
|
+
rescue Errno::ENOENT
|
158
|
+
raise "Couldn't find stems.yml file for season #{config.global :season}"
|
159
|
+
end
|
160
|
+
begin
|
161
|
+
leaf_config = YAML.load(File.open("#{@season_dir}/leaves.yml", 'r'))
|
162
|
+
rescue Errno::ENOENT
|
163
|
+
# build a default leaf config
|
164
|
+
leaf_config = Hash.new
|
165
|
+
Dir.entries("leaves").each do |dir|
|
166
|
+
next if not File.directory? "leaves/#{dir}" or dir[0,1] == '.'
|
167
|
+
leaf_name = dir.camelcase
|
168
|
+
leaf_config[leaf_name] = { 'class' => leaf_name }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
Foliater.instance.load stem_config, leaf_config, invoke
|
173
|
+
if invoke then
|
174
|
+
# suspend execution of the master thread until all stems are dead
|
175
|
+
while Foliater.instance.alive?
|
176
|
+
Thread.stop
|
177
|
+
end
|
178
|
+
end
|
179
|
+
rescue
|
180
|
+
@logger.fatal $!
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def log_name
|
187
|
+
"#{AL_ROOT}/log/#{config.global(:season)}.log"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
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
|
data/lib/autumn/leaf.rb
ADDED
@@ -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
|