fantasy-irc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.md ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2013 Jens Becker
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # fantasy-irc
2
+
3
+ A modular, event-driven IRC client gem for Ruby with plugin support
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'fantasy-irc'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install fantasy-irc
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
30
+
data/docs/PLUGINS.md ADDED
@@ -0,0 +1,11 @@
1
+ PLUGINS
2
+ =======
3
+
4
+ Available Plugins
5
+ -----------------
6
+
7
+ * corecommands
8
+ 'Provides the ollowing commands:'
9
+ * !date
10
+ * !ping
11
+ * !host
data/docs/example.rb ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fantasy-irc'
3
+
4
+ # new connection
5
+ bot = Fantasy::IRC.new
6
+
7
+ # load some plugins
8
+ bot.plugins.load 'corecommands'
9
+
10
+ # log in once we are connected
11
+ connected = Proc.new do
12
+ bot.login :nickname => "example", :username => "example", :realname => "GECOS field"
13
+ end
14
+ bot.events.by_name('connected').register &connected
15
+
16
+ # join a room and say hi
17
+ loggedin = Proc.new do
18
+ %w{#lobby}.each do |r|
19
+ room = bot.rooms.new(r).join.say "ohai"
20
+ end
21
+ end
22
+ bot.events.by_name('loggedin').register &loggedin
23
+
24
+ # we also want to greet users that are joining the room
25
+ user_joined = Proc.new do |room, user|
26
+ room.say("Hey, #{user.name}!")
27
+ end
28
+ bot.events.by_name('user_joined').register &user_joined
29
+
30
+ # and monitor everything they say
31
+ channel_message = Proc.new do |room, user, text|
32
+ puts "!! #{user.name} said in room #{room.name}: #{text}"
33
+ end
34
+ bot.events.by_name('channel_message').register &channel_message
35
+
36
+ bot.connect :server => "irc.example.com", :ssl => true, :port => 6697
37
+ bot.run
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |s|
2
+ s.authors = ["Jens Becker"]
3
+ s.email = ["v2px@v2px.de"]
4
+ s.description = "A modular, event-driven IRC client/bot gem for Ruby with plugin support"
5
+ s.summary = "A modular, event-driven IRC client/bot gem for Ruby with plugin support"
6
+ s.homepage = "https://github.com/v2px/fantasy-irc"
7
+
8
+ s.files = `git ls-files`.split("\n")
9
+ s.name = "fantasy-irc"
10
+ s.require_paths = ['lib']
11
+ s.version = "0.1.0"
12
+
13
+ s.rubyforge_project = s.name
14
+ s.add_runtime_dependency "array-unique", "~> 1.1.1"
15
+ end
@@ -0,0 +1,5 @@
1
+ require "fantasy-irc/irc"
2
+ require "fantasy-irc/events"
3
+ require "fantasy-irc/plugins"
4
+ require "fantasy-irc/rooms"
5
+ require "fantasy-irc/users"
@@ -0,0 +1,64 @@
1
+ require 'securerandom'
2
+
3
+ module Fantasy
4
+ module Event
5
+ class Factory
6
+ def initialize
7
+ puts "Initializing new Event::Factory #{self}"
8
+ @data, @data[:events] = Hash.new, Hash.new
9
+ end
10
+
11
+ def create name
12
+ name.downcase!
13
+
14
+ if not @data[:events][name].nil? then
15
+ return @data[:events][name]
16
+ # TODO: log warning
17
+ end
18
+
19
+ @data[:events][name] = Event.new(name)
20
+ end
21
+
22
+ def by_name name
23
+ name.downcase!
24
+
25
+ if not @data[:events][name] then
26
+ raise "Tried to access unknown event \"#{name}\" in Event::Factory \"#{self}\""
27
+ end
28
+
29
+ @data[:events][name]
30
+ end
31
+ end
32
+
33
+ class Event
34
+ attr_reader :name
35
+
36
+ def initialize(name)
37
+ puts "New Event with name #{name}"
38
+ @name = name
39
+ @data = Hash.new
40
+ end
41
+
42
+ def register(&callback)
43
+ uuid = SecureRandom.uuid()
44
+
45
+ if @data[:callbacks].nil? then
46
+ @data[:callbacks] = Hash.new
47
+ end
48
+
49
+ @data[:callbacks][uuid] = callback
50
+ puts "#{self}: registered callback #{callback} with uuid #{uuid}."
51
+ end
52
+
53
+ def call(args=nil)
54
+ if @data[:callbacks].nil? or @data[:callbacks].empty? then
55
+ return
56
+ end
57
+
58
+ @data[:callbacks].each { |uuid, proc|
59
+ proc.call(args)
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,202 @@
1
+ require 'socket' # TCPSocket
2
+ require 'openssl'
3
+ require 'array-unique'
4
+
5
+ #require "fantasy-irc/rooms"
6
+ #require "fantasy-irc/users"
7
+ #require "fantasy-irc/events"
8
+ #require "fantasy-irc/plugins"
9
+
10
+ module Fantasy
11
+ class IRC
12
+ attr_reader :events, :rooms, :users, :plugins
13
+ attr_reader :started, :session, :loggedin, :connected
14
+
15
+ def initialize options={}
16
+ $bot = self
17
+ @data = Hash.new
18
+ @started = Time.now.to_i
19
+ @running = 0
20
+ @loggedin = 0
21
+ @connected = 0
22
+ @prefix = options[:prefix] || '!'
23
+
24
+ @events = Event::Factory.new
25
+ @rooms = Room::Factory.new self
26
+ @users = User::Factory.new self
27
+ @plugins = Plugins.new
28
+
29
+ %w{tick loggedin connected user_joined channel_message}.each do |e|
30
+ @events.create(e)
31
+ end
32
+ end
33
+
34
+ def login data
35
+ if not data[:nickname] and data[:username] then
36
+ data[:nickname] = data[:username]
37
+ end
38
+ if not data[:username] and data[:nickname] then
39
+ data[:username] = data[:nickname]
40
+ end
41
+ if not data[:realname] then
42
+ data[:realname] = "https://bitbucket.org/v2px/fantasy-irc"
43
+ end
44
+
45
+ if not data[:nickname] then
46
+ raise "you need to specify :nickname and/or :username on login"
47
+ end
48
+
49
+ if data[:password] then
50
+ self.send("PASS #{data[:password]}")
51
+ end
52
+
53
+ self.send("USER #{data[:username]} fantasy fantasy :#{data[:realname]}")
54
+ self.send("NICK #{data[:nickname]}")
55
+ end
56
+
57
+ def send(s)
58
+ # Send a message to the server
59
+ if not @connected then
60
+ raise "not connected to a server!"
61
+ end
62
+ puts "<-- #{s}"
63
+ @data[:socket].puts "#{s}\n"
64
+ end
65
+
66
+ def connect data
67
+ if not data[:server] then
68
+ raise "you need to specify :server on connect"
69
+ end
70
+ if not data[:ssl] then
71
+ data[:ssl] = false
72
+ end
73
+ if not data[:port] then
74
+ data[:port] = data[:ssl] ? 6697 : 6667
75
+ end
76
+
77
+ # Connect to the chat server
78
+ @data[:socket] = @data[:realsocket] = TCPSocket.open(data[:server], data[:port])
79
+
80
+ if !!data[:ssl] then
81
+ @data[:socket] = OpenSSL::SSL::SSLSocket.new(@data[:realsocket])
82
+ @data[:socket].connect
83
+ end
84
+
85
+ @connected = Time.now.to_i
86
+ self.events.by_name('connected').call
87
+ end
88
+
89
+ def cleanup
90
+ puts "[!][Cleanup] Closing socket."
91
+ @data[:socket].close()
92
+ end
93
+
94
+ def parse(s)
95
+ s.chomp!
96
+
97
+ if (s[0,4] == "PING") then
98
+ tok = s.split(' ', 2)
99
+ self.send "PONG #{tok[1]}"
100
+
101
+ elsif (s[0,1] == ":") then
102
+ # Server stuff
103
+ tok = s.split(' ', 4)
104
+
105
+ if (tok[1] == "001") then
106
+ # loggedin
107
+ @loggedin = Time.now.to_i
108
+ # ignore ourself
109
+ self.users.create(tok[2]).ignore!
110
+ self.events.by_name('loggedin').call
111
+
112
+ elsif (tok[1] == "JOIN") then
113
+ # user joined
114
+ room = self.rooms.by_name(tok[2][1,tok[2].length])
115
+ user = self.users.create(tok[0][1,tok[0].length])
116
+
117
+ # add user to room and room to user
118
+ room.users << user
119
+ user.rooms << room
120
+
121
+ return if user.ignored?
122
+
123
+ self.events.by_name('user_joined').call([room, user])
124
+
125
+ elsif (tok[1] == "PRIVMSG") then
126
+ # tok = s.split(' ', 4)
127
+
128
+ # channel or private message
129
+ if (tok[2][0,1] == "#") then
130
+ # channel message
131
+ room = self.rooms.by_name(tok[2])
132
+ user = self.users.create(tok[0][1,tok[0].length])
133
+
134
+ # add user to room and room to user
135
+ room.users << user
136
+ user.rooms << room
137
+
138
+ return if user.ignored?
139
+
140
+ text = tok[3][1,tok[3].length]
141
+ self.events.by_name('channel_message').call([room, user, text])
142
+
143
+ if text[0] == @prefix then
144
+ command, args = text.split(' ', 2)
145
+ self.plugins.command(command[1,command.length], {:room => room, :user => user}, args)
146
+ end
147
+ end
148
+
149
+ else
150
+ # puts "[!] UNKNOWN PROTOCOL PART: #{s}"
151
+ end
152
+ else
153
+
154
+ puts "[!] UNKNOWN PROTOCOL PART: #{s}"
155
+ end
156
+ end
157
+
158
+ def run
159
+ if @running.nonzero? then
160
+ return false
161
+ end
162
+
163
+ @running = Time.now.to_i
164
+ last_tick = @running
165
+ last_ping = @running
166
+ last_session_ping = @running
167
+
168
+ loop do
169
+ time_now = Time.now
170
+
171
+ # tick every second
172
+ if time_now.to_i > last_tick then
173
+ self.events.by_name('tick').call(time_now)
174
+ last_tick = time_now.to_i
175
+ end
176
+
177
+ # chatserver ping, every 5 minutes
178
+ if @connected.nonzero? and time_now.to_i-300 >= last_ping then
179
+ self.send("PING :"+time_now.to_f.to_s)
180
+ last_ping = time_now.to_i
181
+ end
182
+
183
+ # connection
184
+ if @connected.nonzero? then
185
+ ready = select([@data[:socket]], nil, nil, 0.1)
186
+ next if !ready
187
+ for s in ready[0]
188
+ if s == @data[:socket] then
189
+ return if @data[:socket].eof # XXX?
190
+ s = @data[:socket].gets
191
+ puts "--> #{s}"
192
+ self.parse(s)
193
+ end
194
+ end
195
+ else # no connection, less cpu usage
196
+ sleep(0.5)
197
+ end
198
+
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,61 @@
1
+ module Fantasy
2
+ class Plugins
3
+ attr_reader :plugins
4
+
5
+ def initialize
6
+ @plugins = {}
7
+ end
8
+
9
+ def add plugin
10
+ @plugins[plugin.name] = plugin
11
+ puts "#{plugin.name} = #{plugin}"
12
+ end
13
+
14
+ def load name
15
+ Kernel::load "plugins/#{name}.rb"
16
+ end
17
+
18
+ def command command, data, args
19
+ if not args.nil? then
20
+ args = args.split(' ')
21
+ end
22
+ @plugins.values.each do |plugin|
23
+ puts "#{plugin}"
24
+ plugin.handle! command, data, args
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ class Plugin
31
+ attr_reader :name
32
+
33
+ def initialize name
34
+ @name = name
35
+ @handlers = {}
36
+ end
37
+
38
+ def handle pattern, &block
39
+ @handlers[pattern] = block
40
+ end
41
+
42
+ def handles? command
43
+ @handlers.keys.each do |pattern|
44
+ if command.match pattern then
45
+ return true
46
+ end
47
+ end
48
+
49
+ return false
50
+ end
51
+
52
+ def handle! command, data, args=[]
53
+ puts "trying to handle #{command} with #{data} and #{args}"
54
+ @handlers.each do |pattern, block|
55
+ if command.match(pattern) then
56
+ puts "#{block} handles #{command}"
57
+ break block.call data, args
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,79 @@
1
+ #require "fantasy-irc/connection"
2
+
3
+ module Fantasy
4
+ module Room
5
+ class Factory
6
+ def initialize connection
7
+ puts "Initializing new Room::Factory #{self} with connection #{connection}"
8
+ @data, @data[:rooms] = Hash.new, Hash.new
9
+ @connection = connection
10
+ end
11
+
12
+ def new name
13
+ name.downcase!
14
+
15
+ if not @data[:rooms][name].nil? then
16
+ raise "We already know the room #{name}"
17
+ end
18
+
19
+ @data[:rooms][name] = Room.new(name, @connection)
20
+ end
21
+
22
+ alias :create :new
23
+
24
+ def by_name name
25
+ name.downcase!
26
+
27
+ if not @data[:rooms][name] then
28
+ raise "Tried to access unknown room \"#{name}\" in Room::Factory \"#{self}\""
29
+ end
30
+
31
+ @data[:rooms][name]
32
+ end
33
+ end
34
+
35
+ class Room
36
+ attr_reader :name, :users
37
+
38
+ def initialize name, connection
39
+ if not connection.respond_to?(:send) then
40
+ raise "Connection class needs to be able to respond to :send"
41
+ end
42
+
43
+ puts "New Room #{self.object_id} with name #{name}"
44
+ @name = name
45
+ @joined = false
46
+ @connection = connection
47
+ @users = Array::Unique.new
48
+ end
49
+
50
+ def join
51
+ if @joined == true then
52
+ raise "Already joined room #{@name}."
53
+ end
54
+
55
+ @connection.send("JOIN "+@name)
56
+ @joined = true # TODO: maybe we should set that if we get the
57
+ # correct reply. this is for testing only! XXX
58
+ return self
59
+ end
60
+
61
+ def joined?
62
+ !!@joined
63
+ end
64
+
65
+ def say message
66
+ if @joined == false then
67
+ raise "Tried to talk to a room (#{name}) we're not in."
68
+ end
69
+
70
+ @connection.send('PRIVMSG '+@name+' :'+message)
71
+ return self
72
+ end
73
+
74
+ def to_s
75
+ "#{@name}"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,80 @@
1
+ module Fantasy
2
+ module User
3
+ class Factory
4
+ def initialize connection
5
+ puts "Initializing new User::Factory #{self} with connection #{connection}"
6
+ @data, @data[:users] = Hash.new, Hash.new
7
+ @connection = connection
8
+ end
9
+
10
+ def new name
11
+ name = name.split(/!/, 2)[0] # remove mask
12
+ name_lc = name.downcase
13
+
14
+ if not @data[:users][name_lc].nil? then
15
+ return @data[:users][name_lc]
16
+ # TODO: log! not raise
17
+ # raise "We already know the user #{name}"
18
+ end
19
+
20
+ @data[:users][name_lc] = User.new(name, @connection)
21
+ end
22
+
23
+ alias :create :new
24
+
25
+ def by_name name
26
+ name.downcase!
27
+
28
+ if not @data[:users][name] then
29
+ raise "Tried to access unknown user \"#{name}\" in User::Factory \"#{self}\""
30
+ end
31
+
32
+ @data[:users][name]
33
+ end
34
+ end
35
+
36
+ class User
37
+ attr_reader :name, :mask
38
+ attr_accessor :rooms
39
+
40
+ def initialize name, connection
41
+ if not connection.respond_to?(:send) then
42
+ raise "Connection class needs to be able to respond to :send"
43
+ end
44
+
45
+ @name = name
46
+ puts "New User #{self.object_id} with name #{@name}"
47
+ @connection = connection
48
+ @rooms = Array::Unique.new
49
+ end
50
+
51
+ def say message
52
+ @connection.send('PRIVMSG '+@name+' :'+message)
53
+ return self
54
+ end
55
+
56
+ def login
57
+ self.name
58
+ end
59
+
60
+ # ===================
61
+ # Ignoring
62
+ def ignore!
63
+ @ignored = true
64
+ end
65
+
66
+ def unignore!
67
+ @ignored = false
68
+ end
69
+
70
+ def ignored?
71
+ !!@ignored
72
+ end
73
+ # ===================
74
+
75
+ def to_s
76
+ "#{@name}"
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,53 @@
1
+ plugin = Plugin.new "corecommands"
2
+
3
+ plugin.handle(/^date$/i) do |data|
4
+ next data[:room].say Time.now.localtime.strftime "%a, %d %b %Y %T %z"
5
+ end
6
+
7
+ plugin.handle(/^ping$/i) do |data, args|
8
+ if args.empty? then
9
+ next data[:room].say "ping needs more arguments"
10
+ end
11
+
12
+ if args[0].match(/[^a-z0-9\.-]/i) then
13
+ next data[:room].say "invalid characters in argument"
14
+ end
15
+
16
+ Thread.new do
17
+ ping = `/bin/ping -w 3 -c 1 -- #{args[0]} 2>&1`
18
+
19
+ if ping.match /unknown host (.+)/ then
20
+ data[:room].say "unknown host #{$1}"
21
+ elsif ping.match /^(64 bytes.+)/ then
22
+ data[:room].say "#{$1}"
23
+ elsif ping.match /0 received/m then
24
+ data[:room].say "no reply :-("
25
+ else
26
+ data[:room].say "bogus hostname"
27
+ end
28
+
29
+ next
30
+ end
31
+ end
32
+
33
+ plugin.handle(/^host$/i) do |data, args|
34
+ if args.empty? then
35
+ next data[:room].say "host needs more arguments"
36
+ end
37
+
38
+ if args[0].match(/^a-z0-9\.-/i) then
39
+ next data[:room].say "invalid characters in argument"
40
+ end
41
+
42
+ Thread.new do
43
+ host = `/usr/bin/host #{args[0]} 2>&1`
44
+ lines = host.split(/\n/)
45
+ lines.take(3).each do |line|
46
+ data[:room].say line
47
+ end
48
+
49
+ next
50
+ end
51
+ end
52
+
53
+ $bot.plugins.add(plugin)
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fantasy-irc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jens Becker
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: array-unique
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.1.1
30
+ description: A modular, event-driven IRC client/bot gem for Ruby with plugin support
31
+ email:
32
+ - v2px@v2px.de
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - LICENSE.md
38
+ - README.md
39
+ - docs/PLUGINS.md
40
+ - docs/example.rb
41
+ - fantasy-irc.gemspec
42
+ - lib/fantasy-irc.rb
43
+ - lib/fantasy-irc/events.rb
44
+ - lib/fantasy-irc/irc.rb
45
+ - lib/fantasy-irc/plugins.rb
46
+ - lib/fantasy-irc/rooms.rb
47
+ - lib/fantasy-irc/users.rb
48
+ - lib/plugins/corecommands.rb
49
+ homepage: https://github.com/v2px/fantasy-irc
50
+ licenses: []
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project: fantasy-irc
69
+ rubygems_version: 1.8.25
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: A modular, event-driven IRC client/bot gem for Ruby with plugin support
73
+ test_files: []