vetinari 0.0.1

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.
@@ -0,0 +1,249 @@
1
+ module Vetinari
2
+ # Data structure which is used for storing users:
3
+ # {lower_cased_nick => {User => [modes]}}
4
+ #
5
+ # Example:
6
+ # {'ponder' => {:user => #<User nick="Ponder">, :modes => ['v', 'o']}}
7
+ #
8
+ # TODO: Actor?
9
+ class Channel
10
+ attr_reader :name, :users, :users_with_modes, :modes, :lists
11
+
12
+ def initialize(name, bot)
13
+ @name = name
14
+ @bot = bot
15
+ @users = Set.new
16
+ @users_with_modes = {}
17
+ @modes = {}
18
+ @lists = Hash.new { |hash, key| hash[key] = [] }
19
+ @mutex = Mutex.new
20
+ end
21
+
22
+ # Experimental, no tests so far.
23
+ # def topic
24
+ # if @topic
25
+ # @topic
26
+ # else
27
+ # connected do
28
+ # fiber = Fiber.current
29
+ # callbacks = {}
30
+ # [331, 332, 403, 442].each do |numeric|
31
+ # callbacks[numeric] = @thaum.on(numeric) do |event_data|
32
+ # topic = event_data[:params].match(':(.*)').captures.first
33
+ # fiber.resume topic
34
+ # end
35
+ # end
36
+
37
+ # @bot.raw "TOPIC #{@name}"
38
+ # @topic = Fiber.yield
39
+ # callbacks.each do |type, callback|
40
+ # @thaum.callbacks[type].delete(callback)
41
+ # end
42
+
43
+ # @topic
44
+ # end
45
+ # end
46
+ # end
47
+
48
+ def topic=(topic)
49
+ @bot.raw "TOPIC #{@name} :#{topic}"
50
+ end
51
+
52
+ def ban(hostmask)
53
+ mode '+b', hostmask
54
+ end
55
+
56
+ def unban(hostmask)
57
+ mode '-b', hostmask
58
+ end
59
+
60
+ def lock(key)
61
+ @bot.raw "MODE #{@name} +k #{key}"
62
+ end
63
+
64
+ def unlock
65
+ key = @modes['k']
66
+ @bot.raw "MODE #{@name} -k #{key}" if key
67
+ end
68
+
69
+ def kick(user, reason = nil)
70
+ if reason
71
+ @bot.raw "KICK #{@name} #{user.nick} :#{reason}"
72
+ else
73
+ @bot.raw "KICK #{@name} #{user.nick}"
74
+ end
75
+ end
76
+
77
+ def invite(user)
78
+ @bot.raw "INVITE #{@name} #{user.nick}"
79
+ end
80
+
81
+ def op(user)
82
+ mode '+o', user.nick
83
+ end
84
+
85
+ def deop(user)
86
+ mode '-o', user.nick
87
+ end
88
+
89
+ def voice(user_or_nick)
90
+ mode '+v', user.nick
91
+ end
92
+
93
+ def devoice(user_or_nick)
94
+ mode '-v', user.nick
95
+ end
96
+
97
+ def join(key = nil)
98
+ if key
99
+ @bot.raw "JOIN #{@name} #{key}"
100
+ else
101
+ @bot.raw "JOIN #{@name}"
102
+ end
103
+ end
104
+
105
+ def part(message = nil)
106
+ if message
107
+ @bot.raw "PART #{@name} :#{message}"
108
+ else
109
+ @bot.raw "PART #{@name}"
110
+ end
111
+ end
112
+
113
+ def hop(message = nil)
114
+ key = @modes['k']
115
+ part message
116
+ join key
117
+ end
118
+
119
+ def add_user(user, modes = [])
120
+ @mutex.synchronize do
121
+ @users << user
122
+ @users_with_modes[user] = modes
123
+ end
124
+ end
125
+
126
+ def remove_user(user)
127
+ @mutex.synchronize do
128
+ @users_with_modes.delete(user)
129
+ @users.delete?(user) ? user : nil
130
+ end
131
+ end
132
+
133
+ def has_user?(user)
134
+ @users.include?(user)
135
+ end
136
+
137
+ # def find_user(user)
138
+ # case user_or_nick
139
+ # when String
140
+ # @users.find { |u| u.nick.downcase == user_or_nick.downcase }
141
+ # when User
142
+ # has_user?(user_or_nick) ? user_or_nick : nil
143
+ # end
144
+ # end
145
+
146
+ # def find_user_with_modes(user_or_nick)
147
+ # user = case user_or_nick
148
+ # when String
149
+ # @users.find { |u| u.nick.downcase == user_or_nick.downcase }
150
+ # when User
151
+ # has_user?(user_or_nick) ? user_or_nick : nil
152
+ # end
153
+
154
+ # {:user => user, :modes => @users_with_modes[user]} if user
155
+ # end
156
+
157
+ def modes_of(user)
158
+ @users_with_modes[user] if has_user?(user)
159
+ end
160
+
161
+ # TODO
162
+ def set_mode(mode, isupport)
163
+ if isupport['PREFIX'].keys.include?(mode[:mode])
164
+ user = find_user(mode[:param])
165
+ if user
166
+ case mode[:direction]
167
+ when :'+'
168
+ @users_with_modes[user] << mode[:mode]
169
+ when :'-'
170
+ @users_with_modes[user].delete(mode[:mode])
171
+ end
172
+ end
173
+ elsif isupport['CHANMODES']['A'].include?(mode[:mode])
174
+ case mode[:direction]
175
+ when :'+'
176
+ add_to_list(mode[:mode], mode[:param])
177
+ when :'-'
178
+ remove_from_list(mode[:mode], mode[:param])
179
+ end
180
+ elsif isupport['CHANMODES']['B'].include?(mode[:mode])
181
+ case mode[:direction]
182
+ when :'+'
183
+ set_channel_mode(mode[:mode], mode[:param])
184
+ when :'-'
185
+ unset_channel_mode(mode[:mode])
186
+ end
187
+ elsif isupport['CHANMODES']['C'].include?(mode[:mode])
188
+ case mode[:direction]
189
+ when :'+'
190
+ set_channel_mode(mode[:mode], mode[:param])
191
+ when :'-'
192
+ unset_channel_mode(mode[:mode])
193
+ end
194
+ elsif isupport['CHANMODES']['D'].include?(mode[:mode])
195
+ case mode[:direction]
196
+ when :'+'
197
+ set_channel_mode(mode[:mode], true)
198
+ when :'-'
199
+ unset_channel_mode(mode[:mode])
200
+ end
201
+ end
202
+ end
203
+
204
+ def mode(modes, params = nil)
205
+ if params
206
+ @bot.raw "MODE #{@name} #{modes} #{params}"
207
+ else
208
+ @bot.raw "MODE #{@name} #{modes}"
209
+ end
210
+ end
211
+
212
+ def get_mode
213
+ @bot.raw "MODE #{@name}"
214
+ end
215
+
216
+ def message(message)
217
+ @bot.raw "PRIVMSG #{@name} :#{message}"
218
+ end
219
+
220
+ def inspect
221
+ "#<Channel name=#{@name.inspect}>"
222
+ end
223
+
224
+ private
225
+
226
+ def set_channel_mode(mode, param)
227
+ @modes[mode] = param
228
+ end
229
+
230
+ def unset_channel_mode(mode)
231
+ @modes.delete(mode)
232
+ end
233
+
234
+ def add_to_list(list, param)
235
+ @mutex.synchronize do
236
+ @lists[list] ||= []
237
+ @lists[list] << param
238
+ end
239
+ end
240
+
241
+ def remove_from_list(list, param)
242
+ @mutex.synchronize do
243
+ if @lists[list].include?(param)
244
+ @lists[list].delete(param)
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,74 @@
1
+ module Vetinari
2
+ class ChannelContainer
3
+ include Celluloid
4
+
5
+ attr_reader :channels
6
+
7
+ exclusive
8
+
9
+ def initialize
10
+ @channels = Set.new
11
+ end
12
+
13
+ def add(channel)
14
+ @channels << channel
15
+ end
16
+
17
+ def [](channel_or_channel_name)
18
+ case channel_or_channel_name
19
+ when Channel
20
+ if @channels.include?(channel_or_channel_name)
21
+ channel_or_channel_name
22
+ end
23
+ when String
24
+ @channels.find do |c|
25
+ c.name.downcase == channel_or_channel_name.downcase
26
+ end
27
+ end
28
+ end
29
+
30
+ def has_channel?(channel)
31
+ self[channel] ? true : false
32
+ end
33
+
34
+ def remove(channel)
35
+ if has_channel?(channel)
36
+ @channels.delete(channel)
37
+ end
38
+ end
39
+
40
+ # Removes a User from all Channels from the ChannelList.
41
+ # Returning a Set of Channels in which the User was.
42
+ def remove_user(user)
43
+ channels = Set.new
44
+
45
+ @channels.each do |channel|
46
+ if channel.remove_user(user)
47
+ channels << channel
48
+ end
49
+ end
50
+
51
+ channels
52
+ end
53
+
54
+ # Removes all Channels from the ChannelList and returns them.
55
+ def clear
56
+ channels = @channels.dup
57
+ @channels.clear
58
+ channels
59
+ end
60
+
61
+ # Returns a Set of all Users that are in one of the Channels from the
62
+ # ChannelList.
63
+ # TODO: Refactor
64
+ def users
65
+ users = Set.new
66
+
67
+ @channels.each do |channel|
68
+ users.merge(channel.users)
69
+ end
70
+
71
+ users
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,54 @@
1
+ module Vetinari
2
+ class Configuration < OpenStruct
3
+ def initialize(&block)
4
+ super
5
+ self.server = 'chat.freenode.org'
6
+ self.port = 6667
7
+ self.ssl = false
8
+ self.nick = "Vetinari#{rand(10_000)}"
9
+ self.username = 'Vetinari'
10
+ self.real_name = 'Havelock Vetinari'
11
+ self.verbose = true
12
+ self.logging = false
13
+ self.logger = nil
14
+ self.reconnect = true
15
+ self.reconnect_interval = 10
16
+ self.hide_ping_pongs = true
17
+ self.rejoin_after_kick = false
18
+ self.password = nil
19
+
20
+ self.isupport = ISupport.new
21
+ self.dcc = OpenStruct.new
22
+ self.dcc.ports = []
23
+
24
+ block.call(self) if block_given?
25
+ setup_loggers
26
+ end
27
+
28
+ private
29
+
30
+ def setup_loggers
31
+ self.console_logger = if self.verbose
32
+ Logging::Logger.new($stdout)
33
+ else
34
+ Logging::NullLogger.new
35
+ end
36
+
37
+ self.logger = if self.logging
38
+ if self.logger
39
+ self.logger
40
+ else
41
+ log_path = File.join($0, 'logs', 'log.log')
42
+ log_dir = File.dirname(log_path)
43
+ FileUtils.mkdir_p(log_dir) unless File.exist?(log_dir)
44
+ Logging::Logger.new(log_path, File::WRONLY | File::APPEND)
45
+ end
46
+ else
47
+ Logging::NullLogger.new
48
+ end
49
+
50
+ self.loggers = Logging::LoggerList.new
51
+ self.loggers.push(self.console_logger, self.logger)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,113 @@
1
+ module Vetinari
2
+ module Dcc
3
+ module Incoming
4
+ class File
5
+ include Celluloid::IO, Celluloid::Notifications
6
+
7
+ attr_reader :user, :filename, :ip, :port, :filesize, :state
8
+
9
+ def initialize(user, filename, ip, port, filesize, bot)
10
+ @user = user
11
+ @filename = filename
12
+ @ip = ip
13
+ @port = port
14
+ @filesize = filesize
15
+ @bot = bot
16
+ @mutex = Mutex.new
17
+
18
+ self.state = :pending
19
+ end
20
+
21
+ def accept(directory = '~/Downloads/', resume = false)
22
+ if @state == :pending
23
+ self.state = :accepted
24
+ directory = ::File.expand_path(directory)
25
+ @filepath = ::File.join(directory, @filename)
26
+
27
+ if resume && resumable?
28
+ resume_transfer
29
+ else
30
+ download
31
+ end
32
+ end
33
+ end
34
+
35
+ def inspect
36
+ "#<File filename=#{@filename} filesize=#{@filesize} user=#{@user.inspect}"
37
+ end
38
+
39
+ def resume_accepted(position)
40
+ self.state = :resume_accepted
41
+ download(position)
42
+ end
43
+
44
+ def state=(state)
45
+ old_state = @state
46
+ @state = state
47
+ publish('vetinari.dcc.incoming.file', Actor.current, old_state, @state)
48
+ end
49
+
50
+ private
51
+
52
+ def download(position = 0)
53
+ self.state = :connecting
54
+
55
+ begin
56
+ @socket = TCPSocket.new(@ip.to_s, @port)
57
+ self.state = :connected
58
+ rescue Errno::ECONNREFUSED
59
+ self.state = :failed
60
+ return
61
+ end
62
+
63
+ file_mode = position > 0 ? 'a' : 'w'
64
+
65
+ begin
66
+ ::File.open(@filepath, file_mode) do |file|
67
+ self.state = :downloading
68
+
69
+ while buffer = @socket.readpartial(8192)
70
+ position += buffer.bytesize
71
+ file.write(buffer)
72
+ break if position >= @filesize
73
+ end
74
+ end
75
+
76
+ self.state = :finished
77
+ rescue EOFError
78
+ self.state = :aborted
79
+ ensure
80
+ @socket.close
81
+ end
82
+ ensure
83
+ return self
84
+ end
85
+
86
+ def resumable?
87
+ ::File.file?(@filepath) && ::File.size(@filepath) <= @filesize
88
+ end
89
+
90
+ def resume_transfer
91
+ self.state = :resuming
92
+ filename = Regexp.escape(@filename)
93
+ position = ::File.size(@filepath)
94
+ file = Actor.current
95
+
96
+ cb = @bot.on(:query, /\A\001DCC ACCEPT \"?#{filename}\"? #{@port} #{position}\001\z/, 1) do |env|
97
+ file.async.resume_accepted(position)
98
+ cb.async.remove
99
+ end
100
+
101
+ @user.message("\001DCC RESUME \"#{@filename}\" #{@port} #{position}\001")
102
+
103
+ after(30) do
104
+ if @state == :resuming
105
+ cb.remove
106
+ file.state = :resume_not_accepted
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,107 @@
1
+ module Vetinari
2
+ module Dcc
3
+ class Server
4
+ include Celluloid
5
+
6
+ attr_reader :user, :filepath, :state
7
+
8
+ def initialize(user, filepath, filename, server_manager)
9
+ @user = user
10
+ @filepath = filepath
11
+ @filename = filename
12
+ @server_manager = server_manager
13
+ @state = :idling
14
+ end
15
+
16
+ def run(port)
17
+ @port = port
18
+ register_resume_callback
19
+ start
20
+ end
21
+
22
+ def inspect
23
+ "#<Server @user=#{@user.inspect} @filepath=#{filepath}"
24
+ end
25
+
26
+ private
27
+
28
+ def start
29
+ @thread = Thread.new do
30
+ begin
31
+ int_ip = IPAddr.new(@server_manager.external_ip).to_i
32
+ @socket = TCPServer.new(@server_manager.internal_ip, @port)
33
+ @state = :running
34
+ @user.message("\001DCC SEND \"#{@filename}\" #{int_ip} #{@port} #{File.size(@filepath)}\001")
35
+ @client = @socket.accept
36
+ @state = :sending
37
+
38
+ File.open(@filepath, 'rb') do |file|
39
+ file.seek(@position) if @position
40
+
41
+ while buffer = file.read(8096)
42
+ @client.write(buffer)
43
+ end
44
+ end
45
+
46
+ # Clients like mIRC want to close the connection
47
+ # client side. Else the transfer will count as
48
+ # failed. So, give the client a second to close.
49
+ sleep 1
50
+ rescue StandardError => e
51
+ p e.message
52
+ ensure
53
+ # binding.pry
54
+ async.stop
55
+ end
56
+ end
57
+
58
+ after(5) do
59
+ p 'TIMMERERRRRRRRRRRRRRRRRRRRRR'
60
+ if @state == :running
61
+ async.stop(:timeout)
62
+ p :async_stop
63
+ end
64
+ end
65
+ end
66
+
67
+ def stop(state = :stopped)
68
+ p 'SERVER STOPPED'
69
+ @state = state
70
+ @client.close rescue nil
71
+ @socket.close rescue nil
72
+ remove_resume_callback
73
+ start_next
74
+ end
75
+
76
+ def start_next
77
+ @server_manager.release_port(@port)
78
+ p 'release port'
79
+ @server_manager.async.start_sending
80
+ p 'start sending'
81
+ end
82
+
83
+ def register_resume_callback
84
+ @resume_callback = @server_manager.bot.on(:query, /\A\001DCC RESUME \"#{Regexp.escape(@filename)}\" #{@port} \d+\001\z/, 1) do |env|
85
+ if @state == :running
86
+ position = begin
87
+ result = env[:message].scan(/\A\001DCC RESUME \"#{Regexp.escape(@filename)}\" #{@port} (\d+)\001\z/)
88
+ Integer(result.first.first)
89
+ rescue
90
+ 0
91
+ end
92
+
93
+ if position > 0 && position < File.size(@filepath)
94
+ @position = position
95
+ @user.message("\001DCC ACCEPT \"#{@filename}\" #{@port} #{@position}")
96
+ remove_resume_callback
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def remove_resume_callback
103
+ @resume_callback.async.remove
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,83 @@
1
+ module Vetinari
2
+ module Dcc
3
+ class ServerManager
4
+ include Celluloid
5
+
6
+ attr_reader :bot, :internal_ip, :external_ip
7
+
8
+ def initialize(bot)
9
+ @bot = bot
10
+ @external_ip = @bot.config.dcc.external_ip
11
+
12
+ ip = Socket.ip_address_list.detect { |i| i.ipv4_private? }
13
+ @internal_ip = ip ? ip.ip_address : '0.0.0.0'
14
+
15
+ @ports = @bot.config.dcc.ports
16
+ @queue = []
17
+ @mutex = Mutex.new
18
+ @running_servers = {} # {port => #<Vetinari::Dcc::Server>}
19
+ end
20
+
21
+ def add_offering(user, filepath, filename)
22
+ server = Server.new(user, filepath, filename, Actor.current)
23
+ @mutex.synchronize { @queue << server }
24
+ async.start_sending
25
+ server
26
+ end
27
+
28
+ def start_sending
29
+ @mutex.synchronize do
30
+ if @queue.any?
31
+ port = get_available_port
32
+
33
+ if port
34
+ server = @queue.pop
35
+ server.async.run(port)
36
+ @running_servers[port] = server
37
+ else
38
+ # There are items in the queue but for some reasons no ports are
39
+ # available. Try again later.
40
+ after(3) { start_sending }
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def release_port(port)
47
+ @mutex.synchronize do
48
+ @running_servers.delete(port)
49
+ end
50
+ end
51
+
52
+ # def find_server(user, filename, port)
53
+ # server = @running_servers[port]
54
+
55
+ # if server && server.user == user && server.filename == filename
56
+ # end
57
+ # end
58
+
59
+ private
60
+
61
+ def get_available_port
62
+ (@ports - @running_servers.keys).shuffle.each do |port|
63
+ return port if port_available?(port)
64
+ end
65
+
66
+ nil
67
+ end
68
+
69
+ def port_available?(port)
70
+ Timeout::timeout(3) do
71
+ begin
72
+ TCPServer.new(port).close
73
+ true
74
+ rescue Errno::EADDRINUSE
75
+ false
76
+ end
77
+ end
78
+ rescue Timeout::Error
79
+ false
80
+ end
81
+ end
82
+ end
83
+ end