ruby-ponder-0.2.0/0000755000175000017500000000000011626443546013300 5ustar dookiedookieruby-ponder-0.2.0/spec/0000755000175000017500000000000011626443546014232 5ustar dookiedookieruby-ponder-0.2.0/spec/thaum_spec.rb0000644000175000017500000000370111626443546016710 0ustar dookiedookie$LOAD_PATH.unshift(File.dirname(__FILE__)) require 'spec_helper' require 'ponder' describe Ponder::Thaum do before(:each) do @ponder = Ponder::Thaum.new end it 'sets a default configuration' do @ponder.config.server.should eql('localhost') @ponder.config.port.should equal(6667) @ponder.config.nick.should eql('Ponder') @ponder.config.username.should eql('Ponder') @ponder.config.real_name.should eql('Ponder') @ponder.config.verbose.should be_true @ponder.config.logging.should be_false @ponder.config.reconnect.should be_true @ponder.config.reconnect_interval.should equal(30) @ponder.logger.should be_an_instance_of(Ponder::Logger::BlindIo) @ponder.console_logger.should be_an_instance_of(Ponder::Logger::Twoflogger) end it 'sets the logger correctly' do Ponder::Logger::Twoflogger.should_receive(:new).twice @ponder = Ponder::Thaum.new { |t| t.logging = true } end it 'sets default callbacks' do @ponder.callbacks.should have(3)[:query] end context 'creates a correct default callback for' do it 'PING PONG' do time = Time.now.to_i @ponder.should_receive(:notice).with('Peter', "\001PING #{time}\001") EM.run do @ponder.process_callbacks(:query, {:nick => 'Peter', :message => "\001PING #{time}\001"}) EM.schedule { EM.stop } end end it 'VERSION' do @ponder.should_receive(:notice).with('Peter', "\001VERSION Ponder #{Ponder::VERSION} (https://github.com/tbuehlmann/ponder)\001") EM.run do @ponder.process_callbacks(:query, {:nick => 'Peter', :message => "\001VERSION\001"}) EM.schedule { EM.stop } end end it 'TIME' do @ponder.should_receive(:notice).with('Peter', "\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001") EM.run do @ponder.process_callbacks(:query, {:nick => 'Peter', :message => "\001TIME\001"}) EM.schedule { EM.stop } end end end end ruby-ponder-0.2.0/spec/spec_helper.rb0000644000175000017500000000022311626443546017045 0ustar dookiedookie$LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'rubygems' require 'rspec' ruby-ponder-0.2.0/spec/irc_spec.rb0000644000175000017500000000765211626443546016360 0ustar dookiedookie$LOAD_PATH.unshift(File.dirname(__FILE__)) require 'spec_helper' require 'ponder/thaum' describe Ponder::IRC do before(:each) do @ponder = Ponder::Thaum.new do |t| t.nick = 'Ponder' t.username = 'Ponder' t.real_name = 'Ponder Stibbons' t.reconnect = true end end it 'sends a message to a recipient' do @ponder.should_receive(:raw).with('PRIVMSG recipient :foo bar baz').once @ponder.message('recipient', 'foo bar baz') end it 'registers with the server' do @ponder.should_receive(:raw).with('NICK Ponder').once @ponder.should_receive(:raw).with('USER Ponder * * :Ponder Stibbons').once @ponder.register end it 'registers with the server with a password' do @ponder = Ponder::Thaum.new do |t| t.nick = 'Ponder' t.username = 'Ponder' t.real_name = 'Ponder Stibbons' t.reconnect = true t.password = 'secret' end @ponder.should_receive(:raw).with('NICK Ponder').once @ponder.should_receive(:raw).with('USER Ponder * * :Ponder Stibbons').once @ponder.should_receive(:raw).with('PASS secret').once @ponder.register end it 'sends a notice to a recipient' do @ponder.should_receive(:raw).with('NOTICE Ponder :You are cool!').once @ponder.notice('Ponder', 'You are cool!') end it 'sets a mode' do @ponder.should_receive(:raw).with('MODE Ponder +ao').once @ponder.mode('Ponder', '+ao') end it 'kicks an user from a channel' do @ponder.should_receive(:raw).with('KICK #channel Nanny_Ogg').once @ponder.kick('#channel', 'Nanny_Ogg') end it 'kicks an user from a channel with a reason' do @ponder.should_receive(:raw).with('KICK #channel Nanny_Ogg :Go away!').once @ponder.kick('#channel', 'Nanny_Ogg', 'Go away!') end it 'performs an action' do @ponder.should_receive(:raw).with("PRIVMSG #channel :\001ACTION HEX is working!\001").once @ponder.action('#channel', 'HEX is working!') end it 'sets the topic for a channel' do @ponder.should_receive(:raw).with('TOPIC #channel :I like dried frog pills.').once @ponder.topic('#channel', 'I like dried frog pills.') end it 'joins a channel' do @ponder.should_receive(:raw).with('JOIN #channel').once @ponder.join('#channel') end it 'joins a channel with password' do @ponder.should_receive(:raw).with('JOIN #channel secret').once @ponder.join('#channel', 'secret') end it 'parts a channel' do @ponder.should_receive(:raw).with('PART #channel').once @ponder.part('#channel') end it 'parts a channel with a message' do @ponder.should_receive(:raw).with('PART #channel :Partpart').once @ponder.part('#channel', 'Partpart') end it 'quits from the server' do @ponder.should_receive(:raw).with('QUIT').once @ponder.config.reconnect.should eql(true) @ponder.quit @ponder.config.reconnect.should eql(false) end it 'quits from the server with a message' do @ponder.should_receive(:raw).with('QUIT :Gone!').once @ponder.config.reconnect.should eql(true) @ponder.quit('Gone!') @ponder.config.reconnect.should eql(false) end it 'renames itself' do @ponder.should_receive(:raw).with('NICK :Ridcully').once @ponder.rename('Ridcully') end it 'goes away' do @ponder.should_receive(:raw).with('AWAY').once @ponder.away end it 'goes away with a reason' do @ponder.should_receive(:raw).with('AWAY :At the Mended Drum').once @ponder.away('At the Mended Drum') end it 'comes back from its absence' do @ponder.should_receive(:raw).with('AWAY').twice @ponder.away @ponder.back end it 'invites an user to a channel' do @ponder.should_receive(:raw).with('INVITE TheLibrarian #mended_drum').once @ponder.invite('TheLibrarian', '#mended_drum') end it 'bans an user from a channel' do @ponder.should_receive(:raw).with('MODE #mended_drum +b foo!bar@baz').once @ponder.ban('#mended_drum', 'foo!bar@baz') end end ruby-ponder-0.2.0/spec/async_irc_spec.rb0000644000175000017500000001411611626443546017546 0ustar dookiedookie$LOAD_PATH.unshift(File.dirname(__FILE__)) require 'spec_helper' require 'ponder/thaum' require 'ponder/async_irc' describe Ponder::AsyncIRC do before(:each) do @ponder = Ponder::Thaum.new { |c| c.verbose = false } end describe Ponder::AsyncIRC::Whois do context 'tries to get whois information when' do it 'there is no such nick' do @ponder.should_receive(:raw).with('WHOIS not_online') EM.run do whois = @ponder.whois('not_online', 1.5) whois.callback do |result| result.should be_false EM.stop end whois.errback do fail 'Wrong Callback called' EM.stop end EM.next_tick { @ponder.parse(":server 401 Ponder not_online :No such nick\r\n") } end end it 'the user is online' do @ponder.should_receive(:raw).with('WHOIS Ridcully') EM.run do whois = @ponder.whois('Ridcully', 1.5) whois.callback do |result| result.should be_kind_of(Hash) result[:nick].should eql('Ridcully') result[:username].should eql('ridc') result[:host].should eql('host') result[:real_name].should eql('Ridcully the wizard') result[:server].should be_kind_of(Hash) result[:server][:address].should eql('foo.host.net') result[:server][:name].should eql('That host thing') result[:channels].should be_kind_of(Hash) result[:channels]['#foo'].should be_nil result[:channels]['##bar'].should be_nil result[:channels]['#baz'].should eql('<') result[:channels]['#sushi'].should eql('@') result[:channels]['#ramen'].should eql('+') result[:registered].should be_true EM.stop end whois.errback do fail 'Wrong Callback called' EM.stop end EM.next_tick do @ponder.parse(":server 311 Ponder Ridcully :ridc host * :Ridcully the wizard\r\n") @ponder.parse(":server 312 Ponder Ridcully foo.host.net :That host thing\r\n") @ponder.parse(":server 319 Ponder Ridcully :#foo ##bar <#baz @#sushi +#ramen\r\n") @ponder.parse(":server 330 Ponder Ridcully rid_ :is logged in as\r\n") @ponder.parse(":server 318 Ponder Ridcully :End of /WHOIS list.\r\n") end end end end context 'it tries to get channel information when' do it 'there is no such channel' do @ponder.should_receive(:raw).with('MODE #no_channel') EM.run do channel_info = @ponder.channel_info('#no_channel') channel_info.callback do |result| result.should be_false EM.stop end channel_info.errback do fail 'Wrong Callback called' EM.stop end EM.next_tick { @ponder.parse(":server 403 Ponder #no_channel :No such channel\r\n") } end end end end describe Ponder::AsyncIRC::Topic do context 'tries to get a topic' do it ', adds the Deferrable object to the Set and deletes if afterwards for a success' do @ponder.should_receive(:raw).with('TOPIC #channel') EM.run do @ponder.deferrables.should be_empty topic = @ponder.get_topic('#channel') @ponder.deferrables.should include(topic) topic.callback do |result| @ponder.deferrables.should be_empty EM.stop end topic.errback do fail 'Wrong Callback called' EM.stop end EM.next_tick { @ponder.parse(":server 331 Ponder #channel :No topic is set.\r\n") } end end it ', adds the Deferrable object to the Set and deletes if afterwards for a failure (timeout)' do @ponder.should_receive(:raw).with('TOPIC #channel') EM.run do @ponder.deferrables.should be_empty topic = @ponder.get_topic('#channel', 1.5) @ponder.deferrables.should include(topic) topic.callback do |result| fail 'Wrong Callback called' EM.stop end topic.errback do @ponder.deferrables.should be_empty EM.stop end end end it 'when there is no topic set' do @ponder.should_receive(:raw).with('TOPIC #channel') EM.run do @ponder.deferrables.should be_empty topic = @ponder.get_topic('#channel', 2) @ponder.deferrables.should include(topic) topic.callback do |result| result.should eql({:raw_numeric => 331, :message => 'No topic is set'}) @ponder.deferrables.should be_empty EM.stop end topic.errback do fail 'Wrong Callback called' EM.stop end EM.next_tick { @ponder.parse(":server 331 Ponder #channel :No topic is set.\r\n") } end end it 'when there is no such channel' do @ponder.should_receive(:raw).with('TOPIC #no_channel') EM.run do topic = @ponder.get_topic('#no_channel') topic.callback do |result| result.should eql({:raw_numeric => 403, :message => 'No such channel'}) EM.stop end topic.errback do fail 'Wrong Callback called' EM.stop end EM.next_tick { @ponder.parse(":server 403 Ponder #no_channel :No such channel\r\n") } end end it "you're not on that channel" do @ponder.should_receive(:raw).with('TOPIC #channel') EM.run do topic = @ponder.get_topic('#channel') topic.callback do |result| result.should eql({:raw_numeric => 442, :message => "You're not on that channel"}) EM.stop end topic.errback do fail 'Wrong Callback called' EM.stop end EM.next_tick { @ponder.parse(":server 442 Ponder #channel :You're not on that channel\r\n") } end end end end end ruby-ponder-0.2.0/spec/callback_spec.rb0000644000175000017500000000407111626443546017327 0ustar dookiedookie$LOAD_PATH.unshift(File.dirname(__FILE__)) require 'spec_helper' require 'ponder/thaum' describe Ponder::Callback do before(:all) { @proc = Proc.new { } } before(:each) do @ponder = Ponder::Thaum.new { |c| c.verbose = true } end context 'tries to create a callback' do it 'with valid arguments' do lambda { Ponder::Callback.new(:channel, /foo/, @proc) }.should_not raise_error end it 'with an invalid type' do lambda { Ponder::Callback.new(:invalid, /foo/, @proc) }.should raise_error(TypeError) end it 'with an invalid match' do lambda { Ponder::Callback.new(:channel, 8, @proc) }.should raise_error(TypeError) end it 'with an invalid proc' do lambda { Ponder::Callback.new(:channel, /foo/, {}, 8) }.should raise_error(TypeError) end end it "calls the callback's proc on right match" do callback = Ponder::Callback.new(:channel, /wizzard/, {}, Proc.new { 8 }) callback.call(:channel, {:message => 'I like wizzards'}).should eql(8) end it "does not call the callback's proc on the wrong match" do p = Proc.new { 8 } p.should_not_receive(:call) callback = Ponder::Callback.new(:channel, /wizzard/, p) callback.call(:channel, {:message => 'I like hot dogs'}).should be_nil end it "calls the callback's proc on the right match and the right event type" do # `@proc.should_receive(:call).once` does not work here in 1.8.7 proc = Proc.new { @called = true } @ponder.on(:channel, /wizzard/, &proc) EM.run do @ponder.process_callbacks(:channel, {:message => 'I like wizzards'}) EM.schedule { EM.stop } end @called.should be_true end it "calls the callback's proc on the right match and the right event type with multiple types" do # `@proc.should_receive(:call).once` does not work here in 1.8.7 proc = Proc.new { @called = true } @ponder.on([:channel, :query], /wizzard/, &proc) EM.run do @ponder.process_callbacks(:query, {:message => 'I like wizzards'}) EM.schedule { EM.stop } end @called.should be_true end end ruby-ponder-0.2.0/README.md0000644000175000017500000003467311626443546014574 0ustar dookiedookie# Ponder ## Description Ponder (Stibbons) is a Domain Specific Language for writing IRC Bots using the [EventMachine](http://github.com/eventmachine/eventmachine "EventMachine") library. ## Getting started ### Installation $ sudo gem install ponder ### Configuring the Bot (Thaum!) require 'rubygems' require 'ponder' @ponder = Ponder::Thaum.new @ponder.configure do |c| c.nick = 'Ponder' c.server = 'irc.freenode.net' c.port = 6667 end ### Starting the Thaum @ponder.connect ### Event Handling This naked Thaum will connect to the server and answer PING requests (and VERSION and TIME). If you want the Thaum to join a channel when it's connected, use the following: @ponder.on :connect do @ponder.join '#mended_drum' end If you want the Thaum to answer on specific channel messages, register this Event Handler: @ponder.on :channel, /ponder/ do |event_data| @ponder.message event_data[:channel], 'Heard my name!' end Now, if an incoming channel message contains the word "ponder", the Thaum will send the message "Heard my name!" to that specific channel. See the **Advanced Event Handling** chapter for more details on how to register Event Handlers and the `event_data` hash. For more examples, have a look at the examples directory. ## Advanced Configuration Besides the configuration for nick, server and port as shown in the **Getting Started** chapter, there are some more preferences Ponder accepts. All of them: * `server` The `server` variable describes the server the Thaum shall connect to. It defaults to `'localhost'`. * `port` `port` describes the port that is used for the connection. It defaults to `6667`. * `nick` `nick` describes the nick the Thaum will try to register when connecting to the server. It will not be updated if the Thaum changes its nick. It defaults to `'Ponder'`. * `username` `username` is used for describing the username. It defaults to `'Ponder'`. * `real_name` `real_name` is used for describing the real name. It defaults to `'Ponder'`. * `verbose` If `verbose` is set to `true`, all incoming and outgoing traffic will be put to the console. Plus exceptions raised in Callbacks (errors). It defaults to `true`. * `logging` If `logging` is set to `true`, all incoming and outgoing traffic and exceptions will be logged to logs/log.log. You can also define your own logger, use `c.logger = @my_cool_logger` or `c.logger = Logger.new(...)`. Ponder itself just uses the #info and #error methods on the logger. You can access the logger instance via `@ponder.logger`, so you could do: `@ponder.logger.info('I did this and that right now')`. It defaults to `false`. * `reconnect` If `reconnect` is set to `true`, the Thaum will try to reconnect after being disconnected from the server (netsplit, ...). It will not try to reconnect if you call `quit` on the Thaum. It defaults to `true`. * `reconnect_interval` If `reconnect` is set to `true`, `reconnect_interval` describes the time in seconds, which the Thaum will wait before trying to reconnect. It defaults to `30`. For further information, have a look at the examples. ## Advanced Event Handling A Thaum can react on several events, so here is a list of handlers that can be used as argument in the `on` method: * `join` The `join` handler reacts, if an user joins a channel in which the Thaum is in. Example: @ponder.on :join do # ... end If using a block variable, you have access to a hash with detailed information about the event. Example: @ponder.on :join do |event_data| @ponder.message event_data[:channel], "Hello #{event_data[:nick]}! Welcome to #{event_data[:channel]}." end Which will greet a joined user with a channel message. The hash contains data for the keys `:nick`, `:user`, `:host` and `:channel`. * `part` Similar to the `join` handler but reacts on parting users. The block variable hash contains data for the keys `:nick`, `:user`, `:host`, `:channel` and `:message`. The value for `:message` is the message the parting user leaves. * `quit` The `quit` handler reacts, if an user quits from the server (and the Thaum can see it in a channel). The block variable hash contains data for the keys `:nick`, `:user`, `:host` and `:message`. * `channel` If an user sends a message to a channel, you can react with the `channel` handler. Example (from above): @ponder.on :channel, /ponder/ do |event_data| @ponder.message event_data[:channel], 'Heard my name!' end The block variable hash contains data for the keys `:nick`, `:user`, `:host`, `:channel` and `:message`. * `query` The `query` handler is like the `channel` handler, but for queries. Same keys in the data hash but no `:channel`. * `nickchange` `nickchange` reacts on nickchanges. Data hash keys are `:nick`, `:user`, `:host` and `:new_nick`, where `nick` is the nick before renaming and `new_nick` the nick after renaming. * `kick` If an user is being kicked, the `kick` handler can handle that event. Data hash keys are: `:nick`, `:user`, `:host`, `:channel`, `:victim` and `:reason`. * `topic` `topic` is for reacting on topic changes. Data hash keys are: `:nick`, `:user`, `:host`, `:channel` and `:topic`, where `:topic` is the new topic. You can provide a Regexp to just react on specific patterns: @ponder.on :topic, /foo/ do |event_data| # ... end This will just work for topics that include the word "foo". * `disconnect` `disconnect` reacts on being disconnected from the server (netsplit, quit, ...). It does not react if you exit the program with ^C. * Raw numerics A Thaum can seperately react on events with raw numerics, too. So you could do: @ponder.on 301 do |event_data| # ... end The data hash will contain the `:params` key. The corresponding value is the complete traffic line that came in. For all Event Handlers there is a `:type` key in the data hash (if the variable is specified). Its value gives the type of event, like `:channel`, `:join` or `301`. You can even share handler bodies between different events. So you are able to do something like this: @ponder.on [:join, :part, :quit] do |event_data| # ... end or @ponder.on [:channel, :query], /ponder/ do |event_data| @ponder.message((event_data[:channel] || event_data[:nick]), 'Yes?') end or @ponder.on [:channel, :nickchange], /foo/ do |event_data| # ... end They are not really shared at all, Ponder will just copy the Callback, but it's comfortable. ## Commanding the Thaum Command the Thaum, very simple. Just call a method listed below on the Ponder object. I will keep this short, since I assume you're at least little experienced with IRC. * `message(recipient, message)` * `notice(recipient, message)` * `mode(recipient, option)` * `kick(channel, user, reason = nil)` * `action(recipient, message)` * `topic(channel, topic)` * `join(channel, password = nil)` * `part(channel, message = nil)` * `quit(message = nil)` * `rename(nick)` * `away(message = nil)` * `back` * `invite(nick, channel)` * `ban(channel, address)` Last but not least some cool "give me something back" methods: * `get_topic(channel)` * Possible return values for `get_topic` are: * `{:raw_numeric => 331, :message => 'No topic is set'}` if no topic is set * `{:raw_numeric => 332, :message => message}` with `message` the topic message * `{:raw_numeric => 403, :message => 'No such channel'}` if there is no such channel * `{:raw_numeric => 442, :message => "You're not on that channel"}` if you cannot actually see the topic * `false` if the request times out (30 seconds) * `channel_info(channel)` * Possible return values: * If successful, a hash with keys: * `:modes` (letters) * `:channel_limit` (if channel limit is set) * `:created_at` (Time object of the time the channel was created) * `false`, if the request is not successful or times out (30 seconds) * `whois(nick)` * Possible return values: * If successful, a hash with keys: * `:nick` * `:username` * `:host` * `:real_name` * `:server` (a hash with the keys `:address` and `:name`) * `:channels` (a hash like `{'#foo' => '@', '#bar' => nil}` where the values are user privileges) * `:registered` (`true`, if registered, else `nil`) * If not successful * `false` * If times out (30 seconds) * `nil` Example: # Ponder, kick an user (and check if I'm allowed to command you)! @ponder.on :channel, /^!kick \S+$/ do |event_data| user_data = @ponder.whois(event_data[:nick]) if user_data[:registered] && (user_data[:channels][event_data[:channel]] == '@') user_to_kick = event_data[:message].split(' ')[1] @ponder.kick event_data[:channel], user_to_kick, 'GO!' end end ## Filters ### Before Filters You can have Before Filters! They are called before each event handling process and can - among other things - manipulate the `event_data` hash. If a Before Filter returns `false`, no further filters (no After Filters either) are called and the event handling process won't fire up. Example: @ponder.before_filter(:channel, /foo/) do # ... end This Before Filter will be called, if a channel message with the word "foo" gets in. You can use all other event types (like :query, :kick, ...) as well. Also possible is an array notation like `before_filter([:query, :channel], /foo/) ...`. If you want the filter to work an all event types, you can simply use `:all`. Filters will be called in defining order; first defined, first called. Event specific filters are called before `:all` filters. You could use it for authentication/authorization. Example: @ponder.before_filter :channel, // do |event_data| if is_an_admin?(event_data[:nick]) event_data[:authorized_request] = true else event_data[:authorized_request] = false end end Now, one can check for `event_data[:authorized_request]` in a callback. ### After Filters After Filters work the same way as Before Filters do, just after the actual event handling process. An After Filter does not hinder later After Filters to fire up if it returns `false`. Example: @ponder.after_filter(:all, //) do # ... end ## Timers If you need something in an event handling process to be time-displaced, you should not use `sleep`. I recommend using the comfortable timer methods EventMachine provides. A one shot timer looks like this: EventMachine::Timer.new(10) do # code to be run after 10 seconds end If you want the timer to be canceled before starting, you can do it like this: timer = EventMachine::Timer.new(10) do # code to be run after 10 seconds end # ... timer.cancel You can even have periodic timers which will fire up every n seconds: EventMachine::PeriodicTimer.new(10) do # code to be run every 10 seconds end A periodic timer can be canceled just like the other one. ## Formatting You can format your messages with colors, make it bold, italic or underlined. All of those formatting constants are availabe through `Ponder::Formatting`. ### Colors For coloring text you first set the color code with `Ponder::Formatting::COLOR_CODE` followed by a color followed by the text. For ending the colored text, set the uncolor code with `Ponder::Formatting::UNCOLOR`. Availabe colors are white, black, blue, green, red, brown, purple, orange, yellow, lime, teal, cyan, royal, pink, gray and silver. You can set one with the `Ponder::Formatting::COLORS` hash. Example: "This will be #{Ponder::Formatting::COLOR_CODE}#{Ponder::Formatting::COLORS[:red]}red#{Ponder::Formatting::UNCOLOR_CODE}. This not." ### Font Styles If you want to make a text bold, italic or underlined, use `Ponder::Formatting::BOLD`, `Ponder::Formatting::ITALIC` or `Ponder::Formatting::UNDERLINE`. After the text, close it with the same constant. Example: "This will be #{Ponder::Formatting::UNDERLINE}underlined#{Ponder::Formatting::UNDERLINE}. This not." ### Shortened Formatting If you don't always want to use `Ponder::Formatting`, use `include Ponder::Formatting`. All constants will then be availabe without `Ponder::Formatting` in front. ## Source The source can be found at GitHub: [tbuehlmann/ponder](http://github.com/tbuehlmann/ponder "Ponder"). You can contact me through [GitHub](http://github.com/tbuehlmann/ "GitHub") and IRC (named tbuehlmann in the Freenode network). ## Discworld Context So, why all that silly names? Ponder Stibbons? Thaum? Twoflogger (referring to Twoflower), BlindIo? What's the Mended Drum? Who's the Librarian? Simply put, I freaking enshrine Terry Pratchett's Discworld Novels and there were no better name for this project than Ponder. Ponder Stibbons is the Head of Inadvisably Applied Magic at the Unseen University of Ankh Morpork. He researched the Thaum, like the atom, just for magic. And I just love that character, so there we are. If you're a fan too or want to talk about the Discworld, the framework, whatever, don't hesitate to contact me. ## License Copyright (c) 2010, 2011 Tobias Bühlmann 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 or substantial portions of the Software. 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 OR COPYRIGHT HOLDERS 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. ruby-ponder-0.2.0/LICENSE0000644000175000017500000000205211626443546014304 0ustar dookiedookieCopyright (c) 2010, 2011 Tobias Bühlmann 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 or substantial portions of the Software. 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 OR COPYRIGHT HOLDERS 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. ruby-ponder-0.2.0/lib/0000755000175000017500000000000011626443546014046 5ustar dookiedookieruby-ponder-0.2.0/lib/ponder.rb0000644000175000017500000000101111626443546015653 0ustar dookiedookie$LOAD_PATH.unshift(File.dirname(__FILE__)) module Ponder ROOT = File.dirname($0) autoload :AsyncIRC, 'ponder/async_irc' autoload :Callback, 'ponder/callback' autoload :Connection, 'ponder/connection' autoload :Filter, 'ponder/filter' autoload :Formatting, 'ponder/formatting' autoload :IRC, 'ponder/irc' autoload :Thaum, 'ponder/thaum' autoload :VERSION, 'ponder/version' module Logger autoload :Twoflogger, 'ponder/logger/twoflogger' autoload :BlindIo, 'ponder/logger/blind_io' end end ruby-ponder-0.2.0/lib/ponder/0000755000175000017500000000000011626443546015335 5ustar dookiedookieruby-ponder-0.2.0/lib/ponder/version.rb0000644000175000017500000000004711626443546017350 0ustar dookiedookiemodule Ponder VERSION = '0.2.0' end ruby-ponder-0.2.0/lib/ponder/thaum.rb0000644000175000017500000001640411626443546017005 0ustar dookiedookierequire 'core_ext/array' require 'fileutils' require 'ostruct' require 'set' require 'ponder/async_irc' require 'ponder/callback' require 'ponder/connection' require 'ponder/filter' require 'ponder/irc' require 'ponder/logger/twoflogger' require 'ponder/logger/blind_io' module Ponder class Thaum include IRC include AsyncIRC::Delegate attr_reader :config, :callbacks attr_accessor :connected, :logger, :console_logger, :deferrables def initialize(&block) # default settings @config = OpenStruct.new( :server => 'localhost', :port => 6667, :nick => 'Ponder', :username => 'Ponder', :real_name => 'Ponder', :verbose => true, :logging => false, :reconnect => true, :reconnect_interval => 30 ) # custom settings block.call(@config) if block_given? # setting up loggers @console_logger = if @config.verbose Logger::Twoflogger.new($stdout) else Logger::BlindIo.new end @logger = if @config.logging if @config.logger @config.logger else log_path = File.join(ROOT, 'logs', 'log.log') log_dir = File.dirname(log_path) FileUtils.mkdir_p(log_dir) unless File.exist?(log_dir) Logger::Twoflogger.new(log_path, File::WRONLY | File::APPEND) end else Logger::BlindIo.new end # when using methods like #get_topic or #whois, a Deferrable object will wait # for the response and call a callback. these Deferrables are stored in this Set @deferrables = Set.new @connected = false # user callbacks @callbacks = Hash.new { |hash, key| hash[key] = [] } # standard callbacks for PING, VERSION, TIME and Nickname is already in use on :query, /^\001PING \d+\001$/ do |event_data| time = event_data[:message].scan(/\d+/)[0] notice event_data[:nick], "\001PING #{time}\001" end on :query, /^\001VERSION\001$/ do |event_data| notice event_data[:nick], "\001VERSION Ponder #{Ponder::VERSION} (https://github.com/tbuehlmann/ponder)\001" end on :query, /^\001TIME\001$/ do |event_data| notice event_data[:nick], "\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001" end # before and after filter @before_filters = Hash.new { |hash, key| hash[key] = [] } @after_filters = Hash.new { |hash, key| hash[key] = [] } end def on(event_types = [:channel], match = //, *options, &block) options = options.extract_options! if event_types.is_a?(Array) callbacks = event_types.map { |event_type| Callback.new(event_type, match, options, block) } else callbacks = [Callback.new(event_types, match, options, block)] event_types = [event_types] end callbacks.each_with_index do |callback, index| @callbacks[event_types[index]] << callback end end def connect @logger.info '-- Starting Ponder' @console_logger.info '-- Starting Ponder' EventMachine::run do @connection = EventMachine::connect(@config.server, @config.port, Connection, self) end end # parsing incoming traffic def parse(message) message.chomp! @logger.info "<< #{message}" @console_logger.info "<< #{message}" case message when /^PING \S+$/ raw message.sub(/PING/, 'PONG') when /^(?:\:\S+ )?(\d\d\d) / number = $1.to_i parse_event(number, :type => number, :params => $') when /^:(\S+)!(\S+)@(\S+) PRIVMSG #(\S+) :/ parse_event(:channel, :type => :channel, :nick => $1, :user => $2, :host => $3, :channel => "##{$4}", :message => $') when /^:(\S+)!(\S+)@(\S+) PRIVMSG \S+ :/ parse_event(:query, :type => :query, :nick => $1, :user => $2, :host => $3, :message => $') when /^:(\S+)!(\S+)@(\S+) JOIN :*(\S+)$/ parse_event(:join, :type => :join, :nick => $1, :user => $2, :host => $3, :channel => $4) when /^:(\S+)!(\S+)@(\S+) PART (\S+)/ parse_event(:part, :type => :part, :nick => $1, :user => $2, :host => $3, :channel => $4, :message => $'.sub(/ :/, '')) when /^:(\S+)!(\S+)@(\S+) QUIT/ parse_event(:quit, :type => :quit, :nick => $1, :user => $2, :host => $3, :message => $'.sub(/ :/, '')) when /^:(\S+)!(\S+)@(\S+) NICK :/ parse_event(:nickchange, :type => :nickchange, :nick => $1, :user => $2, :host => $3, :new_nick => $') when /^:(\S+)!(\S+)@(\S+) KICK (\S+) (\S+) :/ parse_event(:kick, :type => :kick, :nick => $1, :user => $2, :host => $3, :channel => $4, :victim => $5, :reason => $') when /^:(\S+)!(\S+)@(\S+) TOPIC (\S+) :/ parse_event(:topic, :type => :topic, :nick => $1, :user => $2, :host => $3, :channel => $4, :topic => $') end # if there are pending deferrabels, check if the message suits their matching pattern @deferrables.each { |d| d.try(message) } end # process callbacks with its begin; rescue; end def process_callbacks(event_type, event_data) @callbacks[event_type].each do |callback| # process chain of before_filters, callback handling and after_filters process = proc do begin stop_running = false # before filters (specific filters first, then :all) (@before_filters[event_type] + @before_filters[:all]).each do |filter| if filter.call(event_type, event_data) == false stop_running = true break end end unless stop_running # handling callback.call(event_type, event_data) # after filters (specific filters first, then :all) (@after_filters[event_type] + @after_filters[:all]).each do |filter| filter.call(event_type, event_data) end end rescue => e [@logger, @console_logger].each do |logger| logger.error("-- #{e.class}: #{e.message}") e.backtrace.each { |line| logger.error("-- #{line}") } end end end # defer the whole process if callback.options[:defer] EM.defer(process) else process.call end end end def before_filter(event_types = :all, match = //, &block) filter(@before_filters, event_types, match, block) end def after_filter(event_types = :all, match = //, &block) filter(@after_filters, event_types, match, block) end private # parses incoming traffic (types) def parse_event(event_type, event_data = {}) if ((event_type == 376) || (event_type == 422)) && !@connected @connected = true process_callbacks(:connect, event_data) end process_callbacks(event_type, event_data) end def filter(filter_type, event_types = :all, match = //, block = Proc.new) if event_types.is_a?(Array) event_types.each do |event_type| filter_type[event_type] << Filter.new(event_type, match, {}, block) end else filter_type[event_types] << Filter.new(event_types, match, {}, block) end end end end ruby-ponder-0.2.0/lib/ponder/formatting.rb0000644000175000017500000000141611626443546020036 0ustar dookiedookiemodule Ponder module Formatting PLAIN = 15.chr BOLD = 2.chr ITALIC = 22.chr UNDERLINE = 31.chr COLOR_CODE = 3.chr UNCOLOR_CODE = COLOR_CODE #mIRC color codes from http://www.mirc.com/help/colors.html COLORS = {:white => '00', :black => '01', :blue => '02', :green => '03', :red => '04', :brown => '05', :purple => '06', :orange => '07', :yellow => '08', :lime => '09', :teal => '10', :cyan => '11', :royal => '12', :pink => '13', :gray => '14', :silver => '15' } end end ruby-ponder-0.2.0/lib/ponder/irc.rb0000644000175000017500000000440111626443546016436 0ustar dookiedookiemodule Ponder module IRC # raw IRC messages def raw(message) @connection.send_data "#{message}\r\n" @logger.info ">> #{message}" @console_logger.info ">> #{message}" end # send a message def message(recipient, message) raw "PRIVMSG #{recipient} :#{message}" end # register when connected def register raw "NICK #{@config.nick}" raw "USER #{@config.username} * * :#{@config.real_name}" raw "PASS #{@config.password}" if @config.password end # send a notice def notice(recipient, message) raw "NOTICE #{recipient} :#{message}" end # set a mode def mode(recipient, option) raw "MODE #{recipient} #{option}" end # kick a user def kick(channel, user, reason = nil) if reason raw "KICK #{channel} #{user} :#{reason}" else raw "KICK #{channel} #{user}" end end # perform an action def action(recipient, message) raw "PRIVMSG #{recipient} :\001ACTION #{message}\001" end # set a topic def topic(channel, topic) raw "TOPIC #{channel} :#{topic}" end # joining a channel def join(channel, password = nil) if password raw "JOIN #{channel} #{password}" else raw "JOIN #{channel}" end end # parting a channel def part(channel, message = nil) if message raw "PART #{channel} :#{message}" else raw "PART #{channel}" end end # quitting def quit(message = nil) if message raw "QUIT :#{message}" else raw 'QUIT' end @config.reconnect = false # so Ponder does not reconnect after the socket has been closed end # rename def rename(nick) raw "NICK :#{nick}" end # set an away status def away(message = nil) if message raw "AWAY :#{message}" else raw "AWAY" end end # cancel an away status def back away end # invite an user to a channel def invite(nick, channel) raw "INVITE #{nick} #{channel}" end # ban an user def ban(channel, address) mode channel, "+b #{address}" end end end ruby-ponder-0.2.0/lib/ponder/async_irc.rb0000644000175000017500000001101711626443546017634 0ustar dookiedookierequire 'thread' require 'timeout' require 'eventmachine' module Ponder module AsyncIRC class Topic # number of seconds the deferrable will wait for a response before failing TIMEOUT = 15 include EventMachine::Deferrable def initialize(channel, timeout_after, thaum) @channel = channel @thaum = thaum self.timeout(timeout_after) self.errback { @thaum.deferrables.delete self } @thaum.deferrables.add self @thaum.raw "TOPIC #{@channel}" end def try(message) if message =~ /:\S+ (331|332|403|442) \S+ #{Regexp.escape(@channel)} :/i case $1 when '331' succeed({:raw_numeric => 331, :message => 'No topic is set'}) when '332' succeed({:raw_numeric => 332, :message => message.scan(/ :(.*)/)[0][0]}) when '403' succeed({:raw_numeric => 403, :message => 'No such channel'}) when '442' succeed({:raw_numeric => 442, :message => "You're not on that channel"}) end end end def succeed(*args) @thaum.deferrables.delete self set_deferred_status :succeeded, *args end end class Whois # number of seconds the deferrable will wait for a response before failing TIMEOUT = 15 include EventMachine::Deferrable def initialize(nick, timeout_after, thaum) @nick = nick @thaum = thaum @whois_data = {} self.timeout(timeout_after) self.errback { @thaum.deferrables.delete self } @thaum.deferrables.add self @thaum.raw "WHOIS #{@nick}" end def try(message) if message =~ /^:\S+ (307|311|312|318|319|330|401) \S+ #{Regexp.escape(@nick)}/i case $1 when '307', '330' @whois_data[:registered] = true when '311' message = message.scan(/^:\S+ 311 \S+ (\S+) :?(\S+) (\S+) \* :(.*)$/)[0] @whois_data[:nick] = message[0] @whois_data[:username] = message[1] @whois_data[:host] = message[2] @whois_data[:real_name] = message[3] when '312' message = message.scan(/^:\S+ 312 \S+ \S+ (\S+) :(.*)/)[0] @whois_data[:server] = {:address => message[0], :name => message[1]} when '318' succeed @whois_data when '319' channels_with_mode = message.scan(/^:\S+ 319 \S+ \S+ :(.*)/)[0][0].split(' ') @whois_data[:channels] = {} channels_with_mode.each do |c| @whois_data[:channels][c.scan(/(.)?(#\S+)/)[0][1]] = c.scan(/(.)?(#\S+)/)[0][0] end when '401' succeed false end end end def succeed(*args) @thaum.deferrables.delete self set_deferred_status :succeeded, *args end end class Channel # number of seconds the deferrable will wait for a response before failing TIMEOUT = 15 include EventMachine::Deferrable def initialize(channel, timeout_after, thaum) @channel = channel @thaum = thaum @channel_information = {} self.timeout(timeout_after) self.errback { @ponder.deferrables.delete self } @thaum.deferrables.add self @thaum.raw "MODE #{@channel}" end def try(message) if message =~ /:\S+ (324|329|403|442) \S+ #{Regexp.escape(@channel)}/i case $1 when '324' @channel_information[:modes] = message.scan(/^:\S+ 324 \S+ \S+ \+(\w*)/)[0][0].split('') limit = message.scan(/^:\S+ 324 \S+ \S+ \+\w* (\w*)/)[0] @channel_information[:channel_limit] = limit[0].to_i if limit when '329' @channel_information[:created_at] = Time.at(message.scan(/^:\S+ 329 \S+ \S+ (\d+)/)[0][0].to_i) succeed @channel_information when '403', '442' succeed false end end end def succeed(*args) @thaum.deferrables.delete self set_deferred_status :succeeded, *args end end module Delegate def get_topic(channel, timeout_after = AsyncIRC::Topic::TIMEOUT) AsyncIRC::Topic.new(channel, timeout_after, self) end def whois(nick, timeout_after = AsyncIRC::Whois::TIMEOUT) AsyncIRC::Whois.new(nick, timeout_after, self) end def channel_info(channel, timeout_after = AsyncIRC::Channel::TIMEOUT) AsyncIRC::Channel.new(channel, timeout_after, self) end end end end ruby-ponder-0.2.0/lib/ponder/logger/0000755000175000017500000000000011626443546016614 5ustar dookiedookieruby-ponder-0.2.0/lib/ponder/logger/twoflogger.rb0000644000175000017500000000047211626443546021323 0ustar dookiedookierequire 'logger' module Ponder module Logger class Twoflogger < ::Logger def initialize(*args) super(*args) self.formatter = proc do |severity, datetime, progname, msg| "#{severity} #{datetime.strftime('%Y-%m-%d %H:%M:%S')} #{msg}\n" end end end end end ruby-ponder-0.2.0/lib/ponder/logger/blind_io.rb0000644000175000017500000000056111626443546020722 0ustar dookiedookiemodule Ponder module Logger class BlindIo def debug(*args, &block) end def info(*args, &block) end def warn(*args, &block) end def error(*args, &block) end def fatal(*args, &block) end def unknown(*args, &block) end def method_missing(*args, &block) end end end end ruby-ponder-0.2.0/lib/ponder/connection.rb0000644000175000017500000000170511626443546020024 0ustar dookiedookierequire 'rubygems' require 'eventmachine' module Ponder class Connection < EventMachine::Connection include EventMachine::Protocols::LineText2 def initialize(thaum) @thaum = thaum end def connection_completed @thaum.register end def unbind @thaum.connected = false @thaum.process_callbacks :disconnect, {} @thaum.logger.info '-- Ponder disconnected' @thaum.console_logger.info '-- Ponder disconnected' if @thaum.config.reconnect @thaum.logger.info "-- Reconnecting in #{@thaum.config.reconnect_interval} seconds" @thaum.console_logger.info "-- Reconnecting in #{@thaum.config.reconnect_interval} seconds" EventMachine::add_timer(@thaum.config.reconnect_interval) do reconnect @thaum.config.server, @thaum.config.port end else @thaum.logger.close end end def receive_line(line) @thaum.parse line end end end ruby-ponder-0.2.0/lib/ponder/filter.rb0000644000175000017500000000015211626443546017145 0ustar dookiedookierequire 'ponder/callback' module Ponder class Filter < Callback LISTENED_TYPES += [:all] end end ruby-ponder-0.2.0/lib/ponder/callback.rb0000644000175000017500000000223511626443546017420 0ustar dookiedookiemodule Ponder class Callback LISTENED_TYPES = [:connect, :channel, :query, :join, :part, :quit, :nickchange, :kick, :topic, :disconnect] # + 3-digit numbers attr_reader :options def initialize(event_type = :channel, match = //, options = {}, proc = Proc.new {}) unless self.class::LISTENED_TYPES.include?(event_type) || event_type.is_a?(Integer) raise TypeError, "#{event_type} is an unsupported event-type" end self.match = match self.proc = proc @options = options end def call(event_type, event_data = {}) if (event_type == :channel) || (event_type == :query) @proc.call(event_data) if event_data[:message] =~ @match elsif event_type == :topic @proc.call(event_data) if event_data[:topic] =~ @match else @proc.call(event_data) end end private def match=(match) if match.is_a?(Regexp) @match = match else raise TypeError, "#{match} must be a Regexp" end end def proc=(proc) if proc.is_a?(Proc) @proc = proc else raise TypeError, "#{proc} must be a Proc" end end end end ruby-ponder-0.2.0/lib/core_ext/0000755000175000017500000000000011626443546015656 5ustar dookiedookieruby-ponder-0.2.0/lib/core_ext/array.rb0000644000175000017500000000015111626443546017316 0ustar dookiedookieclass Array def extract_options! if last.is_a?(Hash) pop else {} end end end ruby-ponder-0.2.0/examples/0000755000175000017500000000000011626443546015116 5ustar dookiedookieruby-ponder-0.2.0/examples/echo.rb0000644000175000017500000000073211626443546016363 0ustar dookiedookie$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'ponder' # This Thaum will parrot all channel messages. @ponder = Ponder::Thaum.new do |t| t.server = 'chat.freenode.org' t.port = 6667 t.nick = 'Ponder' t.verbose = true t.logging = false end @ponder.on :connect do @ponder.join '#ponder' end @ponder.on :channel, // do |event_data| @ponder.message event_data[:channel], event_data[:message] end @ponder.connect ruby-ponder-0.2.0/examples/redis_last_seen.rb0000644000175000017500000001130111626443546020602 0ustar dookiedookie# This Thaum remembers users' actions and is able to quote them. # Go for it with `!seen `. You can even use wildcards with "*". # # The redis server version needs to be >= 1.3.10 for hash support. $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'rubygems' require 'ponder' require 'redis' FORMAT = '%Y-%m-%d %H:%M:%S' class String def escape_redis self.gsub(/([\|\[\]\?])/, '\\\\\1') end end @redis = Redis.new(:thread_safe => true) def remember(lowercase_nick, nick, user, host, channel, action, action_content) @redis.hmset( lowercase_nick, 'nick', nick, 'user', user, 'host', host, 'channel', channel, 'action', action, 'action_content', action_content, 'updated_at', Time.now.to_i) end @ponder = Ponder::Thaum.new do |t| t.server = 'chat.freenode.org' t.port = 6667 t.nick = 'Ponder' t.verbose = true t.logging = false end @ponder.on :connect do @ponder.join '#ponder' end @ponder.on :channel do |event_data| remember(event_data[:nick].downcase, event_data[:nick], event_data[:user], event_data[:host], event_data[:channel], event_data[:type], event_data[:message]) end @ponder.on [:join, :part, :quit] do |event_data| remember(event_data[:nick].downcase, event_data[:nick], event_data[:user], event_data[:host], event_data[:channel], event_data[:type], (event_data[:message] || event_data[:channel])) end @ponder.on :nickchange do |event_data| remember(event_data[:nick].downcase, event_data[:nick], event_data[:user], event_data[:host], '', 'nickchange_old', event_data[:new_nick]) remember(event_data[:new_nick].downcase, event_data[:new_nick], event_data[:user], event_data[:host], '', 'nickchange_new', event_data[:nick]) end @ponder.on :kick do |event_data| remember(event_data[:nick].downcase, event_data[:nick], event_data[:user], event_data[:host], event_data[:channel], 'kicker', "#{event_data[:victim]} #{event_data[:reason]}") remember(event_data[:victim].downcase, event_data[:victim], '', '', event_data[:channel], 'victim', "#{event_data[:nick]} #{event_data[:reason]}") end def last_seen(nick) data = @redis.hgetall(nick) case data['action'] when 'channel' "#{data['nick']} wrote something in #{data['channel']} at #{Time.at(data['updated_at'].to_i).strftime(FORMAT)}." when 'join' "#{data['nick']} joined #{data['channel']} at #{Time.at(data['updated_at'].to_i).strftime(FORMAT)}." when 'part' "#{data['nick']} left #{data['channel']} at #{Time.at(data['updated_at'].to_i).strftime(FORMAT)} (#{data['action_content']})." when 'quit' "#{data['nick']} quit at #{Time.at(data['updated_at'].to_i).strftime(FORMAT)} (#{data['action_content']})." when 'nickchange_old' "#{data['nick']} renamed to #{data['action_content']}} at #{Time.at(data['updated_at'].to_i).strftime(FORMAT)}." when 'nickchange_new' "#{data['nick']} renamed to #{data['action_content']} at #{Time.at(data['updated_at'].to_i).strftime(FORMAT)}." when 'kicker' "#{data['nick']} kicked #{data['action_content'].split(' ')[0]} at #{Time.at(data['updated_at'].to_i).strftime(FORMAT)} from #{data['channel']} (#{data['action_content'].split(' ')[1]})." when 'victim' "#{data['nick']} was kicked by #{data['action_content'].split(' ')[0]} at #{Time.at(data['updated_at'].to_i).strftime(FORMAT)} from #{data['channel']} (#{data['action_content'].split(' ')[1]})." end end @ponder.on :channel, /^!seen \S+$/ do |event_data| nick = event_data[:message].split(' ')[1].downcase # wildcards if nick =~ /\*/ users = @redis.keys nick.escape_redis results = users.length case results when 0 @ponder.message event_data[:channel], 'No such nick found.' when 1 @ponder.message event_data[:channel], last_seen(users[0]) when 2..5 nicks = [] users.each do |user| nicks << @redis.hgetall(user)['nick'] end nicks = nicks.join(', ') @ponder.message event_data[:channel], "#{results} nicks found (#{nicks})." else @ponder.message event_data[:channel], "Too many results (#{results})." end # single search else @ponder.whois(nick).callback do |result| message = if @redis.exists nick if result "#{result[:nick]} is online. (#{last_seen(nick)})" else last_seen(nick) end else if result "#{result[:nick]} is online." else "#{nick} not found." end end @ponder.message event_data[:channel], message end end end @ponder.connect ruby-ponder-0.2.0/ponder.gemspec0000644000175000017500000000222011626443546016130 0ustar dookiedookie$LOAD_PATH.unshift(File.dirname(__FILE__)) require 'lib/ponder/version' Gem::Specification.new do |s| s.name = 'ponder' s.version = Ponder::VERSION s.date = '2011-08-28' s.summary = 'IRC bot framework' s.description = 'Ponder (Stibbons) is a Domain Specific Language for writing IRC Bots using the EventMachine library.' s.author = 'Tobias Bühlmann' s.email = 'tobias.buehlmann@gmx.de' s.homepage = 'https://github.com/tbuehlmann/ponder' s.required_ruby_version = '>= 1.8.6' s.add_dependency('eventmachine', '>= 0.12.10') s.add_development_dependency('rspec') s.files = %w[ LICENSE README.md Rakefile examples/echo.rb examples/redis_last_seen.rb lib/core_ext/array.rb lib/ponder.rb lib/ponder/async_irc.rb lib/ponder/callback.rb lib/ponder/connection.rb lib/ponder/filter.rb lib/ponder/formatting.rb lib/ponder/irc.rb lib/ponder/logger/blind_io.rb lib/ponder/logger/twoflogger.rb lib/ponder/thaum.rb lib/ponder/version.rb ponder.gemspec spec/async_irc_spec.rb spec/callback_spec.rb spec/irc_spec.rb spec/spec_helper.rb spec/thaum_spec.rb ] end ruby-ponder-0.2.0/Rakefile0000644000175000017500000000022611626443546014745 0ustar dookiedookierequire 'rubygems' require 'rspec/core/rake_task' desc 'Run all specs' RSpec::Core::RakeTask.new(:spec) task :test => :spec task :default => :spec