return [nil, message] if message['type'] == 'delete'
status = Status.find_by(id: message['id'])
- message['message'] = FeedManager.instance.inline_render(current_user.account, status)
+ message['message'] = FeedManager.instance.inline_render(current_user.account, 'api/v1/statuses/show', status)
[status, message]
end
--- /dev/null
+# frozen_string_literal: true
+
+class Api::V1::NotificationsController < ApiController
+ before_action -> { doorkeeper_authorize! :read }
+ before_action :require_user!
+
+ respond_to :json
+
+ def index
+ @notifications = Notification.where(account: current_account).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
+
+ next_path = api_v1_notifications_url(max_id: @notifications.last.id) if @notifications.size == 20
+ prev_path = api_v1_notifications_url(since_id: @notifications.first.id) unless @notifications.empty?
+
+ set_pagination_headers(next_path, prev_path)
+ end
+end
def push(timeline_type, account, status)
redis.zadd(key(timeline_type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
trim(timeline_type, account.id)
- broadcast(account.id, type: 'update', timeline: timeline_type, message: inline_render(account, status))
+ broadcast(account.id, type: 'update', timeline: timeline_type, message: inline_render(account, 'api/v1/statuses/show', status))
end
def broadcast(timeline_id, options = {})
redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}")
end
- def inline_render(target_account, status)
+ def inline_render(target_account, template, object)
rabl_scope = Class.new do
include RoutingHelper
end
end
- Rabl::Renderer.new('api/v1/statuses/show', status, view_path: 'app/views', format: :json, scope: rabl_scope.new(target_account)).render
+ Rabl::Renderer.new(template, object, view_path: 'app/views', format: :json, scope: rabl_scope.new(target_account)).render
end
private
class NotificationMailer < ApplicationMailer
helper StreamEntriesHelper
- def mention(mentioned_account, status)
- @me = mentioned_account
- @status = status
-
- return unless @me.user.settings(:notification_emails).mention
+ def mention(recipient, notification)
+ @me = recipient
+ @status = notification.target_status
I18n.with_locale(@me.user.locale || I18n.default_locale) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
end
end
- def follow(followed_account, follower)
- @me = followed_account
- @account = follower
-
- return unless @me.user.settings(:notification_emails).follow
+ def follow(recipient, notification)
+ @me = recipient
+ @account = notification.from_account
I18n.with_locale(@me.user.locale || I18n.default_locale) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
end
end
- def favourite(target_status, from_account)
- @me = target_status.account
- @account = from_account
- @status = target_status
-
- return unless @me.user.settings(:notification_emails).favourite
+ def favourite(recipient, notification)
+ @me = recipient
+ @account = notification.from_account
+ @status = notification.target_status
I18n.with_locale(@me.user.locale || I18n.default_locale) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
end
end
- def reblog(target_status, from_account)
- @me = target_status.account
- @account = from_account
- @status = target_status
-
- return unless @me.user.settings(:notification_emails).reblog
+ def reblog(recipient, notification)
+ @me = recipient
+ @account = notification.from_account
+ @status = notification.target_status
I18n.with_locale(@me.user.locale || I18n.default_locale) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
--- /dev/null
+# frozen_string_literal: true
+
+class Notification < ApplicationRecord
+ include Paginable
+
+ belongs_to :account
+ belongs_to :activity, polymorphic: true
+
+ belongs_to :mention, foreign_type: 'Mention', foreign_key: 'activity_id'
+ belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id'
+ belongs_to :follow, foreign_type: 'Follow', foreign_key: 'activity_id'
+ belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id'
+
+ STATUS_INCLUDES = [:account, :media_attachments, mentions: :account, reblog: [:account, mentions: :account]].freeze
+
+ scope :with_includes, -> { includes(status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account) }
+
+ def type
+ case activity_type
+ when 'Status'
+ :reblog
+ else
+ activity_type.downcase.to_sym
+ end
+ end
+
+ def from_account
+ case type
+ when :mention
+ activity.status.account
+ when :follow, :favourite, :reblog
+ activity.account
+ end
+ end
+
+ def target_status
+ case type
+ when :reblog
+ activity.reblog
+ when :favourite, :mention
+ activity.status
+ end
+ end
+end
HubPingWorker.perform_async(account.id)
if status.local?
- NotificationMailer.favourite(status, account).deliver_later unless status.account.blocking?(account)
+ NotifyService.new.call(status.account, favourite)
else
NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
end
follow = source_account.follow!(target_account)
if target_account.local?
- NotificationMailer.follow(target_account, source_account).deliver_later unless target_account.blocking?(source_account)
+ NotifyService.new.call(target_account, follow)
else
subscribe_service.call(target_account)
NotificationWorker.perform_async(follow.stream_entry.id, target_account.id)
--- /dev/null
+# frozen_string_literal: true
+
+class NotifyService < BaseService
+ def call(recipient, activity)
+ @recipient = recipient
+ @activity = activity
+ @notification = Notification.new(account: @recipient, activity: @activity)
+
+ return if blocked?
+
+ create_notification
+ send_email if email_enabled?
+ end
+
+ private
+
+ def blocked?
+ blocked = false
+ blocked ||= @recipient.id == @notification.from_account.id
+ blocked ||= @recipient.blocking?(@notification.from_account)
+ blocked
+ end
+
+ def create_notification
+ @notification.save!
+ FeedManager.instance.broadcast(@recipient.id, type: 'notification', message: FeedManager.instance.inline_render(@recipient, 'api/v1/notifications/show', @notification))
+ end
+
+ def send_email
+ NotificationMailer.send(@notification.type, @recipient, @notification).deliver_later
+ end
+
+ def email_enabled?
+ @recipient.user.settings(:notification_emails).send(@notification.type)
+ end
+end
next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id)
- if mentioned_account.local?
- # Send notifications
- NotificationMailer.mention(mentioned_account, parent).deliver_later unless mentioned_account.blocking?(parent.account)
- end
+ mention = mentioned_account.mentions.where(status: parent).first_or_create(status: parent)
- mentioned_account.mentions.where(status: parent).first_or_create(status: parent)
+ # Notify local user
+ NotifyService.new.call(mentioned_account, mention) if mentioned_account.local?
# So we can skip duplicate mentions
processed_account_ids << mentioned_account.id
end
def follow!(account, target_account)
- account.follow!(target_account)
- NotificationMailer.follow(target_account, account).deliver_later unless target_account.blocking?(account)
+ follow = account.follow!(target_account)
+ NotifyService.new.call(target_account, follow)
end
def unfollow!(account, target_account)
def favourite!(xml, from_account)
current_status = status(xml)
- current_status.favourites.where(account: from_account).first_or_create!(account: from_account)
- NotificationMailer.favourite(current_status, from_account).deliver_later unless current_status.account.blocking?(from_account)
+ favourite = current_status.favourites.where(account: from_account).first_or_create!(account: from_account)
+ NotifyService.new.call(current_status.account, favourite)
end
def add_post!(body, account)
mentioned_account = mention.account
if mentioned_account.local?
- NotificationMailer.mention(mentioned_account, status).deliver_later unless mentioned_account.blocking?(status.account)
+ NotifyService.new.call(mentioned_account, mention)
else
NotificationWorker.perform_async(status.stream_entry.id, mentioned_account.id)
end
HubPingWorker.perform_async(account.id)
if reblogged_status.local?
- NotificationMailer.reblog(reblogged_status, account).deliver_later unless reblogged_status.account.blocking?(account)
+ NotifyService.new.call(reblogged_status.account, reblog)
else
NotificationWorker.perform_async(reblog.stream_entry.id, reblogged_status.account_id)
end
--- /dev/null
+collection @notifications
+extends 'api/v1/notifications/show'
--- /dev/null
+object @notification
+
+attributes :id, :type
+
+child from_account: :account do
+ extends 'api/v1/accounts/show'
+end
+
+node(:status, if: lambda { |n| [:favourite, :reblog, :mention].include?(n.type) }) do |n|
+ partial 'api/v1/statuses/show', object: n.target_status
+end
resources :media, only: [:create]
resources :apps, only: [:create]
+ resources :notifications, only: [:index]
+
resources :accounts, only: [:show] do
collection do
get :relationships
--- /dev/null
+class CreateNotifications < ActiveRecord::Migration[5.0]
+ def change
+ create_table :notifications do |t|
+ t.integer :account_id
+ t.integer :activity_id
+ t.string :activity_type
+
+ t.timestamps
+ end
+
+ add_index :notifications, :account_id
+ end
+end
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161116162355) do
+ActiveRecord::Schema.define(version: 20161119211120) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree
end
+ create_table "notifications", force: :cascade do |t|
+ t.integer "account_id"
+ t.integer "activity_id"
+ t.string "activity_type"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id"], name: "index_notifications_on_account_id", using: :btree
+ end
+
create_table "oauth_access_grants", force: :cascade do |t|
t.integer "resource_owner_id", null: false
t.integer "application_id", null: false
--- /dev/null
+Fabricator(:notification) do
+ activity_id 1
+ activity_type "MyString"
+end
let(:own_status) { Fabricate(:status, account: receiver.account) }
describe "mention" do
- let(:mail) { NotificationMailer.mention(receiver.account, foreign_status) }
+ let(:mention) { Mention.create!(account: receiver.account, status: foreign_status) }
+ let(:mail) { NotificationMailer.mention(receiver.account, Notification.create!(account: receiver.account, activity: mention)) }
it "renders the headers" do
expect(mail.subject).to eq("You were mentioned by bob")
end
describe "follow" do
- let(:mail) { NotificationMailer.follow(receiver.account, sender) }
+ let(:follow) { sender.follow!(receiver.account) }
+ let(:mail) { NotificationMailer.follow(receiver.account, Notification.create!(account: receiver.account, activity: follow)) }
it "renders the headers" do
expect(mail.subject).to eq("bob is now following you")
end
describe "favourite" do
- let(:mail) { NotificationMailer.favourite(own_status, sender) }
+ let(:favourite) { Favourite.create!(account: sender, status: own_status) }
+ let(:mail) { NotificationMailer.favourite(own_status.account, Notification.create!(account: receiver.account, activity: favourite)) }
it "renders the headers" do
expect(mail.subject).to eq("bob favourited your status")
end
describe "reblog" do
- let(:mail) { NotificationMailer.reblog(own_status, sender) }
+ let(:reblog) { Status.create!(account: sender, reblog: own_status) }
+ let(:mail) { NotificationMailer.reblog(own_status.account, Notification.create!(account: receiver.account, activity: reblog)) }
it "renders the headers" do
expect(mail.subject).to eq("bob reblogged your status")
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe Notification, type: :model do
+ describe '#from_account' do
+ pending
+ end
+
+ describe '#type' do
+ it 'returns :reblog for a Status' do
+ notification = Notification.new(activity: Status.new)
+ expect(notification.type).to eq :reblog
+ end
+
+ it 'returns :mention for a Mention' do
+ notification = Notification.new(activity: Mention.new)
+ expect(notification.type).to eq :mention
+ end
+
+ it 'returns :favourite for a Favourite' do
+ notification = Notification.new(activity: Favourite.new)
+ expect(notification.type).to eq :favourite
+ end
+
+ it 'returns :follow for a Follow' do
+ notification = Notification.new(activity: Follow.new)
+ expect(notification.type).to eq :follow
+ end
+ end
+end