Adding unified streamable notifications
This commit is contained in:
parent
3838e6836d
commit
da2ef4d676
20 changed files with 205 additions and 44 deletions
|
@ -10,7 +10,7 @@ module ApplicationCable
|
||||||
return [nil, message] if message['type'] == 'delete'
|
return [nil, message] if message['type'] == 'delete'
|
||||||
|
|
||||||
status = Status.find_by(id: message['id'])
|
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]
|
[status, message]
|
||||||
end
|
end
|
||||||
|
|
17
app/controllers/api/v1/notifications_controller.rb
Normal file
17
app/controllers/api/v1/notifications_controller.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# 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
|
|
@ -26,7 +26,7 @@ class FeedManager
|
||||||
def push(timeline_type, account, status)
|
def push(timeline_type, account, status)
|
||||||
redis.zadd(key(timeline_type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
|
redis.zadd(key(timeline_type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
|
||||||
trim(timeline_type, account.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
|
end
|
||||||
|
|
||||||
def broadcast(timeline_id, options = {})
|
def broadcast(timeline_id, options = {})
|
||||||
|
@ -39,7 +39,7 @@ class FeedManager
|
||||||
redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}")
|
redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def inline_render(target_account, status)
|
def inline_render(target_account, template, object)
|
||||||
rabl_scope = Class.new do
|
rabl_scope = Class.new do
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ class FeedManager
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -3,46 +3,38 @@
|
||||||
class NotificationMailer < ApplicationMailer
|
class NotificationMailer < ApplicationMailer
|
||||||
helper StreamEntriesHelper
|
helper StreamEntriesHelper
|
||||||
|
|
||||||
def mention(mentioned_account, status)
|
def mention(recipient, notification)
|
||||||
@me = mentioned_account
|
@me = recipient
|
||||||
@status = status
|
@status = notification.target_status
|
||||||
|
|
||||||
return unless @me.user.settings(:notification_emails).mention
|
|
||||||
|
|
||||||
I18n.with_locale(@me.user.locale || I18n.default_locale) do
|
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)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow(followed_account, follower)
|
def follow(recipient, notification)
|
||||||
@me = followed_account
|
@me = recipient
|
||||||
@account = follower
|
@account = notification.from_account
|
||||||
|
|
||||||
return unless @me.user.settings(:notification_emails).follow
|
|
||||||
|
|
||||||
I18n.with_locale(@me.user.locale || I18n.default_locale) do
|
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)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def favourite(target_status, from_account)
|
def favourite(recipient, notification)
|
||||||
@me = target_status.account
|
@me = recipient
|
||||||
@account = from_account
|
@account = notification.from_account
|
||||||
@status = target_status
|
@status = notification.target_status
|
||||||
|
|
||||||
return unless @me.user.settings(:notification_emails).favourite
|
|
||||||
|
|
||||||
I18n.with_locale(@me.user.locale || I18n.default_locale) do
|
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)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reblog(target_status, from_account)
|
def reblog(recipient, notification)
|
||||||
@me = target_status.account
|
@me = recipient
|
||||||
@account = from_account
|
@account = notification.from_account
|
||||||
@status = target_status
|
@status = notification.target_status
|
||||||
|
|
||||||
return unless @me.user.settings(:notification_emails).reblog
|
|
||||||
|
|
||||||
I18n.with_locale(@me.user.locale || I18n.default_locale) do
|
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)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
|
||||||
|
|
44
app/models/notification.rb
Normal file
44
app/models/notification.rb
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# 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
|
|
@ -10,7 +10,7 @@ class FavouriteService < BaseService
|
||||||
HubPingWorker.perform_async(account.id)
|
HubPingWorker.perform_async(account.id)
|
||||||
|
|
||||||
if status.local?
|
if status.local?
|
||||||
NotificationMailer.favourite(status, account).deliver_later unless status.account.blocking?(account)
|
NotifyService.new.call(status.account, favourite)
|
||||||
else
|
else
|
||||||
NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
|
NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,7 @@ class FollowService < BaseService
|
||||||
follow = source_account.follow!(target_account)
|
follow = source_account.follow!(target_account)
|
||||||
|
|
||||||
if target_account.local?
|
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
|
else
|
||||||
subscribe_service.call(target_account)
|
subscribe_service.call(target_account)
|
||||||
NotificationWorker.perform_async(follow.stream_entry.id, target_account.id)
|
NotificationWorker.perform_async(follow.stream_entry.id, target_account.id)
|
||||||
|
|
36
app/services/notify_service.rb
Normal file
36
app/services/notify_service.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# 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
|
|
@ -150,12 +150,10 @@ class ProcessFeedService < BaseService
|
||||||
|
|
||||||
next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id)
|
next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id)
|
||||||
|
|
||||||
if mentioned_account.local?
|
mention = mentioned_account.mentions.where(status: parent).first_or_create(status: parent)
|
||||||
# Send notifications
|
|
||||||
NotificationMailer.mention(mentioned_account, parent).deliver_later unless mentioned_account.blocking?(parent.account)
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
# So we can skip duplicate mentions
|
||||||
processed_account_ids << mentioned_account.id
|
processed_account_ids << mentioned_account.id
|
||||||
|
|
|
@ -65,8 +65,8 @@ class ProcessInteractionService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow!(account, target_account)
|
def follow!(account, target_account)
|
||||||
account.follow!(target_account)
|
follow = account.follow!(target_account)
|
||||||
NotificationMailer.follow(target_account, account).deliver_later unless target_account.blocking?(account)
|
NotifyService.new.call(target_account, follow)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unfollow!(account, target_account)
|
def unfollow!(account, target_account)
|
||||||
|
@ -83,8 +83,8 @@ class ProcessInteractionService < BaseService
|
||||||
|
|
||||||
def favourite!(xml, from_account)
|
def favourite!(xml, from_account)
|
||||||
current_status = status(xml)
|
current_status = status(xml)
|
||||||
current_status.favourites.where(account: from_account).first_or_create!(account: from_account)
|
favourite = 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)
|
NotifyService.new.call(current_status.account, favourite)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_post!(body, account)
|
def add_post!(body, account)
|
||||||
|
|
|
@ -29,7 +29,7 @@ class ProcessMentionsService < BaseService
|
||||||
mentioned_account = mention.account
|
mentioned_account = mention.account
|
||||||
|
|
||||||
if mentioned_account.local?
|
if mentioned_account.local?
|
||||||
NotificationMailer.mention(mentioned_account, status).deliver_later unless mentioned_account.blocking?(status.account)
|
NotifyService.new.call(mentioned_account, mention)
|
||||||
else
|
else
|
||||||
NotificationWorker.perform_async(status.stream_entry.id, mentioned_account.id)
|
NotificationWorker.perform_async(status.stream_entry.id, mentioned_account.id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ class ReblogService < BaseService
|
||||||
HubPingWorker.perform_async(account.id)
|
HubPingWorker.perform_async(account.id)
|
||||||
|
|
||||||
if reblogged_status.local?
|
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
|
else
|
||||||
NotificationWorker.perform_async(reblog.stream_entry.id, reblogged_status.account_id)
|
NotificationWorker.perform_async(reblog.stream_entry.id, reblogged_status.account_id)
|
||||||
end
|
end
|
||||||
|
|
2
app/views/api/v1/notifications/index.rabl
Normal file
2
app/views/api/v1/notifications/index.rabl
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
collection @notifications
|
||||||
|
extends 'api/v1/notifications/show'
|
11
app/views/api/v1/notifications/show.rabl
Normal file
11
app/views/api/v1/notifications/show.rabl
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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
|
|
@ -74,6 +74,8 @@ Rails.application.routes.draw do
|
||||||
resources :media, only: [:create]
|
resources :media, only: [:create]
|
||||||
resources :apps, only: [:create]
|
resources :apps, only: [:create]
|
||||||
|
|
||||||
|
resources :notifications, only: [:index]
|
||||||
|
|
||||||
resources :accounts, only: [:show] do
|
resources :accounts, only: [:show] do
|
||||||
collection do
|
collection do
|
||||||
get :relationships
|
get :relationships
|
||||||
|
|
13
db/migrate/20161119211120_create_notifications.rb
Normal file
13
db/migrate/20161119211120_create_notifications.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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
|
11
db/schema.rb
11
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -96,6 +96,15 @@ ActiveRecord::Schema.define(version: 20161116162355) do
|
||||||
t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree
|
t.index ["account_id", "status_id"], name: "index_mentions_on_account_id_and_status_id", unique: true, using: :btree
|
||||||
end
|
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|
|
create_table "oauth_access_grants", force: :cascade do |t|
|
||||||
t.integer "resource_owner_id", null: false
|
t.integer "resource_owner_id", null: false
|
||||||
t.integer "application_id", null: false
|
t.integer "application_id", null: false
|
||||||
|
|
4
spec/fabricators/notification_fabricator.rb
Normal file
4
spec/fabricators/notification_fabricator.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Fabricator(:notification) do
|
||||||
|
activity_id 1
|
||||||
|
activity_type "MyString"
|
||||||
|
end
|
|
@ -7,7 +7,8 @@ RSpec.describe NotificationMailer, type: :mailer do
|
||||||
let(:own_status) { Fabricate(:status, account: receiver.account) }
|
let(:own_status) { Fabricate(:status, account: receiver.account) }
|
||||||
|
|
||||||
describe "mention" do
|
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
|
it "renders the headers" do
|
||||||
expect(mail.subject).to eq("You were mentioned by bob")
|
expect(mail.subject).to eq("You were mentioned by bob")
|
||||||
|
@ -20,7 +21,8 @@ RSpec.describe NotificationMailer, type: :mailer do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "follow" do
|
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
|
it "renders the headers" do
|
||||||
expect(mail.subject).to eq("bob is now following you")
|
expect(mail.subject).to eq("bob is now following you")
|
||||||
|
@ -33,7 +35,8 @@ RSpec.describe NotificationMailer, type: :mailer do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "favourite" do
|
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
|
it "renders the headers" do
|
||||||
expect(mail.subject).to eq("bob favourited your status")
|
expect(mail.subject).to eq("bob favourited your status")
|
||||||
|
@ -46,7 +49,8 @@ RSpec.describe NotificationMailer, type: :mailer do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "reblog" do
|
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
|
it "renders the headers" do
|
||||||
expect(mail.subject).to eq("bob reblogged your status")
|
expect(mail.subject).to eq("bob reblogged your status")
|
||||||
|
|
29
spec/models/notification_spec.rb
Normal file
29
spec/models/notification_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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
|
Loading…
Reference in a new issue