From f3ea8f424fb79d3f0143ccde6e170965b23926be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sch=C3=B6lling?= <manuel.schoelling@gmx.de> Date: Sun, 25 Sep 2011 21:59:25 +0200 Subject: [PATCH] Added oEmbed support --- Gemfile | 1 + app/helpers/markdownify_helper.rb | 6 +- app/models/jobs/gather_o_embed_data.rb | 38 ++++++ app/models/o_embed_cache.rb | 3 + app/models/post.rb | 5 + app/views/comments/_comment.html.haml | 2 +- app/views/messages/_message.haml | 2 +- app/views/people/_profile_sidebar.html.haml | 4 +- app/views/photos/show.html.haml | 2 +- .../status_messages/_status_message.haml | 2 +- config/initializers/oembed.rb | 4 + .../20110924112840_create_o_embed_caches.rb | 14 ++ lib/diaspora/markdownify.rb | 3 +- spec/helpers/markdownify_helper_spec.rb | 128 ++++++++++++++++++ spec/models/jobs/gather_o_embed_data.rb | 59 ++++++++ spec/models/o_embed_cache_spec.rb | 5 + spec/models/post_spec.rb | 11 ++ 17 files changed, 280 insertions(+), 9 deletions(-) create mode 100644 app/models/jobs/gather_o_embed_data.rb create mode 100644 app/models/o_embed_cache.rb create mode 100644 config/initializers/oembed.rb create mode 100644 db/migrate/20110924112840_create_o_embed_caches.rb create mode 100644 spec/models/jobs/gather_o_embed_data.rb create mode 100644 spec/models/o_embed_cache_spec.rb diff --git a/Gemfile b/Gemfile index a32c2ca195..66326040a1 100644 --- a/Gemfile +++ b/Gemfile @@ -64,6 +64,7 @@ gem 'rails-i18n' gem 'nokogiri' gem 'redcarpet', "2.0.0b5" gem 'roxml', :git => 'git://github.com/Empact/roxml.git', :ref => '7ea9a9ffd2338aaef5b0' +gem 'ruby-oembed' # queue diff --git a/app/helpers/markdownify_helper.rb b/app/helpers/markdownify_helper.rb index cf012b581e..4b87e62105 100644 --- a/app/helpers/markdownify_helper.rb +++ b/app/helpers/markdownify_helper.rb @@ -33,7 +33,11 @@ module MarkdownifyHelper return '' if message.blank? #renderer = Redcarpet::Render::HTML.new(render_options) - renderer = Diaspora::Markdownify::HTML.new(render_options) + if render_options[:oembed] + renderer = Diaspora::Markdownify::HTMLwithOEmbed.new(render_options) + else + renderer = Diaspora::Markdownify::HTML.new(render_options) + end markdown = Redcarpet::Markdown.new(renderer, markdown_options) message = markdown.render(message).html_safe diff --git a/app/models/jobs/gather_o_embed_data.rb b/app/models/jobs/gather_o_embed_data.rb new file mode 100644 index 0000000000..ed70e176e6 --- /dev/null +++ b/app/models/jobs/gather_o_embed_data.rb @@ -0,0 +1,38 @@ +# 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 Jobs + class GatherOEmbedData < Base + @queue = :http_service + + class GatherOEmbedRenderer < Redcarpet::Render::HTML + include ActionView::Helpers::TextHelper + include ActionView::Helpers::TagHelper + + def autolink(link, type) + return link if OEmbedCache.exists?(:url => link) + + begin + res = ::OEmbed::Providers.get(link, {:maxwidth => 420, :maxheight => 420, :frame => 1, :iframe => 1}) + rescue Exception => e + # noop + else + data = res.fields + data['trusted_endpoint_url'] = res.provider.endpoint + cache = OEmbedCache.new(:url => link, :data => data) + cache.save + end + + return link + end + end + + def self.perform(text) + renderer = GatherOEmbedRenderer.new({}) + markdown = Redcarpet::Markdown.new(renderer, {:autolink => true}) + message = markdown.render(text) + end + end +end diff --git a/app/models/o_embed_cache.rb b/app/models/o_embed_cache.rb new file mode 100644 index 0000000000..79f472f8c8 --- /dev/null +++ b/app/models/o_embed_cache.rb @@ -0,0 +1,3 @@ +class OEmbedCache < ActiveRecord::Base + serialize :data +end diff --git a/app/models/post.rb b/app/models/post.rb index 178167438e..20613b5a5e 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -66,6 +66,11 @@ class Post < ActiveRecord::Base @reshare_count ||= Post.where(:root_guid => self.guid).count end + def text= nd + Resque.enqueue(Jobs::GatherOEmbedData, nd) + write_attribute(:text, nd) + end + def diaspora_handle= nd self.author = Person.where(:diaspora_handle => nd).first write_attribute(:diaspora_handle, nd) diff --git a/app/views/comments/_comment.html.haml b/app/views/comments/_comment.html.haml index fd8a9834ef..e0955bde36 100644 --- a/app/views/comments/_comment.html.haml +++ b/app/views/comments/_comment.html.haml @@ -12,7 +12,7 @@ = person_link(comment.author, :class => "hovercardable") %span{:class => direction_for(comment.text)} - = markdownify(comment, :youtube_maps => comment.youtube_titles) + = markdownify(comment, :oembed => true, :youtube_maps => comment.youtube_titles) .comment_info %time.timeago{:datetime => comment.created_at} diff --git a/app/views/messages/_message.haml b/app/views/messages/_message.haml index f36ebbc55c..ad44c9b796 100644 --- a/app/views/messages/_message.haml +++ b/app/views/messages/_message.haml @@ -12,5 +12,5 @@ = how_long_ago(message) %div{ :class => direction_for(message.text) } - = markdownify(message) + = markdownify(message, :oembed => true) diff --git a/app/views/people/_profile_sidebar.html.haml b/app/views/people/_profile_sidebar.html.haml index c6d85c2057..b3c5a77392 100644 --- a/app/views/people/_profile_sidebar.html.haml +++ b/app/views/people/_profile_sidebar.html.haml @@ -22,13 +22,13 @@ %h4 =t('.bio') %div{ :class => direction_for(person.profile.bio) } - = markdownify(person.profile.bio, :newlines => true) + = markdownify(person.profile.bio, :oembed => true, :newlines => true) - unless person.profile.location.blank? %li %h4 =t('.location') %div{ :class => direction_for(person.profile.location) } - = markdownify(person.profile.location, :newlines => true) + = markdownify(person.profile.location, :oembed => true, :newlines => true) %li - unless person.profile.gender.blank? diff --git a/app/views/photos/show.html.haml b/app/views/photos/show.html.haml index 283580bb0c..f9805f2322 100644 --- a/app/views/photos/show.html.haml +++ b/app/views/photos/show.html.haml @@ -40,7 +40,7 @@ .span-8.last %p - = markdownify(photo.status_message) + = markdownify(photo.status_message, :oembed => true) %span{:style=>'font-size:smaller'} =link_to t('.collection_permalink'), post_path(photo.status_message) %br diff --git a/app/views/status_messages/_status_message.haml b/app/views/status_messages/_status_message.haml index ec8343327f..2a6347d668 100644 --- a/app/views/status_messages/_status_message.haml +++ b/app/views/status_messages/_status_message.haml @@ -16,4 +16,4 @@ = link_to (image_tag photo.url(:thumb_small), :class => 'stream-photo thumb_small', 'data-small-photo' => photo.url(:thumb_medium), 'data-full-photo' => photo.url), photo_path(photo), :class => 'stream-photo-link' %div{:class => direction_for(post.text)} - != markdownify(post, :youtube_maps => post[:youtube_titles]) + != markdownify(post, :oembed => true, :youtube_maps => post[:youtube_titles]) diff --git a/config/initializers/oembed.rb b/config/initializers/oembed.rb new file mode 100644 index 0000000000..5db1954342 --- /dev/null +++ b/config/initializers/oembed.rb @@ -0,0 +1,4 @@ +require 'oembed' +OEmbed::Providers.register_all +OEmbed::Providers.register_fallback(OEmbed::ProviderDiscovery) + diff --git a/db/migrate/20110924112840_create_o_embed_caches.rb b/db/migrate/20110924112840_create_o_embed_caches.rb new file mode 100644 index 0000000000..78a549e818 --- /dev/null +++ b/db/migrate/20110924112840_create_o_embed_caches.rb @@ -0,0 +1,14 @@ +class CreateOEmbedCaches < ActiveRecord::Migration + def self.up + create_table :o_embed_caches do |t| + t.string :url, :limit => 1024, :null => false, :unique => true + t.text :data, :null => false + end + add_index :o_embed_caches, :url + end + + def self.down + remove_index :o_embed_caches, :column => :url + drop_table :o_embed_caches + end +end diff --git a/lib/diaspora/markdownify.rb b/lib/diaspora/markdownify.rb index 900436eb88..bdd24da541 100644 --- a/lib/diaspora/markdownify.rb +++ b/lib/diaspora/markdownify.rb @@ -3,12 +3,11 @@ require 'erb' module Diaspora module Markdownify class HTML < Redcarpet::Render::HTML - include ActionView::Helpers::TextHelper include ActionView::Helpers::TagHelper def autolink(link, type) - auto_link(link, :link => :urls, :html => { :target => "_blank" }) + auto_link(link, :link => :urls) end end end diff --git a/spec/helpers/markdownify_helper_spec.rb b/spec/helpers/markdownify_helper_spec.rb index ed73c7d1b0..af88b27ba6 100644 --- a/spec/helpers/markdownify_helper_spec.rb +++ b/spec/helpers/markdownify_helper_spec.rb @@ -58,6 +58,134 @@ describe MarkdownifyHelper do formatted = markdownify(message) formatted.should =~ /hovercard/ end + + context 'when posting a link with oEmbed support' do + scenarios = { + "photo" => { + "oembed_data" => { + "trusted_endpoint_url" => "__!SPOOFED!__", + "version" => "1.0", + "type" => "photo", + "title" => "ZB8T0193", + "width" => "240", + "height" => "160", + "url" => "http://farm4.static.flickr.com/3123/2341623661_7c99f48bbf_m.jpg" + }, + "link_url" => 'http://www.flickr.com/photos/bees/2341623661', + "oembed_get_request" => "http://www.flickr.com/services/oembed/?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://www.flickr.com/photos/bees/2341623661", + }, + + "unsupported" => { + "oembed_data" => "", + "oembed_get_request" => 'http://www.we-do-not-support-oembed.com/index.html', + "link_url" => 'http://www.we-do-not-support-oembed.com/index.html' + }, + + "secure_video" => { + "oembed_data" => { + "version" => "1.0", + "type" => "video", + "width" => 425, + "height" => 344, + "title" => "Amazing Nintendo Facts", + "html" => "<object width=\"425\" height=\"344\"> + <param name=\"movie\" value=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"></param> + <param name=\"allowFullScreen\" value=\"true\"></param> + <param name=\"allowscriptaccess\" value=\"always\"></param> + <embed src=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\" + type=\"application/x-shockwave-flash\" width=\"425\" height=\"344\" + allowscriptaccess=\"always\" allowfullscreen=\"true\"></embed> + </object>", + }, + "link_url" => "http://youtube.com/watch?v=M3r2XDceM6A&format=json", + "oembed_get_request" => "http://www.youtube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://youtube.com/watch?v=M3r2XDceM6A", + }, + + "unsecure_video" => { + "oembed_data" => { + "version" => "1.0", + "type" => "video", + "title" => "This is a video from an unsecure source", + "html" => "<object width=\"425\" height=\"344\"> + <param name=\"movie\" value=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"></param> + <param name=\"allowFullScreen\" value=\"true\"></param> + <param name=\"allowscriptaccess\" value=\"always\"></param> + <embed src=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\" + type=\"application/x-shockwave-flash\" width=\"425\" height=\"344\" + allowscriptaccess=\"always\" allowfullscreen=\"true\"></embed> + </object>", + }, + "link_url" => "http://mytube.com/watch?v=M3r2XDceM6A&format=json", + "discovery_data" => '<link rel="alternate" type="application/json+oembed" href="http://www.mytube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://mytube.com/watch?v=M3r2XDceM6A" />', + "oembed_get_request" => "http://www.mytube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://mytube.com/watch?v=M3r2XDceM6A", + }, + + "secure_rich" => { + "oembed_data" => { + "version" => "1.0", + "type" => "rich", + "width" => 425, + "height" => 344, + "title" => "Amazing Nintendo Facts", + "html" => "<object width=\"425\" height=\"344\"> + <param name=\"movie\" value=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"></param> + <param name=\"allowFullScreen\" value=\"true\"></param> + <param name=\"allowscriptaccess\" value=\"always\"></param> + <embed src=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\" + type=\"application/x-shockwave-flash\" width=\"425\" height=\"344\" + allowscriptaccess=\"always\" allowfullscreen=\"true\"></embed> + </object>", + }, + "link_url" => "http://youtube.com/watch?v=M3r2XDceM6A&format=json", + "oembed_get_request" => "http://www.youtube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://youtube.com/watch?v=M3r2XDceM6A", + }, + + "unsecure_rich" => { + "oembed_data" => { + "version" => "1.0", + "type" => "rich", + "title" => "This is a video from an unsecure source", + "html" => "<object width=\"425\" height=\"344\"> + <param name=\"movie\" value=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"></param> + <param name=\"allowFullScreen\" value=\"true\"></param> + <param name=\"allowscriptaccess\" value=\"always\"></param> + <embed src=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\" + type=\"application/x-shockwave-flash\" width=\"425\" height=\"344\" + allowscriptaccess=\"always\" allowfullscreen=\"true\"></embed> + </object>", + }, + "link_url" => "http://mytube.com/watch?v=M3r2XDceM6A&format=json", + "discovery_data" => '<link rel="alternate" type="application/json+oembed" href="http://www.mytube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://mytube.com/watch?v=M3r2XDceM6A" />', + "oembed_get_request" => "http://www.mytube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://mytube.com/watch?v=M3r2XDceM6A", + }, + } + + scenarios.each do |type, data| + specify 'for type "'+type+'"' do + stub_request(:get, data['oembed_get_request']).to_return(:status => 200, :body => data['oembed_data'].to_json.to_s) + stub_request(:get, data['link_url']).to_return(:status => 200, :body => data['discovery_data']) if data.has_key?('discovery_data') + + message = "Look at this! "+data['link_url'] + Jobs::GatherOEmbedData.perform(message) + OEmbedCache.find_by_url(data['link_url']).should_not be_nil unless type == 'unsupported' + + formatted = markdownify(message, :oembed => true) + case type + when 'photo' + formatted.should =~ /#{data['oembed_data']['url']}/ + when 'unsupported' + formatted.should =~ /#{data['link_url']}/ + when 'secure_video', 'secure_rich' + formatted.should =~ /#{data['oembed_data']['html']}/ + when 'unsecure_video', 'unsecure_rich' + formatted.should_not =~ /#{data['oembed_data']['html']}/ + formatted.should =~ /#{data['oembed_data']['title']}/ + formatted.should =~ /#{data['oembed_data']['url']}/ + end + end + end + + end end end end diff --git a/spec/models/jobs/gather_o_embed_data.rb b/spec/models/jobs/gather_o_embed_data.rb new file mode 100644 index 0000000000..5d95a2ff85 --- /dev/null +++ b/spec/models/jobs/gather_o_embed_data.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +describe Jobs::GatherOEmbedData do + before do + @flickr_oembed_data = { + "trusted_endpoint_url" => "__!SPOOFED!__", + "version" => "1.0", + "type" => "photo", + "author_url" => "http://www.flickr.com/photos/bees/", + "cache_age" => 3600, + "provider_name" => "Flickr", + "provider_url" => "http://www.flickr.com/", + "title" => "ZB8T0193", + "author_name" => "Bees", + "width" => "240", + "height" => "160", + "url" => "https://farm4.static.flickr.com/3123/2341623661_7c99f48bbf_m.jpg" + } + + @flickr_oembed_url = 'http://www.flickr.com/services/oembed/' + @flickr_photo_url = 'http://www.flickr.com/photos/bees/2341623661' + @flickr_oembed_get_request = @flickr_oembed_url+"?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url="+@flickr_photo_url + + @no_oembed_url = 'http://www.we-do-not-support-oembed.com/index.html' + + stub_request(:get, @flickr_oembed_get_request).to_return(:status => 200, :body => @flickr_oembed_data.to_json) + stub_request(:get, @no_oembed_url).to_return(:status => 200, :body => '<html><body>hello there</body></html>') + end + + describe '.perform' do + it 'requests not data from the internet' do + Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url) + + a_request(:get, @flickr_oembed_get_request).should have_been_made + end + + it 'requests not data from the internet only once' do + Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url) + Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url) + + a_request(:get, @flickr_oembed_get_request).should have_been_made.times(1) + end + + it 'creates one cache entry' do + Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url) + + expected_data = @flickr_oembed_data + expected_data['trusted_endpoint_url'] = @flickr_oembed_url + OEmbedCache.find_by_url(@flickr_photo_url).data.should == expected_data + + Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url) + OEmbedCache.count(:conditions => {:url => @flickr_photo_url}).should == 1 + end + + it 'creates no cache entry for unsupported pages' do + Jobs::GatherOEmbedData.perform("This page is lame! It does not support oEmbed: "+@no_oembed_url) + OEmbedCache.find_by_url(@no_oembed_url).should be_nil + end + end +end diff --git a/spec/models/o_embed_cache_spec.rb b/spec/models/o_embed_cache_spec.rb new file mode 100644 index 0000000000..e544179624 --- /dev/null +++ b/spec/models/o_embed_cache_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe OEmbedCache do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index fade3ab7ae..10a769bd8c 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -68,6 +68,17 @@ describe Post do end end + describe 'oEmbed' do + it 'should gather oEmbed data' do + stub_request(:get, "http://gdata.youtube.com/feeds/api/videos/M3r2XDceM6A?v=2").to_return(:status => 200, :body => "") + + text = 'http://youtube.com/watch?v=M3r2XDceM6A&format=json' + Resque.should_receive(:enqueue).with(Jobs::GatherOEmbedData, text) + post = Factory.create(:status_message, :author => @user.person, :text => text) + post.save! + end + end + describe 'serialization' do it 'should serialize the handle and not the sender' do post = @user.post :status_message, :text => "hello", :to => @aspect.id -- GitLab