autumn 3.1.8
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +11 -0
- data/CHANGELOG +567 -0
- data/MANIFEST +110 -0
- data/README +1114 -0
- data/README.textile +1153 -0
- data/Rakefile +75 -0
- data/autumn.gemspec +44 -0
- data/bin/autumn +11 -0
- data/lib/autumn.rb +8 -0
- data/lib/autumn/authentication.rb +238 -0
- data/lib/autumn/channel_leaf.rb +107 -0
- data/lib/autumn/coder.rb +166 -0
- data/lib/autumn/console_boot.rb +10 -0
- data/lib/autumn/ctcp.rb +250 -0
- data/lib/autumn/daemon.rb +207 -0
- data/lib/autumn/datamapper_hacks.rb +290 -0
- data/lib/autumn/foliater.rb +231 -0
- data/lib/autumn/formatting.rb +236 -0
- data/lib/autumn/generator.rb +231 -0
- data/lib/autumn/genesis.rb +190 -0
- data/lib/autumn/inheritable_attributes.rb +162 -0
- data/lib/autumn/leaf.rb +738 -0
- data/lib/autumn/log_facade.rb +49 -0
- data/lib/autumn/misc.rb +87 -0
- data/lib/autumn/resources/daemons/Anothernet.yml +3 -0
- data/lib/autumn/resources/daemons/AustHex.yml +29 -0
- data/lib/autumn/resources/daemons/Bahamut.yml +67 -0
- data/lib/autumn/resources/daemons/Dancer.yml +3 -0
- data/lib/autumn/resources/daemons/GameSurge.yml +3 -0
- data/lib/autumn/resources/daemons/IRCnet.yml +3 -0
- data/lib/autumn/resources/daemons/Ithildin.yml +7 -0
- data/lib/autumn/resources/daemons/KineIRCd.yml +56 -0
- data/lib/autumn/resources/daemons/PTlink.yml +6 -0
- data/lib/autumn/resources/daemons/QuakeNet.yml +20 -0
- data/lib/autumn/resources/daemons/RFC1459.yml +158 -0
- data/lib/autumn/resources/daemons/RFC2811.yml +16 -0
- data/lib/autumn/resources/daemons/RFC2812.yml +36 -0
- data/lib/autumn/resources/daemons/RatBox.yml +25 -0
- data/lib/autumn/resources/daemons/Ultimate.yml +24 -0
- data/lib/autumn/resources/daemons/Undernet.yml +6 -0
- data/lib/autumn/resources/daemons/Unreal.yml +110 -0
- data/lib/autumn/resources/daemons/_Other.yml +7 -0
- data/lib/autumn/resources/daemons/aircd.yml +33 -0
- data/lib/autumn/resources/daemons/bdq-ircd.yml +3 -0
- data/lib/autumn/resources/daemons/hybrid.yml +38 -0
- data/lib/autumn/resources/daemons/ircu.yml +67 -0
- data/lib/autumn/resources/daemons/tr-ircd.yml +8 -0
- data/lib/autumn/script.rb +74 -0
- data/lib/autumn/speciator.rb +165 -0
- data/lib/autumn/stem.rb +919 -0
- data/lib/autumn/stem_facade.rb +176 -0
- data/lib/autumn/tool/bin.rb +301 -0
- data/lib/autumn/tool/create.rb +48 -0
- data/lib/autumn/tool/project_creator.rb +110 -0
- data/lib/autumn/version.rb +3 -0
- data/lib/skel/Rakefile +163 -0
- data/lib/skel/config/global.yml +2 -0
- data/lib/skel/config/seasons/testing/database.yml +4 -0
- data/lib/skel/config/seasons/testing/leaves.yml +9 -0
- data/lib/skel/config/seasons/testing/season.yml +2 -0
- data/lib/skel/config/seasons/testing/stems.yml +10 -0
- data/lib/skel/leaves/administrator/README +20 -0
- data/lib/skel/leaves/administrator/controller.rb +67 -0
- data/lib/skel/leaves/administrator/views/autumn.txt.erb +1 -0
- data/lib/skel/leaves/administrator/views/reload.txt.erb +11 -0
- data/lib/skel/leaves/insulter/README +17 -0
- data/lib/skel/leaves/insulter/controller.rb +65 -0
- data/lib/skel/leaves/insulter/views/about.txt.erb +1 -0
- data/lib/skel/leaves/insulter/views/help.txt.erb +1 -0
- data/lib/skel/leaves/insulter/views/insult.txt.erb +1 -0
- data/lib/skel/leaves/scorekeeper/README +34 -0
- data/lib/skel/leaves/scorekeeper/config.yml +2 -0
- data/lib/skel/leaves/scorekeeper/controller.rb +104 -0
- data/lib/skel/leaves/scorekeeper/helpers/general.rb +64 -0
- data/lib/skel/leaves/scorekeeper/models/channel.rb +12 -0
- data/lib/skel/leaves/scorekeeper/models/person.rb +14 -0
- data/lib/skel/leaves/scorekeeper/models/pseudonym.rb +11 -0
- data/lib/skel/leaves/scorekeeper/models/score.rb +14 -0
- data/lib/skel/leaves/scorekeeper/tasks/stats.rake +17 -0
- data/lib/skel/leaves/scorekeeper/views/about.txt.erb +1 -0
- data/lib/skel/leaves/scorekeeper/views/change.txt.erb +5 -0
- data/lib/skel/leaves/scorekeeper/views/history.txt.erb +11 -0
- data/lib/skel/leaves/scorekeeper/views/points.txt.erb +5 -0
- data/lib/skel/leaves/scorekeeper/views/usage.txt.erb +1 -0
- data/lib/skel/log/README +1 -0
- data/lib/skel/script/console +28 -0
- data/lib/skel/script/destroy +48 -0
- data/lib/skel/script/generate +48 -0
- data/lib/skel/shared/README +1 -0
- data/lib/skel/tmp/README +1 -0
- data/spec/authentication_spec.rb +328 -0
- data/spec/channel_leaf_spec.rb +142 -0
- data/spec/coder_spec.rb +146 -0
- data/spec/ctcp_spec.rb +222 -0
- data/spec/daemon_spec.rb +202 -0
- data/spec/datamapper_hacks_spec.rb +164 -0
- data/tasks/authors.rake +30 -0
- data/tasks/changelog.rake +18 -0
- data/tasks/copyright.rake +21 -0
- data/tasks/doc.rake +7 -0
- data/tasks/gem.rake +23 -0
- data/tasks/gem_installer.rake +76 -0
- data/tasks/install_dependencies.rake +6 -0
- data/tasks/manifest.rake +4 -0
- data/tasks/rcov.rake +23 -0
- data/tasks/release.rake +52 -0
- data/tasks/reversion.rake +8 -0
- data/tasks/setup.rake +24 -0
- data/tasks/spec.rake +7 -0
- data/tasks/yard.rake +4 -0
- metadata +188 -0
@@ -0,0 +1,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
|