sup 0.11 → 0.12
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sup might be problematic. Click here for more details.
- data/CONTRIBUTORS +16 -5
- data/History.txt +9 -0
- data/ReleaseNotes +9 -0
- data/bin/sup +9 -24
- data/bin/sup-add +3 -3
- data/bin/sup-config +1 -1
- data/bin/sup-dump +22 -9
- data/bin/sup-recover-sources +1 -11
- data/bin/sup-sync +65 -130
- data/bin/sup-sync-back +6 -5
- data/bin/sup-tweak-labels +5 -6
- data/lib/sup.rb +89 -71
- data/lib/sup/account.rb +3 -2
- data/lib/sup/buffer.rb +28 -16
- data/lib/sup/client.rb +92 -0
- data/lib/sup/crypto.rb +91 -49
- data/lib/sup/draft.rb +14 -17
- data/lib/sup/hook.rb +10 -5
- data/lib/sup/index.rb +72 -28
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +55 -112
- data/lib/sup/mbox.rb +151 -6
- data/lib/sup/message-chunks.rb +20 -4
- data/lib/sup/message.rb +183 -76
- data/lib/sup/modes/compose-mode.rb +2 -1
- data/lib/sup/modes/console-mode.rb +4 -1
- data/lib/sup/modes/edit-message-mode.rb +50 -5
- data/lib/sup/modes/line-cursor-mode.rb +1 -0
- data/lib/sup/modes/reply-mode.rb +17 -11
- data/lib/sup/modes/thread-index-mode.rb +10 -9
- data/lib/sup/modes/thread-view-mode.rb +48 -2
- data/lib/sup/poll.rb +56 -60
- data/lib/sup/protocol.rb +161 -0
- data/lib/sup/sent.rb +8 -11
- data/lib/sup/server.rb +116 -0
- data/lib/sup/source.rb +15 -33
- data/lib/sup/thread.rb +6 -0
- data/lib/sup/util.rb +44 -39
- metadata +126 -88
- data/lib/sup/connection.rb +0 -63
- data/lib/sup/imap.rb +0 -349
- data/lib/sup/mbox/loader.rb +0 -180
- data/lib/sup/mbox/ssh-file.rb +0 -254
- data/lib/sup/mbox/ssh-loader.rb +0 -74
data/lib/sup/modes/reply-mode.rb
CHANGED
@@ -42,6 +42,7 @@ EOS
|
|
42
42
|
|
43
43
|
def initialize message, type_arg=nil
|
44
44
|
@m = message
|
45
|
+
@edited = false
|
45
46
|
|
46
47
|
## it's important to put this early because it forces a read of
|
47
48
|
## the full headers (most importantly the list-post header, if
|
@@ -64,18 +65,15 @@ EOS
|
|
64
65
|
## if we have a value from a hook, use it.
|
65
66
|
from = if hook_reply_from
|
66
67
|
hook_reply_from
|
67
|
-
## otherwise,
|
68
|
-
##
|
69
|
-
##
|
68
|
+
## otherwise, try and find an account somewhere in the list of to's
|
69
|
+
## and cc's and look up the corresponding name form the list of accounts.
|
70
|
+
## if this does not succeed use the recipient_email (=envelope-to) instead.
|
70
71
|
## this is for the case where mail is received from a mailing lists (so the
|
71
72
|
## To: is the list id itself). if the user subscribes via a particular
|
72
73
|
## alias, we want to use that alias in the reply.
|
73
|
-
elsif @m.
|
74
|
-
|
75
|
-
|
76
|
-
## and cc's.
|
77
|
-
elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
|
78
|
-
b
|
74
|
+
elsif(b = (@m.to.collect {|t| t.email} + @m.cc.collect {|c| c.email} + [@m.recipient_email] ).find { |p| AccountManager.is_account_email? p })
|
75
|
+
a = AccountManager.account_for(b)
|
76
|
+
Person.new a.name, b
|
79
77
|
## if all else fails, use the default
|
80
78
|
else
|
81
79
|
AccountManager.default_account
|
@@ -150,11 +148,13 @@ EOS
|
|
150
148
|
:recipient
|
151
149
|
end)
|
152
150
|
|
151
|
+
@bodies = {}
|
153
152
|
@headers.each do |k, v|
|
154
|
-
|
153
|
+
@bodies[k] = body
|
154
|
+
HookManager.run "before-edit", :header => v, :body => @bodies[k]
|
155
155
|
end
|
156
156
|
|
157
|
-
super :header => @headers[@type_selector.val], :body =>
|
157
|
+
super :header => @headers[@type_selector.val], :body => @bodies[@type_selector.val], :twiddles => false
|
158
158
|
add_selector @type_selector
|
159
159
|
end
|
160
160
|
|
@@ -164,6 +164,7 @@ protected
|
|
164
164
|
super
|
165
165
|
if @headers[@type_selector.val] != self.header
|
166
166
|
self.header = @headers[@type_selector.val]
|
167
|
+
self.body = @bodies[@type_selector.val] unless @edited
|
167
168
|
update
|
168
169
|
end
|
169
170
|
end
|
@@ -172,6 +173,7 @@ protected
|
|
172
173
|
super
|
173
174
|
if @headers[@type_selector.val] != self.header
|
174
175
|
self.header = @headers[@type_selector.val]
|
176
|
+
self.body = @bodies[@type_selector.val] unless @edited
|
175
177
|
update
|
176
178
|
end
|
177
179
|
end
|
@@ -188,6 +190,10 @@ protected
|
|
188
190
|
end
|
189
191
|
|
190
192
|
def handle_new_text new_header, new_body
|
193
|
+
if new_body != @bodies[@type_selector.val]
|
194
|
+
@bodies[@type_selector.val] = new_body
|
195
|
+
@edited = true
|
196
|
+
end
|
191
197
|
old_header = @headers[@type_selector.val]
|
192
198
|
if new_header.size != old_header.size || old_header.any? { |k, v| new_header[k] != v }
|
193
199
|
@type_selector.set_to :user
|
@@ -231,7 +231,7 @@ EOS
|
|
231
231
|
old_cursor_thread = cursor_thread
|
232
232
|
@mutex.synchronize do
|
233
233
|
## let's see you do THIS in python
|
234
|
-
@threads = @ts.threads.select { |t| !@hidden_threads
|
234
|
+
@threads = @ts.threads.select { |t| !@hidden_threads.member?(t) }.select(&:has_message?).sort_by(&:sort_key)
|
235
235
|
@size_widgets = @threads.map { |t| size_widget_for_thread t }
|
236
236
|
@size_widget_width = @size_widgets.max_of { |w| w.display_length }
|
237
237
|
@date_widgets = @threads.map { |t| date_widget_for_thread t }
|
@@ -792,8 +792,8 @@ protected
|
|
792
792
|
authors = t.map do |m, *o|
|
793
793
|
next unless m && m.from
|
794
794
|
new[m.from] ||= m.has_label?(:unread)
|
795
|
-
next if seen[m.from]
|
796
|
-
seen[m.from] = true
|
795
|
+
next if seen[m.from.mediumname]
|
796
|
+
seen[m.from.mediumname] = true
|
797
797
|
m.from
|
798
798
|
end.compact
|
799
799
|
|
@@ -876,15 +876,16 @@ protected
|
|
876
876
|
:index_old_color
|
877
877
|
end
|
878
878
|
|
879
|
-
|
879
|
+
size_padding = @size_widget_width - size_widget.display_length
|
880
|
+
size_widget_text = sprintf "%#{size_padding}s%s", "", size_widget
|
880
881
|
|
881
|
-
|
882
|
-
date_widget_text = sprintf "%#{
|
882
|
+
date_padding = @date_widget_width - date_widget.display_length
|
883
|
+
date_widget_text = sprintf "%#{date_padding}s%s", "", date_widget
|
883
884
|
|
884
885
|
[
|
885
886
|
[:tagged_color, @tags.tagged?(t) ? ">" : " "],
|
886
887
|
[:date_color, date_widget_text],
|
887
|
-
(starred ?
|
888
|
+
[:starred_color, (starred ? "*" : " ")],
|
888
889
|
] +
|
889
890
|
from +
|
890
891
|
[
|
@@ -895,7 +896,7 @@ protected
|
|
895
896
|
(t.labels - @hidden_labels).map { |label| [:label_color, "#{label} "] } +
|
896
897
|
[
|
897
898
|
[subj_color, t.subj + (t.subj.empty? ? "" : " ")],
|
898
|
-
[:snippet_color, snippet],
|
899
|
+
[:snippet_color, t.snippet],
|
899
900
|
]
|
900
901
|
end
|
901
902
|
|
@@ -913,7 +914,7 @@ private
|
|
913
914
|
end
|
914
915
|
|
915
916
|
def default_date_widget_for t
|
916
|
-
t.date.to_nice_s
|
917
|
+
t.date.getlocal.to_nice_s
|
917
918
|
end
|
918
919
|
|
919
920
|
def from_width
|
@@ -3,7 +3,7 @@ module Redwood
|
|
3
3
|
class ThreadViewMode < LineCursorMode
|
4
4
|
## this holds all info we need to lay out a message
|
5
5
|
class MessageLayout
|
6
|
-
attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new
|
6
|
+
attr_accessor :top, :bot, :prev, :next, :depth, :width, :state, :color, :star_color, :orig_new, :toggled_state
|
7
7
|
end
|
8
8
|
|
9
9
|
class ChunkLayout
|
@@ -54,7 +54,9 @@ EOS
|
|
54
54
|
k.add :edit_labels, "Edit or add labels for a thread", 'l'
|
55
55
|
k.add :expand_all_quotes, "Expand/collapse all quotes in a message", 'o'
|
56
56
|
k.add :jump_to_next_open, "Jump to next open message", 'n'
|
57
|
+
k.add :jump_to_next_and_open, "Jump to next message and open", "\C-n"
|
57
58
|
k.add :jump_to_prev_open, "Jump to previous open message", 'p'
|
59
|
+
k.add :jump_to_prev_and_open, "Jump to previous message and open", "\C-p"
|
58
60
|
k.add :align_current_message, "Align current message in buffer", 'z'
|
59
61
|
k.add :toggle_starred, "Star or unstar message", '*'
|
60
62
|
k.add :toggle_new, "Toggle unread/read status of message", 'N'
|
@@ -111,7 +113,7 @@ EOS
|
|
111
113
|
## objects. @person_lines is a map from row #s to Person objects.
|
112
114
|
|
113
115
|
def initialize thread, hidden_labels=[], index_mode=nil
|
114
|
-
super
|
116
|
+
super :slip_rows => $config[:slip_rows]
|
115
117
|
@thread = thread
|
116
118
|
@hidden_labels = hidden_labels
|
117
119
|
|
@@ -129,6 +131,7 @@ EOS
|
|
129
131
|
next unless m
|
130
132
|
earliest ||= m
|
131
133
|
@layout[m].state = initial_state_for m
|
134
|
+
@layout[m].toggled_state = false
|
132
135
|
@layout[m].color = altcolor ? :alternate_patina_color : :message_patina_color
|
133
136
|
@layout[m].star_color = altcolor ? :alternate_starred_patina_color : :starred_patina_color
|
134
137
|
@layout[m].orig_new = m.has_label? :read
|
@@ -440,6 +443,29 @@ EOS
|
|
440
443
|
end
|
441
444
|
end
|
442
445
|
|
446
|
+
def jump_to_next_and_open
|
447
|
+
return continue_search_in_buffer if in_search? # err.. don't know why im doing this
|
448
|
+
|
449
|
+
m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
|
450
|
+
return unless m
|
451
|
+
|
452
|
+
if @layout[m].toggled_state == true
|
453
|
+
@layout[m].state = :closed
|
454
|
+
@layout[m].toggled_state = false
|
455
|
+
update
|
456
|
+
end
|
457
|
+
|
458
|
+
nextm = @layout[m].next
|
459
|
+
if @layout[nextm].state == :closed
|
460
|
+
@layout[nextm].state = :open
|
461
|
+
@layout[nextm].toggled_state = true
|
462
|
+
end
|
463
|
+
|
464
|
+
jump_to_message nextm if nextm
|
465
|
+
|
466
|
+
update if @layout[nextm].toggled_state
|
467
|
+
end
|
468
|
+
|
443
469
|
def jump_to_next_open force_alignment=nil
|
444
470
|
return continue_search_in_buffer if in_search? # hack: allow 'n' to apply to both operations
|
445
471
|
m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
|
@@ -456,6 +482,26 @@ EOS
|
|
456
482
|
jump_to_message m, true
|
457
483
|
end
|
458
484
|
|
485
|
+
def jump_to_prev_and_open force_alignment=nil
|
486
|
+
m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] }
|
487
|
+
return unless m
|
488
|
+
|
489
|
+
if @layout[m].toggled_state == true
|
490
|
+
@layout[m].state = :closed
|
491
|
+
@layout[m].toggled_state = false
|
492
|
+
update
|
493
|
+
end
|
494
|
+
|
495
|
+
nextm = @layout[m].prev
|
496
|
+
if @layout[nextm].state == :closed
|
497
|
+
@layout[nextm].state = :open
|
498
|
+
@layout[nextm].toggled_state = true
|
499
|
+
end
|
500
|
+
|
501
|
+
jump_to_message nextm if nextm
|
502
|
+
update if @layout[nextm].toggled_state
|
503
|
+
end
|
504
|
+
|
459
505
|
def jump_to_prev_open
|
460
506
|
m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] } # bah, .to_a
|
461
507
|
return unless m
|
data/lib/sup/poll.rb
CHANGED
@@ -28,9 +28,8 @@ num_inbox_total_unread: the total number of unread messages in the inbox
|
|
28
28
|
only those messages appearing in the inbox
|
29
29
|
EOS
|
30
30
|
|
31
|
-
DELAY = $config[:poll_interval] || 300
|
32
|
-
|
33
31
|
def initialize
|
32
|
+
@delay = $config[:poll_interval] || 300
|
34
33
|
@mutex = Mutex.new
|
35
34
|
@thread = nil
|
36
35
|
@last_poll = nil
|
@@ -83,8 +82,8 @@ EOS
|
|
83
82
|
def start
|
84
83
|
@thread = Redwood::reporting_thread("periodic poll") do
|
85
84
|
while true
|
86
|
-
sleep
|
87
|
-
poll if @last_poll.nil? || (Time.now - @last_poll) >=
|
85
|
+
sleep @delay / 2
|
86
|
+
poll if @last_poll.nil? || (Time.now - @last_poll) >= @delay
|
88
87
|
end
|
89
88
|
end
|
90
89
|
end
|
@@ -102,43 +101,37 @@ EOS
|
|
102
101
|
|
103
102
|
@mutex.synchronize do
|
104
103
|
@poll_sources.each do |source|
|
105
|
-
# yield "source #{source} is done? #{source.done?} (cur_offset #{source.cur_offset} >= #{source.end_offset})"
|
106
104
|
begin
|
107
|
-
yield "Loading from #{source}... "
|
105
|
+
yield "Loading from #{source}... "
|
108
106
|
rescue SourceError => e
|
109
107
|
warn "problem getting messages from #{source}: #{e.message}"
|
110
|
-
Redwood::report_broken_sources :force_to_top => true
|
111
108
|
next
|
112
109
|
end
|
113
110
|
|
114
111
|
num = 0
|
115
112
|
numi = 0
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
Index.update_message m
|
113
|
+
poll_from source do |action,m,old_m,progress|
|
114
|
+
if action == :delete
|
115
|
+
yield "Deleting #{m.id}"
|
116
|
+
elsif action == :add
|
117
|
+
if old_m
|
118
|
+
if not old_m.locations.member? m.location
|
119
|
+
yield "Message at #{m.source_info} is an updated of an old message. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
|
120
|
+
else
|
121
|
+
yield "Skipping already-imported message at #{m.source_info}"
|
122
|
+
end
|
127
123
|
else
|
128
|
-
yield "
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
|
137
|
-
from_and_subj_inbox << [m.from && m.from.longname, m.subj]
|
138
|
-
numi += 1
|
124
|
+
yield "Found new message at #{m.source_info} with labels #{m.labels.to_a * ','}"
|
125
|
+
loaded_labels.merge m.labels
|
126
|
+
num += 1
|
127
|
+
from_and_subj << [m.from && m.from.longname, m.subj]
|
128
|
+
if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
|
129
|
+
from_and_subj_inbox << [m.from && m.from.longname, m.subj]
|
130
|
+
numi += 1
|
131
|
+
end
|
139
132
|
end
|
133
|
+
else fail
|
140
134
|
end
|
141
|
-
m
|
142
135
|
end
|
143
136
|
yield "Found #{num} messages, #{numi} to inbox." unless num == 0
|
144
137
|
total_num += num
|
@@ -153,44 +146,47 @@ EOS
|
|
153
146
|
[total_num, total_numi, from_and_subj, from_and_subj_inbox, loaded_labels]
|
154
147
|
end
|
155
148
|
|
156
|
-
## like Source#
|
157
|
-
## labels and
|
158
|
-
##
|
159
|
-
|
160
|
-
def each_message_from source, opts={}
|
149
|
+
## like Source#poll, but yields successive Message objects, which have their
|
150
|
+
## labels and locations set correctly. The Messages are saved to or removed
|
151
|
+
## from the index after being yielded.
|
152
|
+
def poll_from source, opts={}
|
161
153
|
begin
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
154
|
+
source.poll do |sym, args|
|
155
|
+
case sym
|
156
|
+
when :add
|
157
|
+
m = Message.build_from_source source, args[:info]
|
158
|
+
old_m = Index.build_message m.id
|
159
|
+
m.labels += args[:labels]
|
160
|
+
m.labels.delete :inbox if source.archived?
|
161
|
+
m.labels.delete :unread if source.read?
|
162
|
+
m.labels.delete :unread if m.source_marked_read? # preserve read status if possible
|
163
|
+
m.labels.each { |l| LabelManager << l }
|
164
|
+
m.labels = old_m.labels + (m.labels - [:unread, :inbox]) if old_m
|
165
|
+
m.locations = old_m.locations + m.locations if old_m
|
166
|
+
HookManager.run "before-add-message", :message => m
|
167
|
+
yield :add, m, old_m, args[:progress] if block_given?
|
168
|
+
Index.sync_message m, true
|
169
|
+
|
170
|
+
## We need to add or unhide the message when it either did not exist
|
171
|
+
## before at all or when it was updated. We do *not* add/unhide when
|
172
|
+
## the same message was found at a different location
|
173
|
+
if !old_m or not old_m.locations.member? m.location
|
174
|
+
UpdateManager.relay self, :added, m
|
175
|
+
end
|
176
|
+
when :delete
|
177
|
+
Index.each_message :location => [source.id, args[:info]] do |m|
|
178
|
+
m.locations.delete Location.new(source, args[:info])
|
179
|
+
yield :delete, m, [source,args[:info]], args[:progress] if block_given?
|
180
|
+
Index.sync_message m, false
|
181
|
+
#UpdateManager.relay self, :deleted, m
|
182
|
+
end
|
168
183
|
end
|
169
|
-
|
170
|
-
m = Message.build_from_source source, offset
|
171
|
-
m.labels += source_labels + (source.archived? ? [] : [:inbox])
|
172
|
-
m.labels.delete :unread if m.source_marked_read? # preserve read status if possible
|
173
|
-
m.labels.each { |l| LabelManager << l }
|
174
|
-
|
175
|
-
HookManager.run "before-add-message", :message => m
|
176
|
-
yield m
|
177
184
|
end
|
178
185
|
rescue SourceError => e
|
179
186
|
warn "problem getting messages from #{source}: #{e.message}"
|
180
|
-
Redwood::report_broken_sources :force_to_top => true
|
181
187
|
end
|
182
188
|
end
|
183
189
|
|
184
|
-
## TODO: see if we can do this within PollMode rather than by calling this
|
185
|
-
## method.
|
186
|
-
##
|
187
|
-
## a wrapper around Index.add_message that calls the proper hooks,
|
188
|
-
## does the gui callback stuff, etc.
|
189
|
-
def add_new_message m
|
190
|
-
Index.add_message m
|
191
|
-
UpdateManager.relay self, :added, m
|
192
|
-
end
|
193
|
-
|
194
190
|
def handle_idle_update sender, idle_since; @should_clear_running_totals = false; end
|
195
191
|
def handle_unidle_update sender, idle_since; @should_clear_running_totals = true; clear_running_totals; end
|
196
192
|
def clear_running_totals; @running_totals = {:num => 0, :numi => 0, :loaded_labels => Set.new}; end
|
data/lib/sup/protocol.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'socket'
|
3
|
+
require 'stringio'
|
4
|
+
require 'yajl'
|
5
|
+
|
6
|
+
class EM::P::Redwood < EM::Connection
|
7
|
+
VERSION = 1
|
8
|
+
ENCODINGS = %w(marshal json)
|
9
|
+
|
10
|
+
attr_reader :debug
|
11
|
+
|
12
|
+
def initialize *args
|
13
|
+
@state = :negotiating
|
14
|
+
@version_buf = ""
|
15
|
+
@debug = false
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def receive_data data
|
20
|
+
if @state == :negotiating
|
21
|
+
@version_buf << data
|
22
|
+
if i = @version_buf.index("\n")
|
23
|
+
l = @version_buf.slice!(0..i)
|
24
|
+
receive_version *parse_version(l.strip)
|
25
|
+
x = @version_buf
|
26
|
+
@version_buf = nil
|
27
|
+
@state = :established
|
28
|
+
connection_established
|
29
|
+
receive_data x
|
30
|
+
end
|
31
|
+
else
|
32
|
+
@filter.decode(data).each do |msg|
|
33
|
+
puts "#{self.class.name} received: #{msg.inspect}" if @debug
|
34
|
+
validate_message *msg
|
35
|
+
receive_message *msg
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def connection_established
|
41
|
+
end
|
42
|
+
|
43
|
+
def send_version encodings, extensions
|
44
|
+
fail if encodings.empty?
|
45
|
+
send_data "Redwood #{VERSION} #{encodings * ','} #{extensions.empty? ? :none : (extensions * ',')}\n"
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_message type, tag, params={}
|
49
|
+
fail "attempted to send message during negotiation" unless @state == :established
|
50
|
+
puts "#{self.class.name} sent: #{[type, tag, params].inspect}" if @debug
|
51
|
+
validate_message type, tag, params
|
52
|
+
send_data @filter.encode([type,tag,params])
|
53
|
+
end
|
54
|
+
|
55
|
+
def receive_version l
|
56
|
+
fail "unimplemented"
|
57
|
+
end
|
58
|
+
|
59
|
+
def receive_message type, tag, params
|
60
|
+
fail "unimplemented"
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def validate_message type, tag, params
|
66
|
+
fail unless type.is_a? String or type.is_a? Symbol
|
67
|
+
fail unless tag.is_a? String or tag.is_a? Integer
|
68
|
+
fail unless params.is_a? Hash
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_version l
|
72
|
+
l =~ /^Redwood\s+(\d+)\s+([\w,]+)\s+([\w,]+)$/ or fail "unexpected banner #{l.inspect}"
|
73
|
+
version, encodings, extensions = $1.to_i, $2, $3
|
74
|
+
encodings = encodings.split ','
|
75
|
+
extensions = extensions.split ','
|
76
|
+
extensions = [] if extensions == ['none']
|
77
|
+
fail unless version == VERSION
|
78
|
+
fail if encodings.empty?
|
79
|
+
[encodings, extensions]
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_filter encoding
|
83
|
+
case encoding
|
84
|
+
when 'json' then JSONFilter.new
|
85
|
+
when 'marshal' then MarshalFilter.new
|
86
|
+
else fail "unknown encoding #{encoding.inspect}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class JSONFilter
|
91
|
+
def initialize
|
92
|
+
@parser = Yajl::Parser.new :check_utf8 => false
|
93
|
+
end
|
94
|
+
|
95
|
+
def decode chunk
|
96
|
+
parsed = []
|
97
|
+
@parser.on_parse_complete = lambda { |o| parsed << o }
|
98
|
+
@parser << chunk
|
99
|
+
parsed
|
100
|
+
end
|
101
|
+
|
102
|
+
def encode *os
|
103
|
+
os.inject('') { |s, o| s << Yajl::Encoder.encode(o) }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class MarshalFilter
|
108
|
+
def initialize
|
109
|
+
@buf = ''
|
110
|
+
@state = :prefix
|
111
|
+
@size = 0
|
112
|
+
end
|
113
|
+
|
114
|
+
def decode chunk
|
115
|
+
received = []
|
116
|
+
@buf << chunk
|
117
|
+
|
118
|
+
begin
|
119
|
+
if @state == :prefix
|
120
|
+
break unless @buf.size >= 4
|
121
|
+
prefix = @buf.slice!(0...4)
|
122
|
+
@size = prefix.unpack('N')[0]
|
123
|
+
@state = :data
|
124
|
+
end
|
125
|
+
|
126
|
+
fail unless @state == :data
|
127
|
+
break if @buf.size < @size
|
128
|
+
received << Marshal.load(@buf.slice!(0...@size))
|
129
|
+
@state = :prefix
|
130
|
+
end until @buf.empty?
|
131
|
+
|
132
|
+
received
|
133
|
+
end
|
134
|
+
|
135
|
+
def encode o
|
136
|
+
data = Marshal.dump o
|
137
|
+
[data.size].pack('N') + data
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class EM::P::RedwoodServer < EM::P::Redwood
|
143
|
+
def post_init
|
144
|
+
send_version ENCODINGS, []
|
145
|
+
end
|
146
|
+
|
147
|
+
def receive_version encodings, extensions
|
148
|
+
fail unless encodings.size == 1
|
149
|
+
fail unless ENCODINGS.member? encodings.first
|
150
|
+
@filter = create_filter encodings.first
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class EM::P::RedwoodClient < EM::P::Redwood
|
155
|
+
def receive_version encodings, extensions
|
156
|
+
encoding = (ENCODINGS & encodings).first
|
157
|
+
fail unless encoding
|
158
|
+
@filter = create_filter encoding
|
159
|
+
send_version [encoding], []
|
160
|
+
end
|
161
|
+
end
|