whisperblog 0.6

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.
Files changed (48) hide show
  1. data/Changelog +23 -0
  2. data/LICENSE +661 -0
  3. data/README +59 -0
  4. data/bin/whisper +91 -0
  5. data/bin/whisper-init +40 -0
  6. data/bin/whisper-post +88 -0
  7. data/bin/whisper-process-email +54 -0
  8. data/lib/whisper.rb +52 -0
  9. data/lib/whisper/author_tracker.rb +46 -0
  10. data/lib/whisper/blog.rb +156 -0
  11. data/lib/whisper/cached_file.rb +65 -0
  12. data/lib/whisper/comment.rb +35 -0
  13. data/lib/whisper/comment_set.rb +41 -0
  14. data/lib/whisper/common.rb +276 -0
  15. data/lib/whisper/config.rb +49 -0
  16. data/lib/whisper/dir_scanner.rb +76 -0
  17. data/lib/whisper/dir_set.rb +50 -0
  18. data/lib/whisper/email_receiver.rb +212 -0
  19. data/lib/whisper/email_sender.rb +114 -0
  20. data/lib/whisper/entry.rb +44 -0
  21. data/lib/whisper/entry_set.rb +41 -0
  22. data/lib/whisper/handler.rb +99 -0
  23. data/lib/whisper/mbox.rb +141 -0
  24. data/lib/whisper/page.rb +118 -0
  25. data/lib/whisper/rfc2047.rb +79 -0
  26. data/lib/whisper/router.rb +50 -0
  27. data/lib/whisper/server.rb +88 -0
  28. data/lib/whisper/text.rb +252 -0
  29. data/lib/whisper/timed_map.rb +43 -0
  30. data/lib/whisper/version.rb +3 -0
  31. data/share/whisper/comment-email.rtxt +36 -0
  32. data/share/whisper/config.yaml +53 -0
  33. data/share/whisper/entry-email.rtxt +35 -0
  34. data/share/whisper/entry.rhtml +88 -0
  35. data/share/whisper/entry.rtxt +19 -0
  36. data/share/whisper/formatter.rb +52 -0
  37. data/share/whisper/helper.rb +147 -0
  38. data/share/whisper/list.rhtml +51 -0
  39. data/share/whisper/list.rrss +9 -0
  40. data/share/whisper/list.rtxt +25 -0
  41. data/share/whisper/master.rhtml +81 -0
  42. data/share/whisper/master.rrss +21 -0
  43. data/share/whisper/master.rtxt +5 -0
  44. data/share/whisper/mootools.js +282 -0
  45. data/share/whisper/rss-badge.png +0 -0
  46. data/share/whisper/spinner.gif +0 -0
  47. data/share/whisper/style.css +314 -0
  48. metadata +239 -0
@@ -0,0 +1,65 @@
1
+ require 'mime/types'
2
+ require "whisper/common"
3
+
4
+ module Whisper
5
+
6
+ ## just a simple ol' thing that just holds a lazily-loaded cached copy of a
7
+ ## File in memory, updates itself upon command, and exposes a timestamp for
8
+ ## dependeny calculation purposes.
9
+ ##
10
+ ## TODO: do we actually need min_scan_interval? might be taken care of by
11
+ ## Dependency.min_refresh_interval. Liekwise for DirScanner.
12
+ class CachedFile
13
+ include Loggy
14
+ include Dependency
15
+ attr_reader :path
16
+
17
+ class << self; attr_accessor :min_scan_interval end
18
+
19
+ def initialize path
20
+ @path = path
21
+ @content_type = nil
22
+ @mtime = File.mtime @path
23
+ @last_check = Time.at 0
24
+
25
+ dependency_init
26
+ CachedFile.min_scan_interval ||= 10
27
+ end
28
+
29
+ def dependencies; [] end
30
+ def build old_content
31
+ debug "reading #{@path} from disk"
32
+ @mtime = File.mtime(@path)
33
+ IO.read @path
34
+ end
35
+ def check
36
+ return false unless @last_check + CachedFile.min_scan_interval < Time.now
37
+
38
+ changed = begin
39
+ File.mtime(@path) > @mtime
40
+ rescue SystemCallError => e
41
+ warn e.message
42
+ true
43
+ end
44
+ @last_check = Time.now
45
+ #debug "checking mtime for #{@path}: #{changed}"
46
+ changed
47
+ end
48
+ def body; content end
49
+
50
+ def content_type; @content_type ||= CachedFile.content_type_for @path end
51
+
52
+ def self.content_type_for path
53
+ type = MIME::Types.type_for(path).first
54
+ if type
55
+ type.content_type
56
+ elsif path =~ /\.js$/i
57
+ "application/x-javascript"
58
+ else
59
+ warn "unknown MIME type for #{path}"
60
+ "application/binary"
61
+ end
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,35 @@
1
+ require "whisper/common"
2
+ require "whisper/text"
3
+
4
+ module Whisper
5
+
6
+ class Comment
7
+ include Loggy
8
+ include Dependency
9
+
10
+ RESEND_SETTING = "send future comments to me"
11
+ URL_SETTING = "my url"
12
+
13
+ def initialize meta_file, content_file
14
+ @text = Text.new meta_file, content_file
15
+ dependency_init
16
+ end
17
+
18
+ def dependencies; [@text] end
19
+ def build old, type
20
+ ## nothing yet
21
+ end
22
+
23
+ [:id, :entry_id, :published, :updated, :id, :author, :meta_file, :content_file, :parent_id, :message_id].each { |m| define_method(m) { @text.send m } }
24
+
25
+ ## these next two may not exist in imported and older comments, so we provide defaults here
26
+ def resend_setting; @text.meta[:resend_setting] || "none" end
27
+ def url_setting; @text.meta[:url_setting] end
28
+
29
+ def body format, opts={}; @text.body format, opts end
30
+
31
+ private
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,41 @@
1
+ require "whisper/common"
2
+ require "whisper/dir_set"
3
+ require "whisper/comment"
4
+
5
+ module Whisper
6
+
7
+ ## the set of all entries
8
+ class CommentSet
9
+ include Loggy
10
+ include Dependency
11
+
12
+ def initialize dir
13
+ @dirset = DirSet.new dir, Comment
14
+ dependency_init
15
+ end
16
+
17
+ def dependencies; [@dirset] end
18
+
19
+ %w(comments comments_by_id comments_by_entry_id comment_tree_by_entry_id comments_by_message_id).each do |f|
20
+ f = f.intern
21
+ define_method(f) { content[f] }
22
+ end
23
+
24
+ def size; comments.size end
25
+
26
+ private
27
+
28
+ def build old
29
+ debug "recompiling comment indices"
30
+
31
+ comments = @dirset.things.sort_by { |c| c.published }.reverse
32
+ { :comments => comments,
33
+ :comments_by_id => comments.map_by { |c| c.id },
34
+ :comments_by_entry_id => comments.group_by { |c| c.entry_id },
35
+ :comments_by_message_id => comments.map_by { |c| c.message_id },
36
+ :comment_tree_by_entry_id => comments.group_by { |c| c.entry_id }.maphash { |eid, cs| cs.thread(:id, :parent_id) },
37
+ }
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,276 @@
1
+ require 'thread'
2
+ require 'yaml'
3
+
4
+ class Time
5
+ alias :old_to_yaml :to_yaml
6
+ def to_yaml *a
7
+ utc.old_to_yaml(*a)
8
+ end
9
+ end
10
+
11
+ class Array
12
+ def paginate page_size
13
+ (0 .. ((size - 1) / page_size)).map { |p| self[(p * page_size) ... ((p + 1) * page_size)] }
14
+ end
15
+ end
16
+
17
+ module Enumerable
18
+ def map_by
19
+ inject({}) { |h, e| h[yield(e)] = e; h }
20
+ end
21
+
22
+ def group_by
23
+ inject({}) { |h, e| x = yield e; h[x] = (h[x] || []) << e; h }
24
+ end
25
+
26
+ def group_by_multiple
27
+ inject({}) { |h, e| yield(e).each { |x| h[x] = (h[x] || []) << e }; h }
28
+ end
29
+
30
+ def max_of &b; map(&b).max end
31
+
32
+ def argfind
33
+ each { |e| x = yield(e) and return x; }; nil
34
+ end
35
+
36
+ def sum; inject(0) { |a, b| a + b } end
37
+
38
+ def thread id_method, parent_id_method
39
+ branches = { :root => [:root, []] }
40
+
41
+ each do |e|
42
+ id = e.send(id_method) or next
43
+ parent_id = e.send(parent_id_method) || :root
44
+ if branches[id]
45
+ thing, children = branches[id]
46
+ raise ArgumentError, "multiple objects with #{id_method} #{id.inspect}" if thing
47
+ branches[id] = [e, children]
48
+ else
49
+ branches[id] = [e, []]
50
+ end
51
+ branches[parent_id] ||= [nil, []]
52
+ branches[parent_id][1] << e
53
+ end
54
+
55
+ tree = {}
56
+ branches.each { |label, (thing, children)| tree[thing] = children }
57
+ tree
58
+ end
59
+ end
60
+
61
+ class Hash
62
+ def maphash
63
+ each { |k, v| self[k] = yield(k, v) }
64
+ self
65
+ end
66
+ end
67
+
68
+ class Numeric
69
+ def pluralize s
70
+ nicenice + " " + (self == 1 ? s : s.plural_form)
71
+ end
72
+
73
+ def nicenice
74
+ %w(no one two three four five six)[self] || to_s
75
+ end
76
+ end
77
+
78
+ class String
79
+ EMAIL_RE = /"?(.+?)"? <(\S+@\S+?)>/
80
+
81
+ def plural_form
82
+ case self
83
+ when "reply"; "replies"
84
+ else self + "s"
85
+ end
86
+ end
87
+
88
+ def email_name
89
+ self =~ EMAIL_RE && $1
90
+ end
91
+
92
+ def email_address
93
+ self =~ EMAIL_RE && $2
94
+ end
95
+
96
+ def obfuscated_name
97
+ email_name
98
+ end
99
+
100
+ def escape
101
+ gsub("&", "&amp;").gsub(">", "&gt;").gsub("<", "&lt;")
102
+ end
103
+
104
+ def unescape
105
+ gsub("&gt;", ">").gsub("&lt;", "<").gsub("&amp;", "&")
106
+ end
107
+
108
+ def reformat width
109
+ lines = []
110
+ body = self.clone
111
+
112
+ if body =~ /\A(\n+)/ # preserve any leading newlines
113
+ newlines = $1
114
+ lines << newlines
115
+ body = body[newlines.size .. -1]
116
+ end
117
+
118
+ while body
119
+ newline_point = body.index(/\n([\n\*\#])/) || width
120
+
121
+ split_point, appendage = if newline_point < width
122
+ [newline_point + 1, "\n#{$1}"]
123
+ elsif body.length < width
124
+ [body.length, "\n"]
125
+ else # regular: find a split point
126
+ split_point = body.rindex(/\s/, width) # look backwards
127
+ split_point ||= body.index(/\s/, width) # nope, look forward
128
+ split_point ||= body.length # nope, use everything (don't think this will ever occur)
129
+ [split_point, "\n"]
130
+ end
131
+
132
+ start_split = split_point - 1
133
+ start_split -= 1 while start_split > 0 && [?\ , ?\n].include?(body[start_split])
134
+
135
+ lines << body[0 .. start_split].chomp.gsub(/\n/, " ") + appendage
136
+ body = body[(split_point + 1) .. -1]
137
+ end
138
+
139
+ lines << body if body
140
+ lines.pop if lines.last == "\n"
141
+ lines.join("")
142
+ end
143
+ end
144
+
145
+ class Module # oh ruby
146
+ def bool_reader *args
147
+ args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
148
+ end
149
+ def bool_writer *args; attr_writer(*args); end
150
+ def bool_accessor *args
151
+ bool_reader(*args)
152
+ bool_writer(*args)
153
+ end
154
+ end
155
+
156
+ module Whisper
157
+
158
+ ## quick replacement for Ruby's crappy Logger class
159
+ class Logger
160
+ LOG_LEVELS = %w(debug info warn error fatal)
161
+
162
+ ## keep one instance around
163
+ class << self; attr_accessor :instance end
164
+
165
+ def initialize stream, level
166
+ level = level.to_s
167
+ raise ArgumentError, "log level must be one of #{LOG_LEVELS.join ', '}" unless LOG_LEVELS.include? level
168
+
169
+ @stream = stream
170
+ @level = level
171
+ @level_index = LOG_LEVELS.index(@level)
172
+
173
+ LOG_LEVELS[0 ... @level_index].each do |m|
174
+ self.class.instance_eval do
175
+ define_method(m) { |s, where=nil| }
176
+ define_method("#{m}?") { false }
177
+ define_method("if_#{m}?") { |&b| }
178
+ end
179
+ end
180
+ LOG_LEVELS[@level_index .. -1].each do |m|
181
+ self.class.instance_eval do
182
+ define_method(m) { |s, where=nil| send_message m, s, where }
183
+ define_method("#{m}?") { true }
184
+ define_method("if_#{m}?") { |&b| b.call }
185
+ end
186
+ end
187
+
188
+ Logger.instance ||= self
189
+ end
190
+
191
+ def send_message level, msg, where=nil
192
+ m = format_message level, msg, where
193
+ @stream.print m
194
+ @stream.flush
195
+ end
196
+
197
+ def format_message level, msg, where
198
+ where_s = where.nil? ? "" : "(#{where.sub("Whisper::", "")}) "
199
+ "[#{Time.now}] #{where_s}#{level.upcase}: #{msg}\n"
200
+ end
201
+ end
202
+
203
+ module Loggy
204
+ Logger::LOG_LEVELS.each { |m| define_method(m) { |s| Logger.instance.send(m, s, self.class.name) if Logger.instance } }
205
+ end
206
+
207
+ ## model dependency behaviors.
208
+ ##
209
+ ## methods provided:
210
+ ## #content -- get the latest content, built lazily
211
+ ## #refresh! -- do a check, invalidate content if necessary
212
+ ##
213
+ ## methods you must define:
214
+ ##
215
+ ## #dependencies: returns the set of dependencies, i.e. the set of objects
216
+ ## which, if they change, this object should change too.
217
+ ## #build(old_content): rebuild the content (i.e. from the dependencies' contents)
218
+ ##
219
+ ## optional methods you can define:
220
+ ## #check: check for dirtiness (from external sources; we take care
221
+ ## of the rest), returning true iff dirty. if this is an expensive method,
222
+ ## you should limit operation to once every N seconds or something like that.
223
+ ##
224
+ ## this module requires initialization, so you should call
225
+ ## #dependency_init in your constructor
226
+ ##
227
+ ## also, no cycle checking, so DAGs only.
228
+ ##
229
+ ## also, allows for different types of content, if you pass arguments to #content
230
+ module Dependency
231
+ include Loggy
232
+
233
+ class << self; attr_accessor :min_refresh_interval end
234
+
235
+ def dependency_init
236
+ @mutex = Mutex.new
237
+ @timestamp = Time.now
238
+ @last_refresh = Time.at 0
239
+ @old_content = {}
240
+ @content = {}
241
+
242
+ Dependency.min_refresh_interval ||= 10
243
+ end
244
+
245
+ def timestamp; @mutex.synchronize { @timestamp } end
246
+
247
+ def refresh!
248
+ @mutex.synchronize do
249
+ return if (@last_refresh + Dependency.min_refresh_interval) > Time.now
250
+ #debug "[dep] refresh called, checking #{dependencies.size} deps (content empty is #{@content.empty?}; respond_to check is #{respond_to?(:check)})"
251
+ dependencies.each { |d| d.refresh! }
252
+ dep_changed = dependencies.find { |d| d.timestamp > @timestamp }
253
+ i_changed = !dep_changed && respond_to?(:check) && check
254
+ if !@content.empty? && (dep_changed || i_changed)
255
+ #debug "[dep #{self.class}:#{object_id}] dep #{dep_changed.class}:#{dep_changed.object_id} changed (#{dep_changed.timestamp} vs #{@timestamp}), invalidating content" if dep_changed
256
+ #debug "[dep #{self.class}:#{object_id}] i changed, invalidating content" if i_changed
257
+ @timestamp = Time.now
258
+ @old_content = @content
259
+ @content = {}
260
+ end
261
+ @last_refresh = Time.now
262
+ end
263
+ end
264
+
265
+ def content *a
266
+ @mutex.synchronize do
267
+ unless @content.member?(a)
268
+ @content[a] = build @old_content[a], *a
269
+ #debug "[dep #{self.class}:#{object_id}] content was rebuilt"
270
+ end
271
+ end
272
+ @content[a]
273
+ end
274
+ end
275
+
276
+ end
@@ -0,0 +1,49 @@
1
+ require 'yaml'
2
+
3
+ module Whisper
4
+
5
+ class Config
6
+ class Error < StandardError; end
7
+
8
+ REQUIRED = [ :whisper_dir, :title, :tagline, :root, :public_url_root, :from_email_address, :comment_mbox ]
9
+ OPTIONAL = [ :post_dir, :comment_dir, :template_dir, :static_dir, :helper_dir, :draft_dir, :sendmail,
10
+ :development_port, :port, :page_size, :rack_handler, :mbox_offset_filename, :author_dir,
11
+ :x_accel_redirect, :formatter_dir]
12
+ ALL = REQUIRED + OPTIONAL
13
+
14
+ ALL.each { |k| define_method(k) { @hash[k] } }
15
+
16
+ def initialize fn, base_dir, mode=:development
17
+ @mode = mode
18
+ @hash = YAML.load_file fn
19
+ @hash[:whisper_dir] = base_dir
20
+ REQUIRED.each { |k| raise Error, "missing required configuration value '#{k}'" unless @hash[k] }
21
+ @hash.keys.each { |k| raise Error, "unknown configuration value '#{k}'" unless ALL.member? k }
22
+ fill_defaults!
23
+ end
24
+
25
+ attr_reader :mode
26
+
27
+ def use_this_port; @mode == :development ? @hash[:development_port] : @hash[:port] end
28
+
29
+ private
30
+
31
+ def fill_defaults!
32
+ @hash[:post_dir] ||= File.join @hash[:whisper_dir], "posts"
33
+ @hash[:comment_dir] ||= File.join @hash[:whisper_dir], "comments"
34
+ @hash[:template_dir] ||= File.join @hash[:whisper_dir], "templates"
35
+ @hash[:static_dir] ||= File.join @hash[:whisper_dir], "static"
36
+ @hash[:draft_dir] ||= File.join @hash[:whisper_dir], "drafts"
37
+ @hash[:author_dir] ||= File.join @hash[:whisper_dir], "authors"
38
+ @hash[:helper_dir] ||= @hash[:whisper_dir]
39
+ @hash[:formatter_dir] ||= @hash[:whisper_dir]
40
+ @hash[:mbox_offset_filename] ||= File.join @hash[:whisper_dir], "offset.txt"
41
+ @hash[:sendmail] ||= "/usr/sbin/sendmail -oem -ti"
42
+ @hash[:port] ||= 80
43
+ @hash[:development_port] ||= 9292
44
+ @hash[:page_size] ||= 10
45
+ @hash[:rack_handler] ||= "Thin"
46
+ end
47
+ end
48
+
49
+ end