comboy-autumn 3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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