create NotificationService: send notifications after receive

This commit is contained in:
Benjamin Neff 2016-05-05 16:15:55 +02:00
parent 97f4b0c2e4
commit ebfb0aa884
15 changed files with 255 additions and 134 deletions

View file

@ -3,15 +3,15 @@
# the COPYRIGHT file. # the COPYRIGHT file.
# #
class Notification < ActiveRecord::Base class Notification < ActiveRecord::Base
belongs_to :recipient, :class_name => 'User' belongs_to :recipient, class_name: "User"
has_many :notification_actors, :dependent => :destroy has_many :notification_actors, dependent: :destroy
has_many :actors, :class_name => 'Person', :through => :notification_actors, :source => :person has_many :actors, class_name: "Person", through: :notification_actors, source: :person
belongs_to :target, :polymorphic => true belongs_to :target, polymorphic: true
attr_accessor :note_html attr_accessor :note_html
def self.for(recipient, opts={}) def self.for(recipient, opts={})
self.where(opts.merge!(:recipient_id => recipient.id)).order('updated_at desc') where(opts.merge!(recipient_id: recipient.id)).order("updated_at DESC")
end end
def self.notify(recipient, target, actor) def self.notify(recipient, target, actor)
@ -33,11 +33,11 @@ class Notification < ActiveRecord::Base
end end
def as_json(opts={}) def as_json(opts={})
super(opts.merge(:methods => :note_html)) super(opts.merge(methods: :note_html))
end end
def email_the_user(target, actor) def email_the_user(target, actor)
self.recipient.mail(self.mail_job, self.recipient_id, actor.id, target.id) recipient.mail(mail_job, recipient_id, actor.id, target.id)
end end
def set_read_state( read_state ) def set_read_state( read_state )
@ -45,14 +45,13 @@ class Notification < ActiveRecord::Base
end end
def mail_job def mail_job
raise NotImplementedError.new('Subclass this.') raise NotImplementedError.new("Subclass this.")
end end
def effective_target def linked_object
self.popup_translation_key == "notifications.mentioned" ? self.target.post : self.target target
end end
private
def self.concatenate_or_create(recipient, target, actor, notification_type) def self.concatenate_or_create(recipient, target, actor, notification_type)
return nil if suppress_notification?(recipient, target) return nil if suppress_notification?(recipient, target)
@ -76,7 +75,6 @@ private
end end
end end
def self.make_notification(recipient, target, actor, notification_type) def self.make_notification(recipient, target, actor, notification_type)
return nil if suppress_notification?(recipient, target) return nil if suppress_notification?(recipient, target)
n = notification_type.new(:target => target, n = notification_type.new(:target => target,
@ -87,9 +85,28 @@ private
n n
end end
def self.concatenate_or_create(recipient, target, actor)
return nil if suppress_notification?(recipient, target)
find_or_initialize_by(recipient: recipient, target: target, unread: true).tap do |notification|
notification.actors |= [actor]
# Explicitly touch the notification to update updated_at whenever new actor is inserted in notification.
if notification.new_record? || notification.changed?
notification.save!
else
notification.touch
end
end
end
def self.create_notification(recipient_id, target, actor)
create(recipient_id: recipient_id, target: target, actors: [actor])
end
def self.suppress_notification?(recipient, post) def self.suppress_notification?(recipient, post)
post.is_a?(Post) && recipient.is_shareable_hidden?(post) post.is_a?(Post) && recipient.is_shareable_hidden?(post)
end end
private_class_method :suppress_notification?
def self.types def self.types
{ {

View file

@ -1,17 +1,26 @@
class Notifications::AlsoCommented < Notification module Notifications
def mail_job class AlsoCommented < Notification
Workers::Mail::AlsoCommented def mail_job
end Workers::Mail::AlsoCommented
end
def popup_translation_key
'notifications.also_commented'
end
def deleted_translation_key def popup_translation_key
'notifications.also_commented_deleted' "notifications.also_commented"
end end
def linked_object def deleted_translation_key
Post.where(:id => self.target_id).first "notifications.also_commented_deleted"
end
def self.notify(comment, _recipient_user_ids)
actor = comment.author
commentable = comment.commentable
recipient_ids = commentable.participants.local.where.not(id: [commentable.author_id, actor.id]).pluck(:owner_id)
User.where(id: recipient_ids).find_each do |recipient|
concatenate_or_create(recipient, commentable, actor)
.try {|notification| notification.email_the_user(comment, actor) }
end
end
end end
end end

View file

@ -1,17 +1,24 @@
class Notifications::CommentOnPost < Notification module Notifications
def mail_job class CommentOnPost < Notification
Workers::Mail::CommentOnPost def mail_job
end Workers::Mail::CommentOnPost
end
def popup_translation_key def popup_translation_key
'notifications.comment_on_post' "notifications.comment_on_post"
end end
def deleted_translation_key def deleted_translation_key
'notifications.also_commented_deleted' "notifications.also_commented_deleted"
end end
def linked_object def self.notify(comment, _recipient_user_ids)
Post.where(:id => self.target_id).first actor = comment.author
commentable_author = comment.commentable.author
return unless commentable_author.local? && actor != commentable_author
concatenate_or_create(commentable_author.owner, comment.commentable, actor).email_the_user(comment, actor)
end
end end
end end

View file

@ -1,19 +1,24 @@
class Notifications::Liked < Notification module Notifications
def mail_job class Liked < Notification
Workers::Mail::Liked def mail_job
end Workers::Mail::Liked
end
def popup_translation_key
'notifications.liked'
end
def deleted_translation_key def popup_translation_key
'notifications.liked_post_deleted' "notifications.liked"
end end
def linked_object def deleted_translation_key
post = self.target "notifications.liked_post_deleted"
post = post.target if post.is_a? Like end
post
def self.notify(like, _recipient_user_ids)
actor = like.author
target_author = like.target.author
return unless like.target_type == "Post" && target_author.local? && actor != target_author
concatenate_or_create(target_author.owner, like.target, actor).email_the_user(like, actor)
end
end end
end end

View file

@ -1,17 +1,31 @@
class Notifications::Mentioned < Notification module Notifications
def mail_job class Mentioned < Notification
Workers::Mail::Mentioned def mail_job
end Workers::Mail::Mentioned
end
def popup_translation_key
'notifications.mentioned'
end
def deleted_translation_key def popup_translation_key
'notifications.mentioned_deleted' "notifications.mentioned"
end end
def linked_object def deleted_translation_key
Mention.find(self.target_id).post "notifications.mentioned_deleted"
end
def linked_object
target.post
end
def self.notify(mentionable, recipient_user_ids)
actor = mentionable.author
mentionable.mentions.select {|mention| mention.person.local? }.each do |mention|
recipient = mention.person
next if recipient == actor || !(mentionable.public || recipient_user_ids.include?(recipient.owner_id))
create_notification(recipient.owner_id, mention, actor).email_the_user(mention, actor)
end
end
end end
end end

View file

@ -1,15 +1,30 @@
class Notifications::PrivateMessage < Notification module Notifications
def mail_job class PrivateMessage < Notification
Workers::Mail::PrivateMessage def mail_job
end Workers::Mail::PrivateMessage
def popup_translation_key end
'notifications.private_message'
end def popup_translation_key
def self.make_notification(recipient, target, actor, notification_type) "notifications.private_message"
n = notification_type.new(:target => target, end
:recipient_id => recipient.id)
target.increase_unread(recipient) def self.notify(object, recipient_user_ids)
n.actors << actor case object
n when Conversation
object.messages.each do |message|
recipient_ids = recipient_user_ids - [message.author.owner_id]
User.where(id: recipient_ids).find_each {|recipient| notify_message(message, recipient) }
end
when Message
recipients = object.conversation.participants.select(&:local?) - [object.author]
recipients.each {|recipient| notify_message(object, recipient.owner) }
end
end
def self.notify_message(message, recipient)
message.increase_unread(recipient)
new(recipient: recipient).email_the_user(message, message.author)
end
private_class_method :notify_message
end end
end end

View file

@ -1,8 +0,0 @@
class Notifications::RequestAccepted < Notification
def mail_job
Workers::Mail::RequestAcceptance
end
def popup_translation_key
'notifications.request_accepted'
end
end

View file

@ -1,17 +1,22 @@
class Notifications::Reshared < Notification module Notifications
def mail_job class Reshared < Notification
Workers::Mail::Reshared def mail_job
end Workers::Mail::Reshared
end
def popup_translation_key def popup_translation_key
'notifications.reshared' "notifications.reshared"
end end
def deleted_translation_key def deleted_translation_key
'notifications.reshared_post_deleted' "notifications.reshared_post_deleted"
end end
def linked_object def self.notify(reshare, _recipient_user_ids)
self.target return unless reshare.root.present? && reshare.root.author.local?
actor = reshare.author
concatenate_or_create(reshare.root.author.owner, reshare.root, actor).email_the_user(reshare, actor)
end
end end
end end

View file

@ -1,20 +1,16 @@
class Notifications::StartedSharing < Notification module Notifications
def mail_job class StartedSharing < Notification
Workers::Mail::StartedSharing def mail_job
Workers::Mail::StartedSharing
end
def popup_translation_key
"notifications.started_sharing"
end
def self.notify(contact, _recipient_user_ids)
sender = contact.person
create_notification(contact.user_id, sender, sender).email_the_user(sender, sender)
end
end end
def popup_translation_key
'notifications.started_sharing'
end
def email_the_user(target, actor)
super(target.sender, actor)
end
private
def self.make_notification(recipient, target, actor, notification_type)
super(recipient, target.sender, actor, notification_type)
end
end end

View file

@ -0,0 +1,21 @@
class NotificationService
NOTIFICATION_TYPES = {
Comment => [Notifications::CommentOnPost, Notifications::AlsoCommented],
Like => [Notifications::Liked],
StatusMessage => [Notifications::Mentioned],
Conversation => [Notifications::PrivateMessage],
Message => [Notifications::PrivateMessage],
Reshare => [Notifications::Reshared],
Contact => [Notifications::StartedSharing]
}.freeze
def notify(object, recipient_user_ids)
notification_types(object).each {|type| type.notify(object, recipient_user_ids) }
end
private
def notification_types(object)
NOTIFICATION_TYPES.fetch(object.class, [])
end
end

View file

@ -1,9 +1,9 @@
.media.stream_element{:data=>{:guid => note.id, :type => (Notification.types.key(note.type) || '') }, :class => (note.unread ? 'unread' : 'read')} .media.stream_element{:data=>{:guid => note.id, :type => (Notification.types.key(note.type) || '') }, :class => (note.unread ? 'unread' : 'read')}
.unread-toggle.pull-right .unread-toggle.pull-right
%i.entypo-eye{title: (note.unread ? t("notifications.index.mark_read") : t("notifications.index.mark_unread"))} %i.entypo-eye{title: (note.unread ? t("notifications.index.mark_read") : t("notifications.index.mark_unread"))}
- if note.type == "Notifications::StartedSharing" && contact = current_user.contact_for(note.effective_target) - if note.type == "Notifications::StartedSharing" && contact = current_user.contact_for(note.target)
.pull-right .pull-right
= aspect_membership_dropdown(contact, note.effective_target, "right") = aspect_membership_dropdown(contact, note.target, "right")
.media-object.pull-left .media-object.pull-left
= person_image_link note.actors.first, :size => :thumb_small, :class => 'hovercardable' = person_image_link note.actors.first, :size => :thumb_small, :class => 'hovercardable'

View file

@ -5,7 +5,7 @@ module Workers
def perform(object_class_string, object_id, recipient_user_ids) def perform(object_class_string, object_id, recipient_user_ids)
object = object_class_string.constantize.find(object_id) object = object_class_string.constantize.find(object_id)
# TODO: create visibilities # TODO: create visibilities
# TODO: send notifications NotificationService.new.notify(object, recipient_user_ids)
rescue ActiveRecord::RecordNotFound # Already deleted before the job could run rescue ActiveRecord::RecordNotFound # Already deleted before the job could run
end end
end end

View file

@ -92,14 +92,13 @@ describe "Receive federation messages feature" do
expect(new_contact).not_to be_nil expect(new_contact).not_to be_nil
expect(new_contact.sharing).to eq(true) expect(new_contact.sharing).to eq(true)
# TODO: handle notifications expect(
# expect( Notifications::StartedSharing.exists?(
# Notifications::StartedSharing.exists?( recipient_id: alice.id,
# recipient_id: alice.id, target_type: "Person",
# target_type: "Person", target_id: sender.person.id
# target_id: sender.person.id )
# ) ).to be_truthy
# ).to be_truthy
end end
context "with sharing" do context "with sharing" do

View file

@ -26,8 +26,6 @@ shared_examples_for "messages which are indifferent about sharing fact" do
describe "notifications are sent where required" do describe "notifications are sent where required" do
it "for comment on local post" do it "for comment on local post" do
skip("TODO: handle notifications") # TODO
entity = create_relayable_entity(:comment_entity, local_parent, remote_user_on_pod_b.diaspora_handle) entity = create_relayable_entity(:comment_entity, local_parent, remote_user_on_pod_b.diaspora_handle)
post_message(generate_xml(entity, sender, recipient), recipient) post_message(generate_xml(entity, sender, recipient), recipient)
@ -41,8 +39,6 @@ shared_examples_for "messages which are indifferent about sharing fact" do
end end
it "for like on local post" do it "for like on local post" do
skip("TODO: handle notifications") # TODO
entity = create_relayable_entity(:like_entity, local_parent, remote_user_on_pod_b.diaspora_handle) entity = create_relayable_entity(:like_entity, local_parent, remote_user_on_pod_b.diaspora_handle)
post_message(generate_xml(entity, sender, recipient), recipient) post_message(generate_xml(entity, sender, recipient), recipient)
@ -134,8 +130,6 @@ shared_examples_for "messages which can't be send without sharing" do
# this one shouldn't depend on the sharing fact. this must be fixed # this one shouldn't depend on the sharing fact. this must be fixed
describe "notifications are sent where required" do describe "notifications are sent where required" do
it "for comment on remote post where we participate" do it "for comment on remote post where we participate" do
skip("TODO: handle notifications") # TODO
alice.participate!(remote_parent) alice.participate!(remote_parent)
author_id = remote_user_on_pod_c.diaspora_handle author_id = remote_user_on_pod_c.diaspora_handle
entity = create_relayable_entity(:comment_entity, remote_parent, author_id) entity = create_relayable_entity(:comment_entity, remote_parent, author_id)

View file

@ -0,0 +1,47 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
require "spec_helper"
describe Notifications::AlsoCommented, type: :model do
let(:sm) { FactoryGirl.build(:status_message, author: alice.person, public: true) }
let(:comment) { FactoryGirl.create(:comment, commentable: sm) }
let(:notification) { Notifications::AlsoCommented.new(recipient: bob) }
describe ".notify" do
it "does not notify the commentable author" do
expect(Notifications::AlsoCommented).not_to receive(:concatenate_or_create)
Notifications::AlsoCommented.notify(comment, [])
end
it "notifies a local participant" do
bob.participate!(sm)
expect(Notifications::AlsoCommented).to receive(:concatenate_or_create).with(
bob, sm, comment.author
).and_return(notification)
expect(bob).to receive(:mail).with(Workers::Mail::AlsoCommented, bob.id, comment.author.id, comment.id)
Notifications::AlsoCommented.notify(comment, [])
end
it "does not notify the a remote participant" do
FactoryGirl.create(:participation, target: sm)
expect(Notifications::AlsoCommented).not_to receive(:concatenate_or_create)
Notifications::AlsoCommented.notify(comment, [])
end
it "does not notify the author of the comment" do
bob.participate!(sm)
comment = FactoryGirl.create(:comment, commentable: sm, author: bob.person)
expect(Notifications::AlsoCommented).not_to receive(:concatenate_or_create)
Notifications::AlsoCommented.notify(comment, [])
end
end
end