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.
- data/Changelog +23 -0
- data/LICENSE +661 -0
- data/README +59 -0
- data/bin/whisper +91 -0
- data/bin/whisper-init +40 -0
- data/bin/whisper-post +88 -0
- data/bin/whisper-process-email +54 -0
- data/lib/whisper.rb +52 -0
- data/lib/whisper/author_tracker.rb +46 -0
- data/lib/whisper/blog.rb +156 -0
- data/lib/whisper/cached_file.rb +65 -0
- data/lib/whisper/comment.rb +35 -0
- data/lib/whisper/comment_set.rb +41 -0
- data/lib/whisper/common.rb +276 -0
- data/lib/whisper/config.rb +49 -0
- data/lib/whisper/dir_scanner.rb +76 -0
- data/lib/whisper/dir_set.rb +50 -0
- data/lib/whisper/email_receiver.rb +212 -0
- data/lib/whisper/email_sender.rb +114 -0
- data/lib/whisper/entry.rb +44 -0
- data/lib/whisper/entry_set.rb +41 -0
- data/lib/whisper/handler.rb +99 -0
- data/lib/whisper/mbox.rb +141 -0
- data/lib/whisper/page.rb +118 -0
- data/lib/whisper/rfc2047.rb +79 -0
- data/lib/whisper/router.rb +50 -0
- data/lib/whisper/server.rb +88 -0
- data/lib/whisper/text.rb +252 -0
- data/lib/whisper/timed_map.rb +43 -0
- data/lib/whisper/version.rb +3 -0
- data/share/whisper/comment-email.rtxt +36 -0
- data/share/whisper/config.yaml +53 -0
- data/share/whisper/entry-email.rtxt +35 -0
- data/share/whisper/entry.rhtml +88 -0
- data/share/whisper/entry.rtxt +19 -0
- data/share/whisper/formatter.rb +52 -0
- data/share/whisper/helper.rb +147 -0
- data/share/whisper/list.rhtml +51 -0
- data/share/whisper/list.rrss +9 -0
- data/share/whisper/list.rtxt +25 -0
- data/share/whisper/master.rhtml +81 -0
- data/share/whisper/master.rrss +21 -0
- data/share/whisper/master.rtxt +5 -0
- data/share/whisper/mootools.js +282 -0
- data/share/whisper/rss-badge.png +0 -0
- data/share/whisper/spinner.gif +0 -0
- data/share/whisper/style.css +314 -0
- metadata +239 -0
data/README
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
Whisper is a lightweight, simple weblaaagh server. Whisper is around 1400
|
2
|
+
lines of code, requires no database, can serve over 1500 requests per
|
3
|
+
second, and can output HTML, RSS, and text.
|
4
|
+
|
5
|
+
Demo: http://all-thing.net
|
6
|
+
News: http://all-thing.net/label/whisper/
|
7
|
+
Homepage: http://masanjin.net/whisper/
|
8
|
+
|
9
|
+
Features:
|
10
|
+
|
11
|
+
- No RDBMS. Because storing your blog entries in a RDBMS is like driving to
|
12
|
+
work in the Space Shuttle.
|
13
|
+
|
14
|
+
- YAML+Textile, sitting on a disk. Blog posts and comments are stored on
|
15
|
+
disk in regular files, using a mix of YAML and Textile. This means you can
|
16
|
+
keep your content under version control, and you can edit it with whatever
|
17
|
+
editor you desire.
|
18
|
+
|
19
|
+
- Sits directly on top of Rack (or Thin). No intermediate layer to slow
|
20
|
+
things down. For benchmarks, see http://all-thing.net/whisper-benchmarks.
|
21
|
+
|
22
|
+
- Lazy cached dependency graph: every bit of content is cached, built
|
23
|
+
lazily, and a part of a big dependency graph. That means almost every
|
24
|
+
request is served directly from memory, and making a change, like adding
|
25
|
+
or updating an entry, forces a regeneration of only those bits that
|
26
|
+
require it. Infrequently-requested bits of content eventually expire.
|
27
|
+
|
28
|
+
- Markup enhancements: Whisper has extra processing on top of Textile to
|
29
|
+
syntax highlight ruby code, and turn LaTeX math expressions are turned
|
30
|
+
into MathML (via RiTeX). Write pretty Ruby code and pretty math without
|
31
|
+
any extra effort.
|
32
|
+
|
33
|
+
- Fully threaded comments. Why would you not have this?
|
34
|
+
|
35
|
+
- Blog post as mailing list. Comments can be made by entering your email
|
36
|
+
address, and replying to the resulting email. You can choose to have
|
37
|
+
future replies emailed to you, and replying to them automatically adds a
|
38
|
+
comment. This allows you to quote, thread, and generally have a
|
39
|
+
reasonable discussion, which is what email is good at, and what typing
|
40
|
+
shit into little text areas on your web browser is not.
|
41
|
+
|
42
|
+
- Multiformat support. In addition to HTML and RSS output, there’s a plain
|
43
|
+
text mode for the hard-core.
|
44
|
+
|
45
|
+
- Pagination, labels, per-label and per-author indices, etc.
|
46
|
+
|
47
|
+
Caveats:
|
48
|
+
|
49
|
+
Whisper currently only supports comments via email. Textboxes are for the
|
50
|
+
birds.
|
51
|
+
|
52
|
+
Usage:
|
53
|
+
|
54
|
+
1. gem install whisperblog
|
55
|
+
2. whisper-init <blog directory>
|
56
|
+
3. Follow the instructions!
|
57
|
+
|
58
|
+
To run whisper in production mode (probably what you want for serving your
|
59
|
+
blog), use --production.
|
data/bin/whisper
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rack'
|
5
|
+
require 'trollop'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
require "whisper"
|
9
|
+
|
10
|
+
opts = Trollop.options do
|
11
|
+
text "Whisper v#{Whisper::VERSION}"
|
12
|
+
|
13
|
+
opt :whisper_dir, "Whisper root directory", :default => Dir.pwd
|
14
|
+
opt :port, "Port to run on (default: 80 in production mode, 9292 in development mode)", :short => 'p', :type => Integer
|
15
|
+
opt :socket, "Socket filename to run on instead of a TCP port (requires Thin)", :type => String
|
16
|
+
opt :development, "Development mode", :default => true, :short => :none
|
17
|
+
opt :production, "Production mode", :default => false, :short => :none
|
18
|
+
opt :log_level, "Logging level (default: 'info' in production mode, 'debug' in development mode)", :type => String
|
19
|
+
opt :log_file, "Log file ('-' for stdout)", :default => "-"
|
20
|
+
opt :console, "Run an IRB session instead of a web server", :default => false, :short => :none
|
21
|
+
opt :no_email, "Don't process incoming email", :default => false, :short => :none
|
22
|
+
conflicts :development, :production
|
23
|
+
conflicts :port, :socket
|
24
|
+
end
|
25
|
+
|
26
|
+
Trollop.die :log_level, "must be one of 'warn', 'info' or 'debug'" if opts[:log_level] unless %w(warn info debug).member? opts[:log_level]
|
27
|
+
|
28
|
+
if opts[:production]
|
29
|
+
opts[:development] = false
|
30
|
+
opts[:log_level] ||= "info"
|
31
|
+
else
|
32
|
+
opts[:development] = true
|
33
|
+
opts[:log_level] ||= "debug"
|
34
|
+
end
|
35
|
+
|
36
|
+
log_stream = opts[:log_file] == "-" ? $stdout : File.open(opts[:log_file], "w")
|
37
|
+
logger = Whisper::Logger.new log_stream, opts[:log_level]
|
38
|
+
|
39
|
+
config_fn = File.join opts[:whisper_dir], "config.yaml"
|
40
|
+
config = begin
|
41
|
+
Whisper::Config.new config_fn, opts[:whisper_dir], opts[:development] ? :development : :production
|
42
|
+
rescue SystemCallError => e
|
43
|
+
Trollop::die "Can't load configuration file #{config_fn}. Do you need to specify --whisper-dir, or run whisper-init?"
|
44
|
+
rescue Whisper::Config::Error => e
|
45
|
+
Trollop::die "Error loading configuration file #{config_fn}: #{e.message}"
|
46
|
+
end
|
47
|
+
|
48
|
+
if opts[:socket]
|
49
|
+
Trollop::die :socket, "cannot be specified if rack_handler configuration is not 'Thin'" unless config.rack_handler == "Thin"
|
50
|
+
end
|
51
|
+
|
52
|
+
whisper = Whisper::init config
|
53
|
+
server = Whisper::Server.new whisper[:blog], config.mode, config.static_dir, config.x_accel_redirect
|
54
|
+
|
55
|
+
if opts[:console]
|
56
|
+
@whisper = whisper
|
57
|
+
@server = server
|
58
|
+
|
59
|
+
require 'irb'
|
60
|
+
IRB.start __FILE__
|
61
|
+
exit
|
62
|
+
end
|
63
|
+
|
64
|
+
app = if config.mode == :development
|
65
|
+
Thread.abort_on_exception = true
|
66
|
+
Whisper::Dependency.min_refresh_interval = 1
|
67
|
+
Whisper::TimedMap.expiration_interval = 1
|
68
|
+
Whisper::DirScanner.min_scan_interval = 1
|
69
|
+
Whisper::CachedFile.min_scan_interval = 1
|
70
|
+
|
71
|
+
Rack::Builder.new do
|
72
|
+
use Rack::CommonLogger, $stdout
|
73
|
+
use Rack::ShowExceptions
|
74
|
+
use Rack::Lint
|
75
|
+
run server
|
76
|
+
end.to_app
|
77
|
+
else
|
78
|
+
whisper[:sender].start!
|
79
|
+
whisper[:receiver].start! unless opts[:no_email]
|
80
|
+
|
81
|
+
server
|
82
|
+
end
|
83
|
+
|
84
|
+
handler = Rack::Handler::get config.rack_handler.downcase
|
85
|
+
run_opts = if opts[:socket]
|
86
|
+
{ :Host => File.expand_path(opts[:socket]) }
|
87
|
+
else
|
88
|
+
{ :Port => (opts[:port] || config.use_this_port) }
|
89
|
+
end
|
90
|
+
|
91
|
+
handler.run app, run_opts
|
data/bin/whisper-init
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'trollop'
|
5
|
+
require 'yaml'
|
6
|
+
require 'fileutils'
|
7
|
+
require "whisper"
|
8
|
+
|
9
|
+
def die s; puts s; exit end
|
10
|
+
def mkdir d
|
11
|
+
FileUtils.mkdir d
|
12
|
+
puts "+ created: #{d}"
|
13
|
+
end
|
14
|
+
def copy f, t
|
15
|
+
FileUtils.cp f, t
|
16
|
+
puts "+ created: #{t}/#{File.basename(f)}"
|
17
|
+
end
|
18
|
+
|
19
|
+
dir = ARGV.shift or die("Expecting one argument: the directory to create")
|
20
|
+
mkdir dir
|
21
|
+
|
22
|
+
%w(templates static comments drafts posts authors).each { |d| mkdir File.join(dir, d) }
|
23
|
+
share_dir = File.join File.dirname(__FILE__), "..", "share", "whisper"
|
24
|
+
|
25
|
+
%w(config.yaml helper.rb formatter.rb).each { |f| copy File.join(share_dir, f), dir }
|
26
|
+
%w(style.css mootools.js rss-badge.png spinner.gif).each { |f| copy File.join(share_dir, f), File.join(dir, "static") }
|
27
|
+
|
28
|
+
template_dir = File.join(dir, "templates")
|
29
|
+
%w(rhtml rtxt rrss).map { |ext| Dir[File.join(share_dir, "*.#{ext}")] }.flatten.each do |f|
|
30
|
+
copy f, template_dir
|
31
|
+
end
|
32
|
+
|
33
|
+
puts <<EOS
|
34
|
+
Directory setup is complete. Remaining steps:
|
35
|
+
1. Edit #{File.join dir, "config.yaml"} to configure your weblaaagh.
|
36
|
+
2. Run "whisper-post -w #{dir}" to add a new post.
|
37
|
+
3. Run "whisper -w #{dir}" and see blog at http://localhost:9292.
|
38
|
+
4. To customize, edit #{template_dir}/*, helper.rb and formatter.rb.
|
39
|
+
5. Have fun.
|
40
|
+
EOS
|
data/bin/whisper-post
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'trollop'
|
5
|
+
require 'yaml'
|
6
|
+
require 'readline'
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
require "whisper"
|
10
|
+
|
11
|
+
def ask q
|
12
|
+
ans = Readline.readline q
|
13
|
+
Readline::HISTORY.push ans
|
14
|
+
ans
|
15
|
+
end
|
16
|
+
|
17
|
+
def ask_complete q, comps
|
18
|
+
Readline.completion_append_character = nil
|
19
|
+
Readline.completion_proc = lambda { |prefix| comps.select { |c| c[0 ... prefix.length] == prefix } }
|
20
|
+
ans = Readline.readline q
|
21
|
+
Readline::HISTORY.push ans
|
22
|
+
ans
|
23
|
+
end
|
24
|
+
|
25
|
+
opts = Trollop.options do
|
26
|
+
text "Whisper v#{Whisper::VERSION}"
|
27
|
+
opt :whisper_dir, "Whisper root directory", :default => "."
|
28
|
+
end
|
29
|
+
|
30
|
+
config_fn = File.join opts[:whisper_dir], "config.yaml"
|
31
|
+
begin
|
32
|
+
config = Whisper::Config.new config_fn, opts[:whisper_dir]
|
33
|
+
rescue SystemCallError
|
34
|
+
Trollop::die "can't read config file #{config_fn}. Do you need --whisper-dir?"
|
35
|
+
end
|
36
|
+
|
37
|
+
tags = []
|
38
|
+
authors = []
|
39
|
+
ids = []
|
40
|
+
Dir[File.join(config.post_dir, "*#{Whisper::ENTRY_METADATA_EXTENSION}")].each do |fn|
|
41
|
+
yaml = YAML.load_file(fn)
|
42
|
+
tags += yaml[:labels] if yaml[:labels]
|
43
|
+
authors << yaml[:author]
|
44
|
+
ids << yaml[:id]
|
45
|
+
end
|
46
|
+
cur_tags = tags.flatten.uniq
|
47
|
+
cur_authors = authors.flatten.uniq
|
48
|
+
|
49
|
+
title = ask "Post title: "
|
50
|
+
default_id = title.downcase.gsub(/[^\w0-9\.]+/, "-").gsub(/\-+$/, "")
|
51
|
+
id = ask_complete "Post id (will be used for url) [#{default_id}]: ", ids
|
52
|
+
id = default_id if id.empty?
|
53
|
+
tags = ask_complete("Tags (space-separated, tab for completion): ", cur_tags).split(/\s+/)
|
54
|
+
author = ask_complete "Author email address (in \"Name <address@domain.tld>\" format, tab for completion): ", cur_authors until author && author.email_name && author.email_address
|
55
|
+
|
56
|
+
fn = id
|
57
|
+
real_fn = while true
|
58
|
+
real_fn = fn + Whisper::ENTRY_METADATA_EXTENSION
|
59
|
+
break real_fn unless File.exist? real_fn
|
60
|
+
fn += "-again"
|
61
|
+
end
|
62
|
+
|
63
|
+
yaml_fn = File.join config.draft_dir, real_fn
|
64
|
+
puts "Writing #{yaml_fn}..."
|
65
|
+
File.open(yaml_fn, "w") do |f|
|
66
|
+
f.print({ :author => author, :published => Time.now, :updated => Time.now,
|
67
|
+
:id => id, :title => title, :labels => tags }.to_yaml)
|
68
|
+
end
|
69
|
+
|
70
|
+
textile_fn = File.join config.draft_dir, (fn + Whisper::ENTRY_CONTENT_EXTENSION)
|
71
|
+
editor = ENV["EDITOR"] || "/bin/vi"
|
72
|
+
puts "Editing #{textile_fn} with #{editor}..."
|
73
|
+
system "#{editor} #{textile_fn}"
|
74
|
+
|
75
|
+
puts "Done!"
|
76
|
+
|
77
|
+
if Readline.readline("Edit metadata (y/N)? ") =~ /^[Yy]$/
|
78
|
+
puts "Editing #{yaml_fn} with #{editor}..."
|
79
|
+
system "#{editor} #{yaml_fn}"
|
80
|
+
end
|
81
|
+
|
82
|
+
if Readline.readline("Publish (Y/n)? ") =~ /^[Yy]?$/
|
83
|
+
FileUtils.mv textile_fn, config.post_dir
|
84
|
+
FileUtils.mv yaml_fn, config.post_dir
|
85
|
+
puts "Published to #{config.post_dir}!"
|
86
|
+
else
|
87
|
+
puts "Saved as a draft."
|
88
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rack'
|
5
|
+
require 'trollop'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
require "whisper"
|
9
|
+
|
10
|
+
opts = Trollop.options do
|
11
|
+
text "Whisper v#{Whisper::VERSION}"
|
12
|
+
|
13
|
+
opt :whisper_dir, "Whisper root directory", :default => Dir.pwd
|
14
|
+
opt :log_level, "Set log level (default: 'info')", :type => String, :default => "debug"
|
15
|
+
opt :send, "In addition to writing comments, resend email", :default => false
|
16
|
+
opt :num, "Just process this many emails", :type => Integer
|
17
|
+
opt :start, "Offset to start at, if not the current mbox offset", :type => Integer
|
18
|
+
end
|
19
|
+
|
20
|
+
Trollop.die :log_level, "must be one of 'warn', 'info' or 'debug'" if opts[:log_level] unless %w(warn info debug).member? opts[:log_level]
|
21
|
+
Trollop.die "expecting no arguments" unless ARGV.empty?
|
22
|
+
|
23
|
+
logger = Whisper::Logger.new $stdout, opts[:log_level]
|
24
|
+
|
25
|
+
config_fn = File.join opts[:whisper_dir], "config.yaml"
|
26
|
+
config = begin
|
27
|
+
Whisper::Config.new config_fn, opts[:whisper_dir], opts[:development] ? :development : :production
|
28
|
+
rescue SystemCallError => e
|
29
|
+
Trollop::die "Can't load configuration file #{config_fn}. Do you need to specify --whisper-dir, or run whisper-init?"
|
30
|
+
rescue Whisper::Config::Error => e
|
31
|
+
Trollop::die "Error loading configuration file #{config_fn}: #{e.message}"
|
32
|
+
end
|
33
|
+
|
34
|
+
whisper = Whisper::init config
|
35
|
+
receiver, sender = whisper[:receiver], whisper[:sender]
|
36
|
+
receiver.offset = opts[:start] if opts[:start]
|
37
|
+
|
38
|
+
num = 0
|
39
|
+
until receiver.done?
|
40
|
+
break if opts[:num] && num >= opts[:num]
|
41
|
+
receiver.step
|
42
|
+
num += 1
|
43
|
+
end
|
44
|
+
|
45
|
+
puts "Processed #{num} messages."
|
46
|
+
|
47
|
+
if opts[:send]
|
48
|
+
puts "Have #{sender.q_size} replies to send..."
|
49
|
+
sender.process until sender.done?
|
50
|
+
else
|
51
|
+
puts "NOT sending #{sender.q_size} scheduled reply emails."
|
52
|
+
end
|
53
|
+
|
54
|
+
puts "Done!"
|
data/lib/whisper.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Whisper
|
2
|
+
ENTRY_METADATA_EXTENSION = ".yaml"
|
3
|
+
ENTRY_CONTENT_EXTENSION = ".textile"
|
4
|
+
|
5
|
+
def init config
|
6
|
+
## load the formatters
|
7
|
+
formatter_fn = File.join(config.formatter_dir, "formatter.rb")
|
8
|
+
begin
|
9
|
+
load formatter_fn
|
10
|
+
rescue SystemCallError
|
11
|
+
Logger.instance.warn "no formatter plugins file #{formatter_fn} found"
|
12
|
+
end
|
13
|
+
|
14
|
+
entryset = Whisper::EntrySet.new config.post_dir
|
15
|
+
commentset = Whisper::CommentSet.new config.comment_dir
|
16
|
+
router = Whisper::Router.new config.root
|
17
|
+
authors = Whisper::AuthorTracker.new config.author_dir
|
18
|
+
blog = Whisper::Blog.new config, entryset, commentset, router, authors
|
19
|
+
sender = Whisper::EmailSender.new entryset, commentset, router, authors, blog.template_for("entry-email", "txt"), blog.template_for("comment-email", "txt"), config
|
20
|
+
receiver = Whisper::EmailReceiver.new entryset, commentset, sender, authors, config.comment_mbox, config.mbox_offset_filename, config.comment_dir
|
21
|
+
|
22
|
+
blog.install_default_routes! sender
|
23
|
+
|
24
|
+
{ entryset: entryset, commentset: commentset, router: router, authors: authors,
|
25
|
+
blog: blog, sender: sender, receiver: receiver
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
module_function :init
|
30
|
+
end
|
31
|
+
|
32
|
+
require "whisper/version.rb"
|
33
|
+
require "whisper/common.rb"
|
34
|
+
require "whisper/dir_scanner.rb"
|
35
|
+
require "whisper/text.rb"
|
36
|
+
require "whisper/entry.rb"
|
37
|
+
require "whisper/comment.rb"
|
38
|
+
require "whisper/config.rb"
|
39
|
+
require "whisper/cached_file.rb"
|
40
|
+
require "whisper/page.rb"
|
41
|
+
require "whisper/server.rb"
|
42
|
+
require "whisper/dir_set.rb"
|
43
|
+
require "whisper/entry_set.rb"
|
44
|
+
require "whisper/comment_set.rb"
|
45
|
+
require "whisper/timed_map.rb"
|
46
|
+
require "whisper/handler.rb"
|
47
|
+
require "whisper/router.rb"
|
48
|
+
require "whisper/blog.rb"
|
49
|
+
require "whisper/mbox.rb"
|
50
|
+
require "whisper/email_receiver.rb"
|
51
|
+
require "whisper/email_sender.rb"
|
52
|
+
require "whisper/author_tracker.rb"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "whisper/common"
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Whisper
|
5
|
+
class AuthorTracker
|
6
|
+
include Loggy
|
7
|
+
|
8
|
+
def initialize dir
|
9
|
+
@dir = dir
|
10
|
+
@mutex = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def [] name
|
14
|
+
@mutex.synchronize do
|
15
|
+
fn = fn_for name
|
16
|
+
begin
|
17
|
+
YAML.load_file fn
|
18
|
+
rescue SystemCallError => e
|
19
|
+
{}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def []= name, info
|
25
|
+
unless info.empty?
|
26
|
+
@mutex.synchronize do
|
27
|
+
fn = fn_for name
|
28
|
+
old_info = YAML.load_file(fn) rescue {}
|
29
|
+
info = old_info.merge info
|
30
|
+
debug "writing #{info.inspect} to #{fn}..."
|
31
|
+
begin
|
32
|
+
File.open(fn, "w") { |f| f.print info.to_yaml }
|
33
|
+
rescue SystemCallError => e
|
34
|
+
warn "can't write #{fn}: #{e.message}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
info
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def fn_for name; File.join(@dir, name.gsub(/[\/\.]/, "-")) + ".yaml" end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/lib/whisper/blog.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require "whisper/cached_file"
|
2
|
+
require "whisper/page"
|
3
|
+
require "whisper/common"
|
4
|
+
|
5
|
+
module Whisper
|
6
|
+
|
7
|
+
class Blog
|
8
|
+
include Loggy
|
9
|
+
|
10
|
+
MAX_RECENT_COMMENTS = 30
|
11
|
+
attr_accessor :entryset, :helper
|
12
|
+
|
13
|
+
def initialize config, entryset, commentset, router, authors
|
14
|
+
@config = config
|
15
|
+
@entryset = entryset
|
16
|
+
@commentset = commentset
|
17
|
+
@router = router
|
18
|
+
@authors = authors
|
19
|
+
@helper = CachedFile.new File.join(config.helper_dir, "helper.rb")
|
20
|
+
@templates = {} # we'll create these lazily
|
21
|
+
@paginations = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def title; @config.title end
|
25
|
+
def tagline; @config.tagline end
|
26
|
+
|
27
|
+
def get_content_for path, params, request_method; @router.get_content_for path, params, request_method end
|
28
|
+
def template_for name, format
|
29
|
+
@templates[[name, format]] ||= CachedFile.new File.join(@config.template_dir, "#{name}.r#{format}")
|
30
|
+
end
|
31
|
+
|
32
|
+
## we're basically doing this by hand, but we could also make a separate
|
33
|
+
## paginator object and fit it into the dependency model.
|
34
|
+
def paginate id, timestamp, things
|
35
|
+
time, pages = @paginations[id]
|
36
|
+
if time.nil? || time < timestamp
|
37
|
+
pages = things.paginate @config.page_size
|
38
|
+
pages[0] ||= []
|
39
|
+
debug "repaginated #{id.inspect}: turned #{things.size} things => #{pages.size} pages"
|
40
|
+
time = Time.now
|
41
|
+
@paginations[id] = [time, pages]
|
42
|
+
end
|
43
|
+
pages
|
44
|
+
end
|
45
|
+
|
46
|
+
def install_default_routes! emailer=nil
|
47
|
+
common_opts = { :config => @config, :router => @router, :helper => @helper, :extra_deps => [@entryset, @commentset], :entryset => @entryset }
|
48
|
+
|
49
|
+
@router.add_handler :index, :get, "/index/:page:format" do |route, page, format, params|
|
50
|
+
debug "index handler called for page #{page.inspect} format #{format.inspect}"
|
51
|
+
|
52
|
+
Page.new common_opts.merge({ :format => format, :url_vars => { :route => route },
|
53
|
+
:master_template => template_for("master", format),
|
54
|
+
:template => template_for("list", format) }) do
|
55
|
+
|
56
|
+
pages = paginate :entries_by_page, @entryset.timestamp, @entryset.entries
|
57
|
+
entries = pages[page] or raise InvalidPageError
|
58
|
+
comments = comments_for_entries entries
|
59
|
+
{ :entries => entries.zip(comments), :pages => pages.size, :page => page,
|
60
|
+
:recent_comments => recent_comments_and_entries, :route => route
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
@router.add_handler :label, :get, "/label/+label/:page:format" do |route, label, page, format, params|
|
66
|
+
debug "label handler called for label #{label.inspect} page #{page.inspect} format #{format.inspect}"
|
67
|
+
|
68
|
+
Page.new common_opts.merge({:format => format, :url_vars => { :route => route, :label => label },
|
69
|
+
:master_template => template_for("master", format),
|
70
|
+
:template => template_for("list", format) }) do
|
71
|
+
|
72
|
+
raw_entries = @entryset.entries_by_label[label] or raise InvalidPageError
|
73
|
+
pages = paginate [:entries_by_label, label], @entryset.timestamp, raw_entries
|
74
|
+
entries = pages[page] or raise InvalidPageError
|
75
|
+
comments = comments_for_entries entries
|
76
|
+
{ :entries => entries.zip(comments), :pages => pages.size, :page => page,
|
77
|
+
:recent_comments => recent_comments_and_entries, :route => route, :label => label
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
@router.add_handler :author, :get, "/by/+author/:page:format" do |route, author, page, format, params|
|
83
|
+
debug "author handler called for author #{author.inspect} page #{page.inspect} format #{format.inspect}"
|
84
|
+
|
85
|
+
Page.new common_opts.merge({:format => format, :url_vars => { :route => route, :author => author },
|
86
|
+
:master_template => template_for("master", format),
|
87
|
+
:template => template_for("list", format) }) do
|
88
|
+
|
89
|
+
pages = paginate [:entries_by_author, author], @entryset.timestamp, @entryset.entries_by_author[author]
|
90
|
+
entries = pages[page] or raise InvalidPageError
|
91
|
+
comments = comments_for_entries entries
|
92
|
+
{ :entries => entries.zip(comments), :pages => pages.size, :page => page,
|
93
|
+
:recent_comments => recent_comments_and_entries, :route => route, :author => author
|
94
|
+
}
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
## allow formats on /. why not.
|
99
|
+
@router.add_handler :root, :get, "/:format" do |route, format, params|
|
100
|
+
debug "root handler called with format #{format}"
|
101
|
+
Page.new common_opts.merge({ :format => format, :url_vars => { :route => :index },
|
102
|
+
:template => template_for("list", format),
|
103
|
+
:master_template => template_for("master", format) }) do
|
104
|
+
|
105
|
+
pages = paginate :entries_by_page, @entryset.timestamp, @entryset.entries
|
106
|
+
entries = pages[0]
|
107
|
+
comments = comments_for_entries entries
|
108
|
+
{ :entries => entries.zip(comments), :pages => pages.size, :page => 0,
|
109
|
+
:recent_comments => recent_comments_and_entries, :route => route
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if emailer
|
115
|
+
@router.add_handler :comment, :post, "/comment/+id" do |route, id, params|
|
116
|
+
debug "request comment handler called for id #{id} with params #{params.inspect}"
|
117
|
+
begin
|
118
|
+
emailer.handle_send_request id, params if emailer
|
119
|
+
["email sent!", "text/plain"]
|
120
|
+
rescue EmailSender::Error => e
|
121
|
+
["error: #{e.message}", "text/plain"]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
## individual entry handler is the most generic (entry ids can be close to
|
127
|
+
## anything), so we install it last.
|
128
|
+
@router.add_handler :entry, :get, "/+id:format" do |route, id, format, params|
|
129
|
+
debug "entry handler called for id #{id.inspect} format #{format.inspect}"
|
130
|
+
|
131
|
+
Page.new common_opts.merge({ :format => format, :url_vars => { :route => route, :id => id },
|
132
|
+
:template => template_for("entry", format),
|
133
|
+
:master_template => template_for("master", format)
|
134
|
+
}) do
|
135
|
+
|
136
|
+
raise InvalidPageError unless (entry = @entryset.entries_by_id[id])
|
137
|
+
{ :entry => entry, :comments => (@commentset.comment_tree_by_entry_id[entry.id] || {}),
|
138
|
+
:recent_comments => recent_comments_and_entries, :route => route,
|
139
|
+
:author_info => @authors
|
140
|
+
}
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def recent_comments_and_entries
|
146
|
+
comments = @commentset.comments[0 ... MAX_RECENT_COMMENTS]
|
147
|
+
entries_per_comment = @entryset.entries_by_id.values_at(*comments.map { |c| c.entry_id })
|
148
|
+
comments.zip entries_per_comment
|
149
|
+
end
|
150
|
+
|
151
|
+
def comments_for_entries entries
|
152
|
+
@commentset.comments_by_entry_id.values_at(*entries.map { |e| e.id }).map { |x| x || [] }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|