torchat 0.0.1.rc.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.
- 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
|