cortex-reaver 0.1.0 → 0.2.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 (141) hide show
  1. data/README +1 -1
  2. data/bin/cortex_reaver +3 -4
  3. data/lib/cortex_reaver.rb +270 -110
  4. data/lib/cortex_reaver/cache.rb +23 -0
  5. data/lib/cortex_reaver/config.rb +178 -74
  6. data/lib/cortex_reaver/controller/admin.rb +64 -4
  7. data/lib/cortex_reaver/controller/comment.rb +4 -4
  8. data/lib/cortex_reaver/controller/controller.rb +3 -3
  9. data/lib/cortex_reaver/controller/journal.rb +4 -2
  10. data/lib/cortex_reaver/controller/main.rb +117 -26
  11. data/lib/cortex_reaver/controller/page.rb +7 -0
  12. data/lib/cortex_reaver/controller/photograph.rb +12 -10
  13. data/lib/cortex_reaver/controller/tag.rb +1 -1
  14. data/lib/cortex_reaver/controller/user.rb +7 -2
  15. data/lib/cortex_reaver/helper/attachments.rb +2 -2
  16. data/lib/cortex_reaver/helper/crud.rb +7 -7
  17. data/lib/cortex_reaver/helper/feeds.rb +56 -38
  18. data/lib/cortex_reaver/helper/form.rb +12 -16
  19. data/lib/cortex_reaver/helper/navigation.rb +35 -16
  20. data/lib/cortex_reaver/helper/photographs.rb +2 -2
  21. data/lib/cortex_reaver/helper/sidebar.rb +44 -0
  22. data/lib/cortex_reaver/helper/tags.rb +32 -9
  23. data/lib/cortex_reaver/helper/workflow.rb +2 -14
  24. data/lib/cortex_reaver/layout/blank.rhtml +27 -31
  25. data/lib/cortex_reaver/layout/text.rhtml +54 -67
  26. data/lib/cortex_reaver/migrations/014_convert_projects_to_pages.rb +79 -0
  27. data/lib/cortex_reaver/model/comment.rb +7 -8
  28. data/lib/cortex_reaver/model/page.rb +5 -3
  29. data/lib/cortex_reaver/model/photograph.rb +57 -13
  30. data/lib/cortex_reaver/model/tag.rb +1 -4
  31. data/lib/cortex_reaver/model/user.rb +6 -3
  32. data/lib/cortex_reaver/plugin.rb +1 -1
  33. data/lib/cortex_reaver/plugins/twitter.rb +185 -0
  34. data/lib/cortex_reaver/public/css/actions.css +31 -0
  35. data/lib/cortex_reaver/public/css/admin.css +50 -27
  36. data/lib/cortex_reaver/public/css/attachments.css +11 -0
  37. data/lib/cortex_reaver/public/css/autotags.css +38 -0
  38. data/lib/cortex_reaver/public/css/code.css +10 -2
  39. data/lib/cortex_reaver/public/css/colophon.css +10 -0
  40. data/lib/cortex_reaver/public/css/commments.css +7 -0
  41. data/lib/cortex_reaver/public/css/custom.css +1 -0
  42. data/lib/cortex_reaver/public/css/flash.css +17 -0
  43. data/lib/cortex_reaver/public/css/fonts.css +22 -0
  44. data/lib/cortex_reaver/public/css/form.css +15 -3
  45. data/lib/cortex_reaver/public/css/generics.css +39 -0
  46. data/lib/cortex_reaver/public/css/icons.css +4 -0
  47. data/lib/cortex_reaver/public/css/journals.css +3 -0
  48. data/lib/cortex_reaver/public/css/main.css +30 -312
  49. data/lib/cortex_reaver/public/css/pagination.css +50 -0
  50. data/lib/cortex_reaver/public/css/photo-show.css +87 -0
  51. data/lib/cortex_reaver/public/css/photo.css +28 -97
  52. data/lib/cortex_reaver/public/css/progress.css +35 -0
  53. data/lib/cortex_reaver/public/css/sidebar.css +83 -0
  54. data/lib/cortex_reaver/public/css/table-of-contents.css +26 -0
  55. data/lib/cortex_reaver/public/css/table.css +3 -0
  56. data/lib/cortex_reaver/public/css/tags.css +29 -0
  57. data/lib/cortex_reaver/public/css/text.css +19 -1
  58. data/lib/cortex_reaver/public/css/top_actions.css +50 -0
  59. data/lib/cortex_reaver/public/css/users.css +3 -0
  60. data/lib/cortex_reaver/public/images/admin/icons.png +0 -0
  61. data/lib/cortex_reaver/public/images/admin/icons.xcf +0 -0
  62. data/lib/cortex_reaver/public/images/background_tile.png +0 -0
  63. data/lib/cortex_reaver/public/images/edit_34.png +0 -0
  64. data/lib/cortex_reaver/public/images/edit_34_prelight.png +0 -0
  65. data/lib/cortex_reaver/public/images/elided.png +0 -0
  66. data/lib/cortex_reaver/public/images/grid_34.png +0 -0
  67. data/lib/cortex_reaver/public/images/grid_34_prelight.png +0 -0
  68. data/lib/cortex_reaver/public/images/next_11.png +0 -0
  69. data/lib/cortex_reaver/public/images/next_34.png +0 -0
  70. data/lib/cortex_reaver/public/images/next_34_prelight.png +0 -0
  71. data/lib/cortex_reaver/public/images/prev_11.png +0 -0
  72. data/lib/cortex_reaver/public/images/prev_34.png +0 -0
  73. data/lib/cortex_reaver/public/images/prev_34_prelight.png +0 -0
  74. data/lib/cortex_reaver/public/js/admin.js +15 -22
  75. data/lib/cortex_reaver/public/js/autotags.js +120 -0
  76. data/lib/cortex_reaver/public/js/jquery.autocomplete.js +135 -176
  77. data/lib/cortex_reaver/public/js/jquery.color.js +124 -0
  78. data/lib/cortex_reaver/public/js/jquery.corners.min.js +7 -0
  79. data/lib/cortex_reaver/public/js/jquery.hotkeys-0.7.9.js +244 -0
  80. data/lib/cortex_reaver/public/js/jquery.js +4361 -4
  81. data/lib/cortex_reaver/public/js/jquery.periodicalupdater.js +98 -0
  82. data/lib/cortex_reaver/public/js/photo.js +3 -32
  83. data/lib/cortex_reaver/public/robots.txt +3 -0
  84. data/lib/cortex_reaver/snippets/ramaze/cache/memcached.rb +13 -10
  85. data/lib/cortex_reaver/snippets/range.rb +9 -0
  86. data/lib/cortex_reaver/support/attachments.rb +12 -0
  87. data/lib/cortex_reaver/support/comments.rb +1 -3
  88. data/lib/cortex_reaver/support/renderer.rb +20 -17
  89. data/lib/cortex_reaver/support/sequenceable.rb +6 -6
  90. data/lib/cortex_reaver/support/tags.rb +15 -7
  91. data/lib/cortex_reaver/version.rb +1 -1
  92. data/lib/cortex_reaver/view/admin/configuration.rhtml +7 -0
  93. data/lib/cortex_reaver/view/admin/index.rhtml +3 -0
  94. data/lib/cortex_reaver/view/admin/regenerate_photo_sizes.rhtml +16 -0
  95. data/lib/cortex_reaver/view/adminbox.rhtml +34 -44
  96. data/lib/cortex_reaver/view/comments/comment.rhtml +1 -1
  97. data/lib/cortex_reaver/view/comments/form.rhtml +1 -2
  98. data/lib/cortex_reaver/view/comments/post_form.rhtml +2 -10
  99. data/lib/cortex_reaver/view/head.rhtml +11 -0
  100. data/lib/cortex_reaver/view/journals/journal.rhtml +3 -3
  101. data/lib/cortex_reaver/view/journals/show.rhtml +0 -4
  102. data/lib/cortex_reaver/view/js.rhtml +1 -0
  103. data/lib/cortex_reaver/view/pages/list.rhtml +3 -23
  104. data/lib/cortex_reaver/view/pages/row.rhtml +13 -0
  105. data/lib/cortex_reaver/view/photographs/grid.rhtml +30 -36
  106. data/lib/cortex_reaver/view/photographs/show.rhtml +42 -108
  107. data/lib/cortex_reaver/view/sidebar/explore_photos.rhtml +7 -0
  108. data/lib/cortex_reaver/view/sidebar/photographs.rhtml +15 -0
  109. data/lib/cortex_reaver/view/sidebar/sections.rhtml +4 -0
  110. data/lib/cortex_reaver/view/sidebar/twitter.rhtml +12 -0
  111. data/lib/cortex_reaver/view/tags/show.rhtml +0 -10
  112. data/lib/cortex_reaver/view/tracker.rhtml +0 -0
  113. data/lib/cortex_reaver/view/users/list.rhtml +1 -7
  114. data/lib/cortex_reaver/view/users/login.rhtml +1 -1
  115. metadata +103 -43
  116. data/lib/cortex_reaver/controller/project.rb +0 -53
  117. data/lib/cortex_reaver/helper/template.rb +0 -37
  118. data/lib/cortex_reaver/model/project.rb +0 -57
  119. data/lib/cortex_reaver/public/css/ramaze_error.css +0 -90
  120. data/lib/cortex_reaver/public/images/atom-xml-icon.png +0 -0
  121. data/lib/cortex_reaver/public/images/body.png +0 -0
  122. data/lib/cortex_reaver/public/images/border_bottom.png +0 -0
  123. data/lib/cortex_reaver/public/images/border_bottom_left.png +0 -0
  124. data/lib/cortex_reaver/public/images/border_bottom_right.png +0 -0
  125. data/lib/cortex_reaver/public/images/border_left.png +0 -0
  126. data/lib/cortex_reaver/public/images/border_right.png +0 -0
  127. data/lib/cortex_reaver/public/images/border_top.png +0 -0
  128. data/lib/cortex_reaver/public/images/border_top_left.png +0 -0
  129. data/lib/cortex_reaver/public/images/border_top_right.png +0 -0
  130. data/lib/cortex_reaver/public/images/header.png +0 -0
  131. data/lib/cortex_reaver/public/images/header.xcf +0 -0
  132. data/lib/cortex_reaver/public/images/indicator.gif +0 -0
  133. data/lib/cortex_reaver/public/images/rss-xml-icon.png +0 -0
  134. data/lib/cortex_reaver/public/images/sections.png +0 -0
  135. data/lib/cortex_reaver/public/images/sections_highlight.png +0 -0
  136. data/lib/cortex_reaver/public/js/jquery.autocompletefb.js +0 -125
  137. data/lib/cortex_reaver/view/photographs/sidebar.rhtml +0 -7
  138. data/lib/cortex_reaver/view/projects/form.rhtml +0 -14
  139. data/lib/cortex_reaver/view/projects/list.rhtml +0 -31
  140. data/lib/cortex_reaver/view/projects/show.rhtml +0 -42
  141. data/lib/proto/cortex_reaver.yaml +0 -47
@@ -0,0 +1,79 @@
1
+ module CortexReaver
2
+ class ProjectsToPagesSchema < Sequel::Migration
3
+
4
+ def down
5
+ # Nope. Not even going to try.
6
+ end
7
+
8
+ def up
9
+ pages = self[:pages]
10
+ projects = self[:projects]
11
+
12
+ # Create projects page
13
+ unless root = pages.filter(:name => 'projects').first
14
+ pages.insert(
15
+ :name => 'projects',
16
+ :title => 'Projects',
17
+ :created_on => Time.now,
18
+ :updated_on => Time.now
19
+ )
20
+ end
21
+ root = pages.filter(:name => 'projects').first
22
+
23
+ # Migrate each project
24
+ projects.all.each do |project|
25
+ project_id = project[:id]
26
+
27
+ # Create page
28
+ project.delete :description
29
+ project.delete :id
30
+ project[:page_id] = root[:id]
31
+ pages << project
32
+
33
+ # Get page id
34
+ page_id = pages.filter(:page_id => root[:id], :name => project[:name]).first[:id]
35
+
36
+ # Reparent comments
37
+ self[:comments].filter(:project_id => project_id).update(
38
+ :project_id => nil,
39
+ :page_id => page_id
40
+ )
41
+
42
+ # Copy tags
43
+ self[:projects_tags].filter(:project_id => project_id).each do |t|
44
+ self[:pages_tags] << {:page_id => page_id, :tag_id => t[:tag_id]}
45
+ end
46
+
47
+ # Move attachments
48
+ # This is a little tricky; I'd like to deprecate the entire model at
49
+ # some point. We assume it lives in config.public_dir/data/projects/id
50
+ project_dir = File.join(
51
+ CortexReaver.config.public_root, 'data', 'projects', project_id.to_s
52
+ )
53
+ page_dir = File.join(
54
+ CortexReaver.config.public_root, 'data', 'pages', page_id.to_s
55
+ )
56
+ if File.directory? project_dir
57
+ if File.directory? page_dir
58
+ puts "WARNING: #{page_dir} already exists. Not moving contents of #{project_dir} into it. You should take a look."
59
+ else
60
+ FileUtils.move project_dir, page_dir
61
+ end
62
+ end
63
+
64
+ projects.filter(:id => project_id).delete
65
+ end
66
+
67
+ # Drop associated columns
68
+ # Can't do this yet--foreign key constraints...
69
+ # alter_table :comments do
70
+ # drop_column :project_id
71
+ # end
72
+
73
+ # Drop projects table
74
+ unless table_exists? :projects
75
+ drop_table :projects
76
+ end
77
+ end
78
+ end
79
+ end
@@ -10,7 +10,6 @@ module CortexReaver
10
10
  many_to_one :creator, :class => 'CortexReaver::User', :key => 'created_by'
11
11
  many_to_one :updater, :class => 'CortexReaver::User', :key => 'updated_by'
12
12
  many_to_one :journal, :class => 'CortexReaver::Journal'
13
- many_to_one :project, :class => 'CortexReaver::Project'
14
13
  many_to_one :photograph, :class => 'CortexReaver::Photograph'
15
14
  many_to_one :page, :class => 'CortexReaver::Page'
16
15
  many_to_one :comment, :class => 'CortexReaver::Comment'
@@ -90,21 +89,21 @@ module CortexReaver
90
89
 
91
90
  def validate
92
91
  validates_presence :body
93
- validates_max_length 255, :title
94
- validates_max_length 255, :name
95
- validates_max_length 255, :http
96
- validates_max_length 255, :email
97
- validates_format(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/, :email)
92
+ validates_max_length 255, :title, :allow_blank => true
93
+ validates_max_length 255, :name, :allow_blank => true
94
+ validates_max_length 255, :http, :allow_blank => true
95
+ validates_max_length 255, :email, :allow_blank => true
96
+ validates_format(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/, :email, :allow_blank => true)
98
97
 
99
98
  # Ensure comments with an email specified do *not* conflict with another
100
99
  # user.
101
- if (not email.blank?) and User.filter(:email => value).count > 0
100
+ if (not email.blank?) and User.filter(:email => email).count > 0
102
101
  self.errors[:email] << 'conflicts with a registered user'
103
102
  end
104
103
 
105
104
  # Ensures comments belong to exactly one parent.
106
105
  count = 0
107
- [:page_id, :project_id, :journal_id, :comment_id, :photograph_id].each do |field|
106
+ [:page_id, :journal_id, :comment_id, :photograph_id].each do |field|
108
107
  unless self[field].blank?
109
108
  count += 1
110
109
  if count > 1
@@ -10,8 +10,10 @@ module CortexReaver
10
10
  plugin :viewable
11
11
  include CortexReaver::Model::Renderer
12
12
 
13
+ self.sequence_order = :name
14
+
13
15
  many_to_one :page, :class => 'CortexReaver::Page'
14
- one_to_many :pages, :class => 'CortexReaver::Page'
16
+ one_to_many :pages, :class => 'CortexReaver::Page', :order => :name
15
17
  many_to_one :creator, :class => 'CortexReaver::User', :key => 'created_by'
16
18
  many_to_one :updater, :class => 'CortexReaver::User', :key => 'updated_by'
17
19
  one_to_many :comments, :class => 'CortexReaver::Comment'
@@ -111,12 +113,12 @@ module CortexReaver
111
113
  end
112
114
 
113
115
  # Create a default page if none exists.
114
- if dataset.table_exists? and Page.count == 0
116
+ if db.table_exists?(table_name) and Page.count == 0
115
117
  Page.new(
116
118
  :name => 'about',
117
119
  :title => 'About Cortex Reaver',
118
120
  :body => <<EOF
119
- <p>Cortex Reaver is a blog engine designed for managing photographs, projects,
121
+ <p>Cortex Reaver is a blog engine designed for managing photographs, pages,
120
122
  journal entries, and more, with support for tags and comments. Cortex
121
123
  Reaver is written in <a href="http://ruby-lang.org">Ruby</a> using <a
122
124
  href="http://ramaze.net">Ramaze</a>, uses the <a
@@ -10,7 +10,7 @@ module CortexReaver
10
10
 
11
11
  # Target image sizes
12
12
  SIZES = {
13
- :thumbnail => '150x',
13
+ :thumbnail => '166x',
14
14
  :grid => '150x150',
15
15
  :small => 'x512',
16
16
  :medium => 'x768',
@@ -34,6 +34,12 @@ module CortexReaver
34
34
  reverse_order(:created_on).limit(16)
35
35
  end
36
36
 
37
+ def self.regenerate_sizes
38
+ all.each do |p|
39
+ p.regenerate_sizes
40
+ end
41
+ end
42
+
37
43
  def self.url
38
44
  '/photographs'
39
45
  end
@@ -72,19 +78,10 @@ module CortexReaver
72
78
  end
73
79
 
74
80
  # Write file to disk
75
- attachment = Attachment.new(self, 'original.jpg')
76
- attachment.file = file
77
-
78
- # Read image through ImageMagick
79
- image = Magick::Image.read(attachment.local_path).first
81
+ attachment = Attachment.new(self, 'original.jpg').file = file
80
82
 
81
- # Write appropriate sizes to disk
82
- SIZES.each do |size, geometry|
83
- image.change_geometry(geometry) do |width, height|
84
- attachment = Attachment.new(self, size.to_s + '.jpg')
85
- image.scale(width, height).write(attachment.local_path)
86
- end
87
- end
83
+ # Compute thumbnails
84
+ regenerate_sizes
88
85
 
89
86
  true
90
87
  end
@@ -111,6 +108,53 @@ module CortexReaver
111
108
  attachment(size.to_s + '.jpg').path(type)
112
109
  end
113
110
 
111
+ # Regenerates various photo sizes.
112
+ def regenerate_sizes
113
+ Ramaze::Log.info "Regenerating photo sizes for #{self}"
114
+
115
+ # Find existing attachments, in order of decreasing (roughly) size
116
+ known_files = attachments.map{|a| a.name.sub(/\.jpg$/,'')} & (SIZES.keys.map(&:to_s) + ['original'])
117
+ largest = attachment(
118
+ known_files.sort_by { |f|
119
+ File.stat(attachment("#{f}.jpg").path).size
120
+ }.last + '.jpg'
121
+ )
122
+
123
+ # Replace original.jpg with the largest available, if necessary.
124
+ orig = attachment 'original.jpg'
125
+ unless orig.exists? and orig == largest
126
+ orig.file = largest
127
+ end
128
+
129
+ # Delete everything but original.jpg
130
+ attachments.reject {|a| a.name == 'original.jpg'}.each{|a| a.delete}
131
+
132
+ # Read image through ImageMagick
133
+ begin
134
+ image = Magick::Image.read(orig.local_path).first
135
+ rescue => e
136
+ Ramaze::Log.error "Invalid image #{orig.local_path}; not processing."
137
+ return false
138
+ end
139
+
140
+ # Write appropriate sizes to disk
141
+ SIZES.each do |size, geometry|
142
+ image.change_geometry(geometry) do |width, height|
143
+ attachment = attachment(size.to_s + '.jpg')
144
+ image.scale(width, height).write(attachment.local_path)
145
+ end
146
+
147
+ end
148
+
149
+ # Yep, GC time. Gotta clear out those imagemagick stubs.
150
+ GC.start
151
+
152
+ # Free IM stubs
153
+ GC.start
154
+
155
+ true
156
+ end
157
+
114
158
  # Returns the system path to the thumbnail photograph
115
159
  def thumbnail_local_path
116
160
  path :local, 'thumbnail'
@@ -4,7 +4,6 @@ module CortexReaver
4
4
 
5
5
  many_to_many :photographs, :class => 'CortexReaver::Photograph'
6
6
  many_to_many :journals, :class => 'CortexReaver::Journal'
7
- many_to_many :projects, :class => 'CortexReaver::Project'
8
7
  many_to_many :pages, :class => 'CortexReaver::Page'
9
8
 
10
9
  subset :unused, :count => 0
@@ -23,7 +22,6 @@ module CortexReaver
23
22
 
24
23
  remove_all_photographs
25
24
  remove_all_journals
26
- remove_all_projects
27
25
  remove_all_pages
28
26
 
29
27
  true
@@ -62,8 +60,7 @@ module CortexReaver
62
60
  old_count = self[:count]
63
61
  self[:count] = photographs_dataset.count +
64
62
  journals_dataset.count +
65
- pages_dataset.count +
66
- projects_dataset.count
63
+ pages_dataset.count
67
64
 
68
65
  # Save and return
69
66
  changed = changed_columns.include? :count
@@ -9,12 +9,10 @@ module CortexReaver
9
9
  one_to_many :created_journals, :key => 'created_by', :class => 'CortexReaver::Journal'
10
10
  one_to_many :created_pages, :key => 'created_by', :class => 'CortexReaver::Page'
11
11
  one_to_many :created_photographs, :key => 'created_by', :class => 'CortexReaver::Photograph'
12
- one_to_many :created_projects, :key => 'created_by', :class => 'CortexReaver::Project'
13
12
  one_to_many :updated_comments, :key => 'updated_by', :class => 'CortexReaver::Comment'
14
13
  one_to_many :updated_journals, :key => 'updated_by', :class => 'CortexReaver::Journal'
15
14
  one_to_many :updated_pages, :key => 'updated_by', :class => 'CortexReaver::Page'
16
15
  one_to_many :updated_photographs, :key => 'updated_by', :class => 'CortexReaver::Photograph'
17
- one_to_many :updated_projects, :key => 'updated_by', :class => 'CortexReaver::Project'
18
16
 
19
17
 
20
18
  self.window_size = 64
@@ -70,6 +68,11 @@ module CortexReaver
70
68
  self[:login => id] || self[id]
71
69
  end
72
70
 
71
+ # Class URL
72
+ def self.url
73
+ '/users'
74
+ end
75
+
73
76
  # Returns true if the user is an administrator.
74
77
  def admin?
75
78
  self.admin
@@ -269,7 +272,7 @@ module CortexReaver
269
272
  public
270
273
 
271
274
  # Create default user if none exist
272
- if dataset.table_exists? and count == 0
275
+ if db.table_exists?(table_name) and count == 0
273
276
  u = User.new(
274
277
  :login => 'shodan',
275
278
  :name => 'Shodan',
@@ -1,4 +1,4 @@
1
1
  module CortexReaver
2
- module Plugin
2
+ module Plugins
3
3
  end
4
4
  end
@@ -0,0 +1,185 @@
1
+ #--
2
+ #
3
+ # Kyle Kingsbury: This plugin is adapted from Ryan's Thoth. His license follows:
4
+ #
5
+ # Copyright (c) 2009 Ryan Grove <ryan@wonko.com>
6
+ # All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions are met:
10
+ #
11
+ # * Redistributions of source code must retain the above copyright notice,
12
+ # this list of conditions and the following disclaimer.
13
+ # * Redistributions in binary form must reproduce the above copyright notice,
14
+ # this list of conditions and the following disclaimer in the documentation
15
+ # and/or other materials provided with the distribution.
16
+ # * Neither the name of this project nor the names of its contributors may be
17
+ # used to endorse or promote products derived from this software without
18
+ # specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #++
31
+
32
+ require 'json'
33
+ require 'open-uri'
34
+ require 'timeout'
35
+ require 'uri'
36
+
37
+ module CortexReaver; module Plugins
38
+
39
+ # Twitter plugin for Cortex Reaver.
40
+ module Twitter
41
+ Config = CortexReaver.config.plugins.twitter ||= Construct.new
42
+
43
+ # The twitter username to look for
44
+ Config.define :username, :default => 'aphyr_'
45
+
46
+ # Whether or not to include replies. If this is false, the most recent
47
+ # non-reply tweets will be displayed.
48
+ Config.define :include_replies, :default => false
49
+
50
+ # Time in seconds to cache results. It's a good idea to keep this nice
51
+ # and high both to improve the performance of your site and to avoid
52
+ # pounding on Twitter's servers. Default is 600 seconds (10 minutes).
53
+ Config.define :cache_ttl, :default => 600
54
+
55
+ # Request timeout in seconds.
56
+ Config.define :request_timeout, :default => 3
57
+
58
+ # If Twitter fails to respond at least this many times in a row, no new
59
+ # requests will be sent until the failure_timeout expires in order to
60
+ # avoid hindering your blog's performance.
61
+ Config.define :failure_threshold, :default => 3
62
+
63
+ # After the failure_threshold is reached, the plugin will wait this many
64
+ # seconds before trying again. Default is 600 seconds (10 minutes).
65
+ Config.define :failure_timeout, :default => 600
66
+
67
+ class << self
68
+
69
+ # Parses tweet text and converts it into HTML. Explicit URLs and @username
70
+ # or #hashtag references will be turned into links.
71
+ def parse_tweet(tweet)
72
+ index = 0
73
+ html = tweet.dup
74
+ protocols = ['ftp', 'ftps', 'git', 'http', 'https', 'mailto', 'scp',
75
+ 'sftp', 'ssh', 'telnet']
76
+ urls = []
77
+
78
+ # Extract URLs and replace them with placeholders for later.
79
+ URI.extract(html.dup, protocols) do |url|
80
+ html.sub!(url, "__URL#{index}__")
81
+ urls << url
82
+ index += 1
83
+ end
84
+
85
+ # Replace URL placeholders with links.
86
+ urls.each_with_index do |url, index|
87
+ html.sub!("__URL#{index}__", "<a href=\"#{url}\">" <<
88
+ "#{url.length > 26 ? url[0..26] + '...' : url}</a>")
89
+ end
90
+
91
+ # Turn @username into a link to the specified user's Twitter profile.
92
+ html.gsub!(/@([a-zA-Z0-9_]{1,16})([^a-zA-Z0-9_])?/,
93
+ '@<a href="http://twitter.com/\1">\1</a>\2')
94
+
95
+ # Turn #hashtags into links.
96
+ html.gsub!(/#([a-zA-Z0-9_]{1,32})([^a-zA-Z0-9_])?/,
97
+ '<a href="http://search.twitter.com/search?q=%23\1">#\1</a>\2')
98
+
99
+ return html
100
+ end
101
+
102
+ # Gets a Hash containing recent tweets for the specified _user_. The only
103
+ # valid option currently is <code>:count</code>, which specifies the
104
+ # maximum number of tweets that should be returned.
105
+ def recent_tweets(user = Config.username, options = {:count => 1})
106
+ if @skip_until
107
+ return [] if @skip_until > Time.now
108
+ @skip_until = nil
109
+ end
110
+
111
+ cache = Ramaze::Cache.plugin
112
+ options = {:count => 5}.merge(options)
113
+ count = options[:count].to_i
114
+
115
+ count += 10 unless Config.include_replies
116
+ count = 200 if count > 200
117
+
118
+ url = "http://twitter.com/statuses/user_timeline/#{user}.json?count=" <<
119
+ count.to_s
120
+
121
+ if value = cache[url]
122
+ return value
123
+ end
124
+
125
+ tweets = []
126
+
127
+ Timeout.timeout(Config.request_timeout, StandardError) do
128
+ failed = 0
129
+ begin
130
+ tweets = JSON.parse(open(url).read)
131
+ rescue JSON::ParserError => e
132
+ # Twitter likes to hand out weird HTML responses sometimes. :/
133
+ failed += 1
134
+ retry unless failed > 3
135
+
136
+ # Admit defeat
137
+ raise RuntimeError.new("Failed to parse Twitter response 4 times: #{e}")
138
+ end
139
+ end
140
+
141
+ # Weed out replies if necessary.
142
+ unless Config.include_replies
143
+ tweets.delete_if do |tweet|
144
+ !tweet['in_reply_to_status_id'].nil? ||
145
+ !tweet['in_reply_to_user_id'].nil?
146
+ end
147
+
148
+ tweets = tweets.slice(0, options[:count].to_i)
149
+ end
150
+
151
+ # Parse the tweets into an easier-to-use format.
152
+ tweets.map! do |tweet|
153
+ {
154
+ :created_at => Time.parse(tweet['created_at']),
155
+ :html => parse_tweet(tweet['text']),
156
+ :id => tweet['id'],
157
+ :source => tweet['source'],
158
+ :text => tweet['text'],
159
+ :truncated => tweet['truncated'],
160
+ :url => "http://twitter.com/#{user}/statuses/#{tweet['id']}"
161
+ }
162
+ end
163
+
164
+ @failures = 0
165
+
166
+ return cache.store(url, tweets, :ttl => Config.cache_ttl)
167
+
168
+ rescue => e
169
+ Ramaze::Log.error "CortexReaver::Plugins::Twitter: #{e.message}"
170
+
171
+ @failures ||= 0
172
+ @failures += 1
173
+
174
+ if @failures >= Config.failure_threshold
175
+ @skip_until = Time.now + Config.failure_timeout
176
+ Ramaze::Log.error "CortexReaver::Plugins::Twitter: Twitter failed to respond #{@failures} times. Will retry after #{@skip_until}."
177
+ end
178
+
179
+ return []
180
+ end
181
+
182
+ end
183
+
184
+ end
185
+ end; end