fantasy-irc 0.1.0

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