Commit be3b210f authored by Braulio Bhavamitra's avatar Braulio Bhavamitra
Browse files

Add abstract plugin to federate networks via OpenGraph

parent 6c114d19
......@@ -8,7 +8,8 @@ class Article < ActiveRecord::Base
:accept_comments, :feed, :published, :source, :source_name,
:highlighted, :notify_comments, :display_hits, :slug,
:external_feed_builder, :display_versions, :external_link,
:image_builder, :show_to_followers
:image_builder, :show_to_followers,
:author
acts_as_having_image
......
......@@ -163,4 +163,8 @@ class UploadedFile < Article
true
end
def notifiable?
true
end
end
......@@ -169,7 +169,7 @@ def test_sequence(plugins, tasks)
failed[plugin] << task
end
end
disable_plugins(plugin)
disable_plugins
end
fail_flag = false
failed.each do |plugin, tasks|
......
gem 'jsonify-rails'
class OpenGraphPlugin::MyprofileController < MyProfileController
protect 'edit_profile', :profile
before_filter :set_context
def enterprise_search
scope = environment.enterprises.enabled.public
profile_search scope
end
def community_search
scope = environment.communities.public
profile_search scope
end
def friend_search
scope = profile.friends
profile_search scope
end
def track_config
profile.update_attributes! params[:profile_data]
render partial: 'track_form', locals: {context: context, reload: true}
end
protected
def profile_search scope
@query = params[:query]
@profiles = scope.limit(10).order('name ASC').
where(['name ILIKE ? OR name ILIKE ? OR identifier LIKE ?', "#{@query}%", "% #{@query}%", "#{@query}%"])
render partial: 'profile_search', locals: {profiles: @profiles}
end
def context
:open_graph
end
def set_context
OpenGraphPlugin.context = self.context
end
def default_url_options
# avoid rails' use_relative_controller!
{use_route: '/'}
end
end
class CreateOpenGraphPluginTracks < ActiveRecord::Migration
def up
create_table :open_graph_plugin_tracks do |t|
t.string :type
t.string :context
t.boolean :enabled, default: true
t.integer :tracker_id
t.integer :actor_id
t.string :action
t.string :object_type
t.text :object_data_url
t.integer :object_data_id
t.string :object_data_type
t.timestamps
end
add_index :open_graph_plugin_tracks, [:type]
add_index :open_graph_plugin_tracks, [:context]
add_index :open_graph_plugin_tracks, [:type, :context]
add_index :open_graph_plugin_tracks, [:actor_id]
add_index :open_graph_plugin_tracks, [:action]
add_index :open_graph_plugin_tracks, [:object_type]
add_index :open_graph_plugin_tracks, [:enabled]
add_index :open_graph_plugin_tracks, [:object_data_url]
add_index :open_graph_plugin_tracks, [:object_data_id, :object_data_type], name: 'index_open_graph_plugin_tracks_object_data_id_type'
end
def down
drop_table :open_graph_plugin_tracks
end
end
system "script/noosfero-plugins -q enable metadata"
require_dependency 'article'
class Article
after_update :open_graph_scrape
protected
def open_graph_scrape
activity = OpenGraphPlugin::Activity.where(object_data_id: self.id, object_data_type: self.class.base_class.name).first
activity.scrape if activity
end
handle_asynchronously :open_graph_scrape
end
require_dependency 'profile'
# hate to wrte this, but without Noosfero::Plugin::Settings is loaded instead
require 'open_graph_plugin/settings'
# attr_accessible must be defined on subclasses
Profile.descendants.each do |subclass|
subclass.class_eval do
attr_accessible :open_graph_settings
OpenGraphPlugin::TrackConfig::Types.each do |track, klass|
klass = "OpenGraphPlugin::#{klass}".constantize
attributes = "#{klass.association}_attributes"
profile_ids = "open_graph_#{track}_profiles_ids"
attr_accessible attributes
attr_accessible profile_ids
end
end
end
class Profile
def open_graph_settings attrs = {}
@open_graph_settings ||= OpenGraphPlugin::Settings.new self, attrs
attrs.each{ |a, v| @open_graph_settings.send "#{a}=", v }
@open_graph_settings
end
alias_method :open_graph_settings=, :open_graph_settings
has_many :open_graph_tracks, class_name: 'OpenGraphPlugin::Track', source: :tracker_id, foreign_key: :tracker_id
has_many :open_graph_activities, class_name: 'OpenGraphPlugin::Activity', source: :tracker_id, foreign_key: :tracker_id
has_many :open_graph_track_configs, class_name: 'OpenGraphPlugin::TrackConfig', source: :tracker_id, foreign_key: :tracker_id
OpenGraphPlugin::TrackConfig::Types.each do |track, klass|
klass = "OpenGraphPlugin::#{klass}".constantize
association = klass.association
profile_ids = "open_graph_#{track}_profiles_ids"
has_many association, class_name: klass.name, foreign_key: :tracker_id
accepts_nested_attributes_for association, allow_destroy: true, reject_if: :open_graph_reject_empty_object_type
define_method "#{profile_ids}=" do |ids|
cids = self.send(association).order('created_at ASC').map(&:object_data_id)
nids = if ids.is_a? Array then ids else ids.split ',' end
nids = nids.map(&:to_i)
Profile.where(id: nids-cids).each{ |profile| self.send(association).create! type: klass.name, object_data: profile }
self.send(association).each{ |c| c.destroy unless c.object_data_id.in? nids }
end
end
define_method :open_graph_reject_empty_object_type do |attributes|
exists = attributes[:id].present?
empty = attributes[:object_type].empty?
attributes.merge! _destroy: 1 if exists and empty
return (!exists and empty)
end
end
require_dependency 'profile_activity'
class ProfileActivity
# update happens with grouped ActionTracker
after_save :open_graph_publish
def open_graph_publish
# Scrap not yet supported
if self.activity.is_a? ActionTracker::Record
verb = self.activity.verb.to_sym
return unless object = self.activity.target
return unless stories = OpenGraphPlugin::Stories::TrackerStories[verb]
OpenGraphPlugin::Stories.publish object, stories
end
end
end
require_dependency 'uploaded_file'
class UploadedFile
extend OpenGraphPlugin::AttachStories::ClassMethods
open_graph_attach_stories only: :add_an_image
end
module OpenGraphPlugin
extend Noosfero::Plugin::ParentMethods
def self.plugin_name
I18n.t 'open_graph_plugin.lib.plugin.name'
end
def self.plugin_description
I18n.t 'open_graph_plugin.lib.plugin.description'
end
def self.context
Thread.current[:open_graph_context] || :open_graph
end
def self.context= value
Thread.current[:open_graph_context] = value
end
end
require_dependency 'open_graph_plugin/stories'
# This is used when ActionTracker is not compartible with the way
module OpenGraphPlugin::AttachStories
module ClassMethods
def open_graph_attach_stories options={}
if stories = Array(options[:only])
callbacks = {}
stories.each do |story|
defs = OpenGraphPlugin::Stories::Definitions[story]
Array(defs[:on]).each do |on|
callbacks[on] ||= []
callbacks[on] << story
end
end
else
klass = self.name
callbacks = OpenGraphPlugin::Stories::ModelStories[klass.to_sym]
return if callbacks.blank?
end
callbacks.each do |on, stories|
# subclasses may override this, but the callback is called only once
method = "open_graph_publish_after_#{on}"
self.send "after_#{on}", method
# buggy with rails 3.2
#self.send "after_commit", method, on: on
define_method method do
OpenGraphPlugin::Stories.publish self, stories
end
end
end
end
module InstanceMethods
end
end
class OpenGraphPlugin::Base < Noosfero::Plugin
def js_files
[].map{ |j| "javascripts/#{j}" }
end
def stylesheet?
true
end
end
ActiveSupport.run_load_hooks :open_graph_plugin, OpenGraphPlugin
module OpenGraphPlugin::DisplayHelper
def blah
puts 'here'
end
end
class OpenGraphPlugin::Publisher
attr_accessor :actions
attr_accessor :objects
def self.default
@default ||= self.new
end
def initialize attributes = {}
# defaults
self.actions = OpenGraphPlugin::Stories::DefaultActions
self.objects = OpenGraphPlugin::Stories::DefaultObjects
attributes.each do |attr, value|
self.send "#{attr}=", value
end
end
def publish actor, story_defs, object_data_url
raise 'abstract method called'
end
def publish_stories object_data, actor, stories
stories.each do |story|
begin
self.publish_story object_data, actor, story
rescue => e
ExceptionNotifier.notify_exception e
end
end
end
def update_delay
1.day
end
# only publish recent objects to avoid multiple publications
def recent_publish? actor, object_type, object_data_url
activity_params = {actor_id: actor.id, object_type: object_type, object_data_url: object_data_url}
activity = OpenGraphPlugin::Activity.where(activity_params).first
activity.present? and activity.created_at <= self.update_delay.from_now
end
def publish_story object_data, actor, story
OpenGraphPlugin.context = self.context
defs = OpenGraphPlugin::Stories::Definitions[story]
passive = defs[:passive]
print_debug "open_graph: publish_story #{story}" if debug? actor
match_criteria = if (ret = self.call defs[:criteria], object_data, actor).nil? then true else ret end
return unless match_criteria
print_debug "open_graph: #{story} match criteria" if debug? actor
match_condition = if (ret = self.call defs[:publish_if], object_data, actor).nil? then true else ret end
return unless match_condition
print_debug "open_graph: #{story} match publish_if" if debug? actor
actors = self.story_trackers defs, actor, object_data
return if actors.blank?
print_debug "open_graph: #{story} has enabled trackers" if debug? actor
if publish = defs[:publish]
begin
instance_exec actor, object_data, &publish
rescue => e
print_debug "open_graph: can't publish story: #{e.message}" if debug? actor
ExceptionNotifier.notify_exception e
end
else
# force profile identifier for custom domains and fixed host. see og_url_for
object_profile = self.call(story_defs[:object_profile], object_data) || object_data.profile rescue nil
extra_params = if object_profile then {profile: object_profile.identifier} else {} end
custom_object_data_url = self.call defs[:object_data_url], object_data, actor
object_data_url = if passive then self.passive_url_for object_data, custom_object_data_url, defs, extra_params else self.url_for object_data, custom_object_data_url, extra_params end
actors.each do |actor|
print_debug "open_graph: start publishing" if debug? actor
begin
self.publish actor, defs, object_data_url
rescue => e
print_debug "open_graph: can't publish story: #{e.message}" if debug? actor
ExceptionNotifier.notify_exception e
end
end
end
end
def story_trackers story_defs, actor, object_data
passive = story_defs[:passive]
trackers = []
track_configs = Array(story_defs[:track_config]).compact.map(&:constantize)
return if track_configs.empty?
print_debug "open_graph: using configs: #{track_configs.map(&:name).inspect}" if debug? actor
if passive
object_profile = self.call(story_defs[:object_profile], object_data) || object_data.profile rescue nil
return unless object_profile
track_configs.each do |c|
trackers.concat c.trackers_to_profile(object_profile)
end.flatten
trackers.select! do |t|
track_configs.any?{ |c| c.enabled? self.context, t }
end
else #active
object_actor = self.call(story_defs[:object_actor], object_data) || object_data.profile rescue nil
return unless object_actor and object_actor.person?
custom_actor = self.call(story_defs[:custom_actor], object_data)
actor = custom_actor if custom_actor
match_track = track_configs.any? do |c|
c.enabled?(self.context, actor) and
actor.send("open_graph_#{c.track_name}_track_configs").where(object_type: story_defs[:object_type]).first
end
trackers << actor if match_track
end
trackers
end
protected
include MetadataPlugin::UrlHelper
def register_publish attributes
OpenGraphPlugin::Activity.create! attributes
end
# Call don't ask: move to a og_url method inside object
def url_for object, custom_url=nil, extra_params={}
return custom_url if custom_url.is_a? String
url = custom_url || if object.is_a? Profile then og_profile_url object else object.url end
# for profile when custom domain is used
url.merge! profile: object.profile.identifier if object.respond_to? :profile
url.merge! extra_params
self.og_url_for url
end
def passive_url_for object, custom_url, story_defs, extra_params={}
object_type = story_defs[:object_type]
extra_params.merge! og_type: MetadataPlugin.og_types[object_type]
self.url_for object, custom_url, extra_params
end
def call p, *args
p and instance_exec *args, &p
end
def context
:open_graph
end
def print_debug msg
puts msg
Delayed::Worker.logger.debug msg
end
def debug? actor=nil
!Rails.env.production?
end
end
class OpenGraphPlugin::Settings < Noosfero::Plugin::Settings
def self.new base, attrs = {}
super base, self.parents.first, attrs
end
OpenGraphPlugin::TrackConfig::Types.each do |track, klass|
define_method "#{track}_track_enabled=" do |value|
super ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
end
end
class OpenGraphPlugin::Stories
class_attribute :publishers
self.publishers = []
def self.register_publisher publisher
self.publishers << publisher
end
def self.publish record, stories
actor = User.current.person rescue nil
return unless actor
self.publishers.each do |publisher|
publisher = publisher.delay unless Rails.env.development? or Rails.env.test?
publisher.publish_stories record, actor, stories
end
end
Definitions = {
# needed a patch on UploadedFile: def notifiable?; true; end
add_a_document: {
action_tracker_verb: :create_article,
track_config: 'OpenGraphPlugin::ActivityTrackConfig',
action: :add,
object_type: :uploaded_file,
models: :UploadedFile,
on: :create,
criteria: proc do |article, actor|
article.is_a? UploadedFile
end,
publish_if: proc do |uploaded_file, actor|
# done in add_an_image
next false if uploaded_file.image?
uploaded_file.published?
end,
object_data_url: proc do |uploaded_file, actor|
uploaded_file.url.merge view: true
end,
},
add_an_image: {
# :upload_image verb can't be used as it uses the parent Gallery as target
# hooked via open_graph_attach_stories
action_tracker_verb: nil,
track_config: 'OpenGraphPlugin::ActivityTrackConfig',
action: :add,
object_type: :gallery_image,
models: :UploadedFile,
on: :create,
criteria: proc do |article, actor|
article.is_a? UploadedFile
end,
publish_if: proc do |uploaded_file, actor|
uploaded_file.image? and uploaded_file.parent.is_a? Gallery
end,
object_data_url: proc do |uploaded_file, actor|
uploaded_file.url.merge view: true
end,
},
create_an_article: {
action_tracker_verb: :create_article,
track_config: 'OpenGraphPlugin::ActivityTrackConfig',
action: :create,
object_type: :blog_post,