2016-11-20 10:33:02 +11:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
class NotifyService < BaseService
|
2022-04-09 02:03:31 +10:00
|
|
|
include Redisable
|
|
|
|
|
2023-03-30 23:44:00 +11:00
|
|
|
NON_EMAIL_TYPES = %i(
|
|
|
|
admin.report
|
|
|
|
admin.sign_up
|
2023-04-08 20:51:14 +10:00
|
|
|
update
|
2023-04-17 21:13:36 +10:00
|
|
|
poll
|
2023-09-12 04:23:13 +10:00
|
|
|
status
|
2023-03-30 23:44:00 +11:00
|
|
|
).freeze
|
|
|
|
|
2020-09-19 01:26:45 +10:00
|
|
|
def call(recipient, type, activity)
|
2016-11-20 10:33:02 +11:00
|
|
|
@recipient = recipient
|
|
|
|
@activity = activity
|
2020-09-19 01:26:45 +10:00
|
|
|
@notification = Notification.new(account: @recipient, type: type, activity: @activity)
|
2016-11-20 10:33:02 +11:00
|
|
|
|
2017-04-17 02:04:05 +10:00
|
|
|
return if recipient.user.nil? || blocked?
|
2016-11-20 10:33:02 +11:00
|
|
|
|
2022-04-09 02:03:31 +10:00
|
|
|
@notification.save!
|
|
|
|
|
|
|
|
# It's possible the underlying activity has been deleted
|
|
|
|
# between the save call and now
|
|
|
|
return if @notification.activity.nil?
|
|
|
|
|
2019-12-02 03:25:29 +11:00
|
|
|
push_notification!
|
2018-10-08 08:44:58 +11:00
|
|
|
push_to_conversation! if direct_message?
|
2022-04-09 02:03:31 +10:00
|
|
|
send_email! if email_needed?
|
2016-11-23 03:32:51 +11:00
|
|
|
rescue ActiveRecord::RecordInvalid
|
2020-09-08 11:41:16 +10:00
|
|
|
nil
|
2016-11-20 10:33:02 +11:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2016-11-21 20:37:34 +11:00
|
|
|
def blocked_mention?
|
2020-09-08 11:41:16 +10:00
|
|
|
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
|
2016-11-21 20:37:34 +11:00
|
|
|
end
|
|
|
|
|
2017-11-15 07:12:57 +11:00
|
|
|
def following_sender?
|
|
|
|
return @following_sender if defined?(@following_sender)
|
2023-02-20 16:58:28 +11:00
|
|
|
|
2017-11-15 07:12:57 +11:00
|
|
|
@following_sender = @recipient.following?(@notification.from_account) || @recipient.requested?(@notification.from_account)
|
|
|
|
end
|
|
|
|
|
|
|
|
def optional_non_follower?
|
2023-03-30 23:44:00 +11:00
|
|
|
@recipient.user.settings['interactions.must_be_follower'] && !@notification.from_account.following?(@recipient)
|
2017-11-15 07:12:57 +11:00
|
|
|
end
|
|
|
|
|
|
|
|
def optional_non_following?
|
2023-03-30 23:44:00 +11:00
|
|
|
@recipient.user.settings['interactions.must_be_following'] && !following_sender?
|
2017-11-15 07:12:57 +11:00
|
|
|
end
|
|
|
|
|
2018-10-31 01:02:55 +11:00
|
|
|
def message?
|
|
|
|
@notification.type == :mention
|
|
|
|
end
|
|
|
|
|
2017-11-15 07:12:57 +11:00
|
|
|
def direct_message?
|
2018-10-31 01:02:55 +11:00
|
|
|
message? && @notification.target_status.direct_visibility?
|
2017-11-15 07:12:57 +11:00
|
|
|
end
|
|
|
|
|
2022-02-23 06:14:17 +11:00
|
|
|
# Returns true if the sender has been mentioned by the recipient up the thread
|
2017-11-15 07:12:57 +11:00
|
|
|
def response_to_recipient?
|
2021-11-26 09:46:30 +11: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
|
2022-03-30 19:26:51 +11:00
|
|
|
!Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id, depth_limit: 100]).zero?
|
|
|
|
WITH RECURSIVE ancestors(id, in_reply_to_id, mention_id, path, depth) AS (
|
|
|
|
SELECT s.id, s.in_reply_to_id, m.id, ARRAY[s.id], 0
|
2021-11-26 09:46:30 +11:00
|
|
|
FROM statuses s
|
2022-03-30 19:26:51 +11:00
|
|
|
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
2021-11-26 09:46:30 +11:00
|
|
|
WHERE s.id = :id
|
|
|
|
UNION ALL
|
2024-05-30 22:03:13 +10:00
|
|
|
SELECT s.id, s.in_reply_to_id, m.id, ancestors.path || s.id, ancestors.depth + 1
|
|
|
|
FROM ancestors
|
|
|
|
JOIN statuses s ON s.id = ancestors.in_reply_to_id
|
|
|
|
/* early exit if we already have a mention matching our requirements */
|
|
|
|
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id AND s.account_id = :recipient_id
|
|
|
|
WHERE ancestors.mention_id IS NULL AND NOT s.id = ANY(path) AND ancestors.depth < :depth_limit
|
2021-11-26 09:46:30 +11:00
|
|
|
)
|
|
|
|
SELECT COUNT(*)
|
2024-05-30 22:03:13 +10:00
|
|
|
FROM ancestors
|
|
|
|
JOIN statuses s ON s.id = ancestors.id
|
|
|
|
WHERE ancestors.mention_id IS NOT NULL AND s.account_id = :recipient_id AND s.visibility = 3
|
2021-11-26 09:46:30 +11:00
|
|
|
SQL
|
2017-11-15 07:12:57 +11:00
|
|
|
end
|
|
|
|
|
2018-10-17 04:55:05 +11:00
|
|
|
def from_staff?
|
2024-08-07 16:52:10 +10:00
|
|
|
sender = @notification.from_account
|
2024-08-19 17:52:32 +10:00
|
|
|
sender.local? && sender.user.present? && sender.user_role&.overrides?(@recipient.user_role) && sender.user_role&.highlighted? && sender.user_role&.can?(*UserRole::Flags::CATEGORIES[:moderation].map(&:to_sym))
|
2018-10-17 04:55:05 +11:00
|
|
|
end
|
|
|
|
|
2017-11-15 07:12:57 +11:00
|
|
|
def optional_non_following_and_direct?
|
|
|
|
direct_message? &&
|
2023-03-30 23:44:00 +11:00
|
|
|
@recipient.user.settings['interactions.must_be_following_dm'] &&
|
2017-11-15 07:12:57 +11:00
|
|
|
!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-20 10:33:02 +11:00
|
|
|
def blocked?
|
2022-04-09 02:03:31 +10:00
|
|
|
blocked = @recipient.suspended?
|
|
|
|
blocked ||= from_self? && @notification.type != :poll
|
2018-10-31 01:02:55 +11:00
|
|
|
|
|
|
|
return blocked if message? && from_staff?
|
|
|
|
|
2022-02-24 02:45:22 +11:00
|
|
|
blocked ||= domain_blocking?
|
|
|
|
blocked ||= @recipient.blocking?(@notification.from_account)
|
2017-11-15 13:56:41 +11:00
|
|
|
blocked ||= @recipient.muting_notifications?(@notification.from_account)
|
2022-02-24 02:45:22 +11:00
|
|
|
blocked ||= hellbanned?
|
|
|
|
blocked ||= optional_non_follower?
|
|
|
|
blocked ||= optional_non_following?
|
|
|
|
blocked ||= optional_non_following_and_direct?
|
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 11:04:13 +10:00
|
|
|
blocked ||= conversation_muted?
|
2022-02-24 02:45:22 +11:00
|
|
|
blocked ||= blocked_mention? if @notification.type == :mention
|
2016-11-20 10:33:02 +11: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 11:04:13 +10:00
|
|
|
def conversation_muted?
|
|
|
|
if @notification.target_status
|
|
|
|
@recipient.muting_conversation?(@notification.target_status.conversation)
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-04-09 02:03:31 +10:00
|
|
|
def push_notification!
|
|
|
|
push_to_streaming_api! if subscribed_to_streaming_api?
|
|
|
|
push_to_web_push_subscriptions!
|
2018-05-11 19:49:12 +10:00
|
|
|
end
|
|
|
|
|
2022-04-09 02:03:31 +10:00
|
|
|
def push_to_streaming_api!
|
|
|
|
redis.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
|
|
|
|
end
|
2018-05-11 19:49:12 +10:00
|
|
|
|
2022-04-09 02:03:31 +10:00
|
|
|
def subscribed_to_streaming_api?
|
|
|
|
redis.exists?("subscribed:timeline:#{@recipient.id}") || redis.exists?("subscribed:timeline:#{@recipient.id}:notifications")
|
2017-07-14 06:15:32 +10:00
|
|
|
end
|
|
|
|
|
2018-10-08 08:44:58 +11:00
|
|
|
def push_to_conversation!
|
|
|
|
AccountConversation.add_status(@recipient, @notification.target_status)
|
|
|
|
end
|
|
|
|
|
2022-04-09 02:03:31 +10:00
|
|
|
def push_to_web_push_subscriptions!
|
|
|
|
::Web::PushNotificationWorker.push_bulk(web_push_subscriptions.select { |subscription| subscription.pushable?(@notification) }) { |subscription| [subscription.id, @notification.id] }
|
|
|
|
end
|
2017-07-19 00:25:40 +10:00
|
|
|
|
2022-04-09 02:03:31 +10:00
|
|
|
def web_push_subscriptions
|
|
|
|
@web_push_subscriptions ||= ::Web::PushSubscription.where(user_id: @recipient.user.id).to_a
|
|
|
|
end
|
|
|
|
|
|
|
|
def subscribed_to_web_push?
|
|
|
|
web_push_subscriptions.any?
|
2016-11-20 10:33:02 +11:00
|
|
|
end
|
|
|
|
|
2018-10-08 08:44:58 +11:00
|
|
|
def send_email!
|
2023-07-10 11:06:22 +10:00
|
|
|
return unless NotificationMailer.respond_to?(@notification.type)
|
|
|
|
|
|
|
|
NotificationMailer
|
|
|
|
.with(recipient: @recipient, notification: @notification)
|
|
|
|
.public_send(@notification.type)
|
|
|
|
.deliver_later(wait: 2.minutes)
|
2022-04-09 02:03:31 +10:00
|
|
|
end
|
|
|
|
|
|
|
|
def email_needed?
|
|
|
|
(!recipient_online? || always_send_emails?) && send_email_for_notification_type?
|
|
|
|
end
|
|
|
|
|
|
|
|
def recipient_online?
|
|
|
|
subscribed_to_streaming_api? || subscribed_to_web_push?
|
|
|
|
end
|
|
|
|
|
|
|
|
def always_send_emails?
|
|
|
|
@recipient.user.settings.always_send_emails
|
2016-11-20 10:33:02 +11:00
|
|
|
end
|
|
|
|
|
2022-04-09 02:03:31 +10:00
|
|
|
def send_email_for_notification_type?
|
2023-03-30 23:44:00 +11:00
|
|
|
NON_EMAIL_TYPES.exclude?(@notification.type) && @recipient.user.settings["notification_emails.#{@notification.type}"]
|
2016-11-20 10:33:02 +11:00
|
|
|
end
|
|
|
|
end
|