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