whisperblog 0.6

Sign up to get free protection for your applications and to get access to all the features.
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