murlsh 1.4.1 → 1.5.0

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 (63) hide show
  1. data/.gitignore +4 -0
  2. data/.htaccess +0 -3
  3. data/Gemfile +41 -0
  4. data/README.textile +68 -6
  5. data/Rakefile +65 -152
  6. data/config.ru +13 -2
  7. data/config.yaml +9 -4
  8. data/db/migrate/20110213023123_init.rb +20 -0
  9. data/lib/murlsh/atom_body.rb +78 -0
  10. data/lib/murlsh/atom_server.rb +38 -0
  11. data/lib/murlsh/auth.rb +12 -0
  12. data/lib/murlsh/build_query.rb +1 -1
  13. data/lib/murlsh/cat_files.rb +17 -0
  14. data/lib/murlsh/delicious_parse.rb +1 -0
  15. data/lib/murlsh/dispatch.rb +25 -11
  16. data/lib/murlsh/etag_add_encoding.rb +1 -1
  17. data/lib/murlsh/feed_body.rb +36 -0
  18. data/lib/murlsh/img_store.rb +27 -28
  19. data/lib/murlsh/install.rb +1 -0
  20. data/lib/murlsh/json_body.rb +5 -2
  21. data/lib/murlsh/json_server.rb +9 -13
  22. data/lib/murlsh/m3u_body.rb +28 -0
  23. data/lib/murlsh/m3u_server.rb +50 -0
  24. data/lib/murlsh/markup.rb +1 -1
  25. data/lib/murlsh/plugin.rb +5 -0
  26. data/lib/murlsh/podcast_server.rb +44 -0
  27. data/lib/murlsh/pop_server.rb +78 -0
  28. data/lib/murlsh/random_server.rb +41 -0
  29. data/lib/murlsh/rss_body.rb +46 -0
  30. data/lib/murlsh/rss_server.rb +38 -0
  31. data/lib/murlsh/search_conditions.rb +2 -2
  32. data/lib/murlsh/uri_ask.rb +2 -2
  33. data/lib/murlsh/url_body.rb +21 -6
  34. data/lib/murlsh/url_result_set.rb +2 -2
  35. data/lib/murlsh/url_server.rb +19 -16
  36. data/lib/murlsh/write_ordered_hash.rb +17 -0
  37. data/lib/murlsh.rb +13 -2
  38. data/murlsh.gemspec +41 -194
  39. data/plugins/add_post_60_notify_hubs.rb +3 -2
  40. data/plugins/add_pre_30_unajax_twitter.rb +1 -1
  41. data/plugins/add_pre_40_thumbnail_shortcuts.rb +23 -0
  42. data/plugins/add_pre_45_supplied_thumbnail.rb +4 -9
  43. data/plugins/add_pre_50_media_thumbnail.rb +4 -9
  44. data/plugins/add_pre_50_open_graph_image.rb +4 -8
  45. data/plugins/add_pre_60_github_title.rb +1 -1
  46. data/plugins/add_pre_65_html_thumb.rb +3 -8
  47. data/plugins/add_pre_65_img_thumb.rb +4 -9
  48. data/plugins/avatar_50_gravatar.rb +2 -1
  49. data/plugins/store_asset_40_s3.rb +40 -0
  50. data/plugins/store_asset_50_local.rb +22 -0
  51. data/public/js/js.js +0 -7
  52. data/spec/auth_spec.rb +7 -0
  53. data/spec/cat_files_spec.rb +49 -0
  54. data/spec/img_store_spec.rb +24 -8
  55. metadata +119 -76
  56. data/VERSION +0 -1
  57. data/lib/murlsh/build_md5.rb +0 -12
  58. data/lib/murlsh/head_from_get.rb +0 -15
  59. data/plugins/add_post_50_update_feed.rb +0 -84
  60. data/plugins/add_post_50_update_m3u.rb +0 -35
  61. data/plugins/add_post_50_update_podcast.rb +0 -44
  62. data/plugins/add_post_50_update_rss.rb +0 -51
  63. data/plugins/add_pre_60_s3_image.rb +0 -35
@@ -0,0 +1,38 @@
1
+ require 'uri'
2
+
3
+ require 'rack'
4
+
5
+ require 'murlsh'
6
+
7
+ module Murlsh
8
+
9
+ # Serve Atom feed.
10
+ class AtomServer
11
+
12
+ def initialize(config); @config = config; end
13
+
14
+ # Respond to a GET request for Atom feed.
15
+ def get(req)
16
+ conditions = Murlsh::SearchConditions.new(req['q']).conditions
17
+ page = 1
18
+ per_page = config.fetch('num_posts_feed', 25)
19
+
20
+ result_set = Murlsh::UrlResultSet.new(conditions, page, per_page)
21
+ urls = result_set.results
22
+
23
+ feed_url = URI.join(config.fetch('root_url'), config.fetch('feed_file'))
24
+ body = Murlsh::AtomBody.new(config, req, feed_url, urls)
25
+
26
+ resp = Rack::Response.new(body, 200,
27
+ 'Cache-Control' => 'must-revalidate, max-age=0',
28
+ 'Content-Type' => 'application/atom+xml')
29
+ if u = body.updated
30
+ resp['Last-Modified'] = u.httpdate
31
+ end
32
+ resp
33
+ end
34
+
35
+ attr_reader :config
36
+ end
37
+
38
+ end
data/lib/murlsh/auth.rb CHANGED
@@ -44,6 +44,18 @@ module Murlsh
44
44
  end
45
45
  end
46
46
 
47
+ # Look up a user by email address.
48
+ #
49
+ # Return nil if not found.
50
+ def by_email(email)
51
+ hash = Digest::MD5.hexdigest(email)
52
+
53
+ self.class.csv_iter(@file) do |row|
54
+ return { :name => row[0], :email => row[1] } if hash == row[1]
55
+ end
56
+
57
+ end
58
+
47
59
  end
48
60
 
49
61
  end
@@ -4,7 +4,7 @@ module Murlsh
4
4
 
5
5
  # Query string builder. Takes hash of query string variables.
6
6
  def build_query(h)
7
- h.empty? ? '' : '?' + h.map { |k,v| URI.escape "#{k}=#{v}" }.join('&')
7
+ h.empty? ? '' : '?' + h.map { |k,v| URI.escape("#{k}=#{v}") }.join('&')
8
8
  end
9
9
 
10
10
  end
@@ -0,0 +1,17 @@
1
+ module Murlsh
2
+
3
+ module_function
4
+
5
+ # Concatenate some files and return the result as a string.
6
+ def cat_files(files, sep=nil)
7
+ result = ''
8
+ files.each do |fname|
9
+ open(fname) do |h|
10
+ while (line = h.gets) do; result << line; end
11
+ result << sep if sep
12
+ end
13
+ end
14
+ result
15
+ end
16
+
17
+ end
@@ -1,4 +1,5 @@
1
1
  require 'open-uri'
2
+ require 'time'
2
3
  require 'uri'
3
4
 
4
5
  require 'nokogiri'
@@ -14,25 +14,34 @@ module Murlsh
14
14
  def initialize(config)
15
15
  @config = config
16
16
 
17
- url_server = Murlsh::UrlServer.new(config)
17
+ atom_server = Murlsh::AtomServer.new(config)
18
18
  json_server = Murlsh::JsonServer.new(config)
19
+ m3u_server = Murlsh::M3uServer.new(config)
20
+ podcast_server = Murlsh::PodcastServer.new(config)
21
+ pop_server = Murlsh::PopServer.new(config)
22
+ random_server = Murlsh::RandomServer.new(config)
23
+ rss_server = Murlsh::RssServer.new(config)
24
+ url_server = Murlsh::UrlServer.new(config)
25
+
19
26
  root_path = URI(config.fetch('root_url')).path
20
27
 
21
28
  @routes = [
22
- [%r{^HEAD #{root_path}(url)?$}, url_server.method(:head)],
23
- [%r{^GET #{root_path}(url)?$}, url_server.method(:get)],
29
+ [%r{^(?:HEAD|GET) #{root_path}atom\.atom$}, atom_server.method(:get)],
30
+ [%r{^(?:HEAD|GET) #{root_path}json\.json$}, json_server.method(:get)],
31
+ [%r{^(?:HEAD|GET) #{root_path}m3u\.m3u$}, m3u_server.method(:get)],
32
+ [%r{^(?:HEAD|GET) #{root_path}podcast\.rss$}, podcast_server.method(:get)],
33
+ [%r{^POST #{root_path}pop$}, pop_server.method(:post)],
34
+ [%r{^(?:HEAD|GET) #{root_path}random$}, random_server.method(:get)],
35
+ [%r{^(?:HEAD|GET) #{root_path}rss\.rss$}, rss_server.method(:get)],
36
+ [%r{^(?:HEAD|GET) #{root_path}(url)?$}, url_server.method(:get)],
24
37
  [%r{^POST #{root_path}(url)?$}, url_server.method(:post)],
25
- [%r{^HEAD #{root_path}json\.json$}, json_server.method(:head)],
26
- [%r{^GET #{root_path}json\.json$}, json_server.method(:get)],
27
38
  ]
28
39
 
29
40
  db_init
30
41
  end
31
42
 
32
43
  def db_init
33
- ActiveRecord::Base.establish_connection(
34
- :adapter => 'sqlite3', :database => @config.fetch('db_file'))
35
-
44
+ ActiveRecord::Base.establish_connection config.fetch('db')
36
45
  ActiveRecord::Base.default_timezone = :utc
37
46
  ActiveRecord::Base.include_root_in_json = false
38
47
  # ActiveRecord::Base.logger = Logger.new(STDERR)
@@ -55,13 +64,18 @@ module Murlsh
55
64
 
56
65
  # Called if the request is not found.
57
66
  def not_found(req)
58
- Rack::Response.new "<p>#{req.url} not found</p>
67
+ if req.head?
68
+ Rack::Response.new([], 404)
69
+ else
70
+ Rack::Response.new("<p>#{req.url} not found</p>
59
71
 
60
- <p><a href=\"#{@config['root_url']}\">root<a></p>
72
+ <p><a href=\"#{config.fetch('root_url')}\">root<a></p>
61
73
  ",
62
- 404, { 'Content-Type' => 'text/html' }
74
+ 404, { 'Content-Type' => 'text/html' })
75
+ end
63
76
  end
64
77
 
78
+ attr_reader :config
65
79
  attr_accessor :routes
66
80
  end
67
81
 
@@ -14,7 +14,7 @@ module Murlsh
14
14
  headers = Rack::Utils::HeaderHash.new(headers)
15
15
 
16
16
  if headers['ETag']
17
- headers['ETag'].sub!(/(")?$/, "#{headers['Content-Encoding']}\\1")
17
+ headers['ETag'].sub! /(")?$/, "#{headers['Content-Encoding']}\\1"
18
18
  end
19
19
 
20
20
  [status, headers, body]
@@ -0,0 +1,36 @@
1
+ module Murlsh
2
+
3
+ # Feed body mixin.
4
+ module FeedBody
5
+
6
+ # Content types to add an enclosure for.
7
+ EnclosureContentTypes = %w{
8
+ application/pdf
9
+ audio/mpeg
10
+ image/gif
11
+ image/jpeg
12
+ image/png
13
+ }
14
+
15
+ def initialize(config, req, feed_url, urls)
16
+ @config, @req, @feed_url, @urls = config, req, feed_url, urls
17
+ @updated = nil
18
+ end
19
+
20
+ # Yield body for Rack.
21
+ def each; yield build; end
22
+
23
+ # Build feed title.
24
+ def feed_title
25
+ result = "#{config['page_title']}"
26
+ req['q'] ? "#{result} /#{req['q']}" : result
27
+ end
28
+
29
+ attr_reader :config
30
+ attr_reader :req
31
+ attr_reader :feed_url
32
+ attr_reader :urls
33
+ attr_reader :updated
34
+ end
35
+
36
+ end
@@ -1,7 +1,5 @@
1
- require 'cgi'
2
1
  require 'digest/md5'
3
2
  require 'open-uri'
4
- require 'uri'
5
3
 
6
4
  require 'RMagick'
7
5
 
@@ -9,66 +7,67 @@ require 'murlsh'
9
7
 
10
8
  module Murlsh
11
9
 
12
- # Fetch images from urls and store them locally.
10
+ # Store images from various sources in asset storage.
11
+ #
12
+ # Storage is determined by store_asset plugins.
13
13
  class ImgStore
14
14
 
15
- # Fetch images from urls and store them locally.
16
- # Options:
17
- # * :user_agent - user agent to send with http requests
18
- def initialize(storage_dir, options={})
19
- @storage_dir = storage_dir
20
- @user_agent = options[:user_agent]
15
+ # Store images from various sources in asset storage.
16
+ #
17
+ # Storage is determined by store_asset plugins.
18
+ def initialize(config)
19
+ @config = config
21
20
  end
22
21
 
23
22
  # Build headers to send with request.
24
23
  def headers
25
24
  result = {}
26
- result['User-Agent'] = @user_agent if @user_agent
25
+ result['User-Agent'] = config['user_agent'] if config['user_agent']
27
26
  result
28
27
  end
29
28
 
30
- # Fetch an image from a url and store it locally.
31
- #
32
- # The filename will be the md5sum of the contents plus the correct
33
- # extension.
29
+ # Fetch an image from a url and store it in asset storage.
34
30
  #
35
31
  # If a block is given the Magick::ImageList created will be yielded
36
32
  # before storage.
33
+ #
34
+ # Returns image url.
37
35
  def store_url(url, &block)
38
- open(url, headers) { |fin| store_img_data fin.read, &block }
36
+ open(url, headers) { |fin| store_img_data(fin.read, &block) }
39
37
  end
40
38
 
41
- # Accept a blob of image data and store it locally.
42
- #
43
- # The filename will be the md5sum of the contents plus the correct
44
- # extension.
39
+ # Store a blob of image data in asset storage.
45
40
  #
46
41
  # If a block is given the Magick::ImageList created will be yielded
47
42
  # before storage.
43
+ #
44
+ # Returns image url.
48
45
  def store_img_data(img_data, &block)
49
46
  img = Magick::ImageList.new.from_blob(img_data)
50
47
  yield img if block_given?
51
48
  store_img img
52
49
  end
53
50
 
54
- # Accept a Magick::ImageList and store it locally.
51
+ # Store a Magick::ImageList in asset storage.
55
52
  #
56
- # The filename will be the md5sum of the contents plus the correct
57
- # extension.
53
+ # Returns image url.
58
54
  def store_img(img)
59
55
  img.extend(Murlsh::ImageList) unless img.is_a?(Murlsh::ImageList)
60
56
  img_data = img.to_blob
61
57
  md5 = Digest::MD5.hexdigest(img_data)
62
58
 
63
- local_file = "#{md5}#{img.preferred_extension}"
64
- local_path = File.join(storage_dir, local_file)
65
- unless File.exists?(local_path)
66
- Murlsh::openlock(local_path, 'w') { |fout| fout.write img_data }
59
+ name = "img/thumb/#{md5}#{img.preferred_extension}"
60
+
61
+ Murlsh::Plugin.hooks('store_asset') do |p|
62
+ # run until one returns something
63
+ if url = p.run(name, img_data, config)
64
+ return url
65
+ end
67
66
  end
68
- local_file
67
+ nil
69
68
  end
70
69
 
71
- attr_reader :storage_dir
70
+ attr_reader :config
72
71
  end
73
72
 
74
73
  end
@@ -18,6 +18,7 @@ module Murlsh
18
18
  Rakefile
19
19
  config.ru
20
20
  config.yaml
21
+ db/
21
22
  plugins/
22
23
  public/
23
24
  }.map { |x| File.join(MurlshRoot, x) }, dest_dir, :verbose => true)
@@ -4,7 +4,6 @@ module Murlsh
4
4
 
5
5
  # Recent urls json response builder.
6
6
  class JsonBody
7
- include Murlsh::BuildMd5
8
7
 
9
8
  def initialize(config, req, result_set)
10
9
  @config, @req, @result_set = config, req, result_set
@@ -19,6 +18,10 @@ module Murlsh
19
18
  @body
20
19
  else
21
20
  urls = @result_set.results.map do |mu|
21
+ Murlsh::Plugin.hooks('url_display_pre') do |p|
22
+ p.run mu, @req, @config
23
+ end
24
+
22
25
  h = mu.attributes
23
26
 
24
27
  h['title'] = mu.title_stripped
@@ -26,7 +29,7 @@ module Murlsh
26
29
  # add site root url to relative thumbnail urls
27
30
  if h['thumbnail_url'] and
28
31
  not URI(h['thumbnail_url']).scheme.to_s.downcase[/https?/]
29
- h['thumbnail_url'] = URI.join(@config['root_url'],
32
+ h['thumbnail_url'] = URI.join(@config.fetch('root_url'),
30
33
  h['thumbnail_url']).to_s
31
34
  end
32
35
 
@@ -5,8 +5,6 @@ module Murlsh
5
5
  # Serve most recent urls in json and jsonp.
6
6
  class JsonServer
7
7
 
8
- include Murlsh::HeadFromGet
9
-
10
8
  def initialize(config); @config = config; end
11
9
 
12
10
  # Respond to a GET request. Return json of recent urls or jsonp if
@@ -14,26 +12,24 @@ module Murlsh
14
12
  def get(req)
15
13
  conditions = Murlsh::SearchConditions.new(req['q']).conditions
16
14
  page = 1
17
- per_page = @config.fetch('num_posts_feed', 25)
15
+ per_page = config.fetch('num_posts_feed', 25)
18
16
 
19
17
  result_set = Murlsh::UrlResultSet.new(conditions, page, per_page)
20
18
 
21
- resp = Rack::Response.new
22
-
23
19
  if req['callback']
24
- resp['Content-Type'] = 'application/javascript'
25
- resp.body = Murlsh::JsonpBody.new(@config, req, result_set)
20
+ content_type = 'application/javascript'
21
+ body = Murlsh::JsonpBody.new(config, req, result_set)
26
22
  else
27
- resp['Content-Type'] = 'application/json'
28
- resp.body = Murlsh::JsonBody.new(@config, req, result_set)
23
+ content_type = 'application/json'
24
+ body = Murlsh::JsonBody.new(config, req, result_set)
29
25
  end
30
26
 
31
- resp['Cache-Control'] = 'must-revalidate, max-age=0'
32
- resp['ETag'] = "\"#{resp.body.md5}\""
33
-
34
- resp
27
+ Rack::Response.new body, 200,
28
+ 'Cache-Control' => 'must-revalidate, max-age=0',
29
+ 'Content-Type' => content_type
35
30
  end
36
31
 
32
+ attr_reader :config
37
33
  end
38
34
 
39
35
  end
@@ -0,0 +1,28 @@
1
+ module Murlsh
2
+
3
+ # m3u builder.
4
+ class M3uBody
5
+ include Murlsh::FeedBody
6
+
7
+ # m3u builder.
8
+ def build
9
+ if defined?(@body)
10
+ @body
11
+ else
12
+ result = "# #{feed_url}\r\n\r\n"
13
+ urls.each do |mu|
14
+ Murlsh::Plugin.hooks('url_display_pre') do |p|
15
+ p.run mu, req, config
16
+ end
17
+
18
+ result << "#{mu.url}\r\n"
19
+ @updated = @updated ? [@updated, mu.time].max : mu.time
20
+ end
21
+
22
+ @body = result
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,50 @@
1
+ require 'uri'
2
+
3
+ require 'rack'
4
+
5
+ require 'murlsh'
6
+
7
+ module Murlsh
8
+
9
+ # Serve m3u file of audio urls.
10
+ class M3uServer
11
+
12
+ AudioContentTypes = %w{
13
+ application/ogg
14
+ audio/mpeg
15
+ audio/ogg
16
+ }
17
+
18
+ def initialize(config); @config = config; end
19
+
20
+ # Respond to a GET request for m3u file.
21
+ def get(req)
22
+ conditions = ['content_type IN (?)', AudioContentTypes]
23
+ search_conditions = Murlsh::SearchConditions.new(req['q']).conditions
24
+ unless search_conditions.empty?
25
+ conditions[0] << " AND (#{search_conditions[0]})"
26
+ conditions.push(*search_conditions[1..-1])
27
+ end
28
+
29
+ page = 1
30
+ per_page = config.fetch('num_posts_feed', 25)
31
+
32
+ result_set = Murlsh::UrlResultSet.new(conditions, page, per_page)
33
+ urls = result_set.results
34
+
35
+ feed_url = URI.join(config.fetch('root_url'), 'm3u.m3u')
36
+ body = Murlsh::M3uBody.new(config, req, feed_url, urls)
37
+
38
+ resp = Rack::Response.new(body, 200,
39
+ 'Cache-Control' => 'must-revalidate, max-age=0',
40
+ 'Content-Type' => 'audio/x-mpegurl')
41
+ if u = body.updated
42
+ resp['Last-Modified'] = u.httpdate
43
+ end
44
+ resp
45
+ end
46
+
47
+ attr_reader :config
48
+ end
49
+
50
+ end
data/lib/murlsh/markup.rb CHANGED
@@ -70,7 +70,7 @@ module Murlsh
70
70
  if options[:id]
71
71
  if options[:label]
72
72
  label_suffix = options[:label_suffix] || ':'
73
- label("#{options[:label]}#{label_suffix}", :for => options[:id])
73
+ label "#{options[:label]}#{label_suffix}", :for => options[:id]
74
74
  end
75
75
  options[:name] ||= options[:id]
76
76
  end
data/lib/murlsh/plugin.rb CHANGED
@@ -9,8 +9,13 @@ module Murlsh
9
9
  # run arguments (config hash)
10
10
  # * avatar - called to get an avatar url from an email md5 sum
11
11
  # run arguments (avatar url, url, config hash)
12
+ # * store_asset - store an asset somewhere where it can be loaded by url
13
+ # run arguments (name, data, config hash)
12
14
  # * url_display_add - called to display additional information after urls
13
15
  # run arguments (markup builder, url, config hash)
16
+ # * url_display_pre - called to modify a url on-the-fly before display, does
17
+ # not change database
18
+ # run arguments (url, rack request, config hash)
14
19
  class Plugin
15
20
 
16
21
  # Called when a plugin class inherits from this class (the way plugins
@@ -0,0 +1,44 @@
1
+ require 'uri'
2
+
3
+ require 'rack'
4
+
5
+ require 'murlsh'
6
+
7
+ module Murlsh
8
+
9
+ # Serve podcast RSS feed.
10
+ class PodcastServer
11
+
12
+ def initialize(config); @config = config; end
13
+
14
+ # Respond to a GET request for podcast RSS feed.
15
+ def get(req)
16
+ conditions = ['content_type = ?', 'audio/mpeg']
17
+ search_conditions = Murlsh::SearchConditions.new(req['q']).conditions
18
+ unless search_conditions.empty?
19
+ conditions[0] << " AND (#{search_conditions[0]})"
20
+ conditions.push(*search_conditions[1..-1])
21
+ end
22
+
23
+ page = 1
24
+ per_page = config.fetch('num_posts_feed', 25)
25
+
26
+ result_set = Murlsh::UrlResultSet.new(conditions, page, per_page)
27
+ urls = result_set.results
28
+
29
+ feed_url = URI.join(config.fetch('root_url'), 'podcast.rss')
30
+ body = Murlsh::RssBody.new(config, req, feed_url, urls)
31
+
32
+ resp = Rack::Response.new(body, 200,
33
+ 'Cache-Control' => 'must-revalidate, max-age=0',
34
+ 'Content-Type' => 'application/rss+xml')
35
+ if u = body.updated
36
+ resp['Last-Modified'] = u.httpdate
37
+ end
38
+ resp
39
+ end
40
+
41
+ attr_reader :config
42
+ end
43
+
44
+ end
@@ -0,0 +1,78 @@
1
+ require 'net/pop'
2
+
3
+ require 'active_record'
4
+ require 'json'
5
+ require 'postrank-uri'
6
+ require 'rack'
7
+ require 'rmail'
8
+
9
+ module Murlsh
10
+
11
+ # Pop mail from a pop3 server and add all urls in messages from murlsh users.
12
+ class PopServer
13
+
14
+ def initialize(config); @config = config; end
15
+
16
+ def post(req)
17
+ response_body = []
18
+
19
+ if req['secret'] == config['pop_secret']
20
+ Net::POP3.enable_ssl
21
+ Net::POP3.start(config.fetch('pop_server'), config.fetch('pop_port'),
22
+ config.fetch('pop_user'), config.fetch('pop_password')) do |pop|
23
+ pop.each_mail do |mail|
24
+ begin
25
+ response_body << process_mail(mail.pop)
26
+ rescue Exception
27
+ ensure
28
+ mail.delete
29
+ end
30
+ end
31
+ pop.finish
32
+ end
33
+ end
34
+
35
+ Rack::Response.new response_body.to_json, 200,
36
+ 'Content-Type' => 'application/json'
37
+ end
38
+
39
+ # Authenticate the sender and add all urls extracted from the the email
40
+ # body.
41
+ def process_mail(mail)
42
+ parsed_mail = parse_mail(mail)
43
+
44
+ if user = Murlsh::Auth.new(config.fetch('auth_file')).by_email(
45
+ parsed_mail[:from])
46
+ parsed_mail[:uris].each do |uri|
47
+ mu = Murlsh::Url.new do |u|
48
+ u.url = uri
49
+ u.email = user[:email]
50
+ u.name = user[:name]
51
+ u.time = parsed_mail[:date]
52
+ end
53
+
54
+ # validate before add_pre plugins have run and also after (in save!)
55
+ raise ActiveRecord::RecordInvalid.new(mu) unless mu.valid?
56
+ Murlsh::Plugin.hooks('add_pre') { |p| p.run mu, config }
57
+ mu.save!
58
+ Murlsh::Plugin.hooks('add_post') { |p| p.run mu, config }
59
+ end
60
+ end
61
+ parsed_mail
62
+ end
63
+
64
+ # Parse date, from address and urls found in body from an email message.
65
+ def parse_mail(mail)
66
+ parsed_mail = RMail::Parser.read(mail.gsub(/\r/, ''))
67
+
68
+ {
69
+ :date => parsed_mail.header.date.utc,
70
+ :from => parsed_mail.header.from.first.address.downcase,
71
+ :uris => PostRank::URI.extract(parsed_mail.body),
72
+ }
73
+ end
74
+
75
+ attr_reader :config
76
+ end
77
+
78
+ end
@@ -0,0 +1,41 @@
1
+ require 'rack'
2
+
3
+ require 'murlsh'
4
+
5
+ module Murlsh
6
+
7
+ # Redirect to a random url from the database.
8
+ class RandomServer
9
+
10
+ def initialize(config); @config = config; end
11
+
12
+ # Redirect to a random url from the database optionally matching a query.
13
+ #
14
+ # Redirect to root url if no urls match.
15
+ def get(req)
16
+ if choice = random_url(Murlsh::SearchConditions.new(req['q']).conditions)
17
+ url = choice.url
18
+ else
19
+ url = config.fetch('root_url')
20
+ end
21
+
22
+ resp = Rack::Response.new("<a href=\"#{url}\">#{url}</a>")
23
+ resp.redirect(url)
24
+
25
+ resp
26
+ end
27
+
28
+ # Select a random url from the database optionally matching a query.
29
+ #
30
+ # Return nil if no urls match.
31
+ def random_url(conditions=[])
32
+ count = Murlsh::Url.count(:conditions => conditions)
33
+ if count > 0
34
+ Murlsh::Url.first(:conditions => conditions, :offset => rand(count))
35
+ end
36
+ end
37
+
38
+ attr_reader :config
39
+ end
40
+
41
+ end