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,49 @@
|
|
1
|
+
# Defines the Autumn::LogFacade class, which makes it easier for Stems and
|
2
|
+
# Leaves to add their information to outgoing log messages.
|
3
|
+
|
4
|
+
module Autumn
|
5
|
+
|
6
|
+
# This class is a facade for Ruby's +Logger+ that adds additional information
|
7
|
+
# to log entries. LogFacade will pass any method calls onto a Logger instance,
|
8
|
+
# but reformat log entries to include an Autumn object's type and name.
|
9
|
+
#
|
10
|
+
# For example, if you wanted a LogFacade for a Leaf named "Scorekeeper", you
|
11
|
+
# could instantiate one:
|
12
|
+
#
|
13
|
+
# facade = LogFacade.new(logger, 'Leaf', 'Scorekeeper')
|
14
|
+
#
|
15
|
+
# And a call such as:
|
16
|
+
#
|
17
|
+
# facade.info "Starting up"
|
18
|
+
#
|
19
|
+
# Would be reformatted as "Scorekeeper (Leaf): Starting up".
|
20
|
+
#
|
21
|
+
# In addition, this class will log messages to STDOUT if the +debug+ global
|
22
|
+
# option is set. Instantiation of this class is handled by Genesis and should
|
23
|
+
# not normally be done by the user.
|
24
|
+
|
25
|
+
class LogFacade
|
26
|
+
# The Autumn object type (typically "Stem" or "Leaf").
|
27
|
+
attr :type
|
28
|
+
# The name of the Autumn object.
|
29
|
+
attr :name
|
30
|
+
|
31
|
+
# Creates a new facade for +logger+ that prepends type and name information
|
32
|
+
# to each log message.
|
33
|
+
|
34
|
+
def initialize(logger, type, name)
|
35
|
+
@type = type
|
36
|
+
@name = name
|
37
|
+
@logger = logger
|
38
|
+
@stdout = Speciator.instance.season(:logging) == 'debug'
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing(meth, *args) # :nodoc:
|
42
|
+
if args.size == 1 and args.only.kind_of? String then
|
43
|
+
args = [ "#{name} (#{type}): #{args.only}" ]
|
44
|
+
end
|
45
|
+
@logger.send meth, *args
|
46
|
+
puts (args.first.kind_of?(Exception) ? (args.first.to_s + "\n" + args.first.backtrace.join("\n")) : args.first) if @stdout
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/autumn/misc.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Miscellaneous extra methods and objects used by Autumn, and additions to Ruby
|
2
|
+
# Core objects.
|
3
|
+
|
4
|
+
require 'thread'
|
5
|
+
require 'english/style'
|
6
|
+
|
7
|
+
class Numeric # :nodoc:
|
8
|
+
|
9
|
+
# Possibly pluralizes a noun based on this number's value. Returns this number
|
10
|
+
# and the noun as a string. This method attempts to use the Ruby English gem
|
11
|
+
# if available, and falls back on the very simple default of appending an "s"
|
12
|
+
# to the word to make it plural. If the Ruby English gem is not available, you
|
13
|
+
# can specify a custom plural form for the word. Examples:
|
14
|
+
#
|
15
|
+
# 5.pluralize('dog') #=> "5 dogs"
|
16
|
+
# 1.pluralize('car') #=> "1 car"
|
17
|
+
# 7.pluralize('mouse', 'mice') #=> "7 mice" (only necessary if Ruby English is not installed)
|
18
|
+
|
19
|
+
def pluralize(singular, plural=nil)
|
20
|
+
begin
|
21
|
+
return "#{to_s} #{self == 1 ? singular : singular.plural}"
|
22
|
+
rescue Gem::LoadError
|
23
|
+
plural ||= singular + 's'
|
24
|
+
return "#{to_s} #{(self == 1) ? singular : plural}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class String # :nodoc:
|
30
|
+
include English::Style
|
31
|
+
|
32
|
+
# Returns a copy of this string with the first character dropped.
|
33
|
+
|
34
|
+
def except_first
|
35
|
+
self[1, size-1]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Hash # :nodoc:
|
40
|
+
|
41
|
+
# Returns a hash that gives back the key if it has no value for that key.
|
42
|
+
|
43
|
+
def self.parroting(hsh={})
|
44
|
+
hsh ||= Hash.new
|
45
|
+
Hash.new { |h, k| k }.update(hsh)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# An implementation of +SizedQueue+ that, instead of blocking when the queue is
|
50
|
+
# full, simply discards the overflow, forgetting it.
|
51
|
+
|
52
|
+
class ForgetfulQueue < Queue # :nodoc:
|
53
|
+
|
54
|
+
# Creates a new sized queue.
|
55
|
+
|
56
|
+
def initialize(capacity)
|
57
|
+
@max = capacity
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns true if this queue is at maximum size.
|
61
|
+
|
62
|
+
def full?
|
63
|
+
size == @max
|
64
|
+
end
|
65
|
+
|
66
|
+
# Pushes an object onto the queue. If there is no space left on the queue,
|
67
|
+
# does nothing.
|
68
|
+
|
69
|
+
def push(obj)
|
70
|
+
Thread.exclusive { super unless full? }
|
71
|
+
end
|
72
|
+
alias_method :<<, :push
|
73
|
+
alias_method :enq, :push
|
74
|
+
end
|
75
|
+
|
76
|
+
# Adds the only method to Set.
|
77
|
+
|
78
|
+
class Set # :nodoc:
|
79
|
+
|
80
|
+
# Returns the only element of a one-element set. Raises an exception if there
|
81
|
+
# isn't exactly one element in the set.
|
82
|
+
|
83
|
+
def only
|
84
|
+
raise IndexError, "Set#only called on non-single-element set" unless size == 1
|
85
|
+
to_a.first
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Defines the Autumn::Script class, which runs the script/generate and
|
2
|
+
# script/destroy utilities.
|
3
|
+
|
4
|
+
require 'getoptlong'
|
5
|
+
require 'rdoc/usage'
|
6
|
+
require 'facets'
|
7
|
+
require 'autumn/generator'
|
8
|
+
|
9
|
+
module Autumn
|
10
|
+
|
11
|
+
# Manages data used by the script/generate and script/destroy scripts. This
|
12
|
+
# class is instantiated by the script, and manages the script's data and
|
13
|
+
# encapsulates common functionality between the two scripts. The object must
|
14
|
+
# be initialized and parse_argv must be called before all attributes are ready
|
15
|
+
# for access.
|
16
|
+
|
17
|
+
class Script # :nodoc:
|
18
|
+
# The name of the Autumn object to be created.
|
19
|
+
attr :name
|
20
|
+
# The type of object to be created (e.g., "leaf").
|
21
|
+
attr :object
|
22
|
+
# The version control system in use for this project, or nil if none is being used for this transaction.
|
23
|
+
attr :vcs
|
24
|
+
# The Generator instance used to create files.
|
25
|
+
attr :generator
|
26
|
+
|
27
|
+
# Creates a new instance.
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@generator = Autumn::Generator.new
|
31
|
+
end
|
32
|
+
|
33
|
+
# Parses +ARGV+ or similar array. Normally you would pass +ARGV+ into this
|
34
|
+
# method. Populates the +object+ and +name+ attributes and returns true.
|
35
|
+
# Outputs an error and returns false if the given arguments are invalid.
|
36
|
+
|
37
|
+
def parse_argv(argv)
|
38
|
+
if ARGV.length != 2 then
|
39
|
+
$stderr.puts "Please specify an object (e.g., 'leaf') and its name (e.g., 'Scorekeeper')."
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
|
43
|
+
@object = ARGV.shift
|
44
|
+
@name = ARGV.shift
|
45
|
+
|
46
|
+
return true
|
47
|
+
end
|
48
|
+
|
49
|
+
# Determines the version control system in use by this project and sets the
|
50
|
+
# +vcs+ attribute to its name (<tt>:cvs</tt>, <tt>:svn</tt>, or
|
51
|
+
# <tt>:git</tt>).
|
52
|
+
|
53
|
+
def use_vcs
|
54
|
+
@vcs = find_vcs
|
55
|
+
end
|
56
|
+
|
57
|
+
# Calls the method given by the symbol, with two arguments: the +name+
|
58
|
+
# attribute, and an options hash verbosity enabled and the VCS set to the
|
59
|
+
# value of +vcs+.
|
60
|
+
|
61
|
+
def call_generator(meth)
|
62
|
+
generator.send(meth, name, :verbose => true, :vcs => vcs)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def find_vcs
|
68
|
+
return :svn if File.exist? '.svn' and File.directory? '.svn'
|
69
|
+
return :cvs if File.exist? 'CVS' and File.directory? 'CVS'
|
70
|
+
return :git if File.exist? '.git' and File.directory? '.git'
|
71
|
+
return nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# Defines the Autumn::Speciator class, which stores the configurations of many
|
2
|
+
# Autumn objects.
|
3
|
+
|
4
|
+
require 'singleton'
|
5
|
+
|
6
|
+
module Autumn
|
7
|
+
|
8
|
+
# The Speciator stores the global, season, stem, and leaf configurations. It
|
9
|
+
# generates composite hashes, so that any leaf or stem can know its specific
|
10
|
+
# configuration as a combination of its options and those of the scopes above
|
11
|
+
# it.
|
12
|
+
#
|
13
|
+
# Smaller scopes override larger ones; any season-specific options will
|
14
|
+
# replace global options, and leaf or stem options will overwrite season
|
15
|
+
# options. Leaf and stem options are independent from each other, however,
|
16
|
+
# since leaves and stems share a many-to-many relationship.
|
17
|
+
#
|
18
|
+
# Option identifiers can be specified as strings or symbols but are always
|
19
|
+
# stored as symbols and never accessed as strings.
|
20
|
+
#
|
21
|
+
# This is a singleton class; only one instance of it exists for any Autumn
|
22
|
+
# process. However, for the sake of convenience, many other objects use a
|
23
|
+
# +config+ attribute containing the instance.
|
24
|
+
|
25
|
+
class Speciator
|
26
|
+
include Singleton
|
27
|
+
|
28
|
+
# Creates a new instance storing no options.
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@global_options = Hash.new
|
32
|
+
@season_options = Hash.new
|
33
|
+
@stem_options = Hash.autonew
|
34
|
+
@leaf_options = Hash.autonew
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the global-scope or season-scope config option with the given
|
38
|
+
# symbol. Season-scope config options will override global ones.
|
39
|
+
|
40
|
+
def [](sym)
|
41
|
+
@season_options[sym] or @global_options[sym]
|
42
|
+
end
|
43
|
+
|
44
|
+
# When called with a hash: Takes a hash of options and values, and sets them
|
45
|
+
# at the global scope level.
|
46
|
+
#
|
47
|
+
# When called with an option identifier: Returns the value for that option at
|
48
|
+
# the global scope level.
|
49
|
+
|
50
|
+
def global(arg)
|
51
|
+
arg.kind_of?(Hash) ? @global_options.update(arg.rekey(&:to_sym)) : @global_options[arg]
|
52
|
+
end
|
53
|
+
|
54
|
+
# When called with a hash: Takes a hash of options and values, and sets them
|
55
|
+
# at the season scope level.
|
56
|
+
#
|
57
|
+
# When called with an option identifier: Returns the value for that option
|
58
|
+
# exclusively at the season scope level.
|
59
|
+
#
|
60
|
+
# Since Autumn can only be run in one season per process, there is no need
|
61
|
+
# to store the options of specific seasons, only the current season.
|
62
|
+
|
63
|
+
def season(arg)
|
64
|
+
arg.kind_of?(Hash) ? @season_options.update(arg.rekey(&:to_sym)) : @season_options[arg]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns true if the given identifier is a known stem identifier.
|
68
|
+
|
69
|
+
def stem?(stem)
|
70
|
+
return !@stem_options[stem].nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
# When called with a hash: Takes a hash of options and values, and sets them
|
74
|
+
# at the stem scope level.
|
75
|
+
#
|
76
|
+
# When called with an option identifier: Returns the value for that option
|
77
|
+
# exclusively at the stem scope level.
|
78
|
+
#
|
79
|
+
# The identifier for the stem must be specified.
|
80
|
+
|
81
|
+
def stem(stem, arg)
|
82
|
+
arg.kind_of?(Hash) ? @stem_options[stem].update(arg.rekey(&:to_sym)) : @stem_options[stem][arg]
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns true if the given identifier is a known leaf identifier.
|
86
|
+
|
87
|
+
def leaf?(leaf)
|
88
|
+
return !@leaf_options[leaf].nil?
|
89
|
+
end
|
90
|
+
|
91
|
+
# When called with a hash: Takes a hash of options and values, and sets them
|
92
|
+
# at the leaf scope level.
|
93
|
+
#
|
94
|
+
# When called with an option identifier: Returns the value for that option
|
95
|
+
# exclusively at the leaf scope level.
|
96
|
+
#
|
97
|
+
# The identifier for the leaf must be specified.
|
98
|
+
|
99
|
+
def leaf(leaf, arg)
|
100
|
+
arg.kind_of?(Hash) ? @leaf_options[leaf].update(arg.rekey(&:to_sym)) : @leaf_options[leaf][arg]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Yields each stem identifier and its options.
|
104
|
+
|
105
|
+
def each_stem
|
106
|
+
@stem_options.each { |stem, options| yield stem, options }
|
107
|
+
end
|
108
|
+
|
109
|
+
# Yields each leaf identifier and its options.
|
110
|
+
|
111
|
+
def each_leaf
|
112
|
+
@leaf_options.each { |leaf, options| yield leaf, options }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns an array of all leaf class names in use.
|
116
|
+
|
117
|
+
def all_leaf_classes
|
118
|
+
@leaf_options.values.collect { |opts| opts[:class] }.uniq
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns the composite options for a stem (by identifier), as an
|
122
|
+
# amalgamation of all the scope levels' options.
|
123
|
+
|
124
|
+
def options_for_stem(identifier)
|
125
|
+
OptionsProxy.new(@global_options, @season_options, @stem_options[identifier])
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns the composite options for a leaf (by identifier), as an
|
129
|
+
# amalgamation of all the scope levels' options.
|
130
|
+
|
131
|
+
def options_for_leaf(identifier)
|
132
|
+
OptionsProxy.new(@global_options, @season_options, @leaf_options[identifier])
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class OptionsProxy # :nodoc:
|
137
|
+
MERGED_METHODS = [ :[], :each, :each_key, :each_pair, :each_value, :eql?,
|
138
|
+
:fetch, :has_key?, :include?, :key?, :member?, :has_value?, :value?,
|
139
|
+
:hash, :index, :inspect, :invert, :keys, :length, :size, :merge, :reject,
|
140
|
+
:select, :sort, :to_a, :to_hash, :to_s, :values, :values_at ]
|
141
|
+
|
142
|
+
def initialize(*hashes)
|
143
|
+
raise ArgumentError unless hashes.all? { |hsh| hsh.kind_of? Hash }
|
144
|
+
@hashes = hashes
|
145
|
+
@hashes << Hash.new # the runtime settings, which take precedence over all
|
146
|
+
end
|
147
|
+
|
148
|
+
def method_missing(meth, *args, &block)
|
149
|
+
if MERGED_METHODS.include? meth then
|
150
|
+
merged.send meth, *args, &block
|
151
|
+
else
|
152
|
+
@hashes.last.send meth, *args, &block
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
#TODO optimize this by not regenerating it every time it's accessed
|
159
|
+
def merged
|
160
|
+
merged = Hash.new
|
161
|
+
@hashes.each { |hsh| merged.merge! hsh }
|
162
|
+
return merged
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/lib/autumn/stem.rb
ADDED
@@ -0,0 +1,919 @@
|
|
1
|
+
# Defines the Autumn::Stem class, an IRC client library.
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
require 'socket'
|
5
|
+
require 'openssl'
|
6
|
+
|
7
|
+
module Autumn
|
8
|
+
|
9
|
+
# A connection to an IRC server. The stem acts as the IRC client on which a
|
10
|
+
# Leaf runs. It receives messages from the IRC server and sends messages to
|
11
|
+
# the server. Stem is compatible with many IRC daemons; details of the IRC
|
12
|
+
# protocol are handled by a Daemon instance. (See "Compatibility with
|
13
|
+
# Different Server Types," below).
|
14
|
+
#
|
15
|
+
# Generally stems are initialized by the Foliater, but should you want to
|
16
|
+
# instantiate one yourself, a Stem is instantiated with a server to connect to
|
17
|
+
# and a nickname to acquire (see the initialize method docs). Once you
|
18
|
+
# initialize a Stem, you should call add_listener one or more times to
|
19
|
+
# indicate to the stem what objects are interested in working with it.
|
20
|
+
#
|
21
|
+
# = Listeners and Listener Plug-Ins
|
22
|
+
#
|
23
|
+
# An object that functions as a listener should conform to an implicit
|
24
|
+
# protocol. See the add_listener docs for more infortmation on what methods
|
25
|
+
# you can implement to listen for IRC events. Duck typing is used -- you need
|
26
|
+
# not implement every method of the protocol, only those you are concerned
|
27
|
+
# with.
|
28
|
+
#
|
29
|
+
# Listeners can also act as plugins: Such listeners add functionality to
|
30
|
+
# other listeners (for example, a CTCP listener that adds CTCP support to
|
31
|
+
# other listeners, such as a Leaf instance). For more information, see the
|
32
|
+
# add_listener docs.
|
33
|
+
#
|
34
|
+
# = Starting the IRC Session
|
35
|
+
#
|
36
|
+
# Once you have finished configuring your stem and you are ready to begin the
|
37
|
+
# IRC session, call the start method. This method blocks until the the socket
|
38
|
+
# has been closed, so it should be run in a thread. Once the connection has
|
39
|
+
# been made, you are free to send and receive IRC commands until you close the
|
40
|
+
# connection, which is done with the quit method.
|
41
|
+
#
|
42
|
+
# = Receiving and Sending IRC Commands
|
43
|
+
#
|
44
|
+
# Receiving events is explained in the add_listener docs. To send an IRC
|
45
|
+
# command, simply call a method named after the command name. For instance, if
|
46
|
+
# you wish to PRIVMSG another nick, call the +privmsg+ method. If you wish to
|
47
|
+
# JOIN a channel, call the +join+ method. The parameters should be specified
|
48
|
+
# in the same order as the IRC command expects.
|
49
|
+
#
|
50
|
+
# For more information on what IRC commands are "method-ized", see the
|
51
|
+
# +IRC_COMMANDS+ constant. For more information on the proper way to use these
|
52
|
+
# commands (and thus, the methods that call them), consult the Daemon class.
|
53
|
+
#
|
54
|
+
# = Compatibility with Different Server Types
|
55
|
+
#
|
56
|
+
# Many different IRC server daemons exist, and each one has a slightly
|
57
|
+
# different IRC implementation. To manage this, there is an option called
|
58
|
+
# +server_type+, which is set automatically by the stem if it can determine
|
59
|
+
# the IRC software that the server is running. Server types are instances of
|
60
|
+
# the Daemon class, and are associated with a name. A stem's server type
|
61
|
+
# affects things like response codes, user modes, and channel modes, as these
|
62
|
+
# vary from server to server.
|
63
|
+
#
|
64
|
+
# If the stem is unsure what IRC daemon your server is running, it will use
|
65
|
+
# the default Daemon instance. This default server type will be compatible
|
66
|
+
# with nearly every server out there. You may not be able to leverage some of
|
67
|
+
# the more esoteric IRC features of your particular server, but for the most
|
68
|
+
# common uses of IRC (sending and receiving messages, for example), it will
|
69
|
+
# suffice.
|
70
|
+
#
|
71
|
+
# If you'd like to manually specify a server type, you can pass its name for
|
72
|
+
# the +server_type+ initialization option. Consult the resources/daemons
|
73
|
+
# directory for valid Daemon names and hints on how to make your own Daemon
|
74
|
+
# specification, should you desire.
|
75
|
+
#
|
76
|
+
# = Channel Names
|
77
|
+
#
|
78
|
+
# The convention for Autumn channel names is: When you specify a channel to
|
79
|
+
# an Autumn stem, you can (but don't have to) prefix it with the '#'
|
80
|
+
# character, if it's a normal IRC channel. When an Autumn stem gives a channel
|
81
|
+
# name to you, it will always start with the '#' character (assuming it's a
|
82
|
+
# normal IRC channel, of course). If your channel is prefixed with a different
|
83
|
+
# character (say, '&'), you will need to include that prefix every time you
|
84
|
+
# pass a channel name to a stem method.
|
85
|
+
#
|
86
|
+
# So, if you would like your stem to send a message to the "##kittens"
|
87
|
+
# channel, you can omit the '#' character; but if it's a server-local channel
|
88
|
+
# called "&kittens", you will have to provide the '&' character. Likewise, if
|
89
|
+
# you are overriding a hook method, you can be guaranteed that the channel
|
90
|
+
# given to you will always be called "##kittens", and not "kittens".
|
91
|
+
#
|
92
|
+
# = Synchronous Methods
|
93
|
+
#
|
94
|
+
# Because new messages are received and processed in separate threads, methods
|
95
|
+
# can sometimes receive messages out of order (for instance, if a first
|
96
|
+
# message takes a long time to process and a second message takes a short time
|
97
|
+
# to process). In the event that you require a guarantee that your method will
|
98
|
+
# receive messages in order, and that it will only be invoked in a single
|
99
|
+
# thread, annotate your method with the +stem_sync+ property.
|
100
|
+
#
|
101
|
+
# For instance, you might want to ensure that you are finished processing 353
|
102
|
+
# messages (replies to NAMES commands) before you tackle 366 messages (end of
|
103
|
+
# NAMES list). To ensure these methods are invoked in the correct order:
|
104
|
+
#
|
105
|
+
# class MyListener
|
106
|
+
# def irc_rpl_namreply_response(stem, sender, recipient, arguments, msg)
|
107
|
+
# [...]
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# def irc_rpl_endofnames_response(stem, sender, recipient, arguments, msg)
|
111
|
+
# [...]
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# ann :irc_rpl_namreply_response, :stem_sync => true
|
115
|
+
# ann :irc_rpl_endofnames_response, :stem_sync => true
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# All such methods will be run in a single thread, and will receive server
|
119
|
+
# messages in order. Because of this, it is important that synchronized
|
120
|
+
# methods do not spend a lot of time processing a single message, as it forces
|
121
|
+
# all other synchronous methods to wait their turn.
|
122
|
+
#
|
123
|
+
# This annotation is only relevant to "invoked" methods, those methods in
|
124
|
+
# listeners that are invoked by the stem's broadcast method. Methods that are
|
125
|
+
# marked with this annotation will also run faster, because they don't have
|
126
|
+
# the overhead of setting up a new thread.
|
127
|
+
#
|
128
|
+
# Many of Stem's own internal methods are synchronized, to ensure internal
|
129
|
+
# data such as the channels list and channel members list stays consistent.
|
130
|
+
# Because of this, any method marked as synchronized can be guaranteed that
|
131
|
+
# the stem's channel data is consistent and "in sync" for the moment of time
|
132
|
+
# that the message was received.
|
133
|
+
#
|
134
|
+
# = Throttling
|
135
|
+
#
|
136
|
+
# If you send a message with the +privmsg+ command, it will not be throttled.
|
137
|
+
# (Most IRC servers have some form of flood control that throttles rapid
|
138
|
+
# privmsg commands, however.)
|
139
|
+
#
|
140
|
+
# If your IRC server does not have flood control, or you want to use
|
141
|
+
# client-side flood control, you can enable the +throttling+ option. The stem
|
142
|
+
# will throttle large numbers of simultaneous messages, sending them with
|
143
|
+
# short pauses in between.
|
144
|
+
#
|
145
|
+
# The +privmsg+ command will still _not_ be throttled (since it is a facade
|
146
|
+
# for the pure IRC command), but the StemFacade#message command will gain the
|
147
|
+
# ability to throttle its messages.
|
148
|
+
#
|
149
|
+
# By default, the stem will begin throttling when there are five or more
|
150
|
+
# messages queued to be sent. It will continue throttling until the queue is
|
151
|
+
# emptied. When throttling, messages will be sent with a delay of one second
|
152
|
+
# between them. These options can be customized (see the initialize method
|
153
|
+
# options).
|
154
|
+
|
155
|
+
class Stem
|
156
|
+
include StemFacade
|
157
|
+
include Anise::Annotation
|
158
|
+
|
159
|
+
# Describes all possible channel names. Omits the channel prefix, as that
|
160
|
+
# can vary from server to server. (See channel?)
|
161
|
+
CHANNEL_REGEX = "[^\\s\\x7,:]+"
|
162
|
+
# The default regular expression for IRC nicknames.
|
163
|
+
NICK_REGEX = "[a-zA-Z][a-zA-Z0-9\\-_\\[\\]\\{\\}\\\\|`\\^]+"
|
164
|
+
|
165
|
+
# A parameter in an IRC command.
|
166
|
+
|
167
|
+
class Parameter # :nodoc:
|
168
|
+
attr :name
|
169
|
+
attr :required
|
170
|
+
attr :colonize
|
171
|
+
attr :list
|
172
|
+
|
173
|
+
def initialize(newname, options={})
|
174
|
+
@name = newname
|
175
|
+
@required = options[:required] or true
|
176
|
+
@colonize = options[:colonize] or false
|
177
|
+
@list = options[:list] or false
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.param(name, opts={}) # :nodoc:
|
182
|
+
Parameter.new(name, opts)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Valid IRC command names, mapped to information about their parameters.
|
186
|
+
IRC_COMMANDS = {
|
187
|
+
:pass => [ param('password') ],
|
188
|
+
:nick => [ param('nickname') ],
|
189
|
+
:user => [ param('user'), param('host'), param('server'), param('name') ],
|
190
|
+
:oper => [ param('user'), param('password') ],
|
191
|
+
:quit => [ param('message', :required => false, :colonize => true) ],
|
192
|
+
|
193
|
+
:join => [ param('channels', :list => true), param('keys', :list => true) ],
|
194
|
+
:part => [ param('channels', :list => true) ],
|
195
|
+
:mode => [ param('channel/nick'), param('mode'), param('limit', :required => false), param('user', :required => false), param('mask', :required => false) ],
|
196
|
+
:topic => [ param('channel'), param('topic', :required => false, :colonize => true) ],
|
197
|
+
:names => [ param('channels', :required => false, :list => true) ],
|
198
|
+
:list => [ param('channels', :required => false, :list => true), param('server', :required => false) ],
|
199
|
+
:invite => [ param('nick'), param('channel') ],
|
200
|
+
:kick => [ param('channels', :list => true), param('users', :list => true), param('comment', :required => false, :colonize => true) ],
|
201
|
+
|
202
|
+
:version => [ param('server', :required => false) ],
|
203
|
+
:stats => [ param('query', :required => false), param('server', :required => false) ],
|
204
|
+
:links => [ param('server/mask', :required => false), param('server/mask', :required => false) ],
|
205
|
+
:time => [ param('server', :required => false) ],
|
206
|
+
:connect => [ param('target server'), param('port', :required => false), param('remote server', :required => false) ],
|
207
|
+
:trace => [ param('server', :required => false) ],
|
208
|
+
:admin => [ param('server', :required => false) ],
|
209
|
+
:info => [ param('server', :required => false) ],
|
210
|
+
|
211
|
+
:privmsg => [ param('receivers', :list => true), param('message', :colonize => true) ],
|
212
|
+
:notice => [ param('nick'), param('message', :colonize => true) ],
|
213
|
+
|
214
|
+
:who => [ param('name', :required => false), param('is mask', :required => false) ],
|
215
|
+
:whois => [ param('server/nicks', :list => true), param('nicks', :list => true, :required => false) ],
|
216
|
+
:whowas => [ param('nick'), param('history count', :required => false), param('server', :required => false) ],
|
217
|
+
|
218
|
+
:pong => [ param('code', :required => false, :colonize => true) ]
|
219
|
+
}
|
220
|
+
|
221
|
+
# The address of the server this stem is connected to.
|
222
|
+
attr :server
|
223
|
+
# The remote port that this stem is connecting to.
|
224
|
+
attr :port
|
225
|
+
# The local IP to bind to (virtual hosting).
|
226
|
+
attr :local_ip
|
227
|
+
# The global configuration options plus those for the current season and
|
228
|
+
# this stem.
|
229
|
+
attr :options
|
230
|
+
# The channels that this stem is a member of.
|
231
|
+
attr :channels
|
232
|
+
# The LogFacade instance handling this stem.
|
233
|
+
attr :logger
|
234
|
+
# A Proc that will be called if a nickname is in use. It should take one
|
235
|
+
# argument, the nickname that was unavailable, and return a new nickname to
|
236
|
+
# try. The default Proc appends an underscore to the nickname to produce a
|
237
|
+
# new one, or GHOSTs the nick if possible. This block should return nil if
|
238
|
+
# you do not want another NICK attempt to be made.
|
239
|
+
attr :nick_generator
|
240
|
+
# The Daemon instance that describes the IRC server this client is connected
|
241
|
+
# to.
|
242
|
+
attr :server_type
|
243
|
+
# A hash of channel members by channel name.
|
244
|
+
attr :channel_members
|
245
|
+
|
246
|
+
# Creates an instance that connects to a given IRC server and requests a
|
247
|
+
# given nick. Valid options:
|
248
|
+
#
|
249
|
+
# +port+:: The port that the IRC client should connect on (default 6667).
|
250
|
+
# +local_ip+:: Set this if you want to bind to an IP other than your default
|
251
|
+
# (for virtual hosting).
|
252
|
+
# +logger+:: Specifies a logger instance to use. If none is specified, a new
|
253
|
+
# LogFacade instance is created for the current season.
|
254
|
+
# +ssl+:: If true, indicates that the connection will be made over SSL.
|
255
|
+
# +user+:: The username to transmit to the IRC server (by default it's the
|
256
|
+
# user's nick).
|
257
|
+
# +name+:: The real name to transmit to the IRC server (by default it's the
|
258
|
+
# user's nick).
|
259
|
+
# +server_password+:: The server password (not the nick password), if
|
260
|
+
# necessary.
|
261
|
+
# +password+:: The password to send to NickServ, if your leaf's nick is
|
262
|
+
# registered.
|
263
|
+
# +channel+:: The name of a channel to join.
|
264
|
+
# +channels+:: An array of channel names to join.
|
265
|
+
# +sever_type+:: The name of the server type. (See Daemon). If left blank,
|
266
|
+
# the default Daemon instance is used.
|
267
|
+
# +rejoin+:: If true, the stem will rejoin a channel it is kicked from.
|
268
|
+
# +case_sensitive_channel_names+:: If true, indicates to the IRC client that
|
269
|
+
# this IRC server uses case-sensitive
|
270
|
+
# channel names.
|
271
|
+
# +dont_ghost+:: If true, does not issue a /ghost command if the stem's nick
|
272
|
+
# is taken. (This is only relevant if the nick is registered
|
273
|
+
# and +password+ is specified.) <b>You should use this on IRC
|
274
|
+
# servers that don't use "NickServ" -- otherwise someone may
|
275
|
+
# change their nick to NickServ and discover your
|
276
|
+
# password!</b>
|
277
|
+
# +ghost_without_password+:: Set this to true if your IRC server uses
|
278
|
+
# hostname authentication instead of password
|
279
|
+
# authentication for GHOST commands.
|
280
|
+
# +throttle+:: If enabled, the stem will throttle large amounts of
|
281
|
+
# simultaneous messages.
|
282
|
+
# +throttle_rate+:: Sets the number of seconds that pass between consecutive
|
283
|
+
# PRIVMSG's when the leaf's output is throttled.
|
284
|
+
# +throttle_threshold+:: Sets the number of simultaneous messages that must
|
285
|
+
# be queued before the leaf begins throttling output.
|
286
|
+
#
|
287
|
+
# Any channel name can be a one-item hash, in which case it is taken to be
|
288
|
+
# a channel name-channel password association.
|
289
|
+
|
290
|
+
def initialize(server, newnick, opts)
|
291
|
+
raise ArgumentError, "Please specify at least one channel" unless opts[:channel] or opts[:channels]
|
292
|
+
|
293
|
+
@nick = newnick
|
294
|
+
@server = server
|
295
|
+
@port = opts[:port]
|
296
|
+
@port ||= 6667
|
297
|
+
@local_ip = opts[:local_ip]
|
298
|
+
@options = opts
|
299
|
+
@listeners = Set.new
|
300
|
+
@listeners << self
|
301
|
+
@logger = @options[:logger]
|
302
|
+
@nick_generator = Proc.new do |oldnick|
|
303
|
+
if options[:ghost_without_password] then
|
304
|
+
message "GHOST #{oldnick}", 'NickServ'
|
305
|
+
nil
|
306
|
+
elsif options[:dont_ghost] or options[:password].nil? then
|
307
|
+
"#{oldnick}_"
|
308
|
+
else
|
309
|
+
message "GHOST #{oldnick} #{options[:password]}", 'NickServ'
|
310
|
+
nil
|
311
|
+
end
|
312
|
+
end
|
313
|
+
@server_type = Daemon[opts[:server_type]]
|
314
|
+
@server_type ||= Daemon.default
|
315
|
+
@throttle_rate = opts[:throttle_rate]
|
316
|
+
@throttle_rate ||= 1
|
317
|
+
@throttle_threshold = opts[:throttle_threshold]
|
318
|
+
@throttle_threshold ||= 5
|
319
|
+
|
320
|
+
@nick_regex = (opts[:nick_regex] ? opts[:nick_regex].to_re : NICK_REGEX)
|
321
|
+
|
322
|
+
@channels = Set.new
|
323
|
+
@channels.merge opts[:channels] if opts[:channels]
|
324
|
+
@channels << opts[:channel] if opts[:channel]
|
325
|
+
@channels.map! do |chan|
|
326
|
+
if chan.kind_of? Hash then
|
327
|
+
{ normalized_channel_name(chan.keys.only) => chan.values.only }
|
328
|
+
else
|
329
|
+
normalized_channel_name chan
|
330
|
+
end
|
331
|
+
end
|
332
|
+
# Make a hash of channels to their passwords
|
333
|
+
@channel_passwords = @channels.select { |ch| ch.kind_of? Hash }.mash { |pair| pair }
|
334
|
+
# Strip the passwords from @channels, making it an array of channel names only
|
335
|
+
@channels.map! { |chan| chan.kind_of?(Hash) ? chan.keys.only : chan }
|
336
|
+
@channel_members = Hash.new
|
337
|
+
@updating_channel_members = Hash.new # stores the NAMES list as its being built
|
338
|
+
|
339
|
+
if @throttle = opts[:throttle] then
|
340
|
+
@messages_queue = Queue.new
|
341
|
+
@messages_thread = Thread.new do
|
342
|
+
throttled = false
|
343
|
+
loop do
|
344
|
+
args = @messages_queue.pop
|
345
|
+
throttled = true if not throttled and @messages_queue.length >= @throttle_threshold
|
346
|
+
throttled = false if throttled and @messages_queue.empty?
|
347
|
+
sleep @throttle_rate if throttled
|
348
|
+
privmsg *args
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
@chan_mutex = Mutex.new
|
354
|
+
@join_mutex = Mutex.new
|
355
|
+
@socket_mutex = Mutex.new
|
356
|
+
end
|
357
|
+
|
358
|
+
# Adds an object that will receive notifications of incoming IRC messages.
|
359
|
+
# For each IRC event that the listener is interested in, the listener should
|
360
|
+
# implement a method in the form <tt>irc_[event]_event</tt>, where [event]
|
361
|
+
# is the name of the event, as taken from the +IRC_COMMANDS+ hash. For
|
362
|
+
# example, to register interest in PRIVMSG events, implement the method:
|
363
|
+
#
|
364
|
+
# irc_privmsg_event(stem, sender, arguments)
|
365
|
+
#
|
366
|
+
# If you wish to perform an operation each time any IRC event is received,
|
367
|
+
# you can implement the method:
|
368
|
+
#
|
369
|
+
# irc_event(stem, command, sender, arguments)
|
370
|
+
#
|
371
|
+
# The parameters for both methods are as follows:
|
372
|
+
#
|
373
|
+
# +stem+:: This Stem instance.
|
374
|
+
# +sender+:: A sender hash (see the Leaf docs).
|
375
|
+
# +arguments+:: A hash whose keys depend on the IRC command. Keys can be,
|
376
|
+
# for example, <tt>:recipient</tt>, <tt>:channel</tt>,
|
377
|
+
# <tt>:mode</tt>, or <tt>:message</tt>. Any can be nil.
|
378
|
+
#
|
379
|
+
# The +irc_event+ method also receives the command name as a symbol.
|
380
|
+
#
|
381
|
+
# In addition to events, the Stem will also pass IRC server responses along
|
382
|
+
# to its listeners. Known responses (those specified by the Daemon) are
|
383
|
+
# translated to programmer-friendly symbols using the Daemon.event hash. The
|
384
|
+
# rest are left in numerical form.
|
385
|
+
#
|
386
|
+
# If you wish to register interest in a response code, implement a method of
|
387
|
+
# the form <tt>irc_[response]_response</tt>, where [response] is the symbol
|
388
|
+
# or numerical form of the response. For instance, to register interest in
|
389
|
+
# channel-full errors, you'd implement:
|
390
|
+
#
|
391
|
+
# irc_err_channelisfull_response(stem, sender, recipient, arguments, msg)
|
392
|
+
#
|
393
|
+
# You can also register an interest in all server responses by implementing:
|
394
|
+
#
|
395
|
+
# irc_response(stem, response, sender, recipient, arguments, msg)
|
396
|
+
#
|
397
|
+
# This method is invoked when the server sends a response message. The
|
398
|
+
# parameters for both methods are:
|
399
|
+
#
|
400
|
+
# +sender+:: The server's address.
|
401
|
+
# +recipient+:: The nick of the recipient (sometimes "*" if no nick has been
|
402
|
+
# assigned yet).
|
403
|
+
# +arguments+:: Array of response arguments, as strings.
|
404
|
+
# +message+:: An additional message attached to the end of the response.
|
405
|
+
#
|
406
|
+
# The +irc_server_response+ method additionally receives the response code
|
407
|
+
# as a symbol or numerical parameter.
|
408
|
+
#
|
409
|
+
# Please note that there are hundreds of possible responses, and IRC servers
|
410
|
+
# differ in what information they send along with each response code. I
|
411
|
+
# recommend inspecting the output of the specific IRC server you are working
|
412
|
+
# with, so you know what arguments to expect.
|
413
|
+
#
|
414
|
+
# If your listener is interested in IRC server notices, implement the
|
415
|
+
# method:
|
416
|
+
#
|
417
|
+
# irc_server_notice(stem, server, sender, msg)
|
418
|
+
#
|
419
|
+
# This method will be invoked for notices from the IRC server. Its
|
420
|
+
# parameters are:
|
421
|
+
#
|
422
|
+
# +server+:: The server's address.
|
423
|
+
# +sender+:: The message originator (e.g., "Auth" for authentication-related
|
424
|
+
# messages).
|
425
|
+
# +msg+:: The notice.
|
426
|
+
#
|
427
|
+
# If your listener is interested in IRC server errors, implement the method:
|
428
|
+
#
|
429
|
+
# irc_server_error(stem, msg)
|
430
|
+
#
|
431
|
+
# This method will be invoked whenever an IRC server reports an error, and
|
432
|
+
# is passed the error message. Server errors differ from normal server
|
433
|
+
# responses, which themselves can sometimes indicate errors.
|
434
|
+
#
|
435
|
+
# Some listeners can act as listener plugins; see the broadcast method for
|
436
|
+
# more information.
|
437
|
+
#
|
438
|
+
# If you'd like your listener to perform actions after it's been added to a
|
439
|
+
# Stem, implement a method called +added+. This method will be called when
|
440
|
+
# the listener is added to a stem, and will be passed the Stem instance it
|
441
|
+
# was added to. You can use this method, for instance, to add additional
|
442
|
+
# methods to the stem.
|
443
|
+
#
|
444
|
+
# Your listener can implement the +stem_ready+ method, which will be called
|
445
|
+
# once the stem has started up, connected to the server, and joined all its
|
446
|
+
# channels. This method is passed the stem instance.
|
447
|
+
|
448
|
+
def add_listener(obj)
|
449
|
+
@listeners << obj
|
450
|
+
obj.class.extend Anise::Annotation # give it the ability to sync
|
451
|
+
obj.respond :added, self
|
452
|
+
end
|
453
|
+
|
454
|
+
# Sends the method with the name +meth+ (a symbol) to all listeners that
|
455
|
+
# respond to that method. You can optionally specify one or more arguments.
|
456
|
+
# This method is meant for use by <b>listener plugins</b>: listeners that
|
457
|
+
# add features to other listeners by allowing them to implement optional
|
458
|
+
# methods.
|
459
|
+
#
|
460
|
+
# For example, you might have a listener plugin that adds CTCP support to
|
461
|
+
# stems. Such a method would parse incoming messages for CTCP commands, and
|
462
|
+
# then use the broadcast method to call methods named after those commands.
|
463
|
+
# Other listeners who want to use CTCP support can implement the methods
|
464
|
+
# that your listener plugin broadcasts.
|
465
|
+
#
|
466
|
+
# <b>Note:</b> Each method call will be executed in its own thread, and all
|
467
|
+
# exceptions will be caught and reported. This method will only invoke
|
468
|
+
# listener methods that have _not_ been marked as synchronized. (See
|
469
|
+
# "Synchronous Methods" in the class docs.)
|
470
|
+
|
471
|
+
def broadcast(meth, *args)
|
472
|
+
@listeners.select { |listener| not listener.class.ann(meth, :stem_sync) }.each do |listener|
|
473
|
+
Thread.new do
|
474
|
+
begin
|
475
|
+
listener.respond meth, *args
|
476
|
+
rescue Exception
|
477
|
+
options[:logger].error $!
|
478
|
+
message("Listener #{listener.class.to_s} raised an exception responding to #{meth}: " + $!.to_s) rescue nil # Try to report the error if possible
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
# Same as the broadcast method, but only invokes listener methods that
|
485
|
+
# _have_ been marked as synchronized.
|
486
|
+
|
487
|
+
def broadcast_sync(meth, *args)
|
488
|
+
@listeners.select { |listener| listener.class.ann(meth, :stem_sync) }.each { |listener| listener.respond meth, *args }
|
489
|
+
end
|
490
|
+
|
491
|
+
# Opens a connection to the IRC server and begins listening on it. This
|
492
|
+
# method runs until the socket is closed, and should be run in a thread. It
|
493
|
+
# will terminate when the connection is closed. No messages should be
|
494
|
+
# transmitted, nor will messages be received, until this method is called.
|
495
|
+
#
|
496
|
+
# In the event that the nick is unavailable, the +nick_generator+ proc will
|
497
|
+
# be called.
|
498
|
+
|
499
|
+
def start
|
500
|
+
# Synchronous (mutual exclusion) message processing is handled by a
|
501
|
+
# producer-consumer approach. The socket pushes messages onto this queue,
|
502
|
+
# which are processed by a consumer thread one at a time.
|
503
|
+
@messages = Queue.new
|
504
|
+
@message_consumer = Thread.new do
|
505
|
+
loop do
|
506
|
+
meths = @messages.pop
|
507
|
+
begin
|
508
|
+
meths.each { |meth, args| broadcast_sync meth, *args }
|
509
|
+
rescue
|
510
|
+
options[:logger].error $!
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
@socket = connect
|
516
|
+
username = @options[:user]
|
517
|
+
username ||= @nick
|
518
|
+
realname = @options[:name]
|
519
|
+
realname ||= @nick
|
520
|
+
|
521
|
+
pass @options[:server_password] if @options[:server_password]
|
522
|
+
user username, @nick, @nick, realname
|
523
|
+
nick @nick
|
524
|
+
|
525
|
+
while line = @socket.gets
|
526
|
+
meths = receive line # parse the line and get a list of methods to call
|
527
|
+
@messages.push meths # push the methods on the queue; the consumer thread will execute all the synchronous methods
|
528
|
+
# then execute all the other methods in their own thread
|
529
|
+
meths.each { |meth, args| broadcast meth, *args }
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
# Returns true if this stem has started up completely, connected to the IRC
|
534
|
+
# server, and joined all its channels. A period of 10 seconds is allowed to
|
535
|
+
# join all channels, after which the stem will report ready even if some
|
536
|
+
# channels could not be joined.
|
537
|
+
|
538
|
+
def ready?
|
539
|
+
@ready == true
|
540
|
+
end
|
541
|
+
|
542
|
+
# Normalizes a channel name by placing a "#" character before the name if no
|
543
|
+
# channel prefix is otherwise present. Also converts the name to lowercase
|
544
|
+
# if the +case_sensitive_channel_names+ option is false. You can suppress
|
545
|
+
# the automatic prefixing by passing false for +add_prefix+.
|
546
|
+
|
547
|
+
def normalized_channel_name(channel, add_prefix=true)
|
548
|
+
norm_chan = channel.dup
|
549
|
+
norm_chan.downcase! unless options[:case_sensitive_channel_names]
|
550
|
+
norm_chan = "##{norm_chan}" unless server_type.channel_prefix?(channel[0,1]) or not add_prefix
|
551
|
+
return norm_chan
|
552
|
+
end
|
553
|
+
|
554
|
+
def method_missing(meth, *args) # :nodoc:
|
555
|
+
if IRC_COMMANDS.include? meth then
|
556
|
+
param_info = IRC_COMMANDS[meth]
|
557
|
+
params = Array.new
|
558
|
+
param_info.each do |param|
|
559
|
+
raise ArgumentError, "#{param.name} is required" if args.empty? and param.required
|
560
|
+
arg = args.shift
|
561
|
+
next if arg.nil? or arg.empty?
|
562
|
+
arg = (param.list and arg.kind_of? Array) ? arg.map(&:to_s).join(',') : arg.to_s
|
563
|
+
arg = ":#{arg}" if param.colonize
|
564
|
+
params << arg
|
565
|
+
end
|
566
|
+
raise ArgumentError, "Too many parameters" unless args.empty?
|
567
|
+
transmit "#{meth.to_s.upcase} #{params.join(' ')}"
|
568
|
+
else
|
569
|
+
super
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# Given a full channel name, returns the channel type as a symbol. Values
|
574
|
+
# can be found in the Daemons instance. Returns <tt>:unknown</tt> for
|
575
|
+
# unknown channel types.
|
576
|
+
|
577
|
+
def channel_type(channel)
|
578
|
+
type = server_type.channel_prefix[channel[0,1]]
|
579
|
+
type ? type : :unknown
|
580
|
+
end
|
581
|
+
|
582
|
+
# Returns true if the string appears to be a channel name.
|
583
|
+
|
584
|
+
def channel?(str)
|
585
|
+
prefixes = Regexp.escape(server_type.channel_prefix.keys.join)
|
586
|
+
str.match("[#{prefixes}]#{CHANNEL_REGEX}") != nil
|
587
|
+
end
|
588
|
+
|
589
|
+
# Returns true if the string appears to be a nickname.
|
590
|
+
|
591
|
+
def nick?(str)
|
592
|
+
str.match(@nick_regex) != nil
|
593
|
+
end
|
594
|
+
|
595
|
+
# Returns the nick this stem is using.
|
596
|
+
|
597
|
+
def nickname
|
598
|
+
@nick
|
599
|
+
end
|
600
|
+
|
601
|
+
def inspect # :nodoc:
|
602
|
+
"#<#{self.class.to_s} #{server}:#{port}>"
|
603
|
+
end
|
604
|
+
|
605
|
+
protected
|
606
|
+
|
607
|
+
def irc_ping_event(stem, sender, arguments) # :nodoc:
|
608
|
+
arguments[:message].nil? ? pong : pong(arguments[:message])
|
609
|
+
end
|
610
|
+
ann :irc_ping_event, :stem_sync => true # To avoid overhead of a whole new thread just for a pong
|
611
|
+
|
612
|
+
def irc_rpl_yourhost_response(stem, sender, recipient, arguments, msg) # :nodoc:
|
613
|
+
return if options[:server_type]
|
614
|
+
type = nil
|
615
|
+
Daemon.each_name do |name|
|
616
|
+
next unless msg.include? name
|
617
|
+
if type then
|
618
|
+
logger.info "Ambiguous server type; could be #{type} or #{name}"
|
619
|
+
return
|
620
|
+
else
|
621
|
+
type = name
|
622
|
+
end
|
623
|
+
end
|
624
|
+
return unless type
|
625
|
+
@server_type = Daemon[type]
|
626
|
+
logger.info "Auto-detected #{type} server daemon type"
|
627
|
+
end
|
628
|
+
ann :irc_rpl_yourhost_response, :stem_sync => true # So methods that synchronize can be guaranteed the host is known ASAP
|
629
|
+
|
630
|
+
def irc_err_nicknameinuse_response(stem, sender, recipient, arguments, msg) # :nodoc:
|
631
|
+
return unless nick_generator
|
632
|
+
newnick = nick_generator.call(arguments[0])
|
633
|
+
nick newnick if newnick
|
634
|
+
end
|
635
|
+
|
636
|
+
def irc_rpl_endofmotd_response(stem, sender, recipient, arguments, msg) # :nodoc:
|
637
|
+
post_startup
|
638
|
+
end
|
639
|
+
|
640
|
+
def irc_err_nomotd_response(stem, sender, recipient, arguments, msg) # :nodoc:
|
641
|
+
post_startup
|
642
|
+
end
|
643
|
+
|
644
|
+
def irc_rpl_namreply_response(stem, sender, recipient, arguments, msg) # :nodoc:
|
645
|
+
update_names_list normalized_channel_name(arguments[1]), msg.words unless arguments[1] == "*" # "*" refers to users not on a channel
|
646
|
+
end
|
647
|
+
ann :irc_rpl_namreply_response, :stem_sync => true # So endofnames isn't processed before namreply
|
648
|
+
|
649
|
+
def irc_rpl_endofnames_response(stem, sender, recipient, arguments, msg) # :nodoc:
|
650
|
+
finish_names_list_update normalized_channel_name(arguments[0])
|
651
|
+
end
|
652
|
+
ann :irc_rpl_endofnames_response, :stem_sync => true # so endofnames isn't processed before namreply
|
653
|
+
|
654
|
+
def irc_kick_event(stem, sender, arguments) # :nodoc:
|
655
|
+
if arguments[:recipient] == @nick then
|
656
|
+
old_pass = @channel_passwords[arguments[:channel]]
|
657
|
+
@chan_mutex.synchronize do
|
658
|
+
drop_channel arguments[:channel]
|
659
|
+
#TODO what should we do if we are in the middle of receiving NAMES replies?
|
660
|
+
end
|
661
|
+
join_channel arguments[:channel], old_pass if options[:rejoin]
|
662
|
+
else
|
663
|
+
@chan_mutex.synchronize do
|
664
|
+
@channel_members[arguments[:channel]].delete arguments[:recipient]
|
665
|
+
#TODO what should we do if we are in the middle of receiving NAMES replies?
|
666
|
+
end
|
667
|
+
end
|
668
|
+
end
|
669
|
+
ann :irc_kick_event, :stem_sync => true # So methods that synchronize can be guaranteed the channel variables are up to date
|
670
|
+
|
671
|
+
def irc_mode_event(stem, sender, arguments) # :nodoc:
|
672
|
+
names arguments[:channel] if arguments[:parameter] and server_type.privilege_mode?(arguments[:mode])
|
673
|
+
end
|
674
|
+
ann :irc_mode_event, :stem_sync => true # To avoid overhead of a whole new thread for a names reply
|
675
|
+
|
676
|
+
def irc_join_event(stem, sender, arguments) # :nodoc:
|
677
|
+
if sender[:nick] == @nick then
|
678
|
+
should_broadcast = false
|
679
|
+
@chan_mutex.synchronize do
|
680
|
+
@channels << arguments[:channel]
|
681
|
+
@channel_members[arguments[:channel]] ||= Hash.new
|
682
|
+
@channel_members[arguments[:channel]][sender[:nick]] = :unvoiced
|
683
|
+
#TODO what should we do if we are in the middle of receiving NAMES replies?
|
684
|
+
#TODO can we assume that all new channel members are unvoiced?
|
685
|
+
end
|
686
|
+
@join_mutex.synchronize do
|
687
|
+
if @channels_to_join then
|
688
|
+
@channels_to_join.delete arguments[:channel]
|
689
|
+
if @channels_to_join.empty? then
|
690
|
+
should_broadcast = true unless @ready
|
691
|
+
@ready = true
|
692
|
+
@channels_to_join = nil
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|
696
|
+
# The ready_thread is also looking to set ready to true and broadcast,
|
697
|
+
# so to prevent us both from doing it, we enter a critical section and
|
698
|
+
# record whether the broadcast has been made already. We set @ready to
|
699
|
+
# true and record if it was already set to true. If it wasn't already
|
700
|
+
# set to true, we know the broadcast hasn't gone out, so we send it out.
|
701
|
+
broadcast :stem_ready, self if should_broadcast
|
702
|
+
end
|
703
|
+
end
|
704
|
+
ann :irc_join_event, :stem_sync => true # So methods that synchronize can be guaranteed the channel variables are up to date
|
705
|
+
|
706
|
+
def irc_part_event(stem, sender, arguments) # :nodoc:
|
707
|
+
@chan_mutex.synchronize do
|
708
|
+
if sender[:nick] == @nick then
|
709
|
+
drop_channel arguments[:channel]
|
710
|
+
else
|
711
|
+
@channel_members[arguments[:channel]].delete sender[:nick]
|
712
|
+
end
|
713
|
+
#TODO what should we do if we are in the middle of receiving NAMES replies?
|
714
|
+
end
|
715
|
+
end
|
716
|
+
ann :irc_part_event, :stem_sync => true # So methods that synchronize can be guaranteed the channel variables are up to date
|
717
|
+
|
718
|
+
def irc_nick_event(stem, sender, arguments) # :nodoc:
|
719
|
+
@nick = arguments[:nick] if sender[:nick] == @nick
|
720
|
+
@chan_mutex.synchronize do
|
721
|
+
@channel_members.each { |chan, members| members[arguments[:nick]] = members.delete(sender[:nick]) }
|
722
|
+
#TODO what should we do if we are in the middle of receiving NAMES replies?
|
723
|
+
end
|
724
|
+
end
|
725
|
+
ann :irc_nick_event, :stem_sync => true # So methods that synchronize can be guaranteed the channel variables are up to date
|
726
|
+
|
727
|
+
def irc_quit_event(stem, sender, arguments) # :nodoc:
|
728
|
+
@chan_mutex.synchronize do
|
729
|
+
@channel_members.each { |chan, members| members.delete sender[:nick] }
|
730
|
+
#TODO what should we do if we are in the middle of receiving NAMES replies?
|
731
|
+
end
|
732
|
+
end
|
733
|
+
ann :irc_quit_event, :stem_sync => true # So methods that synchronize can be guaranteed the channel variables are up to date
|
734
|
+
|
735
|
+
private
|
736
|
+
|
737
|
+
def connect
|
738
|
+
logger.debug "Connecting to #{@server}:#{@port}..."
|
739
|
+
socket = TCPSocket.new @server, @port, @local_ip
|
740
|
+
return socket unless options[:ssl]
|
741
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
742
|
+
unless ssl_context.verify_mode
|
743
|
+
logger.warn "SSL - Peer certificate won't be verified this session."
|
744
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
745
|
+
end
|
746
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
|
747
|
+
ssl_socket.sync_close = true
|
748
|
+
ssl_socket.connect
|
749
|
+
return ssl_socket
|
750
|
+
end
|
751
|
+
|
752
|
+
def transmit(comm)
|
753
|
+
@socket_mutex.synchronize do
|
754
|
+
raise "IRC connection not opened yet" unless @socket
|
755
|
+
logger.debug ">> " + comm
|
756
|
+
@socket.puts comm
|
757
|
+
end
|
758
|
+
end
|
759
|
+
|
760
|
+
# Parses a message and returns a hash of methods to their arguments
|
761
|
+
def receive(comm)
|
762
|
+
meths = Hash.new
|
763
|
+
logger.debug "<< " + comm
|
764
|
+
|
765
|
+
if comm =~ /^:(.+?)\s+NOTICE\s+(\S+)\s+:(.+?)[\r\n]$/
|
766
|
+
server, sender, msg = $1, $2, $3
|
767
|
+
meths[:irc_server_notice] = [ self, server, sender, msg ]
|
768
|
+
return meths
|
769
|
+
elsif comm =~ /^ERROR :(.+?)[\r\n]*$/ then
|
770
|
+
msg = $1
|
771
|
+
meths[:irc_server_error] = [ self, msg ]
|
772
|
+
return meths
|
773
|
+
elsif comm =~ /^:(#{@nick_regex})!(\S+?)@(\S+?)\s+([A-Z]+)\s+(.*?)[\r\n]*$/ then
|
774
|
+
sender = { :nick => $1, :user => $2, :host => $3 }
|
775
|
+
command, arg_str = $4, $5
|
776
|
+
elsif comm =~ /^:(#{@nick_regex})\s+([A-Z]+)\s+(.*?)[\r\n]*$/ then
|
777
|
+
sender = { :nick => $1 }
|
778
|
+
command, arg_str = $2, $3
|
779
|
+
elsif comm =~ /^:([^\s:]+?)\s+([A-Z]+)\s+(.*?)[\r\n]*$/ then
|
780
|
+
server, command, arg_str = $1, $2, $3
|
781
|
+
arg_array, msg = split_out_message(arg_str)
|
782
|
+
elsif comm =~ /^(\w+)\s+:(.+?)[\r\n]*$/ then
|
783
|
+
command, msg = $1, $2
|
784
|
+
elsif comm =~ /^:([^\s:]+?)\s+(\d+)\s+(.*?)[\r\n]*$/ then
|
785
|
+
server, code, arg_str = $1, $2, $3
|
786
|
+
arg_array, msg = split_out_message(arg_str)
|
787
|
+
|
788
|
+
numeric_method = "irc_#{code}_response".to_sym
|
789
|
+
readable_method = "irc_#{server_type.event[code.to_i]}_response".to_sym if not code.to_i.zero? and server_type.event?(code.to_i)
|
790
|
+
name = arg_array.shift
|
791
|
+
meths[numeric_method] = [ self, server, name, arg_array, msg ]
|
792
|
+
meths[readable_method] = [ self, server, name, arg_array, msg ] if readable_method
|
793
|
+
meths[:irc_response] = [ self, code, server, name, arg_array, msg ]
|
794
|
+
return meths
|
795
|
+
else
|
796
|
+
logger.error "Couldn't parse IRC message: #{comm.inspect}"
|
797
|
+
return meths
|
798
|
+
end
|
799
|
+
|
800
|
+
if arg_str then
|
801
|
+
arg_array, msg = split_out_message(arg_str)
|
802
|
+
else
|
803
|
+
arg_array = Array.new
|
804
|
+
end
|
805
|
+
command = command.downcase.to_sym
|
806
|
+
|
807
|
+
case command
|
808
|
+
when :nick then
|
809
|
+
arguments = { :nick => arg_array.at(0) }
|
810
|
+
# Some IRC servers put the nick in the message field
|
811
|
+
unless arguments[:nick]
|
812
|
+
arguments[:nick] = msg
|
813
|
+
msg = nil
|
814
|
+
end
|
815
|
+
when :quit then
|
816
|
+
arguments = { }
|
817
|
+
when :join then
|
818
|
+
arguments = { :channel => msg }
|
819
|
+
msg = nil
|
820
|
+
when :part then
|
821
|
+
arguments = { :channel => arg_array.at(0) }
|
822
|
+
when :mode then
|
823
|
+
arguments = if channel?(arg_array.at(0)) then { :channel => arg_array.at(0) } else { :recipient => arg_array.at(0) } end
|
824
|
+
params = arg_array[2, arg_array.size]
|
825
|
+
if params then
|
826
|
+
params = params.only if params.size == 1
|
827
|
+
params = nil if params.empty? # empty? is a method on String too, so this has to come second to prevent an error
|
828
|
+
end
|
829
|
+
arguments.update(:mode => arg_array.at(1), :parameter => params)
|
830
|
+
# Usermodes stick the mode in the message
|
831
|
+
if arguments[:mode].nil? and msg =~ /^[\+\-]\w+$/ then
|
832
|
+
arguments[:mode] = msg
|
833
|
+
msg = nil
|
834
|
+
end
|
835
|
+
when :topic then
|
836
|
+
arguments = { :channel => arg_array.at(0), :topic => msg }
|
837
|
+
msg = nil
|
838
|
+
when :invite then
|
839
|
+
arguments = { :recipient => arg_array.at(0), :channel => msg }
|
840
|
+
msg = nil
|
841
|
+
when :kick then
|
842
|
+
arguments = { :channel => arg_array.at(0), :recipient => arg_array.at(1) }
|
843
|
+
when :privmsg then
|
844
|
+
arguments = if channel?(arg_array.at(0)) then { :channel => arg_array.at(0) } else { :recipient => arg_array.at(0) } end
|
845
|
+
when :notice then
|
846
|
+
arguments = if channel?(arg_array.at(0)) then { :channel => arg_array.at(0) } else { :recipient => arg_array.at(0) } end
|
847
|
+
when :ping then
|
848
|
+
arguments = { :server => arg_array.at(0) }
|
849
|
+
else
|
850
|
+
logger.warn "Unknown IRC command #{command.to_s}"
|
851
|
+
return
|
852
|
+
end
|
853
|
+
arguments.update :message => msg
|
854
|
+
arguments[:channel] = normalized_channel_name(arguments[:channel]) if arguments[:channel]
|
855
|
+
|
856
|
+
method = "irc_#{command}_event".to_sym
|
857
|
+
meths[method] = [ self, sender, arguments ]
|
858
|
+
meths[:irc_event] = [ self, command, sender, arguments ]
|
859
|
+
return meths
|
860
|
+
end
|
861
|
+
|
862
|
+
def split_out_message(arg_str)
|
863
|
+
if arg_str.match(/^(.*?):(.*)$/) then
|
864
|
+
arg_array = $1.strip.words
|
865
|
+
msg = $2
|
866
|
+
return arg_array, msg
|
867
|
+
else
|
868
|
+
# no colon in message
|
869
|
+
return arg_str.strip.words, nil
|
870
|
+
end
|
871
|
+
end
|
872
|
+
|
873
|
+
def post_startup
|
874
|
+
@ready_thread = Thread.new do
|
875
|
+
sleep 10
|
876
|
+
should_broadcast = false
|
877
|
+
@join_mutex.synchronize do
|
878
|
+
should_broadcast = true unless @ready
|
879
|
+
@ready = true
|
880
|
+
# If irc_join_event set @ready to true, then we know that they have
|
881
|
+
# already broadcasted, because those two events are in a critical
|
882
|
+
# section. Otherwise, we set ready to true, thus ensuring they won't
|
883
|
+
# broadcast, and then broadcast if they haven't already.
|
884
|
+
@channels_to_join = nil
|
885
|
+
end
|
886
|
+
broadcast :stem_ready, self if should_broadcast
|
887
|
+
end
|
888
|
+
@channels_to_join = @channels
|
889
|
+
@channels = Set.new
|
890
|
+
@channels_to_join.each { |chan| join chan, @channel_passwords[chan] }
|
891
|
+
privmsg 'NickServ', "IDENTIFY #{options[:password]}" if options[:password]
|
892
|
+
end
|
893
|
+
|
894
|
+
def update_names_list(channel, names)
|
895
|
+
@chan_mutex.synchronize do
|
896
|
+
@updating_channel_members[channel] ||= Hash.new
|
897
|
+
names.each do |name|
|
898
|
+
@updating_channel_members[channel][server_type.just_nick(name)] = server_type.nick_privilege(name)
|
899
|
+
end
|
900
|
+
end
|
901
|
+
end
|
902
|
+
|
903
|
+
def finish_names_list_update(channel)
|
904
|
+
@chan_mutex.synchronize do
|
905
|
+
@channel_members[channel] = @updating_channel_members.delete(channel) if @updating_channel_members[channel]
|
906
|
+
end
|
907
|
+
end
|
908
|
+
|
909
|
+
def drop_channel(channel)
|
910
|
+
@channels.delete channel
|
911
|
+
@channel_passwords.delete channel
|
912
|
+
@channel_members.delete channel
|
913
|
+
end
|
914
|
+
|
915
|
+
def privmsgt(*args) # a throttled privmsg
|
916
|
+
@messages_queue << args
|
917
|
+
end
|
918
|
+
end
|
919
|
+
end
|