2016-11-19 23:33:02 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
class NotifyService < BaseService
|
2022-04-08 16:03:31 +00:00
|
|
|
include Redisable
|
|
|
|
|
2023-03-30 12:44:00 +00:00
|
|
|
NON_EMAIL_TYPES = %i(
|
|
|
|
admin.report
|
|
|
|
admin.sign_up
|
2023-04-08 10:51:14 +00:00
|
|
|
update
|
2023-04-17 11:13:36 +00:00
|
|
|
poll
|
2023-09-11 18:23:13 +00:00
|
|
|
status
|
2023-03-30 12:44:00 +00:00
|
|
|
).freeze
|
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
class DismissCondition
|
|
|
|
def initialize(notification)
|
|
|
|
@recipient = notification.account
|
|
|
|
@sender = notification.from_account
|
|
|
|
@notification = notification
|
|
|
|
end
|
2016-11-19 23:33:02 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def dismiss?
|
|
|
|
blocked = @recipient.unavailable?
|
|
|
|
blocked ||= from_self? && @notification.type != :poll
|
2016-11-19 23:33:02 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
return blocked if message? && from_staff?
|
2022-04-08 16:03:31 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
blocked ||= domain_blocking?
|
|
|
|
blocked ||= @recipient.blocking?(@sender)
|
|
|
|
blocked ||= @recipient.muting_notifications?(@sender)
|
|
|
|
blocked ||= conversation_muted?
|
|
|
|
blocked ||= blocked_mention? if message?
|
|
|
|
blocked
|
|
|
|
end
|
2022-04-08 16:03:31 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
private
|
2016-11-19 23:33:02 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def blocked_mention?
|
|
|
|
FeedManager.instance.filter?(:mentions, @notification.target_status, @recipient)
|
|
|
|
end
|
2016-11-19 23:33:02 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def message?
|
|
|
|
@notification.type == :mention
|
|
|
|
end
|
2016-11-21 09:37:34 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def from_staff?
|
|
|
|
@sender.local? && @sender.user.present? && @sender.user_role&.overrides?(@recipient.user_role)
|
|
|
|
end
|
2023-02-20 05:58:28 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def from_self?
|
|
|
|
@recipient.id == @sender.id
|
|
|
|
end
|
2017-11-14 20:12:57 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def domain_blocking?
|
|
|
|
@recipient.domain_blocking?(@sender.domain) && !following_sender?
|
|
|
|
end
|
2017-11-14 20:12:57 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def conversation_muted?
|
|
|
|
@notification.target_status && @recipient.muting_conversation?(@notification.target_status.conversation)
|
|
|
|
end
|
2017-11-14 20:12:57 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def following_sender?
|
|
|
|
@recipient.following?(@sender)
|
|
|
|
end
|
2018-10-30 14:02:55 +00:00
|
|
|
end
|
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
class FilterCondition
|
|
|
|
NEW_ACCOUNT_THRESHOLD = 30.days.freeze
|
2017-11-14 20:12:57 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
NEW_FOLLOWER_THRESHOLD = 3.days.freeze
|
2017-11-14 20:12:57 +00:00
|
|
|
|
2024-03-13 10:17:55 +00:00
|
|
|
NON_FILTERABLE_TYPES = %i(
|
|
|
|
admin.sign_up
|
|
|
|
admin.report
|
|
|
|
poll
|
|
|
|
update
|
|
|
|
).freeze
|
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def initialize(notification)
|
|
|
|
@notification = notification
|
|
|
|
@recipient = notification.account
|
|
|
|
@sender = notification.from_account
|
|
|
|
@policy = NotificationPolicy.find_or_initialize_by(account: @recipient)
|
|
|
|
end
|
2018-10-16 17:55:05 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def filter?
|
2024-03-13 10:17:55 +00:00
|
|
|
return false if NON_FILTERABLE_TYPES.include?(@notification.type)
|
2024-03-07 14:53:37 +00:00
|
|
|
return false if override_for_sender?
|
|
|
|
|
|
|
|
from_limited? ||
|
|
|
|
filtered_by_not_following_policy? ||
|
|
|
|
filtered_by_not_followers_policy? ||
|
|
|
|
filtered_by_new_accounts_policy? ||
|
|
|
|
filtered_by_private_mentions_policy?
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def filtered_by_not_following_policy?
|
|
|
|
@policy.filter_not_following? && not_following?
|
|
|
|
end
|
|
|
|
|
|
|
|
def filtered_by_not_followers_policy?
|
|
|
|
@policy.filter_not_followers? && not_follower?
|
|
|
|
end
|
|
|
|
|
|
|
|
def filtered_by_new_accounts_policy?
|
|
|
|
@policy.filter_new_accounts? && new_account?
|
|
|
|
end
|
|
|
|
|
|
|
|
def filtered_by_private_mentions_policy?
|
|
|
|
@policy.filter_private_mentions? && not_following? && private_mention_not_in_response?
|
|
|
|
end
|
2017-11-14 20:12:57 +00:00
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def not_following?
|
|
|
|
!@recipient.following?(@sender)
|
|
|
|
end
|
|
|
|
|
|
|
|
def not_follower?
|
|
|
|
follow = Follow.find_by(account: @sender, target_account: @recipient)
|
|
|
|
follow.nil? || follow.created_at > NEW_FOLLOWER_THRESHOLD.ago
|
|
|
|
end
|
|
|
|
|
|
|
|
def new_account?
|
|
|
|
@sender.created_at > NEW_ACCOUNT_THRESHOLD.ago
|
|
|
|
end
|
|
|
|
|
|
|
|
def override_for_sender?
|
|
|
|
NotificationPermission.exists?(account: @recipient, from_account: @sender)
|
|
|
|
end
|
|
|
|
|
|
|
|
def from_limited?
|
|
|
|
@sender.silenced? && not_following?
|
|
|
|
end
|
|
|
|
|
|
|
|
def private_mention_not_in_response?
|
|
|
|
@notification.type == :mention && @notification.target_status.direct_visibility? && !response_to_recipient?
|
|
|
|
end
|
|
|
|
|
|
|
|
def response_to_recipient?
|
|
|
|
return false if @notification.target_status.in_reply_to_id.nil?
|
|
|
|
|
|
|
|
statuses_that_mention_sender.positive?
|
|
|
|
end
|
|
|
|
|
|
|
|
def statuses_that_mention_sender
|
|
|
|
Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @sender.id, depth_limit: 100])
|
|
|
|
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
|
|
|
|
FROM statuses s
|
|
|
|
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
|
|
|
WHERE s.id = :id
|
|
|
|
UNION ALL
|
|
|
|
SELECT s.id, s.in_reply_to_id, m.id, st.path || s.id, st.depth + 1
|
|
|
|
FROM ancestors st
|
|
|
|
JOIN statuses s ON s.id = st.in_reply_to_id
|
|
|
|
LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
|
|
|
WHERE st.mention_id IS NULL AND NOT s.id = ANY(path) AND st.depth < :depth_limit
|
|
|
|
)
|
|
|
|
SELECT COUNT(*)
|
|
|
|
FROM ancestors st
|
|
|
|
JOIN statuses s ON s.id = st.id
|
|
|
|
WHERE st.mention_id IS NOT NULL AND s.visibility = 3
|
|
|
|
SQL
|
|
|
|
end
|
2017-11-14 20:12:57 +00:00
|
|
|
end
|
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def call(recipient, type, activity)
|
|
|
|
return if recipient.user.nil?
|
|
|
|
|
|
|
|
@recipient = recipient
|
|
|
|
@activity = activity
|
|
|
|
@notification = Notification.new(account: @recipient, type: type, activity: @activity)
|
|
|
|
|
|
|
|
# For certain conditions we don't need to create a notification at all
|
|
|
|
return if dismiss?
|
|
|
|
|
|
|
|
@notification.filtered = filter?
|
|
|
|
@notification.save!
|
|
|
|
|
|
|
|
# It's possible the underlying activity has been deleted
|
|
|
|
# between the save call and now
|
|
|
|
return if @notification.activity.nil?
|
|
|
|
|
|
|
|
if @notification.filtered?
|
|
|
|
update_notification_request!
|
|
|
|
else
|
|
|
|
push_notification!
|
|
|
|
push_to_conversation! if direct_message?
|
|
|
|
send_email! if email_needed?
|
|
|
|
end
|
|
|
|
rescue ActiveRecord::RecordInvalid
|
|
|
|
nil
|
2017-11-14 20:12:57 +00:00
|
|
|
end
|
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
private
|
|
|
|
|
|
|
|
def dismiss?
|
|
|
|
DismissCondition.new(@notification).dismiss?
|
2017-11-14 20:12:57 +00:00
|
|
|
end
|
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def filter?
|
|
|
|
FilterCondition.new(@notification).filter?
|
2016-11-19 23:33:02 +00:00
|
|
|
end
|
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def update_notification_request!
|
|
|
|
return unless @notification.type == :mention
|
|
|
|
|
|
|
|
notification_request = NotificationRequest.find_or_initialize_by(account_id: @recipient.id, from_account_id: @notification.from_account_id)
|
|
|
|
notification_request.last_status_id = @notification.target_status.id
|
|
|
|
notification_request.save
|
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
|
|
|
end
|
|
|
|
|
2022-04-08 16:03:31 +00:00
|
|
|
def push_notification!
|
|
|
|
push_to_streaming_api! if subscribed_to_streaming_api?
|
|
|
|
push_to_web_push_subscriptions!
|
2018-05-11 09:49:12 +00:00
|
|
|
end
|
|
|
|
|
2022-04-08 16:03:31 +00: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 09:49:12 +00:00
|
|
|
|
2022-04-08 16:03:31 +00:00
|
|
|
def subscribed_to_streaming_api?
|
|
|
|
redis.exists?("subscribed:timeline:#{@recipient.id}") || redis.exists?("subscribed:timeline:#{@recipient.id}:notifications")
|
2017-07-13 20:15:32 +00:00
|
|
|
end
|
|
|
|
|
2018-10-07 21:44:58 +00:00
|
|
|
def push_to_conversation!
|
|
|
|
AccountConversation.add_status(@recipient, @notification.target_status)
|
|
|
|
end
|
|
|
|
|
2024-03-07 14:53:37 +00:00
|
|
|
def direct_message?
|
|
|
|
@notification.type == :mention && @notification.target_status.direct_visibility?
|
|
|
|
end
|
|
|
|
|
2022-04-08 16:03:31 +00: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-18 14:25:40 +00:00
|
|
|
|
2022-04-08 16:03:31 +00: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-19 23:33:02 +00:00
|
|
|
end
|
|
|
|
|
2018-10-07 21:44:58 +00:00
|
|
|
def send_email!
|
2023-07-10 01:06:22 +00: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-08 16:03:31 +00: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-19 23:33:02 +00:00
|
|
|
end
|
|
|
|
|
2022-04-08 16:03:31 +00:00
|
|
|
def send_email_for_notification_type?
|
2023-03-30 12:44:00 +00:00
|
|
|
NON_EMAIL_TYPES.exclude?(@notification.type) && @recipient.user.settings["notification_emails.#{@notification.type}"]
|
2016-11-19 23:33:02 +00:00
|
|
|
end
|
|
|
|
end
|