rbot-0.9.5+post20100705+gitb3aa806/0000755002342000234200000000000011414373414015272 5ustar duckdc-usersrbot-0.9.5+post20100705+gitb3aa806/REQUIREMENTS0000644002342000234200000000633211411605044017136 0ustar duckdc-usersRuby modules needed for rbot ============================ Core requirements tokyocabinet for "tc" DB adaptor http://1978th.net/tokyocabinet/ you can install Ruby bindings via "gem install tokyocabinet", but this still requires libtokyocabinet to be installed system-wide bdb (berkeley db) for "bdb" DB adaptor or converting from it http://raa.ruby-lang.org/project/bdb/ (which requires libdb4.x or better, formerly from www.sleepycat.com, now at http://www.oracle.com/technology/products/berkeley-db/index.html) Most of the time you don't need to compile anything. If you're running Linux, your distribution should have a libdb-ruby packaged (or similar). For Windows instructions, check at the bottom of this file. net/http 1.2+ net/https (for debian, this will also need libopenssl-ruby) socket uri Useful but fallback provided ruby-gettext 1.8.0+ http://www.yotabanana.com/hiki/ruby-gettext.html?ruby-gettext optional; if installed rbot can use localized messages htmlentities http://htmlentities.rubyforge.org/ optional; if installed rbot will use it to decode HTML entities; if missing, an internal table with the most common HTML entities will be used instead hpricot http://code.whytheluckystiff.net/hpricot/ optional, if installed rbot will used it to find the first paragraph in HTML files; if missing, regular expressions will be used instead Plugin requirements (these are all optional, if you don't have them, the plugins just won't function) bash, digg, slashdot, freshmeat, forecast: REXML rss: rss shortenurls: shorturl time: tzinfo translator: mechanize External programs needed for rbot ================================= Plugin requirements (These are all optional) cal plugin: cal(1) figlet plugin: figlet(6) fortune plugin: fortune(6) host plugin: host(1) spell plugin: ispell(1) Running rbot on win32 ===================== You can install Ruby using the One-Click Ruby installer, available from http://rubyinstaller.rubyforge.org/ You can find a precompiled version of the bdb package for ruby here http://ftp.ruby-lang.org/pub/ruby/binaries/mingw/1.8/ext/bdb-0.5.1-i386-mingw32-1.8.tar.gz When you unpack the archive (e.g. using WinZip or 7-Zip or any other tool of your choice) you'll notice that it contains the following directory structure: usr +---local +---doc +---lib and you have to move the doc and lib folders (and all their contents) in the folder where you installed Ruby (typically C:\Ruby\) Further instructions ==================== For further instructions, check http://ruby-rbot.org/rbot-trac/wiki/InstallGuide rbot-0.9.5+post20100705+gitb3aa806/man/0000755002342000234200000000000011414327523016045 5ustar duckdc-usersrbot-0.9.5+post20100705+gitb3aa806/man/rbot.xml0000644002342000234200000001604611414326121017535 0ustar duckdc-users .
will be generated. You may view the manual page with: nroff -man .
| less'. A typical entry in a Makefile or Makefile.am is: DB2MAN=/usr/share/sgml/docbook/stylesheet/xsl/nwalsh/\ manpages/docbook.xsl XP=xsltproc -''-nonet manpage.1: manpage.dbk $(XP) $(DB2MAN) $< The xsltproc binary is found in the xsltproc package. The XSL files are in docbook-xsl. Please remember that if you create the nroff version in one of the debian/rules file targets (such as build), you will need to include xsltproc and docbook-xsl in your Build-Depends control field. --> Marc"> Dequènes"> Giuseppe"> Bilotta"> 20100701"> 1"> Duck@DuckCorp.org"> giuseppe.bilotta@gmail.com"> RBOT"> Debian"> GNU"> GPL"> ]> &dhapp; &dhfirstname; &dhsurname; &dhemail; &debian; package maintainer &gbgname; &gbfname; &gbemail; &dhapp; maintainer 2004-2009 &dhusername; 2010 &gbusername; &dhdate; &dhucapp; &dhsection; &dhapp; man page &dhpackage; &dhpackageversion; &dhapp; IRC bot written in ruby &dhapp; confdir DESCRIPTION &dhapp; starts the Rbot (ruby IRC bot). OPTIONS This program follow the usual &gnu; command line syntax, with long options starting with two dashes (`-'). A summary of options is included below. Display debug information (very verbose). Show summary of options. Display version information. Sets the minimum log level verbosity. Possible values for the loglevel are 0 (DEBUG), 1 (INFO), 2 (WARN), 3 (ERROR), 4 (FATAL). The default loglevel is 1 (INFO messages). The logfile is located at BOTDIR/BOTNAME.log and doesn't contain IRC logs (which are located at BOTDIR/logs/*), but only rbot diagnostic messages. Background (daemonize) the bot. Write the bot pid to PIDFILE. The default pidfile is BOTDIR/rbot.pid. BOTDIR Path to the directory where are stored the bot's configuration files. The default config directory is ~/.rbot. VERSION This manual page was written by &dhusername; &dhemail; for the &debian; system (but may be used by others). Permission is granted to copy, distribute and/or modify this document under the terms of the &gnu; General Public License, Version 3 or any later version published by the Free Software Foundation. On Debian systems, the complete text of the GNU General Public License can be found in /usr/share/common-licenses/GPL. rbot-0.9.5+post20100705+gitb3aa806/man/rbot-remote.xml0000644002342000234200000001536411414326121021030 0ustar duckdc-users .
will be generated. You may view the manual page with: nroff -man .
| less'. A typical entry in a Makefile or Makefile.am is: DB2MAN=/usr/share/sgml/docbook/stylesheet/xsl/nwalsh/\ manpages/docbook.xsl XP=xsltproc -''-nonet manpage.1: manpage.dbk $(XP) $(DB2MAN) $< The xsltproc binary is found in the xsltproc package. The XSL files are in docbook-xsl. Please remember that if you create the nroff version in one of the debian/rules file targets (such as build), you will need to include xsltproc and docbook-xsl in your Build-Depends control field. --> Marc"> Dequènes"> Giuseppe"> Bilotta"> 20100701"> 1"> Duck@DuckCorp.org"> giuseppe.bilotta@gmail.com"> RBOT-REMOTE"> Debian"> GNU"> GPL"> ]> &dhapp; &dhfirstname; &dhsurname; &dhemail; &debian; package maintainer &gbgname; &gbfname; &gbemail; &dhapp; maintainer 2004-2009 &dhusername; 2010 &gbusername; &dhdate; &dhucapp; &dhsection; &dhapp; man page &dhpackage; &dhpackageversion; &dhapp; IRC bot written in ruby &dhapp; DESCRIPTION &dhapp; is a proof-of-concept example for rbot druby-based api. This program reads lines of text from the standard input and sends them to a specified irc channel or user via rbot. Make sure you have the remotectl plugin loaded and assigned the needed rights to the user before use. OPTIONS This program follow the usual &gnu; command line syntax, with long options starting with two dashes (`-'). A summary of options is included below. Remote user. Remote user password. Destination for message (user or channel). Rbot url. Show summary of options. Tell what it's all about. VERSION This manual page was written by &dhusername; &dhemail; for the &debian; system (but may be used by others). Permission is granted to copy, distribute and/or modify this document under the terms of the &gnu; General Public License, Version 3 or any later version published by the Free Software Foundation. On Debian systems, the complete text of the GNU General Public License can be found in /usr/share/common-licenses/GPL. rbot-0.9.5+post20100705+gitb3aa806/test/0000755002342000234200000000000011411605044016243 5ustar duckdc-usersrbot-0.9.5+post20100705+gitb3aa806/test/test_plugins_priority.rb0000644002342000234200000001126111411605044023252 0ustar duckdc-users$:.unshift File.join(File.dirname(__FILE__), '../lib') require 'test/unit' require 'rbot/config' require 'rbot/plugins' require 'pp' include Irc::Bot::Plugins class TestRealBotModule < BotModule def initialize end end class MockModule < BotModule attr_reader :test_called_at attr_reader :connect_called_at def initialize(prio) @test_called_at = [] @connect_called_at = [] @priority = prio end def test @test_called_at << Time.new end # an connect fast-delegate event def connect @connect_called_at << Time.new end def botmodule_class :CoreBotModule end end class PluginsPriorityTest < Test::Unit::TestCase @@manager = nil def setup @mock1 = MockModule.new(1) @mock2 = MockModule.new(2) @mock3 = MockModule.new(3) @mock4 = MockModule.new(4) @mock5 = MockModule.new(5) # This whole thing is a PITA because PluginManagerClass is a singleton unless @@manager @@manager = PluginManagerClass.instance # this is needed because debug is setup in the rbot starter def @@manager.debug(m); puts m; end @@manager.instance_eval { alias real_sort_modules sort_modules } def @@manager.sort_modules @sort_call_count ||= 0 @sort_call_count += 1 real_sort_modules end end @@manager.instance_eval { @sort_call_count = nil } @@manager.mark_priorities_dirty # We add the modules to the lists in the wrong order # on purpose to make sure the sort is working @@manager.plugins.clear @@manager.core_modules.clear @@manager.plugins << @mock1 @@manager.plugins << @mock4 @@manager.plugins << @mock3 @@manager.plugins << @mock2 @@manager.plugins << @mock5 dlist = @@manager.instance_eval {@delegate_list['connect'.intern]} dlist.clear dlist << @mock1 dlist << @mock4 dlist << @mock3 dlist << @mock2 dlist << @mock5 end def test_default_priority plugin = TestRealBotModule.new assert_equal 1, plugin.priority end def test_sort_called @@manager.delegate('test') assert @@manager.instance_eval { @sort_call_count } end def test_sort_called_once @@manager.delegate('test') @@manager.delegate('test') @@manager.delegate('test') @@manager.delegate('test') assert_equal 1, @@manager.instance_eval { @sort_call_count } end def test_sorted plugins = @@manager.plugins assert_equal @mock1, plugins[0] assert_equal @mock4, plugins[1] assert_equal @mock3, plugins[2] assert_equal @mock2, plugins[3] assert_equal @mock5, plugins[4] @@manager.sort_modules plugins = @@manager.instance_eval { @sorted_modules } assert_equal @mock1, plugins[0] assert_equal @mock2, plugins[1] assert_equal @mock3, plugins[2] assert_equal @mock4, plugins[3] assert_equal @mock5, plugins[4] end def test_fast_delegate_sort list = @@manager.instance_eval {@delegate_list['connect'.intern]} assert_equal @mock1, list[0] assert_equal @mock4, list[1] assert_equal @mock3, list[2] assert_equal @mock2, list[3] assert_equal @mock5, list[4] @@manager.sort_modules assert_equal @mock1, list[0] assert_equal @mock2, list[1] assert_equal @mock3, list[2] assert_equal @mock4, list[3] assert_equal @mock5, list[4] end def test_slow_called_in_order @@manager.delegate('test') assert_equal 1, @mock1.test_called_at.size assert_equal 1, @mock2.test_called_at.size assert_equal 1, @mock3.test_called_at.size assert_equal 1, @mock4.test_called_at.size assert_equal 1, @mock5.test_called_at.size assert @mock1.test_called_at.first < @mock2.test_called_at.first assert @mock2.test_called_at.first < @mock3.test_called_at.first assert @mock3.test_called_at.first < @mock4.test_called_at.first assert @mock4.test_called_at.first < @mock5.test_called_at.first end def test_fast_called_in_order @@manager.delegate('connect') assert_equal 1, @mock1.connect_called_at.size assert_equal 1, @mock2.connect_called_at.size assert_equal 1, @mock3.connect_called_at.size assert_equal 1, @mock4.connect_called_at.size assert_equal 1, @mock5.connect_called_at.size assert @mock1.connect_called_at.first < @mock2.connect_called_at.first assert @mock2.connect_called_at.first < @mock3.connect_called_at.first assert @mock3.connect_called_at.first < @mock4.connect_called_at.first assert @mock4.connect_called_at.first < @mock5.connect_called_at.first end def test_add_botmodule @@manager.sort_modules mock_n1 = MockModule.new(-1) @@manager.add_botmodule mock_n1 @@manager.delegate('test') assert mock_n1.test_called_at.first < @mock1.test_called_at.first end end rbot-0.9.5+post20100705+gitb3aa806/test/test_plugins_threshold.rb0000644002342000234200000000720711411605044023372 0ustar duckdc-users$:.unshift File.join(File.dirname(__FILE__), '../lib') require 'test/unit' require 'rbot/config' require 'rbot/plugins' require 'pp' include Irc::Bot::Plugins class TestRealBotModule < BotModule def initialize end end class MockModule < BotModule attr_reader :test_called_at attr_reader :test_arg_called_at attr_reader :connect_called_at attr_reader :test_arg_val def initialize(prio) @test_called_at = [] @test_arg_called_at = [] @connect_called_at = [] @priority = prio @test_arg_val = nil end def test @test_called_at << Time.new end def test_arg a @test_arg_val = a @test_arg_called_at << Time.new end # an connect fast-delegate event def connect @connect_called_at << Time.new end def botmodule_class :CoreBotModule end end class PluginsPriorityTest < Test::Unit::TestCase @@manager = nil def setup @mock1 = MockModule.new(1) @mock2 = MockModule.new(2) @mock3 = MockModule.new(3) @mock4 = MockModule.new(4) @mock5 = MockModule.new(5) # This whole thing is a PITA because PluginManagerClass is a singleton unless @@manager @@manager = PluginManagerClass.instance # this is needed because debug is setup in the rbot starter def @@manager.debug(m); puts m; end def @@manager.error(m); puts m; end @@manager.instance_eval { alias real_sort_modules sort_modules } def @@manager.sort_modules @sort_call_count ||= 0 @sort_call_count += 1 real_sort_modules end end @@manager.instance_eval { @sort_call_count = nil } @@manager.mark_priorities_dirty # We add the modules to the lists in the wrong order # on purpose to make sure the sort is working @@manager.plugins.clear @@manager.core_modules.clear @@manager.plugins << @mock1 @@manager.plugins << @mock4 @@manager.plugins << @mock3 @@manager.plugins << @mock2 @@manager.plugins << @mock5 dlist = @@manager.instance_eval {@delegate_list['connect'.intern]} dlist.clear dlist << @mock1 dlist << @mock4 dlist << @mock3 dlist << @mock2 dlist << @mock5 end def test_above @@manager.delegate_event('test', :above => 3) assert_equal 0, @mock1.test_called_at.size assert_equal 0, @mock2.test_called_at.size assert_equal 0, @mock3.test_called_at.size assert_equal 1, @mock4.test_called_at.size assert_equal 1, @mock5.test_called_at.size end def test_below @@manager.delegate_event('test', :below => 3) assert_equal 1, @mock1.test_called_at.size assert_equal 1, @mock2.test_called_at.size assert_equal 0, @mock3.test_called_at.size assert_equal 0, @mock4.test_called_at.size assert_equal 0, @mock5.test_called_at.size end def test_fast_delagate_above @@manager.delegate_event('connect', :above => 3) assert_equal 0, @mock1.connect_called_at.size assert_equal 0, @mock2.connect_called_at.size assert_equal 0, @mock3.connect_called_at.size assert_equal 1, @mock4.connect_called_at.size assert_equal 1, @mock5.connect_called_at.size end def test_fast_delagate_above @@manager.delegate_event('connect', :below => 3) assert_equal 1, @mock1.connect_called_at.size assert_equal 1, @mock2.connect_called_at.size assert_equal 0, @mock3.connect_called_at.size assert_equal 0, @mock4.connect_called_at.size assert_equal 0, @mock5.connect_called_at.size end def test_call_with_args @@manager.delegate_event('test_arg', :above => 3, :args => [1]) assert_equal 0, @mock3.test_arg_called_at.size assert_equal 1, @mock4.test_arg_called_at.size assert_equal 1, @mock4.test_arg_val end end rbot-0.9.5+post20100705+gitb3aa806/COPYING.rbot0000644002342000234200000000227711411605044017274 0ustar duckdc-usersCopyright (C) 2002-2006 Tom Gilbert. Copyright (C) 2007-2008 Giuseppe Bilotta and the rbot development team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies of the Software and its documentation and acknowledgment shall be given in the documentation and software packages that this Software was used. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rbot-0.9.5+post20100705+gitb3aa806/README.rdoc0000644002342000234200000000344211411605044017075 0ustar duckdc-users= rbot - The Ruby IRC bot rbot is a ruby IRC bot. Think of him as a ruby bot framework with a highly modular design based around plugins. == rbot features * Runtime configuration via irc chat * User authentication and access levels for using different bot features * Built in infobot-style keywords. * Support for underlying fact database (infobot fact files), which can be overridden or supplemented by runtime keyword controls * Multi-language support - comes with english, dutch, german, french, italian japanese, chinese, russian and finnish definitions so far - more translations welcome * Powerful plugin architecture, comes with plugins for: - RSS feed updates - IMDb queries - Translator with multitude of services, it's easy as !translate Ein Automobil - Last.fm - Google searching - URL information - Seen +nick+? * rbot: seen tango? tango_ was last seen 20 minutes and 7 seconds ago, joining #rbot - Reminders * Example: remind me about pizza in the oven in 15 minutes - Checking the weather - Doing math - GeoIP lookup - Lots of games, including uno, hangman, azgame and roshambo - Karma - Per-channel quote storage, searching and retrieval - Check the spelling of a word - RPG dice rolling - larting people - Conversation stats — also, rbot log format is supported by {pisg}[http://pisg.sourceforge.net/] - more... Thanks are owed to the infobot developers - several of rbot's features are inspired by infobot and so are some of the default plugins. Thanks are also owed to RADKade1, as rbot's quote plugin is a direct reimplementation of his "quotesaq" - simply because it's a great quote interface. Mainly, rbot's fun to play with, although the plugin architecture can be used to write very useful modules rbot-0.9.5+post20100705+gitb3aa806/rbot.gemspec0000644002342000234200000000206711414326121017603 0ustar duckdc-usersGem::Specification.new do |s| s.name = 'rbot' s.version = '0.9.15' s.summary = <<-EOF A modular ruby IRC bot. EOF s.description = <<-EOF A modular ruby IRC bot specifically designed for ease of extension via plugins. EOF s.requirements << 'Ruby, version 1.8.0 (or newer)' s.files = FileList[ 'lib/**/*.rb', 'bin/*', 'data/rbot/**/*', 'AUTHORS', 'COPYING', 'COPYING.rbot', 'GPLv2', 'README.rdoc', 'REQUIREMENTS', 'TODO', 'ChangeLog', 'INSTALL', 'Usage_en.txt', 'man/rbot.1', 'man/rbot-remote.1', 'setup.rb', 'launch_here.rb', 'po/*.pot', 'po/**/*.po' ] s.bindir = 'bin' s.executables = ['rbot', 'rbot-remote'] s.default_executable = 'rbot' s.extensions = 'Rakefile' # s.autorequire = 'rbot/ircbot' s.has_rdoc = true s.rdoc_options = ['--exclude', 'post-install.rb', '--title', 'rbot API Documentation', '--main', 'README.rdoc', 'README.rdoc'] s.author = 'Tom Gilbert' s.email = 'tom@linuxbrit.co.uk' s.homepage = 'http://ruby-rbot.org' s.rubyforge_project = 'rbot' end rbot-0.9.5+post20100705+gitb3aa806/lib/0000755002342000234200000000000011411605044016032 5ustar duckdc-usersrbot-0.9.5+post20100705+gitb3aa806/lib/rbot/0000755002342000234200000000000011414373321017003 5ustar duckdc-usersrbot-0.9.5+post20100705+gitb3aa806/lib/rbot/plugins.rb0000644002342000234200000010404511411605044021012 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot plugin management require 'singleton' module Irc class Bot Config.register Config::ArrayValue.new('plugins.blacklist', :default => [], :wizard => false, :requires_rescan => true, :desc => "Plugins that should not be loaded") Config.register Config::ArrayValue.new('plugins.whitelist', :default => [], :wizard => false, :requires_rescan => true, :desc => "Only whitelisted plugins will be loaded unless the list is empty") module Plugins require 'rbot/messagemapper' =begin rdoc BotModule is the base class for the modules that enhance the rbot functionality. Rather than subclassing BotModule, however, one should subclass either CoreBotModule (reserved for system modules) or Plugin (for user plugins). A BotModule interacts with Irc events by defining one or more of the following methods, which get called as appropriate when the corresponding Irc event happens. map(template, options):: map!(template, options):: map is the new, cleaner way to respond to specific message formats without littering your plugin code with regexps, and should be used instead of #register() and #privmsg() (see below) when possible. The difference between map and map! is that map! will not register the new command as an alternative name for the plugin. Examples: plugin.map 'karmastats', :action => 'karma_stats' # while in the plugin... def karma_stats(m, params) m.reply "..." end # the default action is the first component plugin.map 'karma' # attributes can be pulled out of the match string plugin.map 'karma for :key' plugin.map 'karma :key' # while in the plugin... def karma(m, params) item = params[:key] m.reply 'karma for #{item}' end # you can setup defaults, to make parameters optional plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'} # the default auth check is also against the first component # but that can be changed plugin.map 'karmastats', :auth => 'karma' # maps can be restricted to public or private message: plugin.map 'karmastats', :private => false plugin.map 'karmastats', :public => false See MessageMapper#map for more information on the template format and the allowed options. listen(UserMessage):: Called for all messages of any type. To differentiate them, use message.kind_of? It'll be either a PrivMessage, NoticeMessage, KickMessage, QuitMessage, PartMessage, JoinMessage, NickMessage, etc. ctcp_listen(UserMessage):: Called for all messages that contain a CTCP command. Use message.ctcp to get the CTCP command, and message.message to get the parameter string. To reply, use message.ctcp_reply, which sends a private NOTICE to the sender. message(PrivMessage):: Called for all PRIVMSG. Hook on this method if you need to handle PRIVMSGs regardless of whether they are addressed to the bot or not, and regardless of privmsg(PrivMessage):: Called for a PRIVMSG if the first word matches one the plugin #register()ed for. Use m.plugin to get that word and m.params for the rest of the message, if applicable. unreplied(PrivMessage):: Called for a PRIVMSG which has not been replied to. notice(NoticeMessage):: Called for all Notices. Please notice that in general should not be replied to. kick(KickMessage):: Called when a user (or the bot) is kicked from a channel the bot is in. invite(InviteMessage):: Called when the bot is invited to a channel. join(JoinMessage):: Called when a user (or the bot) joins a channel part(PartMessage):: Called when a user (or the bot) parts a channel quit(QuitMessage):: Called when a user (or the bot) quits IRC nick(NickMessage):: Called when a user (or the bot) changes Nick modechange(ModeChangeMessage):: Called when a User or Channel mode is changed topic(TopicMessage):: Called when a user (or the bot) changes a channel topic welcome(WelcomeMessage):: Called when the welcome message is received on joining a server succesfully. motd(MotdMessage):: Called when the Message Of The Day is fully recevied from the server. connect:: Called when a server is joined successfully, but before autojoin channels are joined (no params) set_language(String):: Called when the user sets a new language whose name is the given String save:: Called when you are required to save your plugin's state, if you maintain data between sessions cleanup:: called before your plugin is "unloaded", prior to a plugin reload or bot quit - close any open files/connections or flush caches here =end class BotModule # the associated bot attr_reader :bot # the plugin registry attr_reader :registry # the message map handler attr_reader :handler # Initialise your bot module. Always call super if you override this method, # as important variables are set up for you: # # @bot:: # the rbot instance # @registry:: # the botmodule's registry, which can be used to store permanent data # (see Registry::Accessor for additional documentation) # # Other instance variables which are defined and should not be overwritten # byt the user, but aren't usually accessed directly, are: # # @manager:: # the plugins manager instance # @botmodule_triggers:: # an Array of words this plugin #register()ed itself for # @handler:: # the MessageMapper that handles this plugin's maps # def initialize @manager = Plugins::manager @bot = @manager.bot @priority = nil @botmodule_triggers = Array.new @handler = MessageMapper.new(self) @registry = Registry::Accessor.new(@bot, self.class.to_s.gsub(/^.*::/, "")) @manager.add_botmodule(self) if self.respond_to?('set_language') self.set_language(@bot.lang.language) end end # Changing the value of @priority directly will cause problems, # Please use priority=. def priority @priority ||= 1 end # Returns the symbol :BotModule def botmodule_class :BotModule end # Method called to flush the registry, thus ensuring that the botmodule's permanent # data is committed to disk # def flush_registry # debug "Flushing #{@registry}" @registry.flush end # Method called to cleanup before the plugin is unloaded. If you overload # this method to handle additional cleanup tasks, remember to call super() # so that the default cleanup actions are taken care of as well. # def cleanup # debug "Closing #{@registry}" @registry.close end # Handle an Irc::PrivMessage for which this BotModule has a map. The method # is called automatically and there is usually no need to call it # explicitly. # def handle(m) @handler.handle(m) end # Signal to other BotModules that an even happened. # def call_event(ev, *args) @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *(args.push Hash.new)) end # call-seq: map(template, options) # # This is the preferred way to register the BotModule so that it # responds to appropriately-formed messages on Irc. # def map(*args) do_map(false, *args) end # call-seq: map!(template, options) # # This is the same as map but doesn't register the new command # as an alternative name for the plugin. # def map!(*args) do_map(true, *args) end # Auxiliary method called by #map and #map! def do_map(silent, *args) @handler.map(self, *args) # register this map map = @handler.last name = map.items[0] self.register name, :auth => nil, :hidden => silent @manager.register_map(self, map) unless self.respond_to?('privmsg') def self.privmsg(m) #:nodoc: handle(m) end end end # Sets the default auth for command path _cmd_ to _val_ on channel _chan_: # usually _chan_ is either "*" for everywhere, public and private (in which # case it can be omitted) or "?" for private communications # def default_auth(cmd, val, chan="*") case cmd when "*", "" c = nil else c = cmd end Auth::defaultbotuser.set_default_permission(propose_default_path(c), val) end # Gets the default command path which would be given to command _cmd_ def propose_default_path(cmd) [name, cmd].compact.join("::") end # Return an identifier for this plugin, defaults to a list of the message # prefixes handled (used for error messages etc) def name self.class.to_s.downcase.sub(/^#::/,"").sub(/(plugin|module)?$/,"") end # Just calls name def to_s name end # Intern the name def to_sym self.name.to_sym end # Return a help string for your module. For complex modules, you may wish # to break your help into topics, and return a list of available topics if # +topic+ is nil. +plugin+ is passed containing the matching prefix for # this message - if your plugin handles multiple prefixes, make sure you # return the correct help for the prefix requested def help(plugin, topic) "no help" end # Register the plugin as a handler for messages prefixed _cmd_. # # This can be called multiple times for a plugin to handle multiple message # prefixes. # # This command is now superceded by the #map() command, which should be used # instead whenever possible. # def register(cmd, opts={}) raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash) who = @manager.who_handles?(cmd) if who raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self return end if opts.has_key?(:auth) @manager.register(self, cmd, opts[:auth]) else @manager.register(self, cmd, propose_default_path(cmd)) end @botmodule_triggers << cmd unless opts.fetch(:hidden, false) end # Default usage method provided as a utility for simple plugins. The # MessageMapper uses 'usage' as its default fallback method. # def usage(m, params = {}) if params[:failures].respond_to? :find friendly = params[:failures].find do |f| f.kind_of? MessageMapper::FriendlyFailure end if friendly m.reply friendly.friendly return end end m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"}) end # Define the priority of the module. During event delegation, lower # priority modules will be called first. Default priority is 1 def priority=(prio) if @priority != prio @priority = prio @bot.plugins.mark_priorities_dirty end end # Directory name to be joined to the botclass to access data files. By # default this is the plugin name itself, but may be overridden, for # example by plugins that share their datafiles or for backwards # compatibilty def dirname name end # Filename for a datafile built joining the botclass, plugin dirname and # actual file name def datafile(*fname) @bot.path dirname, *fname end end # A CoreBotModule is a BotModule that provides core functionality. # # This class should not be used by user plugins, as it's reserved for system # plugins such as the ones that handle authentication, configuration and basic # functionality. # class CoreBotModule < BotModule def botmodule_class :CoreBotModule end end # A Plugin is a BotModule that provides additional functionality. # # A user-defined plugin should subclass this, and then define any of the # methods described in the documentation for BotModule to handle interaction # with Irc events. # class Plugin < BotModule def botmodule_class :Plugin end end # Singleton to manage multiple plugins and delegate messages to them for # handling class PluginManagerClass include Singleton attr_reader :bot attr_reader :botmodules attr_reader :maps # This is the list of patterns commonly delegated to plugins. # A fast delegation lookup is enabled for them. DEFAULT_DELEGATE_PATTERNS = %r{^(?: connect|names|nick| listen|ctcp_listen|privmsg|unreplied| kick|join|part|quit| save|cleanup|flush_registry| set_.*|event_.* )$}x def initialize @botmodules = { :CoreBotModule => [], :Plugin => [] } @names_hash = Hash.new @commandmappers = Hash.new @maps = Hash.new # modules will be sorted on first delegate call @sorted_modules = nil @delegate_list = Hash.new { |h, k| h[k] = Array.new } @core_module_dirs = [] @plugin_dirs = [] @failed = Array.new @ignored = Array.new bot_associate(nil) end def inspect ret = self.to_s[0..-2] ret << ' corebotmodules=' ret << @botmodules[:CoreBotModule].map { |m| m.name }.inspect ret << ' plugins=' ret << @botmodules[:Plugin].map { |m| m.name }.inspect ret << ">" end # Reset lists of botmodules def reset_botmodule_lists @botmodules[:CoreBotModule].clear @botmodules[:Plugin].clear @names_hash.clear @commandmappers.clear @maps.clear @failures_shown = false mark_priorities_dirty end # Associate with bot _bot_ def bot_associate(bot) reset_botmodule_lists @bot = bot end # Returns the botmodule with the given _name_ def [](name) @names_hash[name.to_sym] end # Returns +true+ if _cmd_ has already been registered as a command def who_handles?(cmd) return nil unless @commandmappers.has_key?(cmd.to_sym) return @commandmappers[cmd.to_sym][:botmodule] end # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_ def register(botmodule, cmd, auth_path) raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule) @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path} end # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash # which has three keys: # # botmodule:: the associated botmodule # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map # map:: the actual MessageTemplate object # # def register_map(botmodule, map) raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule) @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map } end def add_botmodule(botmodule) raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule) kl = botmodule.botmodule_class if @names_hash.has_key?(botmodule.to_sym) case self[botmodule].botmodule_class when kl raise "#{kl} #{botmodule} already registered!" else raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}" end end @botmodules[kl] << botmodule @names_hash[botmodule.to_sym] = botmodule mark_priorities_dirty end # Returns an array of the loaded plugins def core_modules @botmodules[:CoreBotModule] end # Returns an array of the loaded plugins def plugins @botmodules[:Plugin] end # Returns a hash of the registered message prefixes and associated # plugins def commands @commandmappers end # Tells the PluginManager that the next time it delegates an event, it # should sort the modules by priority def mark_priorities_dirty @sorted_modules = nil end # Makes a string of error _err_ by adding text _str_ def report_error(str, err) ([str, err.inspect] + err.backtrace).join("\n") end # This method is the one that actually loads a module from the # file _fname_ # # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever) # # It returns the Symbol :loaded on success, and an Exception # on failure # def load_botmodule_file(fname, desc=nil) # create a new, anonymous module to "house" the plugin # the idea here is to prevent namespace pollution. perhaps there # is another way? plugin_module = Module.new # each plugin uses its own textdomain, we bind it automatically here bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}") desc = desc.to_s + " " if desc begin plugin_string = IO.read(fname) debug "loading #{desc}#{fname}" plugin_module.module_eval(plugin_string, fname) return :loaded rescue Exception => err # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err error report_error("#{desc}#{fname} load failed", err) bt = err.backtrace.select { |line| line.match(/^(\(eval\)|#{fname}):\d+/) } bt.map! { |el| el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m| "#{fname}#{$1}#{$3}" } } msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m| "#{fname}#{$1}#{$3}" } begin newerr = err.class.new(msg) rescue ArgumentError => err_in_err # Somebody should hang the ActiveSupport developers by their balls # with barbed wire. Their MissingSourceFile extension to LoadError # _expects_ a second argument, breaking the usual Exception interface # (instead, the smart thing to do would have been to make the second # parameter optional and run the code in the from_message method if # it was missing). # Anyway, we try to cope with this in the simplest possible way. On # the upside, this new block can be extended to handle other similar # idiotic approaches if err.class.respond_to? :from_message newerr = err.class.from_message(msg) else raise err_in_err end end newerr.set_backtrace(bt) return newerr end end private :load_botmodule_file # add one or more directories to the list of directories to # load core modules from def add_core_module_dir(*dirlist) @core_module_dirs += dirlist debug "Core module loading paths: #{@core_module_dirs.join(', ')}" end # add one or more directories to the list of directories to # load plugins from def add_plugin_dir(*dirlist) @plugin_dirs += dirlist debug "Plugin loading paths: #{@plugin_dirs.join(', ')}" end def clear_botmodule_dirs @core_module_dirs.clear @plugin_dirs.clear debug "Core module and plugin loading paths cleared" end def scan_botmodules(opts={}) type = opts[:type] processed = Hash.new case type when :core dirs = @core_module_dirs when :plugins dirs = @plugin_dirs @bot.config['plugins.blacklist'].each { |p| pn = p + ".rb" processed[pn.intern] = :blacklisted } whitelist = @bot.config['plugins.whitelist'].map { |p| p + ".rb" } end dirs.each do |dir| next unless FileTest.directory?(dir) d = Dir.new(dir) d.sort.each do |file| next unless file =~ /\.rb$/ next if file =~ /^\./ case type when :plugins if !whitelist.empty? && !whitelist.include?(file) @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" } next elsif processed.has_key?(file.intern) @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]} next end if(file =~ /^(.+\.rb)\.disabled$/) # GB: Do we want to do this? This means that a disabled plugin in a directory # will disable in all subsequent directories. This was probably meant # to be used before plugins.blacklist was implemented, so I think # we don't need this anymore processed[$1.intern] = :disabled @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]} next end end did_it = load_botmodule_file("#{dir}/#{file}", "plugin") case did_it when Symbol processed[file.intern] = did_it when Exception @failed << { :name => file, :dir => dir, :reason => did_it } end end end end # load plugins from pre-assigned list of directories def scan @failed.clear @ignored.clear @delegate_list.clear scan_botmodules(:type => :core) scan_botmodules(:type => :plugins) debug "finished loading plugins: #{status(true)}" (core_modules + plugins).each { |p| p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m| @delegate_list[m.intern] << p } } mark_priorities_dirty end # call the save method for each active plugin def save delegate 'flush_registry' delegate 'save' end # call the cleanup method for each active plugin def cleanup delegate 'cleanup' reset_botmodule_lists end # drop all plugins and rescan plugins on disk # calls save and cleanup for each plugin before dropping them def rescan save cleanup scan end def status(short=false) output = [] if self.core_length > 0 if short output << n_("%{count} core module loaded", "%{count} core modules loaded", self.core_length) % {:count => self.core_length} else output << n_("%{count} core module: %{list}", "%{count} core modules: %{list}", self.core_length) % { :count => self.core_length, :list => core_modules.collect{ |p| p.name}.sort.join(", ") } end else output << _("no core botmodules loaded") end # Active plugins first if(self.length > 0) if short output << n_("%{count} plugin loaded", "%{count} plugins loaded", self.length) % {:count => self.length} else output << n_("%{count} plugin: %{list}", "%{count} plugins: %{list}", self.length) % { :count => self.length, :list => plugins.collect{ |p| p.name}.sort.join(", ") } end else output << "no plugins active" end # Ignored plugins next unless @ignored.empty? or @failures_shown if short output << n_("%{highlight}%{count} plugin ignored%{highlight}", "%{highlight}%{count} plugins ignored%{highlight}", @ignored.length) % { :count => @ignored.length, :highlight => Underline } else output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why", "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why", @ignored.length) % { :count => @ignored.length, :highlight => Underline, :bold => Bold, :command => "help ignored plugins"} end end # Failed plugins next unless @failed.empty? or @failures_shown if short output << n_("%{highlight}%{count} plugin failed to load%{highlight}", "%{highlight}%{count} plugins failed to load%{highlight}", @failed.length) % { :count => @failed.length, :highlight => Reverse } else output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why", "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why", @failed.length) % { :count => @failed.length, :highlight => Reverse, :bold => Bold, :command => "help failed plugins"} end end output.join '; ' end # return list of help topics (plugin names) def helptopics rv = status @failures_shown = true rv end def length plugins.length end def core_length core_modules.length end # return help for +topic+ (call associated plugin's help method) def help(topic="") case topic when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/ # debug "Failures: #{@failed.inspect}" return _("no plugins failed to load") if @failed.empty? return @failed.collect { |p| _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % { :highlight => Bold, :plugin => p[:name], :dir => p[:dir], :exception => p[:reason].class, :reason => p[:reason], } + if $1 && !p[:reason].backtrace.empty? _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')} else '' end }.join("\n") when /ignored?\s*plugins?/ return _('no plugins were ignored') if @ignored.empty? tmp = Hash.new @ignored.each do |p| reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s) ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name]) end return tmp.map do |dir, reasons| # FIXME get rid of these string concatenations to make gettext easier s = reasons.map { |r, list| list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})" }.join('; ') "in #{dir}: #{s}" end.join('; ') when /^(\S+)\s*(.*)$/ key = $1 params = $2 # Let's see if we can match a plugin by the given name (core_modules + plugins).each { |p| next unless p.name == key begin return p.help(key, params) rescue Exception => err #rescue TimeoutError, StandardError, NameError, SyntaxError => err error report_error("#{p.botmodule_class} #{p.name} help() failed:", err) end } # Nope, let's see if it's a command, and ask for help at the corresponding botmodule k = key.to_sym if commands.has_key?(k) p = commands[k][:botmodule] begin return p.help(key, params) rescue Exception => err #rescue TimeoutError, StandardError, NameError, SyntaxError => err error report_error("#{p.botmodule_class} #{p.name} help() failed:", err) end end end return false end def sort_modules @sorted_modules = (core_modules + plugins).sort do |a, b| a.priority <=> b.priority end || [] @delegate_list.each_value do |list| list.sort! {|a,b| a.priority <=> b.priority} end end # call-seq: delegate(method, m, opts={}) # delegate(method, opts={}) # # see if each plugin handles _method_, and if so, call it, passing # _m_ as a parameter (if present). BotModules are called in order of # priority from lowest to highest. # # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it # will only be delegated to plugins with negative priority. Conversely, if # it's a fake message (see BotModule#fake_message), it will only be # delegated to plugins with positive priority. # # Note that _m_ can also be an exploded Array, but in this case the last # element of it cannot be a Hash, or it will be interpreted as the options # Hash for delegate itself. The last element can be a subclass of a Hash, though. # To be on the safe side, you can add an empty Hash as last parameter for delegate # when calling it with an exploded Array: # @bot.plugins.delegate(method, *(args.push Hash.new)) # # Currently supported options are the following: # :above :: # if specified, the delegation will only consider plugins with a priority # higher than the specified value # :below :: # if specified, the delegation will only consider plugins with a priority # lower than the specified value # def delegate(method, *args) # if the priorities order of the delegate list is dirty, # meaning some modules have been added or priorities have been # changed, then the delegate list will need to be sorted before # delegation. This should always be true for the first delegation. sort_modules unless @sorted_modules opts = {} opts.merge(args.pop) if args.last.class == Hash m = args.first if BasicUserMessage === m # ignored messages should not be delegated # to plugins with positive priority opts[:below] ||= 0 if m.ignored? # fake messages should not be delegated # to plugins with negative priority opts[:above] ||= 0 if m.recurse_depth > 0 end above = opts[:above] below = opts[:below] # debug "Delegating #{method.inspect}" ret = Array.new if method.match(DEFAULT_DELEGATE_PATTERNS) debug "fast-delegating #{method}" m = method.to_sym debug "no-one to delegate to" unless @delegate_list.has_key?(m) return [] unless @delegate_list.has_key?(m) @delegate_list[m].each { |p| begin prio = p.priority unless (above and above >= prio) or (below and below <= prio) ret.push p.send(method, *args) end rescue Exception => err raise if err.kind_of?(SystemExit) error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err) raise if err.kind_of?(BDB::Fatal) end } else debug "slow-delegating #{method}" @sorted_modules.each { |p| if(p.respond_to? method) begin # debug "#{p.botmodule_class} #{p.name} responds" prio = p.priority unless (above and above >= prio) or (below and below <= prio) ret.push p.send(method, *args) end rescue Exception => err raise if err.kind_of?(SystemExit) error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err) raise if err.kind_of?(BDB::Fatal) end end } end return ret # debug "Finished delegating #{method.inspect}" end # see if we have a plugin that wants to handle this message, if so, pass # it to the plugin and return true, otherwise false def privmsg(m) debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}" return unless m.plugin k = m.plugin.to_sym if commands.has_key?(k) p = commands[k][:botmodule] a = commands[k][:auth] # We check here for things that don't check themselves # (e.g. mapped things) debug "Checking auth ..." if a.nil? || @bot.auth.allow?(a, m.source, m.replyto) debug "Checking response ..." if p.respond_to?("privmsg") begin debug "#{p.botmodule_class} #{p.name} responds" p.privmsg(m) rescue Exception => err raise if err.kind_of?(SystemExit) error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err) raise if err.kind_of?(BDB::Fatal) end debug "Successfully delegated #{m.inspect}" return true else debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()" end else debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}" end else debug "Command #{k} isn't handled" end return false end # delegate IRC messages, by delegating 'listen' first, and the actual method # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message # as appropriate. def irc_delegate(method, m) delegate('listen', m) if method.to_sym == :privmsg delegate('ctcp_listen', m) if m.ctcp delegate('message', m) privmsg(m) if m.address? and not m.ignored? delegate('unreplied', m) unless m.replied else delegate(method, m) end end end # Returns the only PluginManagerClass instance def Plugins.manager return PluginManagerClass.instance end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/config.rb0000644002342000234200000002457411411605044020606 0ustar duckdc-usersrequire 'singleton' require 'yaml' unless YAML.respond_to?(:load_file) def YAML.load_file( filepath ) File.open( filepath ) do |f| YAML::load( f ) end end end module Irc class Bot module Config class Value # allow the definition order to be preserved so that sorting by # definition order is possible. The Wizard does this to allow # the :wizard questions to be in a sensible order. @@order = 0 attr_reader :type attr_reader :desc attr_reader :key attr_reader :wizard attr_reader :requires_restart attr_reader :requires_rescan attr_reader :order attr_reader :manager attr_reader :auth_path def initialize(key, params) @manager = Config.manager # Keys must be in the form 'module.name'. # They will be internally passed around as symbols, # but we accept them both in string and symbol form. unless key.to_s =~ /^.+\..+$/ raise ArgumentError,"key must be of the form 'module.name'" end @order = @@order @@order += 1 @key = key.to_sym if @manager.overrides.key?(@key) @default = @manager.overrides[@key] elsif params.has_key? :default @default = params[:default] else @default = false end @desc = params[:desc] @type = params[:type] || String @on_change = params[:on_change] @validate = params[:validate] @wizard = params[:wizard] @requires_restart = params[:requires_restart] @requires_rescan = params[:requires_rescan] @auth_path = "config::key::#{key.sub('.','::')}" end def default if @default.instance_of?(Proc) @default.call else @default end end def get return @manager.config[@key] if @manager.config.has_key?(@key) return default end alias :value :get def set(value, on_change = true) @manager.config[@key] = value @manager.changed = true @on_change.call(@manager.bot, value) if on_change && @on_change return self end def unset @manager.config.delete(@key) @manager.changed = true @on_change.call(@manager.bot, value) if @on_change return self end # set string will raise ArgumentErrors on failed parse/validate def set_string(string, on_change = true) value = parse string if validate value set value, on_change else raise ArgumentError, "invalid value: #{string}" end end # override this. the default will work for strings only def parse(string) string end def to_s get.to_s end protected def validate(val, validator = @validate) case validator when false, nil return true when Proc return validator.call(val) when Regexp raise ArgumentError, "validation via Regexp only supported for strings!" unless String === val return validator.match(val) else raise ArgumentError, "validation type #{validator.class} not supported" end end end class StringValue < Value end class BooleanValue < Value def parse(string) return true if string == "true" return false if string == "false" if string =~ /^-?\d+$/ return string.to_i != 0 end raise ArgumentError, "#{string} does not match either 'true' or 'false', and it's not an integer either" end def get r = super if r.kind_of?(Integer) return r != 0 else return r end end end class IntegerValue < Value def parse(string) return 1 if string == "true" return 0 if string == "false" raise ArgumentError, "not an integer: #{string}" unless string =~ /^-?\d+$/ string.to_i end def get r = super if r.kind_of?(Integer) return r else return r ? 1 : 0 end end end class FloatValue < Value def parse(string) raise ArgumentError, "not a float #{string}" unless string =~ /^-?[\d.]+$/ string.to_f end end class ArrayValue < Value def initialize(key, params) super @validate_item = params[:validate_item] @validate ||= Proc.new do |v| !v.find { |i| !validate_item(i) } end end def validate_item(item) validate(item, @validate_item) end def parse(string) string.split(/,\s+/) end def to_s get.join(", ") end def add(val) newval = self.get.dup unless newval.include? val newval << val validate_item(val) or raise ArgumentError, "invalid item: #{val}" validate(newval) or raise ArgumentError, "invalid value: #{newval.inspect}" set(newval) end end def rm(val) curval = self.get raise ArgumentError, "value #{val} not present" unless curval.include?(val) set(curval - [val]) end end class EnumValue < Value def initialize(key, params) super @values = params[:values] end def values if @values.instance_of?(Proc) return @values.call(@manager.bot) else return @values end end def parse(string) unless values.include?(string) raise ArgumentError, "invalid value #{string}, allowed values are: " + values.join(", ") end string end def desc _("%{desc} [valid values are: %{values}]") % {:desc => @desc, :values => values.join(', ')} end end # container for bot configuration class ManagerClass include Singleton attr_reader :bot attr_reader :items attr_reader :config attr_reader :overrides attr_accessor :changed def initialize bot_associate(nil,true) end def reset_config @items = Hash.new @config = Hash.new(false) # We allow default values for config keys to be overridden by # the config file /etc/rbot.conf # The main purpose for this is to allow distro or system-wide # settings such as external program paths (figlet, toilet, ispell) # to be set once for all the bots. @overrides = Hash.new etcfile = "/etc/rbot.conf" if File.exist?(etcfile) log "Loading defaults from #{etcfile}" etcconf = YAML::load_file(etcfile) etcconf.each { |k, v| @overrides[k.to_sym] = v } end end # Associate with bot _bot_ def bot_associate(bot, reset=false) reset_config if reset @bot = bot return unless @bot @changed = false conf = @bot.path 'conf.yaml' if File.exist? conf begin newconfig = YAML::load_file conf newconfig.each { |key, val| @config[key.to_sym] = val } return rescue error "failed to read conf.yaml: #{$!}" end end # if we got here, we need to run the first-run wizard Wizard.new(@bot).run # save newly created config @changed = true save end def register(item) unless item.kind_of?(Value) raise ArgumentError,"item must be an Irc::Bot::Config::Value" end @items[item.key] = item end # currently we store values in a hash but this could be changed in the # future. We use hash semantics, however. # components that register their config keys and setup defaults are # supported via [] def [](key) # return @items[key].value if @items.has_key?(key) return @items[key.to_sym].value if @items.has_key?(key.to_sym) # try to still support unregistered lookups # but warn about them # if @config.has_key?(key) # warning "Unregistered lookup #{key.inspect}" # return @config[key] # end if @config.has_key?(key.to_sym) warning _("Unregistered lookup #{key.to_sym.inspect}") return @config[key.to_sym] end return false end def []=(key, value) return @items[key.to_sym].set(value) if @items.has_key?(key.to_sym) if @config.has_key?(key.to_sym) warning _("Unregistered lookup #{key.to_sym.inspect}") return @config[key.to_sym] = value end end # pass everything else through to the hash def method_missing(method, *args, &block) return @config.send(method, *args, &block) end # write current configuration to #{botclass}/conf.yaml def save if not @changed debug "Not writing conf.yaml (unchanged)" return end begin conf = @bot.path 'conf.yaml' fnew = conf + '.new' debug "Writing new conf.yaml ..." File.open(fnew, "w") do |file| savehash = {} @config.each { |key, val| savehash[key.to_s] = val } file.puts savehash.to_yaml end debug "Officializing conf.yaml ..." File.rename(fnew, conf) @changed = false rescue => e error "failed to write configuration file conf.yaml! #{$!}" error "#{e.class}: #{e}" error e.backtrace.join("\n") end end end # Returns the only Irc::Bot::Config::ManagerClass # def Config.manager return ManagerClass.instance end # Register a config value def Config.register(item) Config.manager.register(item) end class Wizard def initialize(bot) @bot = bot @manager = Config.manager @questions = @manager.items.values.find_all {|i| i.wizard } end def run() $stdout.sync = true puts _("First time rbot configuration wizard") puts "====================================" puts _("This is the first time you have run rbot with a config directory of: #{@bot.botclass}") puts _("This wizard will ask you a few questions to get you started.") puts _("The rest of rbot's configuration can be manipulated via IRC once rbot is connected and you are auth'd.") puts "-----------------------------------" return unless @questions @questions.sort{|a,b| a.order <=> b.order }.each do |q| puts _(q.desc) begin print q.key.to_s + " [#{q.to_s}]: " response = STDIN.gets response.chop! unless response.empty? q.set_string response, false end puts _("configured #{q.key} => #{q.to_s}") puts "-----------------------------------" rescue ArgumentError => e puts _("failed to set #{q.key}: #{e.message}") retry end end end end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/irc.rb0000644002342000234200000015475011411605044020116 0ustar duckdc-users#-- vim:sw=2:et # General TODO list # * do we want to handle a Channel list for each User telling which # Channels is the User on (of those the client is on too)? # We may want this so that when a User leaves all Channels and he hasn't # sent us privmsgs, we know we can remove him from the Server @users list # FIXME for the time being, we do it with a method that scans the server # (if defined), so the method is slow and should not be used frequently. # * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf? # See items marked as TODO Ho. # The framework to do this is now in place, thanks to the new [] method # for NetmaskList, which allows retrieval by Netmask or String #++ # :title: IRC module # # Basic IRC stuff # # This module defines the fundamental building blocks for IRC # # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com) require 'singleton' # The following monkeypatch is to fix a bug in Singleton where marshaling would # fail when trying to restore a marshaled Singleton due to _load being declared # private. if RUBY_VERSION < '1.9' module ::Singleton public :_dump end class << Singleton module SingletonClassMethods public :_load end end end class Object # We extend the Object class with a method that # checks if the receiver is nil or empty def nil_or_empty? return true unless self return true if self.respond_to? :empty? and self.empty? return false end # We alias the to_s method to __to_s__ to make # it accessible in all classes alias :__to_s__ :to_s end # The Irc module is used to keep all IRC-related classes # in the same namespace # module Irc # Due to its Scandinavian origins, IRC has strange case mappings, which # consider the characters {}|^ as the uppercase # equivalents of # []\~. # # This is however not the same on all IRC servers: some use standard ASCII # casemapping, other do not consider ^ as the uppercase of # ~ # class Casemap @@casemaps = {} # Create a new casemap with name _name_, uppercase characters _upper_ and # lowercase characters _lower_ # def initialize(name, upper, lower) @key = name.to_sym raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key) @@casemaps[@key] = { :upper => upper, :lower => lower, :casemap => self } end # Returns the Casemap with the given name # def Casemap.get(name) @@casemaps[name.to_sym][:casemap] end # Retrieve the 'uppercase characters' of this Casemap # def upper @@casemaps[@key][:upper] end # Retrieve the 'lowercase characters' of this Casemap # def lower @@casemaps[@key][:lower] end # Return a Casemap based on the receiver # def to_irc_casemap self end # A Casemap is represented by its lower/upper mappings # def inspect self.__to_s__[0..-2] + " #{upper.inspect} ~(#{self})~ #{lower.inspect}>" end # As a String we return our name # def to_s @key.to_s end # Two Casemaps are equal if they have the same upper and lower ranges # def ==(arg) other = arg.to_irc_casemap return self.upper == other.upper && self.lower == other.lower end # Give a warning if _arg_ and self are not the same Casemap # def must_be(arg) other = arg.to_irc_casemap if self == other return true else warn "Casemap mismatch (#{self.inspect} != #{other.inspect})" return false end end end # The rfc1459 casemap # class RfcCasemap < Casemap include Singleton def initialize super('rfc1459', "\x41-\x5a\x7b-\x7e", "\x61-\x7a\x5b-\x5e") end end RfcCasemap.instance # The strict-rfc1459 Casemap # class StrictRfcCasemap < Casemap include Singleton def initialize super('strict-rfc1459', "\x41-\x5a\x7b-\x7d", "\x61-\x7a\x5b-\x5d") end end StrictRfcCasemap.instance # The ascii Casemap # class AsciiCasemap < Casemap include Singleton def initialize super('ascii', "\x41-\x5a", "\x61-\x7a") end end AsciiCasemap.instance # This module is included by all classes that are either bound to a server # or should have a casemap. # module ServerOrCasemap attr_reader :server # This method initializes the instance variables @server and @casemap # according to the values of the hash keys :server and :casemap in _opts_ # def init_server_or_casemap(opts={}) @server = opts.fetch(:server, nil) raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server) @casemap = opts.fetch(:casemap, nil) if @server if @casemap @server.casemap.must_be(@casemap) @casemap = nil end else @casemap = (@casemap || 'rfc1459').to_irc_casemap end end # This is an auxiliary method: it returns true if the receiver fits the # server and casemap specified in _opts_, false otherwise. # def fits_with_server_and_casemap?(opts={}) srv = opts.fetch(:server, nil) cmap = opts.fetch(:casemap, nil) cmap = cmap.to_irc_casemap unless cmap.nil? if srv.nil? return true if cmap.nil? or cmap == casemap else return true if srv == @server and (cmap.nil? or cmap == casemap) end return false end # Returns the casemap of the receiver, by looking at the bound # @server (if possible) or at the @casemap otherwise # def casemap return @server.casemap if defined?(@server) and @server return @casemap end # Returns a hash with the current @server and @casemap as values of # :server and :casemap # def server_and_casemap h = {} h[:server] = @server if defined?(@server) and @server h[:casemap] = @casemap if defined?(@casemap) and @casemap return h end # We allow up/downcasing with a different casemap # def irc_downcase(cmap=casemap) self.to_s.irc_downcase(cmap) end # Up/downcasing something that includes this module returns its # Up/downcased to_s form # def downcase self.irc_downcase end # We allow up/downcasing with a different casemap # def irc_upcase(cmap=casemap) self.to_s.irc_upcase(cmap) end # Up/downcasing something that includes this module returns its # Up/downcased to_s form # def upcase self.irc_upcase end end end # We start by extending the String class # with some IRC-specific methods # class String # This method returns the Irc::Casemap whose name is the receiver # def to_irc_casemap begin Irc::Casemap.get(self) rescue # raise TypeError, "Unkown Irc::Casemap #{self.inspect}" error "Unkown Irc::Casemap #{self.inspect} requested, defaulting to rfc1459" Irc::Casemap.get('rfc1459') end end # This method returns a string which is the downcased version of the # receiver, according to the given _casemap_ # # def irc_downcase(casemap='rfc1459') cmap = casemap.to_irc_casemap self.tr(cmap.upper, cmap.lower) end # This is the same as the above, except that the string is altered in place # # See also the discussion about irc_downcase # def irc_downcase!(casemap='rfc1459') cmap = casemap.to_irc_casemap self.tr!(cmap.upper, cmap.lower) end # Upcasing functions are provided too # # See also the discussion about irc_downcase # def irc_upcase(casemap='rfc1459') cmap = casemap.to_irc_casemap self.tr(cmap.lower, cmap.upper) end # In-place upcasing # # See also the discussion about irc_downcase # def irc_upcase!(casemap='rfc1459') cmap = casemap.to_irc_casemap self.tr!(cmap.lower, cmap.upper) end # This method checks if the receiver contains IRC glob characters # # IRC has a very primitive concept of globs: a * stands for "any # number of arbitrary characters", a ? stands for "one and exactly # one arbitrary character". These characters can be escaped by prefixing them # with a slash (\\). # # A known limitation of this glob syntax is that there is no way to escape # the escape character itself, so it's not possible to build a glob pattern # where the escape character precedes a glob. # def has_irc_glob? self =~ /^[*?]|[^\\][*?]/ end # This method is used to convert the receiver into a Regular Expression # that matches according to the IRC glob syntax # def to_irc_regexp regmask = Regexp.escape(self) regmask.gsub!(/(\\\\)?\\[*?]/) { |m| case m when /\\(\\[*?])/ $1 when /\\\*/ '.*' when /\\\?/ '.' else raise "Unexpected match #{m} when converting #{self}" end } Regexp.new("^#{regmask}$") end end # ArrayOf is a subclass of Array whose elements are supposed to be all # of the same class. This is not intended to be used directly, but rather # to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList) # # Presently, only very few selected methods from Array are overloaded to check # if the new elements are the correct class. An orthodox? method is provided # to check the entire ArrayOf against the appropriate class. # class ArrayOf < Array attr_reader :element_class # Create a new ArrayOf whose elements are supposed to be all of type _kl_, # optionally filling it with the elements from the Array argument. # def initialize(kl, ar=[]) raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class) super() @element_class = kl case ar when Array insert(0, *ar) else raise TypeError, "#{self.class} can only be initialized from an Array" end end def inspect self.__to_s__[0..-2].sub(/:[^:]+$/,"[#{@element_class}]\\0") + " #{super}>" end # Private method to check the validity of the elements passed to it # and optionally raise an error # # TODO should it accept nils as valid? # def internal_will_accept?(raising, *els) els.each { |el| unless el.kind_of?(@element_class) raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising return false end } return true end private :internal_will_accept? # This method checks if the passed arguments are acceptable for our ArrayOf # def will_accept?(*els) internal_will_accept?(false, *els) end # This method checks that all elements are of the appropriate class # def valid? will_accept?(*self) end # This method is similar to the above, except that it raises an exception # if the receiver is not valid # def validate raise TypeError unless valid? end # Overloaded from Array#<<, checks for appropriate class of argument # def <<(el) super(el) if internal_will_accept?(true, el) end # Overloaded from Array#&, checks for appropriate class of argument elements # def &(ar) r = super(ar) ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r) end # Overloaded from Array#+, checks for appropriate class of argument elements # def +(ar) ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar) end # Overloaded from Array#-, so that an ArrayOf is returned. There is no need # to check the validity of the elements in the argument # def -(ar) ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar) end # Overloaded from Array#|, checks for appropriate class of argument elements # def |(ar) ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar) end # Overloaded from Array#concat, checks for appropriate class of argument # elements # def concat(ar) super(ar) if internal_will_accept?(true, *ar) end # Overloaded from Array#insert, checks for appropriate class of argument # elements # def insert(idx, *ar) super(idx, *ar) if internal_will_accept?(true, *ar) end # Overloaded from Array#replace, checks for appropriate class of argument # elements # def replace(ar) super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar) end # Overloaded from Array#push, checks for appropriate class of argument # elements # def push(*ar) super(*ar) if internal_will_accept?(true, *ar) end # Overloaded from Array#unshift, checks for appropriate class of argument(s) # def unshift(*els) els.each { |el| super(el) if internal_will_accept?(true, *els) } end # We introduce the 'downcase' method, which maps downcase() to all the Array # elements, properly failing when the elements don't have a downcase method # def downcase self.map { |el| el.downcase } end # Modifying methods which we don't handle yet are made private # private :[]=, :collect!, :map!, :fill, :flatten! end # We extend the Regexp class with an Irc module which will contain some # Irc-specific regexps # class Regexp # We start with some general-purpose ones which will be used in the # Irc module too, but are useful regardless DIGITS = /\d+/ HEX_DIGIT = /[0-9A-Fa-f]/ HEX_DIGITS = /#{HEX_DIGIT}+/ HEX_OCTET = /#{HEX_DIGIT}#{HEX_DIGIT}?/ DEC_OCTET = /[01]?\d?\d|2[0-4]\d|25[0-5]/ DEC_IP_ADDR = /#{DEC_OCTET}\.#{DEC_OCTET}\.#{DEC_OCTET}\.#{DEC_OCTET}/ HEX_IP_ADDR = /#{HEX_OCTET}\.#{HEX_OCTET}\.#{HEX_OCTET}\.#{HEX_OCTET}/ IP_ADDR = /#{DEC_IP_ADDR}|#{HEX_IP_ADDR}/ # IPv6, from Resolv::IPv6, without the \A..\z anchors HEX_16BIT = /#{HEX_DIGIT}{1,4}/ IP6_8Hex = /(?:#{HEX_16BIT}:){7}#{HEX_16BIT}/ IP6_CompressedHex = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)/ IP6_6Hex4Dec = /((?:#{HEX_16BIT}:){6,6})#{DEC_IP_ADDR}/ IP6_CompressedHex4Dec = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}:)*)#{DEC_IP_ADDR}/ IP6_ADDR = /(?:#{IP6_8Hex})|(?:#{IP6_CompressedHex})|(?:#{IP6_6Hex4Dec})|(?:#{IP6_CompressedHex4Dec})/ # We start with some IRC related regular expressions, used to match # Irc::User nicks and users and Irc::Channel names # # For each of them we define two versions of the regular expression: # * a generic one, which should match for any server but may turn out to # match more than a specific server would accept # * an RFC-compliant matcher # module Irc # Channel-name-matching regexps CHAN_FIRST = /[#&+]/ CHAN_SAFE = /![A-Z0-9]{5}/ CHAN_ANY = /[^\x00\x07\x0A\x0D ,:]/ GEN_CHAN = /(?:#{CHAN_FIRST}|#{CHAN_SAFE})#{CHAN_ANY}+/ RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/ # Nick-matching regexps SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/ NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/ NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/ GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/ RFC_NICK = /#{NICK_FIRST}#{NICK_ANY}{0,8}/ USER_CHAR = /[^\x00\x0a\x0d @]/ GEN_USER = /#{USER_CHAR}+/ # Host-matching regexps HOSTNAME_COMPONENT = /[[:alnum:]](?:[[:alnum:]]|-)*[[:alnum:]]*/ HOSTNAME = /#{HOSTNAME_COMPONENT}(?:\.#{HOSTNAME_COMPONENT})*/ HOSTADDR = /#{IP_ADDR}|#{IP6_ADDR}/ GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/ # # FreeNode network replaces the host of affiliated users with # # 'virtual hosts' # # FIXME we need the true syntax to match it properly ... # PDPC_HOST_PART = /[0-9A-Za-z.-]+/ # PDPC_HOST = /#{PDPC_HOST_PART}(?:\/#{PDPC_HOST_PART})+/ # # NOTE: the final optional and non-greedy dot is needed because some # # servers (e.g. FreeNode) send the hostname of the services as "services." # # which is not RFC compliant, but sadly done. # GEN_HOST_EXT = /#{PDPC_HOST}|#{GEN_HOST}\.??/ # Sadly, different networks have different, RFC-breaking ways of cloaking # the actualy host address: see above for an example to handle FreeNode. # Another example would be Azzurra, wich also inserts a "=" in the # cloacked host. So let's just not care about this and go with the simplest # thing: GEN_HOST_EXT = /\S+/ # User-matching Regexp GEN_USER_ID = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/ # Things such has the BIP proxy send invalid nicks in a complete netmask, # so we want to match this, rather: this matches either a compliant nick # or a a string with a very generic nick, a very generic hostname after an # @ sign, and an optional user after a ! BANG_AT = /#{GEN_NICK}|\S+?(?:!\S+?)?@\S+?/ # # For Netmask, we want to allow wildcards * and ? in the nick # # (they are already allowed in the user and host part # GEN_NICK_MASK = /(?:#{NICK_FIRST}|[?*])?(?:#{NICK_ANY}|[?*])+/ # # Netmask-matching Regexp # GEN_MASK = /(#{GEN_NICK_MASK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/ end end module Irc # A Netmask identifies each user by collecting its nick, username and # hostname in the form nick!user@host # # Netmasks can also contain glob patterns in any of their components; in # this form they are used to refer to more than a user or to a user # appearing under different forms. # # Example: # * *!*@* refers to everybody # * *!someuser@somehost refers to user +someuser+ on host +somehost+ # regardless of the nick used. # class Netmask # Netmasks have an associated casemap unless they are bound to a server # include ServerOrCasemap attr_reader :nick, :user, :host alias :ident :user # Create a new Netmask from string _str_, which must be in the form # _nick_!_user_@_host_ # # It is possible to specify a server or a casemap in the optional Hash: # these are used to associate the Netmask with the given server and to set # its casemap: if a server is specified and a casemap is not, the server's # casemap is used. If both a server and a casemap are specified, the # casemap must match the server's casemap or an exception will be raised. # # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern # def initialize(str="", opts={}) # First of all, check for server/casemap option # init_server_or_casemap(opts) # Now we can see if the given string _str_ is an actual Netmask if str.respond_to?(:to_str) case str.to_str # We match a pretty generic string, to work around non-compliant # servers when /^(?:(\S+?)(?:(?:!(\S+?))?@(\S+))?)?$/ # We do assignment using our internal methods self.nick = $1 self.user = $2 self.host = $3 else raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}" end else raise TypeError, "#{str} cannot be converted to a #{self.class}" end end # A Netmask is easily converted to a String for the usual representation. # We skip the user or host parts if they are "*", unless we've been asked # for the full form # def to_s ret = nick.dup ret << "!" << user unless user == "*" ret << "@" << host unless host == "*" return ret end def fullform "#{nick}!#{user}@#{host}" end alias :to_str :fullform # This method downcases the fullform of the netmask. While this may not be # significantly different from the #downcase() method provided by the # ServerOrCasemap mixin, it's significantly different for Netmask # subclasses such as User whose simple downcasing uses the nick only. # def full_irc_downcase(cmap=casemap) self.fullform.irc_downcase(cmap) end # full_downcase() will return the fullform downcased according to the # User's own casemap # def full_downcase self.full_irc_downcase end # This method returns a new Netmask which is the fully downcased version # of the receiver def downcased return self.full_downcase.to_irc_netmask(server_and_casemap) end # Converts the receiver into a Netmask with the given (optional) # server/casemap association. We return self unless a conversion # is needed (different casemap/server) # # Subclasses of Netmask will return a new Netmask, using full_downcase # def to_irc_netmask(opts={}) if self.class == Netmask return self if fits_with_server_and_casemap?(opts) end return self.full_downcase.to_irc_netmask(server_and_casemap.merge(opts)) end # Converts the receiver into a User with the given (optional) # server/casemap association. We return self unless a conversion # is needed (different casemap/server) # def to_irc_user(opts={}) self.fullform.to_irc_user(server_and_casemap.merge(opts)) end # Inspection of a Netmask reveals the server it's bound to (if there is # one), its casemap and the nick, user and host part # def inspect str = self.__to_s__[0..-2] str << " @server=#{@server}" if defined?(@server) and @server str << " @nick=#{@nick.inspect} @user=#{@user.inspect}" str << " @host=#{@host.inspect} casemap=#{casemap.inspect}" str << ">" end # Equality: two Netmasks are equal if they downcase to the same thing # # TODO we may want it to try other.to_irc_netmask # def ==(other) return false unless other.kind_of?(self.class) self.downcase == other.downcase end # This method changes the nick of the Netmask, defaulting to the generic # glob pattern if the result is the null string. # def nick=(newnick) @nick = newnick.to_s @nick = "*" if @nick.empty? end # This method changes the user of the Netmask, defaulting to the generic # glob pattern if the result is the null string. # def user=(newuser) @user = newuser.to_s @user = "*" if @user.empty? end alias :ident= :user= # This method changes the hostname of the Netmask, defaulting to the generic # glob pattern if the result is the null string. # def host=(newhost) @host = newhost.to_s @host = "*" if @host.empty? end # We can replace everything at once with data from another Netmask # def replace(other) case other when Netmask nick = other.nick user = other.user host = other.host @server = other.server @casemap = other.casemap unless @server else replace(other.to_irc_netmask(server_and_casemap)) end end # This method checks if a Netmask is definite or not, by seeing if # any of its components are defined by globs # def has_irc_glob? return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob? end def generalize u = user.dup unless u.has_irc_glob? u.sub!(/^[in]=/, '=') or u.sub!(/^\W(\w+)/, '\1') u = '*' + u end h = host.dup unless h.has_irc_glob? if h.include? '/' h.sub!(/x-\w+$/, 'x-*') else h.match(/^[^\.]+\.[^\.]+$/) or h.sub!(/azzurra[=-][0-9a-f]+/i, '*') or # hello, azzurra, you suck! h.sub!(/^(\d+\.\d+\.\d+\.)\d+$/, '\1*') or h.sub!(/^[^\.]+\./, '*.') end end return Netmask.new("*!#{u}@#{h}", server_and_casemap) end # This method is used to match the current Netmask against another one # # The method returns true if each component of the receiver matches the # corresponding component of the argument. By _matching_ here we mean # that any netmask described by the receiver is also described by the # argument. # # In this sense, matching is rather simple to define in the case when the # receiver has no globs: it is just necessary to check if the argument # describes the receiver, which can be done by matching it against the # argument converted into an IRC Regexp (see String#to_irc_regexp). # # The situation is also easy when the receiver has globs and the argument # doesn't, since in this case the result is false. # # The more complex case in which both the receiver and the argument have # globs is not handled yet. # def matches?(arg) cmp = arg.to_irc_netmask(:casemap => casemap) debug "Matching #{self.fullform} against #{arg.inspect} (#{cmp.fullform})" [:nick, :user, :host].each { |component| us = self.send(component).irc_downcase(casemap) them = cmp.send(component).irc_downcase(casemap) if us.has_irc_glob? && them.has_irc_glob? next if us == them warn NotImplementedError return false end return false if us.has_irc_glob? && !them.has_irc_glob? return false unless us =~ them.to_irc_regexp } return true end # Case equality. Checks if arg matches self # def ===(arg) arg.to_irc_netmask(:casemap => casemap).matches?(self) end # Sorting is done via the fullform # def <=>(arg) case arg when Netmask self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap) else self.downcase <=> arg.downcase end end end # A NetmaskList is an ArrayOf Netmasks # class NetmaskList < ArrayOf # Create a new NetmaskList, optionally filling it with the elements from # the Array argument fed to it. # def initialize(ar=[]) super(Netmask, ar) end # We enhance the [] method by allowing it to pick an element that matches # a given Netmask, a String or a Regexp # TODO take into consideration the opportunity to use select() instead of # find(), and/or a way to let the user choose which one to take (second # argument?) # def [](*args) if args.length == 1 case args[0] when Netmask self.find { |mask| mask.matches?(args[0]) } when String self.find { |mask| mask.matches?(args[0].to_irc_netmask(:casemap => mask.casemap)) } when Regexp self.find { |mask| mask.fullform =~ args[0] } else super(*args) end else super(*args) end end end end class String # We keep extending String, this time adding a method that converts a # String into an Irc::Netmask object # def to_irc_netmask(opts={}) Irc::Netmask.new(self, opts) end end module Irc # An IRC User is identified by his/her Netmask (which must not have globs). # In fact, User is just a subclass of Netmask. # # Ideally, the user and host information of an IRC User should never # change, and it shouldn't contain glob patterns. However, IRC is somewhat # idiosincratic and it may be possible to know the nick of a User much before # its user and host are known. Moreover, some networks (namely Freenode) may # change the hostname of a User when (s)he identifies with Nickserv. # # As a consequence, we must allow changes to a User host and user attributes. # We impose a restriction, though: they may not contain glob patterns, except # for the special case of an unknown user/host which is represented by a *. # # It is possible to create a totally unknown User (e.g. for initializations) # by setting the nick to * too. # # TODO list: # * see if it's worth to add the other USER data # * see if it's worth to add NICKSERV status # class User < Netmask alias :to_s :nick attr_accessor :real_name, :idle_since, :signon # Create a new IRC User from a given Netmask (or anything that can be converted # into a Netmask) provided that the given Netmask does not have globs. # def initialize(str="", opts={}) super raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*" raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*" raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*" @away = false @real_name = String.new @idle_since = nil @signon = nil end # The nick of a User may be changed freely, but it must not contain glob patterns. # def nick=(newnick) raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob? super end # We have to allow changing the user of an Irc User due to some networks # (e.g. Freenode) changing hostmasks on the fly. We still check if the new # user data has glob patterns though. # def user=(newuser) raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob? super end # We have to allow changing the host of an Irc User due to some networks # (e.g. Freenode) changing hostmasks on the fly. We still check if the new # host data has glob patterns though. # def host=(newhost) raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob? super end # Checks if a User is well-known or not by looking at the hostname and user # def known? return nick != "*" && user != "*" && host != "*" end # Is the user away? # def away? return @away end # Set the away status of the user. Use away=(nil) or away=(false) # to unset away # def away=(msg="") if msg @away = msg else @away = false end end # Since to_irc_user runs the same checks on server and channel as # to_irc_netmask, we just try that and return self if it works. # # Subclasses of User will return self if possible. # def to_irc_user(opts={}) return self if fits_with_server_and_casemap?(opts) return self.full_downcase.to_irc_user(opts) end # We can replace everything at once with data from another User # def replace(other) case other when User self.nick = other.nick self.user = other.user self.host = other.host @server = other.server @casemap = other.casemap unless @server @away = other.away? else self.replace(other.to_irc_user(server_and_casemap)) end end def modes_on(channel) case channel when Channel channel.modes_of(self) else return @server.channel(channel).modes_of(self) if @server raise "Can't resolve channel #{channel}" end end def is_op?(channel) case channel when Channel channel.has_op?(self) else return @server.channel(channel).has_op?(self) if @server raise "Can't resolve channel #{channel}" end end def is_voice?(channel) case channel when Channel channel.has_voice?(self) else return @server.channel(channel).has_voice?(self) if @server raise "Can't resolve channel #{channel}" end end def channels if @server @server.channels.select { |ch| ch.has_user?(self) } else Array.new end end end # A UserList is an ArrayOf Users # We derive it from NetmaskList, which allows us to inherit any special # NetmaskList method # class UserList < NetmaskList # Create a new UserList, optionally filling it with the elements from # the Array argument fed to it. # def initialize(ar=[]) super(ar) @element_class = User end # Convenience method: convert the UserList to a list of nicks. The indices # are preserved # def nicks self.map { |user| user.nick } end end end class String # We keep extending String, this time adding a method that converts a # String into an Irc::User object # def to_irc_user(opts={}) Irc::User.new(self, opts) end end module Irc # An IRC Channel is identified by its name, and it has a set of properties: # * a Channel::Topic # * a UserList # * a set of Channel::Modes # # The Channel::Topic and Channel::Mode classes are defined within the # Channel namespace because they only make sense there # class Channel # Mode on a Channel # class Mode attr_reader :channel def initialize(ch) @channel = ch end end # Hash of modes. Subclass of Hash that defines any? and all? # to check if boolean modes (Type D) are set class ModeHash < Hash def any?(*ar) !!ar.find { |m| s = m.to_sym ; self[s] && self[s].set? } end def all?(*ar) !ar.find { |m| s = m.to_sym ; !(self[s] && self[s].set?) } end end # Channel modes of type A manipulate lists # # Example: b (banlist) # class ModeTypeA < Mode attr_reader :list def initialize(ch) super @list = NetmaskList.new end def set(val) nm = @channel.server.new_netmask(val) @list << nm unless @list.include?(nm) end def reset(val) nm = @channel.server.new_netmask(val) @list.delete(nm) end end # Channel modes of type B need an argument # # Example: k (key) # class ModeTypeB < Mode def initialize(ch) super @arg = nil end def status @arg end alias :value :status def set(val) @arg = val end def reset(val) @arg = nil if @arg == val end end # Channel modes that change the User prefixes are like # Channel modes of type B, except that they manipulate # lists of Users, so they are somewhat similar to channel # modes of type A # class UserMode < ModeTypeB attr_reader :list alias :users :list def initialize(ch) super @list = UserList.new end def set(val) u = @channel.server.user(val) @list << u unless @list.include?(u) end def reset(val) u = @channel.server.user(val) @list.delete(u) end end # Channel modes of type C need an argument when set, # but not when they get reset # # Example: l (limit) # class ModeTypeC < Mode def initialize(ch) super @arg = nil end def status @arg end alias :value :status def set(val) @arg = val end def reset @arg = nil end end # Channel modes of type D are basically booleans # # Example: m (moderate) # class ModeTypeD < Mode def initialize(ch) super @set = false end def set? return @set end def set @set = true end def reset @set = false end end # A Topic represents the topic of a channel. It consists of # the topic itself, who set it and when # class Topic attr_accessor :text, :set_by, :set_on alias :to_s :text # Create a new Topic setting the text, the creator and # the creation time # def initialize(text="", set_by="", set_on=Time.new) @text = text @set_by = set_by.to_irc_netmask @set_on = set_on end # Replace a Topic with another one # def replace(topic) raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class) @text = topic.text.dup @set_by = topic.set_by.dup @set_on = topic.set_on.dup end # Returns self # def to_irc_channel_topic self end end end end class String # Returns an Irc::Channel::Topic with self as text # def to_irc_channel_topic Irc::Channel::Topic.new(self) end end module Irc # Here we start with the actual Channel class # class Channel # Return the non-prefixed part of a channel name. # Also works with ## channels found on some networks # (e.g. FreeNode) def self.npname(str) return str.to_s.sub(/^[&#+!]+/,'') end include ServerOrCasemap attr_reader :name, :topic, :mode, :users alias :to_s :name attr_accessor :creation_time, :url def inspect str = self.__to_s__[0..-2] str << " on server #{server}" if server str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}" str << " @users=[#{user_nicks.sort.join(', ')}]" str << " (created on #{creation_time})" if creation_time str << " (URL #{url})" if url str << ">" end # Returns self # def to_irc_channel self end # TODO Ho def user_nicks @users.map { |u| u.downcase } end # Checks if the receiver already has a user with the given _nick_ # def has_user?(nick) @users.index(nick.to_irc_user(server_and_casemap)) end # Returns the user with nick _nick_, if available # def get_user(nick) idx = has_user?(nick) @users[idx] if idx end # Adds a user to the channel # def add_user(user, opts={}) silent = opts.fetch(:silent, false) if has_user?(user) warn "Trying to add user #{user} to channel #{self} again" unless silent else @users << user.to_irc_user(server_and_casemap) end end # Creates a new channel with the given name, optionally setting the topic # and an initial users list. # # No additional info is created here, because the channel flags and userlists # allowed depend on the server. # def initialize(name, topic=nil, users=[], opts={}) raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty? warn "Unknown channel prefix #{name[0,1]}" if name !~ /^[&#+!]/ raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/ init_server_or_casemap(opts) @name = name @topic = topic ? topic.to_irc_channel_topic : Channel::Topic.new @users = UserList.new users.each { |u| add_user(u) } # Flags @mode = ModeHash.new # creation time, only on some networks @creation_time = nil # URL, only on some networks @url = nil end # Removes a user from the channel # def delete_user(user) @mode.each { |sym, mode| mode.reset(user) if mode.kind_of?(UserMode) } @users.delete(user) end # The channel prefix # def prefix name[0,1] end # A channel is local to a server if it has the '&' prefix # def local? name[0,1] == '&' end # A channel is modeless if it has the '+' prefix # def modeless? name[0,1] == '+' end # A channel is safe if it has the '!' prefix # def safe? name[0,1] == '!' end # A channel is normal if it has the '#' prefix # def normal? name[0,1] == '#' end # Create a new mode # def create_mode(sym, kl) @mode[sym.to_sym] = kl.new(self) end def modes_of(user) l = [] @mode.map { |s, m| l << s if (m.class <= UserMode and m.list[user]) } l end def has_op?(user) @mode.has_key?(:o) and @mode[:o].list[user] end def has_voice?(user) @mode.has_key?(:v) and @mode[:v].list[user] end end # A ChannelList is an ArrayOf Channels # class ChannelList < ArrayOf # Create a new ChannelList, optionally filling it with the elements from # the Array argument fed to it. # def initialize(ar=[]) super(Channel, ar) end # Convenience method: convert the ChannelList to a list of channel names. # The indices are preserved # def names self.map { |chan| chan.name } end end end class String # We keep extending String, this time adding a method that converts a # String into an Irc::Channel object # def to_irc_channel(opts={}) Irc::Channel.new(self, opts) end end module Irc # An IRC Server represents the Server the client is connected to. # class Server attr_reader :hostname, :version, :usermodes, :chanmodes alias :to_s :hostname attr_reader :supports, :capabilities attr_reader :channels, :users # TODO Ho def channel_names @channels.map { |ch| ch.downcase } end # TODO Ho def user_nicks @users.map { |u| u.downcase } end def inspect chans, users = [@channels, @users].map {|d| d.sort { |a, b| a.downcase <=> b.downcase }.map { |x| x.inspect } } str = self.__to_s__[0..-2] str << " @hostname=#{hostname}" str << " @channels=#{chans}" str << " @users=#{users}" str << ">" end # Create a new Server, with all instance variables reset to nil (for # scalar variables), empty channel and user lists and @supports # initialized to the default values for all known supported features. # def initialize @hostname = @version = @usermodes = @chanmodes = nil @channels = ChannelList.new @users = UserList.new reset_capabilities end # Resets the server capabilities # def reset_capabilities @supports = { :casemapping => 'rfc1459'.to_irc_casemap, :chanlimit => {}, :chanmodes => { :typea => nil, # Type A: address lists :typeb => nil, # Type B: needs a parameter :typec => nil, # Type C: needs a parameter when set :typed => nil # Type D: must not have a parameter }, :channellen => 50, :chantypes => "#&!+", :excepts => nil, :idchan => {}, :invex => nil, :kicklen => nil, :maxlist => {}, :modes => 3, :network => nil, :nicklen => 9, :prefix => { :modes => [:o, :v], :prefixes => [:"@", :+] }, :safelist => nil, :statusmsg => nil, :std => nil, :targmax => {}, :topiclen => nil } @capabilities = {} end # Convert a mode (o, v, h, ...) to the corresponding # prefix (@, +, %, ...). See also mode_for_prefix def prefix_for_mode(mode) return @supports[:prefix][:prefixes][ @supports[:prefix][:modes].index(mode.to_sym) ] end # Convert a prefix (@, +, %, ...) to the corresponding # mode (o, v, h, ...). See also prefix_for_mode def mode_for_prefix(pfx) return @supports[:prefix][:modes][ @supports[:prefix][:prefixes].index(pfx.to_sym) ] end # Resets the Channel and User list # def reset_lists @users.reverse_each { |u| delete_user(u) } @channels.reverse_each { |u| delete_channel(u) } end # Clears the server # def clear reset_lists reset_capabilities @hostname = @version = @usermodes = @chanmodes = nil end # This method is used to parse a 004 RPL_MY_INFO line # def parse_my_info(line) ar = line.split(' ') @hostname = ar[0] @version = ar[1] @usermodes = ar[2] @chanmodes = ar[3] end def noval_warn(key, val, &block) if val yield if block_given? else warn "No #{key.to_s.upcase} value" end end def val_warn(key, val, &block) if val == true or val == false or val.nil? yield if block_given? else warn "No #{key.to_s.upcase} value must be specified, got #{val}" end end private :noval_warn, :val_warn # This method is used to parse a 005 RPL_ISUPPORT line # # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt] # def parse_isupport(line) debug "Parsing ISUPPORT #{line.inspect}" ar = line.split(' ') reparse = [] ar.each { |en| prekey, val = en.split('=', 2) if prekey =~ /^-(.*)/ key = $1.downcase.to_sym val = false else key = prekey.downcase.to_sym end case key when :casemapping noval_warn(key, val) { if val == 'charset' reparse << "CASEMAPPING=(charset)" else # TODO some servers offer non-standard CASEMAPPINGs in the form # locale.charset[-options], which indicate an extended set of # allowed characters (mostly for nicks). This might be supported # with hooks for the unicode core module @supports[key] = val.to_irc_casemap end } when :chanlimit, :idchan, :maxlist, :targmax noval_warn(key, val) { groups = val.split(',') groups.each { |g| k, v = g.split(':') @supports[key][k] = v.to_i || 0 if @supports[key][k] == 0 warn "Deleting #{key} limit of 0 for #{k}" @supports[key].delete(k) end } } when :chanmodes noval_warn(key, val) { groups = val.split(',') @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym} @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym} @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym} @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym} } when :channellen, :kicklen, :modes, :topiclen if val @supports[key] = val.to_i else @supports[key] = nil end when :chantypes @supports[key] = val # can also be nil when :excepts val ||= 'e' @supports[key] = val when :invex val ||= 'I' @supports[key] = val when :maxchannels noval_warn(key, val) { reparse << "CHANLIMIT=(chantypes):#{val} " } when :maxtargets noval_warn(key, val) { @supports[:targmax]['PRIVMSG'] = val.to_i @supports[:targmax]['NOTICE'] = val.to_i } when :network noval_warn(key, val) { @supports[key] = val } when :nicklen noval_warn(key, val) { @supports[key] = val.to_i } when :prefix if val val.scan(/\((.*)\)(.*)/) { |m, p| @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym} @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym} } else @supports[key][:modes] = nil @supports[key][:prefixes] = nil end when :safelist val_warn(key, val) { @supports[key] = val.nil? ? true : val } when :statusmsg noval_warn(key, val) { @supports[key] = val.scan(/./) } when :std noval_warn(key, val) { @supports[key] = val.split(',') } else @supports[key] = val.nil? ? true : val end } unless reparse.empty? reparse_str = reparse.join(" ") reparse_str.gsub!("(chantypes)",@supports[:chantypes]) reparse_str.gsub!("(charset)",@supports[:charset] || 'rfc1459') parse_isupport(reparse_str) end end # Returns the casemap of the server. # def casemap @supports[:casemapping] end # Returns User or Channel depending on what _name_ can be # a name of # def user_or_channel?(name) if supports[:chantypes].include?(name[0]) return Channel else return User end end # Returns the actual User or Channel object matching _name_ # def user_or_channel(name) if supports[:chantypes].include?(name[0]) return channel(name) else return user(name) end end # Checks if the receiver already has a channel with the given _name_ # def has_channel?(name) return false if name.nil_or_empty? channel_names.index(name.irc_downcase(casemap)) end alias :has_chan? :has_channel? # Returns the channel with name _name_, if available # def get_channel(name) return nil if name.nil_or_empty? idx = has_channel?(name) channels[idx] if idx end alias :get_chan :get_channel # Create a new Channel object bound to the receiver and add it to the # list of Channels on the receiver, unless the channel was # present already. In this case, the default action is to raise an # exception, unless _fails_ is set to false. An exception can also be # raised if _str_ is nil or empty, again only if _fails_ is set to true; # otherwise, the method just returns nil # def new_channel(name, topic=nil, users=[], fails=true) if name.nil_or_empty? raise "Tried to look for empty or nil channel name #{name.inspect}" if fails return nil end ex = get_chan(name) if ex raise "Channel #{name} already exists on server #{self}" if fails return ex else prefix = name[0,1] # Give a warning if the new Channel goes over some server limits. # # FIXME might need to raise an exception # warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix) warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[:channellen]})" unless name.length <= @supports[:channellen] # Next, we check if we hit the limit for channels of type +prefix+ # if the server supports +chanlimit+ # @supports[:chanlimit].keys.each { |k| next unless k.include?(prefix) count = 0 channel_names.each { |n| count += 1 if k.include?(n[0]) } # raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k] warn "Already joined #{count}/#{@supports[:chanlimit][k]} channels with prefix #{k}, we may be going over server limits" if count >= @supports[:chanlimit][k] } # So far, everything is fine. Now create the actual Channel # chan = Channel.new(name, topic, users, :server => self) # We wade through +prefix+ and +chanmodes+ to create appropriate # lists and flags for this channel @supports[:prefix][:modes].each { |mode| chan.create_mode(mode, Channel::UserMode) } if @supports[:prefix][:modes] @supports[:chanmodes].each { |k, val| if val case k when :typea val.each { |mode| chan.create_mode(mode, Channel::ModeTypeA) } when :typeb val.each { |mode| chan.create_mode(mode, Channel::ModeTypeB) } when :typec val.each { |mode| chan.create_mode(mode, Channel::ModeTypeC) } when :typed val.each { |mode| chan.create_mode(mode, Channel::ModeTypeD) } end end } @channels << chan # debug "Created channel #{chan.inspect}" return chan end end # Returns the Channel with the given _name_ on the server, # creating it if necessary. This is a short form for # new_channel(_str_, nil, [], +false+) # def channel(str) new_channel(str,nil,[],false) end # Remove Channel _name_ from the list of Channels # def delete_channel(name) idx = has_channel?(name) raise "Tried to remove unmanaged channel #{name}" unless idx @channels.delete_at(idx) end # Checks if the receiver already has a user with the given _nick_ # def has_user?(nick) return false if nick.nil_or_empty? user_nicks.index(nick.irc_downcase(casemap)) end # Returns the user with nick _nick_, if available # def get_user(nick) idx = has_user?(nick) @users[idx] if idx end # Create a new User object bound to the receiver and add it to the list # of Users on the receiver, unless the User was present # already. In this case, the default action is to raise an exception, # unless _fails_ is set to false. An exception can also be raised # if _str_ is nil or empty, again only if _fails_ is set to true; # otherwise, the method just returns nil # def new_user(str, fails=true) if str.nil_or_empty? raise "Tried to look for empty or nil user name #{str.inspect}" if fails return nil end tmp = str.to_irc_user(:server => self) old = get_user(tmp.nick) # debug "Tmp: #{tmp.inspect}" # debug "Old: #{old.inspect}" if old # debug "User already existed as #{old.inspect}" if tmp.known? if old.known? # debug "Both were known" # Do not raise an error: things like Freenode change the hostname after identification warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp raise "User #{tmp} already exists on server #{self}" if fails end if old.fullform.downcase != tmp.fullform.downcase old.replace(tmp) # debug "Known user now #{old.inspect}" end end return old else warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen] @users << tmp return @users.last end end # Returns the User with the given Netmask on the server, # creating it if necessary. This is a short form for # new_user(_str_, +false+) # def user(str) new_user(str, false) end # Deletes User _user_ from Channel _channel_ # def delete_user_from_channel(user, channel) channel.delete_user(user) end # Remove User _someuser_ from the list of Users. # _someuser_ must be specified with the full Netmask. # def delete_user(someuser) idx = has_user?(someuser) raise "Tried to remove unmanaged user #{user}" unless idx have = self.user(someuser) @channels.each { |ch| delete_user_from_channel(have, ch) } @users.delete_at(idx) end # Create a new Netmask object with the appropriate casemap # def new_netmask(str) str.to_irc_netmask(:server => self) end # Finds all Users on server whose Netmask matches _mask_ # def find_users(mask) nm = new_netmask(mask) @users.inject(UserList.new) { |list, user| if user.user == "*" or user.host == "*" list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp else list << user if user.matches?(nm) end list } end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/ircsocket.rb0000644002342000234200000002666111411605044021326 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: IRC Socket # # This module implements the IRC socket interface, including IRC message # penalty computation and the message queue system require 'monitor' class ::String # Calculate the penalty which will be assigned to this message # by the IRCd def irc_send_penalty # According to eggdrop, the initial penalty is penalty = 1 + self.size/100 # on everything but UnderNET where it's # penalty = 2 + self.size/120 cmd, pars = self.split($;,2) debug "cmd: #{cmd}, pars: #{pars.inspect}" case cmd.to_sym when :KICK chan, nick, msg = pars.split chan = chan.split(',') nick = nick.split(',') penalty += nick.size penalty *= chan.size when :MODE chan, modes, argument = pars.split extra = 0 if modes extra = 1 if argument extra += modes.split(/\+|-/).size else extra += 3 * modes.split(/\+|-/).size end end if argument extra += 2 * argument.split.size end penalty += extra * chan.split.size when :TOPIC penalty += 1 penalty += 2 unless pars.split.size < 2 when :PRIVMSG, :NOTICE dests = pars.split($;,2).first penalty += dests.split(',').size when :WHO args = pars.split if args.length > 0 penalty += args.inject(0){ |sum,x| sum += ((x.length > 4) ? 3 : 5) } else penalty += 10 end when :PART penalty += 4 when :AWAY, :JOIN, :VERSION, :TIME, :TRACE, :WHOIS, :DNS penalty += 2 when :INVITE, :NICK penalty += 3 when :ISON penalty += 1 else # Unknown messages penalty += 1 end if penalty > 99 debug "Wow, more than 99 secs of penalty!" penalty = 99 end if penalty < 2 debug "Wow, less than 2 secs of penalty!" penalty = 2 end debug "penalty: #{penalty}" return penalty end end module Irc require 'socket' require 'thread' class QueueRing # A QueueRing is implemented as an array with elements in the form # [chan, [message1, message2, ...] # Note that the channel +chan+ has no actual bearing with the channels # to which messages will be sent def initialize @storage = Array.new @last_idx = -1 end def clear @storage.clear @last_idx = -1 end def length len = 0 @storage.each {|c| len += c[1].size } return len end alias :size :length def empty? @storage.empty? end def push(mess, chan) cmess = @storage.assoc(chan) if cmess idx = @storage.index(cmess) cmess[1] << mess @storage[idx] = cmess else @storage << [chan, [mess]] end end def next if empty? warning "trying to access empty ring" return nil end save_idx = @last_idx @last_idx = (@last_idx + 1) % @storage.size mess = @storage[@last_idx][1].first @last_idx = save_idx return mess end def shift if empty? warning "trying to access empty ring" return nil end @last_idx = (@last_idx + 1) % @storage.size mess = @storage[@last_idx][1].shift @storage.delete(@storage[@last_idx]) if @storage[@last_idx][1] == [] return mess end end class MessageQueue def initialize # a MessageQueue is an array of QueueRings # rings have decreasing priority, so messages in ring 0 # are more important than messages in ring 1, and so on @rings = Array.new(3) { |i| if i > 0 QueueRing.new else # ring 0 is special in that if it's not empty, it will # be popped. IOW, ring 0 can starve the other rings # ring 0 is strictly FIFO and is therefore implemented # as an array Array.new end } # the other rings are satisfied round-robin @last_ring = 0 self.extend(MonitorMixin) @non_empty = self.new_cond end def clear self.synchronize do @rings.each { |r| r.clear } @last_ring = 0 end end def push(mess, chan=nil, cring=0) ring = cring self.synchronize do if ring == 0 warning "message #{mess} at ring 0 has channel #{chan}: channel will be ignored" if !chan.nil? @rings[0] << mess else error "message #{mess} at ring #{ring} must have a channel" if chan.nil? @rings[ring].push mess, chan end @non_empty.signal end end def shift(tmout = nil) self.synchronize do @non_empty.wait(tmout) if self.empty? return unsafe_shift end end protected def empty? !@rings.find { |r| !r.empty? } end def length @rings.inject(0) { |s, r| s + r.size } end alias :size :length def unsafe_shift if !@rings[0].empty? return @rings[0].shift end (@rings.size - 1).times do @last_ring = (@last_ring % (@rings.size - 1)) + 1 return @rings[@last_ring].shift unless @rings[@last_ring].empty? end warning "trying to access an empty message queue" return nil end end # wrapped TCPSocket for communication with the server. # emulates a subset of TCPSocket functionality class Socket MAX_IRC_SEND_PENALTY = 10 # total number of lines sent to the irc server attr_reader :lines_sent # total number of lines received from the irc server attr_reader :lines_received # total number of bytes sent to the irc server attr_reader :bytes_sent # total number of bytes received from the irc server attr_reader :bytes_received # accumulator for the throttle attr_reader :throttle_bytes # an optional filter object. we call @filter.in(data) for # all incoming data and @filter.out(data) for all outgoing data attr_reader :filter # normalized uri of the current server attr_reader :server_uri # penalty multiplier (percent) attr_accessor :penalty_pct # default trivial filter class class IdentityFilter def in(x) x end def out(x) x end end # set filter to identity, not to nil def filter=(f) @filter = f || IdentityFilter.new end # server_list:: list of servers to connect to # host:: optional local host to bind to (ruby 1.7+ required) # create a new Irc::Socket def initialize(server_list, host, opts={}) @server_list = server_list.dup @server_uri = nil @conn_count = 0 @host = host @sock = nil @filter = IdentityFilter.new @spooler = false @lines_sent = 0 @lines_received = 0 @ssl = opts[:ssl] @penalty_pct = opts[:penalty_pct] || 100 end def connected? !@sock.nil? end # open a TCP connection to the server def connect if connected? warning "reconnecting while connected" return end srv_uri = @server_list[@conn_count % @server_list.size].dup srv_uri = 'irc://' + srv_uri if !(srv_uri =~ /:\/\//) @conn_count += 1 @server_uri = URI.parse(srv_uri) @server_uri.port = 6667 if !@server_uri.port debug "connection attempt \##{@conn_count} (#{@server_uri.host}:#{@server_uri.port})" if(@host) begin sock=TCPSocket.new(@server_uri.host, @server_uri.port, @host) rescue ArgumentError => e error "Your version of ruby does not support binding to a " error "specific local address, please upgrade if you wish " error "to use HOST = foo" error "(this option has been disabled in order to continue)" sock=TCPSocket.new(@server_uri.host, @server_uri.port) end else sock=TCPSocket.new(@server_uri.host, @server_uri.port) end if(@ssl) require 'openssl' ssl_context = OpenSSL::SSL::SSLContext.new() ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE sock = OpenSSL::SSL::SSLSocket.new(sock, ssl_context) sock.sync_close = true sock.connect end @sock = sock @last_send = Time.new @flood_send = Time.new @burst = 0 @sock.extend(MonitorMixin) @sendq = MessageQueue.new @qthread = Thread.new { writer_loop } end # used to send lines to the remote IRCd by skipping the queue # message: IRC message to send # it should only be used for stuff that *must not* be queued, # i.e. the initial PASS, NICK and USER command # or the final QUIT message def emergency_puts(message, penalty = false) @sock.synchronize do # debug "In puts - got @sock" puts_critical(message, penalty) end end def handle_socket_error(string, e) error "#{string} failed: #{e.pretty_inspect}" # We assume that an error means that there are connection # problems and that we should reconnect, so we shutdown raise SocketError.new(e.inspect) end # get the next line from the server (blocks) def gets if @sock.nil? warning "socket get attempted while closed" return nil end begin reply = @filter.in(@sock.gets) @lines_received += 1 reply.strip! if reply debug "RECV: #{reply.inspect}" return reply rescue Exception => e handle_socket_error(:RECV, e) end end def queue(msg, chan=nil, ring=0) @sendq.push msg, chan, ring end def clearq @sendq.clear end # flush the TCPSocket def flush @sock.flush end # Wraps Kernel.select on the socket def select(timeout=nil) Kernel.select([@sock], nil, nil, timeout) end # shutdown the connection to the server def shutdown(how=2) return unless connected? @qthread.kill @qthread = nil begin @sock.close rescue Exception => e error "error while shutting down: #{e.pretty_inspect}" end @sock = nil @sendq.clear end private def writer_loop loop do begin now = Time.now flood_delay = @flood_send - MAX_IRC_SEND_PENALTY - now delay = [flood_delay, 0].max if delay > 0 debug "sleep(#{delay}) # (f: #{flood_delay})" sleep(delay) end msg = @sendq.shift debug "got #{msg.inspect} from queue, sending" emergency_puts(msg, true) rescue Exception => e error "Spooling failed: #{e.pretty_inspect}" debug e.backtrace.join("\n") raise e end end end # same as puts, but expects to be called with a lock held on @sock def puts_critical(message, penalty=false) # debug "in puts_critical" begin debug "SEND: #{message.inspect}" if @sock.nil? error "SEND attempted on closed socket" else # we use Socket#syswrite() instead of Socket#puts() because # the latter is racy and can cause double message output in # some circumstances actual = @filter.out(message) + "\n" now = Time.new @sock.syswrite actual @last_send = now @flood_send = now if @flood_send < now @flood_send += message.irc_send_penalty*@penalty_pct/100.0 if penalty @lines_sent += 1 end rescue Exception => e handle_socket_error(:SEND, e) end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/post-clean.rb0000644002342000234200000000007711411605044021376 0ustar duckdc-usersFile.unlink("pkgconfig.rb") if FileTest.exist?("pkgconfig.rb") rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/config-compat.rb0000644002342000234200000000135711411605044022061 0ustar duckdc-users#-- vim:sw=2:et #++ # :title: Config namespace backwards compatibility # # The move of everything rbot-related to the Irc::Bot::* namespace from Irc::* # would cause off-repo plugins to fail if they register any configuration key, # so we have to handle this case. # # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com) module Irc Config = Bot::Config module BotConfig def BotConfig.register(*args) warn "deprecated usage: please use Irc::Bot::Config instead of Irc::BotConfig (e.g. Config.register instead of BotConfig.register, Config::Value instead of BotConfigValue" Bot::Config.register(*args) end end Bot::Config.constants.each { |c| Irc.module_eval("BotConfig#{c} = Bot::Config::#{c}") } end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/0000755002342000234200000000000011414326121017727 5ustar duckdc-usersrbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/irclog.rb0000644002342000234200000002457111411605044021545 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot IRC logging facilities # # Author:: Giuseppe "Oblomov" Bilotta class IrcLogModule < CoreBotModule Config.register Config::IntegerValue.new('irclog.max_open_files', :default => 20, :validate => Proc.new { |v| v > 0 }, :desc => "Maximum number of irclog files to keep open at any one time.") Config.register Config::ArrayValue.new('irclog.no_log', :default => [], :on_change => Proc.new { |bot, v| bot.plugins.delegate 'event_irclog_list_changed', v, bot.config['irclog.do_log'] }, :desc => "List of channels and nicks for which logging is disabled. IRC patterns can be used too.") Config.register Config::ArrayValue.new('irclog.do_log', :default => [], :on_change => Proc.new { |bot, v| bot.plugins.delegate 'event_irclog_list_changed', bot.config['irclog.no_log'], v }, :desc => "List of channels and nicks for which logging is enabled. IRC patterns can be used too. This can be used to override wide patters in irclog.no_log") Config.register Config::StringValue.new('irclog.filename_format', :default => '%%{where}', :requires_rescan => true, :desc => "filename pattern for the IRC log. You can put typical strftime keys such as %Y for year and %m for month, plus the special %%{where} key for location (channel name or user nick)") Config.register Config::StringValue.new('irclog.timestamp_format', :default => '[%Y/%m/%d %H:%M:%S]', :requires_rescan => true, :desc => "timestamp pattern for the IRC log, using typical strftime keys") attr :nolog_rx, :dolog_rx def initialize super @queue = Queue.new @thread = Thread.new { loggers_thread } @logs = Hash.new logdir = @bot.path 'logs' Dir.mkdir(logdir) unless File.exist?(logdir) # TODO what shall we do if the logdir couldn't be created? (e.g. it existed as a file) event_irclog_list_changed(@bot.config['irclog.no_log'], @bot.config['irclog.do_log']) @fn_format = @bot.config['irclog.filename_format'] end def can_log_on(where) return true if @dolog_rx and where.match @dolog_rx return false if @nolog_rx and where.match @nolog_rx return true end def timestamp(time) return time.strftime(@bot.config['irclog.timestamp_format']) end def event_irclog_list_changed(nolist, dolist) @nolog_rx = nolist.empty? ? nil : Regexp.union(*(nolist.map { |r| r.to_irc_regexp })) debug "no log: #{@nolog_rx}" @dolog_rx = dolist.empty? ? nil : Regexp.union(*(dolist.map { |r| r.to_irc_regexp })) debug "do log: #{@dolog_rx}" @logs.inject([]) { |ar, kv| ar << kv.first unless can_log_on(kv.first) ar }.each { |w| logfile_close(w, 'logging disabled here') } end def logfile_close(where_str, reason = 'unknown reason') f = @logs.delete(where_str) or return stamp = timestamp(Time.now) f[1].puts "#{stamp} @ Log closed by #{@bot.myself.nick} (#{reason})" f[1].close end # log IRC-related message +message+ to a file determined by +where+. # +where+ can be a channel name, or a nick for private message logging def irclog(message, where="server") @queue.push [message, where] end def cleanup @queue << nil @thread.join @thread = nil end def sent(m) case m when NoticeMessage irclog "-#{m.source}- #{m.message}", m.target when PrivMessage logtarget = who = m.target if m.ctcp case m.ctcp.intern when :ACTION irclog "* #{m.source} #{m.logmessage}", logtarget when :VERSION irclog "@ #{m.source} asked #{who} about version info", logtarget when :SOURCE irclog "@ #{m.source} asked #{who} about source info", logtarget when :PING irclog "@ #{m.source} pinged #{who}", logtarget when :TIME irclog "@ #{m.source} asked #{who} what time it is", logtarget else irclog "@ #{m.source} asked #{who} about #{[m.ctcp, m.message].join(' ')}", logtarget end else irclog "<#{m.source}> #{m.logmessage}", logtarget end when QuitMessage m.was_on.each { |ch| irclog "@ quit (#{m.message})", ch } end end def welcome(m) irclog "joined server #{m.server} as #{m.target}", "server" end def listen(m) case m when PrivMessage method = 'log_message' else method = 'log_' + m.class.name.downcase.match(/^irc::(\w+)message$/).captures.first end if self.respond_to?(method) self.__send__(method, m) else warning "unhandled logging for #{m.pretty_inspect} (no such method #{method})" unknown_message(m) end end def log_message(m) if m.ctcp who = m.private? ? "me" : m.target logtarget = m.private? ? m.source : m.target case m.ctcp.intern when :ACTION if m.public? irclog "* #{m.source} #{m.logmessage}", m.target else irclog "* #{m.source}(#{m.sourceaddress}) #{m.logmessage}", m.source end when :VERSION irclog "@ #{m.source} asked #{who} about version info", logtarget when :SOURCE irclog "@ #{m.source} asked #{who} about source info", logtarget when :PING irclog "@ #{m.source} pinged #{who}", logtarget when :TIME irclog "@ #{m.source} asked #{who} what time it is", logtarget else irclog "@ #{m.source} asked #{who} about #{[m.ctcp, m.message].join(' ')}", logtarget end else if m.public? irclog "<#{m.source}> #{m.logmessage}", m.target else irclog "<#{m.source}(#{m.sourceaddress})> #{m.logmessage}", m.source end end end def log_notice(m) if m.private? irclog "-#{m.source}(#{m.sourceaddress})- #{m.logmessage}", m.source else irclog "-#{m.source}- #{m.logmessage}", m.target end end def motd(m) m.message.each_line { |line| irclog "MOTD: #{line}", "server" } end def log_nick(m) (m.is_on & @bot.myself.channels).each { |ch| irclog "@ #{m.oldnick} is now known as #{m.newnick}", ch } end def log_quit(m) (m.was_on & @bot.myself.channels).each { |ch| irclog "@ Quit: #{m.source}: #{m.logmessage}", ch } end def modechange(m) irclog "@ Mode #{m.logmessage} by #{m.source}", m.target end def log_join(m) if m.address? debug "joined channel #{m.channel}" irclog "@ Joined channel #{m.channel}", m.channel else irclog "@ #{m.source} joined channel #{m.channel}", m.channel end end def log_part(m) if(m.address?) debug "left channel #{m.channel}" irclog "@ Left channel #{m.channel} (#{m.logmessage})", m.channel else irclog "@ #{m.source} left channel #{m.channel} (#{m.logmessage})", m.channel end end def log_kick(m) if(m.address?) debug "kicked from channel #{m.channel}" irclog "@ You have been kicked from #{m.channel} by #{m.source} (#{m.logmessage})", m.channel else irclog "@ #{m.target} has been kicked from #{m.channel} by #{m.source} (#{m.logmessage})", m.channel end end # def log_invite(m) # # TODO # end def log_topic(m) case m.info_or_set when :set if m.source == @bot.myself irclog "@ I set topic \"#{m.topic}\"", m.channel else irclog "@ #{m.source} set topic \"#{m.topic}\"", m.channel end when :info topic = m.channel.topic irclog "@ Topic is \"#{m.topic}\"", m.channel irclog "@ Topic set by #{topic.set_by} on #{topic.set_on}", m.channel end end # def names(m) # # TODO # end def unknown_message(m) irclog m.logmessage, ".unknown" end def logfilepath(where_str, now) @bot.path('logs', now.strftime(@fn_format) % { :where => where_str }) end protected def loggers_thread ls = nil debug 'loggers_thread starting' while ls = @queue.pop message, where = ls message = message.chomp now = Time.now stamp = timestamp(now) if where.class <= Server where_str = "server" else where_str = where.downcase.gsub(/[:!?$*()\/\\<>|"']/, "_") end next unless can_log_on(where_str) # close the previous logfile if we're rotating if @logs.has_key? where_str fp = logfilepath(where_str, now) logfile_close(where_str, 'log rotation') if fp != @logs[where_str][1].path end # (re)open the logfile if necessary unless @logs.has_key? where_str if @logs.size > @bot.config['irclog.max_open_files'] @logs.keys.sort do |a, b| @logs[a][0] <=> @logs[b][0] end.slice(0, @logs.size - @bot.config['irclog.max_open_files']).each do |w| logfile_close w, "idle since #{@logs[w][0]}" end end fp = logfilepath(where_str, now) begin dir = File.dirname(fp) # first of all, we check we're not trying to build a directory structure # where one of the components exists already as a file, so we # backtrack along dir until we come across the topmost existing name. # If it's a file, we rename to filename.old.filedate up = dir.dup until File.exist? up up.replace(File.dirname(up)) end unless File.directory? up backup = up.dup backup << ".old." << File.atime(up).strftime('%Y%m%d%H%M%S') debug "#{up} is not a directory! renaming to #{backup}" File.rename(up, backup) end FileUtils.mkdir_p(dir) # conversely, it may happen that fp exists and is a directory, in # which case we rename the directory instead if File.directory? fp backup = fp.dup backup << ".old." << File.atime(fp).strftime('%Y%m%d%H%M%S') debug "#{fp} is not a file! renaming to #{backup}" File.rename(fp, backup) end # it should be fine to create the file now f = File.new(fp, "a") f.sync = true f.puts "#{stamp} @ Log started by #{@bot.myself.nick}" rescue Exception => e error e next end @logs[where_str] = [now, f] end @logs[where_str][1].puts "#{stamp} #{message}" @logs[where_str][0] = now #debug "#{stamp} <#{where}> #{message}" end @logs.keys.each { |w| logfile_close(w, 'rescan or shutdown') } debug 'loggers_thread terminating' end end ilm = IrcLogModule.new ilm.priority = -1 rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/userdata.rb0000644002342000234200000001242511411605044022071 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot user data management from IRC # # Author:: Giuseppe "Oblomov" Bilotta module ::Irc class User # Retrive Bot data associated with the receiver. This method is # intended for data retrieval only. See #set_bot_data() if you # need to alter User data. # def botdata(key=nil) Irc::Utils.bot.plugins['userdata'].get_data(self,key) end alias :get_botdata :botdata # This method is used to store Bot data associated with the # receiver. If no block is passed, _value_ is stored for the key # _key_; if a block is passed, it will be called with the previous # _key_ value as parameter, and its return value will be stored as # the new value. If _value_ is present in the block form, it will # be used to initialize _key_ if it's missing. # # If you have to do large-scale editing of the Bot data Hash, # please use with_botdata. # def set_botdata(key, value=nil, &block) Irc::Utils.bot.plugins['userdata'].set_data(self, key, value, &block) end # This method yields the entire Bot data Hash to the block, # and stores any changes done to it, returning a copy # of the (changed) Hash. # def with_botdata(&block) Irc::Utils.bot.plugins['userdata'].with_data(self, &block) end # This method removes the data associated with the key, returning # the value of the deleted key. def delete_botdata(*keys) Irc::Utils.bot.plugins['userdata'].delete_data(self, *keys) end end end # User data is stored in registries indexed by BotUser # name and Irc::User nick. This core module takes care # of handling its usage. # class UserDataModule < CoreBotModule def initialize super @ircuser = @registry.sub_registry('ircuser') @transient = @registry.sub_registry('transient') @botuser = @registry.sub_registry('botuser') end def get_data_hash(user, opts={}) plain = opts[:plain] iu = user.to_irc_user bu = iu.botuser ih = @ircuser[iu.nick] || {} if bu.default? return ih elsif bu.transient? bh = @transient[bu.netmasks.first.fullform] || {} else bh = @botuser[bu.username] || {} end ih.merge!(bh) unless plain class << ih alias :single_retrieve :[] alias :single_assign :[]= include DottedIndex end end return ih end def get_data(user, key=nil) h = get_data_hash(user) debug h return h if key.nil? return h[key] end def set_data_hash(user, hh) iu = user.to_irc_user bu = iu.botuser # we .dup the hash to remove singleton methods # and make it dump-able h = hh.dup @ircuser[iu.nick] = h return h if bu.default? if bu.transient? @transient[bu.netmasks.first.fullform] = h else @botuser[bu.username] = h end return h end def set_data(user, key, value=nil, &block) h = get_data_hash(user) debug h ret = value if not block_given? h[key] = value else if value and not h.has_key?(key) h[key] = value end ret = yield h[key] end debug ret set_data_hash(user, h) return ret end def with_data(user, &block) h = get_data_hash(user) debug h yield h set_data_hash(user, h) return h end def delete_data(user, *keys) h = get_data_hash(user) debug h rv = keys.map { |k| h.delete k } set_data_hash(user, h) rv.size == 1 ? rv.first : rv end def handle_get(m, params) user = m.server.get_user(params[:nick]) || m.source key = params[:key].intern data = get_data(user, key) if data m.reply(_("%{key} data for %{user}: %{data}") % { :key => key, :user => user.nick, :data => data }) else m.reply(_("sorry, no %{key} data for %{user}") % { :key => key, :user => user.nick, }) end end ### TODO FIXME not yet: are we going to allow non-string ### values for data? if so, this can't work ... # # def handle_set(m, params) # user = m.server.get_user(params[:nick]) || m.source # key = params[:key].intern # data = params[:data].to_s # end def event_botuser(action, opts={}) case action when :copy, :rename source = opts[:source] return unless @botuser.key?(source) dest = opts[:dest] @botuser[dest] = @botuser[source].dup @botuser.delete(source) if action == :rename when :pre_perm @permification ||= {} k = [opts[:irc_user], opts[:bot_user]] @permification[k] = get_data_hash(opts[:irc_user], :plain => true) when :post_perm @permification ||= {} k = [opts[:irc_user], opts[:bot_user]] if @permification.has_key?(k) @botuser[opts[:bot_user]] = @permification[k] @permification.delete(k) end end end end plugin = UserDataModule.new plugin.map "get [:nick's] :key [data]", :action => 'handle_get' plugin.map "get :key [data] [for :nick]", :action => 'handle_get' plugin.map "get :key [data] [of :nick]", :action => 'handle_get' # plugin.map "set [:nick's] :key [data] to :data", :action => handle_get # plugin.map "set :key [data] [for :nick] to :data", :action => handle_get # plugin.map "set :key [data] [of :nick] to :data", :action => handle_get rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/utils/0000755002342000234200000000000011414326121021067 5ustar duckdc-usersrbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/utils/httputil.rb0000644002342000234200000005727311414326121023307 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot HTTP provider # # Author:: Tom Gilbert # Author:: Giuseppe "Oblomov" Bilotta # Author:: Dmitry "jsn" Kim require 'resolv' require 'net/http' require 'cgi' require 'iconv' begin require 'net/https' rescue LoadError => e error "Couldn't load 'net/https': #{e.pretty_inspect}" error "Secured HTTP connections will fail" end # To handle Gzipped pages require 'stringio' require 'zlib' module ::Net class HTTPResponse attr_accessor :no_cache unless method_defined? :raw_body alias :raw_body :body end def body_charset(str=self.raw_body) ctype = self['content-type'] || 'text/html' return nil unless ctype =~ /^text/i || ctype =~ /x(ht)?ml/i charsets = ['latin1'] # should be in config if ctype.match(/charset=["']?([^\s"']+)["']?/i) charsets << $1 debug "charset #{charsets.last} added from header" end case str when /<\?xml\s[^>]*encoding=['"]([^\s"'>]+)["'][^>]*\?>/i charsets << $1 debug "xml charset #{charsets.last} added from xml pi" when /<(meta\s[^>]*http-equiv=["']?Content-Type["']?[^>]*)>/i meta = $1 if meta =~ /charset=['"]?([^\s'";]+)['"]?/ charsets << $1 debug "html charset #{charsets.last} added from meta" end end return charsets.uniq end def body_to_utf(str) charsets = self.body_charset(str) or return str charsets.reverse_each do |charset| # XXX: this one is really ugly, but i don't know how to make it better # -jsn 0.upto(5) do |off| begin debug "trying #{charset} / offset #{off}" return Iconv.iconv('utf-8//ignore', charset, str.slice(0 .. (-1 - off))).first rescue debug "conversion failed for #{charset} / offset #{off}" end end end return str end def decompress_body(str) method = self['content-encoding'] case method when nil return str when /gzip/ # Matches gzip, x-gzip, and the non-rfc-compliant gzip;q=\d sent by some servers debug "gunzipping body" begin return Zlib::GzipReader.new(StringIO.new(str)).read rescue Zlib::Error => e # If we can't unpack the whole stream (e.g. because we're doing a # partial read debug "full gunzipping failed (#{e}), trying to recover as much as possible" ret = "" begin Zlib::GzipReader.new(StringIO.new(str)).each_byte { |byte| ret << byte } rescue end return ret end when 'deflate' debug "inflating body" # From http://www.koders.com/ruby/fid927B4382397E5115AC0ABE21181AB5C1CBDD5C17.aspx?s=thread: # -MAX_WBITS stops zlib from looking for a zlib header inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) begin return inflater.inflate(str) rescue Zlib::Error => e raise e # TODO # debug "full inflation failed (#{e}), trying to recover as much as possible" end when /^(?:iso-8859-\d+|windows-\d+|utf-8|utf8)$/i # B0rked servers (Freshmeat being one of them) sometimes return the charset # in the content-encoding; in this case we assume that the document has # a standarc content-encoding old_hsh = self.to_hash self['content-type']= self['content-type']+"; charset="+method.downcase warning "Charset vs content-encoding confusion, trying to recover: from\n#{old_hsh.pretty_inspect}to\n#{self.to_hash.pretty_inspect}" return str else debug self.to_hash raise "Unhandled content encoding #{method}" end end def cooked_body return self.body_to_utf(self.decompress_body(self.raw_body)) end # Read chunks from the body until we have at least _size_ bytes, yielding # the partial text at each chunk. Return the partial body. def partial_body(size=0, &block) partial = String.new if @read debug "using body() as partial" partial = self.body yield self.body_to_utf(self.decompress_body(partial)) if block_given? else debug "disabling cache" self.no_cache = true self.read_body { |chunk| partial << chunk yield self.body_to_utf(self.decompress_body(partial)) if block_given? break if size and size > 0 and partial.length >= size } end return self.body_to_utf(self.decompress_body(partial)) end end end Net::HTTP.version_1_2 module ::Irc module Utils # class for making http requests easier (mainly for plugins to use) # this class can check the bot proxy configuration to determine if a proxy # needs to be used, which includes support for per-url proxy configuration. class HttpUtil Bot::Config.register Bot::Config::IntegerValue.new('http.read_timeout', :default => 10, :desc => "Default read timeout for HTTP connections") Bot::Config.register Bot::Config::IntegerValue.new('http.open_timeout', :default => 20, :desc => "Default open timeout for HTTP connections") Bot::Config.register Bot::Config::BooleanValue.new('http.use_proxy', :default => false, :desc => "should a proxy be used for HTTP requests?") Bot::Config.register Bot::Config::StringValue.new('http.proxy_uri', :default => false, :desc => "Proxy server to use for HTTP requests (URI, e.g http://proxy.host:port)") Bot::Config.register Bot::Config::StringValue.new('http.proxy_user', :default => nil, :desc => "User for authenticating with the http proxy (if required)") Bot::Config.register Bot::Config::StringValue.new('http.proxy_pass', :default => nil, :desc => "Password for authenticating with the http proxy (if required)") Bot::Config.register Bot::Config::ArrayValue.new('http.proxy_include', :default => [], :desc => "List of regexps to check against a URI's hostname/ip to see if we should use the proxy to access this URI. All URIs are proxied by default if the proxy is set, so this is only required to re-include URIs that might have been excluded by the exclude list. e.g. exclude /.*\.foo\.com/, include bar\.foo\.com") Bot::Config.register Bot::Config::ArrayValue.new('http.proxy_exclude', :default => [], :desc => "List of regexps to check against a URI's hostname/ip to see if we should use avoid the proxy to access this URI and access it directly") Bot::Config.register Bot::Config::IntegerValue.new('http.max_redir', :default => 5, :desc => "Maximum number of redirections to be used when getting a document") Bot::Config.register Bot::Config::IntegerValue.new('http.expire_time', :default => 60, :desc => "After how many minutes since last use a cached document is considered to be expired") Bot::Config.register Bot::Config::IntegerValue.new('http.max_cache_time', :default => 60*24, :desc => "After how many minutes since first use a cached document is considered to be expired") Bot::Config.register Bot::Config::BooleanValue.new('http.no_expire_cache', :default => false, :desc => "Set this to true if you want the bot to never expire the cached pages") Bot::Config.register Bot::Config::IntegerValue.new('http.info_bytes', :default => 8192, :desc => "How many bytes to download from a web page to find some information. Set to 0 to let the bot download the whole page.") class CachedObject attr_accessor :response, :last_used, :first_used, :count, :expires, :date def self.maybe_new(resp) debug "maybe new #{resp}" return nil if resp.no_cache return nil unless Net::HTTPOK === resp || Net::HTTPMovedPermanently === resp || Net::HTTPFound === resp || Net::HTTPPartialContent === resp cc = resp['cache-control'] return nil if cc && (cc =~ /no-cache/i) date = Time.now if d = resp['date'] date = Time.httpdate(d) end return nil if resp['expires'] && (Time.httpdate(resp['expires']) < date) debug "creating cache obj" self.new(resp) end def use now = Time.now @first_used = now if @count == 0 @last_used = now @count += 1 end def expired? debug "checking expired?" if cc = self.response['cache-control'] && cc =~ /must-revalidate/ return true end return self.expires < Time.now end def setup_headers(hdr) hdr['if-modified-since'] = self.date.rfc2822 debug "ims == #{hdr['if-modified-since']}" if etag = self.response['etag'] hdr['if-none-match'] = etag debug "etag: #{etag}" end end def revalidate(resp = self.response) @count = 0 self.use self.date = resp.key?('date') ? Time.httpdate(resp['date']) : Time.now cc = resp['cache-control'] if cc && (cc =~ /max-age=(\d+)/) self.expires = self.date + $1.to_i elsif resp.key?('expires') self.expires = Time.httpdate(resp['expires']) elsif lm = resp['last-modified'] delta = self.date - Time.httpdate(lm) delta = 10 if delta <= 0 delta /= 5 self.expires = self.date + delta else self.expires = self.date + 300 end # self.expires = Time.now + 10 # DEBUG debug "expires on #{self.expires}" return true end private def initialize(resp) @response = resp begin self.revalidate self.response.raw_body rescue Exception => e error e raise e end end end # Create the HttpUtil instance, associating it with Bot _bot_ # def initialize(bot) @bot = bot @cache = Hash.new @headers = { 'Accept-Charset' => 'utf-8;q=1.0, *;q=0.8', 'Accept-Encoding' => 'gzip;q=1, deflate;q=1, identity;q=0.8, *;q=0.2', 'User-Agent' => "rbot http util #{$version} (#{Irc::Bot::SOURCE_URL})" } debug "starting http cache cleanup timer" @timer = @bot.timer.add(300) { self.remove_stale_cache unless @bot.config['http.no_expire_cache'] } end # Clean up on HttpUtil unloading, by stopping the cache cleanup timer. def cleanup debug 'stopping http cache cleanup timer' @bot.timer.remove(@timer) end # This method checks if a proxy is required to access _uri_, by looking at # the values of config values +http.proxy_include+ and +http.proxy_exclude+. # # Each of these config values, if set, should be a Regexp the server name and # IP address should be checked against. # def proxy_required(uri) use_proxy = true if @bot.config["http.proxy_exclude"].empty? && @bot.config["http.proxy_include"].empty? return use_proxy end list = [uri.host] begin list.concat Resolv.getaddresses(uri.host) rescue StandardError => err warning "couldn't resolve host uri.host" end unless @bot.config["http.proxy_exclude"].empty? re = @bot.config["http.proxy_exclude"].collect{|r| Regexp.new(r)} re.each do |r| list.each do |item| if r.match(item) use_proxy = false break end end end end unless @bot.config["http.proxy_include"].empty? re = @bot.config["http.proxy_include"].collect{|r| Regexp.new(r)} re.each do |r| list.each do |item| if r.match(item) use_proxy = true break end end end end debug "using proxy for uri #{uri}?: #{use_proxy}" return use_proxy end # _uri_:: URI to create a proxy for # # Return a net/http Proxy object, configured for proxying based on the # bot's proxy configuration. See proxy_required for more details on this. # def get_proxy(uri, options = {}) opts = { :read_timeout => @bot.config["http.read_timeout"], :open_timeout => @bot.config["http.open_timeout"] }.merge(options) proxy = nil proxy_host = nil proxy_port = nil proxy_user = nil proxy_pass = nil if @bot.config["http.use_proxy"] if (ENV['http_proxy']) proxy = URI.parse ENV['http_proxy'] rescue nil end if (@bot.config["http.proxy_uri"]) proxy = URI.parse @bot.config["http.proxy_uri"] rescue nil end if proxy debug "proxy is set to #{proxy.host} port #{proxy.port}" if proxy_required(uri) proxy_host = proxy.host proxy_port = proxy.port proxy_user = @bot.config["http.proxy_user"] proxy_pass = @bot.config["http.proxy_pass"] end end end h = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port, proxy_user, proxy_pass) h.use_ssl = true if uri.scheme == "https" h.read_timeout = opts[:read_timeout] h.open_timeout = opts[:open_timeout] return h end # Internal method used to hanlde response _resp_ received when making a # request for URI _uri_. # # It follows redirects, optionally yielding them if option :yield is :all. # # Also yields and returns the final _resp_. # def handle_response(uri, resp, opts, &block) # :yields: resp if Net::HTTPRedirection === resp && opts[:max_redir] >= 0 if resp.key?('location') raise 'Too many redirections' if opts[:max_redir] <= 0 yield resp if opts[:yield] == :all && block_given? # some servers actually provide unescaped location, e.g. # http://ulysses.soup.io/post/60734021/Image%20curve%20ball # rediects to something like # http://ulysses.soup.io/post/60734021/Image curve ball?sessid=8457b2a3752085cca3fb1d79b9965446 # causing the URI parser to (obviously) complain. We cannot just # escape blindly, as this would make a mess of already-escaped # locations, so we only do it if the URI.parse fails loc = resp['location'] escaped = false debug "redirect location: #{loc.inspect}" begin new_loc = URI.join(uri.to_s, loc) rescue URI.parse(loc) rescue if escaped raise $! else loc = URI.escape(loc) escaped = true debug "escaped redirect location: #{loc.inspect}" retry end end new_opts = opts.dup new_opts[:max_redir] -= 1 case opts[:method].to_s.downcase.intern when :post, :"net::http::post" new_opts[:method] = :get end if resp['set-cookie'] debug "set cookie request for #{resp['set-cookie']}" cookie, cookie_flags = (resp['set-cookie']+'; ').split('; ', 2) domain = uri.host cookie_flags.scan(/(\S+)=(\S+);/) { |key, val| if key.intern == :domain domain = val break end } debug "cookie domain #{domain} / #{new_loc.host}" if new_loc.host.rindex(domain) == new_loc.host.length - domain.length debug "setting cookie" new_opts[:headers] ||= Hash.new new_opts[:headers]['Cookie'] = cookie else debug "cookie is for another domain, ignoring" end end debug "following the redirect to #{new_loc}" return get_response(new_loc, new_opts, &block) else warning ":| redirect w/o location?" end end class << resp undef_method :body alias :body :cooked_body end unless resp['content-type'] debug "No content type, guessing" resp['content-type'] = case resp['x-rbot-location'] when /.html?$/i 'text/html' when /.xml$/i 'application/xml' when /.xhtml$/i 'application/xml+xhtml' when /.(gif|png|jpe?g|jp2|tiff?)$/i "image/#{$1.sub(/^jpg$/,'jpeg').sub(/^tif$/,'tiff')}" else 'application/octetstream' end end if block_given? yield(resp) else # Net::HTTP wants us to read the whole body here resp.raw_body end return resp end # _uri_:: uri to query (URI object or String) # # Generic http transaction method. It will return a Net::HTTPResponse # object or raise an exception # # If a block is given, it will yield the response (see :yield option) # # Currently supported _options_: # # method:: request method [:get (default), :post or :head] # open_timeout:: open timeout for the proxy # read_timeout:: read timeout for the proxy # cache:: should we cache results? # yield:: if :final [default], calls the block for the response object; # if :all, call the block for all intermediate redirects, too # max_redir:: how many redirects to follow before raising the exception # if -1, don't follow redirects, just return them # range:: make a ranged request (usually GET). accepts a string # for HTTP/1.1 "Range:" header (i.e. "bytes=0-1000") # body:: request body (usually for POST requests) # headers:: additional headers to be set for the request. Its value must # be a Hash in the form { 'Header' => 'value' } # def get_response(uri_or_s, options = {}, &block) # :yields: resp uri = uri_or_s.kind_of?(URI) ? uri_or_s : URI.parse(uri_or_s.to_s) unless URI::HTTP === uri if uri.scheme raise "#{uri.scheme.inspect} URI scheme is not supported" else raise "don't know what to do with #{uri.to_s.inspect}" end end opts = { :max_redir => @bot.config['http.max_redir'], :yield => :final, :cache => true, :method => :GET }.merge(options) resp = nil req_class = case opts[:method].to_s.downcase.intern when :head, :"net::http::head" opts[:max_redir] = -1 Net::HTTP::Head when :get, :"net::http::get" Net::HTTP::Get when :post, :"net::http::post" opts[:cache] = false opts[:body] or raise 'post request w/o a body?' warning "refusing to cache POST request" if options[:cache] Net::HTTP::Post else warning "unsupported method #{opts[:method]}, doing GET" Net::HTTP::Get end if req_class != Net::HTTP::Get && opts[:range] warning "can't request ranges for #{req_class}" opts.delete(:range) end cache_key = "#{opts[:range]}|#{req_class}|#{uri.to_s}" if req_class != Net::HTTP::Get && req_class != Net::HTTP::Head if opts[:cache] warning "can't cache #{req_class.inspect} requests, working w/o cache" opts[:cache] = false end end debug "get_response(#{uri}, #{opts.inspect})" cached = @cache[cache_key] if opts[:cache] && cached debug "got cached" if !cached.expired? debug "using cached" cached.use return handle_response(uri, cached.response, opts, &block) end end headers = @headers.dup.merge(opts[:headers] || {}) headers['Range'] = opts[:range] if opts[:range] headers['Authorization'] = opts[:auth_head] if opts[:auth_head] if opts[:cache] && cached && (req_class == Net::HTTP::Get) cached.setup_headers headers end req = req_class.new(uri.request_uri, headers) if uri.user && uri.password req.basic_auth(uri.user, uri.password) opts[:auth_head] = req['Authorization'] end req.body = opts[:body] if req_class == Net::HTTP::Post debug "prepared request: #{req.to_hash.inspect}" begin get_proxy(uri, opts).start do |http| http.request(req) do |resp| resp['x-rbot-location'] = uri.to_s if Net::HTTPNotModified === resp debug "not modified" begin cached.revalidate(resp) rescue Exception => e error e end debug "reusing cached" resp = cached.response elsif Net::HTTPServerError === resp || Net::HTTPClientError === resp debug "http error, deleting cached obj" if cached @cache.delete(cache_key) end begin return handle_response(uri, resp, opts, &block) ensure if cached = CachedObject.maybe_new(resp) rescue nil debug "storing to cache" @cache[cache_key] = cached end end end end rescue Exception => e error e raise e.message end end # _uri_:: uri to query (URI object or String) # # Simple GET request, returns (if possible) response body following redirs # and caching if requested, yielding the actual response(s) to the optional # block. See get_response for details on the supported _options_ # def get(uri, options = {}, &block) # :yields: resp begin resp = get_response(uri, options, &block) raise "http error: #{resp}" unless Net::HTTPOK === resp || Net::HTTPPartialContent === resp return resp.body rescue Exception => e error e end return nil end # _uri_:: uri to query (URI object or String) # # Simple HEAD request, returns (if possible) response head following redirs # and caching if requested, yielding the actual response(s) to the optional # block. See get_response for details on the supported _options_ # def head(uri, options = {}, &block) # :yields: resp opts = {:method => :head}.merge(options) begin resp = get_response(uri, opts, &block) # raise "http error #{resp}" if Net::HTTPClientError === resp || # Net::HTTPServerError == resp return resp rescue Exception => e error e end return nil end # _uri_:: uri to query (URI object or String) # _data_:: body of the POST # # Simple POST request, returns (if possible) response following redirs and # caching if requested, yielding the response(s) to the optional block. See # get_response for details on the supported _options_ # def post(uri, data, options = {}, &block) # :yields: resp opts = {:method => :post, :body => data, :cache => false}.merge(options) begin resp = get_response(uri, opts, &block) raise 'http error' unless Net::HTTPOK === resp or Net::HTTPCreated === resp return resp rescue Exception => e error e end return nil end # _uri_:: uri to query (URI object or String) # _nbytes_:: number of bytes to get # # Partial GET request, returns (if possible) the first _nbytes_ bytes of the # response body, following redirs and caching if requested, yielding the # actual response(s) to the optional block. See get_response for details on # the supported _options_ # def get_partial(uri, nbytes = @bot.config['http.info_bytes'], options = {}, &block) # :yields: resp opts = {:range => "bytes=0-#{nbytes}"}.merge(options) return get(uri, opts, &block) end def remove_stale_cache debug "Removing stale cache" now = Time.new max_last = @bot.config['http.expire_time'] * 60 max_first = @bot.config['http.max_cache_time'] * 60 debug "#{@cache.size} pages before" begin @cache.reject! { |k, val| (now - val.last_used > max_last) || (now - val.first_used > max_first) } rescue => e error "Failed to remove stale cache: #{e.pretty_inspect}" end debug "#{@cache.size} pages after" end end end end class HttpUtilPlugin < CoreBotModule def initialize(*a) super(*a) debug 'initializing httputil' @bot.httputil = Irc::Utils::HttpUtil.new(@bot) end def cleanup debug 'shutting down httputil' @bot.httputil.cleanup @bot.httputil = nil super end end HttpUtilPlugin.new rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/utils/utils.rb0000644002342000234200000005515611411605044022571 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot utilities provider # # Author:: Tom Gilbert # Author:: Giuseppe "Oblomov" Bilotta # # TODO some of these Utils should be rewritten as extensions to the approriate # standard Ruby classes and accordingly be moved to extends.rb require 'tempfile' require 'set' # Try to load htmlentities, fall back to an HTML escape table. begin require 'htmlentities' rescue LoadError module ::Irc module Utils UNESCAPE_TABLE = { 'laquo' => '«', 'raquo' => '»', 'quot' => '"', 'apos' => '\'', 'micro' => 'µ', 'copy' => '©', 'trade' => '™', 'reg' => '®', 'amp' => '&', 'lt' => '<', 'gt' => '>', 'hellip' => '…', 'nbsp' => ' ', 'Agrave' => 'À', 'Aacute' => 'Á', 'Acirc' => 'Â', 'Atilde' => 'Ã', 'Auml' => 'Ä', 'Aring' => 'Å', 'AElig' => 'Æ', 'OElig' => 'Œ', 'Ccedil' => 'Ç', 'Egrave' => 'È', 'Eacute' => 'É', 'Ecirc' => 'Ê', 'Euml' => 'Ë', 'Igrave' => 'Ì', 'Iacute' => 'Í', 'Icirc' => 'Î', 'Iuml' => 'Ï', 'ETH' => 'Ð', 'Ntilde' => 'Ñ', 'Ograve' => 'Ò', 'Oacute' => 'Ó', 'Ocirc' => 'Ô', 'Otilde' => 'Õ', 'Ouml' => 'Ö', 'Oslash' => 'Ø', 'Ugrave' => 'Ù', 'Uacute' => 'Ú', 'Ucirc' => 'Û', 'Uuml' => 'Ü', 'Yacute' => 'Ý', 'THORN' => 'Þ', 'szlig' => 'ß', 'agrave' => 'à', 'aacute' => 'á', 'acirc' => 'â', 'atilde' => 'ã', 'auml' => 'ä', 'aring' => 'å', 'aelig' => 'æ', 'oelig' => 'œ', 'ccedil' => 'ç', 'egrave' => 'è', 'eacute' => 'é', 'ecirc' => 'ê', 'euml' => 'ë', 'igrave' => 'ì', 'iacute' => 'í', 'icirc' => 'î', 'iuml' => 'ï', 'eth' => 'ð', 'ntilde' => 'ñ', 'ograve' => 'ò', 'oacute' => 'ó', 'ocirc' => 'ô', 'otilde' => 'õ', 'ouml' => 'ö', 'oslash' => 'ø', 'ugrave' => 'ù', 'uacute' => 'ú', 'ucirc' => 'û', 'uuml' => 'ü', 'yacute' => 'ý', 'thorn' => 'þ', 'yuml' => 'ÿ' } end end end begin require 'hpricot' module ::Irc module Utils AFTER_PAR_PATH = /^(?:div|span)$/ AFTER_PAR_EX = /^(?:td|tr|tbody|table)$/ AFTER_PAR_CLASS = /body|message|text/i end end rescue LoadError module ::Irc module Utils # Some regular expressions to manage HTML data # Title TITLE_REGEX = /<\s*?title\s*?>(.+?)<\s*?\/title\s*?>/im # H1, H2, etc HX_REGEX = /]*)?>(.*?)<\/h\1>/im # A paragraph PAR_REGEX = /]*)?>.*?<\/?(?:p|div|html|body|table|td|tr)(?:\s+[^>]*)?>/im # Some blogging and forum platforms use spans or divs with a 'body' or 'message' or 'text' in their class # to mark actual text AFTER_PAR1_REGEX = /<\w+\s+[^>]*(?:body|message|text)[^>]*>.*?<\/?(?:p|div|html|body|table|td|tr)(?:\s+[^>]*)?>/im # At worst, we can try stuff which is comprised between two
AFTER_PAR2_REGEX = /]*)?\/?>.*?<\/?(?:br|p|div|html|body|table|td|tr)(?:\s+[^>]*)?\/?>/im end end end module ::Irc # Miscellaneous useful functions module Utils @@bot = nil unless defined? @@bot @@safe_save_dir = nil unless defined?(@@safe_save_dir) # The bot instance def Utils.bot @@bot end # Set up some Utils routines which depend on the associated bot. def Utils.bot=(b) debug "initializing utils" @@bot = b @@safe_save_dir = @@bot.path('safe_save') end # Seconds per minute SEC_PER_MIN = 60 # Seconds per hour SEC_PER_HR = SEC_PER_MIN * 60 # Seconds per day SEC_PER_DAY = SEC_PER_HR * 24 # Seconds per week SEC_PER_WK = SEC_PER_DAY * 7 # Seconds per (30-day) month SEC_PER_MNTH = SEC_PER_DAY * 30 # Second per (non-leap) year SEC_PER_YR = SEC_PER_DAY * 365 # Auxiliary method needed by Utils.secs_to_string def Utils.secs_to_string_case(array, var, string, plural) case var when 1 array << "1 #{string}" else array << "#{var} #{plural}" end end # Turn a number of seconds into a human readable string, e.g # 2 days, 3 hours, 18 minutes and 10 seconds def Utils.secs_to_string(secs) ret = [] years, secs = secs.divmod SEC_PER_YR secs_to_string_case(ret, years, _("year"), _("years")) if years > 0 months, secs = secs.divmod SEC_PER_MNTH secs_to_string_case(ret, months, _("month"), _("months")) if months > 0 days, secs = secs.divmod SEC_PER_DAY secs_to_string_case(ret, days, _("day"), _("days")) if days > 0 hours, secs = secs.divmod SEC_PER_HR secs_to_string_case(ret, hours, _("hour"), _("hours")) if hours > 0 mins, secs = secs.divmod SEC_PER_MIN secs_to_string_case(ret, mins, _("minute"), _("minutes")) if mins > 0 secs = secs.to_i secs_to_string_case(ret, secs, _("second"), _("seconds")) if secs > 0 or ret.empty? case ret.length when 0 raise "Empty ret array!" when 1 return ret.to_s else return [ret[0, ret.length-1].join(", ") , ret[-1]].join(_(" and ")) end end # Turn a number of seconds into a hours:minutes:seconds e.g. # 3:18:10 or 5'12" or 7s # def Utils.secs_to_short(seconds) secs = seconds.to_i # make sure it's an integer mins, secs = secs.divmod 60 hours, mins = mins.divmod 60 if hours > 0 return ("%s:%s:%s" % [hours, mins, secs]) elsif mins > 0 return ("%s'%s\"" % [mins, secs]) else return ("%ss" % [secs]) end end # Returns human readable time. # Like: 5 days ago # about one hour ago # options # :start_date, sets the time to measure against, defaults to now # :date_format, used with to_formatted_s, default to :default def Utils.timeago(time, options = {}) start_date = options.delete(:start_date) || Time.new date_format = options.delete(:date_format) || "%x" delta = (start_date - time).round if delta.abs < 2 _("right now") else distance = Utils.age_string(delta) if delta < 0 _("%{d} from now") % {:d => distance} else _("%{d} ago") % {:d => distance} end end end # Converts age in seconds to "nn units". Inspired by previous attempts # but also gitweb's age_string() sub def Utils.age_string(secs) case when secs < 0 Utils.age_string(-secs) when secs > 2*SEC_PER_YR _("%{m} years") % { :m => secs/SEC_PER_YR } when secs > 2*SEC_PER_MNTH _("%{m} months") % { :m => secs/SEC_PER_MNTH } when secs > 2*SEC_PER_WK _("%{m} weeks") % { :m => secs/SEC_PER_WK } when secs > 2*SEC_PER_DAY _("%{m} days") % { :m => secs/SEC_PER_DAY } when secs > 2*SEC_PER_HR _("%{m} hours") % { :m => secs/SEC_PER_HR } when (20*SEC_PER_MIN..40*SEC_PER_MIN).include?(secs) _("half an hour") when (50*SEC_PER_MIN..70*SEC_PER_MIN).include?(secs) # _("about one hour") _("an hour") when (80*SEC_PER_MIN..100*SEC_PER_MIN).include?(secs) _("an hour and a half") when secs > 2*SEC_PER_MIN _("%{m} minutes") % { :m => secs/SEC_PER_MIN } when secs > 1 _("%{m} seconds") % { :m => secs } else _("one second") end end # Execute an external program, returning a String obtained by redirecting # the program's standards errors and output # def Utils.safe_exec(command, *args) IO.popen("-") { |p| if p return p.readlines.join("\n") else begin $stderr.reopen($stdout) exec(command, *args) rescue Exception => e puts "exception #{e.pretty_inspect} trying to run #{command}" Kernel::exit! 1 end puts "exec of #{command} failed" Kernel::exit! 1 end } end # Try executing an external program, returning true if the run was successful # and false otherwise def Utils.try_exec(command, *args) IO.popen("-") { |p| if p.nil? begin $stderr.reopen($stdout) exec(command, *args) rescue Exception => e Kernel::exit! 1 end Kernel::exit! 1 else debug p.readlines end } debug $? return $?.success? end # Safely (atomically) save to _file_, by passing a tempfile to the block # and then moving the tempfile to its final location when done. # # call-seq: Utils.safe_save(file, &block) # def Utils.safe_save(file) raise 'No safe save directory defined!' if @@safe_save_dir.nil? basename = File.basename(file) temp = Tempfile.new(basename,@@safe_save_dir) temp.binmode yield temp if block_given? temp.close File.rename(temp.path, file) end # Decode HTML entities in the String _str_, using HTMLEntities if the # package was found, or UNESCAPE_TABLE otherwise. # def Utils.decode_html_entities(str) if defined? ::HTMLEntities return HTMLEntities.decode_entities(str) else str.gsub(/(&(.+?);)/) { symbol = $2 # remove the 0-paddng from unicode integers if symbol =~ /^#(\d+)$/ symbol = $1.to_i.to_s end # output the symbol's irc-translated character, or a * if it's unknown UNESCAPE_TABLE[symbol] || (symbol.match(/^\d+$/) ? [symbol.to_i].pack("U") : '*') } end end # Try to grab and IRCify the first HTML par (

tag) in the given string. # If possible, grab the one after the first heading # # It is possible to pass some options to determine how the stripping # occurs. Currently supported options are # strip:: Regex or String to strip at the beginning of the obtained # text # min_spaces:: minimum number of spaces a paragraph should have # def Utils.ircify_first_html_par(xml_org, opts={}) if defined? ::Hpricot Utils.ircify_first_html_par_wh(xml_org, opts) else Utils.ircify_first_html_par_woh(xml_org, opts) end end # HTML first par grabber using hpricot def Utils.ircify_first_html_par_wh(xml_org, opts={}) doc = Hpricot(xml_org) # Strip styles and scripts (doc/"style|script").remove debug doc strip = opts[:strip] strip = Regexp.new(/^#{Regexp.escape(strip)}/) if strip.kind_of?(String) min_spaces = opts[:min_spaces] || 8 min_spaces = 0 if min_spaces < 0 txt = String.new pre_h = pars = by_span = nil while true debug "Minimum number of spaces: #{min_spaces}" # Initial attempt:

that follows if pre_h.nil? pre_h = Hpricot::Elements[] found_h = false doc.search("*") { |e| next if e.bogusetag? case e.pathname when /^h\d/ found_h = true when 'p' pre_h << e if found_h end } debug "Hx: found: #{pre_h.pretty_inspect}" end pre_h.each { |p| debug p txt = p.to_html.ircify_html txt.sub!(strip, '') if strip debug "(Hx attempt) #{txt.inspect} has #{txt.count(" ")} spaces" break unless txt.empty? or txt.count(" ") < min_spaces } return txt unless txt.empty? or txt.count(" ") < min_spaces # Second natural attempt: just get any

pars = doc/"p" if pars.nil? debug "par: found: #{pars.pretty_inspect}" pars.each { |p| debug p txt = p.to_html.ircify_html txt.sub!(strip, '') if strip debug "(par attempt) #{txt.inspect} has #{txt.count(" ")} spaces" break unless txt.empty? or txt.count(" ") < min_spaces } return txt unless txt.empty? or txt.count(" ") < min_spaces # Nothing yet ... let's get drastic: we look for non-par elements too, # but only for those that match something that we know is likely to # contain text # Some blogging and forum platforms use spans or divs with a 'body' or # 'message' or 'text' in their class to mark actual text. Since we want # the class match to be partial and case insensitive, we collect # the common elements that may have this class and then filter out those # we don't need. If no divs or spans are found, we'll accept additional # elements too (td, tr, tbody, table). if by_span.nil? by_span = Hpricot::Elements[] extra = Hpricot::Elements[] doc.search("*") { |el| next if el.bogusetag? case el.pathname when AFTER_PAR_PATH by_span.push el if el[:class] =~ AFTER_PAR_CLASS or el[:id] =~ AFTER_PAR_CLASS when AFTER_PAR_EX extra.push el if el[:class] =~ AFTER_PAR_CLASS or el[:id] =~ AFTER_PAR_CLASS end } if by_span.empty? and not extra.empty? by_span.concat extra end debug "other \#1: found: #{by_span.pretty_inspect}" end by_span.each { |p| debug p txt = p.to_html.ircify_html txt.sub!(strip, '') if strip debug "(other attempt \#1) #{txt.inspect} has #{txt.count(" ")} spaces" break unless txt.empty? or txt.count(" ") < min_spaces } return txt unless txt.empty? or txt.count(" ") < min_spaces # At worst, we can try stuff which is comprised between two
# TODO debug "Last candidate #{txt.inspect} has #{txt.count(" ")} spaces" return txt unless txt.count(" ") < min_spaces break if min_spaces == 0 min_spaces /= 2 end end # HTML first par grabber without hpricot def Utils.ircify_first_html_par_woh(xml_org, opts={}) xml = xml_org.gsub(//m, '').gsub(/]*)?>.*?<\/script>/im, "").gsub(/]*)?>.*?<\/style>/im, "") strip = opts[:strip] strip = Regexp.new(/^#{Regexp.escape(strip)}/) if strip.kind_of?(String) min_spaces = opts[:min_spaces] || 8 min_spaces = 0 if min_spaces < 0 txt = String.new while true debug "Minimum number of spaces: #{min_spaces}" header_found = xml.match(HX_REGEX) if header_found header_found = $' while txt.empty? or txt.count(" ") < min_spaces candidate = header_found[PAR_REGEX] break unless candidate txt = candidate.ircify_html header_found = $' txt.sub!(strip, '') if strip debug "(Hx attempt) #{txt.inspect} has #{txt.count(" ")} spaces" end end return txt unless txt.empty? or txt.count(" ") < min_spaces # If we haven't found a first par yet, try to get it from the whole # document header_found = xml while txt.empty? or txt.count(" ") < min_spaces candidate = header_found[PAR_REGEX] break unless candidate txt = candidate.ircify_html header_found = $' txt.sub!(strip, '') if strip debug "(par attempt) #{txt.inspect} has #{txt.count(" ")} spaces" end return txt unless txt.empty? or txt.count(" ") < min_spaces # Nothing yet ... let's get drastic: we look for non-par elements too, # but only for those that match something that we know is likely to # contain text # Attempt #1 header_found = xml while txt.empty? or txt.count(" ") < min_spaces candidate = header_found[AFTER_PAR1_REGEX] break unless candidate txt = candidate.ircify_html header_found = $' txt.sub!(strip, '') if strip debug "(other attempt \#1) #{txt.inspect} has #{txt.count(" ")} spaces" end return txt unless txt.empty? or txt.count(" ") < min_spaces # Attempt #2 header_found = xml while txt.empty? or txt.count(" ") < min_spaces candidate = header_found[AFTER_PAR2_REGEX] break unless candidate txt = candidate.ircify_html header_found = $' txt.sub!(strip, '') if strip debug "(other attempt \#2) #{txt.inspect} has #{txt.count(" ")} spaces" end debug "Last candidate #{txt.inspect} has #{txt.count(" ")} spaces" return txt unless txt.count(" ") < min_spaces break if min_spaces == 0 min_spaces /= 2 end end # This method extracts title, content (first par) and extra # information from the given document _doc_. # # _doc_ can be an URI, a Net::HTTPResponse or a String. # # If _doc_ is a String, only title and content information # are retrieved (if possible), using standard methods. # # If _doc_ is an URI or a Net::HTTPResponse, additional # information is retrieved, and special title/summary # extraction routines are used if possible. # def Utils.get_html_info(doc, opts={}) case doc when String Utils.get_string_html_info(doc, opts) when Net::HTTPResponse Utils.get_resp_html_info(doc, opts) when URI ret = DataStream.new @@bot.httputil.get_response(doc) { |resp| ret.replace Utils.get_resp_html_info(resp, opts) } return ret else raise end end class ::UrlLinkError < RuntimeError end # This method extracts title, content (first par) and extra # information from the given Net::HTTPResponse _resp_. # # Currently, the only accepted options (in _opts_) are # uri_fragment:: the URI fragment of the original request # full_body:: get the whole body instead of # @@bot.config['http.info_bytes'] bytes only # # Returns a DataStream with the following keys: # text:: the (partial) body # title:: the title of the document (if any) # content:: the first paragraph of the document (if any) # headers:: # the headers of the Net::HTTPResponse. The value is # a Hash whose keys are lowercase forms of the HTTP # header fields, and whose values are Arrays. # def Utils.get_resp_html_info(resp, opts={}) case resp when Net::HTTPSuccess loc = URI.parse(resp['x-rbot-location'] || resp['location']) rescue nil if loc and loc.fragment and not loc.fragment.empty? opts[:uri_fragment] ||= loc.fragment end ret = DataStream.new(opts.dup) ret[:headers] = resp.to_hash ret[:text] = partial = opts[:full_body] ? resp.body : resp.partial_body(@@bot.config['http.info_bytes']) filtered = Utils.try_htmlinfo_filters(ret) if filtered return filtered elsif resp['content-type'] =~ /^text\/|(?:x|ht)ml/ ret.merge!(Utils.get_string_html_info(partial, opts)) end return ret else raise UrlLinkError, "getting link (#{resp.code} - #{resp.message})" end end # This method runs an appropriately-crafted DataStream _ds_ through the # filters in the :htmlinfo filter group, in order. If one of the filters # returns non-nil, its results are merged in _ds_ and returned. Otherwise # nil is returned. # # The input DataStream should have the downloaded HTML as primary key # (:text) and possibly a :headers key holding the resonse headers. # def Utils.try_htmlinfo_filters(ds) filters = @@bot.filter_names(:htmlinfo) return nil if filters.empty? cur = nil # TODO filter priority filters.each { |n| debug "testing htmlinfo filter #{n}" cur = @@bot.filter(@@bot.global_filter_name(n, :htmlinfo), ds) debug "returned #{cur.pretty_inspect}" break if cur } return ds.merge(cur) if cur end # HTML info filters often need to check if the webpage location # of a passed DataStream _ds_ matches a given Regexp. def Utils.check_location(ds, rx) debug ds[:headers] if h = ds[:headers] loc = [h['x-rbot-location'],h['location']].flatten.grep(rx) end loc ||= [] debug loc return loc.empty? ? nil : loc end # This method extracts title and content (first par) # from the given HTML or XML document _text_, using # standard methods (String#ircify_html_title, # Utils.ircify_first_html_par) # # Currently, the only accepted option (in _opts_) is # uri_fragment:: the URI fragment of the original request # def Utils.get_string_html_info(text, opts={}) debug "getting string html info" txt = text.dup title = txt.ircify_html_title debug opts if frag = opts[:uri_fragment] and not frag.empty? fragreg = /]+\s+)?(?:name|id)=["']?#{frag}["']?[^>]*>/im debug fragreg debug txt if txt.match(fragreg) # grab the post-match txt = $' end debug txt end c_opts = opts.dup c_opts[:strip] ||= title content = Utils.ircify_first_html_par(txt, c_opts) content = nil if content.empty? return {:title => title, :content => content} end # Get the first pars of the first _count_ _urls_. # The pages are downloaded using the bot httputil service. # Returns an array of the first paragraphs fetched. # If (optional) _opts_ :message is specified, those paragraphs are # echoed as replies to the IRC message passed as _opts_ :message # def Utils.get_first_pars(urls, count, opts={}) idx = 0 msg = opts[:message] retval = Array.new while count > 0 and urls.length > 0 url = urls.shift idx += 1 begin info = Utils.get_html_info(URI.parse(url), opts) par = info[:content] retval.push(par) if par msg.reply "[#{idx}] #{par}", :overlong => :truncate if msg count -=1 end rescue debug "Unable to retrieve #{url}: #{$!}" next end end return retval end # Returns a comma separated list except for the last element # which is joined in with specified conjunction # def Utils.comma_list(words, options={}) defaults = { :join_with => ", ", :join_last_with => _(" and ") } opts = defaults.merge(options) if words.size < 2 words.last else [words[0..-2].join(opts[:join_with]), words.last].join(opts[:join_last_with]) end end end end Irc::Utils.bot = Irc::Bot::Plugins.manager.bot rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/utils/filters.rb0000644002342000234200000001400311411605044023063 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: Stream filters # # Author:: Giuseppe "Oblomov" Bilotta # # This file collects methods to handle 'stream filters', a generic mechanism # to transform text+attributes into other text+attributes module ::Irc class Bot # The DataStream class. A DataStream is just a Hash. The :text key has # a special meaning because it's the value that will be used when # converting to String class DataStream < Hash # call-seq: new(text, hash) # # Create a new DataStream with text _text_ and attributes held by _hash_. # Either parameter can be missing; if _text_ is missing, the text can be # be defined in the _hash_ with a :text key. # def initialize(*args) self.replace(args.pop) if Hash === args.last self[:text] = args.first if args.length > 0 end # Returns the :text key def to_s return self[:text] end end # The DataFilter class. A DataFilter is a wrapper around a block # that can be run on a DataStream to process it. The block is supposed to # return another DataStream object class DataFilter def initialize(&block) raise "No block provided" unless block_given? @block = block end def call(stream) @block.call(stream) end alias :run :call alias :filter :call end # call-seq: # filter(filter1, filter2, ..., filterN, stream) -> stream # filter(filter1, filter2, ..., filterN, text, hash) -> stream # filter(filter1, filter2, ..., filterN, hash) -> stream # # This method processes the DataStream _stream_ with the filters filter1, # filter2, ..., _filterN_, in sequence (the output of each filter is used # as input for the next one. # _stream_ can be provided either as a DataStream or as a String and a Hash # (see DataStream.new). # def filter(*args) @filters ||= {} if Hash === args.last # the stream is a Hash, check if the previous element is not a Symbol if Symbol === args[-2] ds = DataStream.new(args.pop) else ds = DataStream.new(*args.slice!(-2, 2)) end else # the stream is just whatever else ds = DataStream.new(args.pop) end names = args.dup return ds if names.empty? # check if filters exist missing = names - @filters.keys raise "Missing filters: #{missing.join(', ')}" unless missing.empty? fs = @filters.values_at(*names) fs.inject(ds) { |mid, f| mid = f.call(mid) } end # This method returns the global filter name for filter _name_ # in group _group_ def global_filter_name(name, group=nil) (group ? "#{group}.#{name}" : name.to_s).intern end # This method checks if the bot has a filter named _name_ (in group # _group_) def has_filter?(name, group=nil) @filters.key?(global_filter_name(name, group)) end # This method checks if the bot has a filter group named _name_ def has_filter_group?(name) @filter_group.key?(name) end # This method is used to register a new filter def register_filter(name, group=nil, &block) raise "No block provided" unless block_given? @filters ||= {} tlkey = global_filter_name(name, group) key = name.to_sym if has_filter?(tlkey) debug "Overwriting filter #{tlkey}" end @filters[tlkey] = DataFilter.new(&block) if group gkey = group.to_sym @filter_group ||= {} @filter_group[gkey] ||= {} if @filter_group[gkey].key?(key) debug "Overwriting filter #{key} in group #{gkey}" end @filter_group[gkey][key] = @filters[tlkey] end end # This method is used to retrieve the filter names (in a given group) def filter_names(group=nil) if group gkey = group.to_sym return [] unless defined? @filter_group and @filter_group.key?(gkey) return @filter_group[gkey].keys else return [] unless defined? @filters return @filters.keys end end # This method is used to retrieve the filter group names def filter_groups return [] unless defined? @filter_group return @filter_group.keys end # This method clears the filter list and installs the identity filter def clear_filters @filters ||= {} @filters.clear @filter_group ||= {} @filter_group.clear register_filter(:identity) { |stream| stream } end module Plugins class BotModule # read accessor for the default filter group for this BotModule def filter_group @filter_group ||= name end # write accessor for the default filter group for this BotModule def filter_group=(name) @filter_group = name end # define a filter defaulting to the default filter group # for this BotModule def define_filter(filter, &block) @bot.register_filter(filter, self.filter_group, &block) end # load filters associated with the BotModule by looking in # the path(s) specified by the :path option, defaulting to # * Config::datadir/filters/.rb # * botclass/filters/.rb # (note that as we use #dirname() rather than #name(), # since we're looking for datafiles; this is only relevant # for the very few plugins whose dirname differs from name) def load_filters(options={}) case options[:path] when nil file = "#{self.dirname}.rb" paths = [ File.join(Config::datadir, 'filters', file), @bot.path('filters', file) ] when Array paths = options[:path] else paths = [options[:path]] end paths.each do |file| instance_eval(File.read(file), file) if File.exist?(file) end end end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/utils/parse_time.rb0000644002342000234200000001200411411605044023542 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot time parsing utilities # # Author:: Giuseppe "Oblomov" Bilotta # # These routines read a string and return the number of seconds they # represent. module ::Irc module Utils module ParseTime FLOAT_RX = /((?:\d*\.)?\d+)/ ONE_TO_NINE = { :one => 1, :two => 2, :three => 3, :four => 4, :five => 5, :six => 6, :seven => 7, :eight => 8, :nine => 9, } ONE_TO_NINE_RX = Regexp.new ONE_TO_NINE.keys.join('|') TEENS_ETC = { :an => 1, :a => 1, :ten => 10, :eleven => 11, :twelve => 12, :thirteen => 13, :fourteen => 14, :fifteen => 15, :sixteen => 16, :seventeen => 17, :eighteen => 18, :nineteen => 19, } TEENS_ETC_RX = Regexp.new TEENS_ETC.keys.join('|') ENTIES = { :twenty => 20, :thirty => 30, :forty => 40, :fifty => 50, :sixty => 60, } ENTIES_RX = Regexp.new ENTIES.keys.join('|') LITNUM_RX = /(#{ONE_TO_NINE_RX})|(#{TEENS_ETC_RX})|(#{ENTIES_RX})\s*(#{ONE_TO_NINE_RX})?/ FRACTIONS = { :"half" => 0.5, :"half a" => 0.5, :"half an" => 0.5, :"a half" => 0.5, :"a quarter" => 0.25, :"a quarter of" => 0.25, :"a quarter of a" => 0.25, :"a quarter of an" => 0.25, :"three quarter" => 0.75, :"three quarters" => 0.75, :"three quarter of" => 0.75, :"three quarters of" => 0.75, :"three quarter of a" => 0.75, :"three quarters of a" => 0.75, :"three quarter of an" => 0.75, :"three quarters of an" => 0.75, } FRACTION_RX = Regexp.new FRACTIONS.keys.join('|') UNITSPEC_RX = /(years?|months?|s(?:ec(?:ond)?s?)?|m(?:in(?:ute)?s?)?|h(?:(?:ou)?rs?)?|d(?:ays?)?|weeks?)/ # str must much UNITSPEC_RX def ParseTime.time_unit(str) case str[0,1].intern when :s 1 when :m if str[1,1] == 'o' # months 3600*24*30 else #minutes 60 end when :h 3600 when :d 3600*24 when :w 3600*24*7 when :y 3600*24*365 end end # example: half an hour, two and a half weeks, 5 seconds, an hour and 5 minutes def ParseTime.parse_period(str) clean = str.gsub(/\s+/, ' ').strip sofar = 0 until clean.empty? if clean.sub!(/^(#{FRACTION_RX})\s+#{UNITSPEC_RX}/, '') # fraction followed by unit num = FRACTIONS[$1.intern] unit = ParseTime.time_unit($2) elsif clean.sub!(/^#{FLOAT_RX}\s*(?:\s+and\s+(#{FRACTION_RX})\s+)?#{UNITSPEC_RX}/, '') # float plus optional fraction followed by unit num = $1.to_f frac = $2 unit = ParseTime.time_unit($3) clean.strip! if frac.nil? and clean.sub!(/^and\s+(#{FRACTION_RX})/, '') frac = $1 end if frac num += FRACTIONS[frac.intern] end elsif clean.sub!(/^(?:#{LITNUM_RX})\s+(?:and\s+(#{FRACTION_RX})\s+)?#{UNITSPEC_RX}/, '') if $1 num = ONE_TO_NINE[$1.intern] elsif $2 num = TEENS_ETC[$2.intern] elsif $3 num = ENTIES[$3.intern] if $4 num += ONE_TO_NINE[$4.intern] end end frac = $5 unit = ParseTime.time_unit($6) clean.strip! if frac.nil? and clean.sub!(/^and\s+(#{FRACTION_RX})/, '') frac = $1 end if frac num += FRACTIONS[frac.intern] end else raise "invalid time string: #{clean} (parsed #{sofar} so far)" end sofar += num * unit clean.sub!(/^and\s+/, '') end return sofar end # TODO 'at hh:mm:ss', 'next week, 'tomorrow', 'saturday' etc end def Utils.parse_time_offset(str) case str when /^(\d+):(\d+)(?:\:(\d+))?$/ # TODO refactor hour = $1.to_i min = $2.to_i sec = $3.to_i now = Time.now later = Time.mktime(now.year, now.month, now.day, hour, min, sec) # if the given hour is earlier than current hour, given timestr # must have been meant to be in the future if hour < now.hour || hour <= now.hour && min < now.min later += 60*60*24 end return later - now when /^(\d+):(\d+)(am|pm)$/ # TODO refactor hour = $1.to_i min = $2.to_i ampm = $3 if ampm == "pm" hour += 12 end now = Time.now later = Time.mktime(now.year, now.month, now.day, hour, min, now.sec) return later - now else ParseTime.parse_period(str) end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/utils/wordlist.rb0000755002342000234200000000330611411605044023271 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot wordlist provider # # Author:: Raine Virta require "find" module ::Irc class Bot class Wordlist def self.wordlist_base @@wordlist_base ||= Utils.bot.path 'wordlists' end def self.get(path, options={}) opts = { :spaces => false }.merge(options) wordlist_path = File.join(wordlist_base, path) raise "wordlist not found: #{wordlist_path}" unless File.exist?(wordlist_path) # Location is a directory -> combine all lists beneath it wordlist = if File.directory?(wordlist_path) wordlists = [] Find.find(wordlist_path) do |path| next if path == wordlist_path wordlists << path unless File.directory?(path) end wordlists.map { |list| File.readlines(list) }.flatten else File.readlines(wordlist_path) end # wordlists are assumed to be UTF-8, but we need to strip the BOM, if present wordlist.map! { |l| l.sub("\xef\xbb\xbf",'').strip } wordlist.reject do |word| word =~ /\s/ && !opts[:spaces] || word.empty? end end # Return an array with the list of available wordlists. # Available options: # pattern:: pattern that should be matched by the wordlist filename def self.list(options={}) pattern = options[:pattern] || "**" # refuse patterns that contain ../ return [] if pattern =~ /\.\.\// striplen = self.wordlist_base.length+1 Dir.glob(File.join(self.wordlist_base, pattern)).map { |name| name[striplen..-1] } end def self.exist?(path) fn = path.to_s # refuse to check outside of the wordlist base directory return false if fn =~ /\.\.\// File.exist?(File.join(self.wordlist_base, fn)) end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/utils/extends.rb0000644002342000234200000003606011411605044023074 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: Standard classes extensions # # Author:: Giuseppe "Oblomov" Bilotta # # This file collects extensions to standard Ruby classes and to some core rbot # classes to be used by the various plugins # # Please note that global symbols have to be prefixed by :: because this plugin # will be read into an anonymous module # Extensions to the Module class # class ::Module # Many plugins define Struct objects to hold their data. On rescans, lots of # warnings are echoed because of the redefinitions. Using this method solves # the problem, by checking if the Struct already exists, and if it has the # same attributes # def define_structure(name, *members) sym = name.to_sym if Struct.const_defined?(sym) kl = Struct.const_get(sym) if kl.new.members.map { |member| member.intern } == members.map debug "Struct #{sym} previously defined, skipping" const_set(sym, kl) return end end debug "Defining struct #{sym} with members #{members.inspect}" const_set(sym, Struct.new(name.to_s, *members)) end end # DottedIndex mixin: extend a Hash or Array class with this module # to achieve [] and []= methods that automatically split indices # at dots (indices are automatically converted to symbols, too) # # You have to define the single_retrieve(_key_) and # single_assign(_key_,_value_) methods (usually aliased at the # original :[] and :[]= methods) # module ::DottedIndex def rbot_index_split(*ar) keys = ([] << ar).flatten keys.map! { |k| k.to_s.split('.').map { |kk| kk.to_sym rescue nil }.compact }.flatten end def [](*ar) keys = self.rbot_index_split(ar) return self.single_retrieve(keys.first) if keys.length == 1 h = self while keys.length > 1 k = keys.shift h[k] ||= self.class.new h = h[k] end h[keys.last] end def []=(*arr) val = arr.last ar = arr[0..-2] keys = self.rbot_index_split(ar) return self.single_assign(keys.first, val) if keys.length == 1 h = self while keys.length > 1 k = keys.shift h[k] ||= self.class.new h = h[k] end h[keys.last] = val end end # Extensions to the Array class # class ::Array # This method returns a random element from the array, or nil if the array is # empty # def pick_one return nil if self.empty? self[rand(self.length)] end # This method returns a given element from the array, deleting it from the # array itself. The method returns nil if the element couldn't be found. # # If nil is specified, a random element is returned and deleted. # def delete_one(val=nil) return nil if self.empty? if val.nil? index = rand(self.length) else index = self.index(val) return nil unless index end self.delete_at(index) end # shuffle and shuffle! are defined in Ruby >= 1.8.7 # This method returns a new array with the same items as # the receiver, but shuffled unless method_defined? :shuffle def shuffle sort_by { rand } end end # This method shuffles the items in the array unless method_defined? :shuffle! def shuffle! replace shuffle end end end module ::Enumerable # This method is an advanced version of #join # allowing fine control of separators: # # [1,2,3].conjoin(', ', ' and ') # => "1, 2 and 3 # # [1,2,3,4].conjoin{ |i, a, b| i % 2 == 0 ? '.' : '-' } # => "1.2-3.4" # # Code lifted from the ruby facets project: # # git-rev: c8b7395255b977d3c7de268ff563e3c5bc7f1441 # file: lib/core/facets/array/conjoin.rb def conjoin(*args, &block) num = count - 1 return first.to_s if num < 1 sep = [] if block_given? num.times do |i| sep << yield(i, *slice(i, 2)) end else options = (Hash === args.last) ? args.pop : {} separator = args.shift || "" options[-1] = args.shift unless args.empty? sep = [separator] * num if options.key?(:last) options[-1] = options.delete(:last) end options[-1] ||= _(" and ") options.each{ |i, s| sep[i] = s } end zip(sep).join end end # Extensions to the Range class # class ::Range # This method returns a random number between the lower and upper bound # def pick_one len = self.last - self.first len += 1 unless self.exclude_end? self.first + Kernel::rand(len) end alias :rand :pick_one end # Extensions for the Numeric classes # class ::Numeric # This method forces a real number to be not more than a given positive # number or not less than a given positive number, or between two any given # numbers # def clip(left,right=0) raise ArgumentError unless left.kind_of?(Numeric) and right.kind_of?(Numeric) l = [left,right].min u = [left,right].max return l if self < l return u if self > u return self end end # Extensions to the String class # # TODO make riphtml() just call ircify_html() with stronger purify options. # class ::String # This method will return a purified version of the receiver, with all HTML # stripped off and some of it converted to IRC formatting # def ircify_html(opts={}) txt = self.dup # remove scripts txt.gsub!(/]*)?>.*?<\/script>/im, "") # remove styles txt.gsub!(/]*)?>.*?<\/style>/im, "") # bold and strong -> bold txt.gsub!(/<\/?(?:b|strong)(?:\s+[^>]*)?>/im, "#{Bold}") # italic, emphasis and underline -> underline txt.gsub!(/<\/?(?:i|em|u)(?:\s+[^>]*)?>/im, "#{Underline}") ## This would be a nice addition, but the results are horrible ## Maybe make it configurable? # txt.gsub!(/<\/?a( [^>]*)?>/, "#{Reverse}") case val = opts[:a_href] when Reverse, Bold, Underline txt.gsub!(/<(?:\/a\s*|a (?:[^>]*\s+)?href\s*=\s*(?:[^>]*\s*)?)>/, val) when :link_out # Not good for nested links, but the best we can do without something like hpricot txt.gsub!(/]*\s+)?href\s*=\s*(?:([^"'>][^\s>]*)\s+|"((?:[^"]|\\")*)"|'((?:[^']|\\')*)')(?:[^>]*\s+)?>(.*?)<\/a>/) { |match| debug match debug [$1, $2, $3, $4].inspect link = $1 || $2 || $3 str = $4 str + ": " + link } else warning "unknown :a_href option #{val} passed to ircify_html" if val end # If opts[:img] is defined, it should be a String. Each image # will be replaced by the string itself, replacing occurrences of # %{alt} %{dimensions} and %{src} with the alt text, image dimensions # and URL if val = opts[:img] if val.kind_of? String txt.gsub!(//) do |imgtag| attrs = Hash.new imgtag.scan(/([[:alpha:]]+)\s*=\s*(['"])?(.*?)\2/) do |key, quote, value| k = key.downcase.intern rescue 'junk' attrs[k] = value end attrs[:alt] ||= attrs[:title] attrs[:width] ||= '...' attrs[:height] ||= '...' attrs[:dimensions] ||= "#{attrs[:width]}x#{attrs[:height]}" val % attrs end else warning ":img option is not a string" end end # Paragraph and br tags are converted to whitespace txt.gsub!(/<\/?(p|br)(?:\s+[^>]*)?\s*\/?\s*>/i, ' ') txt.gsub!("\n", ' ') txt.gsub!("\r", ' ') # Superscripts and subscripts are turned into ^{...} and _{...} # where the {} are omitted for single characters txt.gsub!(/(.*?)<\/sup>/, '^{\1}') txt.gsub!(/(.*?)<\/sub>/, '_{\1}') txt.gsub!(/(^|_)\{(.)\}/, '\1\2') # List items are converted to *). We don't have special support for # nested or ordered lists. txt.gsub!(/

  • /, ' *) ') # All other tags are just removed txt.gsub!(/<[^>]+>/, '') # Convert HTML entities. We do it now to be able to handle stuff # such as   txt = Utils.decode_html_entities(txt) # Keep unbreakable spaces or conver them to plain spaces? case val = opts[:nbsp] when :space, ' ' txt.gsub!([160].pack('U'), ' ') else warning "unknown :nbsp option #{val} passed to ircify_html" if val end # Remove double formatting options, since they only waste bytes txt.gsub!(/#{Bold}(\s*)#{Bold}/, '\1') txt.gsub!(/#{Underline}(\s*)#{Underline}/, '\1') # Simplify whitespace that appears on both sides of a formatting option txt.gsub!(/\s+(#{Bold}|#{Underline})\s+/, ' \1') txt.sub!(/\s+(#{Bold}|#{Underline})\z/, '\1') txt.sub!(/\A(#{Bold}|#{Underline})\s+/, '\1') # And finally whitespace is squeezed txt.gsub!(/\s+/, ' ') txt.strip! if opts[:limit] && txt.size > opts[:limit] txt = txt.slice(0, opts[:limit]) + "#{Reverse}...#{Reverse}" end # Decode entities and strip whitespace return txt end # As above, but modify the receiver # def ircify_html!(opts={}) old_hash = self.hash replace self.ircify_html(opts) return self unless self.hash == old_hash end # This method will strip all HTML crud from the receiver # def riphtml self.gsub(/<[^>]+>/, '').gsub(/&/,'&').gsub(/"/,'"').gsub(/</,'<').gsub(/>/,'>').gsub(/&ellip;/,'...').gsub(/'/, "'").gsub("\n",'') end # This method tries to find an HTML title in the string, # and returns it if found def get_html_title if defined? ::Hpricot Hpricot(self).at("title").inner_html else return unless Irc::Utils::TITLE_REGEX.match(self) $1 end end # This method returns the IRC-formatted version of an # HTML title found in the string def ircify_html_title self.get_html_title.ircify_html rescue nil end # This method is used to wrap a nonempty String by adding # the prefix and postfix def wrap_nonempty(pre, post, opts={}) if self.empty? String.new else "#{pre}#{self}#{post}" end end end # Extensions to the Regexp class, with some common and/or complex regular # expressions. # class ::Regexp # A method to build a regexp that matches a list of something separated by # optional commas and/or the word "and", an optionally repeated prefix, # and whitespace. def Regexp.new_list(reg, pfx = "") if pfx.kind_of?(String) and pfx.empty? return %r(#{reg}(?:,?(?:\s+and)?\s+#{reg})*) else return %r(#{reg}(?:,?(?:\s+and)?(?:\s+#{pfx})?\s+#{reg})*) end end IN_ON = /in|on/ module Irc # Match a list of channel anmes separated by optional commas, whitespace # and optionally the word "and" CHAN_LIST = Regexp.new_list(GEN_CHAN) # Match "in #channel" or "on #channel" and/or "in private" (optionally # shortened to "in pvt"), returning the channel name or the word 'private' # or 'pvt' as capture IN_CHAN = /#{IN_ON}\s+(#{GEN_CHAN})|(here)|/ IN_CHAN_PVT = /#{IN_CHAN}|in\s+(private|pvt)/ # As above, but with channel lists IN_CHAN_LIST_SFX = Regexp.new_list(/#{GEN_CHAN}|here/, IN_ON) IN_CHAN_LIST = /#{IN_ON}\s+#{IN_CHAN_LIST_SFX}|anywhere|everywhere/ IN_CHAN_LIST_PVT_SFX = Regexp.new_list(/#{GEN_CHAN}|here|private|pvt/, IN_ON) IN_CHAN_LIST_PVT = /#{IN_ON}\s+#{IN_CHAN_LIST_PVT_SFX}|anywhere|everywhere/ # Match a list of nicknames separated by optional commas, whitespace and # optionally the word "and" NICK_LIST = Regexp.new_list(GEN_NICK) end end module ::Irc class BasicUserMessage # We extend the BasicUserMessage class with a method that parses a string # which is a channel list as matched by IN_CHAN(_LIST) and co. The method # returns an array of channel names, where 'private' or 'pvt' is replaced # by the Symbol :"?", 'here' is replaced by the channel of the message or # by :"?" (depending on whether the message target is the bot or a # Channel), and 'anywhere' and 'everywhere' are replaced by Symbol :* # def parse_channel_list(string) return [:*] if [:anywhere, :everywhere].include? string.to_sym string.scan( /(?:^|,?(?:\s+and)?\s+)(?:in|on\s+)?(#{Regexp::Irc::GEN_CHAN}|here|private|pvt)/ ).map { |chan_ar| chan = chan_ar.first case chan.to_sym when :private, :pvt :"?" when :here case self.target when Channel self.target.name else :"?" end else chan end }.uniq end # The recurse depth of a message, for fake messages. 0 means an original # message def recurse_depth unless defined? @recurse_depth @recurse_depth = 0 end @recurse_depth end # Set the recurse depth of a message, for fake messages. 0 should only # be used by original messages def recurse_depth=(val) @recurse_depth = val end end class Bot module Plugins # Maximum fake message recursion MAX_RECURSE_DEPTH = 10 class RecurseTooDeep < RuntimeError end class BotModule # Sometimes plugins need to create a new fake message based on an existing # message: for example, this is done by alias, linkbot, reaction and remotectl. # # This method simplifies the message creation, including a recursion depth # check. # # In the options you can specify the :bot, the :server, the :source, # the :target, the message :class and whether or not to :delegate. To # initialize these entries from an existing message, you can use :from # # Additionally, if :from is given, the reply method of created message # is overriden to reply to :from instead. The #in_thread attribute # for created mesage is also copied from :from # # If you don't specify a :from you should specify a :source. # def fake_message(string, opts={}) if from = opts[:from] o = { :bot => from.bot, :server => from.server, :source => from.source, :target => from.target, :class => from.class, :delegate => true, :depth => from.recurse_depth + 1 }.merge(opts) else o = { :bot => @bot, :server => @bot.server, :target => @bot.myself, :class => PrivMessage, :delegate => true, :depth => 1 }.merge(opts) end raise RecurseTooDeep if o[:depth] > MAX_RECURSE_DEPTH new_m = o[:class].new(o[:bot], o[:server], o[:source], o[:target], string) new_m.recurse_depth = o[:depth] if from # the created message will reply to the originating message class << new_m self end.send(:define_method, :reply) do |*args| debug "replying to '#{from.message}' with #{args.first}" from.reply(*args) end # the created message will follow originating message's in_thread new_m.in_thread = from.in_thread if from.respond_to?(:in_thread) end return new_m unless o[:delegate] method = o[:class].to_s.gsub(/^Irc::|Message$/,'').downcase method = 'privmsg' if method == 'priv' o[:bot].plugins.irc_delegate(method, new_m) end end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/config.rb0000644002342000234200000002444611411605044021534 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot config management from IRC # # Author:: Giuseppe "Oblomov" Bilotta class ConfigModule < CoreBotModule def version_string if $version_timestamp.to_i > 0 ago = _(" [%{secs} ago]") % { :secs => Utils.secs_to_string(Time.now.to_i - $version_timestamp.to_i) } else ago = '' end _("I'm a v. %{version}%{ago} rubybot%{copyright}%{url}") % { :version => $version, :ago => ago, :copyright => ", #{Irc::Bot::COPYRIGHT_NOTICE}", :url => " - #{Irc::Bot::SOURCE_URL}" } end def save @bot.config.save end def handle_list(m, params) modules = [] if params[:module] @bot.config.items.each_key do |key| mod, name = key.to_s.split('.') next unless mod == params[:module] modules.push key unless modules.include?(name) end if modules.empty? m.reply _("no such module %{module}") % {:module => params[:module]} else m.reply modules.join(", ") end else @bot.config.items.each_key do |key| name = key.to_s.split('.').first modules.push name unless modules.include?(name) end m.reply "modules: " + modules.join(", ") end end def handle_get(m, params) key = params[:key].to_s.intern unless @bot.config.items.has_key?(key) m.reply _("no such config key %{key}") % {:key => key} return end return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto) value = @bot.config.items[key].to_s m.reply "#{key}: #{value}" end def handle_desc(m, params) key = params[:key].to_s.intern unless @bot.config.items.has_key?(key) m.reply _("no such config key %{key}") % {:key => key} end m.reply "#{key}: #{@bot.config.items[key].desc}" end def handle_search(m, params) rx = Regexp.new(params[:rx].to_s, true) cfs = [] @bot.config.items.each do |k, v| cfs << [Bold + k.to_s + Bold, v.desc] if k.to_s.match(rx) or (v.desc.match(rx) rescue false) end if cfs.empty? m.reply _("no config key found matching %{r}") % { :r => params[:rx].to_s} else m.reply _("possible keys: %{kl}") % { :kl => cfs.map { |c| c.first}.sort.join(', ') } if cfs.length > 1 m.reply cfs.map { |c| c.join(': ') }.join("\n") end end def handle_unset(m, params) key = params[:key].to_s.intern unless @bot.config.items.has_key?(key) m.reply _("no such config key %{key}") % {:key => key} end return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto) @bot.config.items[key].unset handle_get(m, params) m.reply _("this config change will take effect on the next restart") if @bot.config.items[key].requires_restart m.reply _("this config change will take effect on the next rescan") if @bot.config.items[key].requires_rescan end def handle_set(m, params) key = params[:key].to_s.intern value = params[:value].join(" ") unless @bot.config.items.has_key?(key) m.reply _("no such config key %{key}") % {:key => key} unless params[:silent] return false end return false if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto) begin @bot.config.items[key].set_string(value) rescue ArgumentError => e m.reply _("failed to set %{key}: %{error}") % {:key => key, :error => e.message} unless params[:silent] return false end if @bot.config.items[key].requires_restart m.reply _("this config change will take effect on the next restart") unless params[:silent] return :restart elsif @bot.config.items[key].requires_rescan m.reply _("this config change will take effect on the next rescan") unless params[:silent] return :rescan else m.okay unless params[:silent] return true end end def handle_add(m, params) key = params[:key].to_s.intern values = params[:value].to_s.split(/,\s+/) unless @bot.config.items.has_key?(key) m.reply _("no such config key %{key}") % {:key => key} return end unless @bot.config.items[key].kind_of?(Config::ArrayValue) m.reply _("config key %{key} is not an array") % {:key => key} return end return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto) values.each do |value| begin @bot.config.items[key].add(value) rescue ArgumentError => e m.reply _("failed to add %{value} to %{key}: %{error}") % {:value => value, :key => key, :error => e.message} next end end handle_get(m,{:key => key}) m.reply _("this config change will take effect on the next restart") if @bot.config.items[key].requires_restart m.reply _("this config change will take effect on the next rescan") if @bot.config.items[key].requires_rescan end def handle_rm(m, params) key = params[:key].to_s.intern values = params[:value].to_s.split(/,\s+/) unless @bot.config.items.has_key?(key) m.reply _("no such config key %{key}") % {:key => key} return end unless @bot.config.items[key].kind_of?(Config::ArrayValue) m.reply _("config key %{key} is not an array") % {:key => key} return end return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto) values.each do |value| begin @bot.config.items[key].rm(value) rescue ArgumentError => e m.reply _("failed to remove %{value} from %{key}: %{error}") % {:value => value, :key => key, :error => e.message} next end end handle_get(m,{:key => key}) m.reply _("this config change will take effect on the next restart") if @bot.config.items[key].requires_restart m.reply _("this config change will take effect on the next rescan") if @bot.config.items[key].requires_rescan end def bot_save(m, param) @bot.save m.okay end def bot_rescan(m, param) m.reply _("saving ...") @bot.save m.reply _("rescanning ...") @bot.rescan m.reply _("done. %{plugin_status}") % {:plugin_status => @bot.plugins.status(true)} end def bot_nick(m, param) @bot.nickchg(param[:nick]) @bot.wanted_nick = param[:nick] end def bot_status(m, param) m.reply @bot.status end # TODO is this one of the methods that disappeared when the bot was moved # from the single-file to the multi-file registry? # # def bot_reg_stat(m, param) # m.reply @registry.stat.inspect # end def bot_version(m, param) m.reply version_string end def ctcp_listen(m) who = m.private? ? "me" : m.target case m.ctcp.intern when :VERSION m.ctcp_reply version_string when :SOURCE m.ctcp_reply Irc::Bot::SOURCE_URL end end def handle_help(m, params) m.reply help(params[:topic]) end def help(plugin, topic="") case plugin when "config" case topic when "list" _("config list => list configuration modules, config list => list configuration keys for module ") when "get" _("config get => get configuration value for key ") when "unset" _("reset key to the default") when "set" _("config set => set configuration value for key to ") when "desc" _("config desc => describe what key configures") when "add" _("config add to => add values to key if is an array") when "rm" _("config rm from => remove value from key if is an array") else _("config module - bot configuration. usage: list, desc, get, set, unset, add, rm") # else # "no help for config #{topic}" end when "nick" _("nick => change the bot nick to , if possible") when "status" _("status => display some information on the bot's status") when "save" _("save => save current dynamic data and configuration") when "rescan" _("rescan => reload modules and static facts") when "version" _("version => describes software version") else _("config-related tasks: config, save, rescan, version, nick, status") end end end conf = ConfigModule.new conf.map 'config list :module', :action => 'handle_list', :defaults => {:module => false}, :auth_path => 'show' # TODO this one is presently a security risk, since the bot # stores the master password in the config. Do we need auth levels # on the Bot::Config keys too? conf.map 'config get :key', :action => 'handle_get', :auth_path => 'show' conf.map 'config desc :key', :action => 'handle_desc', :auth_path => 'show' conf.map 'config describe :key', :action => 'handle_desc', :auth_path => 'show::desc!' conf.map 'config search *rx', :action => 'handle_search', :auth_path => 'show' conf.map "save", :action => 'bot_save' conf.map "rescan", :action => 'bot_rescan' conf.map "nick :nick", :action => 'bot_nick' conf.map "status", :action => 'bot_status', :auth_path => 'show::status' # TODO see above # # conf.map "registry stats", # :action => 'bot_reg_stat', # :auth_path => '!config::status' conf.map "version", :action => 'bot_version', :auth_path => 'show::status' conf.map 'config set :key *value', :action => 'handle_set', :auth_path => 'edit' conf.map 'config add *value to :key', :action => 'handle_add', :auth_path => 'edit' conf.map 'config rm *value from :key', :action => 'handle_rm', :auth_path => 'edit' conf.map 'config remove *value from :key', :action => 'handle_rm', :auth_path => 'edit' conf.map 'config del *value from :key', :action => 'handle_rm', :auth_path => 'edit' conf.map 'config delete *value from :key', :action => 'handle_rm', :auth_path => 'edit' conf.map 'config unset :key', :action => 'handle_unset', :auth_path => 'edit' conf.map 'config reset :key', :action => 'handle_unset', :auth_path => 'edit' conf.map 'config help :topic', :action => 'handle_help', :defaults => {:topic => false}, :auth_path => '!help!' conf.default_auth('*', false) conf.default_auth('show', true) conf.default_auth('show::get', false) # TODO these shouldn't be set here, we need a way to let the default # permission be specified together with the ConfigValue conf.default_auth('key', true) conf.default_auth('key::auth::password', false) rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/remote.rb0000644002342000234200000002545311411605044021561 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: Remote service provider for rbot # # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com) # # From an idea by halorgium . # # TODO find a way to manage session id (logging out, manually and/or # automatically) require 'drb/drb' module ::Irc class Bot module Auth # We extend the BotUser class to handle remote logins # class BotUser # A rather simple method to handle remote logins. Nothing special, just a # password check. # def remote_login(password) if password == @password debug "remote login for #{self.inspect} succeeded" return true else return false end end end # We extend the ManagerClass to handle remote logins # class ManagerClass MAX_SESSION_ID = 2**128 - 1 # Creates a session id when the given password matches the given # botusername # def remote_login(botusername, pwd) @remote_users = Hash.new unless defined? @remote_users n = BotUser.sanitize_username(botusername) k = n.to_sym raise "No such BotUser #{n}" unless include?(k) bu = @allbotusers[k] if bu.remote_login(pwd) raise "ran out of session ids!" if @remote_users.length == MAX_SESSION_ID session_id = rand(MAX_SESSION_ID) while @remote_users.has_key?(session_id) session_id = rand(MAX_SESSION_ID) end @remote_users[session_id] = bu return session_id end return false end # Returns the botuser associated with the given session id def remote_user(session_id) return everyone unless session_id return nil unless defined? @remote_users if @remote_users.has_key?(session_id) return @remote_users[session_id] else return nil end end end end # A RemoteMessage is similar to a BasicUserMessage # class RemoteMessage # associated bot attr_reader :bot # when the message was received attr_reader :time # remote client that originated the message attr_reader :source # contents of the message attr_accessor :message def initialize(bot, source, message) @bot = bot @source = source @message = message @time = Time.now end # The target of a RemoteMessage def target @bot end # Remote messages are always 'private' def private? true end end # The RemoteDispatcher is a kind of MessageMapper, tuned to handle # RemoteMessages # class RemoteDispatcher < MessageMapper # It is initialized by passing it the bot instance # def initialize(bot) super end # The map method for the RemoteDispatcher returns the index of the inserted # template # def map(botmodule, *args) super return @templates.length - 1 end # The unmap method for the RemoteDispatcher nils the template at the given index, # therefore effectively removing the mapping # def unmap(botmodule, handle) tmpl = @templates[handle] raise "Botmodule #{botmodule.name} tried to unmap #{tmpl.inspect} that was handled by #{tmpl.botmodule}" unless tmpl.botmodule == botmodule.name debug "Unmapping #{tmpl.inspect}" @templates[handle] = nil @templates.clear unless @templates.compact.size > 0 end # We redefine the handle() method from MessageMapper, taking into account # that @parent is a bot, and that we don't handle fallbacks. # # On failure to dispatch anything, the method returns false. If dispatching # is successfull, the method returns a Hash. # # Presently, the hash returned on success has only one key, :return, whose # value is the actual return value of the successfull dispatch. # # TODO this same kind of mechanism could actually be used in MessageMapper # itself to be able to handle the case of multiple plugins having the same # 'first word' ... # # def handle(m) return false if @templates.empty? failures = [] @templates.each do |tmpl| # Skip this element if it was unmapped next unless tmpl botmodule = @parent.plugins[tmpl.botmodule] options = tmpl.recognize(m) if options.kind_of? Failure failures << options else action = tmpl.options[:action] unless botmodule.respond_to?(action) failures << NoActionFailure.new(tmpl, m) next end auth = tmpl.options[:full_auth_path] debug "checking auth for #{auth}" # We check for private permission if m.bot.auth.permit?(m.source, auth, '?') debug "template match found and auth'd: #{action.inspect} #{options.inspect}" return :return => botmodule.send(action, m, options) end debug "auth failed for #{auth}" # if it's just an auth failure but otherwise the match is good, # don't try any more handlers return false end end failures.each {|r| debug "#{r.template.inspect} => #{r}" } debug "no handler found" return false end end # The Irc::Bot::RemoteObject class represents and object that will take care # of interfacing with remote clients # # Example client session: # # require 'drb' # rbot = DRbObject.new_with_uri('druby://localhost:7268') # id = rbot.delegate(nil, 'remote login someuser somepass')[:return] # rbot.delegate(id, 'some secret command') # # Of course, the remote login is only neede for commands which may not be available # to everyone # class RemoteObject # We don't want this object to be copied clientside, so we make it undumpable include DRbUndumped # Initialization is simple def initialize(bot) @bot = bot end # The delegate method. This is the main method used by remote clients to send # commands to the bot. Most of the time, the method will be called with only # two parameters (session id and a String), but we allow more parameters # for future expansions. # # The session_id can be nil, meaning that the remote client wants to work as # an anoynomus botuser. # def delegate(session_id, *pars) warn "Ignoring extra parameters" if pars.length > 1 cmd = pars.first client = @bot.auth.remote_user(session_id) raise "No such session id #{session_id}" unless client debug "Trying to dispatch command #{cmd.inspect} from #{client.inspect} authorized by #{session_id.inspect}" m = RemoteMessage.new(@bot, client, cmd) @bot.remote_dispatcher.handle(m) end private :instance_variables, :instance_variable_get, :instance_variable_set end # The bot also manages a single (for the moment) remote dispatcher. This method # makes it accessible to the outside world, creating it if necessary. # def remote_dispatcher if defined? @remote_dispatcher @remote_dispatcher else @remote_dispatcher = RemoteDispatcher.new(self) end end # The bot also manages a single (for the moment) remote object. This method # makes it accessible to the outside world, creating it if necessary. # def remote_object if defined? @remote_object @remote_object else @remote_object = RemoteObject.new(self) end end module Plugins # We create a new Ruby module that can be included by BotModules that want to # provide remote interfaces # module RemoteBotModule # The remote_map acts just like the BotModule#map method, except that # the map is registered to the @bot's remote_dispatcher. Also, the remote map handle # is handled for the cleanup management # def remote_map(*args) @remote_maps = Array.new unless defined? @remote_maps @remote_maps << @bot.remote_dispatcher.map(self, *args) end # Unregister the remote maps. # def remote_cleanup return unless defined? @remote_maps @remote_maps.each { |h| @bot.remote_dispatcher.unmap(self, h) } @remote_maps.clear end # Redefine the default cleanup method. # def cleanup super remote_cleanup end end # And just because I like consistency: # module RemoteCoreBotModule include RemoteBotModule end module RemotePlugin include RemoteBotModule end end end end class RemoteModule < CoreBotModule include RemoteCoreBotModule Config.register Config::BooleanValue.new('remote.autostart', :default => true, :requires_rescan => true, :desc => "Whether the remote service provider should be started automatically") Config.register Config::IntegerValue.new('remote.port', :default => 7268, # that's 'rbot' :requires_rescan => true, :desc => "Port on which the remote interface will be presented") Config.register Config::StringValue.new('remote.host', :default => '127.0.0.1', :requires_rescan => true, :desc => "Host on which the remote interface will be presented") def initialize super @port = @bot.config['remote.port'] @host = @bot.config['remote.host'] @drb = nil begin start_service if @bot.config['remote.autostart'] rescue => e error "couldn't start remote service provider: #{e.inspect}" end end def start_service raise "Remote service provider already running" if @drb @drb = DRb.start_service("druby://#{@host}:#{@port}", @bot.remote_object) end def stop_service @drb.stop_service if @drb @drb = nil end def cleanup stop_service super end def handle_start(m, params) if @drb rep = "remote service provider already running" rep << " on port #{@port}" if m.private? else begin start_service(@port) rep = "remote service provider started" rep << " on port #{@port}" if m.private? rescue rep = "couldn't start remote service provider" end end m.reply rep end def remote_test(m, params) @bot.say params[:channel], "This is a remote test" end def remote_login(m, params) id = @bot.auth.remote_login(params[:botuser], params[:password]) raise "login failed" unless id return id end end remote = RemoteModule.new remote.map "remote start", :action => 'handle_start', :auth_path => ':manage:' remote.map "remote stop", :action => 'handle_stop', :auth_path => ':manage:' remote.default_auth('*', false) remote.remote_map "remote test :channel", :action => 'remote_test' remote.remote_map "remote login :botuser :password", :action => 'remote_login' remote.default_auth('login', true) rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/basics.rb0000644002342000234200000001512711411605044021527 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot basic management from IRC # # Author:: Giuseppe "Oblomov" Bilotta class BasicsModule < CoreBotModule Config.register Config::BooleanValue.new('irc.join_after_identify', :default => false, :wizard => true, :requires_restart => true, :desc => "Should the bot wait until its identification is confirmed before joining any channels?") def join_channels @bot.config['irc.join_channels'].each { |c| debug "autojoining channel #{c}" if(c =~ /^(\S+)\s+(\S+)$/i) @bot.join $1, $2 else @bot.join c if(c) end } end def identified join_channels end # on connect, we join the default channels unless we have to wait for # identification. Observe that this means the bot may not connect any channels # until the 'identified' method gets delegated def connect if @bot.config['irc.join_after_identify'] log "waiting for identififcation before JOINing default channels" else join_channels end end def ctcp_listen(m) who = m.private? ? "me" : m.target case m.ctcp.intern when :PING m.ctcp_reply m.message when :TIME m.ctcp_reply Time.now.to_s end end def bot_join(m, param) if param[:pass] @bot.join param[:chan], param[:pass] else @bot.join param[:chan] end end def invite(m) if @bot.auth.allow?(:"basics::move::join", m.source, m.source) @bot.join m.channel end end def bot_part(m, param) if param[:chan] @bot.part param[:chan] else @bot.part m.target if m.public? end end def bot_channel_list(m, param) ret = _('I am in: ') # sort the channels by the base name and then map with prefixes for the # mode and display. ret << @bot.channels.compact.sort { |a,b| a.name.downcase <=> b.name.downcase }.map { |c| c.modes_of(@bot.myself).map{ |mo| m.server.prefix_for_mode(mo) }.to_s + c.name }.join(', ') m.reply ret end def bot_quit(m, param) @bot.quit param[:msg].to_s end def bot_restart(m, param) @bot.restart param[:msg].to_s end def bot_reconnect(m, param) @bot.reconnect param[:msg].to_s end def bot_hide(m, param) @bot.join 0 end def bot_say(m, param) @bot.say param[:where], param[:what].to_s end def bot_action(m, param) @bot.action param[:where], param[:what].to_s end def bot_mode(m, param) @bot.mode param[:where], param[:what], param[:who].join(" ") end def bot_ping(m, param) m.reply "pong" end def bot_quiet(m, param) if param.has_key?(:where) @bot.set_quiet param[:where].sub(/^here$/, m.target.downcase) else @bot.set_quiet end # Make sense when the commmand is given in private or in a non-quieted # channel m.okay end def bot_talk(m, param) if param.has_key?(:where) @bot.reset_quiet param[:where].sub(/^here$/, m.target.downcase) else @bot.reset_quiet end # Make sense when the commmand is given in private or in a non-quieted # channel m.okay end def bot_help(m, param) m.reply @bot.help(param[:topic].join(" ")) end #TODO move these to a "chatback" plugin # when (/^(botsnack|ciggie)$/i) # @bot.say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?) # @bot.say m.replyto, @lang.get("thanks") if(m.private?) # when (/^#{Regexp.escape(@bot.nick)}!*$/) # @bot.say m.replyto, @lang.get("hello_X") % m.sourcenick # handle help requests for "core" topics def help(cmd, topic="") case cmd when "quit" _("quit [] => quit IRC with message ") when "restart" _("restart => completely stop and restart the bot (including reconnect)") when "reconnect" _("reconnect => ask the bot to disconnect and then connect again") when "join" _("join [] => join channel with secret key if specified. #{@bot.myself} also responds to invites if you have the required access level") when "part" _("part => part channel ") when "hide" _("hide => part all channels") when "say" _("say | => say to or in private message to ") when "action" _("action | => does a /me to or in private message to ") when "quiet" _("quiet [in here|] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in ") when "talk" _("talk [in here|] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in ") when "ping" _("ping => replies with a pong") when "mode" _("mode => set channel modes for on to ") # when "botsnack" # return "botsnack => reward #{@bot.myself} for being good" # when "hello" # return "hello|hi|hey|yo [#{@bot.myself}] => greet the bot" else _("%{name}: quit, restart, join, part, hide, save, say, action, topic, quiet, talk, ping, mode") % {:name=>name} #, botsnack, hello end end end basics = BasicsModule.new basics.map "quit *msg", :action => 'bot_quit', :defaults => { :msg => nil }, :auth_path => 'quit' basics.map "restart *msg", :action => 'bot_restart', :defaults => { :msg => nil }, :auth_path => 'quit' basics.map "reconnect *msg", :action => 'bot_reconnect', :defaults => { :msg => nil }, :auth_path => 'quit' basics.map "quiet [in] [:where]", :action => 'bot_quiet', :auth_path => 'talk::set' basics.map "talk [in] [:where]", :action => 'bot_talk', :auth_path => 'talk::set' basics.map "say :where *what", :action => 'bot_say', :auth_path => 'talk::do' basics.map "action :where *what", :action => 'bot_action', :auth_path => 'talk::do' basics.map "mode :where :what *who", :action => 'bot_mode', :auth_path => 'talk::do' basics.map "join :chan :pass", :action => 'bot_join', :defaults => {:pass => nil}, :auth_path => 'move' basics.map "part :chan", :action => 'bot_part', :defaults => {:chan => nil}, :auth_path => 'move' basics.map "channels", :action => 'bot_channel_list', :auth_path => 'move' basics.map "hide", :action => 'bot_hide', :auth_path => 'move' basics.map "ping", :action => 'bot_ping', :auth_path => '!ping!' basics.map "help *topic", :action => 'bot_help', :defaults => { :topic => [""] }, :auth_path => '!help!' basics.default_auth('*', false) rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/unicode.rb0000644002342000234200000000551211411605044021706 0ustar duckdc-users#-- vim:sw=4:et #++ # # :title: Unicode plugin # # Author:: jsn (Dmitry Kim) # # This plugin adds unicode-awareness to rbot. When it's loaded, all the # character strings inside of rbot are assumed to be in proper utf-8 # encoding. The plugin takes care of translation to/from utf-8 on server IO, # if necessary (translation charsets are configurable). # TODO autoconfigure using server-provided allowed charset when these are # available, see also comment in irc.rb require 'iconv' class UnicodePlugin < CoreBotModule Config.register Config::BooleanValue.new( 'encoding.enable', :default => true, :desc => "Support for non-ascii charsets", :on_change => Proc.new { |bot, v| reconfigure_filter(bot) }) Config.register Config::ArrayValue.new( 'encoding.charsets', :default => ['utf-8', 'cp1252', 'iso-8859-15'], :desc => "Ordered list of iconv(3) charsets the bot should try", :validate_item => Proc.new { |x| !!(Iconv.new('utf-8', x) rescue nil) }, :on_change => Proc.new { |bot, v| reconfigure_filter(bot) }) class UnicodeFilter def initialize(oenc, *iencs) o = oenc.dup o += '//ignore' if !o.include?('/') i = iencs[0].dup # i += '//ignore' if !i.include?('/') @iencs = iencs.dup @iconvs = @iencs.map { |_| Iconv.new('utf-8', _) } debug "*** o = #{o}, i = #{i}, iencs = #{iencs.inspect}" @default_in = Iconv.new('utf-8//ignore', i) @default_out = Iconv.new(o, 'utf-8//ignore') end def in(data) rv = nil @iconvs.each_with_index { |ic, idx| begin debug "trying #{@iencs[idx]}" rv = ic.iconv(data) break rescue end } rv = @default_in.iconv(data) if !rv debug ">> #{rv.inspect}" return rv end def out(data) rv = @default_out.iconv(data) rescue data # XXX: yeah, i know :/ debug "<< #{rv}" rv end end def initialize(*a) super @@old_kcode = $KCODE self.class.reconfigure_filter(@bot) end def cleanup debug "cleaning up encodings" @bot.socket.filter = nil $KCODE = @@old_kcode super end def UnicodePlugin.reconfigure_filter(bot) debug "configuring encodings" enable = bot.config['encoding.enable'] if not enable bot.socket.filter = nil $KCODE = @@old_kcode return end charsets = bot.config['encoding.charsets'] charsets = ['utf-8'] if charsets.empty? bot.socket.filter = UnicodeFilter.new(charsets[0], *charsets) $KCODE = 'u' end end UnicodePlugin.new rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/wordlist_ui.rb0000644002342000234200000000111711411605044022621 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: wordlist management from IRC # # Author:: Giuseppe "Oblomov" Bilotta class WordlistModule < CoreBotModule def help(plugin, topic="") _("wordlist list [] => list wordlists (matching )") end def do_list(m, p) found = Wordlist.list(p) if found.empty? m.reply _("no wordlist found") else m.reply _("Wordlists: %{found}") % { :found => found.sort.join(', ') } end end end plugin = WordlistModule.new plugin.map "wordlist list [:pattern]", :action => :do_list rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/filters_ui.rb0000644002342000234200000000304511411605044022424 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: filters management from IRC # # Author:: Giuseppe "Oblomov" Bilotta class FiltersModule < CoreBotModule def initialize super @bot.clear_filters @bot.register_filter(:htmlinfo) { |s| Utils.get_html_info(s.to_s, s) } end def help(plugin, topic="") "filters list [] => list filters (in group ) | filters search => list filters matching regexp " end def do_list(m, params) g = params[:group] ar = @bot.filter_names(g).map { |s| s.to_s }.sort! if ar.empty? if g msg = _("no filters in group %{g}") % {:g => g} else msg = _("no known filters") end else msg = _("known filters: ") << ar.join(", ") end m.reply msg end def do_listgroups(m, params) ar = @bot.filter_groups.map { |s| s.to_s }.sort! if ar.empty? msg = _("no known filter groups") else msg = _("known filter groups: ") << ar.join(", ") end m.reply msg end def do_search(m, params) l = @bot.filter_names.map { |s| s.to_s } pat = params[:pat].to_s sl = l.grep(Regexp.new(pat)) if sl.empty? msg = _("no filters match %{pat}") % { :pat => pat } else msg = _("filters matching %{pat}: ") % { :pat => pat } msg << sl.sort!.join(", ") end m.reply msg end end plugin = FiltersModule.new plugin.map "filters list [:group]", :action => :do_list plugin.map "filters search *pat", :action => :do_search plugin.map "filter groups", :action => :do_listgroups rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/core/auth.rb0000644002342000234200000010557511414326121021232 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: rbot auth management from IRC # # Author:: Giuseppe "Oblomov" Bilotta class AuthModule < CoreBotModule def initialize super # The namespace migration causes each Irc::Auth::PermissionSet to be # unrecoverable, and we have to rename their class name to # Irc::Bot::Auth::PermissionSet @registry.recovery = Proc.new { |val| patched = val.sub("o:\035Irc::Auth::PermissionSet", "o:\042Irc::Bot::Auth::PermissionSet") Marshal.restore(patched) } load_array(:default, true) debug "initialized auth. Botusers: #{@bot.auth.save_array.pretty_inspect}" end def save save_array end def save_array(key=:default) if @bot.auth.changed? @registry[key] = @bot.auth.save_array @bot.auth.reset_changed debug "saved botusers (#{key}): #{@registry[key].pretty_inspect}" end end def load_array(key=:default, forced=false) debug "loading botusers (#{key}): #{@registry[key].pretty_inspect}" @bot.auth.load_array(@registry[key], forced) if @registry.has_key?(key) if @bot.auth.botowner.password != @bot.config['auth.password'] error "Master password is out of sync!" debug " db password: #{@bot.auth.botowner.password}" debug "conf password: #{@bot.config['auth.password']}" error "Using conf password" @bot.auth.botowner.password = @bot.config['auth.password'] end end # The permission parameters accept arguments with the following syntax: # cmd_path... [on #chan .... | in here | in private] # This auxiliary method scans the array _ar_ to see if it matches # the given syntax: it expects + or - signs in front of _cmd_path_ # elements when _setting_ = true # # It returns an array whose first element is the array of cmd_path, # the second element is an array of locations and third an array of # warnings occurred while parsing the strings # def parse_args(ar, setting) cmds = [] locs = [] warns = [] doing_cmds = true next_must_be_chan = false want_more = false last_idx = 0 ar.each_with_index { |x, i| if doing_cmds # parse cmd_path # check if the list is done if x == "on" or x == "in" doing_cmds = false next_must_be_chan = true if x == "on" next end if "+-".include?(x[0]) warns << ArgumentError.new(_("please do not use + or - in front of command %{command} when resetting") % {:command => x}) unless setting else warns << ArgumentError.new(_("+ or - expected in front of %{string}") % {:string => x}) if setting end cmds << x else # parse locations if x[-1].chr == ',' want_more = true else want_more = false end case next_must_be_chan when false locs << x.gsub(/^here$/,'_').gsub(/^private$/,'?') else warns << ArgumentError.new(_("'%{string}' doesn't look like a channel name") % {:string => x}) unless @bot.server.supports[:chantypes].include?(x[0]) locs << x end unless want_more last_idx = i break end end } warns << _("trailing comma") if want_more warns << _("you probably forgot a comma") unless last_idx == ar.length - 1 return cmds, locs, warns end def auth_edit_perm(m, params) setting = m.message.split[1] == "set" splits = params[:args] has_for = splits[-2] == "for" return usage(m) unless has_for begin user = @bot.auth.get_botuser(splits[-1].sub(/^all$/,"everyone")) rescue return m.reply(_("couldn't find botuser %{name}") % {:name => splits[-1]}) end return m.reply(_("you can't change permissions for %{username}") % {:username => user.username}) if user.owner? splits.slice!(-2,2) if has_for cmds, locs, warns = parse_args(splits, setting) errs = warns.select { |w| w.kind_of?(Exception) } unless errs.empty? m.reply _("couldn't satisfy your request: %{errors}") % {:errors => errs.join(',')} return end if locs.empty? locs << "*" end begin locs.each { |loc| ch = loc if m.private? ch = "?" if loc == "_" else ch = m.target.to_s if loc == "_" end cmds.each { |setval| if setting val = setval[0].chr == '+' cmd = setval[1..-1] user.set_permission(cmd, val, ch) else cmd = setval user.reset_permission(cmd, ch) end } } rescue => e m.reply "something went wrong while trying to set the permissions" raise end @bot.auth.set_changed debug "user #{user} permissions changed" m.okay end def auth_view_perm(m, params) begin if params[:user].nil? user = get_botuser_for(m.source) return m.reply(_("you are owner, you can do anything")) if user.owner? else user = @bot.auth.get_botuser(params[:user].sub(/^all$/,"everyone")) return m.reply(_("owner can do anything")) if user.owner? end rescue return m.reply(_("couldn't find botuser %{name}") % {:name => params[:user]}) end perm = user.perm str = [] perm.each { |k, val| next if val.perm.empty? case k when :* str << _("on any channel: ") when :"?" str << _("in private: ") else str << _("on #{k}: ") end sub = [] val.perm.each { |cmd, bool| sub << (bool ? "+" : "-") sub.last << cmd.to_s } str.last << sub.join(', ') } if str.empty? m.reply _("no permissions set for %{user}") % {:user => user.username} else m.reply _("permissions for %{user}:: %{permissions}") % { :user => user.username, :permissions => str.join('; ')} end end def auth_search_perm(m, p) pattern = Regexp.new(p[:pattern].to_s) results = @bot.plugins.maps.select { |k, v| k.match(pattern) } count = results.length max = @bot.config['send.max_lines'] extra = (count > max ? _(". only %{max} will be shown") : "") % { :max => max } m.reply _("%{count} commands found matching %{pattern}%{extra}") % { :count => count, :pattern => pattern, :extra => extra } return if count == 0 results[0,max].each { |cmd, hash| m.reply _("%{cmd}: %{perms}") % { :cmd => cmd, :perms => hash[:auth].join(", ") } } end def find_auth(pseudo) k = pseudo.plugin.intern cmds = @bot.plugins.commands auth = nil if cmds.has_key?(k) cmds[k][:botmodule].handler.each do |tmpl| options, failure = tmpl.recognize(pseudo) next if options.nil? auth = tmpl.options[:full_auth_path] break end end return auth end def auth_allow_deny(m, p) begin botuser = @bot.auth.get_botuser(p[:user].sub(/^all$/,"everyone")) rescue return m.reply(_("couldn't find botuser %{name}") % {:name => p[:user]}) end if p[:where].to_s.empty? where = :* else where = m.parse_channel_list(p[:where].to_s).first # should only be one anyway end if p.has_key? :auth_path auth_path = p[:auth_path] else # pseudo-message to find the template. The source is ignored, and the # target is set according to where the template should be checked # (public or private) # This might still fail in the case of 'everywhere' for commands there are # really only private case where when :"?" pseudo_target = @bot.myself when :* pseudo_target = m.channel else pseudo_target = m.server.channel(where) end pseudo = PrivMessage.new(bot, m.server, m.source, pseudo_target, p[:stuff].to_s) auth_path = find_auth(pseudo) end debug auth_path if auth_path allow = p[:allow] if @bot.auth.permit?(botuser, auth_path, where) return m.reply(_("%{user} can already do that") % {:user => botuser}) if allow else return m.reply(_("%{user} can't do that already") % {:user => botuser}) if !allow end cmd = PrivMessage.new(bot, m.server, m.source, m.target, "permissions set %{sign}%{path} %{where} for %{user}" % { :path => auth_path, :user => p[:user], :sign => (allow ? '+' : '-'), :where => p[:where].to_s }) handle(cmd) else m.reply(_("sorry, %{cmd} doesn't look like a valid command. maybe you misspelled it, or you need to specify it should be in private?") % { :cmd => p[:stuff].to_s }) end end def auth_allow(m, p) auth_allow_deny(m, p.merge(:allow => true)) end def auth_deny(m, p) auth_allow_deny(m, p.merge(:allow => false)) end def get_botuser_for(user) @bot.auth.irc_to_botuser(user) end def get_botusername_for(user) get_botuser_for(user).username end def say_welcome(m) m.reply _("welcome, %{user}") % {:user => get_botusername_for(m.source)} end def auth_auth(m, params) params[:botuser] = 'owner' auth_login(m,params) end def auth_login(m, params) begin case @bot.auth.login(m.source, params[:botuser], params[:password]) when true say_welcome(m) @bot.auth.set_changed else m.reply _("sorry, can't do") end rescue => e m.reply _("couldn't login: %{exception}") % {:exception => e} raise end end def auth_autologin(m, params) u = do_autologin(m.source) if u.default? m.reply _("I couldn't find anything to let you login automatically") else say_welcome(m) end end def do_autologin(user) @bot.auth.autologin(user) end def auth_whoami(m, params) m.reply _("you are %{who}") % { :who => get_botusername_for(m.source).gsub( /^everyone$/, _("no one that I know")).gsub( /^owner$/, _("my boss")) } end def auth_whois(m, params) return auth_whoami(m, params) if !m.public? u = m.channel.users[params[:user]] return m.reply("I don't see anyone named '#{params[:user]}' here") unless u m.reply _("#{params[:user]} is %{who}") % { :who => get_botusername_for(u).gsub( /^everyone$/, _("no one that I know")).gsub( /^owner$/, _("my boss")) } end def help(cmd, topic="") case cmd when "login" return _("login [] []: logs in to the bot as botuser with password . When using the full form, you must contact the bot in private. can be omitted if allows login-by-mask and your netmask is among the known ones. if is omitted too autologin will be attempted") when "whoami" return _("whoami: names the botuser you're linked to") when "who" return _("who is : names the botuser is linked to") when /^permission/ case topic when "syntax" return _("a permission is specified as module::path::to::cmd; when you want to enable it, prefix it with +; when you want to disable it, prefix it with -; when using the +reset+ command, do not use any prefix") when "set", "reset", "[re]set", "(re)set" return _("permissions [re]set [in ] for : sets or resets the permissions for botuser in channel (use ? to change the permissions for private addressing)") when "view" return _("permissions view [for ]: display the permissions for user ") when "search" return _("permissions search : display the permissions associated with the commands matching ") else return _("permission topics: syntax, (re)set, view, search") end when "user" case topic when "show" return _("user show : shows info about the user; can be any of autologin, login-by-mask, netmasks") when /^(en|dis)able/ return _("user enable|disable : turns on or off (autologin, login-by-mask)") when "set" return _("user set password : sets the user password to ; passwords can only contain upper and lowercase letters and numbers, and must be at least 4 characters long") when "add", "rm" return _("user add|rm netmask : adds/removes netmask from the list of netmasks known to the botuser you're linked to") when "reset" return _("user reset : resets to the default values. can be +netmasks+ (the list will be emptied), +autologin+ or +login-by-mask+ (will be reset to the default value) or +password+ (a new one will be generated and you'll be told in private)") when "tell" return _("user tell the password for : contacts in private to tell him/her the password for ") when "create" return _("user create : create botuser named with password . The password can be omitted, in which case a random one will be generated. The should only contain alphanumeric characters and the underscore (_)") when "list" return _("user list : lists all the botusers") when "destroy" return _("user destroy : destroys . This function %{highlight}must%{highlight} be called in two steps. On the first call is queued for destruction. On the second call, which must be in the form 'user confirm destroy ', the botuser will be destroyed. If you want to cancel the destruction, issue the command 'user cancel destroy '") % {:highlight => Bold} else return _("user topics: show, enable|disable, add|rm netmask, set, reset, tell, create, list, destroy") end when "auth" return _("auth : log in as the bot owner; other commands: login, whoami, permissions syntax, permissions [re]set, permissions view, user, meet, hello, allow, deny") when "meet" return _("meet [as ]: creates a bot user for nick, calling it user (defaults to the nick itself)") when "hello" return _("hello: creates a bot user for the person issuing the command") when "allow" return [ _("allow to do []: gives botuser the permissions to execute a command such as the provided sample command"), _("(in private or in channel, according to the optional )."), _(" should be a full command, not just the command keyword --"), _("correct: allow user to do addquote stuff --"), _("wrong: allow user to do addquote.") ].join(" ") when "deny" return [ _("deny from doing []: removes from botuser the permissions to execute a command such as the provided sample command"), _("(in private or in channel, according to the optional )."), _(" should be a full command, not just the command keyword --"), _("correct: deny user from doing addquote stuff --"), _("wrong: deny user from doing addquote.") ].join(" ") else return _("auth commands: auth, login, whoami, who, permission[s], user, meet, hello, allow, deny") end end def need_args(cmd) _("sorry, I need more arguments to %{command}") % {:command => cmd} end def not_args(cmd, *stuff) _("I can only %{command} these: %{arguments}") % {:command => cmd, :arguments => stuff.join(', ')} end def set_prop(botuser, prop, val) k = prop.to_s.gsub("-","_") botuser.send( (k + "=").to_sym, val) if prop == :password and botuser == @bot.auth.botowner @bot.config.items[:'auth.password'].set_string(@bot.auth.botowner.password) end end def reset_prop(botuser, prop) k = prop.to_s.gsub("-","_") botuser.send( ("reset_"+k).to_sym) end def ask_bool_prop(botuser, prop) k = prop.to_s.gsub("-","_") botuser.send( (k + "?").to_sym) end def auth_manage_user(m, params) splits = params[:data] cmd = splits.first return auth_whoami(m, params) if cmd.nil? botuser = get_botuser_for(m.source) # By default, we do stuff on the botuser the irc user is bound to butarget = botuser has_for = splits[-2] == "for" if has_for butarget = @bot.auth.get_botuser(splits[-1]) rescue nil return m.reply(_("no such bot user %{user}") % {:user => splits[-1]}) unless butarget splits.slice!(-2,2) end return m.reply(_("you can't mess with %{user}") % {:user => butarget.username}) if butarget.owner? && botuser != butarget bools = [:autologin, :"login-by-mask"] can_set = [:password] can_addrm = [:netmasks] can_reset = bools + can_set + can_addrm can_show = can_reset + ["perms"] begin case cmd.to_sym when :show return m.reply(_("you can't see the properties of %{user}") % {:user => butarget.username}) if botuser != butarget && !botuser.permit?("auth::show::other") case splits[1] when nil, "all" props = can_reset when "password" if botuser != butarget return m.reply(_("no way I'm telling you the master password!")) if butarget == @bot.auth.botowner return m.reply(_("you can't ask for someone else's password")) end return m.reply(_("c'mon, you can't be asking me seriously to tell you the password in public!")) if m.public? return m.reply(_("the password for %{user} is %{password}") % { :user => butarget.username, :password => butarget.password }) else props = splits[1..-1] end str = [] props.each { |arg| k = arg.to_sym next if k == :password case k when *bools if ask_bool_prop(butarget, k) str << _("can %{action}") % {:action => k} else str << _("can not %{action}") % {:action => k} end when :netmasks if butarget.netmasks.empty? str << _("knows no netmasks") else str << _("knows %{netmasks}") % {:netmasks => butarget.netmasks.join(", ")} end end } return m.reply("#{butarget.username} #{str.join('; ')}") when :enable, :disable return m.reply(_("you can't change the default user")) if butarget.default? && !botuser.permit?("auth::edit::other::default") return m.reply(_("you can't edit %{user}") % {:user => butarget.username}) if butarget != botuser && !botuser.permit?("auth::edit::other") return m.reply(need_args(cmd)) unless splits[1] things = [] skipped = [] splits[1..-1].each { |a| arg = a.to_sym if bools.include?(arg) set_prop(butarget, arg, cmd.to_sym == :enable) things << a else skipped << a end } m.reply(_("I ignored %{things} because %{reason}") % { :things => skipped.join(', '), :reason => not_args(cmd, *bools)}) unless skipped.empty? if things.empty? m.reply _("I haven't changed anything") else @bot.auth.set_changed return auth_manage_user(m, {:data => ["show"] + things + ["for", butarget.username] }) end when :set return m.reply(_("you can't change the default user")) if butarget.default? && !botuser.permit?("auth::edit::default") return m.reply(_("you can't edit %{user}") % {:user=>butarget.username}) if butarget != botuser && !botuser.permit?("auth::edit::other") return m.reply(need_args(cmd)) unless splits[1] arg = splits[1].to_sym return m.reply(not_args(cmd, *can_set)) unless can_set.include?(arg) argarg = splits[2] return m.reply(need_args([cmd, splits[1]].join(" "))) unless argarg if arg == :password && m.public? return m.reply(_("is that a joke? setting the password in public?")) end set_prop(butarget, arg, argarg) @bot.auth.set_changed auth_manage_user(m, {:data => ["show", arg.to_s, "for", butarget.username] }) when :reset return m.reply(_("you can't change the default user")) if butarget.default? && !botuser.permit?("auth::edit::default") return m.reply(_("you can't edit %{user}") % {:user=>butarget.username}) if butarget != botuser && !botuser.permit?("auth::edit::other") return m.reply(need_args(cmd)) unless splits[1] things = [] skipped = [] splits[1..-1].each { |a| arg = a.to_sym if can_reset.include?(arg) reset_prop(butarget, arg) things << a else skipped << a end } m.reply(_("I ignored %{things} because %{reason}") % { :things => skipped.join(', '), :reason => not_args(cmd, *can_reset)}) unless skipped.empty? if things.empty? m.reply _("I haven't changed anything") else @bot.auth.set_changed @bot.say(m.source, _("the password for %{user} is now %{password}") % {:user => butarget.username, :password => butarget.password}) if things.include?("password") return auth_manage_user(m, {:data => (["show"] + things - ["password"]) + ["for", butarget.username]}) end when :add, :rm, :remove, :del, :delete return m.reply(_("you can't change the default user")) if butarget.default? && !botuser.permit?("auth::edit::default") return m.reply(_("you can't edit %{user}") % {:user => butarget.username}) if butarget != botuser && !botuser.permit?("auth::edit::other") arg = splits[1] if arg.nil? or arg !~ /netmasks?/ or splits[2].nil? return m.reply(_("I can only add/remove netmasks. See +help user add+ for more instructions")) end method = cmd.to_sym == :add ? :add_netmask : :delete_netmask failed = [] splits[2..-1].each { |mask| begin butarget.send(method, mask.to_irc_netmask(:server => @bot.server)) rescue => e debug "failed with #{e.message}" debug e.backtrace.join("\n") failed << mask end } m.reply "I failed to #{cmd} #{failed.join(', ')}" unless failed.empty? @bot.auth.set_changed return auth_manage_user(m, {:data => ["show", "netmasks", "for", butarget.username] }) else m.reply _("sorry, I don't know how to %{request}") % {:request => m.message} end rescue => e m.reply _("couldn't %{cmd}: %{exception}") % {:cmd => cmd, :exception => e} end end def auth_meet(m, params) nick = params[:nick] if !nick # we are actually responding to a 'hello' command unless m.botuser.transient? m.reply @bot.lang.get('hello_X') % m.botuser return end nick = m.sourcenick irc_user = m.source else # m.channel is always an Irc::Channel because the command is either # public-only 'meet' or private/public 'hello' which was handled by # the !nick case, so this shouldn't fail irc_user = m.channel.users[nick] return m.reply("I don't see anyone named '#{nick}' here") unless irc_user end # BotUser name buname = params[:user] || nick begin call_event(:botuser,:pre_perm, {:irc_user => irc_user, :bot_user => buname}) met = @bot.auth.make_permanent(irc_user, buname) @bot.auth.set_changed call_event(:botuser,:post_perm, {:irc_user => irc_user, :bot_user => buname}) m.reply @bot.lang.get('hello_X') % met @bot.say nick, _("you are now registered as %{buname}. I created a random password for you : %{pass} and you can change it at any time by telling me 'user set password ' in private" % { :buname => buname, :pass => met.password }) rescue RuntimeError # or can this happen for other cases too? # TODO autologin if forced m.reply _("but I already know %{buname}" % {:buname => buname}) rescue => e m.reply _("I had problems meeting %{nick}: %{e}" % { :nick => nick, :e => e }) end end def auth_tell_password(m, params) user = params[:user] begin botuser = @bot.auth.get_botuser(params[:botuser]) rescue return m.reply(_("couldn't find botuser %{user}") % {:user => params[:botuser]}) end return m.reply(_("I'm not telling the master password to anyone, pal")) if botuser == @bot.auth.botowner msg = _("the password for botuser %{user} is %{password}") % {:user => botuser.username, :password => botuser.password} @bot.say user, msg @bot.say m.source, _("I told %{user} that %{message}") % {:user => user, :message => msg} end def auth_create_user(m, params) name = params[:name] password = params[:password] return m.reply(_("are you nuts, creating a botuser with a publicly known password?")) if m.public? and not password.nil? begin bu = @bot.auth.create_botuser(name, password) @bot.auth.set_changed rescue => e m.reply(_("failed to create %{user}: %{exception}") % {:user => name, :exception => e}) debug e.inspect + "\n" + e.backtrace.join("\n") return end m.reply(_("created botuser %{user}") % {:user => bu.username}) end def auth_list_users(m, params) # TODO name regexp to filter results list = @bot.auth.save_array.inject([]) { |list, x| ['everyone', 'owner'].include?(x[:username]) ? list : list << x[:username] } if defined?(@destroy_q) list.map! { |x| @destroy_q.include?(x) ? x + _(" (queued for destruction)") : x } end return m.reply(_("I have no botusers other than the default ones")) if list.empty? return m.reply(n_("botuser: %{list}", "botusers: %{list}", list.length) % {:list => list.join(', ')}) end def auth_destroy_user(m, params) @destroy_q = [] unless defined?(@destroy_q) buname = params[:name] return m.reply(_("You can't destroy %{user}") % {:user => buname}) if ["everyone", "owner"].include?(buname) mod = params[:modifier].to_sym rescue nil buser_array = @bot.auth.save_array buser_hash = buser_array.inject({}) { |h, u| h[u[:username]] = u h } return m.reply(_("no such botuser %{user}") % {:user=>buname}) unless buser_hash.keys.include?(buname) case mod when :cancel if @destroy_q.include?(buname) @destroy_q.delete(buname) m.reply(_("%{user} removed from the destruction queue") % {:user=>buname}) else m.reply(_("%{user} was not queued for destruction") % {:user=>buname}) end return when nil if @destroy_q.include?(buname) return m.reply(_("%{user} already queued for destruction, use %{highlight}user confirm destroy %{user}%{highlight} to destroy it") % {:user=>buname, :highlight=>Bold}) else @destroy_q << buname return m.reply(_("%{user} queued for destruction, use %{highlight}user confirm destroy %{user}%{highlight} to destroy it") % {:user=>buname, :highlight=>Bold}) end when :confirm begin return m.reply(_("%{user} is not queued for destruction yet") % {:user=>buname}) unless @destroy_q.include?(buname) buser_array.delete_if { |u| u[:username] == buname } @destroy_q.delete(buname) @bot.auth.load_array(buser_array, true) @bot.auth.set_changed rescue => e return m.reply(_("failed: %{exception}") % {:exception => e}) end return m.reply(_("botuser %{user} destroyed") % {:user => buname}) end end def auth_copy_ren_user(m, params) source = Auth::BotUser.sanitize_username(params[:source]) dest = Auth::BotUser.sanitize_username(params[:dest]) return m.reply(_("please don't touch the default users")) unless (["everyone", "owner"] & [source, dest]).empty? buser_array = @bot.auth.save_array buser_hash = buser_array.inject({}) { |h, u| h[u[:username]] = u h } return m.reply(_("no such botuser %{source}") % {:source=>source}) unless buser_hash.keys.include?(source) return m.reply(_("botuser %{dest} exists already") % {:dest=>dest}) if buser_hash.keys.include?(dest) copying = m.message.split[1] == "copy" begin if copying h = {} buser_hash[source].each { |k, val| h[k] = val.dup } else h = buser_hash[source] end h[:username] = dest buser_array << h if copying @bot.auth.load_array(buser_array, true) @bot.auth.set_changed call_event(:botuser, copying ? :copy : :rename, :source => source, :dest => dest) rescue => e return m.reply(_("failed: %{exception}") % {:exception=>e}) end if copying m.reply(_("botuser %{source} copied to %{dest}") % {:source=>source, :dest=>dest}) else m.reply(_("botuser %{source} renamed to %{dest}") % {:source=>source, :dest=>dest}) end end def auth_export(m, params) exportfile = @bot.path "new-auth.users" what = params[:things] has_to = what[-2] == "to" if has_to exportfile = @bot.path what[-1] what.slice!(-2,2) end what.delete("all") m.reply _("selecting data to export ...") buser_array = @bot.auth.save_array buser_hash = buser_array.inject({}) { |h, u| h[u[:username]] = u h } if what.empty? we_want = buser_hash else we_want = buser_hash.delete_if { |key, val| not what.include?(key) } end m.reply _("preparing data for export ...") begin yaml_hash = {} we_want.each { |k, val| yaml_hash[k] = {} val.each { |kk, v| case kk when :username next when :netmasks yaml_hash[k][kk] = [] v.each { |nm| yaml_hash[k][kk] << { :fullform => nm.fullform, :casemap => nm.casemap.to_s } } else yaml_hash[k][kk] = v end } } rescue => e m.reply _("failed to prepare data: %{exception}") % {:exception=>e} debug e.backtrace.dup.unshift(e.inspect).join("\n") return end m.reply _("exporting to %{file} ...") % {:file=>exportfile} begin # m.reply yaml_hash.inspect File.open(exportfile, "w") do |file| file.puts YAML::dump(yaml_hash) end rescue => e m.reply _("failed to export users: %{exception}") % {:exception=>e} debug e.backtrace.dup.unshift(e.inspect).join("\n") return end m.reply _("done") end def auth_import(m, params) importfile = @bot.path "new-auth.users" what = params[:things] has_from = what[-2] == "from" if has_from importfile = @bot.path what[-1] what.slice!(-2,2) end what.delete("all") m.reply _("reading %{file} ...") % {:file=>importfile} begin yaml_hash = YAML::load_file(importfile) rescue => e m.reply _("failed to import from: %{exception}") % {:exception=>e} debug e.backtrace.dup.unshift(e.inspect).join("\n") return end # m.reply yaml_hash.inspect m.reply _("selecting data to import ...") if what.empty? we_want = yaml_hash else we_want = yaml_hash.delete_if { |key, val| not what.include?(key) } end m.reply _("parsing data from import ...") buser_hash = {} begin yaml_hash.each { |k, val| buser_hash[k] = { :username => k } val.each { |kk, v| case kk when :netmasks buser_hash[k][kk] = [] v.each { |nm| buser_hash[k][kk] << nm[:fullform].to_irc_netmask(:casemap => nm[:casemap].to_irc_casemap).to_irc_netmask(:server => @bot.server) } else buser_hash[k][kk] = v end } } rescue => e m.reply _("failed to parse data: %{exception}") % {:exception=>e} debug e.backtrace.dup.unshift(e.inspect).join("\n") return end # m.reply buser_hash.inspect org_buser_array = @bot.auth.save_array org_buser_hash = org_buser_array.inject({}) { |h, u| h[u[:username]] = u h } # TODO we may want to do a(n optional) key-by-key merge # org_buser_hash.merge!(buser_hash) new_buser_array = org_buser_hash.values @bot.auth.load_array(new_buser_array, true) @bot.auth.set_changed m.reply _("done") end end auth = AuthModule.new auth.map "user export *things", :action => 'auth_export', :defaults => { :things => ['all'] }, :auth_path => ':manage:fedex:' auth.map "user import *things", :action => 'auth_import', :auth_path => ':manage:fedex:' auth.map "user create :name :password", :action => 'auth_create_user', :defaults => {:password => nil}, :auth_path => ':manage:' auth.map "user [:modifier] destroy :name", :action => 'auth_destroy_user', :requirements => { :modifier => /^(cancel|confirm)?$/ }, :defaults => { :modifier => '' }, :auth_path => ':manage::destroy!' auth.map "user copy :source [to] :dest", :action => 'auth_copy_ren_user', :auth_path => ':manage:' auth.map "user rename :source [to] :dest", :action => 'auth_copy_ren_user', :auth_path => ':manage:' auth.map "meet :nick [as :user]", :action => 'auth_meet', :auth_path => 'user::manage', :private => false auth.map "hello", :action => 'auth_meet', :auth_path => 'user::manage::meet' auth.default_auth("user::manage", false) auth.default_auth("user::manage::meet::hello", true) auth.map "user tell :user the password for :botuser", :action => 'auth_tell_password', :auth_path => ':manage:' auth.map "user list", :action => 'auth_list_users', :auth_path => '::' auth.map "user *data", :action => 'auth_manage_user' auth.map "allow :user to do *stuff [*where]", :action => 'auth_allow', :requirements => {:where => /^(?:anywhere|everywhere|[io]n \S+)$/}, :auth_path => ':edit::other:' auth.map "deny :user from doing *stuff [*where]", :action => 'auth_deny', :requirements => {:where => /^(?:anywhere|everywhere|[io]n \S+)$/}, :auth_path => ':edit::other:' auth.default_auth("user", true) auth.default_auth("edit::other", false) auth.map "whoami", :action => 'auth_whoami', :auth_path => '!*!' auth.map "who is :user", :action => 'auth_whois', :auth_path => '!*!' auth.map "auth :password", :action => 'auth_auth', :public => false, :auth_path => '!login!' auth.map "login :botuser :password", :action => 'auth_login', :public => false, :defaults => { :password => nil }, :auth_path => '!login!' auth.map "login :botuser", :action => 'auth_login', :auth_path => '!login!' auth.map "login", :action => 'auth_autologin', :auth_path => '!login!' auth.map "permissions set *args", :action => 'auth_edit_perm', :auth_path => ':edit::set:' auth.map "permissions reset *args", :action => 'auth_edit_perm', :auth_path => ':edit::set:' auth.map "permissions view [for :user]", :action => 'auth_view_perm', :auth_path => '::' auth.map "permissions search *pattern", :action => 'auth_search_perm', :auth_path => '::' auth.default_auth('*', false) rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/language.rb0000644002342000234200000001030411411605044021106 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: Language module for rbot # # This module takes care of language handling for rbot: # setting the core.language value, loading the appropriate # .lang file etc. module Irc class Bot class Language # This constant hash holds the mapping # from long language names to the usual POSIX # locale specifications Lang2Locale = { :english => 'en', :british_english => 'en_GB', :american_english => 'en_US', :italian => 'it', :french => 'fr', :german => 'de', :dutch => 'nl', :japanese => 'ja', :russian => 'ru', :finnish => 'fi', :traditional_chinese => 'zh_TW', :simplified_chinese => 'zh_CN' } # On WIN32 it appears necessary to have ".UTF-8" explicitly for gettext to use UTF-8 Lang2Locale.each_value {|v| v.replace(v + '.UTF-8')} # Return the shortest language for the current # GetText locale def Language.from_locale return 'english' unless defined?(GetText) lang = locale.language if locale.country str = lang + "_#{locale.country}" if Lang2Locale.value?(str) # Get the shortest key in Lang2Locale which maps to the given lang_country lang_str = Lang2Locale.select { |k, v| v == str }.transpose.first.map { |v| v.to_s }.sort { |a, b| a.length <=> b.length }.first if File.exist?(File.join(Config::datadir, "languages/#{lang_str}.lang")) return lang_str end end end # lang_country didn't work, let's try lan if Lang2Locale.value?(lang) # Get the shortest key in Lang2Locale which maps to the given lang lang_str = Lang2Locale.select { |k, v| v == lang }.transpose.first.map { |v| v.to_s }.sort { |a, b| a.length <=> b.length }.first if File.exist?(File.join(Config::datadir, "/languages/#{lang_str}.lang")) return lang_str end end # all else fail, return 'english' return 'english' end Config.register Config::EnumValue.new('core.language', :default => Irc::Bot::Language.from_locale, :wizard => true, :values => Proc.new{|bot| Dir.new(Config::datadir + "/languages").collect {|f| f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil }.compact }, :on_change => Proc.new {|bot, v| bot.lang.set_language v}, :desc => "Which language file the bot should use") def initialize(bot, language) @bot = bot set_language language end attr_reader :language def set_language(language) lang_str = language.to_s.downcase.gsub(/\s+/,'_') lang_sym = lang_str.intern if defined?(GetText) and Lang2Locale.key?(lang_sym) setlocale(Lang2Locale[lang_sym]) debug "locale set to #{locale}" rbot_gettext_debug else warning "Unable to set locale, unknown language #{language} (#{lang_str})" end file = Config::datadir + "/languages/#{lang_str}.lang" unless(FileTest.exist?(file)) raise "no such language: #{lang_str} (no such file #{file})" end @language = lang_str @file = file scan return if @bot.plugins.nil? @bot.plugins.core_modules.each { |p| if p.respond_to?('set_language') p.set_language(@language) end } @bot.plugins.plugins.each { |p| if p.respond_to?('set_language') p.set_language(@language) end } end def scan @strings = Hash.new current_key = nil IO.foreach(@file) {|l| next if l =~ /^$/ next if l =~ /^\s*#/ if(l =~ /^(\S+):$/) @strings[$1] = Array.new current_key = $1 elsif(l =~ /^\s*(.*)$/) @strings[current_key] << $1 end } end def rescan scan end def get(key) if(@strings.has_key?(key)) return @strings[key][rand(@strings[key].length)] else raise "undefined language key" end end def save File.open(@file, "w") {|file| @strings.each {|key,val| file.puts "#{key}:" val.each_value {|v| file.puts " #{v}" } } } end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/registry/0000755002342000234200000000000011411605044020650 5ustar duckdc-usersrbot-0.9.5+post20100705+gitb3aa806/lib/rbot/registry/bdb.rb0000644002342000234200000003732011411605044021731 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: Berkeley DB interface begin require 'bdb' rescue LoadError fatal "rbot couldn't load the bdb module, perhaps you need to install it? try: http://www.ruby-lang.org/en/raa-list.rhtml?name=bdb" rescue Exception => e fatal "rbot couldn't load the bdb module: #{e.pretty_inspect}" end if not defined? BDB exit 2 end if BDB::VERSION_MAJOR < 4 fatal "Your bdb (Berkeley DB) version #{BDB::VERSION} is too old!" fatal "rbot will only run with bdb version 4 or higher, please upgrade." fatal "For maximum reliability, upgrade to version 4.2 or higher." raise BDB::Fatal, BDB::VERSION + " is too old" end if BDB::VERSION_MAJOR == 4 and BDB::VERSION_MINOR < 2 warning "Your bdb (Berkeley DB) version #{BDB::VERSION} may not be reliable." warning "If possible, try upgrade version 4.2 or later." end # make BTree lookups case insensitive module BDB class CIBtree < Btree def bdb_bt_compare(a, b) if a == nil || b == nil warning "CIBTree: comparing #{a.inspect} (#{self[a].inspect}) with #{b.inspect} (#{self[b].inspect})" end (a||'').downcase <=> (b||'').downcase end end end module Irc # DBHash is for tying a hash to disk (using bdb). # Call it with an identifier, for example "mydata". It'll look for # mydata.db, if it exists, it will load and reference that db. # Otherwise it'll create and empty db called mydata.db class DBHash # absfilename:: use +key+ as an actual filename, don't prepend the bot's # config path and don't append ".db" def initialize(bot, key, absfilename=false) @bot = bot @key = key relfilename = @bot.path key relfilename << '.db' if absfilename && File.exist?(key) # db already exists, use it @db = DBHash.open_db(key) elsif absfilename # create empty db @db = DBHash.create_db(key) elsif File.exist? relfilename # db already exists, use it @db = DBHash.open_db relfilename else # create empty db @db = DBHash.create_db relfilename end end def method_missing(method, *args, &block) return @db.send(method, *args, &block) end def DBHash.create_db(name) debug "DBHash: creating empty db #{name}" return BDB::Hash.open(name, nil, BDB::CREATE | BDB::EXCL, 0600) end def DBHash.open_db(name) debug "DBHash: opening existing db #{name}" return BDB::Hash.open(name, nil, "r+", 0600) end end # DBTree is a BTree equivalent of DBHash, with case insensitive lookups. class DBTree @@env=nil # TODO: make this customizable # Note that it must be at least four times lg_bsize @@lg_max = 8*1024*1024 # absfilename:: use +key+ as an actual filename, don't prepend the bot's # config path and don't append ".db" def initialize(bot, key, absfilename=false) @bot = bot @key = key if @@env.nil? begin @@env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER, "set_lg_max" => @@lg_max) debug "DBTree: environment opened with max log size #{@@env.conf['lg_max']}" rescue => e debug "DBTree: failed to open environment: #{e.pretty_inspect}. Retrying ..." @@env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER) end #@@env = BDB::Env.open(@bot.botclass, BDB::CREATE | BDB::INIT_MPOOL | BDB::RECOVER) end relfilename = @bot.path key relfilename << '.db' if absfilename && File.exist?(key) # db already exists, use it @db = DBTree.open_db(key) elsif absfilename # create empty db @db = DBTree.create_db(key) elsif File.exist? relfilename # db already exists, use it @db = DBTree.open_db relfilename else # create empty db @db = DBTree.create_db relfilename end end def method_missing(method, *args, &block) return @db.send(method, *args, &block) end def DBTree.create_db(name) debug "DBTree: creating empty db #{name}" return @@env.open_db(BDB::CIBtree, name, nil, BDB::CREATE | BDB::EXCL, 0600) end def DBTree.open_db(name) debug "DBTree: opening existing db #{name}" return @@env.open_db(BDB::CIBtree, name, nil, "r+", 0600) end def DBTree.cleanup_logs() begin debug "DBTree: checkpointing ..." @@env.checkpoint rescue Exception => e debug "Failed: #{e.pretty_inspect}" end begin debug "DBTree: flushing log ..." @@env.log_flush logs = @@env.log_archive(BDB::ARCH_ABS) debug "DBTree: deleting archivable logs: #{logs.join(', ')}." logs.each { |log| File.delete(log) } rescue Exception => e debug "Failed: #{e.pretty_inspect}" end end def DBTree.stats() begin debug "General stats:" debug @@env.stat debug "Lock stats:" debug @@env.lock_stat debug "Log stats:" debug @@env.log_stat debug "Txn stats:" debug @@env.txn_stat rescue debug "Couldn't dump stats" end end def DBTree.cleanup_env() begin debug "DBTree: checking transactions ..." has_active_txn = @@env.txn_stat["st_nactive"] > 0 if has_active_txn warning "DBTree: not all transactions completed!" end DBTree.cleanup_logs debug "DBTree: closing environment #{@@env}" path = @@env.home @@env.close @@env = nil if has_active_txn debug "DBTree: keeping file because of incomplete transactions" else debug "DBTree: cleaning up environment in #{path}" BDB::Env.remove("#{path}") end rescue Exception => e error "failed to clean up environment: #{e.pretty_inspect}" end end end end module Irc class Bot # This class is now used purely for upgrading from prior versions of rbot # the new registry is split into multiple DBHash objects, one per plugin class Registry def initialize(bot) @bot = bot upgrade_data upgrade_data2 end # check for older versions of rbot with data formats that require updating # NB this function is called _early_ in init(), pretty much all you have to # work with is @bot.botclass. def upgrade_data oldreg = @bot.path 'registry.db' newreg = @bot.path 'plugin_registry.db' if File.exist?(oldreg) log _("upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format") old = BDB::Hash.open(oldreg, nil, "r+", 0600) new = BDB::CIBtree.open(newreg, nil, BDB::CREATE | BDB::EXCL, 0600) old.each {|k,v| new[k] = v } old.close new.close File.rename(oldreg, oldreg + ".old") end end def upgrade_data2 oldreg = @bot.path 'plugin_registry.db' newdir = @bot.path 'registry' if File.exist?(oldreg) Dir.mkdir(newdir) unless File.exist?(newdir) env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC) dbs = Hash.new log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format") old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env) old.each {|k,v| prefix,key = k.split("/", 2) prefix.downcase! # subregistries were split with a +, now they are in separate folders if prefix.gsub!(/\+/, "/") # Ok, this code needs to be put in the db opening routines dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/") dirs.length.times { |i| dir = dirs[0,i+1].join("/")+"/" unless File.exist?(dir) log _("creating subregistry directory #{dir}") Dir.mkdir(dir) end } end unless dbs.has_key?(prefix) log _("creating db #{@bot.botclass}/registry/#{prefix}.db") dbs[prefix] = BDB::CIBtree.open("#{@bot.botclass}/registry/#{prefix}.db", nil, BDB::CREATE | BDB::EXCL, 0600, "env" => env) end dbs[prefix][key] = v } old.close File.rename(oldreg, oldreg + ".old") dbs.each {|k,v| log _("closing db #{k}") v.close } env.close end end # This class provides persistent storage for plugins via a hash interface. # The default mode is an object store, so you can store ruby objects and # reference them with hash keys. This is because the default store/restore # methods of the plugins' RegistryAccessor are calls to Marshal.dump and # Marshal.restore, # for example: # blah = Hash.new # blah[:foo] = "fum" # @registry[:blah] = blah # then, even after the bot is shut down and disconnected, on the next run you # can access the blah object as it was, with: # blah = @registry[:blah] # The registry can of course be used to store simple strings, fixnums, etc as # well, and should be useful to store or cache plugin data or dynamic plugin # configuration. # # WARNING: # in object store mode, don't make the mistake of treating it like a live # object, e.g. (using the example above) # @registry[:blah][:foo] = "flump" # will NOT modify the object in the registry - remember that Registry#[] # returns a Marshal.restore'd object, the object you just modified in place # will disappear. You would need to: # blah = @registry[:blah] # blah[:foo] = "flump" # @registry[:blah] = blah # # If you don't need to store objects, and strictly want a persistant hash of # strings, you can override the store/restore methods to suit your needs, for # example (in your plugin): # def initialize # class << @registry # def store(val) # val # end # def restore(val) # val # end # end # end # Your plugins section of the registry is private, it has its own namespace # (derived from the plugin's class name, so change it and lose your data). # Calls to registry.each etc, will only iterate over your namespace. class Accessor attr_accessor :recovery # plugins don't call this - a Registry::Accessor is created for them and # is accessible via @registry. def initialize(bot, name) @bot = bot @name = name.downcase @filename = @bot.path 'registry', @name dirs = File.dirname(@filename).split("/") dirs.length.times { |i| dir = dirs[0,i+1].join("/")+"/" unless File.exist?(dir) debug "creating subregistry directory #{dir}" Dir.mkdir(dir) end } @filename << ".db" @registry = nil @default = nil @recovery = nil # debug "initializing registry accessor with name #{@name}" end def registry @registry ||= DBTree.new @bot, "registry/#{@name}" end def flush # debug "fushing registry #{registry}" return if !@registry registry.flush registry.sync end def close # debug "closing registry #{registry}" return if !@registry registry.close end # convert value to string form for storing in the registry # defaults to Marshal.dump(val) but you can override this in your module's # registry object to use any method you like. # For example, if you always just handle strings use: # def store(val) # val # end def store(val) Marshal.dump(val) end # restores object from string form, restore(store(val)) must return val. # If you override store, you should override restore to reverse the # action. # For example, if you always just handle strings use: # def restore(val) # val # end def restore(val) begin Marshal.restore(val) rescue Exception => e error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default") debug e if defined? @recovery and @recovery begin return @recovery.call(val) rescue Exception => ee error _("marshal recovery failed, trying default") debug ee end end return default end end # lookup a key in the registry def [](key) if File.exist?(@filename) && registry.has_key?(key) return restore(registry[key]) else return default end end # set a key in the registry def []=(key,value) registry[key] = store(value) end # set the default value for registry lookups, if the key sought is not # found, the default will be returned. The default default (har) is nil. def set_default (default) @default = default end def default @default && (@default.dup rescue @default) end # just like Hash#each def each(set=nil, bulk=0, &block) return nil unless File.exist?(@filename) registry.each(set, bulk) {|key,value| block.call(key, restore(value)) } end # just like Hash#each_key def each_key(set=nil, bulk=0, &block) return nil unless File.exist?(@filename) registry.each_key(set, bulk) {|key| block.call(key) } end # just like Hash#each_value def each_value(set=nil, bulk=0, &block) return nil unless File.exist?(@filename) registry.each_value(set, bulk) { |value| block.call(restore(value)) } end # just like Hash#has_key? def has_key?(key) return false unless File.exist?(@filename) return registry.has_key?(key) end alias include? has_key? alias member? has_key? alias key? has_key? # just like Hash#has_both? def has_both?(key, value) return false unless File.exist?(@filename) return registry.has_both?(key, store(value)) end # just like Hash#has_value? def has_value?(value) return false unless File.exist?(@filename) return registry.has_value?(store(value)) end # just like Hash#index? def index(value) return nil unless File.exist?(@filename) ind = registry.index(store(value)) if ind return ind else return nil end end # delete a key from the registry def delete(key) return default unless File.exist?(@filename) return registry.delete(key) end # returns a list of your keys def keys return [] unless File.exist?(@filename) return registry.keys end # Return an array of all associations [key, value] in your namespace def to_a return [] unless File.exist?(@filename) ret = Array.new registry.each {|key, value| ret << [key, restore(value)] } return ret end # Return an hash of all associations {key => value} in your namespace def to_hash return {} unless File.exist?(@filename) ret = Hash.new registry.each {|key, value| ret[key] = restore(value) } return ret end # empties the registry (restricted to your namespace) def clear return true unless File.exist?(@filename) registry.clear end alias truncate clear # returns an array of the values in your namespace of the registry def values return [] unless File.exist?(@filename) ret = Array.new self.each {|k,v| ret << restore(v) } return ret end def sub_registry(prefix) return Accessor.new(@bot, @name + "/" + prefix.to_s) end # returns the number of keys in your registry namespace def length self.keys.length end alias size length end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/registry/tc.rb0000644002342000234200000003640711411605044021615 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: DB interface begin require 'bdb' rescue LoadError warning "rbot couldn't load the bdb module. Old registries won't be upgraded" rescue Exception => e warning "rbot couldn't load the bdb module: #{e.pretty_inspect}" end if BDB::VERSION_MAJOR < 4 fatal "Your bdb (Berkeley DB) version #{BDB::VERSION} is too old!" fatal "rbot will only run with bdb version 4 or higher, please upgrade." fatal "For maximum reliability, upgrade to version 4.2 or higher." raise BDB::Fatal, BDB::VERSION + " is too old" end if BDB::VERSION_MAJOR == 4 and BDB::VERSION_MINOR < 2 warning "Your bdb (Berkeley DB) version #{BDB::VERSION} may not be reliable." warning "If possible, try upgrade version 4.2 or later." end require 'tokyocabinet' module Irc if defined? BDB # DBHash is for tying a hash to disk (using bdb). # Call it with an identifier, for example "mydata". It'll look for # mydata.db, if it exists, it will load and reference that db. # Otherwise it'll create and empty db called mydata.db class DBHash # absfilename:: use +key+ as an actual filename, don't prepend the bot's # config path and don't append ".db" def initialize(bot, key, absfilename=false) @bot = bot @key = key relfilename = @bot.path key relfilename << '.db' if absfilename && File.exist?(key) # db already exists, use it @db = DBHash.open_db(key) elsif absfilename # create empty db @db = DBHash.create_db(key) elsif File.exist? relfilename # db already exists, use it @db = DBHash.open_db relfilename else # create empty db @db = DBHash.create_db relfilename end end def method_missing(method, *args, &block) return @db.send(method, *args, &block) end def DBHash.create_db(name) debug "DBHash: creating empty db #{name}" return BDB::Hash.open(name, nil, BDB::CREATE | BDB::EXCL, 0600) end def DBHash.open_db(name) debug "DBHash: opening existing db #{name}" return BDB::Hash.open(name, nil, "r+", 0600) end end # make BTree lookups case insensitive module ::BDB class CIBtree < Btree def bdb_bt_compare(a, b) if a == nil || b == nil warning "CIBTree: comparing #{a.inspect} (#{self[a].inspect}) with #{b.inspect} (#{self[b].inspect})" end (a||'').downcase <=> (b||'').downcase end end end end module ::TokyoCabinet class CIBDB < TokyoCabinet::BDB def open(path, omode) res = super if res self.setcmpfunc(Proc.new do |a, b| a.downcase <=> b.downcase end) end res end end end # DBTree is a BTree equivalent of DBHash, with case insensitive lookups. class DBTree # absfilename:: use +key+ as an actual filename, don't prepend the bot's # config path and don't append ".db" def initialize(bot, key, absfilename=false) @bot = bot @key = key relfilename = @bot.path key relfilename << '.tdb' if absfilename && File.exist?(key) # db already exists, use it @db = DBTree.open_db(key) elsif absfilename # create empty db @db = DBTree.create_db(key) elsif File.exist? relfilename # db already exists, use it @db = DBTree.open_db relfilename else # create empty db @db = DBTree.create_db relfilename end oldbasename = (absfilename ? key : relfilename).gsub(/\.tdb$/, ".db") if File.exists? oldbasename and defined? BDB # upgrading warning "Upgrading old database #{oldbasename}..." oldb = ::BDB::Btree.open(oldbasename, nil, "r", 0600) oldb.each_key do |k| @db.outlist k @db.putlist k, (oldb.duplicates(k, false)) end oldb.close File.rename oldbasename, oldbasename+".bak" end @db end def method_missing(method, *args, &block) return @db.send(method, *args, &block) end def DBTree.create_db(name) debug "DBTree: creating empty db #{name}" db = TokyoCabinet::CIBDB.new res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER) warning "DBTree: creating empty db #{name}: #{db.errmsg(db.ecode) unless res}" return db end def DBTree.open_db(name) debug "DBTree: opening existing db #{name}" db = TokyoCabinet::CIBDB.new res = db.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OWRITER) warning "DBTree:opening db #{name}: #{db.errmsg(db.ecode) unless res}" return db end def DBTree.cleanup_logs() # no-op end def DBTree.stats() # no-op end def DBTree.cleanup_env() # no-op end end end module Irc class Bot # This class is now used purely for upgrading from prior versions of rbot # the new registry is split into multiple DBHash objects, one per plugin class Registry def initialize(bot) @bot = bot upgrade_data upgrade_data2 end # check for older versions of rbot with data formats that require updating # NB this function is called _early_ in init(), pretty much all you have to # work with is @bot.botclass. def upgrade_data if defined? DBHash oldreg = @bot.path 'registry.db' newreg = @bot.path 'plugin_registry.db' if File.exist?(oldreg) log _("upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format") old = ::BDB::Hash.open(oldreg, nil, "r+", 0600) new = TokyoCabinet::CIBDB.new new.open(name, TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER) old.each_key do |k| new.outlist k new.putlist k, (old.duplicates(k, false)) end old.close new.close File.rename(oldreg, oldreg + ".old") end else warning "Won't upgrade data: BDB not installed" end end def upgrade_data2 oldreg = @bot.path 'plugin_registry.db' newdir = @bot.path 'registry' if File.exist?(oldreg) Dir.mkdir(newdir) unless File.exist?(newdir) env = BDB::Env.open(@bot.botclass, BDB::INIT_TRANSACTION | BDB::CREATE | BDB::RECOVER)# | BDB::TXN_NOSYNC) dbs = Hash.new log _("upgrading previous (rbot 0.9.9 or earlier) plugin registry to new split format") old = BDB::CIBtree.open(oldreg, nil, "r+", 0600, "env" => env) old.each {|k,v| prefix,key = k.split("/", 2) prefix.downcase! # subregistries were split with a +, now they are in separate folders if prefix.gsub!(/\+/, "/") # Ok, this code needs to be put in the db opening routines dirs = File.dirname("#{@bot.botclass}/registry/#{prefix}.db").split("/") dirs.length.times { |i| dir = dirs[0,i+1].join("/")+"/" unless File.exist?(dir) log _("creating subregistry directory #{dir}") Dir.mkdir(dir) end } end unless dbs.has_key?(prefix) log _("creating db #{@bot.botclass}/registry/#{prefix}.db") dbs[prefix] = TokyoCabinet::CIBDB.open("#{@bot.botclass}/registry/#{prefix}.db", TokyoCabinet::CIBDB::OREADER | TokyoCabinet::CIBDB::OCREAT | TokyoCabinet::CIBDB::OWRITER) end dbs[prefix][key] = v } old.close File.rename(oldreg, oldreg + ".old") dbs.each {|k,v| log _("closing db #{k}") v.close } env.close end end # This class provides persistent storage for plugins via a hash interface. # The default mode is an object store, so you can store ruby objects and # reference them with hash keys. This is because the default store/restore # methods of the plugins' RegistryAccessor are calls to Marshal.dump and # Marshal.restore, # for example: # blah = Hash.new # blah[:foo] = "fum" # @registry[:blah] = blah # then, even after the bot is shut down and disconnected, on the next run you # can access the blah object as it was, with: # blah = @registry[:blah] # The registry can of course be used to store simple strings, fixnums, etc as # well, and should be useful to store or cache plugin data or dynamic plugin # configuration. # # WARNING: # in object store mode, don't make the mistake of treating it like a live # object, e.g. (using the example above) # @registry[:blah][:foo] = "flump" # will NOT modify the object in the registry - remember that Registry#[] # returns a Marshal.restore'd object, the object you just modified in place # will disappear. You would need to: # blah = @registry[:blah] # blah[:foo] = "flump" # @registry[:blah] = blah # # If you don't need to store objects, and strictly want a persistant hash of # strings, you can override the store/restore methods to suit your needs, for # example (in your plugin): # def initialize # class << @registry # def store(val) # val # end # def restore(val) # val # end # end # end # Your plugins section of the registry is private, it has its own namespace # (derived from the plugin's class name, so change it and lose your data). # Calls to registry.each etc, will only iterate over your namespace. class Accessor attr_accessor :recovery # plugins don't call this - a Registry::Accessor is created for them and # is accessible via @registry. def initialize(bot, name) @bot = bot @name = name.downcase @filename = @bot.path 'registry', @name dirs = File.dirname(@filename).split("/") dirs.length.times { |i| dir = dirs[0,i+1].join("/")+"/" unless File.exist?(dir) debug "creating subregistry directory #{dir}" Dir.mkdir(dir) end } @filename << ".tdb" @registry = nil @default = nil @recovery = nil # debug "initializing registry accessor with name #{@name}" end def registry @registry ||= DBTree.new @bot, "registry/#{@name}" end def flush # debug "fushing registry #{registry}" return if !@registry registry.sync end def close # debug "closing registry #{registry}" return if !@registry registry.close end # convert value to string form for storing in the registry # defaults to Marshal.dump(val) but you can override this in your module's # registry object to use any method you like. # For example, if you always just handle strings use: # def store(val) # val # end def store(val) Marshal.dump(val) end # restores object from string form, restore(store(val)) must return val. # If you override store, you should override restore to reverse the # action. # For example, if you always just handle strings use: # def restore(val) # val # end def restore(val) begin Marshal.restore(val) rescue Exception => e error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default") debug e if defined? @recovery and @recovery begin return @recovery.call(val) rescue Exception => ee error _("marshal recovery failed, trying default") debug ee end end return default end end # lookup a key in the registry def [](key) if File.exist?(@filename) and registry.has_key?(key.to_s) return restore(registry[key.to_s]) else return default end end # set a key in the registry def []=(key,value) registry[key.to_s] = store(value) end # set the default value for registry lookups, if the key sought is not # found, the default will be returned. The default default (har) is nil. def set_default (default) @default = default end def default @default && (@default.dup rescue @default) end # just like Hash#each def each(set=nil, bulk=0, &block) return nil unless File.exist?(@filename) registry.fwmkeys(set).each {|key| block.call(key, restore(registry[key])) } end # just like Hash#each_key def each_key(set=nil, bulk=0, &block) return nil unless File.exist?(@filename) registry.fwmkeys(set).each do |key| block.call(key) end end # just like Hash#each_value def each_value(set=nil, bulk=0, &block) return nil unless File.exist?(@filename) registry.fwmkeys(set).each do |key| block.call(restore(registry[key])) end end # just like Hash#has_key? def has_key?(key) return false unless File.exist?(@filename) return registry.has_key?(key.to_s) end alias include? has_key? alias member? has_key? alias key? has_key? # just like Hash#has_both? def has_both?(key, value) return false unless File.exist?(@filename) registry.has_key?(key.to_s) and registry.has_value?(store(value)) end # just like Hash#has_value? def has_value?(value) return false unless File.exist?(@filename) return registry.has_value?(store(value)) end # just like Hash#index? def index(value) self.each do |k,v| return k if v == value end return nil end # delete a key from the registry def delete(key) return default unless File.exist?(@filename) return registry.delete(key.to_s) end # returns a list of your keys def keys return [] unless File.exist?(@filename) return registry.keys end # Return an array of all associations [key, value] in your namespace def to_a return [] unless File.exist?(@filename) ret = Array.new registry.each {|key, value| ret << [key, restore(value)] } return ret end # Return an hash of all associations {key => value} in your namespace def to_hash return {} unless File.exist?(@filename) ret = Hash.new registry.each {|key, value| ret[key] = restore(value) } return ret end # empties the registry (restricted to your namespace) def clear return true unless File.exist?(@filename) registry.vanish end alias truncate clear # returns an array of the values in your namespace of the registry def values return [] unless File.exist?(@filename) ret = Array.new self.each {|k,v| ret << restore(v) } return ret end def sub_registry(prefix) return Accessor.new(@bot, @name + "/" + prefix.to_s) end # returns the number of keys in your registry namespace def length return 0 unless File.exist?(@filename) registry.length end alias size length # That is btree! def putdup(key, value) registry.putdup(key.to_s, store(value)) end def putlist(key, values) registry.putlist(key.to_s, value.map {|v| store(v)}) end def getlist(key) return [] unless File.exist?(@filename) (registry.getlist(key.to_s) || []).map {|v| restore(v)} end end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/post-config.rb0000644002342000234200000000044111411605044021554 0ustar duckdc-users# write out our datadir so we can reference it at runtime File.open("pkgconfig.rb", "w") {|f| f.puts "module Irc" f.puts " module PKGConfig" f.puts " DATADIR = '#{config('datadir')}/rbot'" f.puts " COREDIR = '#{config('rbdir')}/rbot/core'" f.puts " end" f.puts "end" } rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/compat19.rb0000644002342000234200000000270211411605044020763 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: ruby 1.9 compatibility (monkey)patches require 'timeout' require "thread" class ConditionVariable def wait(mutex, timeout=nil) begin # TODO: mutex should not be used @waiters_mutex.synchronize do @waiters.push(Thread.current) end if timeout elapsed = mutex.sleep timeout if timeout > 0.0 unless timeout > 0.0 and elapsed < timeout t = @waiters_mutex.synchronize { @waiters.delete Thread.current } signal unless t # if we got notified, pass it along raise TimeoutError, "wait timed out" end else mutex.sleep end end nil end end require 'monitor' module MonitorMixin class ConditionVariable def wait(timeout = nil) #if timeout # raise NotImplementedError, "timeout is not implemented yet" #end @monitor.__send__(:mon_check_owner) count = @monitor.__send__(:mon_exit_for_cond) begin @cond.wait(@monitor.instance_variable_get("@mon_mutex"), timeout) return true ensure @monitor.__send__(:mon_enter_for_cond, count) end end def signal @monitor.__send__(:mon_check_owner) @cond.signal end def broadcast @monitor.__send__(:mon_check_owner) @cond.broadcast end end # ConditionVariable def self.extend_object(obj) super(obj) obj.__send__(:mon_initialize) end end # MonitorMixin rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/timer.rb0000644002342000234200000001617411411605044020456 0ustar duckdc-users# changes: # 1. Timer::Timer ---> Timer # 2. timer id is now the object_id of the action # 3. Timer resolution removed, we're always arbitrary precision now # 4. I don't see any obvious races [not that i did see any in old impl, though] # 5. We're tickless now, so no need to jerk start/stop # 6. We should be pretty fast now, wrt old impl # 7. reschedule/remove/block now accept nil as an action id (meaning "current") # 8. repeatability is ignored for 0-period repeatable timers # 9. configure() method superceeds reschedule() [the latter stays as compat] require 'thread' require 'monitor' # Timer handler, manage multiple Action objects, calling them when required. # When the Timer is constructed, a new Thread is created to manage timed # delays and run Actions. # # XXX: there is no way to stop the timer currently. I'm keeping it this way # to weed out old Timer implementation legacy in rbot code. -jsn. class Timer # class representing individual timed action class Action # Time when the Action should be called next attr_accessor :next # Options are: # start:: Time when the Action should be run for the first time. # Repeatable Actions will be repeated after that, see # :period. One-time Actions will not (obviously) # Default: Time.now + :period # period:: How often repeatable Action should be run, in seconds. # Default: 1 # blocked:: if true, Action starts as blocked (i.e. will stay dormant # until unblocked) # args:: Arguments to pass to the Action callback. Default: [] # repeat:: Should the Action be called repeatedly? Default: false # code:: You can specify the Action body using &block, *or* using # this option. def initialize(options = {}, &block) opts = { :period => 1, :blocked => false, :args => [], :repeat => false }.merge(options) @block = nil debug("adding timer #{self} :period => #{opts[:period]}, :repeat => #{opts[:repeat].inspect}") self.configure(opts, &block) debug("added #{self}") end # Provides for on-the-fly reconfiguration of the Actions # Accept the same arguments as the constructor def configure(opts = {}, &block) @period = opts[:period] if opts.include? :period @blocked = opts[:blocked] if opts.include? :blocked @repeat = opts[:repeat] if opts.include? :repeat if block_given? @block = block elsif opts[:code] @block = opts[:code] end raise 'huh?? blockless action?' unless @block if opts.include? :args @args = Array === opts[:args] ? opts[:args] : [opts[:args]] end if opts[:start] and (Time === opts[:start]) self.next = opts[:start] else self.next = Time.now + (opts[:start] || @period) end end # modify the Action period def reschedule(period, &block) self.configure(:period => period, &block) end # blocks an Action, so it won't be run def block @blocked = true end # unblocks a blocked Action def unblock @blocked = false end def blocked? @blocked end # calls the Action callback, resets .next to the Time of the next call, # if the Action is repeatable. def run(now = Time.now) raise 'inappropriate time to run()' unless self.next && self.next <= now self.next = nil begin @block.call(*@args) rescue Exception => e error "Timer action #{self.inspect}: block #{@block.inspect} failed!" error e.pretty_inspect debug e.backtrace.join("\n") end if @repeat && @period > 0 self.next = now + @period end return self.next end end # creates a new Timer and starts it. def initialize self.extend(MonitorMixin) @tick = self.new_cond @thread = nil @actions = Hash.new @current = nil self.start end # Creates and installs a new Action, repeatable by default. # _period_:: Action period # _opts_:: options for Action#new, see there # _block_:: Action callback code # # Returns the id of the created Action def add(period, opts = {}, &block) a = Action.new({:repeat => true, :period => period}.merge(opts), &block) self.synchronize do @actions[a.object_id] = a @tick.signal end return a.object_id end # Creates and installs a new Action, one-time by default. # _period_:: Action delay # _opts_:: options for Action#new, see there # _block_:: Action callback code # # Returns the id of the created Action def add_once(period, opts = {}, &block) self.add(period, {:repeat => false}.merge(opts), &block) end # blocks an existing Action # _aid_:: Action id, obtained previously from add() or add_once() def block(aid) debug "blocking #{aid}" self.synchronize { self[aid].block } end # unblocks an existing blocked Action # _aid_:: Action id, obtained previously from add() or add_once() def unblock(aid) debug "unblocking #{aid}" self.synchronize do self[aid].unblock @tick.signal end end # removes an existing blocked Action # _aid_:: Action id, obtained previously from add() or add_once() def remove(aid) self.synchronize do @actions.delete(aid) # or raise "nonexistent action #{aid}" end end alias :delete :remove # Provides for on-the-fly reconfiguration of Actions # _aid_:: Action id, obtained previously from add() or add_once() # _opts_:: see Action#new # _block_:: (optional) new Action callback code def configure(aid, opts = {}, &block) self.synchronize do self[aid].configure(opts, &block) @tick.signal end end # changes Action period # _aid_:: Action id # _period_:: new period # _block_:: (optional) new Action callback code def reschedule(aid, period, &block) self.configure(aid, :period => period, &block) end def start raise 'already started' if @thread @stopping = false debug "starting timer #{self}" @thread = Thread.new do loop do tmout = self.run_actions break if tmout and tmout < 0 self.synchronize { @tick.wait(tmout) } end end end def stop raise 'already stopped' unless @thread debug "stopping timer #{self}..." @stopping = true self.synchronize { @tick.signal } @thread.join(60) or @thread.kill debug "timer #{self} stopped" @thread = nil end protected def [](aid) aid ||= @current raise "no current action" unless aid raise "nonexistent action #{aid}" unless @actions.include? aid @actions[aid] end def run_actions(now = Time.now) @actions.keys.each do |k| return -1 if @stopping a = @actions[k] or next next if a.blocked? || a.next > now begin @current = k a.run(now) ensure @current = nil end @actions.delete k unless a.next end nxt = @actions.values.find_all { |v| !v.blocked? }.map{ |v| v.next }.min if nxt delta = nxt - now delta = 0 if delta < 0 return delta else return nil end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/load-gettext.rb0000644002342000234200000001444611414373321021742 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: GetText interface for rbot # # Load gettext module and provide fallback in case of failure class GetTextVersionError < Exception end # try to load gettext, or provide fake getttext functions begin # workaround for gettext not checking empty LANGUAGE if ENV["LANGUAGE"] and ENV["LANGUAGE"].empty? ENV.delete "LANGUAGE" end require 'gettext/version' gettext_version = GetText::VERSION.split('.').map {|n| n.to_i} include Comparable # for Array#>= unless gettext_version >= [1, 8, 0] raise GetTextVersionError, "Unsupported ruby-gettext version installed: #{gettext_version.join '.'}; supported versions are 1.8.0 and above" end require 'gettext' include GetText rbot_locale_path = File.join(Irc::Bot::Config.datadir, "../locale/%{locale}/LC_MESSAGES/%{name}.mo") if gettext_version < [2, 0, 0] add_default_locale_path(rbot_locale_path) else LocalePath.add_default_rule(rbot_locale_path) end if GetText.respond_to? :cached= GetText.cached = false elsif TextDomain.respond_to? :cached= TextDomain.cached = false else warning 'This version of ruby-gettext does not support non-cached mode; mo files are not reloaded when setting language' end bindtextdomain 'rbot' module GetText # patch for ruby-gettext 1.x to cope with anonymous modules used by rbot. # bound_targets and related methods are not used nor present in 2.x, and # this patch is not needed if respond_to? :bound_targets, true alias :orig_bound_targets :bound_targets def bound_targets(*a) # :nodoc: bt = orig_bound_targets(*a) rescue [] bt.empty? ? orig_bound_targets(Object) : bt end end require 'stringio' # GetText 2.1.0 does not provide current_textdomain_info, # so we adapt the one from 1.9.10 # TODO we would _really_ like to have a future-proof version of this, # but judging by the ruby gettext source code, this isn't going to # happen anytime soon. if not respond_to? :current_textdomain_info # Show the current textdomain information. This function is for debugging. # * options: options as a Hash. # * :with_messages - show informations with messages of the current mo file. Default is false. # * :out - An output target. Default is STDOUT. # * :with_paths - show the load paths for mo-files. def current_textdomain_info(options = {}) opts = {:with_messages => false, :with_paths => false, :out => STDOUT}.merge(options) ret = nil # this is for 2.1.0 TextDomainManager.each_textdomains(self) {|textdomain, lang| opts[:out].puts "TextDomain name: #{textdomain.name.inspect}" opts[:out].puts "TextDomain current locale: #{lang.to_s.inspect}" opts[:out].puts "TextDomain current mo path: #{textdomain.instance_variable_get(:@locale_path).current_path(lang).inspect}" if opts[:with_paths] opts[:out].puts "TextDomain locale file paths:" textdomain.locale_paths.each do |v| opts[:out].puts " #{v}" end end if opts[:with_messages] opts[:out].puts "The messages in the mo file:" textdomain.current_mo.each{|k, v| opts[:out].puts " \"#{k}\": \"#{v}\"" } end } end end # This method is used to output debug information on the GetText # textdomain, and it's called by the language setting routines # in rbot def rbot_gettext_debug begin gettext_info = StringIO.new current_textdomain_info(:out => gettext_info) # fails sometimes rescue Exception warning "failed to retrieve textdomain info. maybe an mo file doesn't exist for your locale." debug $! ensure gettext_info.string.each_line { |l| debug l} end end end log "gettext loaded" rescue LoadError, GetTextVersionError warning "failed to load ruby-gettext package: #{$!}; translations are disabled" # undefine GetText, in case it got defined because the error was caused by a # wrong version if defined?(GetText) Object.module_eval { remove_const("GetText") } end # dummy functions that return msg_id without translation def _(s) s end def N_(s) s end def n_(s_single, s_plural, n) n > 1 ? s_plural : s_single end def Nn_(s_single, s_plural) n_(s_single, s_plural) end def s_(*args) args[0] end def bindtextdomain_to(*args) end # the following extension to String#% is from ruby-gettext's string.rb file. # it needs to be included in the fallback since the source already use this form =begin string.rb - Extension for String. Copyright (C) 2005,2006 Masao Mutoh You may redistribute it and/or modify it under the same license terms as Ruby. =end # Extension for String class. # # String#% method which accept "named argument". The translator can know # the meaning of the msgids using "named argument" instead of %s/%d style. class String alias :_old_format_m :% # :nodoc: # call-seq: # %(arg) # %(hash) # # Format - Uses str as a format specification, and returns the result of applying it to arg. # If the format specification contains more than one substitution, then arg must be # an Array containing the values to be substituted. See Kernel::sprintf for details of the # format string. This is the default behavior of the String class. # * arg: an Array or other class except Hash. # * Returns: formatted String # # (e.g.) "%s, %s" % ["Masao", "Mutoh"] # # Also you can use a Hash as the "named argument". This is recommanded way for Ruby-GetText # because the translators can understand the meanings of the msgids easily. # * hash: {:key1 => value1, :key2 => value2, ... } # * Returns: formatted String # # (e.g.) "%{firstname}, %{familyname}" % {:firstname => "Masao", :familyname => "Mutoh"} def %(args) if args.kind_of?(Hash) ret = dup args.each {|key, value| ret.gsub!(/\%\{#{key}\}/, value.to_s) } ret else ret = gsub(/%\{/, '%%{') begin ret._old_format_m(args) rescue ArgumentError $stderr.puts " The string:#{ret}" $stderr.puts " args:#{args.inspect}" end end end end end rbot-0.9.5+post20100705+gitb3aa806/lib/rbot/rfc2812.rb0000644002342000234200000014255711411605044020432 0ustar duckdc-users#-- vim:sw=2:et #++ # # :title: RFC 2821 Client Protocol module # # This module defines the Irc::Client class, a class that can handle and # dispatch messages based on RFC 2821 (Internet Relay Chat: Client Protocol) module Irc # - The server sends Replies 001 to 004 to a user upon # successful registration. # "Welcome to the Internet Relay Network # !@" # RPL_WELCOME=001 # "Your host is , running version " RPL_YOURHOST=002 # "This server was created " RPL_CREATED=003 # " " RPL_MYINFO=004 # "005 nick PREFIX=(ov)@+ CHANTYPES=#& :are supported by this server" # # defines the capabilities supported by the server. # # Previous RFCs defined message 005 as follows: # # - Sent by the server to a user to suggest an alternative # server. This is often used when the connection is # refused because the server is already full. # # # "Try server , port " # # RPL_BOUNCE=005 # RPL_ISUPPORT=005 # ":*1 *( " " )" # # - Reply format used by USERHOST to list replies to # the query list. The reply string is composed as # follows: # # reply = nickname [ "*" ] "=" ( "+" / "-" ) hostname # # The '*' indicates whether the client has registered # as an Operator. The '-' or '+' characters represent # whether the client has set an AWAY message or not # respectively. # RPL_USERHOST=302 # ":*1 *( " " )" # # - Reply format used by ISON to list replies to the # query list. # RPL_ISON=303 # - These replies are used with the AWAY command (if # allowed). RPL_AWAY is sent to any client sending a # PRIVMSG to a client which is away. RPL_AWAY is only # sent by the server to which the client is connected. # Replies RPL_UNAWAY and RPL_NOWAWAY are sent when the # client removes and sets an AWAY message. # " :" RPL_AWAY=301 # ":You are no longer marked as being away" RPL_UNAWAY=305 # ":You have been marked as being away" RPL_NOWAWAY=306 # - Replies 311 - 313, 317 - 319 are all replies # generated in response to a WHOIS message. Given that # there are enough parameters present, the answering # server MUST either formulate a reply out of the above # numerics (if the query nick is found) or return an # error reply. The '*' in RPL_WHOISUSER is there as # the literal character and not as a wild card. For # each reply set, only RPL_WHOISCHANNELS may appear # more than once (for long lists of channel names). # The '@' and '+' characters next to the channel name # indicate whether a client is a channel operator or # has been granted permission to speak on a moderated # channel. The RPL_ENDOFWHOIS reply is used to mark # the end of processing a WHOIS message. # " * :" RPL_WHOISUSER=311 # " :" RPL_WHOISSERVER=312 # " :is an IRC operator" RPL_WHOISOPERATOR=313 # " :seconds idle" RPL_WHOISIDLE=317 # " :End of WHOIS list" RPL_ENDOFWHOIS=318 # " :*( ( "@" / "+" ) " " )" RPL_WHOISCHANNELS=319 # - When replying to a WHOWAS message, a server MUST use # the replies RPL_WHOWASUSER, RPL_WHOISSERVER or # ERR_WASNOSUCHNICK for each nickname in the presented # list. At the end of all reply batches, there MUST # be RPL_ENDOFWHOWAS (even if there was only one reply # and it was an error). # " * :" RPL_WHOWASUSER=314 # " :End of WHOWAS" RPL_ENDOFWHOWAS=369 # - Replies RPL_LIST, RPL_LISTEND mark the actual replies # with data and end of the server's response to a LIST # command. If there are no channels available to return, # only the end reply MUST be sent. # Obsolete. Not used. RPL_LISTSTART=321 # " <# visible> :" RPL_LIST=322 # ":End of LIST" RPL_LISTEND=323 # " " RPL_UNIQOPIS=325 # " " RPL_CHANNELMODEIS=324 # " " RPL_CREATIONTIME=329 # " " RPL_CHANNEL_URL=328 # " :No topic is set" RPL_NOTOPIC=331 # - When sending a TOPIC message to determine the # channel topic, one of two replies is sent. If # the topic is set, RPL_TOPIC is sent back else # RPL_NOTOPIC. # " :" RPL_TOPIC=332 # RPL_TOPIC_INFO=333 # " " # # - Returned by the server to indicate that the # attempted INVITE message was successful and is # being passed onto the end client. # RPL_INVITING=341 # " :Summoning user to IRC" # # - Returned by a server answering a SUMMON message to # indicate that it is summoning that user. # RPL_SUMMONING=342 # " " RPL_INVITELIST=346 # " :End of channel invite list" # # - When listing the 'invitations masks' for a given channel, # a server is required to send the list back using the # RPL_INVITELIST and RPL_ENDOFINVITELIST messages. A # separate RPL_INVITELIST is sent for each active mask. # After the masks have been listed (or if none present) a # RPL_ENDOFINVITELIST MUST be sent. # RPL_ENDOFINVITELIST=347 # " " RPL_EXCEPTLIST=348 # " :End of channel exception list" # # - When listing the 'exception masks' for a given channel, # a server is required to send the list back using the # RPL_EXCEPTLIST and RPL_ENDOFEXCEPTLIST messages. A # separate RPL_EXCEPTLIST is sent for each active mask. # After the masks have been listed (or if none present) # a RPL_ENDOFEXCEPTLIST MUST be sent. # RPL_ENDOFEXCEPTLIST=349 # ". :" # # - Reply by the server showing its version details. # # The is the version of the software being # used (including any patchlevel revisions) and the # is used to indicate if the server is # running in "debug mode". # # The "comments" field may contain any comments about # the version or further version details. # RPL_VERSION=351 # - The RPL_WHOREPLY and RPL_ENDOFWHO pair are used # to answer a WHO message. The RPL_WHOREPLY is only # sent if there is an appropriate match to the WHO # query. If there is a list of parameters supplied # with a WHO message, a RPL_ENDOFWHO MUST be sent # after processing each list item with being # the item. # " # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] # : " # RPL_WHOREPLY=352 # " :End of WHO list" RPL_ENDOFWHO=315 # - To reply to a NAMES message, a reply pair consisting # of RPL_NAMREPLY and RPL_ENDOFNAMES is sent by the # server back to the client. If there is no channel # found as in the query, then only RPL_ENDOFNAMES is # returned. The exception to this is when a NAMES # message is sent with no parameters and all visible # channels and contents are sent back in a series of # RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark # the end. # "( "=" / "*" / "@" ) # :[ "@" / "+" ] *( " " [ "@" / "+" ] ) # - "@" is used for secret channels, "*" for private # channels, and "=" for others (public channels). # RPL_NAMREPLY=353 # " :End of NAMES list" RPL_ENDOFNAMES=366 # - In replying to the LINKS message, a server MUST send # replies back using the RPL_LINKS numeric and mark the # end of the list using an RPL_ENDOFLINKS reply. # " : " RPL_LINKS=364 # " :End of LINKS list" RPL_ENDOFLINKS=365 # - When listing the active 'bans' for a given channel, # a server is required to send the list back using the # RPL_BANLIST and RPL_ENDOFBANLIST messages. A separate # RPL_BANLIST is sent for each active banmask. After the # banmasks have been listed (or if none present) a # RPL_ENDOFBANLIST MUST be sent. # " " RPL_BANLIST=367 # " :End of channel ban list" RPL_ENDOFBANLIST=368 # - A server responding to an INFO message is required to # send all its 'info' in a series of RPL_INFO messages # with a RPL_ENDOFINFO reply to indicate the end of the # replies. # ":" RPL_INFO=371 # ":End of INFO list" RPL_ENDOFINFO=374 # - When responding to the MOTD message and the MOTD file # is found, the file is displayed line by line, with # each line no longer than 80 characters, using # RPL_MOTD format replies. These MUST be surrounded # by a RPL_MOTDSTART (before the RPL_MOTDs) and an # RPL_ENDOFMOTD (after). # ":- Message of the day - " RPL_MOTDSTART=375 # ":- " RPL_MOTD=372 # ":End of MOTD command" RPL_ENDOFMOTD=376 # ":You are now an IRC operator" # # - RPL_YOUREOPER is sent back to a client which has # just successfully issued an OPER message and gained # operator status. # RPL_YOUREOPER=381 # " :Rehashing" # # - If the REHASH option is used and an operator sends # a REHASH message, an RPL_REHASHING is sent back to # the operator. # RPL_REHASHING=382 # "You are service " # # - Sent by the server to a service upon successful # registration. # RPL_YOURESERVICE=383 # " :" # # - When replying to the TIME message, a server MUST send # the reply using the RPL_TIME format above. The string # showing the time need only contain the correct day and # time there. There is no further requirement for the # time string. # RPL_TIME=391 # - If the USERS message is handled by a server, the # replies RPL_USERSTART, RPL_USERS, RPL_ENDOFUSERS and # RPL_NOUSERS are used. RPL_USERSSTART MUST be sent # first, following by either a sequence of RPL_USERS # or a single RPL_NOUSER. Following this is # RPL_ENDOFUSERS. # ":UserID Terminal Host" RPL_USERSSTART=392 # ": " RPL_USERS=393 # ":End of users" RPL_ENDOFUSERS=394 # ":Nobody logged in" RPL_NOUSERS=395 # - The RPL_TRACE* are all returned by the server in # response to the TRACE message. How many are # returned is dependent on the TRACE message and # whether it was sent by an operator or not. There # is no predefined order for which occurs first. # Replies RPL_TRACEUNKNOWN, RPL_TRACECONNECTING and # RPL_TRACEHANDSHAKE are all used for connections # which have not been fully established and are either # unknown, still attempting to connect or in the # process of completing the 'server handshake'. # RPL_TRACELINK is sent by any server which handles # a TRACE message and has to pass it on to another # server. The list of RPL_TRACELINKs sent in # response to a TRACE command traversing the IRC # network should reflect the actual connectivity of # the servers themselves along that path. # # RPL_TRACENEWTYPE is to be used for any connection # which does not fit in the other categories but is # being displayed anyway. # RPL_TRACEEND is sent to indicate the end of the list. # "Link # V # # " RPL_TRACELINK=200 # "Try. " RPL_TRACECONNECTING=201 # "H.S. " RPL_TRACEHANDSHAKE=202 # "???? []" RPL_TRACEUNKNOWN=203 # "Oper " RPL_TRACEOPERATOR=204 # "User " RPL_TRACEUSER=205 # "Serv S C # @ V" RPL_TRACESERVER=206 # "Service " RPL_TRACESERVICE=207 # " 0 " RPL_TRACENEWTYPE=208 # "Class " RPL_TRACECLASS=209 # Unused. RPL_TRACERECONNECT=210 # "File " RPL_TRACELOG=261 # " :End of TRACE" RPL_TRACEEND=262 # ":Current local users: 3 Max: 4" RPL_LOCALUSERS=265 # ":Current global users: 3 Max: 4" RPL_GLOBALUSERS=266 # "::Highest connection count: 4 (4 clients) (251 since server was # (re)started)" RPL_STATSCONN=250 # " # #