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.
- data/README.textile +1192 -0
- data/autumn.gemspec +25 -0
- data/bin/autumn +27 -0
- data/lib/autumn.rb +2 -0
- data/lib/autumn/authentication.rb +290 -0
- data/lib/autumn/channel_leaf.rb +107 -0
- data/lib/autumn/coder.rb +166 -0
- data/lib/autumn/console_boot.rb +9 -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 +191 -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/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/resources/daemons/Anothernet.yml +3 -0
- data/resources/daemons/AustHex.yml +29 -0
- data/resources/daemons/Bahamut.yml +67 -0
- data/resources/daemons/Dancer.yml +3 -0
- data/resources/daemons/GameSurge.yml +3 -0
- data/resources/daemons/IRCnet.yml +3 -0
- data/resources/daemons/Ithildin.yml +7 -0
- data/resources/daemons/KineIRCd.yml +56 -0
- data/resources/daemons/PTlink.yml +6 -0
- data/resources/daemons/QuakeNet.yml +20 -0
- data/resources/daemons/RFC1459.yml +158 -0
- data/resources/daemons/RFC2811.yml +16 -0
- data/resources/daemons/RFC2812.yml +36 -0
- data/resources/daemons/RatBox.yml +25 -0
- data/resources/daemons/Ultimate.yml +24 -0
- data/resources/daemons/Undernet.yml +6 -0
- data/resources/daemons/Unreal.yml +110 -0
- data/resources/daemons/_Other.yml +7 -0
- data/resources/daemons/aircd.yml +33 -0
- data/resources/daemons/bdq-ircd.yml +3 -0
- data/resources/daemons/hybrid.yml +38 -0
- data/resources/daemons/ircu.yml +67 -0
- data/resources/daemons/tr-ircd.yml +8 -0
- data/skel/Rakefile +135 -0
- data/skel/config/global.yml +2 -0
- data/skel/config/seasons/testing/database.yml +7 -0
- data/skel/config/seasons/testing/leaves.yml +7 -0
- data/skel/config/seasons/testing/season.yml +2 -0
- data/skel/config/seasons/testing/stems.yml +9 -0
- data/skel/leaves/administrator/README +20 -0
- data/skel/leaves/administrator/controller.rb +67 -0
- data/skel/leaves/administrator/views/autumn.txt.erb +1 -0
- data/skel/leaves/administrator/views/reload.txt.erb +11 -0
- data/skel/leaves/insulter/README +17 -0
- data/skel/leaves/insulter/controller.rb +65 -0
- data/skel/leaves/insulter/views/about.txt.erb +1 -0
- data/skel/leaves/insulter/views/help.txt.erb +1 -0
- data/skel/leaves/insulter/views/insult.txt.erb +1 -0
- data/skel/leaves/scorekeeper/README +34 -0
- data/skel/leaves/scorekeeper/config.yml +2 -0
- data/skel/leaves/scorekeeper/controller.rb +104 -0
- data/skel/leaves/scorekeeper/helpers/general.rb +64 -0
- data/skel/leaves/scorekeeper/models/channel.rb +12 -0
- data/skel/leaves/scorekeeper/models/person.rb +14 -0
- data/skel/leaves/scorekeeper/models/pseudonym.rb +11 -0
- data/skel/leaves/scorekeeper/models/score.rb +14 -0
- data/skel/leaves/scorekeeper/tasks/stats.rake +17 -0
- data/skel/leaves/scorekeeper/views/about.txt.erb +1 -0
- data/skel/leaves/scorekeeper/views/change.txt.erb +5 -0
- data/skel/leaves/scorekeeper/views/history.txt.erb +11 -0
- data/skel/leaves/scorekeeper/views/points.txt.erb +5 -0
- data/skel/leaves/scorekeeper/views/usage.txt.erb +1 -0
- data/skel/script/console +34 -0
- data/skel/script/daemon +29 -0
- data/skel/script/destroy +48 -0
- data/skel/script/generate +48 -0
- data/skel/script/server +15 -0
- 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
|
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
|