dragoon 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  Dragoon
2
2
  ============
3
3
 
4
- An IRC bot that alerts you whenever certain keywords pop up in your favorite channels. You can listen to multiple channels but right now, you can only connect to one network at a time. And I've only tested it on freenode.net. Support for multiple networks will be done soon.
4
+ An IRC bot that alerts you whenever certain keywords pop up in your favorite channels. Notifications are displayed via growl if supported or text highlights on the terminal.
5
5
 
6
+ Listening to multiple network connections is supported via IO.select()
6
7
 
7
8
  Dependencies
8
9
  -----------
data/TODO CHANGED
@@ -6,7 +6,7 @@ TODO
6
6
  - [x] channels: #ubuntu, #ruby, #ffmpeg
7
7
  - [x] keywords: and, how, why, help
8
8
  - [x] connect to network
9
- - [ ] - multiple networks
9
+ - [x] - multiple networks
10
10
  - [x] login
11
11
  - [x] join channels
12
12
  - [x] - normal channels
@@ -19,6 +19,8 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ["lib"]
20
20
 
21
21
  s.add_development_dependency "rspec"
22
+ s.add_development_dependency "pry"
22
23
  s.add_dependency "term-ansicolor"
23
24
  s.add_dependency "json"
25
+ s.add_dependency "active_support"
24
26
  end
@@ -4,25 +4,26 @@ require 'forwardable'
4
4
 
5
5
  require 'term/ansicolor'
6
6
  require 'json'
7
+ require 'active_support/core_ext/hash/indifferent_access'
7
8
 
8
- require 'dragoon/commands'
9
9
  require 'dragoon/event'
10
+ require 'dragoon/network'
10
11
  require 'dragoon/event_manager'
11
12
  require 'dragoon/color'
12
13
  require 'dragoon/core_ext'
13
14
 
15
+ $verbose = true
14
16
 
15
17
  module Dragoon
16
18
  class Bot
17
19
  extend Forwardable
18
-
19
- include Commands
20
20
  include Color
21
21
 
22
- attr_accessor :nickname, :network, :channels, :keywords
22
+ attr_accessor :nickname, :networks, :keywords
23
23
 
24
24
  def_delegators :@event_manager, :on, :dispatch
25
25
 
26
+ SAMPLE_CONFIG = File.expand_path("#{File.dirname(__FILE__)}/../sample_config")
26
27
  CONFIG_FILE = File.expand_path("~/.dragoon")
27
28
 
28
29
  def initialize
@@ -34,41 +35,18 @@ module Dragoon
34
35
  def load_config
35
36
  begin
36
37
  if File.exist?(CONFIG_FILE)
37
- puts("*** Loading config file #{CONFIG_FILE}")
38
- config = JSON.parse(File.read(CONFIG_FILE))
39
- @network = config["network"]
40
- @port = config["port"]
41
- @nickname = config["nickname"]
42
- @password = config["password"]
43
- @channels = config["channels"]
44
- @keywords = config["keywords"]
38
+ log("*** Loading config file #{CONFIG_FILE}")
39
+ config = HashWithIndifferentAccess.new JSON.parse(File.read(CONFIG_FILE))
40
+ @nickname = config[:nickname]
41
+ @password = config[:password]
42
+ @hostname = config[:hostname]
43
+ @servername = config[:servername]
44
+ @networks = config[:networks].map { |n| Network.new(n) }
45
+ @keywords = config[:keywords]
45
46
  else
46
-
47
- config = (<<-EOF).gsub(/^\s{10}/,"")
48
- {
49
- "network": "chat.freenode.net",
50
- "port": "6667",
51
- "nickname": "fenix#{rand(10)}#{rand(10)}#{rand(10)}",
52
- "password": "",
53
- "channels": [
54
- "#ubuntu",
55
- "#ruby",
56
- "#rubyonrails",
57
- "#linux",
58
- "#programming",
59
- "#javascript"
60
- ],
61
- "keywords": [
62
- "and",
63
- "how",
64
- "who"
65
- ]
66
- }
67
- EOF
68
-
69
- File.open(CONFIG_FILE,'w+') {|f| f.write(config) }
70
- puts("Config file does not exist...created a default one at #{CONFIG_FILE}".red)
71
- puts("Please modify it according to your preference".red)
47
+ File.open(CONFIG_FILE,'w+') {|f| f.write(File.read(SAMPLE_CONFIG)) }
48
+ log("Config file does not exist...created a default one at #{CONFIG_FILE}".red)
49
+ log("Please modify it according to your preference".red)
72
50
  exit 1
73
51
  end
74
52
  rescue => e
@@ -78,40 +56,49 @@ module Dragoon
78
56
  end
79
57
 
80
58
  def init_callbacks
81
- on :startup do |msg|
82
- puts msg.text
59
+ on :startup do |msg, source|
60
+ log msg.text
83
61
  if msg.text =~ /.*Found your hostname.*/
84
62
  # login
85
- pass(@password) if !@password.empty?
86
- user(@nickname, 0, 0, @nickname)
87
- nick(@nickname)
63
+ source.socket.write "PASS #{@password}\r\n" if !@password.empty?
64
+ source.socket.write "USER #{@nickname} #{@hostname} #{@servername} #{@nickname}\r\n"
65
+ source.socket.write "NICK #{@nickname}\r\n"
88
66
  end
89
67
  end
90
68
 
91
- on :ping do |msg|
92
- pong(msg.server) # keep alive
69
+ on :ping do |msg, source|
70
+ source.socket.write "PONG #{msg.server}\r\n" # keep alive
93
71
  end
94
72
 
95
- on :mode do |msg|
96
- puts "*** Logged in as #{msg.nickname}"
97
- join_channels
73
+ on :welcome do |msg, source|
74
+ log "*** Logged in on #{source.name} as #{msg.nickname}"
75
+
76
+ source.channels.each do |channel|
77
+ source.socket.write "JOIN #{channel}\r\n"
78
+ end
98
79
  end
99
80
 
100
- on :join do |msg|
101
- puts "*** Listening on channel #{colorize(msg.channel)}" if @nickname == msg.nickname
81
+ on :join do |msg, source|
82
+ log "*** Listening on channel #{colorize(msg.channel)}" if @nickname == msg.nickname
102
83
  end
103
84
 
104
- on :err do |msg|
105
- $stderr.puts msg.text.red unless msg.error_code == '470' # channel forwarding
85
+ on :err do |msg, source|
86
+ unless ['470', '422'].include?(msg.error_code) # ignore channel forwarding + motd file missing
87
+ $stderr.puts msg.text.red
88
+ end
89
+
106
90
  if msg.error_code == '433' # nick already in use
107
91
  print("Enter another nickname: ")
108
92
  @nickname = gets.chomp
109
- user(@nickname, 0, 0, @nickname)
110
- nick(@nickname)
93
+ print("Enter password (optional): ")
94
+ @password = gets.chomp
95
+ source.socket.write "PASS #{@password}\r\n" if !@password.empty?
96
+ source.socket.write "USER #{@nickname} #{@hostname} #{@servername} #{@nickname}\r\n"
97
+ source.socket.write "NICK #{@nickname}\r\n"
111
98
  end
112
99
  end
113
100
 
114
- on :privmsg do |msg|
101
+ on :privmsg do |msg, source|
115
102
  if growlnotify_supported?
116
103
  if keyword = @keywords.detect { |keyword| msg.text.include?(keyword) }
117
104
  system("growlnotify " +
@@ -119,42 +106,63 @@ module Dragoon
119
106
  "-m \"#{msg.privmsg.escape_double_quotes}\"")
120
107
  end
121
108
  end
122
- puts "#{colorize(msg.channel)} <#{msg.nickname}> #{highlight_keywords(msg.privmsg,@keywords)}"
109
+ log "#{colorize(msg.channel)} <#{msg.nickname}> #{highlight_keywords(msg.privmsg,@keywords)}"
123
110
  end
124
111
  end
125
112
 
126
113
  def run
127
- @socket = connect
114
+ log "*** Press Ctrl + c to stop the program"
115
+ sockets = []
116
+ @networks.each do |network|
117
+ socket = connect(network)
118
+ network.server = socket.peeraddr[2]
119
+ network.socket = socket
120
+ sockets << socket
121
+ end
128
122
 
129
123
  begin
130
- while line = @socket.gets
131
- @event = Event.parse(line)
132
- dispatch(@event)
133
- end
124
+ while true; listen(sockets); end
134
125
  rescue Interrupt
135
126
  stop
136
127
  end
128
+ end
137
129
 
130
+ # should pass an array of sockets
131
+ def listen(sockets)
132
+ read_fds, write_fds, error_fds = IO.select(sockets, nil, nil, nil)
133
+ read_fds.each do |socket|
134
+ line = socket.gets
135
+ @event = Event.parse(line)
136
+ dispatch(@event,
137
+ @networks.select{ |n|
138
+ n.server == socket.peeraddr[2] &&
139
+ n.port = socket.peeraddr[1]
140
+ }.first
141
+ )
142
+ end
138
143
  end
139
144
 
140
- def connect
141
- puts "*** Connecting to #{@network} on port #{@port}"
142
- puts "*** Press Ctrl + c to stop the program"
143
- TCPSocket.new(@network, @port)
144
- rescue Errno::EADDRNOTAVAIL => e
145
- puts "#{e.class} Cannot connect to #{@network} at port #{@port}".red
146
- exit
145
+ def connect(network)
146
+ log "*** Connecting to #{network.name} on port #{network.port}"
147
+ TCPSocket.new(network.name, network.port)
148
+ rescue => e
149
+ log "#{e.class} Cannot connect to #{network.name} at port #{network.port}".red
150
+ raise
147
151
  end
148
152
 
149
153
  def stop
150
154
  puts
151
- puts "*** Program stopped"
152
- @socket.close
155
+ log "*** Program stopped"
156
+ @networks.each { |network| network.socket.close }
153
157
  exit
154
158
  end
155
159
 
156
160
  private
157
161
 
162
+ def log(text)
163
+ puts text if $verbose
164
+ end
165
+
158
166
  def growlnotify_supported?
159
167
  @growlnotify_supported ||= system("which growlnotify > /dev/null")
160
168
  end
@@ -13,12 +13,12 @@ module Dragoon
13
13
 
14
14
  def self.parse(line)
15
15
  case line
16
- when /^:\S+ NOTICE \* :\*{3} .*/
16
+ when /^:\S+ NOTICE .*:\*{3} .*/
17
17
  self.new(:startup, Message.new(:text => line))
18
18
  when /^PING :(\S+)/
19
19
  self.new(:ping, Message.new(:text => line, :server => $1))
20
- when /^:(\S+) MODE \S+ :.*/
21
- self.new(:mode, Message.new(:text => line, :nickname => $1))
20
+ when /^:\S+ 001 (\S+) :.*/
21
+ self.new(:welcome, Message.new(:text => line, :nickname => $1))
22
22
  when /^:(\S+)!(\S+)@(\S+) JOIN (\S+)/
23
23
  self.new(:join, Message.new(:text => line,
24
24
  :nickname => $1,
@@ -5,9 +5,9 @@ module Dragoon
5
5
 
6
6
  attr_reader :handlers
7
7
 
8
- def dispatch(event)
8
+ def dispatch(event, source)
9
9
  if @handlers.include?(event.name)
10
- @handlers[event.name].call(event.message)
10
+ @handlers[event.name].call(event.message, source)
11
11
  end
12
12
  end
13
13
 
@@ -0,0 +1,14 @@
1
+ module Dragoon
2
+ class Network
3
+ attr_accessor :name, :port, :server, :socket, :channels
4
+
5
+ def initialize(options={})
6
+ @name = options[:name]
7
+ @port = options[:port]
8
+ @server = options[:server]
9
+ @socket = options[:socket]
10
+ @channels = options[:channels]
11
+ end
12
+
13
+ end
14
+ end
@@ -1,3 +1,3 @@
1
1
  module Dragoon
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
@@ -0,0 +1,32 @@
1
+ {
2
+ "nickname": "fenix457",
3
+ "password": "",
4
+ "hostname": "some_host",
5
+ "servername": "some_server",
6
+ "keywords": [
7
+ "and",
8
+ "how",
9
+ "who"
10
+ ],
11
+ "networks": [
12
+ {
13
+ "name": "irc.freenode.net",
14
+ "port": 6667,
15
+ "channels": [
16
+ "#ubuntu",
17
+ "#ruby",
18
+ "#rubyonrails",
19
+ "#linux",
20
+ "#programming",
21
+ "#javascript"
22
+ ]
23
+ },
24
+ {
25
+ "name": "irc.quartznet.org",
26
+ "port": 6667,
27
+ "channels": [
28
+ "#PlayGames"
29
+ ]
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dragoon::Bot do
4
+ before(:all) do
5
+ @local_servers = []
6
+ [7777, 7778].each { |port| @local_servers << LocalServer.new(port).run }
7
+ end
8
+
9
+ after(:all) do
10
+ @local_servers.each { |server| server.stop}
11
+ end
12
+
13
+ before(:each) do
14
+ @client = Dragoon::Bot.new
15
+ end
16
+
17
+ describe "#connect" do
18
+ context "network parameters are wrong" do
19
+ before(:each) do
20
+ @network = Dragoon::Network.new(:name => 'localhost', :port => 6665, :channels => %w[ #ruby #java])
21
+ end
22
+
23
+ it "should raise error" do
24
+ lambda {
25
+ socket = @client.connect(@network)
26
+ }.should raise_error
27
+ end
28
+ end
29
+
30
+ context "network parameters are correct" do
31
+ before(:each) do
32
+ @network = Dragoon::Network.new(:name => 'localhost', :port => 7777, :channels => %w[ #ruby #java])
33
+ end
34
+
35
+ it "should return the correct TCP Socket" do
36
+ socket = @client.connect(@network)
37
+ socket.class.should == TCPSocket
38
+ socket.peeraddr[1].should == 7777
39
+ socket.peeraddr[2].should == 'localhost'
40
+ end
41
+ end
42
+ end
43
+
44
+ describe "#listen" do
45
+ context "listening to only 1 network" do
46
+ before(:each) do
47
+ @network = Dragoon::Network.new(:name => 'localhost', :port => 7777, :channels => %w[ #ruby #java])
48
+ @socket = @client.connect(@network)
49
+ end
50
+
51
+ it "should receive data from that network" do
52
+ @socket.should_receive(:gets)
53
+ @client.listen([@socket])
54
+ end
55
+ end
56
+
57
+ context "listening to more than 1 networks" do
58
+ before(:each) do
59
+ @networks = []
60
+ @networks << Dragoon::Network.new(:name => 'localhost', :port => 7777, :channels => %w[ #ruby #java])
61
+ @networks << Dragoon::Network.new(:name => 'localhost', :port => 7778, :channels => %w[ #ps3 #wii])
62
+ @sockets = @networks.map { |network| @client.connect(network) }
63
+ end
64
+
65
+ it "should receive data from all networks" do
66
+ @sockets.each { |socket| socket.should_receive(:gets).at_least(:once) }
67
+ 40.times { @client.listen(@sockets) }
68
+ end
69
+ end
70
+ end
71
+
72
+ end
@@ -7,40 +7,33 @@ describe Dragoon::EventManager do
7
7
  end
8
8
 
9
9
  describe "#dispatch" do
10
- context "event is unknown" do
11
- it "should not do anything" do
12
- pending "help from other people"
13
- #@event_manager.dispatch(:unknown, ":adams.freenode.net 372 felix21 :- portrayed in his uniquely British irony. He is sorely missed")
14
- end
15
- end
16
-
17
- context "no handler for an event" do
18
- it "should not do anything" do
19
- pending "help from other people"
20
- #@event_manager.dispatch(:mode, ":felix21 MODE felix21 :+i")
21
- end
22
- end
23
-
24
10
  context "callback exists for an event" do
25
11
  before(:each) do
26
- @callback = lambda { puts "hello world" }
12
+ @callback = lambda { |event, source| puts "hello world" }
27
13
  @event = Dragoon::Event.parse(":felix21 MODE felix21 :+i")
14
+ @network = Dragoon::Network.new
28
15
  @event_manager.on @event.name, &@callback
29
16
  end
30
17
 
31
18
  it "should run the corresponding callback" do
32
19
  @callback.should_receive(:call)
33
- @event_manager.dispatch(@event)
20
+ @event_manager.dispatch(@event, @network)
21
+ end
22
+
23
+ it "should have message object and network source as parameters to the callback" do
24
+ @callback.should_receive(:call).with(@event.message, @network)
25
+ @event_manager.dispatch(@event, @network)
34
26
  end
35
27
  end
36
28
  end
37
29
 
38
30
  describe "#on" do
39
31
  it "should register that event" do
40
- callback = lambda { puts "this is a callback" }
32
+ callback = lambda { |event, source| puts "this is a callback" }
41
33
  @event_manager.on(:mode, &callback)
42
34
  @event_manager.handlers[:mode].should == callback
43
35
  end
36
+
44
37
  end
45
38
 
46
39
  end
@@ -5,9 +5,13 @@ describe Dragoon::Event do
5
5
  describe "#get_event" do
6
6
  it "should process startup event" do
7
7
  msg = ":zelazny.freenode.net NOTICE * :*** Found your hostname"
8
+ msg2 = ":luca002.QuartzNet.Org NOTICE AUTH :*** Found your hostname"
8
9
  event = Dragoon::Event.parse(msg)
9
10
  event.name.should == :startup
10
11
  event.message.text.should == msg
12
+ event = Dragoon::Event.parse(msg2)
13
+ event.name.should == :startup
14
+ event.message.text.should == msg2
11
15
  end
12
16
 
13
17
  it "should process ping event" do
@@ -18,12 +22,12 @@ describe Dragoon::Event do
18
22
  event.message.server.should == "zelazny.freenode.net"
19
23
  end
20
24
 
21
- it "should process mode event" do
22
- msg = ":favor212 MODE favor212 :+i"
25
+ it "should process welcome event" do
26
+ msg = ":luca002.QuartzNet.Org 001 fenix456 :Welcome to the QuartzNet IRC Network fenix456!~fenix456@some_host"
23
27
  event = Dragoon::Event.parse(msg)
24
- event.name.should == :mode
28
+ event.name.should == :welcome
25
29
  event.message.text.should == msg
26
- event.message.nickname.should == "favor212"
30
+ event.message.nickname.should == "fenix456"
27
31
  end
28
32
 
29
33
  it "should process join event" do
@@ -1,4 +1,37 @@
1
1
  $LOAD_PATH.unshift("#{File.expand_path(File.dirname(__FILE__))}/../lib")
2
2
 
3
3
  require 'dragoon'
4
+ require 'pry'
4
5
 
6
+ $verbose = false
7
+
8
+ # A localserver that will run in the background for test suites' bots to establish tcp connection to
9
+ # It handles multiple connections by further forking child processes as needed
10
+ # NOTE: Make sure you call #stop for all servers at the end of the test suite
11
+
12
+ class LocalServer
13
+ attr_reader :pid
14
+
15
+ def initialize(port)
16
+ @port = port
17
+ end
18
+
19
+ def run
20
+ server = TCPServer.new('localhost', @port)
21
+ server.listen(5)
22
+ @pid = fork {
23
+ while socket = server.accept
24
+ fork {
25
+ trap("PIPE") { exit }
26
+ loop { socket.write("localhost port #{socket.addr[1]} dumping text\r\n") }
27
+ }
28
+ end
29
+ }
30
+ self
31
+ end
32
+
33
+ def stop
34
+ `kill -9 #{@pid}`
35
+ end
36
+
37
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dragoon
3
3
  version: !ruby/object:Gem::Version
4
- hash: 17
4
+ hash: 15
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 7
10
- version: 0.0.7
9
+ - 8
10
+ version: 0.0.8
11
11
  platform: ruby
12
12
  authors:
13
13
  - Reginald Tan
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-02-08 00:00:00 Z
18
+ date: 2012-02-12 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: rspec
@@ -32,7 +32,7 @@ dependencies:
32
32
  type: :development
33
33
  version_requirements: *id001
34
34
  - !ruby/object:Gem::Dependency
35
- name: term-ansicolor
35
+ name: pry
36
36
  prerelease: false
37
37
  requirement: &id002 !ruby/object:Gem::Requirement
38
38
  none: false
@@ -43,10 +43,10 @@ dependencies:
43
43
  segments:
44
44
  - 0
45
45
  version: "0"
46
- type: :runtime
46
+ type: :development
47
47
  version_requirements: *id002
48
48
  - !ruby/object:Gem::Dependency
49
- name: json
49
+ name: term-ansicolor
50
50
  prerelease: false
51
51
  requirement: &id003 !ruby/object:Gem::Requirement
52
52
  none: false
@@ -59,6 +59,34 @@ dependencies:
59
59
  version: "0"
60
60
  type: :runtime
61
61
  version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: json
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :runtime
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ name: active_support
78
+ prerelease: false
79
+ requirement: &id005 !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ type: :runtime
89
+ version_requirements: *id005
62
90
  description: "An IRC bot that alerts you whenever certain keywords appear in your favorite channels "
63
91
  email:
64
92
  - redge.tan@gmail.com
@@ -79,12 +107,14 @@ files:
79
107
  - dragoon.gemspec
80
108
  - lib/dragoon.rb
81
109
  - lib/dragoon/color.rb
82
- - lib/dragoon/commands.rb
83
110
  - lib/dragoon/core_ext.rb
84
111
  - lib/dragoon/event.rb
85
112
  - lib/dragoon/event_manager.rb
113
+ - lib/dragoon/network.rb
86
114
  - lib/dragoon/version.rb
115
+ - sample_config
87
116
  - spec/colors_spec.rb
117
+ - spec/dragoon_spec.rb
88
118
  - spec/event_manager_spec.rb
89
119
  - spec/event_spec.rb
90
120
  - spec/spec_helper.rb
@@ -123,6 +153,7 @@ specification_version: 3
123
153
  summary: IRC bot for keyword notification
124
154
  test_files:
125
155
  - spec/colors_spec.rb
156
+ - spec/dragoon_spec.rb
126
157
  - spec/event_manager_spec.rb
127
158
  - spec/event_spec.rb
128
159
  - spec/spec_helper.rb
@@ -1,35 +0,0 @@
1
- module Dragoon
2
- module Commands
3
-
4
- def nick(nickname)
5
- write "NICK #{nickname}"
6
- end
7
-
8
- def user(username, hostname, servername, realname)
9
- write "USER #{username} #{hostname} #{servername} #{realname}"
10
- end
11
-
12
- def pass(password)
13
- write "PASS #{password}"
14
- end
15
-
16
- def pong(server)
17
- write "PONG #{server}"
18
- end
19
-
20
- def join(channel)
21
- write "JOIN #{channel}"
22
- end
23
-
24
- def join_channels
25
- @channels.each { |channel| join(channel) }
26
- end
27
-
28
- private
29
-
30
- def write(text)
31
- @socket.write "#{text}\r\n"
32
- end
33
-
34
- end
35
- end