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,53 @@
1
+ ## Whisper configuration file.
2
+ ##
3
+ ## This is a YAML-ified hash with symbol keys. If you don't understand what
4
+ ## that means, just know that lines starting with a '#' are comments, and lines
5
+ ## starting with : are settings.
6
+
7
+ ## blog title
8
+ :title: my fancy new whisper blog
9
+
10
+ ## blog tagline
11
+ :tagline: the internet is my internet
12
+
13
+ ## page size, for pagination purposes. max number of entries per page.
14
+ :page_size: 10
15
+
16
+ ## the underlying web server you'd like to use. I suggest "Thin". Other values
17
+ ## might work but are untested!
18
+ :rack_handler: Thin
19
+
20
+ ## the base path for the site. used for making relative links.
21
+ :root: "/"
22
+
23
+ ## the public, full URL for the site.
24
+ :public_url_root: "http://YOUR-SITE.HERE"
25
+
26
+ ## the email address that comments should be sent from
27
+ :from_email_address: comments@YOUR-SITE.HERE
28
+
29
+ ## the location of the mbox file that holds email sent to the address in
30
+ ## :from_email_address:.
31
+ :comment_mbox: /var/mail/comments
32
+
33
+ ## the sendmail command
34
+ #:sendmail: /usr/sbin/sendmail -oem -ti
35
+
36
+ ## the development port to listen on, if not set with --port
37
+ #:development_port: 9292
38
+
39
+ ## the production port to listen on, if not set with --port
40
+ #:port: 80
41
+
42
+ ## the path to use with X-Accel-Redirect headers, if using nginx. Only active
43
+ ## in production mode. Should be coupled with an nginx configuration block like
44
+ ## this:
45
+ ##
46
+ ## location /static-internal/ {
47
+ ## internal;
48
+ ## root /var/www/YOUR-SITE.HERE/static;
49
+ ## }
50
+ ## which gives the actual location of those static files.
51
+ #:x_accel_redirect: "/static-internal/"
52
+
53
+ ## and that's it!
@@ -0,0 +1,35 @@
1
+ From: <%= author.email_name %> (on <%= blog_title %>) <<%= from_address %>>
2
+ To: <%= to_address %>
3
+ Subject: [<%= blog_title %>] <%= entry.title(:email) %>
4
+ Date: <%= Time.now %>
5
+ Message-Id: <<%= message_id %>>
6
+
7
+ <% if web_request -%>
8
+ ; Hi there! This is an email containing the original post or a reply from
9
+ ; <%= full_url_for :route => :entry, :id => entry.id %>. Someone (hopefully you!)
10
+ ; requested this email.
11
+ ;
12
+ ; To reply, simply reply to this message, quoting as normal.
13
+ ; You can use Textile markup: http://hobix.com/textile/
14
+ ; DO NOT top-post: http://en.wikipedia.org/wiki/Top-posting#Top-posting
15
+ ;
16
+ ; Future replies may also be sent to you via email, depending on
17
+ ; the setting below.
18
+ ;
19
+ ; SETTINGS:
20
+ ;
21
+ ; Lines starting with a ;, like these, will be stripped out, even if quoted.
22
+ ; Lines starting with a !, like those below, allow you to configure settings.
23
+ ; Just edit them in place and leave them in your reply (quoted is fine).
24
+ ;
25
+ ! <%= Comment::RESEND_SETTING %>: <%= settings["send future comments to me"] || "all" %>
26
+ ; (one of "all", "replies-only", or "none")
27
+ ! <%= Comment::URL_SETTING %>: <%= settings["my url"] || "" %>
28
+ ; (a url to link your name to)
29
+ ;
30
+ <% else -%>
31
+ ; A reply to this message will be posted at <%= full_url_for :route => :entry, :id => entry.id %>
32
+ ; and emailed to any subscribers to this thread.
33
+ <% end -%>
34
+
35
+ <%= entry.body :email %>
@@ -0,0 +1,88 @@
1
+ <h2><%= link_to_current entry.title(:html) %></h2>
2
+
3
+ <% unless entry.labels.empty? %>
4
+ <div class="labels">
5
+ Topics:
6
+ <ul class="label-list">
7
+ <% entry.labels.each do |x| %>
8
+ <li class='label'><%= link_to x, :route => :label, :label => x %></li>
9
+ <% end %>
10
+ </ul>
11
+ </div>
12
+ <% end %>
13
+
14
+ <div class="byline">
15
+ <div style="float: left">
16
+ <span class="author-name">
17
+ <%= link_to entry.author.email_name, :route => :author, :author => entry.author.obfuscated_name, :html_opts => { :class => "author-name" } %>
18
+ </span>
19
+ </div>
20
+ <div style="float: right">
21
+ <span class="date" title="<%= pretty_time_diff entry.published %>"><%= entry.published.strftime "%B %e, %Y %l:%M%P" %></span>
22
+ </div>
23
+ <div style="clear: both;"></div>
24
+ </div>
25
+
26
+ <div class="entry">
27
+ <%= entry.body :html %>
28
+ </div>
29
+
30
+ <a name="comments"></a>
31
+
32
+ <ul class="comment-tree">
33
+ <% thread_to_html_nested_list(comments) do |prefix, c| %>
34
+ <%= prefix %>
35
+ <% next unless c %>
36
+ <a name="<%= c.id.escape %>"></a>
37
+ <div class="comment-body">
38
+ <div class="byline">
39
+ <div style="float: left">
40
+ <span class="author-name"><%= author_link c.author, author_info %></span>
41
+ </div>
42
+ <div style="float: right">
43
+ <span class="date" title="<%= pretty_time_diff c.published %>">
44
+ <a href="#<%= c.id.escape %>"><%= c.published.strftime "%B %e, %Y %l:%M%P" %></a>
45
+ </span>
46
+ </div>
47
+ <div style="clear: both"></div>
48
+ </div>
49
+ <%= c.body :html %>
50
+ <div class="reply-to-outer">
51
+ <div class="reply-to-header"><a href="#" class="reply-to-link">reply</a></div>
52
+ <div class="reply-to-box" id="reply-to-<%= entry.id %>-<%= c.id %>" >
53
+ <span class="comment-instructions">To reply, enter your email address. A copy of the comment will be sent to you via email.</span>
54
+ <%= start_comment_form entry, c %>
55
+ <%= textfield "email" %>
56
+ <%= submit_button "email me" %>
57
+ <%= end_comment_form entry, c %>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <% end %>
62
+ </ul>
63
+
64
+ <p class="comment-instructions">To reply to the article, enter your email
65
+ address. A copy of the article will be sent to you via email.</p>
66
+ <%= start_comment_form entry %>
67
+ <%= textfield "email" %>
68
+ <%= submit_button "email me" %>
69
+ <%= end_comment_form entry %>
70
+
71
+ <%= comment_form_script %>
72
+
73
+ <script type="text/javascript">
74
+ /* <![CDATA[ */
75
+ $$('.reply-to-link').each(function(link, i) {
76
+ var box = link.getParent().getParent().getElement('.reply-to-box');
77
+ var oldHeight = box.getStyle("height");
78
+ box.setStyle("height", 0);
79
+ box.setStyle("opacity", 0);
80
+ link.addEvent('click', function(e) {
81
+ e.stop();
82
+ box.setStyle("opacity", 1);
83
+ box.setStyle("height", oldHeight);
84
+ box.getElement("input").focus();
85
+ });
86
+ });
87
+ /* ]]> */
88
+ </script>
@@ -0,0 +1,19 @@
1
+ <%= entry.title(:txt) %>
2
+ <%= "-" * entry.title(:txt).size %>
3
+ Date: <%= entry.published.strftime "%B %e, %Y %l:%M%P" %>
4
+ Author: <%= entry.author.email_name %>
5
+ <% unless entry.labels.empty? -%>
6
+ Labels: <%= entry.labels.join(", ") %>
7
+ <% end -%>
8
+ URL: <%= full_url_for_current %>
9
+
10
+ <%= entry.body :txt %>
11
+ <% unless comments.empty? -%>
12
+
13
+ Replies
14
+ --------
15
+ <% thread_to_text_nested_list(comments) do |c, prefix|%>
16
+ <%= prefix %><%= c.author.email_name %>, on <%= c.published.strftime "%B %e, %Y %l:%M%P" %>:
17
+ <%= c.body :email, :prefix => prefix + "| "%>
18
+ <% end -%>
19
+ <% end -%>
@@ -0,0 +1,52 @@
1
+ ### here is the default set of formatting plugins provided by whisper. feel free
2
+ ### to add your own!
3
+ ###
4
+ ### these allow you to pull out regions of the post and do arbitrary stuff to
5
+ ### them, based on the format.
6
+
7
+ ## format quoted regions (typically in comments, but can be in posts too)
8
+ ## Note that format_quote is special and different from all the other handlers.
9
+ ## (In particular, it is the only nested formatter.)
10
+ Whisper::Text.format_quote do |format, body, attribution|
11
+ case format
12
+ when :html
13
+ [ attribution ? "<div class='comment-quote' title=\"#{attribution.escape}\">" : "<div class='comment-quote'>",
14
+ body,
15
+ "</div>"
16
+ ].join
17
+ when :txt
18
+ (attribution ? "#{attribution}\n" : "") + body.lines.map { |l| "> " + l }
19
+ end
20
+ end
21
+
22
+ ### the rest of these are generic user-defined handlers
23
+
24
+ ## format pre blocks as html pre blocks (very exciting)
25
+ Whisper::Text.format "<pre>", "</pre>" do |format, body, smatch, ematch|
26
+ case format
27
+ when :html; "<pre>#{body}</pre>"
28
+ end
29
+ end
30
+
31
+ ## format poem blocks with some fancy html
32
+ Whisper::Text.format /<poem(\s+title\s*=\s*"(.*?)"\s*)?>/, "</poem>" do |format, body, smatch, ematch|
33
+ title = smatch[2]
34
+ case format
35
+ when :html, :rss
36
+ (title ? "<div class='poem-title'>#{title.escape}</div>" : "") +
37
+ "<pre class='poem'>#{body}</pre>"
38
+ end
39
+ end
40
+
41
+ ## format <ruby> blocks with some nice markup
42
+ require 'syntax/convertors/html'
43
+ Whisper::Text.format "<ruby>", "</ruby>" do |format, body, smatch, ematch|
44
+ case format
45
+ when :html
46
+ x = Syntax::Convertors::HTML.for_syntax("ruby").convert body
47
+ x.sub!(/\A<pre>\n+/, "<pre class=\"ruby\">")
48
+ "<code>#{x}</code>"
49
+ when :rss
50
+ "<pre>" + body + "</pre>"
51
+ end
52
+ end
@@ -0,0 +1,147 @@
1
+ def labels_and_counts
2
+ entryset.entries_by_label.sort_by { |k, v| -v.flatten.size }.map do |k, v|
3
+ [k, v.flatten.size]
4
+ end
5
+ end
6
+
7
+ def authors_and_counts
8
+ entryset.entries_by_author.sort_by { |k, v| -v.flatten.size }.map do |k, v|
9
+ [k, v.flatten.size]
10
+ end
11
+ end
12
+
13
+ def author_link author, author_info
14
+ link = author_info[author.email_address]["my url"]
15
+ if link.nil? || link.empty?
16
+ author.email_name
17
+ else
18
+ "<a href=\"#{link.escape.gsub('"', "")}\">#{author.email_name}</a>"
19
+ end
20
+ end
21
+
22
+ def pretty_time_diff t, from=Time.now
23
+ d = (from - t).to_i
24
+ ad = d.abs
25
+ num, word = if ad < 60; [ad, "second"]
26
+ elsif ad < 60*60; [ad/60, "minute"]
27
+ elsif ad < 60*60*24; [ad/60/60, "hour"]
28
+ elsif ad < 60*60*24*7; [ad/60/60/24, "day"]
29
+ elsif ad < 60*60*24*7*10; [ad/60/60/24/7, "week"]
30
+ else [ad/60/60/24/30, "month"]
31
+ end
32
+
33
+ s = [num.nicenice, " ", word, (num == 1 ? "" : "s")].join
34
+ d > 0 ? s + " ago" : "in " + s
35
+ end
36
+
37
+ def textfield name; "<input type='text' name='#{name}' id='textfield-#{name}'/>" end
38
+ def submit_button label; "<input type='submit' value='#{label}' id='submit-#{label}'/>" end
39
+ def radio_button label, name, value, selected=false
40
+ id = [name, value].join "-"
41
+ "<input type='radio' id='#{id}' name='#{name}' value='#{value}' #{selected ? "checked='checked'" : ""}/><label for='#{id}'>#{label}</label>"
42
+ end
43
+
44
+ def start_comment_form entry, comment=nil
45
+ id = entry.id
46
+ id += "-#{comment.id}" if comment
47
+ url = url_for :route => :comment, :id => entry.id
48
+ "<form id='comment-form-#{id}' action='#{url}' method='post' class='comment-form'>"
49
+ end
50
+
51
+ def end_comment_form entry, comment=nil
52
+ id, stuff = if comment
53
+ ["#{entry.id}-#{comment.id.escape}",
54
+ "<input type='hidden' name='comment-id' value='#{comment.id.escape}'/>\n"]
55
+ else
56
+ [entry.id, ""]
57
+ end
58
+ stuff + <<EOS
59
+ <span class="form-result" id="form-result-#{id}"><!-- spanna --></span>
60
+ </form>
61
+ EOS
62
+ end
63
+
64
+ def thread_to_nested_list t # t the output of a Enumerable#thread
65
+ return if t.empty?
66
+
67
+ s = ""
68
+ stack = [:root]
69
+ level = 0
70
+
71
+ until stack.empty?
72
+ case(el = stack.pop)
73
+ when :end_item
74
+ yield :end_item, level
75
+ next
76
+ when :end_nest
77
+ level -= 1
78
+ yield :end_nest, level
79
+ next
80
+ when :root
81
+ # nothin
82
+ else
83
+ yield :start_item, level
84
+ yield :item, level, el
85
+ end
86
+
87
+ children = t[el]
88
+ if children.nil? || children.empty?
89
+ yield :end_item, level
90
+ else
91
+ unless el == :root
92
+ level += 1
93
+ yield :start_nest, level
94
+ stack << :end_item
95
+ stack << :end_nest
96
+ end
97
+ stack += children
98
+ end
99
+ end
100
+ end
101
+
102
+ def thread_to_html_nested_list t
103
+ prefix = ""
104
+ thread_to_nested_list(t) do |what, level, obj|
105
+ case what
106
+ when :start_item; prefix << "<li>"
107
+ when :start_nest; prefix << "<ul>"
108
+ when :end_item; prefix << "</li>"
109
+ when :end_nest; prefix << "</ul>"
110
+ when :item
111
+ yield prefix, obj
112
+ prefix = ""
113
+ end
114
+ end
115
+
116
+ yield prefix, nil
117
+ end
118
+
119
+ def thread_to_text_nested_list t, spaces=2
120
+ thread_to_nested_list(t) do |what, level, obj|
121
+ next unless what == :item
122
+ prefix = " " * spaces * level
123
+ yield obj, prefix
124
+ end
125
+ end
126
+
127
+ def comment_form_script
128
+ <<EOS
129
+ <script type="text/javascript">
130
+ /* <![CDATA[ */
131
+ $$('.comment-form').addEvent('submit', function(e) {
132
+ e.stop();
133
+ var el = this.getElement('.form-result');
134
+ var result = el.empty().addClass('ajax-loading');
135
+ this.set('send', {
136
+ method: 'post',
137
+ onComplete: function(response) {
138
+ result.removeClass('ajax-loading');
139
+ result.set('html', response);
140
+ },
141
+ });
142
+ this.send();
143
+ });
144
+ /* ]]> */
145
+ </script>
146
+ EOS
147
+ end
@@ -0,0 +1,51 @@
1
+ <% entries.each do |e, comments| %>
2
+ <h2><%= link_to e.title(:html), :route => :entry, :id => e.id %></h2>
3
+
4
+ <% unless e.labels.empty? %>
5
+ <div class="labels">
6
+ <ul class="label-list">
7
+ Topics:
8
+ <% e.labels.each do |x| %>
9
+ <li class='label'><%= link_to x, :route => :label, :label => x %></li>
10
+ <% end %>
11
+ </ul>
12
+ </div>
13
+ <% end %>
14
+
15
+ <div class="byline">
16
+ <div style="float: left">
17
+ <span class="author-name">
18
+ <%= link_to e.author.email_name, :route => :author, :author => e.author.obfuscated_name, :html_opts => { :class => "author-name" } %>
19
+ </span>
20
+ </div>
21
+ <div style="float: right">
22
+ <span class="date" title="<%= pretty_time_diff e.published %>"><%= e.published.strftime "%B %e, %Y %l:%M%P" %></span>
23
+ </div>
24
+ <div style="clear: both;"></div>
25
+ </div>
26
+
27
+ <div class="entry">
28
+ <%= e.body :html %>
29
+ </div>
30
+ <div class="comment-link">
31
+ <% authors = comments.map { |c| c.author.email_name.escape }.uniq.sort_by { rand }
32
+ title = comments.size.pluralize("reply").capitalize + case authors.size
33
+ when 0; ""
34
+ when 1; " by <b>#{authors[0]}</b>"
35
+ when 2; " by <b>#{authors[0]}</b> and <b>#{authors[1]}</b>"
36
+ else " by <b>#{authors[0]}</b> and #{(authors.size - 1).pluralize 'other'}"
37
+ end
38
+ %>
39
+ <%= link_to title, :route => :entry, :id => e.id, :anchor => "comments" %>.
40
+ </div>
41
+ <% end %>
42
+
43
+ <% if pages > 1 %>
44
+ <div class="pageblock">
45
+ <% if page > 0 %> <%= link_to_current "prev", :page => (page - 1), :html_opts => { :class => "pageword" } %>&nbsp;<% end %>
46
+ <% for i in 0...pages %>
47
+ <% if i == page %><span class="currentpagelink"><%= sprintf "%2d", i %></span><% else %><%= link_to_current sprintf("%2d", i), :page => i, :html_opts => { :class => "pagelink" } %><% end %>
48
+ <% end %>
49
+ <% if page < pages - 1 %>&nbsp;<%= link_to_current "next", :page => (page + 1), :html_opts => { :class => "pageword" } %><% end %>
50
+ </div>
51
+ <% end %>