torchat 0.0.1.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +43 -0
- data/bin/torchatd +537 -0
- data/doc/protocol/broadcast.md +26 -0
- data/doc/protocol/groupchat.md +140 -0
- data/doc/protocol/latency.md +30 -0
- data/doc/protocol/standard.md +279 -0
- data/doc/protocol/typing.md +41 -0
- data/etc/torchat.yml +12 -0
- data/lib/torchat.rb +239 -0
- data/lib/torchat/protocol.rb +256 -0
- data/lib/torchat/protocol/broadcast.rb +40 -0
- data/lib/torchat/protocol/groupchat.rb +197 -0
- data/lib/torchat/protocol/latency.rb +44 -0
- data/lib/torchat/protocol/standard.rb +367 -0
- data/lib/torchat/protocol/typing.rb +36 -0
- data/lib/torchat/session.rb +603 -0
- data/lib/torchat/session/broadcast/message.rb +50 -0
- data/lib/torchat/session/broadcasts.rb +72 -0
- data/lib/torchat/session/buddies.rb +152 -0
- data/lib/torchat/session/buddy.rb +343 -0
- data/lib/torchat/session/buddy/joined_group_chat.rb +42 -0
- data/lib/torchat/session/buddy/joined_group_chats.rb +46 -0
- data/lib/torchat/session/buddy/latency.rb +54 -0
- data/lib/torchat/session/event.rb +79 -0
- data/lib/torchat/session/file_transfer.rb +173 -0
- data/lib/torchat/session/file_transfer/block.rb +51 -0
- data/lib/torchat/session/file_transfers.rb +89 -0
- data/lib/torchat/session/group_chat.rb +123 -0
- data/lib/torchat/session/group_chat/participant.rb +38 -0
- data/lib/torchat/session/group_chat/participants.rb +52 -0
- data/lib/torchat/session/group_chats.rb +74 -0
- data/lib/torchat/session/incoming.rb +187 -0
- data/lib/torchat/session/outgoing.rb +102 -0
- data/lib/torchat/tor.rb +107 -0
- data/lib/torchat/utils.rb +87 -0
- data/lib/torchat/version.rb +24 -0
- data/test/file_transfer/receiver.rb +41 -0
- data/test/file_transfer/sender.rb +45 -0
- data/test/group_chat/a.rb +37 -0
- data/test/group_chat/b.rb +37 -0
- data/test/group_chat/c.rb +57 -0
- data/torchat.gemspec +21 -0
- metadata +140 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
Typing notice extension lifecycle and packet description
|
2
|
+
========================================================
|
3
|
+
This is a simple extension to support typing notice.
|
4
|
+
|
5
|
+
They're simply packets sent to the buddy you're talking to to tell them your current
|
6
|
+
typing status.
|
7
|
+
|
8
|
+
Packets
|
9
|
+
-------
|
10
|
+
Following is a list and description of the packets in the groupchat extension.
|
11
|
+
|
12
|
+
In the examples `>` refers to packet sent to the buddy, while `<` refers to packet received
|
13
|
+
from the buddy.
|
14
|
+
|
15
|
+
### Typing Start
|
16
|
+
|
17
|
+
This packet is used to tell the other end we started typing.
|
18
|
+
|
19
|
+
```
|
20
|
+
> typing_start
|
21
|
+
```
|
22
|
+
|
23
|
+
### Typing Thinking
|
24
|
+
|
25
|
+
This packet is used to tell the other end we stopped typing momentarily to think.
|
26
|
+
|
27
|
+
This packet can be not sent at all, it's just a fancy addition because BitlBee supports it.
|
28
|
+
|
29
|
+
```
|
30
|
+
> typing_thinking
|
31
|
+
```
|
32
|
+
|
33
|
+
### Typing Stop
|
34
|
+
|
35
|
+
This packet is used to tell the other end we stopped typing.
|
36
|
+
|
37
|
+
This packet can avoid being sent if the message is sent.
|
38
|
+
|
39
|
+
```
|
40
|
+
> typing_stop
|
41
|
+
```
|
data/etc/torchat.yml
ADDED
data/lib/torchat.rb
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
#--
|
2
|
+
# Copyleft meh. [http://meh.paranoid.pk | meh@paranoici.org]
|
3
|
+
#
|
4
|
+
# This file is part of torchat for ruby.
|
5
|
+
#
|
6
|
+
# torchat for ruby is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Affero General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# torchat for ruby is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Affero General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Affero General Public License
|
17
|
+
# along with torchat for ruby. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#++
|
19
|
+
|
20
|
+
require 'yaml'
|
21
|
+
require 'iniparse'
|
22
|
+
require 'fileutils'
|
23
|
+
|
24
|
+
require 'torchat/version'
|
25
|
+
require 'torchat/utils'
|
26
|
+
require 'torchat/session'
|
27
|
+
require 'torchat/protocol'
|
28
|
+
require 'torchat/tor'
|
29
|
+
|
30
|
+
class Torchat
|
31
|
+
def self.profile (name = nil)
|
32
|
+
FileUtils.mkpath(directory = File.expand_path("~/.torchat#{"_#{name}" if name}"))
|
33
|
+
|
34
|
+
new("#{directory}/torchat.ini").tap {|t|
|
35
|
+
t.name = name
|
36
|
+
t.path = directory
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :config, :session, :tor, :path
|
41
|
+
attr_accessor :name
|
42
|
+
|
43
|
+
def initialize (path)
|
44
|
+
@config = if path.is_a? Hash
|
45
|
+
path
|
46
|
+
elsif path.end_with?('ini')
|
47
|
+
ini = IniParse.parse(File.read(File.expand_path(path)))
|
48
|
+
|
49
|
+
{
|
50
|
+
'id' => ini[:client][:own_hostname],
|
51
|
+
'name' => ini[:profile][:name],
|
52
|
+
'description' => ini[:profile][:text],
|
53
|
+
|
54
|
+
'connection' => {
|
55
|
+
'outgoing' => {
|
56
|
+
'host' => ini[:tor_portable][:tor_server],
|
57
|
+
'port' => ini[:tor_portable][:tor_server_socks_port]
|
58
|
+
},
|
59
|
+
|
60
|
+
'incoming' => {
|
61
|
+
'host' => ini[:client][:listen_interface],
|
62
|
+
'port' => ini[:client][:listen_port]
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
else
|
67
|
+
YAML.parse_file(path).transform
|
68
|
+
end
|
69
|
+
|
70
|
+
if @config['path']
|
71
|
+
if @config['path'].is_a? String
|
72
|
+
self.path = @config['path']
|
73
|
+
else
|
74
|
+
if @config['path']['buddy_list']
|
75
|
+
buddy_list_at @config['path']['buddy_list']
|
76
|
+
end
|
77
|
+
|
78
|
+
if @config['path']['blocked_buddy_list']
|
79
|
+
blocked_buddy_list_at @config['path']['blocked_buddy_list']
|
80
|
+
end
|
81
|
+
|
82
|
+
if @config['path']['offline_messages']
|
83
|
+
offline_messages_at @config['path']['offline_messages']
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
@tor = Tor.new(@config)
|
89
|
+
end
|
90
|
+
|
91
|
+
def method_missing (id, *args, &block)
|
92
|
+
return @session.__send__ id, *args, &block if @session.respond_to? id
|
93
|
+
|
94
|
+
super
|
95
|
+
end
|
96
|
+
|
97
|
+
def respond_to_missing? (id, include_private = false)
|
98
|
+
@session.respond_to? id, include_private
|
99
|
+
end
|
100
|
+
|
101
|
+
def start (&block)
|
102
|
+
@session = Session.new(@config, &block)
|
103
|
+
|
104
|
+
if @config['buddies']
|
105
|
+
@config['buddies'].each {|id|
|
106
|
+
@session.buddies.add(id)
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
if @buddy_list && File.readable?(@buddy_list)
|
111
|
+
File.open(@buddy_list).lines.each {|line|
|
112
|
+
line.match(/^(.+?)(?: (.*?))?$/) {|m|
|
113
|
+
next if m[1] == @session.id
|
114
|
+
|
115
|
+
@session.buddies.add(m[1], m[2])
|
116
|
+
}
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
if @blocked_buddy_list && File.readable?(@blocked_buddy_list)
|
121
|
+
File.open(@blocked_buddy_list).lines.each {|line|
|
122
|
+
line.match(/^(.+?)(?: (.*?))?$/) {|m|
|
123
|
+
next if m[1] == @session.id
|
124
|
+
|
125
|
+
@session.buddies.add_temporary(m[1], m[2]).block!
|
126
|
+
}
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
if @offline_messages
|
131
|
+
Dir["#@offline_messages/*_offline.txt"].each {|path|
|
132
|
+
unless buddy = @session.buddies[path[/([^\/_]+)_offline\.txt$/, 1]]
|
133
|
+
File.delete(path)
|
134
|
+
|
135
|
+
next
|
136
|
+
end
|
137
|
+
|
138
|
+
File.open(path) {|f|
|
139
|
+
current = ''
|
140
|
+
|
141
|
+
until f.eof?
|
142
|
+
line = f.readline
|
143
|
+
|
144
|
+
if line.start_with? '[delayed] '
|
145
|
+
buddy.messages.push current
|
146
|
+
|
147
|
+
current = line[10 .. -1]
|
148
|
+
else
|
149
|
+
current << "\n" << line
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
buddy.messages.push current unless current.empty?
|
154
|
+
}
|
155
|
+
|
156
|
+
File.delete(path)
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
@session.start
|
161
|
+
end
|
162
|
+
|
163
|
+
def stop
|
164
|
+
@session.stop
|
165
|
+
@tor.stop
|
166
|
+
|
167
|
+
if @buddy_list
|
168
|
+
File.open(@buddy_list, 'w') {|f|
|
169
|
+
@session.buddies.each {|id, buddy|
|
170
|
+
next if buddy.temporary?
|
171
|
+
|
172
|
+
f.puts "#{id} #{buddy.alias || buddy.name}"
|
173
|
+
}
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
if @blocked_buddy_list
|
178
|
+
File.open(@blocked_buddy_list, 'w') {|f|
|
179
|
+
@session.buddies.each {|id, buddy|
|
180
|
+
next unless buddy.blocked?
|
181
|
+
|
182
|
+
f.puts "#{id} #{buddy.alias || buddy.name}"
|
183
|
+
}
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
if @offline_messages
|
188
|
+
@session.buddies.each {|id, buddy|
|
189
|
+
next if buddy.messages.empty?
|
190
|
+
|
191
|
+
File.open("#@offline_messages/#{id}_offline.txt") {|f|
|
192
|
+
buddy.messages.each {|message|
|
193
|
+
f.write "[delayed] #{message}\n"
|
194
|
+
}
|
195
|
+
}
|
196
|
+
}
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def path= (directory)
|
201
|
+
@path = directory
|
202
|
+
|
203
|
+
unless @buddy_list
|
204
|
+
buddy_list_at "#{directory}/buddy-list.txt"
|
205
|
+
end
|
206
|
+
|
207
|
+
unless @blocked_buddy_list
|
208
|
+
blocked_buddy_list_at "#{directory}/blocked-buddy-list.txt"
|
209
|
+
end
|
210
|
+
|
211
|
+
unless @offline_messages
|
212
|
+
offline_messages_at directory
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def buddy_list_at (path)
|
217
|
+
@buddy_list = File.expand_path(path)
|
218
|
+
end
|
219
|
+
|
220
|
+
def blocked_buddy_list_at (path)
|
221
|
+
@blocked_buddy_list = File.expand_path(path)
|
222
|
+
end
|
223
|
+
|
224
|
+
def offline_messages_at (path)
|
225
|
+
@offline_messages = File.expand_path(path)
|
226
|
+
end
|
227
|
+
|
228
|
+
def send_packet_to (name, packet)
|
229
|
+
@session.buddies[name].send_packet(packet)
|
230
|
+
end
|
231
|
+
|
232
|
+
def send_message_to (name, message)
|
233
|
+
@session.buddies[name].send_message(message)
|
234
|
+
end
|
235
|
+
|
236
|
+
def send_broadcast (message)
|
237
|
+
@session.broadcasts.send_message(message)
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,256 @@
|
|
1
|
+
#--
|
2
|
+
# Copyleft meh. [http://meh.paranoid.pk | meh@paranoici.org]
|
3
|
+
#
|
4
|
+
# This file is part of torchat for ruby.
|
5
|
+
#
|
6
|
+
# torchat for ruby is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU Affero General Public License as published
|
8
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# torchat for ruby is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Affero General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Affero General Public License
|
17
|
+
# along with torchat for ruby. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#++
|
19
|
+
|
20
|
+
class Torchat; module Protocol
|
21
|
+
|
22
|
+
def self.encode (data)
|
23
|
+
data = data.dup
|
24
|
+
|
25
|
+
data.force_encoding 'BINARY'
|
26
|
+
data.gsub!("\\", "\\/")
|
27
|
+
data.gsub!("\n", "\\n")
|
28
|
+
|
29
|
+
data
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.decode (data)
|
33
|
+
data = data.dup
|
34
|
+
|
35
|
+
data.force_encoding 'BINARY'
|
36
|
+
data.gsub!("\\n", "\n")
|
37
|
+
data.gsub!("\\/", "\\")
|
38
|
+
|
39
|
+
data
|
40
|
+
end
|
41
|
+
|
42
|
+
@packets = Hash.new { |h, k| h[k] = {} }
|
43
|
+
@extensions = []
|
44
|
+
|
45
|
+
def self.[] (extension = nil, name)
|
46
|
+
extension = extension.to_sym.downcase if extension
|
47
|
+
name = name.to_sym.downcase
|
48
|
+
|
49
|
+
@packets[extension][name]
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.[]= (extension = nil, name, value)
|
53
|
+
extension = extension.to_sym.downcase if extension
|
54
|
+
name = name.to_sym.downcase
|
55
|
+
|
56
|
+
if value.nil?
|
57
|
+
@packets[extension].delete(name)
|
58
|
+
else
|
59
|
+
@packets[extension][name] = value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.has_packet? (extension = nil, name)
|
64
|
+
extension = extension.to_sym.downcase if extension
|
65
|
+
name = name.to_sym.downcase
|
66
|
+
|
67
|
+
@packets[extension].has_key?(name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.packets (extension = nil)
|
71
|
+
if extension
|
72
|
+
@packets[extension].values
|
73
|
+
else
|
74
|
+
@packets.map { |extension, packets|
|
75
|
+
packets.map { |name, packet|
|
76
|
+
packet
|
77
|
+
}
|
78
|
+
}.flatten
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.extensions
|
83
|
+
@extensions.map {|name|
|
84
|
+
Struct.new(:name, :packets).new(name, packets(name))
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.define_packet (name, &block)
|
89
|
+
raise ArgumentError, "#{name} already exists" if has_packet?(@extension, name)
|
90
|
+
|
91
|
+
self[@extension, name] = Packet.define(name, @extension, &block)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.define_packet! (name, &block)
|
95
|
+
self[@extension, name] = nil
|
96
|
+
|
97
|
+
define_packet(name, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.define_extension (name)
|
101
|
+
@extensions.push(name).uniq!
|
102
|
+
|
103
|
+
tmp, @extension = @extension, name
|
104
|
+
result = yield
|
105
|
+
@extension = tmp
|
106
|
+
|
107
|
+
result
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.packet (*args)
|
111
|
+
if args.first.is_a? Packet
|
112
|
+
args.first
|
113
|
+
else
|
114
|
+
unless packet = self[*args.first]
|
115
|
+
raise ArgumentError, "#{args.first.inspect} packet unknown"
|
116
|
+
end
|
117
|
+
|
118
|
+
args.shift
|
119
|
+
|
120
|
+
packet.new(*args)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.unpack (data, from = nil)
|
125
|
+
name, data = data.chomp.split(' ', 2)
|
126
|
+
|
127
|
+
unless packet = self[name] || self[*name.split('_', 2)]
|
128
|
+
raise ArgumentError, "#{name} packet unknown"
|
129
|
+
end
|
130
|
+
|
131
|
+
unless packet.respond_to? :unpack
|
132
|
+
raise ArgumentError, "#{name} packet has no unpacker"
|
133
|
+
end
|
134
|
+
|
135
|
+
packet = packet.unpack(data ? decode(data) : nil)
|
136
|
+
packet.from = from
|
137
|
+
|
138
|
+
packet
|
139
|
+
end
|
140
|
+
|
141
|
+
class Packet
|
142
|
+
def self.define (name, extension = nil, &block)
|
143
|
+
Class.new(self, &block).tap {|c|
|
144
|
+
c.instance_eval {
|
145
|
+
define_singleton_method :type do name end
|
146
|
+
define_method :type do name end
|
147
|
+
|
148
|
+
define_singleton_method :extension do extension end
|
149
|
+
define_method :extension do extension end
|
150
|
+
|
151
|
+
define_singleton_method :inspect do
|
152
|
+
"#<Torchat::Packet: #{"#{extension}_" if extension}#{type}>"
|
153
|
+
end
|
154
|
+
}
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.define_unpacker (&block)
|
159
|
+
define_singleton_method :unpack do |data|
|
160
|
+
new(*block.call(data))
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.define_unpacker_for (range, &block)
|
165
|
+
unless range.is_a? Range
|
166
|
+
range = range .. range
|
167
|
+
end
|
168
|
+
|
169
|
+
if block
|
170
|
+
define_unpacker &block
|
171
|
+
else
|
172
|
+
define_unpacker do |data|
|
173
|
+
if data.nil? || data.empty?
|
174
|
+
if range.begin == 0 || range.end == 0
|
175
|
+
next
|
176
|
+
else
|
177
|
+
raise ArgumentError, "wrong number of arguments (0 for #{range.begin})"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
args = range.end == -1 ? data.split(' ') : data.split(' ', range.end)
|
182
|
+
arity = range.end == -1 ? range.begin .. args.length : range
|
183
|
+
|
184
|
+
if args.last && args.last.empty?
|
185
|
+
args[-1] = nil
|
186
|
+
end
|
187
|
+
|
188
|
+
unless arity === args.length
|
189
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for #{args.length < range.begin ? range.begin : range.end})"
|
190
|
+
end
|
191
|
+
|
192
|
+
args
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
if range == (0 .. 0)
|
197
|
+
define_method :pack do
|
198
|
+
super('')
|
199
|
+
end
|
200
|
+
|
201
|
+
define_method :inspect do
|
202
|
+
"#<Torchat::Packet[#{"#{extension}_" if extension}#{type}]>"
|
203
|
+
end
|
204
|
+
elsif range.end == 1
|
205
|
+
if range.begin == 0
|
206
|
+
define_method :initialize do |value = nil|
|
207
|
+
@internal = value
|
208
|
+
end
|
209
|
+
else
|
210
|
+
define_method :initialize do |value|
|
211
|
+
@internal = value
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
define_method :pack do
|
216
|
+
super(@internal.to_s)
|
217
|
+
end
|
218
|
+
|
219
|
+
define_method :nil? do
|
220
|
+
@internal.nil?
|
221
|
+
end
|
222
|
+
|
223
|
+
define_method :inspect do
|
224
|
+
"#<Torchat::Packet[#{"#{extension}_" if extension}#{type}]#{": #{@internal.inspect}" if @internal}>"
|
225
|
+
end
|
226
|
+
else
|
227
|
+
define_method :initialize do |*args|
|
228
|
+
@internal = args
|
229
|
+
end
|
230
|
+
|
231
|
+
define_method :inspect do
|
232
|
+
"#<Torchat::Packet[#{"#{extension}_" if extension}#{type}]: #{@internal.map(&:inspect).join(', ')}>"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def self.new (*args, &block)
|
238
|
+
super(*args, &block).tap {|packet|
|
239
|
+
packet.at = Time.new
|
240
|
+
}
|
241
|
+
end
|
242
|
+
|
243
|
+
attr_accessor :from, :at
|
244
|
+
|
245
|
+
def pack (data)
|
246
|
+
"#{"#{extension}_" if extension}#{type}#{" #{Protocol.encode(data)}" if data}\n"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
require 'torchat/protocol/standard'
|
251
|
+
require 'torchat/protocol/groupchat'
|
252
|
+
require 'torchat/protocol/typing'
|
253
|
+
require 'torchat/protocol/broadcast'
|
254
|
+
require 'torchat/protocol/latency'
|
255
|
+
|
256
|
+
end; end
|