comboy-autumn 3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/README.textile +1192 -0
  2. data/autumn.gemspec +25 -0
  3. data/bin/autumn +27 -0
  4. data/lib/autumn.rb +2 -0
  5. data/lib/autumn/authentication.rb +290 -0
  6. data/lib/autumn/channel_leaf.rb +107 -0
  7. data/lib/autumn/coder.rb +166 -0
  8. data/lib/autumn/console_boot.rb +9 -0
  9. data/lib/autumn/ctcp.rb +250 -0
  10. data/lib/autumn/daemon.rb +207 -0
  11. data/lib/autumn/datamapper_hacks.rb +290 -0
  12. data/lib/autumn/foliater.rb +231 -0
  13. data/lib/autumn/formatting.rb +236 -0
  14. data/lib/autumn/generator.rb +231 -0
  15. data/lib/autumn/genesis.rb +191 -0
  16. data/lib/autumn/inheritable_attributes.rb +162 -0
  17. data/lib/autumn/leaf.rb +738 -0
  18. data/lib/autumn/log_facade.rb +49 -0
  19. data/lib/autumn/misc.rb +87 -0
  20. data/lib/autumn/script.rb +74 -0
  21. data/lib/autumn/speciator.rb +165 -0
  22. data/lib/autumn/stem.rb +919 -0
  23. data/lib/autumn/stem_facade.rb +176 -0
  24. data/resources/daemons/Anothernet.yml +3 -0
  25. data/resources/daemons/AustHex.yml +29 -0
  26. data/resources/daemons/Bahamut.yml +67 -0
  27. data/resources/daemons/Dancer.yml +3 -0
  28. data/resources/daemons/GameSurge.yml +3 -0
  29. data/resources/daemons/IRCnet.yml +3 -0
  30. data/resources/daemons/Ithildin.yml +7 -0
  31. data/resources/daemons/KineIRCd.yml +56 -0
  32. data/resources/daemons/PTlink.yml +6 -0
  33. data/resources/daemons/QuakeNet.yml +20 -0
  34. data/resources/daemons/RFC1459.yml +158 -0
  35. data/resources/daemons/RFC2811.yml +16 -0
  36. data/resources/daemons/RFC2812.yml +36 -0
  37. data/resources/daemons/RatBox.yml +25 -0
  38. data/resources/daemons/Ultimate.yml +24 -0
  39. data/resources/daemons/Undernet.yml +6 -0
  40. data/resources/daemons/Unreal.yml +110 -0
  41. data/resources/daemons/_Other.yml +7 -0
  42. data/resources/daemons/aircd.yml +33 -0
  43. data/resources/daemons/bdq-ircd.yml +3 -0
  44. data/resources/daemons/hybrid.yml +38 -0
  45. data/resources/daemons/ircu.yml +67 -0
  46. data/resources/daemons/tr-ircd.yml +8 -0
  47. data/skel/Rakefile +135 -0
  48. data/skel/config/global.yml +2 -0
  49. data/skel/config/seasons/testing/database.yml +7 -0
  50. data/skel/config/seasons/testing/leaves.yml +7 -0
  51. data/skel/config/seasons/testing/season.yml +2 -0
  52. data/skel/config/seasons/testing/stems.yml +9 -0
  53. data/skel/leaves/administrator/README +20 -0
  54. data/skel/leaves/administrator/controller.rb +67 -0
  55. data/skel/leaves/administrator/views/autumn.txt.erb +1 -0
  56. data/skel/leaves/administrator/views/reload.txt.erb +11 -0
  57. data/skel/leaves/insulter/README +17 -0
  58. data/skel/leaves/insulter/controller.rb +65 -0
  59. data/skel/leaves/insulter/views/about.txt.erb +1 -0
  60. data/skel/leaves/insulter/views/help.txt.erb +1 -0
  61. data/skel/leaves/insulter/views/insult.txt.erb +1 -0
  62. data/skel/leaves/scorekeeper/README +34 -0
  63. data/skel/leaves/scorekeeper/config.yml +2 -0
  64. data/skel/leaves/scorekeeper/controller.rb +104 -0
  65. data/skel/leaves/scorekeeper/helpers/general.rb +64 -0
  66. data/skel/leaves/scorekeeper/models/channel.rb +12 -0
  67. data/skel/leaves/scorekeeper/models/person.rb +14 -0
  68. data/skel/leaves/scorekeeper/models/pseudonym.rb +11 -0
  69. data/skel/leaves/scorekeeper/models/score.rb +14 -0
  70. data/skel/leaves/scorekeeper/tasks/stats.rake +17 -0
  71. data/skel/leaves/scorekeeper/views/about.txt.erb +1 -0
  72. data/skel/leaves/scorekeeper/views/change.txt.erb +5 -0
  73. data/skel/leaves/scorekeeper/views/history.txt.erb +11 -0
  74. data/skel/leaves/scorekeeper/views/points.txt.erb +5 -0
  75. data/skel/leaves/scorekeeper/views/usage.txt.erb +1 -0
  76. data/skel/script/console +34 -0
  77. data/skel/script/daemon +29 -0
  78. data/skel/script/destroy +48 -0
  79. data/skel/script/generate +48 -0
  80. data/skel/script/server +15 -0
  81. metadata +170 -0
@@ -0,0 +1,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
@@ -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
@@ -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