]> git.cworth.org Git - sup/blob - lib/sup/thread.rb
moved evertying to devel
[sup] / lib / sup / thread.rb
1 require 'date'
2
3 module Redwood
4
5 class Thread
6   include Enumerable
7
8   attr_reader :containers
9   def initialize
10     @containers = []
11   end
12
13   def << c
14     @containers << c
15   end
16
17   def empty?; @containers.empty?; end
18
19   def drop c
20     raise "bad drop" unless @containers.member? c
21     @containers.delete c
22   end
23
24   def dump
25     puts "=== start thread #{self} with #{@containers.length} trees ==="
26     @containers.each { |c| c.dump_recursive }
27     puts "=== end thread ==="
28   end
29
30   ## yields each message and some stuff
31   def each fake_root=false
32     adj = 0
33     root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date }
34
35     if root
36       adj = 1
37       root.first_useful_descendant.each_with_stuff do |c, d, par|
38         yield c.message, d, (par ? par.message : nil)
39       end
40     elsif @containers.length > 1 && fake_root
41       adj = 1
42       yield :fake_root, 0, nil
43     end
44
45     @containers.each do |cont|
46       next if cont == root
47       fud = cont.first_useful_descendant
48       fud.each_with_stuff do |c, d, par|
49         ## special case here: if we're an empty root that's already
50         ## been joined by a fake root, don't emit
51         yield c.message, d + adj, (par ? par.message : nil) unless
52           fake_root && c.message.nil? && root.nil? && c == fud 
53       end
54     end
55   end
56
57   def dirty?; any? { |m, *o| m && m.dirty? }; end
58   def date; map { |m, *o| m.date if m }.compact.max; end
59   def snippet; argfind { |m, *o| m && m.snippet }; end
60   def authors; map { |m, *o| m.from if m }.compact.uniq; end
61
62   def apply_label t; each { |m, *o| m && m.add_label(t) }; end
63   def remove_label t
64     each { |m, *o| m && m.remove_label(t) }
65   end
66
67   def toggle_label label
68     if has_label? label
69       remove_label label
70       return false
71     else
72       apply_label label
73       return true
74     end
75   end
76
77   def set_labels l; each { |m, *o| m && m.labels = l }; end
78   
79   def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
80   def save index; each { |m, *o| m && m.save(index) }; end
81
82   def direct_participants
83     map { |m, *o| [m.from] + m.to if m }.flatten.compact.uniq
84   end
85
86   def participants
87     map { |m, *o| [m.from] + m.to + m.cc + m.bcc if m }.flatten.compact.uniq
88   end
89
90   def size; map { |m, *o| m ? 1 : 0 }.sum; end
91   def subj; argfind { |m, *o| m && m.subj }; end
92   def labels
93       map { |m, *o| m && m.labels }.flatten.compact.uniq.sort_by { |t| t.to_s }
94   end
95   def labels= l
96     each { |m, *o| m && m.labels = l.clone }
97   end
98
99   def latest_message
100     inject(nil) do |a, b| 
101       b = b.first
102       if a.nil?
103         b
104       elsif b.nil?
105         a
106       else
107         b.date > a.date ? b : a
108       end
109     end
110   end
111
112   def to_s
113     "<thread containing: #{@containers.join ', '}>"
114   end
115 end
116
117 ## recursive structure used internally to represent message trees as
118 ## described by reply-to: and references: headers.
119 ##
120 ## the 'id' field is the same as the message id. but the message might
121 ## be empty, in the case that we represent a message that was referenced
122 ## by another message (as an ancestor) but never received.
123 class Container
124   attr_accessor :message, :parent, :children, :id, :thread
125
126   def initialize id
127     raise "non-String #{id.inspect}" unless id.is_a? String
128     @id = id
129     @message, @parent, @thread = nil, nil, nil
130     @children = []
131   end      
132
133   def each_with_stuff parent=nil
134     yield self, 0, parent
135     @children.each do |c|
136       c.each_with_stuff(self) { |cc, d, par| yield cc, d + 1, par }
137     end
138   end
139
140   def descendant_of? o
141     if o == self
142       true
143     else
144       @parent && @parent.descendant_of?(o)
145     end
146   end
147
148   def == o; Container === o && id == o.id; end
149
150   def empty?; @message.nil?; end
151   def root?; @parent.nil?; end
152   def root; root? ? self : @parent.root; end
153
154   def first_useful_descendant
155     if empty? && @children.size == 1
156       @children.first.first_useful_descendant
157     else
158       self
159     end
160   end
161
162   def find_attr attr
163     if empty?
164       @children.argfind { |c| c.find_attr attr }
165     else
166       @message.send attr
167     end
168   end
169   def subj; find_attr :subj; end
170   def date; find_attr :date; end
171
172   def is_reply?; subj && Message.subject_is_reply?(subj); end
173
174   def to_s
175     [ "<#{id}",
176       (@parent.nil? ? nil : "parent=#{@parent.id}"),
177       (@children.empty? ? nil : "children=#{@children.map { |c| c.id }.inspect}"),
178     ].compact.join(" ") + ">"
179   end
180
181   def dump_recursive indent=0, root=true, parent=nil
182     raise "inconsistency" unless parent.nil? || parent.children.include?(self)
183     unless root
184       print " " * indent
185       print "+->"
186     end
187     line = #"[#{useful? ? 'U' : ' '}] " +
188       if @message
189         "[#{thread}] #{@message.subj} " ##{@message.refs.inspect} / #{@message.replytos.inspect}"
190       else
191         "<no message>"
192       end
193
194     puts "#{id} #{line}"#[0 .. (105 - indent)]
195     indent += 3
196     @children.each { |c| c.dump_recursive indent, false, self }
197   end
198 end
199
200 ## a set of threads (so a forest). builds the thread structures by
201 ## reading messages from an index.
202 class ThreadSet
203   attr_reader :num_messages
204
205   def initialize index
206     @index = index
207     @num_messages = 0
208     @messages = {} ## map from message ids to container objects
209     @subj_thread = {} ## map from subject strings to thread objects
210   end
211
212   def contains_id? id; @messages.member?(id) && !@messages[id].empty?; end
213   def thread_for m
214     (c = @messages[m.id]) && c.root.thread
215   end
216
217   def delete_empties
218     @subj_thread.each { |k, v| @subj_thread.delete(k) if v.empty? }
219   end
220   private :delete_empties
221
222   def threads; delete_empties; @subj_thread.values; end
223   def size; delete_empties; @subj_thread.size; end
224
225   def dump
226     @subj_thread.each do |s, t|
227       puts "**********************"
228       puts "** for subject #{s} **"
229       puts "**********************"
230       t.dump
231     end
232   end
233
234   def link p, c, overwrite=false
235     if p == c || p.descendant_of?(c) || c.descendant_of?(p) # would create a loop
236 #      puts "*** linking parent #{p} and child #{c} would create a loop"
237       return
238     end
239
240     if c.parent.nil? || overwrite
241       c.parent.children.delete c if overwrite && c.parent
242       if c.thread
243         c.thread.drop c 
244         c.thread = nil
245       end
246       p.children << c
247       c.parent = p
248     end
249   end
250   private :link
251
252   def remove mid
253     return unless(c = @messages[mid])
254
255     c.parent.children.delete c if c.parent
256     if c.thread
257       c.thread.drop c
258       c.thread = nil
259     end
260   end
261
262   ## load in (at most) num number of threads from the index
263   def load_n_threads num, opts={}
264     @index.each_id_by_date opts do |mid, builder|
265       break if size >= num
266       next if contains_id? mid
267
268       m = builder.call
269       add_message m
270       load_thread_for_message m
271       yield @subj_thread.size if block_given?
272     end
273   end
274
275   ## loads in all messages needed to thread m
276   def load_thread_for_message m
277     @index.each_message_in_thread_for m, :limit => 100 do |mid, builder|
278       next if contains_id? mid
279       add_message builder.call
280     end
281   end
282
283   def is_relevant? m
284     m.refs.any? { |ref_id| @messages[ref_id] }
285   end
286
287   ## an "online" version of the jwz threading algorithm.
288   def add_message message
289     id = message.id
290     el = (@messages[id] ||= Container.new id)
291     return if @messages[id].message # we've seen it before
292
293     el.message = message
294     oldroot = el.root
295
296     ## link via references:
297     prev = nil
298     message.refs.each do |ref_id|
299       raise "non-String ref id #{ref_id.inspect} (full: #{message.refs.inspect})" unless ref_id.is_a?(String)
300       ref = (@messages[ref_id] ||= Container.new ref_id)
301       link prev, ref if prev
302       prev = ref
303     end
304     link prev, el, true if prev
305
306     ## link via in-reply-to:
307     message.replytos.each do |ref_id|
308       ref = (@messages[ref_id] ||= Container.new ref_id)
309       link ref, el, true
310       break # only do the first one
311     end
312
313     ## update subject grouping
314     root = el.root
315     #    puts "> have #{el}, root #{root}, oldroot #{oldroot}"
316     #    el.dump_recursive
317
318     if root == oldroot
319       if oldroot.thread
320         #        puts "*** root (#{root.subj}) == oldroot (#{oldroot.subj}); ignoring"
321       else
322         ## to disable subject grouping, use the next line instead
323         ## (and the same for below)
324         thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
325         #thread = (@subj_thread[root.id] ||= Thread.new)
326
327         thread << root
328         root.thread = thread
329         #        puts "# (1) added #{root} to #{thread}"
330       end
331     else
332       if oldroot.thread
333         ## new root. need to drop old one and put this one in its place
334         #        puts "*** DROPPING #{oldroot} from #{oldroot.thread}"
335         oldroot.thread.drop oldroot
336         oldroot.thread = nil
337       end
338
339       if root.thread
340         #        puts "*** IGNORING cuz root already has a thread"
341       else
342         ## to disable subject grouping, use the next line instead
343         ## (and the same above)
344         thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
345         #thread = (@subj_thread[root.id] ||= Thread.new)
346
347         thread << root
348         root.thread = thread
349         #        puts "# (2) added #{root} to #{thread}"
350       end
351     end
352
353     ## last bit
354     @num_messages += 1
355   end
356 end
357
358 end