opengraph POC

Fixed small-frame opengraph view

Fixed incompletely saved OpenGraphCache bug
This commit is contained in:
Fábián Tamás László 2013-06-09 01:38:54 +02:00 committed by Jonne Haß
parent 9d6ac1abe5
commit 176c6826e0
19 changed files with 200 additions and 6 deletions

View file

@ -64,6 +64,7 @@ gem 'rails_autolink', '1.1.0'
gem 'redcarpet', '3.0.0' gem 'redcarpet', '3.0.0'
gem 'roxml', '3.1.6' gem 'roxml', '3.1.6'
gem 'ruby-oembed', '0.8.8' gem 'ruby-oembed', '0.8.8'
gem 'opengraph', '0.0.4'
# Please remove when migrating to Rails 4 # Please remove when migrating to Rails 4

View file

@ -249,6 +249,10 @@ GEM
omniauth-twitter (1.0.0) omniauth-twitter (1.0.0)
multi_json (~> 1.3) multi_json (~> 1.3)
omniauth-oauth (~> 1.0) omniauth-oauth (~> 1.0)
opengraph (0.0.4)
hashie
nokogiri (~> 1.5.0)
rest-client (~> 1.6.0)
orm_adapter (0.4.0) orm_adapter (0.4.0)
polyglot (0.3.3) polyglot (0.3.3)
pry (0.9.12.2) pry (0.9.12.2)
@ -322,6 +326,8 @@ GEM
redis-namespace (1.3.0) redis-namespace (1.3.0)
redis (~> 3.0.0) redis (~> 3.0.0)
remotipart (1.2.1) remotipart (1.2.1)
rest-client (1.6.7)
mime-types (>= 1.16)
rmagick (2.13.2) rmagick (2.13.2)
roxml (3.1.6) roxml (3.1.6)
activesupport (>= 2.3.0) activesupport (>= 2.3.0)
@ -464,6 +470,7 @@ DEPENDENCIES
omniauth-facebook (= 1.4.1) omniauth-facebook (= 1.4.1)
omniauth-tumblr (= 1.1) omniauth-tumblr (= 1.1)
omniauth-twitter (= 1.0.0) omniauth-twitter (= 1.0.0)
opengraph (= 0.0.4)
rack-cors (= 0.2.8) rack-cors (= 0.2.8)
rack-google-analytics (= 0.11.0) rack-google-analytics (= 0.11.0)
rack-piwik (= 0.2.2) rack-piwik (= 0.2.2)

View file

@ -0,0 +1,8 @@
(function(){
app.helpers.openGraph = {
html : function (open_graph_cache) {
if (!open_graph_cache) { return "" }
return '<img src="' + open_graph_cache.image + '" />'
}
}
})();

View file

@ -44,9 +44,13 @@ app.views.Content = app.views.Base.extend({
var collHeight = 200 var collHeight = 200
, elem = this.$(".collapsible") , elem = this.$(".collapsible")
, oembed = elem.find(".oembed") , oembed = elem.find(".oembed")
, opengraph = elem.find(".opengraph")
, addHeight = 0; , addHeight = 0;
if($.trim(oembed.html()) != "") { if($.trim(oembed.html()) != "") {
addHeight = oembed.height(); addHeight += oembed.height();
}
if($.trim(opengraph.html()) != "") {
addHeight += opengraph.height();
} }
// only collapse if height exceeds collHeight+20% // only collapse if height exceeds collHeight+20%
@ -102,3 +106,7 @@ app.views.OEmbed = app.views.Base.extend({
this.$el.html(insertHTML); this.$el.html(insertHTML);
} }
}); });
app.views.OpenGraph = app.views.Base.extend({
templateName : "opengraph"
});

View file

@ -10,7 +10,8 @@ app.views.Post.SmallFrame = app.views.Post.extend({
}, },
subviews : { subviews : {
'.embed-frame' : "oEmbedView" '.embed-frame' : "oEmbedView",
'.open-graph-frame' : 'openGraphView'
}, },
initialize : function(options) { initialize : function(options) {
@ -22,6 +23,10 @@ app.views.Post.SmallFrame = app.views.Post.extend({
return new app.views.OEmbed({model : this.model}) return new app.views.OEmbed({model : this.model})
}, },
openGraphView : function(){
return new app.views.OpenGraph({model : this.model})
},
smallFramePresenter : function(){ smallFramePresenter : function(){
//todo : we need to have something better for small frame text, probably using the headline() scenario. //todo : we need to have something better for small frame text, probably using the headline() scenario.
return _.extend(this.defaultPresenter(), return _.extend(this.defaultPresenter(),
@ -50,10 +55,10 @@ app.views.Post.SmallFrame = app.views.Post.extend({
var text = this.model.get("text") var text = this.model.get("text")
, baseClass = $.trim(text).length == 0 ? "no-text" : "has-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"; 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'])); var randomColor = _.first(_.shuffle(['cyan', 'green', 'yellow', 'purple', 'lime-green', 'orange', 'red', 'turquoise', 'sand']));

View file

@ -8,6 +8,7 @@ app.views.StreamPost = app.views.Post.extend({
".comments" : "commentStreamView", ".comments" : "commentStreamView",
".post-content" : "postContentView", ".post-content" : "postContentView",
".oembed" : "oEmbedView", ".oembed" : "oEmbedView",
".opengraph" : "openGraphView",
".status-message-location" : "postLocationStreamView" ".status-message-location" : "postLocationStreamView"
}, },
@ -29,6 +30,7 @@ app.views.StreamPost = app.views.Post.extend({
//subviews //subviews
this.commentStreamView = new app.views.CommentStream({model : this.model}); this.commentStreamView = new app.views.CommentStream({model : this.model});
this.oEmbedView = new app.views.OEmbed({model : this.model}); this.oEmbedView = new app.views.OEmbed({model : this.model});
this.openGraphView = new app.views.OpenGraph({model : this.model});
}, },

View file

@ -1953,6 +1953,23 @@ ul#press_logos
iframe, .thumb img iframe, .thumb img
:width 100% :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 .conversation_participants
:background :background

View file

@ -0,0 +1,10 @@
{{#if open_graph_cache}}
<a href="{{open_graph_cache.url}}" target="_blank">
<div>
<h3>{{open_graph_cache.title}}</h3>
<img src="{{open_graph_cache.image}}" />
<p>{{open_graph_cache.description}}</p>
<br />
</div>
</a>
{{/if}}

View file

@ -16,6 +16,7 @@
{{/if}} {{/if}}
<div class="embed-frame" /> <div class="embed-frame" />
<div class="open-graph-frame" />
{{#if text}} {{#if text}}
<div class="text-content"> <div class="text-content">

View file

@ -17,4 +17,5 @@
<div class="collapsible"> <div class="collapsible">
{{{text}}} {{{text}}}
<div class="oembed"></div> <div class="oembed"></div>
</div> <div class="opengraph"></div>
</div>

View file

@ -44,6 +44,25 @@ module OpenGraphHelper
content_tag(:meta, '', :property => name, :content => content) content_tag(:meta, '', :property => name, :content => content)
end end
def og_html(cache)
title = cache.title
html =
"<div class=\"og-concent\">" +
"<a class=\"og-link\" href=\"#{cache.url}\">" +
"<img class=\"og-image\" src=\"#{cache.image}\"/>"
"<h1 class=\"og-title\">#{cache.title}</h1>" +
"<p class=\"og-description\">#{cache.description}</p>"
"</a></div>"
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 private
# This method compensates for hosting assets off of s3 # This method compensates for hosting assets off of s3

View file

@ -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

View file

@ -24,13 +24,14 @@ class Post < ActiveRecord::Base
has_many :resharers, :class_name => 'Person', :through => :reshares, :source => :author has_many :resharers, :class_name => 'Person', :through => :reshares, :source => :author
belongs_to :o_embed_cache belongs_to :o_embed_cache
belongs_to :open_graph_cache
after_create do after_create do
self.touch(:interacted_at) self.touch(:interacted_at)
end end
#scopes #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| scope :commented_by, lambda { |person|

View file

@ -33,6 +33,10 @@ class Reshare < Post
self.root ? root.o_embed_cache : super self.root ? root.o_embed_cache : super
end end
def open_graph_cache
self.root ? root.open_graph_cache : super
end
def raw_message def raw_message
self.root ? root.raw_message : super self.root ? root.raw_message : super
end end

View file

@ -26,10 +26,12 @@ class StatusMessage < Post
before_destroy :presence_of_content before_destroy :presence_of_content
attr_accessor :oembed_url attr_accessor :oembed_url
attr_accessor :open_graph_url
before_create :filter_mentions before_create :filter_mentions
after_create :create_mentions after_create :create_mentions
after_create :queue_gather_oembed_data, :if => :contains_oembed_url_in_text? 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 #scopes
scope :where_person_is_mentioned, lambda { |person| scope :where_person_is_mentioned, lambda { |person|
@ -143,11 +145,19 @@ class StatusMessage < Post
Workers::GatherOEmbedData.perform_async(self.id, self.oembed_url) Workers::GatherOEmbedData.perform_async(self.id, self.oembed_url)
end end
def queue_gather_open_graph_data
Workers::GatherOpenGraphData.perform_async(self.id, self.open_graph_url)
end
def contains_oembed_url_in_text? def contains_oembed_url_in_text?
urls = URI.extract(self.raw_message, ['http', 'https']) urls = URI.extract(self.raw_message, ['http', 'https'])
self.oembed_url = urls.find{ |url| !TRUSTED_OEMBED_PROVIDERS.find(url).nil? } self.oembed_url = urls.find{ |url| !TRUSTED_OEMBED_PROVIDERS.find(url).nil? }
end end
def contains_open_graph_url_in_text?
self.open_graph_url = URI.extract(self.raw_message, ['http', 'https'])[0]
end
def address def address
location.try(:address) location.try(:address)
end end

View file

@ -29,6 +29,7 @@ class PostPresenter
:nsfw => @post.nsfw, :nsfw => @post.nsfw,
:author => @post.author.as_api_response(:backbone), :author => @post.author.as_api_response(:backbone),
:o_embed_cache => @post.o_embed_cache.try(: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), :mentioned_people => @post.mentioned_people.as_api_response(:backbone),
:photos => @post.photos.map {|p| p.as_api_response(:backbone)}, :photos => @post.photos.map {|p| p.as_api_response(:backbone)},
:frame_name => @post.frame_name || template_name, :frame_name => @post.frame_name || template_name,

View file

@ -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

View file

@ -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

View file

@ -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} 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| create_table "participations", :force => true do |t|
t.string "guid" t.string "guid"
t.integer "target_id" t.integer "target_id"
@ -309,6 +317,7 @@ ActiveRecord::Schema.define(:version => 20130801063213) do
t.string "facebook_id" t.string "facebook_id"
t.string "tweet_id" t.string "tweet_id"
t.text "tumblr_ids" t.text "tumblr_ids"
t.integer "open_graph_cache_id"
end end
add_index "posts", ["author_id", "root_guid"], :name => "index_posts_on_author_id_and_root_guid", :unique => true add_index "posts", ["author_id", "root_guid"], :name => "index_posts_on_author_id_and_root_guid", :unique => true