2016-11-19 23:33:02 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
class NotifyService < BaseService
|
2020-09-18 15:26:45 +00:00
|
|
|
def call(recipient, type, activity)
|
2016-11-19 23:33:02 +00:00
|
|
|
@recipient = recipient
|
|
|
|
@activity = activity
|
2020-09-18 15:26:45 +00:00
|
|
|
@notification = Notification.new(account: @recipient, type: type, activity: @activity)
|
2016-11-19 23:33:02 +00:00
|
|
|
|
2017-04-16 16:04:05 +00:00
|
|
|
return if recipient.user.nil? || blocked?
|
2016-11-19 23:33:02 +00:00
|
|
|
|
2018-10-07 21:44:58 +00:00
|
|
|
create_notification!
|
2019-12-01 16:25:29 +00:00
|
|
|
push_notification!
|
2018-10-07 21:44:58 +00:00
|
|
|
push_to_conversation! if direct_message?
|
|
|
|
send_email! if email_enabled?
|
2016-11-22 16:32:51 +00:00
|
|
|
rescue ActiveRecord::RecordInvalid
|
2020-09-08 01:41:16 +00:00
|
|
|
nil
|
2016-11-19 23:33:02 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2016-11-21 09:37:34 +00:00
|
|
|
def blocked_mention?
|
2020-09-08 01:41:16 +00:00
|
|
|
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
|
2016-11-21 09:37:34 +00:00
|
|
|
end
|
|
|
|
|
2020-09-18 15:26:45 +00:00
|
|
|
def blocked_status?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2016-11-21 09:37:34 +00:00
|
|
|
def blocked_favourite?
|
2017-07-22 14:36:36 +00:00
|
|
|
false
|
2016-11-21 09:37:34 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def blocked_follow?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
def blocked_reblog?
|
2018-10-29 23:47:31 +00:00
|
|
|
false
|
2016-11-21 09:37:34 +00:00
|
|
|
end
|
|
|
|
|
2016-12-26 20:52:03 +00:00
|
|
|
def blocked_follow_request?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2019-03-10 23:49:31 +00:00
|
|
|
def blocked_poll?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2021-05-19 05:58:27 +00:00
|
|
|
def blocked_emoji_reaction?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2022-03-17 14:25:16 +00:00
|
|
|
def blocked_status_reference?
|
|
|
|
FeedManager.instance.filter?(:status_references, @notification.status_reference.status, @recipient)
|
|
|
|
end
|
|
|
|
|
2023-01-08 16:07:46 +00:00
|
|
|
def blocked_scheduled_status?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2017-11-14 20:12:57 +00:00
|
|
|
def following_sender?
|
|
|
|
return @following_sender if defined?(@following_sender)
|
|
|
|
@following_sender = @recipient.following?(@notification.from_account) || @recipient.requested?(@notification.from_account)
|
|
|
|
end
|
|
|
|
|
|
|
|
def optional_non_follower?
|
|
|
|
@recipient.user.settings.interactions['must_be_follower'] && !@notification.from_account.following?(@recipient)
|
|
|
|
end
|
|
|
|
|
|
|
|
def optional_non_following?
|
|
|
|
@recipient.user.settings.interactions['must_be_following'] && !following_sender?
|
|
|
|
end
|
|
|
|
|
2022-09-11 14:03:39 +00:00
|
|
|
def optional_non_following_and_reference?
|
|
|
|
status_reference? && @recipient.user.settings.interactions['must_be_following_reference'] && !following_sender?
|
|
|
|
end
|
|
|
|
|
2022-09-06 13:06:40 +00:00
|
|
|
def optional_non_direct_message?
|
|
|
|
message? && @recipient.user.settings.interactions['must_be_dm_to_send_email'] && !@notification.target_status.direct_visibility?
|
|
|
|
end
|
|
|
|
|
2018-10-30 14:02:55 +00:00
|
|
|
def message?
|
|
|
|
@notification.type == :mention
|
|
|
|
end
|
|
|
|
|
2022-09-11 14:03:39 +00:00
|
|
|
def status_reference?
|
|
|
|
@notification.type == :status_reference
|
|
|
|
end
|
|
|
|
|
2017-11-14 20:12:57 +00:00
|
|
|
def direct_message?
|
2018-10-30 14:02:55 +00:00
|
|
|
message? && @notification.target_status.direct_visibility?
|
2017-11-14 20:12:57 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def response_to_recipient?
|
2022-02-03 13:07:43 +00:00
|
|
|
return false if @notification.target_status.in_reply_to_id.nil?
|
|
|
|
|
|
|
|
# Using an SQL CTE to avoid unneeded back-and-forth with SQL server in case of long threads
|
|
|
|
!Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id]).zero?
|
|
|
|
WITH RECURSIVE ancestors(id, in_reply_to_id, replying_to_sender, path) AS (
|
|
|
|
SELECT
|
|
|
|
s.id,
|
|
|
|
s.in_reply_to_id,
|
|
|
|
(CASE
|
|
|
|
WHEN s.account_id = :recipient_id THEN
|
|
|
|
EXISTS (
|
|
|
|
SELECT *
|
|
|
|
FROM mentions m
|
|
|
|
WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
|
|
|
)
|
|
|
|
ELSE
|
|
|
|
FALSE
|
|
|
|
END),
|
|
|
|
ARRAY[s.id]
|
|
|
|
FROM statuses s
|
|
|
|
WHERE s.id = :id
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
|
|
s.id,
|
|
|
|
s.in_reply_to_id,
|
|
|
|
(CASE
|
|
|
|
WHEN s.account_id = :recipient_id THEN
|
|
|
|
EXISTS (
|
|
|
|
SELECT *
|
|
|
|
FROM mentions m
|
|
|
|
WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
|
|
|
)
|
|
|
|
ELSE
|
|
|
|
FALSE
|
|
|
|
END),
|
|
|
|
st.path || s.id
|
|
|
|
FROM ancestors st
|
|
|
|
JOIN statuses s ON s.id = st.in_reply_to_id
|
|
|
|
WHERE st.replying_to_sender IS FALSE AND NOT s.id = ANY(path)
|
|
|
|
)
|
|
|
|
SELECT COUNT(*)
|
|
|
|
FROM ancestors st
|
|
|
|
JOIN statuses s ON s.id = st.id
|
|
|
|
WHERE st.replying_to_sender IS TRUE AND s.visibility = 3
|
|
|
|
SQL
|
2017-11-14 20:12:57 +00:00
|
|
|
end
|
|
|
|
|
2018-10-16 17:55:05 +00:00
|
|
|
def from_staff?
|
|
|
|
@notification.from_account.local? && @notification.from_account.user.present? && @notification.from_account.user.staff?
|
|
|
|
end
|
|
|
|
|
2017-11-14 20:12:57 +00:00
|
|
|
def optional_non_following_and_direct?
|
|
|
|
direct_message? &&
|
|
|
|
@recipient.user.settings.interactions['must_be_following_dm'] &&
|
|
|
|
!following_sender? &&
|
|
|
|
!response_to_recipient?
|
|
|
|
end
|
|
|
|
|
|
|
|
def hellbanned?
|
|
|
|
@notification.from_account.silenced? && !following_sender?
|
|
|
|
end
|
|
|
|
|
|
|
|
def from_self?
|
|
|
|
@recipient.id == @notification.from_account.id
|
|
|
|
end
|
|
|
|
|
|
|
|
def domain_blocking?
|
|
|
|
@recipient.domain_blocking?(@notification.from_account.domain) && !following_sender?
|
|
|
|
end
|
|
|
|
|
2016-11-19 23:33:02 +00:00
|
|
|
def blocked?
|
2017-11-14 20:12:57 +00:00
|
|
|
blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway
|
2023-01-08 16:07:46 +00:00
|
|
|
blocked ||= from_self? && !%i(poll scheduled_status).include?(@notification.type) # Skip for interactions with self
|
2018-10-30 14:02:55 +00:00
|
|
|
|
|
|
|
return blocked if message? && from_staff?
|
|
|
|
|
2017-11-14 20:12:57 +00:00
|
|
|
blocked ||= domain_blocking? # Skip for domain blocked accounts
|
|
|
|
blocked ||= @recipient.blocking?(@notification.from_account) # Skip for blocked accounts
|
2017-11-15 02:56:41 +00:00
|
|
|
blocked ||= @recipient.muting_notifications?(@notification.from_account)
|
2017-11-14 20:12:57 +00:00
|
|
|
blocked ||= hellbanned? # Hellban
|
|
|
|
blocked ||= optional_non_follower? # Options
|
|
|
|
blocked ||= optional_non_following? # Options
|
2022-09-11 14:03:39 +00:00
|
|
|
blocked ||= optional_non_following_and_reference? # Options
|
2017-11-14 20:12:57 +00:00
|
|
|
blocked ||= optional_non_following_and_direct? # Options
|
Feature conversations muting (#3017)
* Add <ostatus:conversation /> tag to Atom input/output
Only uses ref attribute (not href) because href would be
the alternate link that's always included also.
Creates new conversation for every non-reply status. Carries
over conversation for every reply. Keeps remote URIs verbatim,
generates local URIs on the fly like the rest of them.
* Conversation muting - prevents notifications that reference a conversation
(including replies, favourites, reblogs) from being created. API endpoints
/api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute
Currently no way to tell when a status/conversation is muted, so the web UI
only has a "disable notifications" button, doesn't work as a toggle
* Display "Dismiss notifications" on all statuses in notifications column, not just own
* Add "muted" as a boolean attribute on statuses JSON
For now always false on contained reblogs, since it's only relevant for
statuses returned from the notifications endpoint, which are not nested
Remove "Disable notifications" from detailed status view, since it's
only relevant in the notifications column
* Up max class length
* Remove pending test for conversation mute
* Add tests, clean up
* Rename to "mute conversation" and "unmute conversation"
* Raise validation error when trying to mute/unmute status without conversation
2017-05-15 01:04:13 +00:00
|
|
|
blocked ||= conversation_muted?
|
2017-11-14 20:12:57 +00:00
|
|
|
blocked ||= send("blocked_#{@notification.type}?") # Type-dependent filters
|
2016-11-19 23:33:02 +00:00
|
|
|
blocked
|
|
|
|
end
|
|
|
|
|
Feature conversations muting (#3017)
* Add <ostatus:conversation /> tag to Atom input/output
Only uses ref attribute (not href) because href would be
the alternate link that's always included also.
Creates new conversation for every non-reply status. Carries
over conversation for every reply. Keeps remote URIs verbatim,
generates local URIs on the fly like the rest of them.
* Conversation muting - prevents notifications that reference a conversation
(including replies, favourites, reblogs) from being created. API endpoints
/api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute
Currently no way to tell when a status/conversation is muted, so the web UI
only has a "disable notifications" button, doesn't work as a toggle
* Display "Dismiss notifications" on all statuses in notifications column, not just own
* Add "muted" as a boolean attribute on statuses JSON
For now always false on contained reblogs, since it's only relevant for
statuses returned from the notifications endpoint, which are not nested
Remove "Disable notifications" from detailed status view, since it's
only relevant in the notifications column
* Up max class length
* Remove pending test for conversation mute
* Add tests, clean up
* Rename to "mute conversation" and "unmute conversation"
* Raise validation error when trying to mute/unmute status without conversation
2017-05-15 01:04:13 +00:00
|
|
|
def conversation_muted?
|
|
|
|
if @notification.target_status
|
|
|
|
@recipient.muting_conversation?(@notification.target_status.conversation)
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-10-07 21:44:58 +00:00
|
|
|
def create_notification!
|
2016-11-19 23:33:02 +00:00
|
|
|
@notification.save!
|
2018-05-11 09:49:12 +00:00
|
|
|
end
|
|
|
|
|
2018-10-07 21:44:58 +00:00
|
|
|
def push_notification!
|
2018-05-11 09:49:12 +00:00
|
|
|
return if @notification.activity.nil?
|
|
|
|
|
2017-07-07 02:02:06 +00:00
|
|
|
Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
|
2018-10-07 21:44:58 +00:00
|
|
|
send_push_notifications!
|
2017-07-13 20:15:32 +00:00
|
|
|
end
|
|
|
|
|
2018-10-07 21:44:58 +00:00
|
|
|
def push_to_conversation!
|
|
|
|
return if @notification.activity.nil?
|
|
|
|
AccountConversation.add_status(@recipient, @notification.target_status)
|
|
|
|
end
|
|
|
|
|
|
|
|
def send_push_notifications!
|
2018-05-11 09:49:12 +00:00
|
|
|
subscriptions_ids = ::Web::PushSubscription.where(user_id: @recipient.user.id)
|
|
|
|
.select { |subscription| subscription.pushable?(@notification) }
|
|
|
|
.map(&:id)
|
2017-07-18 14:25:40 +00:00
|
|
|
|
2018-05-11 09:49:12 +00:00
|
|
|
::Web::PushNotificationWorker.push_bulk(subscriptions_ids) do |subscription_id|
|
|
|
|
[subscription_id, @notification.id]
|
2017-07-18 14:25:40 +00:00
|
|
|
end
|
2016-11-19 23:33:02 +00:00
|
|
|
end
|
|
|
|
|
2018-10-07 21:44:58 +00:00
|
|
|
def send_email!
|
2022-09-06 13:06:40 +00:00
|
|
|
return if @notification.activity.nil? || optional_non_direct_message?
|
2018-08-26 14:53:19 +00:00
|
|
|
NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later(wait: 2.minutes)
|
2016-11-19 23:33:02 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def email_enabled?
|
2017-05-06 21:06:52 +00:00
|
|
|
@recipient.user.settings.notification_emails[@notification.type.to_s]
|
2016-11-19 23:33:02 +00:00
|
|
|
end
|
|
|
|
end
|