diff --git a/Gemfile b/Gemfile index 59087dbd4..fe0e640d8 100644 --- a/Gemfile +++ b/Gemfile @@ -64,6 +64,7 @@ gem 'rails_autolink', '1.1.0' gem 'redcarpet', '3.0.0' gem 'roxml', '3.1.6' gem 'ruby-oembed', '0.8.8' +gem 'opengraph', '0.0.4' # Please remove when migrating to Rails 4 diff --git a/Gemfile.lock b/Gemfile.lock index 2f89ad7a8..0f95c9437 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -249,6 +249,10 @@ GEM omniauth-twitter (1.0.0) multi_json (~> 1.3) omniauth-oauth (~> 1.0) + opengraph (0.0.4) + hashie + nokogiri (~> 1.5.0) + rest-client (~> 1.6.0) orm_adapter (0.4.0) polyglot (0.3.3) pry (0.9.12.2) @@ -322,6 +326,8 @@ GEM redis-namespace (1.3.0) redis (~> 3.0.0) remotipart (1.2.1) + rest-client (1.6.7) + mime-types (>= 1.16) rmagick (2.13.2) roxml (3.1.6) activesupport (>= 2.3.0) @@ -464,6 +470,7 @@ DEPENDENCIES omniauth-facebook (= 1.4.1) omniauth-tumblr (= 1.1) omniauth-twitter (= 1.0.0) + opengraph (= 0.0.4) rack-cors (= 0.2.8) rack-google-analytics (= 0.11.0) rack-piwik (= 0.2.2) diff --git a/app/assets/javascripts/app/helpers/open_graph.js b/app/assets/javascripts/app/helpers/open_graph.js new file mode 100644 index 000000000..cc22e312f --- /dev/null +++ b/app/assets/javascripts/app/helpers/open_graph.js @@ -0,0 +1,8 @@ +(function(){ + app.helpers.openGraph = { + html : function (open_graph_cache) { + if (!open_graph_cache) { return "" } + return '' + } + } +})(); diff --git a/app/assets/javascripts/app/views/content_view.js b/app/assets/javascripts/app/views/content_view.js index 17762e057..b175aae1c 100644 --- a/app/assets/javascripts/app/views/content_view.js +++ b/app/assets/javascripts/app/views/content_view.js @@ -44,9 +44,13 @@ app.views.Content = app.views.Base.extend({ var collHeight = 200 , elem = this.$(".collapsible") , oembed = elem.find(".oembed") + , opengraph = elem.find(".opengraph") , addHeight = 0; if($.trim(oembed.html()) != "") { - addHeight = oembed.height(); + addHeight += oembed.height(); + } + if($.trim(opengraph.html()) != "") { + addHeight += opengraph.height(); } // only collapse if height exceeds collHeight+20% @@ -102,3 +106,7 @@ app.views.OEmbed = app.views.Base.extend({ this.$el.html(insertHTML); } }); + +app.views.OpenGraph = app.views.Base.extend({ + templateName : "opengraph" +}); diff --git a/app/assets/javascripts/app/views/post/small_frame.js b/app/assets/javascripts/app/views/post/small_frame.js index fe847d45d..6920c19aa 100644 --- a/app/assets/javascripts/app/views/post/small_frame.js +++ b/app/assets/javascripts/app/views/post/small_frame.js @@ -10,7 +10,8 @@ app.views.Post.SmallFrame = app.views.Post.extend({ }, subviews : { - '.embed-frame' : "oEmbedView" + '.embed-frame' : "oEmbedView", + '.open-graph-frame' : 'openGraphView' }, initialize : function(options) { @@ -22,6 +23,10 @@ app.views.Post.SmallFrame = app.views.Post.extend({ return new app.views.OEmbed({model : this.model}) }, + openGraphView : function(){ + return new app.views.OpenGraph({model : this.model}) + }, + smallFramePresenter : function(){ //todo : we need to have something better for small frame text, probably using the headline() scenario. return _.extend(this.defaultPresenter(), @@ -50,10 +55,10 @@ app.views.Post.SmallFrame = app.views.Post.extend({ var text = this.model.get("text") , baseClass = $.trim(text).length == 0 ? "no-text" : "has-text"; - if(this.model.get("photos").length > 0 || this.model.get("o_embed_cache")) + if(this.model.get("photos").length > 0 || this.model.get("o_embed_cache") || this.model.get("open_graph_cache")) baseClass += " has-media"; - if(baseClass == "no-text" || this.model.get("photos").length > 0 || this.model.get("o_embed_cache")) { return baseClass } + if(baseClass == "no-text" || this.model.get("photos").length > 0 || this.model.get("o_embed_cache") || this.model.get("open_graph_cache")) { return baseClass } var randomColor = _.first(_.shuffle(['cyan', 'green', 'yellow', 'purple', 'lime-green', 'orange', 'red', 'turquoise', 'sand'])); diff --git a/app/assets/javascripts/app/views/stream_post_views.js b/app/assets/javascripts/app/views/stream_post_views.js index 9be5a63b8..0c8faafe7 100644 --- a/app/assets/javascripts/app/views/stream_post_views.js +++ b/app/assets/javascripts/app/views/stream_post_views.js @@ -8,6 +8,7 @@ app.views.StreamPost = app.views.Post.extend({ ".comments" : "commentStreamView", ".post-content" : "postContentView", ".oembed" : "oEmbedView", + ".opengraph" : "openGraphView", ".status-message-location" : "postLocationStreamView" }, @@ -29,6 +30,7 @@ app.views.StreamPost = app.views.Post.extend({ //subviews this.commentStreamView = new app.views.CommentStream({model : this.model}); this.oEmbedView = new app.views.OEmbed({model : this.model}); + this.openGraphView = new app.views.OpenGraph({model : this.model}); }, diff --git a/app/assets/stylesheets/application.css.sass b/app/assets/stylesheets/application.css.sass index 9c7ea04bc..bcf92ee20 100644 --- a/app/assets/stylesheets/application.css.sass +++ b/app/assets/stylesheets/application.css.sass @@ -1953,6 +1953,23 @@ ul#press_logos iframe, .thumb img :width 100% + .opengraph + :float left + :width 100% + a + :display block + :text-decoration none + :color #000 + :margin-top 10px + :border-top solid 1px #DDD + :border-bottom solid 1px #DDD + :padding 10px 0px 10px 0px + + img + :margin 10px 0px 10px 0px + :float left + br + :clear both .conversation_participants :background diff --git a/app/assets/templates/opengraph_tpl.jst.hbs b/app/assets/templates/opengraph_tpl.jst.hbs new file mode 100644 index 000000000..dc4f1fca5 --- /dev/null +++ b/app/assets/templates/opengraph_tpl.jst.hbs @@ -0,0 +1,10 @@ +{{#if open_graph_cache}} + +
+

{{open_graph_cache.title}}

+ +

{{open_graph_cache.description}}

+
+
+
+{{/if}} diff --git a/app/assets/templates/small-frame/default_tpl.jst.hbs b/app/assets/templates/small-frame/default_tpl.jst.hbs index f1da9f9c5..6c6c6ca39 100644 --- a/app/assets/templates/small-frame/default_tpl.jst.hbs +++ b/app/assets/templates/small-frame/default_tpl.jst.hbs @@ -16,6 +16,7 @@ {{/if}}
+
{{#if text}}
diff --git a/app/assets/templates/status-message_tpl.jst.hbs b/app/assets/templates/status-message_tpl.jst.hbs index 3eb3cda82..166c8cb1d 100644 --- a/app/assets/templates/status-message_tpl.jst.hbs +++ b/app/assets/templates/status-message_tpl.jst.hbs @@ -17,4 +17,5 @@
{{{text}}}
-
\ No newline at end of file +
+
diff --git a/app/helpers/open_graph_helper.rb b/app/helpers/open_graph_helper.rb index a9fd791a2..009ee0ed5 100644 --- a/app/helpers/open_graph_helper.rb +++ b/app/helpers/open_graph_helper.rb @@ -44,6 +44,25 @@ module OpenGraphHelper content_tag(:meta, '', :property => name, :content => content) end + def og_html(cache) + title = cache.title + html = + "" + return html + end + + def link_to_oembed_image(cache, prefix = 'thumbnail_') + link_to(oembed_image_tag(cache, prefix), cache.url, :target => '_blank') + end + + def oembed_image_tag(cache, prefix) + image_tag(cache.data[prefix + 'url'], cache.options_hash(prefix)) + end private # This method compensates for hosting assets off of s3 diff --git a/app/models/open_graph_cache.rb b/app/models/open_graph_cache.rb new file mode 100644 index 000000000..f146da52f --- /dev/null +++ b/app/models/open_graph_cache.rb @@ -0,0 +1,49 @@ +class OpenGraphCache < ActiveRecord::Base + attr_accessible :title + attr_accessible :ob_type + attr_accessible :image + attr_accessible :url + attr_accessible :description + + validates :title, :presence => true + validates :ob_type, :presence => true + validates :image, :presence => true + validates :url, :presence => true + + has_many :posts + + acts_as_api + api_accessible :backbone do |t| + t.add :title + t.add :ob_type + t.add :image + t.add :description + t.add :url + end + + def self.find_or_create_by_url(url) + cache = OpenGraphCache.find_or_initialize_by_url(url) + return cache if cache.persisted? + cache.fetch_and_save_opengraph_data! + return cache if cache.persisted? + return nil + end + + def fetch_and_save_opengraph_data! + begin + response = OpenGraph.fetch(self.url) + if !response + return + end + rescue => e + # noop + else + self.title = response.title + self.ob_type = response.type + self.image = response.image + self.url = response.url + self.description = response.description + self.save + end + end +end diff --git a/app/models/post.rb b/app/models/post.rb index 59968356f..671d4660d 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -24,13 +24,14 @@ class Post < ActiveRecord::Base has_many :resharers, :class_name => 'Person', :through => :reshares, :source => :author belongs_to :o_embed_cache + belongs_to :open_graph_cache after_create do self.touch(:interacted_at) end #scopes - scope :includes_for_a_stream, includes(:o_embed_cache, {:author => :profile}, :mentions => {:person => :profile}) #note should include root and photos, but i think those are both on status_message + scope :includes_for_a_stream, includes(:o_embed_cache, :open_graph_cache, {:author => :profile}, :mentions => {:person => :profile}) #note should include root and photos, but i think those are both on status_message scope :commented_by, lambda { |person| diff --git a/app/models/reshare.rb b/app/models/reshare.rb index e5728429b..338f389e6 100644 --- a/app/models/reshare.rb +++ b/app/models/reshare.rb @@ -33,6 +33,10 @@ class Reshare < Post self.root ? root.o_embed_cache : super end + def open_graph_cache + self.root ? root.open_graph_cache : super + end + def raw_message self.root ? root.raw_message : super end diff --git a/app/models/status_message.rb b/app/models/status_message.rb index f580be28d..52dbb7b24 100644 --- a/app/models/status_message.rb +++ b/app/models/status_message.rb @@ -26,10 +26,12 @@ class StatusMessage < Post before_destroy :presence_of_content attr_accessor :oembed_url + attr_accessor :open_graph_url before_create :filter_mentions after_create :create_mentions after_create :queue_gather_oembed_data, :if => :contains_oembed_url_in_text? + after_create :queue_gather_open_graph_data, :if => :contains_open_graph_url_in_text? #scopes scope :where_person_is_mentioned, lambda { |person| @@ -143,11 +145,19 @@ class StatusMessage < Post Workers::GatherOEmbedData.perform_async(self.id, self.oembed_url) end + def queue_gather_open_graph_data + Workers::GatherOpenGraphData.perform_async(self.id, self.open_graph_url) + end + def contains_oembed_url_in_text? urls = URI.extract(self.raw_message, ['http', 'https']) self.oembed_url = urls.find{ |url| !TRUSTED_OEMBED_PROVIDERS.find(url).nil? } end + def contains_open_graph_url_in_text? + self.open_graph_url = URI.extract(self.raw_message, ['http', 'https'])[0] + end + def address location.try(:address) end diff --git a/app/presenters/post_presenter.rb b/app/presenters/post_presenter.rb index 919123266..db4fdf90b 100644 --- a/app/presenters/post_presenter.rb +++ b/app/presenters/post_presenter.rb @@ -29,6 +29,7 @@ class PostPresenter :nsfw => @post.nsfw, :author => @post.author.as_api_response(:backbone), :o_embed_cache => @post.o_embed_cache.try(:as_api_response, :backbone), + :open_graph_cache => @post.open_graph_cache.try(:as_api_response, :backbone), :mentioned_people => @post.mentioned_people.as_api_response(:backbone), :photos => @post.photos.map {|p| p.as_api_response(:backbone)}, :frame_name => @post.frame_name || template_name, diff --git a/app/workers/gather_open_graph_data.rb b/app/workers/gather_open_graph_data.rb new file mode 100644 index 000000000..d73954d86 --- /dev/null +++ b/app/workers/gather_open_graph_data.rb @@ -0,0 +1,22 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. +# + +module Workers + class GatherOpenGraphData < Base + sidekiq_options queue: :http_service + + def perform(post_id, url, retry_count=1) + post = Post.find(post_id) + post.open_graph_cache = OpenGraphCache.find_or_create_by_url(url) + post.save + rescue ActiveRecord::RecordNotFound + # User created a post and deleted it right afterwards before we + # we had a chance to run the job. + # On the other hand sometimes the job runs before the Post is + # fully persisted. So we just reduce the amount of retries. + GatherOpenGraphData.perform_in(1.minute, post_id, url, retry_count+1) unless retry_count > 3 + end + end +end diff --git a/db/migrate/20130608171134_add_open_graph_cache.rb b/db/migrate/20130608171134_add_open_graph_cache.rb new file mode 100644 index 000000000..ccf4aecc4 --- /dev/null +++ b/db/migrate/20130608171134_add_open_graph_cache.rb @@ -0,0 +1,19 @@ +class AddOpenGraphCache < ActiveRecord::Migration + def up + create_table :open_graph_caches do |t| + t.string :title + t.string :ob_type + t.string :image + t.string :url + t.text :description + end + change_table :posts do |t| + t.integer :open_graph_cache_id + end + end + + def down + remove_column :posts, :open_graph_cache_id + drop_table :open_graph_caches + end +end diff --git a/db/schema.rb b/db/schema.rb index cbbfa94c2..f5b374c6b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -215,6 +215,14 @@ ActiveRecord::Schema.define(:version => 20130801063213) do add_index "o_embed_caches", ["url"], :name => "index_o_embed_caches_on_url", :length => {"url"=>255} + create_table "open_graph_caches", :force => true do |t| + t.string "title" + t.string "ob_type" + t.string "image" + t.string "url" + t.text "description" + end + create_table "participations", :force => true do |t| t.string "guid" t.integer "target_id" @@ -309,6 +317,7 @@ ActiveRecord::Schema.define(:version => 20130801063213) do t.string "facebook_id" t.string "tweet_id" t.text "tumblr_ids" + t.integer "open_graph_cache_id" end add_index "posts", ["author_id", "root_guid"], :name => "index_posts_on_author_id_and_root_guid", :unique => true