cortex-reaver 0.1.0 → 0.2.0

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