Merge pull request #678 from Retrospring/feature/webpush
WebPush support
This commit is contained in:
commit
cc1c262256
1
Gemfile
1
Gemfile
|
@ -29,6 +29,7 @@ gem "haml", "~> 6.1"
|
|||
gem "hcaptcha", "~> 7.0"
|
||||
gem "mini_magick"
|
||||
gem "oj"
|
||||
gem "rpush"
|
||||
gem "rqrcode"
|
||||
|
||||
gem "rolify", "~> 6.0"
|
||||
|
|
20
Gemfile.lock
20
Gemfile.lock
|
@ -197,6 +197,8 @@ GEM
|
|||
hashie (5.0.0)
|
||||
hcaptcha (7.1.0)
|
||||
json
|
||||
hkdf (0.3.0)
|
||||
http-2 (0.11.0)
|
||||
http (4.4.1)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
|
@ -268,6 +270,10 @@ GEM
|
|||
multipart-post (2.1.1)
|
||||
naught (1.1.0)
|
||||
nested_form (0.3.2)
|
||||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
net-http2 (0.18.4)
|
||||
http-2 (~> 0.11)
|
||||
net-imap (0.3.4)
|
||||
date
|
||||
net-protocol
|
||||
|
@ -370,6 +376,16 @@ GEM
|
|||
rexml (3.2.5)
|
||||
rolify (6.0.0)
|
||||
rotp (6.2.0)
|
||||
rpush (7.0.1)
|
||||
activesupport (>= 5.2)
|
||||
jwt (>= 1.5.6)
|
||||
multi_json (~> 1.0)
|
||||
net-http-persistent
|
||||
net-http2 (~> 0.18, >= 0.18.3)
|
||||
railties
|
||||
rainbow
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
webpush (~> 1.0)
|
||||
rqrcode (2.1.2)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
|
@ -506,6 +522,9 @@ GEM
|
|||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webpush (1.1.0)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
|
@ -567,6 +586,7 @@ DEPENDENCIES
|
|||
redcarpet
|
||||
redis
|
||||
rolify (~> 6.0)
|
||||
rpush
|
||||
rqrcode
|
||||
rspec-its (~> 1.3)
|
||||
rspec-mocks
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ajax::WebPushController < AjaxController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def key
|
||||
certificate = Rpush::Webpush::App.find_by(name: "webpush").certificate
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:success] = true
|
||||
@response[:key] = JSON.parse(certificate)["public_key"]
|
||||
end
|
||||
|
||||
def check
|
||||
params.permit(:endpoint)
|
||||
|
||||
found = current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).first
|
||||
|
||||
@response[:status] = if found
|
||||
if found.failures >= 3
|
||||
:failed
|
||||
else
|
||||
:subscribed
|
||||
end
|
||||
else
|
||||
:unsubscribed
|
||||
end
|
||||
@response[:success] = true
|
||||
end
|
||||
|
||||
def subscribe
|
||||
WebPushSubscription.create!(
|
||||
user: current_user,
|
||||
subscription: params[:subscription]
|
||||
)
|
||||
|
||||
@response[:status] = :okay
|
||||
@response[:success] = true
|
||||
@response[:message] = t(".subscription_count", count: current_user.web_push_subscriptions.count)
|
||||
end
|
||||
|
||||
def unsubscribe # rubocop:disable Metrics/AbcSize
|
||||
params.permit(:endpoint)
|
||||
|
||||
removed = if params.key?(:endpoint)
|
||||
current_user.web_push_subscriptions.where("subscription ->> 'endpoint' = ?", params[:endpoint]).destroy_all
|
||||
else
|
||||
current_user.web_push_subscriptions.destroy_all
|
||||
end
|
||||
|
||||
count = current_user.web_push_subscriptions.count
|
||||
|
||||
@response[:status] = removed.any? ? :okay : :err
|
||||
@response[:success] = removed.any?
|
||||
@response[:message] = t(".subscription_count", count:)
|
||||
@response[:count] = count
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::PushNotificationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@subscriptions = current_user.web_push_subscriptions.active
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@ import { definitionsFromContext } from '@hotwired/stimulus-webpack-helpers';
|
|||
|
||||
import start from 'retrospring/common';
|
||||
import initAnswerbox from 'retrospring/features/answerbox/index';
|
||||
import initCapabilities from 'retrospring/features/capabilities';
|
||||
import initInbox from 'retrospring/features/inbox/index';
|
||||
import initUser from 'retrospring/features/user';
|
||||
import initSettings from 'retrospring/features/settings/index';
|
||||
|
@ -15,8 +16,10 @@ import initModeration from 'retrospring/features/moderation';
|
|||
import initMemes from 'retrospring/features/memes';
|
||||
import initLocales from 'retrospring/features/locales';
|
||||
import initFront from 'retrospring/features/front';
|
||||
import initWebpush from 'retrospring/features/webpush';
|
||||
|
||||
start();
|
||||
document.addEventListener('turbo:load', initCapabilities);
|
||||
document.addEventListener('DOMContentLoaded', initAnswerbox);
|
||||
document.addEventListener('DOMContentLoaded', initInbox);
|
||||
document.addEventListener('DOMContentLoaded', initUser);
|
||||
|
@ -28,6 +31,7 @@ document.addEventListener('DOMContentLoaded', initModeration);
|
|||
document.addEventListener('DOMContentLoaded', initMemes);
|
||||
document.addEventListener('turbo:load', initLocales);
|
||||
document.addEventListener('turbo:load', initFront);
|
||||
document.addEventListener('turbo:load', initWebpush);
|
||||
|
||||
window['Stimulus'] = Application.start();
|
||||
const context = require.context('../retrospring/controllers', true, /\.ts$/);
|
||||
|
|
|
@ -7,10 +7,6 @@ import { answerboxSmileHandler } from './smile';
|
|||
import { answerboxSubscribeHandler } from './subscribe';
|
||||
|
||||
export default (): void => {
|
||||
if ('share' in navigator) {
|
||||
document.body.classList.add('cap-web-share');
|
||||
}
|
||||
|
||||
registerEvents([
|
||||
{ type: 'click', target: '[name=ab-share]', handler: shareEventHandler, global: true },
|
||||
{ type: 'click', target: '[data-action=ab-submarine]', handler: answerboxSubscribeHandler, global: true },
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
export default (): void => {
|
||||
if ('share' in navigator) {
|
||||
document.body.classList.add('cap-web-share');
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
document.body.classList.add('cap-service-worker');
|
||||
}
|
||||
|
||||
if ('Notification' in window) {
|
||||
document.body.classList.add('cap-notification');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export function dismissHandler (event: Event): void {
|
||||
event.preventDefault();
|
||||
|
||||
const sender: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
sender.closest<HTMLDivElement>('.push-settings').classList.add('d-none');
|
||||
localStorage.setItem('dismiss-push-settings-prompt', 'true');
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { get, post } from '@rails/request.js';
|
||||
import I18n from "retrospring/i18n";
|
||||
import { showNotification } from "utilities/notifications";
|
||||
|
||||
export function enableHandler (event: Event): void {
|
||||
event.preventDefault();
|
||||
const sender = event.target as HTMLButtonElement;
|
||||
|
||||
try {
|
||||
installServiceWorker()
|
||||
.then(subscribe)
|
||||
.then(async subscription => {
|
||||
return Notification.requestPermission().then(permission => {
|
||||
if (permission != "granted") {
|
||||
return;
|
||||
}
|
||||
|
||||
post('/ajax/webpush', {
|
||||
body: {
|
||||
subscription
|
||||
},
|
||||
contentType: 'application/json'
|
||||
}).then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
if (data.success) {
|
||||
new Notification(I18n.translate("frontend.push_notifications.subscribe.success.title"), {
|
||||
body: I18n.translate("frontend.push_notifications.subscribe.success.body")
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLButtonElement>('button[data-action="push-disable"], button[data-action="push-remove-all"]')
|
||||
.forEach(button => button.classList.remove('d-none'));
|
||||
|
||||
sender.classList.add('d-none');
|
||||
|
||||
document.getElementById('subscription-count').textContent = data.message;
|
||||
} else {
|
||||
new Notification(I18n.translate("frontend.push_notifications.fail.title"), {
|
||||
body: I18n.translate("frontend.push_notifications.fail.body")
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to set up push notifications", error);
|
||||
showNotification(I18n.translate("frontend.push_notifications.setup_fail"));
|
||||
}
|
||||
}
|
||||
|
||||
async function installServiceWorker(): Promise<ServiceWorkerRegistration> {
|
||||
return navigator.serviceWorker.register("/service_worker.js", { scope: "/" });
|
||||
}
|
||||
|
||||
async function getServerKey(): Promise<Buffer> {
|
||||
const response = await get("/ajax/webpush/key");
|
||||
const data = await response.json;
|
||||
return Buffer.from(data.key, 'base64');
|
||||
}
|
||||
|
||||
async function subscribe(registration: ServiceWorkerRegistration): Promise<PushSubscription> {
|
||||
const key = await getServerKey();
|
||||
return await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: key
|
||||
});
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import registerEvents from 'retrospring/utilities/registerEvents';
|
||||
import { enableHandler } from './enable';
|
||||
import { dismissHandler } from "./dismiss";
|
||||
import { unsubscribeHandler, checkSubscription } from "retrospring/features/webpush/unsubscribe";
|
||||
|
||||
let subscriptionChecked = false;
|
||||
|
||||
export default (): void => {
|
||||
const swCapable = 'serviceWorker' in navigator;
|
||||
const notificationCapable = 'Notification' in window;
|
||||
|
||||
if (swCapable && notificationCapable) {
|
||||
const enableBtn = document.querySelector('button[data-action="push-enable"]');
|
||||
|
||||
navigator.serviceWorker.getRegistration().then(registration =>
|
||||
registration?.pushManager.getSubscription().then(subscription => {
|
||||
if (subscription) {
|
||||
document.querySelector('button[data-action="push-enable"]')?.classList.add('d-none');
|
||||
document.querySelector('[data-action="push-disable"]')?.classList.remove('d-none');
|
||||
|
||||
if (!subscriptionChecked) {
|
||||
checkSubscription(subscription);
|
||||
subscriptionChecked = true;
|
||||
}
|
||||
} else {
|
||||
enableBtn?.classList.remove('d-none');
|
||||
|
||||
if (localStorage.getItem('dismiss-push-settings-prompt') == null) {
|
||||
document.querySelector('.push-settings')?.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
registerEvents([
|
||||
{type: 'click', target: '[data-action="push-enable"]', handler: enableHandler, global: true},
|
||||
{type: 'click', target: '[data-action="push-dismiss"]', handler: dismissHandler, global: true},
|
||||
{type: 'click', target: '[data-action="push-disable"]', handler: unsubscribeHandler, global: true},
|
||||
{
|
||||
type: 'click',
|
||||
target: '[data-action="push-remove-all"]',
|
||||
handler: unsubscribeHandler,
|
||||
global: true
|
||||
},
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { post, destroy } from '@rails/request.js';
|
||||
import { showErrorNotification, showNotification } from "utilities/notifications";
|
||||
import I18n from "retrospring/i18n";
|
||||
|
||||
export function unsubscribeHandler(): void {
|
||||
navigator.serviceWorker.getRegistration()
|
||||
.then(registration => registration.pushManager.getSubscription())
|
||||
.then(subscription => unsubscribeClient(subscription))
|
||||
.then(subscription => unsubscribeServer(subscription))
|
||||
.catch(error => {
|
||||
showErrorNotification(I18n.translate("frontend.push_notifications.unsubscribe.error"));
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
export function checkSubscription(subscription: PushSubscription): void {
|
||||
post('/ajax/webpush/check', {
|
||||
body: {
|
||||
endpoint: subscription.endpoint
|
||||
},
|
||||
contentType: 'application/json'
|
||||
}).then(async response => {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status == 'subscribed') return;
|
||||
if (data.status == 'failed') await unsubscribeServer(subscription);
|
||||
await unsubscribeClient(subscription);
|
||||
})
|
||||
}
|
||||
|
||||
async function unsubscribeClient(subscription?: PushSubscription): Promise<PushSubscription|null> {
|
||||
subscription?.unsubscribe().then(success => {
|
||||
if (!success) {
|
||||
throw new Error("Failed to unsubscribe.");
|
||||
}
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
async function unsubscribeServer(subscription?: PushSubscription): Promise<void> {
|
||||
const body = subscription != null ? { endpoint: subscription.endpoint } : undefined;
|
||||
|
||||
return destroy('/ajax/webpush', {
|
||||
body,
|
||||
contentType: 'application/json',
|
||||
}).then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
if (data.success) {
|
||||
showNotification(I18n.translate("frontend.push_notifications.unsubscribe.success"));
|
||||
|
||||
document.getElementById('subscription-count').textContent = data.message;
|
||||
|
||||
if (data.count == 0) {
|
||||
document.querySelectorAll<HTMLButtonElement>('button[data-action="push-disable"], button[data-action="push-remove-all"]')
|
||||
.forEach(button => button.classList.add('d-none'));
|
||||
}
|
||||
|
||||
if (document.body.classList.contains('cap-service-worker') && document.body.classList.contains('cap-notification')) {
|
||||
document.querySelector<HTMLButtonElement>('button[data-action="push-enable"]')?.classList.remove('d-none');
|
||||
}
|
||||
} else {
|
||||
showErrorNotification(I18n.translate("frontend.push_notifications.unsubscribe.fail"));
|
||||
}
|
||||
})
|
||||
}
|
|
@ -108,6 +108,7 @@
|
|||
"components/mobile-nav",
|
||||
"components/notifications",
|
||||
"components/profile",
|
||||
"components/push-settings",
|
||||
"components/question",
|
||||
"components/smiles",
|
||||
"components/themes",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.push-notifications {
|
||||
&-unavailable {
|
||||
body.cap-service-worker.cap-notification & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Inbox < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :question
|
||||
|
@ -24,4 +26,20 @@ class Inbox < ApplicationRecord
|
|||
self.question.destroy if self.question.can_be_removed?
|
||||
self.destroy
|
||||
end
|
||||
|
||||
def as_push_notification
|
||||
{
|
||||
type: :inbox,
|
||||
title: I18n.t(
|
||||
"frontend.push_notifications.inbox.title",
|
||||
user: if question.author_is_anonymous
|
||||
user.profile.anon_display_name || APP_CONFIG["anonymous_name"]
|
||||
else
|
||||
question.user.profile.safe_name
|
||||
end
|
||||
),
|
||||
icon: question.author_is_anonymous ? "/icons/maskable_icon_x128.png" : question.user.profile_picture.url(:small),
|
||||
body: question.content
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Notification::PushSubscriptionError < Notification
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class User < ApplicationRecord
|
||||
class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
||||
include User::Relationship
|
||||
include User::Relationship::Follow
|
||||
include User::Relationship::Block
|
||||
|
@ -9,6 +9,7 @@ class User < ApplicationRecord
|
|||
include User::BanMethods
|
||||
include User::InboxMethods
|
||||
include User::QuestionMethods
|
||||
include User::PushNotificationMethods
|
||||
include User::ReactionMethods
|
||||
include User::RelationshipMethods
|
||||
include User::TimelineMethods
|
||||
|
@ -42,6 +43,7 @@ class User < ApplicationRecord
|
|||
|
||||
has_many :subscriptions, dependent: :destroy_async
|
||||
has_many :totp_recovery_codes, dependent: :destroy_async
|
||||
has_many :web_push_subscriptions, dependent: :destroy_async
|
||||
|
||||
has_one :profile, dependent: :destroy
|
||||
has_one :theme, dependent: :destroy
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module User::PushNotificationMethods
|
||||
def push_notification(app, resource)
|
||||
raise ArgumentError("Resource must respond to `as_push_notification`") unless resource.respond_to? :as_push_notification
|
||||
|
||||
web_push_subscriptions.active.find_each do |s|
|
||||
n = Rpush::Webpush::Notification.new
|
||||
n.app = app
|
||||
n.registration_ids = [s.subscription.symbolize_keys]
|
||||
n.data = {
|
||||
message: resource.as_push_notification.to_json
|
||||
}
|
||||
n.save!
|
||||
|
||||
PushNotificationWorker.perform_async(n.id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class WebPushSubscription < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
scope :active, -> { where(failures: ...3) }
|
||||
scope :failed, -> { where(failures: 3..) }
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
.card.push-settings.d-none
|
||||
.card-body
|
||||
= t(".description")
|
||||
%button.btn.btn-primary{ data: { action: "push-enable" } }= t("voc.y")
|
||||
%button.btn{ data: { action: "push-dismiss" } }= t("voc.n")
|
|
@ -2,6 +2,7 @@
|
|||
.row
|
||||
.col-sm-10.col-md-10.col-lg-9.mx-auto
|
||||
= render 'inbox/actions', delete_id: @delete_id, disabled: @disabled, inbox_count: @inbox_count
|
||||
= render 'inbox/push_settings'
|
||||
= render 'layouts/messages'
|
||||
= yield
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
.media.notification
|
||||
.notification__icon
|
||||
%span.fa-stack
|
||||
%i.fa.fa-2x.fa-fw.fa-bell
|
||||
%i.fa.fa-stack-1x.fa-fw.fa-exclamation-triangle.text-danger.pl-2
|
||||
.media-body
|
||||
%h6.media-heading.notification__user
|
||||
= t(".heading")
|
||||
.notification__text
|
||||
= t(".text_html", settings_push: link_to(t(".settings_push"), settings_push_notifications_path))
|
|
@ -0,0 +1,17 @@
|
|||
.card.push-notifications-settings
|
||||
.card-body
|
||||
%p= t('.description')
|
||||
%p#subscription-count= t('.subscription_count', count: subscriptions.count)
|
||||
|
||||
.push-notifications-unavailable.text-danger
|
||||
%i.fa.fa-warning
|
||||
= t('.unsupported')
|
||||
|
||||
.push-notifications-current-target.d-none.text-success
|
||||
%i.fa.fa-check
|
||||
= t('.current_target')
|
||||
|
||||
.button-group{ role: 'group' }
|
||||
%button.btn.btn-primary{ data: { action: 'push-enable' }, class: 'd-none' }= t('.subscribe')
|
||||
%button.btn.btn-primary{ data: { action: 'push-disable' }, class: 'd-none' }= t('.unsubscribe_current')
|
||||
%button.btn.btn-danger{ data: { action: 'push-remove-all' }, class: subscriptions.empty? ? 'd-none' : '' }= t('.unsubscribe_all')
|
|
@ -0,0 +1,4 @@
|
|||
= render "settings/push_notifications", subscriptions: @subscriptions
|
||||
|
||||
- provide(:title, generate_title(t(".title")))
|
||||
- parent_layout "user/settings"
|
|
@ -8,6 +8,7 @@
|
|||
= list_group_item t(".mutes"), settings_muted_path
|
||||
= list_group_item t(".blocks"), settings_blocks_path
|
||||
= list_group_item t(".theme"), edit_settings_theme_path
|
||||
= list_group_item t(".push_notifications"), settings_push_notifications_path
|
||||
= list_group_item t(".data"), settings_data_path
|
||||
= list_group_item t(".export"), settings_export_path
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rpush/daemon"
|
||||
|
||||
class PushNotificationWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: :push_notification, retry: 0
|
||||
|
||||
def perform(notification_id)
|
||||
Rpush::Daemon::AppRunner.enqueue(Rpush::Client::ActiveRecord::Notification.where(id: notification_id))
|
||||
end
|
||||
end
|
|
@ -10,17 +10,31 @@ class QuestionWorker
|
|||
def perform(user_id, question_id)
|
||||
user = User.find(user_id)
|
||||
question = Question.find(question_id)
|
||||
webpush_app = Rpush::App.find_by(name: "webpush")
|
||||
|
||||
user.followers.each do |f|
|
||||
next if f.inbox_locked?
|
||||
next if f.banned?
|
||||
next if MuteRule.where(user: f).any? { |rule| rule.applies_to? question }
|
||||
next if user.muting?(question.user)
|
||||
next if skip_inbox?(f, question, user)
|
||||
|
||||
Inbox.create(user_id: f.id, question_id: question_id, new: true)
|
||||
inbox = Inbox.create(user_id: f.id, question_id:, new: true)
|
||||
f.push_notification(webpush_app, inbox) if webpush_app
|
||||
end
|
||||
rescue StandardError => e
|
||||
logger.info "failed to ask question: #{e.message}"
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def skip_inbox?(follower, question, user)
|
||||
return true if follower.inbox_locked?
|
||||
return true if follower.banned?
|
||||
return true if muted?(follower, question)
|
||||
return true if user.muting?(question.user)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def muted?(user, question)
|
||||
MuteRule.where(user:).any? { |rule| rule.applies_to? question }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
require "rpush/daemon"
|
||||
require "rpush/daemon/store/active_record"
|
||||
require "rpush/client/active_record"
|
||||
|
||||
redis_url = ENV.fetch("REDIS_URL") { APP_CONFIG["redis_url"] }
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = { url: redis_url }
|
||||
Rpush.config.push = true
|
||||
Rpush::Daemon.store = Rpush::Daemon::Store::ActiveRecord.new
|
||||
Rpush::Daemon.common_init
|
||||
Rpush::Daemon::Synchronizer.sync
|
||||
|
||||
at_exit do
|
||||
Rpush::Daemon::AppRunner.stop
|
||||
end
|
||||
end
|
||||
|
||||
Sidekiq.configure_client do |config|
|
||||
config.redis = { url: redis_url }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
Rpush.configure do |config|
|
||||
|
||||
# Supported clients are :active_record and :redis
|
||||
config.client = :active_record
|
||||
|
||||
# Options passed to Redis.new
|
||||
# config.redis_options = {}
|
||||
|
||||
# Frequency in seconds to check for new notifications.
|
||||
config.push_poll = 2
|
||||
|
||||
# The maximum number of notifications to load from the store every `push_poll` seconds.
|
||||
# If some notifications are still enqueued internally, Rpush will load the batch_size less
|
||||
# the number enqueued. An exception to this is if the service is able to receive multiple
|
||||
# notification payloads over the connection with a single write, such as APNs.
|
||||
config.batch_size = 100
|
||||
|
||||
# Path to write PID file. Relative to current directory unless absolute.
|
||||
config.pid_file = 'tmp/rpush.pid'
|
||||
|
||||
# Path to log file. Relative to current directory unless absolute.
|
||||
config.log_file = 'log/rpush.log'
|
||||
|
||||
config.log_level = (defined?(Rails) && Rails.logger) ? Rails.logger.level : ::Logger::Severity::INFO
|
||||
|
||||
# Define a custom logger.
|
||||
# config.logger = MyLogger.new
|
||||
|
||||
# By default in foreground mode logs are directed both to the logger and to stdout.
|
||||
# If the logger goes to stdout, you can disable foreground logging to avoid duplication.
|
||||
# config.foreground_logging = false
|
||||
|
||||
# config.apns.feedback_receiver.enabled = true
|
||||
# config.apns.feedback_receiver.frequency = 60
|
||||
|
||||
end
|
||||
|
||||
Rpush.reflect do |on|
|
||||
|
||||
# Called with a Rpush::Apns::Feedback instance when feedback is received
|
||||
# from the APNs that a notification has failed to be delivered.
|
||||
# Further notifications should not be sent to the device.
|
||||
# on.apns_feedback do |feedback|
|
||||
# end
|
||||
|
||||
# Called when a notification is queued internally for delivery.
|
||||
# The internal queue for each app runner can be inspected:
|
||||
#
|
||||
# Rpush::Daemon::AppRunner.status
|
||||
#
|
||||
# on.notification_enqueued do |notification|
|
||||
# end
|
||||
|
||||
# Called when a notification is successfully delivered.
|
||||
# on.notification_delivered do |notification|
|
||||
# end
|
||||
|
||||
# Called when notification delivery failed.
|
||||
# Call 'error_code' and 'error_description' on the notification for the cause.
|
||||
on.notification_failed do |notification|
|
||||
# See: https://developer.apple.com/documentation/usernotifications/sending_web_push_notifications_in_safari_and_other_browsers#3994594
|
||||
if %i[403 410].include? notification.error_code
|
||||
subscription = WebPushSubscription::where("subscription ->> 'endpoint' = ?", notification.registration_ids.first[:endpoint])
|
||||
subscription.increment :failures
|
||||
subscription.save
|
||||
|
||||
if subscription.failures > 3
|
||||
Notification::PushSubscriptionError.create(
|
||||
target: subscription,
|
||||
recipient: subscription.user,
|
||||
new: true
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Called when the notification delivery failed and only the notification ID
|
||||
# is present in memory.
|
||||
# on.notification_id_failed do |app, notification_id, error_code, error_description|
|
||||
# end
|
||||
|
||||
# Called when a notification will be retried at a later date.
|
||||
# Call 'deliver_after' on the notification for the next delivery date
|
||||
# and 'retries' for the number of times this notification has been retried.
|
||||
# on.notification_will_retry do |notification|
|
||||
# end
|
||||
|
||||
# Called when a notification will be retried and only the notification ID
|
||||
# is present in memory.
|
||||
# on.notification_id_will_retry do |app, notification_id, retry_after|
|
||||
# end
|
||||
|
||||
# Called when a TCP connection is lost and will be reconnected.
|
||||
# on.tcp_connection_lost do |app, error|
|
||||
# end
|
||||
|
||||
# Called for each recipient which successfully receives a notification. This
|
||||
# can occur more than once for the same notification when there are multiple
|
||||
# recipients.
|
||||
# on.gcm_delivered_to_recipient do |notification, registration_id|
|
||||
# end
|
||||
|
||||
# Called for each recipient which fails to receive a notification. This
|
||||
# can occur more than once for the same notification when there are multiple
|
||||
# recipients. (do not handle invalid registration IDs here)
|
||||
# on.gcm_failed_to_recipient do |notification, error, registration_id|
|
||||
# end
|
||||
|
||||
# Called when the GCM returns a canonical registration ID.
|
||||
# You will need to replace old_id with canonical_id in your records.
|
||||
# on.gcm_canonical_id do |old_id, canonical_id|
|
||||
# end
|
||||
|
||||
# Called when the GCM returns a failure that indicates an invalid registration id.
|
||||
# You will need to delete the registration_id from your records.
|
||||
# on.gcm_invalid_registration_id do |app, error, registration_id|
|
||||
# end
|
||||
|
||||
# Called when an SSL certificate will expire within 1 month.
|
||||
# Implement on.error to catch errors raised when the certificate expires.
|
||||
# on.ssl_certificate_will_expire do |app, expiration_time|
|
||||
# end
|
||||
|
||||
# Called when an SSL certificate has been revoked.
|
||||
# on.ssl_certificate_revoked do |app, error|
|
||||
# end
|
||||
|
||||
# Called when the ADM returns a canonical registration ID.
|
||||
# You will need to replace old_id with canonical_id in your records.
|
||||
# on.adm_canonical_id do |old_id, canonical_id|
|
||||
# end
|
||||
|
||||
# Called when Failed to deliver to ADM. Check the 'reason' string for further
|
||||
# explanations.
|
||||
#
|
||||
# If the reason is the string 'Unregistered', you should remove
|
||||
# this registration id from your records.
|
||||
# on.adm_failed_to_recipient do |notification, registration_id, reason|
|
||||
# end
|
||||
|
||||
# Called when Failed to deliver to WNS. Check the 'reason' string for further
|
||||
# explanations.
|
||||
# You should remove this uri from your records
|
||||
# on.wns_invalid_channel do |notification, uri, reason|
|
||||
# end
|
||||
|
||||
# Called when an exception is raised.
|
||||
# on.error do |error|
|
||||
# end
|
||||
end
|
||||
|
||||
Rails.application.config.active_record.yaml_column_permitted_classes = [Symbol, Hash]
|
|
@ -8,6 +8,8 @@ hostname: "justask.rrerr.net"
|
|||
https: true
|
||||
|
||||
email_from: "noreply@justask.rrerr.net"
|
||||
# Required by WebPush spec in case of problems with notifications
|
||||
contact_email: "contact@justask.rrerr.net"
|
||||
|
||||
# Name of the "Anonymous" user. (e.g. "Anonymous Coward", "Arno Nym", "Mr. X", ...)
|
||||
anonymous_name: "Anonymous"
|
||||
|
|
|
@ -139,6 +139,11 @@ en:
|
|||
destroy_comment:
|
||||
success: "Successfully unsmiled comment."
|
||||
error: "You have not smiled that comment."
|
||||
web_push:
|
||||
subscription_count:
|
||||
zero: "You are not currently subscribed to push notifications on any devices."
|
||||
one: "You are currently receiving push notifications on one device."
|
||||
other: "You are currently receiving push notifications on %{count} devices."
|
||||
inbox:
|
||||
author:
|
||||
info: "No questions from @%{author} found, showing entries from all users instead!"
|
||||
|
|
|
@ -49,6 +49,20 @@ en:
|
|||
confirm:
|
||||
title: "Are you sure?"
|
||||
text: "This will mute this user for everyone."
|
||||
push_notifications:
|
||||
subscribe:
|
||||
success:
|
||||
title: Push notifications enabled!
|
||||
body: You will now receive push notifications for new questions on this device.
|
||||
fail:
|
||||
title: Failed to subscribe to push notifications
|
||||
body: Please try again later
|
||||
unsubscribe:
|
||||
success: Push notifications disabled successfully.
|
||||
fail: Failed to disable push notifications.
|
||||
setup_fail: Failed to set up push notifications. Please try again later.
|
||||
inbox:
|
||||
title: New question from %{user}
|
||||
report:
|
||||
confirm:
|
||||
title: "Are you sure you want to report this %{type}?"
|
||||
|
|
|
@ -231,6 +231,8 @@ en:
|
|||
share:
|
||||
heading: "Share"
|
||||
button: "Share on %{service}"
|
||||
push_settings:
|
||||
description: "Want to receive push notifications for new questions on your device?"
|
||||
layouts:
|
||||
feedback:
|
||||
heading: "Feedback"
|
||||
|
@ -352,6 +354,10 @@ en:
|
|||
link_text: "your comment"
|
||||
follow:
|
||||
heading_html: "followed you %{time} ago"
|
||||
webpushsubscription:
|
||||
heading: "Push notifications are failing to send to one of your devices."
|
||||
text_html: "Please check the %{settings_push} if you still want to be notified."
|
||||
settings_push: "push notification settings"
|
||||
expiredtwitterserviceconnection:
|
||||
heading: "Twitter connection expired"
|
||||
text_html: "If you would like to continue automatically sharing your answers to Twitter, head to %{settings_sharing} and re-connect your account."
|
||||
|
@ -529,6 +535,19 @@ en:
|
|||
body: "Raised content includes all the different boxes and panels you can see across the site."
|
||||
accent:
|
||||
example: "Some text on top of a accented area on a raised element!"
|
||||
push_notifications:
|
||||
index:
|
||||
title: "Push Notifications"
|
||||
subscription_count:
|
||||
zero: "You are not currently subscribed to push notifications on any devices."
|
||||
one: "You are currently receiving push notifications on one device."
|
||||
other: "You are currently receiving push notifications on %{count} devices."
|
||||
unsupported: "This browser does not support push notifications."
|
||||
current_target: "You are currently receiving push notifications on this device."
|
||||
subscribe: "Enable on this device"
|
||||
unsubscribe_current: "Disable on this device"
|
||||
unsubscribe_all: "Disable on all devices"
|
||||
description: "Here you can set up or disable push notifications for new questions in your inbox."
|
||||
shared:
|
||||
links:
|
||||
about: "About"
|
||||
|
@ -592,6 +611,8 @@ en:
|
|||
theme: "Theme"
|
||||
data: "Your Data"
|
||||
export: "Export"
|
||||
blocks: "Blocks"
|
||||
push_notifications: "Push Notifications"
|
||||
moderation:
|
||||
inbox:
|
||||
header:
|
||||
|
|
|
@ -89,6 +89,8 @@ Rails.application.routes.draw do
|
|||
|
||||
get :data, to: "data#index"
|
||||
|
||||
resources :push_notifications, only: %i[index]
|
||||
|
||||
namespace :two_factor_authentication do
|
||||
get :otp_authentication, to: "otp_authentication#index"
|
||||
patch :otp_authentication, to: "otp_authentication#update"
|
||||
|
@ -132,6 +134,10 @@ Rails.application.routes.draw do
|
|||
post "/list_membership", to: "list#membership", as: :list_membership
|
||||
post "/subscribe", to: "subscription#subscribe", as: :subscribe_answer
|
||||
post "/unsubscribe", to: "subscription#unsubscribe", as: :unsubscribe_answer
|
||||
get "/webpush/key", to: "web_push#key", as: :webpush_key
|
||||
post "/webpush/check", to: "web_push#check", as: :webpush_check
|
||||
post "/webpush", to: "web_push#subscribe", as: :webpush_subscribe
|
||||
delete "/webpush", to: "web_push#unsubscribe", as: :webpush_unsubscribe
|
||||
end
|
||||
|
||||
resource :anonymous_block, controller: :anonymous_block, only: %i[create destroy]
|
||||
|
|
|
@ -12,4 +12,5 @@ production:
|
|||
- mailers
|
||||
- question
|
||||
- export
|
||||
- push_notification
|
||||
|
||||
|
|
|
@ -0,0 +1,341 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# NOTE: TO THE CURIOUS.
|
||||
#
|
||||
# Congratulations on being a diligent developer and vetting the migrations
|
||||
# added to your project!
|
||||
#
|
||||
# You're probably thinking "This migration is huge!". It is, but that doesn't
|
||||
# mean it'll take a long time to run, or that the reason for it being
|
||||
# this size is because of lousy developers.
|
||||
#
|
||||
# Rpush used to be known as Rapns. In an effort to reduce clutter in db/migrate
|
||||
# for new users of Rpush, what you see below is a concatenation of the
|
||||
# migrations added to Rapns over its lifetime.
|
||||
#
|
||||
# The reason for concatenating old migrations - instead of producing a new
|
||||
# one that attempts to recreate their accumulative state - is that I don't
|
||||
# want to introduce any bugs by writing a new migration.
|
||||
#
|
||||
# So while this looks like a scary amount of code, it is in fact the safest
|
||||
# approach. The constituent parts of this migration have been executed
|
||||
# many times, by many people!
|
||||
|
||||
class AddRpush < ActiveRecord::Migration[5.0]
|
||||
# rubocop:disable all
|
||||
def self.migrations
|
||||
[CreateRapnsNotifications, CreateRapnsFeedback,
|
||||
AddAlertIsJsonToRapnsNotifications, AddAppToRapns,
|
||||
CreateRapnsApps, AddGcm, AddWpns, AddAdm, RenameRapnsToRpush,
|
||||
AddFailAfterToRpushNotifications]
|
||||
end
|
||||
|
||||
def self.up
|
||||
migrations.map(&:up)
|
||||
end
|
||||
|
||||
def self.down
|
||||
migrations.reverse.each do |m|
|
||||
m.down
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
Rails.logger.debug e
|
||||
end
|
||||
end
|
||||
|
||||
class CreateRapnsNotifications < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
create_table :rapns_notifications do |t|
|
||||
t.integer :badge, null: true
|
||||
t.string :device_token, null: false, limit: 64
|
||||
t.string :sound, null: true, default: "1.aiff"
|
||||
t.string :alert, null: true
|
||||
t.text :attributes_for_device, null: true
|
||||
t.integer :expiry, null: false, default: 1.day.to_i
|
||||
t.boolean :delivered, null: false, default: false
|
||||
t.timestamp :delivered_at, null: true
|
||||
t.boolean :failed, null: false, default: false
|
||||
t.timestamp :failed_at, null: true
|
||||
t.integer :error_code, null: true
|
||||
t.string :error_description, null: true
|
||||
t.timestamp :deliver_after, null: true
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :rapns_notifications, %i[delivered failed deliver_after], name: "index_rapns_notifications_multi"
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_index :rapns_notifications, name: "index_rapns_notifications_multi" if index_name_exists?(:rapns_notifications, "index_rapns_notifications_multi")
|
||||
|
||||
drop_table :rapns_notifications
|
||||
end
|
||||
end
|
||||
|
||||
class CreateRapnsFeedback < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
create_table :rapns_feedback do |t|
|
||||
t.string :device_token, null: false, limit: 64
|
||||
t.timestamp :failed_at, null: false
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :rapns_feedback, :device_token
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_index :rapns_feedback, name: :index_rapns_feedback_on_device_token if index_name_exists?(:rapns_feedback, :index_rapns_feedback_on_device_token)
|
||||
|
||||
drop_table :rapns_feedback
|
||||
end
|
||||
end
|
||||
|
||||
class AddAlertIsJsonToRapnsNotifications < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
add_column :rapns_notifications, :alert_is_json, :boolean, null: true, default: false
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rapns_notifications, :alert_is_json
|
||||
end
|
||||
end
|
||||
|
||||
class AddAppToRapns < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
add_column :rapns_notifications, :app, :string, null: true
|
||||
add_column :rapns_feedback, :app, :string, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rapns_notifications, :app
|
||||
remove_column :rapns_feedback, :app
|
||||
end
|
||||
end
|
||||
|
||||
class CreateRapnsApps < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
create_table :rapns_apps do |t|
|
||||
t.string :key, null: false
|
||||
t.string :environment, null: false
|
||||
t.text :certificate, null: false
|
||||
t.string :password, null: true
|
||||
t.integer :connections, null: false, default: 1
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :rapns_apps
|
||||
end
|
||||
end
|
||||
|
||||
class AddGcm < ActiveRecord::Migration[5.0]
|
||||
module Rapns
|
||||
class App < ActiveRecord::Base
|
||||
self.table_name = 'rapns_apps'
|
||||
end
|
||||
|
||||
class Notification < ActiveRecord::Base
|
||||
belongs_to :app
|
||||
self.table_name = "rapns_notifications"
|
||||
end
|
||||
end
|
||||
|
||||
def self.up
|
||||
add_column :rapns_notifications, :type, :string, null: true
|
||||
add_column :rapns_apps, :type, :string, null: true
|
||||
|
||||
AddGcm::Rapns::Notification.update_all type: "Rapns::Apns::Notification"
|
||||
AddGcm::Rapns::App.update_all type: "Rapns::Apns::App"
|
||||
|
||||
change_column :rapns_notifications, :type, :string, null: false
|
||||
change_column :rapns_apps, :type, :string, null: false
|
||||
change_column :rapns_notifications, :device_token, :string, null: true, limit: 64
|
||||
change_column :rapns_notifications, :expiry, :integer, null: true, default: 1.day.to_i
|
||||
change_column :rapns_apps, :environment, :string, null: true
|
||||
change_column :rapns_apps, :certificate, :text, null: true, default: nil
|
||||
|
||||
change_column :rapns_notifications, :error_description, :text, null: true, default: nil
|
||||
change_column :rapns_notifications, :sound, :string, default: "default"
|
||||
|
||||
rename_column :rapns_notifications, :attributes_for_device, :data
|
||||
rename_column :rapns_apps, :key, :name
|
||||
|
||||
add_column :rapns_apps, :auth_key, :string, null: true
|
||||
|
||||
add_column :rapns_notifications, :collapse_key, :string, null: true
|
||||
add_column :rapns_notifications, :delay_while_idle, :boolean, null: false, default: false
|
||||
|
||||
reg_ids_type = ActiveRecord::Base.connection.adapter_name.include?("Mysql") ? :mediumtext : :text
|
||||
add_column :rapns_notifications, :registration_ids, reg_ids_type, null: true
|
||||
add_column :rapns_notifications, :app_id, :integer, null: true
|
||||
add_column :rapns_notifications, :retries, :integer, null: true, default: 0
|
||||
|
||||
AddGcm::Rapns::Notification.reset_column_information
|
||||
AddGcm::Rapns::App.reset_column_information
|
||||
|
||||
AddGcm::Rapns::App.all.each do |app|
|
||||
AddGcm::Rapns::Notification.where(app: app.name).update_all(app_id: app.id)
|
||||
end
|
||||
|
||||
change_column :rapns_notifications, :app_id, :integer, null: false
|
||||
remove_column :rapns_notifications, :app
|
||||
|
||||
if index_name_exists?(:rapns_notifications, "index_rapns_notifications_multi")
|
||||
remove_index :rapns_notifications, name: "index_rapns_notifications_multi"
|
||||
elsif index_name_exists?(:rapns_notifications, "index_rapns_notifications_on_delivered_failed_deliver_after")
|
||||
remove_index :rapns_notifications, name: "index_rapns_notifications_on_delivered_failed_deliver_after"
|
||||
end
|
||||
|
||||
add_index :rapns_notifications, %i[app_id delivered failed deliver_after], name: "index_rapns_notifications_multi"
|
||||
end
|
||||
|
||||
def self.down
|
||||
AddGcm::Rapns::Notification.where(type: "Rapns::Gcm::Notification").delete_all
|
||||
|
||||
remove_column :rapns_notifications, :type
|
||||
remove_column :rapns_apps, :type
|
||||
|
||||
change_column :rapns_notifications, :device_token, :string, null: false, limit: 64
|
||||
change_column :rapns_notifications, :expiry, :integer, null: false, default: 1.day.to_i
|
||||
change_column :rapns_apps, :environment, :string, null: false
|
||||
change_column :rapns_apps, :certificate, :text, null: false
|
||||
|
||||
change_column :rapns_notifications, :error_description, :string, null: true, default: nil
|
||||
change_column :rapns_notifications, :sound, :string, default: "1.aiff"
|
||||
|
||||
rename_column :rapns_notifications, :data, :attributes_for_device
|
||||
rename_column :rapns_apps, :name, :key
|
||||
|
||||
remove_column :rapns_apps, :auth_key
|
||||
|
||||
remove_column :rapns_notifications, :collapse_key
|
||||
remove_column :rapns_notifications, :delay_while_idle
|
||||
remove_column :rapns_notifications, :registration_ids
|
||||
remove_column :rapns_notifications, :retries
|
||||
|
||||
add_column :rapns_notifications, :app, :string, null: true
|
||||
|
||||
AddGcm::Rapns::Notification.reset_column_information
|
||||
AddGcm::Rapns::App.reset_column_information
|
||||
|
||||
AddGcm::Rapns::App.all.each do |app|
|
||||
AddGcm::Rapns::Notification.where(app_id: app.id).update_all(app: app.key)
|
||||
end
|
||||
|
||||
remove_index :rapns_notifications, name: :index_rapns_notifications_multi if index_name_exists?(:rapns_notifications, :index_rapns_notifications_multi)
|
||||
|
||||
remove_column :rapns_notifications, :app_id
|
||||
|
||||
add_index :rapns_notifications, %i[delivered failed deliver_after], name: :index_rapns_notifications_multi
|
||||
end
|
||||
end
|
||||
|
||||
class AddWpns < ActiveRecord::Migration[5.0]
|
||||
module Rapns
|
||||
class Notification < ActiveRecord::Base
|
||||
self.table_name = 'rapns_notifications'
|
||||
end
|
||||
end
|
||||
|
||||
def self.up
|
||||
add_column :rapns_notifications, :uri, :string, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
AddWpns::Rapns::Notification.where(type: "Rapns::Wpns::Notification").delete_all
|
||||
remove_column :rapns_notifications, :uri
|
||||
end
|
||||
end
|
||||
|
||||
class AddAdm < ActiveRecord::Migration[5.0]
|
||||
module Rapns
|
||||
class Notification < ActiveRecord::Base
|
||||
self.table_name = 'rapns_notifications'
|
||||
end
|
||||
end
|
||||
|
||||
def self.up
|
||||
add_column :rapns_apps, :client_id, :string, null: true
|
||||
add_column :rapns_apps, :client_secret, :string, null: true
|
||||
add_column :rapns_apps, :access_token, :string, null: true
|
||||
add_column :rapns_apps, :access_token_expiration, :datetime, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
AddAdm::Rapns::Notification.where(type: "Rapns::Adm::Notification").delete_all
|
||||
|
||||
remove_column :rapns_apps, :client_id
|
||||
remove_column :rapns_apps, :client_secret
|
||||
remove_column :rapns_apps, :access_token
|
||||
remove_column :rapns_apps, :access_token_expiration
|
||||
end
|
||||
end
|
||||
|
||||
class RenameRapnsToRpush < ActiveRecord::Migration[5.0]
|
||||
module Rpush
|
||||
class App < ActiveRecord::Base
|
||||
self.table_name = 'rpush_apps'
|
||||
end
|
||||
|
||||
class Notification < ActiveRecord::Base
|
||||
self.table_name = 'rpush_notifications'
|
||||
end
|
||||
end
|
||||
|
||||
def self.update_type(model, from, to)
|
||||
model.where(type: from).update_all(type: to)
|
||||
end
|
||||
|
||||
def self.up
|
||||
rename_table :rapns_notifications, :rpush_notifications
|
||||
rename_table :rapns_apps, :rpush_apps
|
||||
rename_table :rapns_feedback, :rpush_feedback
|
||||
|
||||
rename_index :rpush_notifications, :index_rapns_notifications_multi, :index_rpush_notifications_multi if index_name_exists?(:rpush_notifications, :index_rapns_notifications_multi)
|
||||
|
||||
rename_index :rpush_feedback, :index_rapns_feedback_on_device_token, :index_rpush_feedback_on_device_token if index_name_exists?(:rpush_feedback, :index_rapns_feedback_on_device_token)
|
||||
|
||||
update_type(RenameRapnsToRpush::Rpush::Notification, "Rapns::Apns::Notification", "Rpush::Apns::Notification")
|
||||
update_type(RenameRapnsToRpush::Rpush::Notification, "Rapns::Gcm::Notification", "Rpush::Gcm::Notification")
|
||||
update_type(RenameRapnsToRpush::Rpush::Notification, "Rapns::Adm::Notification", "Rpush::Adm::Notification")
|
||||
update_type(RenameRapnsToRpush::Rpush::Notification, "Rapns::Wpns::Notification", "Rpush::Wpns::Notification")
|
||||
|
||||
update_type(RenameRapnsToRpush::Rpush::App, "Rapns::Apns::App", "Rpush::Apns::App")
|
||||
update_type(RenameRapnsToRpush::Rpush::App, "Rapns::Gcm::App", "Rpush::Gcm::App")
|
||||
update_type(RenameRapnsToRpush::Rpush::App, "Rapns::Adm::App", "Rpush::Adm::App")
|
||||
update_type(RenameRapnsToRpush::Rpush::App, "Rapns::Wpns::App", "Rpush::Wpns::App")
|
||||
end
|
||||
|
||||
def self.down
|
||||
update_type(RenameRapnsToRpush::Rpush::Notification, "Rpush::Apns::Notification", "Rapns::Apns::Notification")
|
||||
update_type(RenameRapnsToRpush::Rpush::Notification, "Rpush::Gcm::Notification", "Rapns::Gcm::Notification")
|
||||
update_type(RenameRapnsToRpush::Rpush::Notification, "Rpush::Adm::Notification", "Rapns::Adm::Notification")
|
||||
update_type(RenameRapnsToRpush::Rpush::Notification, "Rpush::Wpns::Notification", "Rapns::Wpns::Notification")
|
||||
|
||||
update_type(RenameRapnsToRpush::Rpush::App, "Rpush::Apns::App", "Rapns::Apns::App")
|
||||
update_type(RenameRapnsToRpush::Rpush::App, "Rpush::Gcm::App", "Rapns::Gcm::App")
|
||||
update_type(RenameRapnsToRpush::Rpush::App, "Rpush::Adm::App", "Rapns::Adm::App")
|
||||
update_type(RenameRapnsToRpush::Rpush::App, "Rpush::Wpns::App", "Rapns::Wpns::App")
|
||||
|
||||
rename_index :rpush_notifications, :index_rpush_notifications_multi, :index_rapns_notifications_multi if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi)
|
||||
|
||||
rename_index :rpush_feedback, :index_rpush_feedback_on_device_token, :index_rapns_feedback_on_device_token if index_name_exists?(:rpush_feedback, :index_rpush_feedback_on_device_token)
|
||||
|
||||
rename_table :rpush_notifications, :rapns_notifications
|
||||
rename_table :rpush_apps, :rapns_apps
|
||||
rename_table :rpush_feedback, :rapns_feedback
|
||||
end
|
||||
end
|
||||
|
||||
class AddFailAfterToRpushNotifications < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
add_column :rpush_notifications, :fail_after, :timestamp, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_notifications, :fail_after
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable all
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush200Updates < ActiveRecord::Migration[5.0]
|
||||
module Rpush
|
||||
class App < ApplicationRecord
|
||||
self.table_name = "rpush_apps"
|
||||
end
|
||||
|
||||
class Notification < ApplicationRecord
|
||||
self.table_name = "rpush_notifications"
|
||||
end
|
||||
end
|
||||
|
||||
def self.update_type(model, from, to)
|
||||
model.where(type: from).update_all(type: to) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def self.up
|
||||
add_column :rpush_notifications, :processing, :boolean, null: false, default: false
|
||||
add_column :rpush_notifications, :priority, :integer, null: true
|
||||
|
||||
remove_index :rpush_notifications, name: :index_rpush_notifications_multi if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi)
|
||||
|
||||
add_index :rpush_notifications, %i[delivered failed], name: "index_rpush_notifications_multi", where: "NOT delivered AND NOT failed"
|
||||
|
||||
rename_column :rpush_feedback, :app, :app_id
|
||||
|
||||
if postgresql?
|
||||
execute("ALTER TABLE rpush_feedback ALTER COLUMN app_id TYPE integer USING (trim(app_id)::integer)")
|
||||
else
|
||||
change_column :rpush_feedback, :app_id, :integer
|
||||
end
|
||||
|
||||
%i[Apns Gcm Wpns Adm].each do |service|
|
||||
update_type(Rpush200Updates::Rpush::App, "Rpush::#{service}::App", "Rpush::Client::ActiveRecord::#{service}::App")
|
||||
update_type(Rpush200Updates::Rpush::Notification, "Rpush::#{service}::Notification", "Rpush::Client::ActiveRecord::#{service}::Notification")
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
%i[Apns Gcm Wpns Adm].each do |service|
|
||||
update_type(Rpush200Updates::Rpush::App, "Rpush::Client::ActiveRecord::#{service}::App", "Rpush::#{service}::App")
|
||||
update_type(Rpush200Updates::Rpush::Notification, "Rpush::Client::ActiveRecord::#{service}::Notification", "Rpush::#{service}::Notification")
|
||||
end
|
||||
|
||||
change_column :rpush_feedback, :app_id, :string
|
||||
rename_column :rpush_feedback, :app_id, :app
|
||||
|
||||
remove_index :rpush_notifications, name: :index_rpush_notifications_multi if index_name_exists?(:rpush_notifications, :index_rpush_notifications_multi)
|
||||
|
||||
add_index :rpush_notifications, %i[app_id delivered failed deliver_after], name: "index_rpush_notifications_multi"
|
||||
|
||||
remove_column :rpush_notifications, :priority
|
||||
remove_column :rpush_notifications, :processing
|
||||
end
|
||||
|
||||
def self.adapter_name
|
||||
env = defined?(Rails) && Rails.env ? Rails.env : "development"
|
||||
if ActiveRecord::VERSION::MAJOR > 6
|
||||
ActiveRecord::Base.configurations.configs_for(env_name: env).first.configuration_hash[:adapter]
|
||||
else
|
||||
ActiveRecord::Base.configurations[env].to_h { |k, v| [k.to_sym, v] }[:adapter]
|
||||
end
|
||||
end
|
||||
|
||||
def self.postgresql?
|
||||
adapter_name =~ /postgresql|postgis/
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush210Updates < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
add_column :rpush_notifications, :url_args, :text, null: true
|
||||
add_column :rpush_notifications, :category, :string, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_notifications, :url_args
|
||||
remove_column :rpush_notifications, :category
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush260Updates < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
add_column :rpush_notifications, :content_available, :boolean, default: false
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_notifications, :content_available
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush270Updates < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
change_column :rpush_notifications, :alert, :text
|
||||
add_column :rpush_notifications, :notification, :text
|
||||
end
|
||||
|
||||
def self.down
|
||||
change_column :rpush_notifications, :alert, :string
|
||||
remove_column :rpush_notifications, :notification
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush300Updates < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
add_column :rpush_notifications, :mutable_content, :boolean, default: false
|
||||
change_column :rpush_notifications, :sound, :string, default: nil
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_notifications, :mutable_content
|
||||
change_column :rpush_notifications, :sound, :string, default: "default"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush301Updates < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
change_column_null :rpush_notifications, :mutable_content, false
|
||||
change_column_null :rpush_notifications, :content_available, false
|
||||
change_column_null :rpush_notifications, :alert_is_json, false
|
||||
end
|
||||
|
||||
def self.down
|
||||
change_column_null :rpush_notifications, :mutable_content, true
|
||||
change_column_null :rpush_notifications, :content_available, true
|
||||
change_column_null :rpush_notifications, :alert_is_json, true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush310AddPushy < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
add_column :rpush_notifications, :external_device_id, :string, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_notifications, :external_device_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush311Updates < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
change_table :rpush_notifications do |t|
|
||||
t.remove_index name: "index_rpush_notifications_multi"
|
||||
t.index %i[delivered failed processing deliver_after created_at], name: "index_rpush_notifications_multi", where: "NOT delivered AND NOT failed"
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
change_table :rpush_notifications do |t|
|
||||
t.remove_index name: "index_rpush_notifications_multi"
|
||||
t.index %i[delivered failed], name: "index_rpush_notifications_multi", where: "NOT delivered AND NOT failed"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush320AddApnsP8 < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
add_column :rpush_apps, :apn_key, :string, null: true
|
||||
add_column :rpush_apps, :apn_key_id, :string, null: true
|
||||
add_column :rpush_apps, :team_id, :string, null: true
|
||||
add_column :rpush_apps, :bundle_id, :string, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_apps, :apn_key
|
||||
remove_column :rpush_apps, :apn_key_id
|
||||
remove_column :rpush_apps, :team_id
|
||||
remove_column :rpush_apps, :bundle_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush324Updates < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
change_column :rpush_apps, :apn_key, :text, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
change_column :rpush_apps, :apn_key, :string, null: true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush330Updates < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
add_column :rpush_notifications, :thread_id, :string, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_notifications, :thread_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush331Updates < ActiveRecord::Migration[5.0]
|
||||
def self.up
|
||||
change_column :rpush_notifications, :device_token, :string, null: true
|
||||
change_column :rpush_feedback, :device_token, :string, null: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
change_column :rpush_notifications, :device_token, :string, null: true, limit: 64
|
||||
change_column :rpush_feedback, :device_token, :string, null: true, limit: 64
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush410Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
||||
def self.up
|
||||
add_column :rpush_notifications, :dry_run, :boolean, null: false, default: false
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_notifications, :dry_run
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush411Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
||||
def self.up
|
||||
add_column :rpush_apps, :feedback_enabled, :boolean, default: true
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_apps, :feedback_enabled
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Rpush420Updates < ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
||||
def self.up
|
||||
add_column :rpush_notifications, :sound_is_json, :boolean, null: true, default: false
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_column :rpush_notifications, :sound_is_json
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "webpush"
|
||||
|
||||
class AddWebpushApp < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
vapid_keypair = Webpush.generate_key.to_hash
|
||||
app = Rpush::Webpush::App.new
|
||||
app.name = "webpush"
|
||||
app.certificate = vapid_keypair.merge(subject: APP_CONFIG.fetch("contact_email")).to_json
|
||||
app.connections = 1
|
||||
app.save!
|
||||
end
|
||||
|
||||
def down
|
||||
Rpush::Webpush::App.find_by(name: "webpush").destroy!
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateWebPushSubscriptions < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :web_push_subscriptions do |t|
|
||||
t.bigint :user_id, null: false
|
||||
t.json :subscription
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :web_push_subscriptions, :user_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddFailuresToWebPushSubscriptions < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :web_push_subscriptions, :failures, :integer, default: 0
|
||||
end
|
||||
end
|
78
db/schema.rb
78
db/schema.rb
|
@ -183,6 +183,75 @@ ActiveRecord::Schema.define(version: 2022_12_27_065923) do
|
|||
t.index ["resource_type", "resource_id"], name: "index_roles_on_resource"
|
||||
end
|
||||
|
||||
create_table "rpush_apps", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "environment"
|
||||
t.text "certificate"
|
||||
t.string "password"
|
||||
t.integer "connections", default: 1, null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.string "type", null: false
|
||||
t.string "auth_key"
|
||||
t.string "client_id"
|
||||
t.string "client_secret"
|
||||
t.string "access_token"
|
||||
t.datetime "access_token_expiration"
|
||||
t.text "apn_key"
|
||||
t.string "apn_key_id"
|
||||
t.string "team_id"
|
||||
t.string "bundle_id"
|
||||
t.boolean "feedback_enabled", default: true
|
||||
end
|
||||
|
||||
create_table "rpush_feedback", force: :cascade do |t|
|
||||
t.string "device_token"
|
||||
t.datetime "failed_at", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.integer "app_id"
|
||||
t.index ["device_token"], name: "index_rpush_feedback_on_device_token"
|
||||
end
|
||||
|
||||
create_table "rpush_notifications", force: :cascade do |t|
|
||||
t.integer "badge"
|
||||
t.string "device_token"
|
||||
t.string "sound"
|
||||
t.text "alert"
|
||||
t.text "data"
|
||||
t.integer "expiry", default: 86400
|
||||
t.boolean "delivered", default: false, null: false
|
||||
t.datetime "delivered_at"
|
||||
t.boolean "failed", default: false, null: false
|
||||
t.datetime "failed_at"
|
||||
t.integer "error_code"
|
||||
t.text "error_description"
|
||||
t.datetime "deliver_after"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.boolean "alert_is_json", default: false, null: false
|
||||
t.string "type", null: false
|
||||
t.string "collapse_key"
|
||||
t.boolean "delay_while_idle", default: false, null: false
|
||||
t.text "registration_ids"
|
||||
t.integer "app_id", null: false
|
||||
t.integer "retries", default: 0
|
||||
t.string "uri"
|
||||
t.datetime "fail_after"
|
||||
t.boolean "processing", default: false, null: false
|
||||
t.integer "priority"
|
||||
t.text "url_args"
|
||||
t.string "category"
|
||||
t.boolean "content_available", default: false, null: false
|
||||
t.text "notification"
|
||||
t.boolean "mutable_content", default: false, null: false
|
||||
t.string "external_device_id"
|
||||
t.string "thread_id"
|
||||
t.boolean "dry_run", default: false, null: false
|
||||
t.boolean "sound_is_json", default: false
|
||||
t.index ["delivered", "failed", "processing", "deliver_after", "created_at"], name: "index_rpush_notifications_multi", where: "((NOT delivered) AND (NOT failed))"
|
||||
end
|
||||
|
||||
create_table "services", id: :serial, force: :cascade do |t|
|
||||
t.string "type", null: false
|
||||
t.bigint "user_id", null: false
|
||||
|
@ -313,5 +382,14 @@ ActiveRecord::Schema.define(version: 2022_12_27_065923) do
|
|||
t.index ["user_id"], name: "index_users_roles_on_user_id"
|
||||
end
|
||||
|
||||
create_table "web_push_subscriptions", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.json "subscription"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.integer "failures", default: 0
|
||||
t.index ["user_id"], name: "index_web_push_subscriptions_on_user_id"
|
||||
end
|
||||
|
||||
add_foreign_key "profiles", "users"
|
||||
end
|
||||
|
|
|
@ -11,24 +11,16 @@ module UseCase
|
|||
option :direct, type: Types::Params::Bool, default: proc { true }
|
||||
|
||||
def call
|
||||
check_user
|
||||
check_lock
|
||||
check_anonymous_rules
|
||||
check_blocks
|
||||
do_checks!
|
||||
|
||||
question = ::Question.create!(
|
||||
content: content,
|
||||
author_is_anonymous: anonymous,
|
||||
author_identifier: author_identifier,
|
||||
user: source_user_id.nil? ? nil : source_user,
|
||||
direct: direct
|
||||
)
|
||||
question = create_question
|
||||
|
||||
return if filtered?(question)
|
||||
|
||||
increment_asked_count
|
||||
|
||||
inbox = ::Inbox.create!(user: target_user, question: question, new: true)
|
||||
inbox = ::Inbox.create!(user: target_user, question:, new: true)
|
||||
notify
|
||||
|
||||
{
|
||||
status: 201,
|
||||
|
@ -41,6 +33,13 @@ module UseCase
|
|||
|
||||
private
|
||||
|
||||
def do_checks!
|
||||
check_user
|
||||
check_lock
|
||||
check_anonymous_rules
|
||||
check_blocks
|
||||
end
|
||||
|
||||
def check_lock
|
||||
raise Errors::InboxLocked if target_user.inbox_locked?
|
||||
end
|
||||
|
@ -66,6 +65,21 @@ module UseCase
|
|||
raise Errors::NotAuthorized if target_user.privacy_require_user && !source_user_id
|
||||
end
|
||||
|
||||
def create_question
|
||||
::Question.create!(
|
||||
content:,
|
||||
author_is_anonymous: anonymous,
|
||||
author_identifier:,
|
||||
user: source_user_id.nil? ? nil : source_user,
|
||||
direct:
|
||||
)
|
||||
end
|
||||
|
||||
def notify
|
||||
webpush_app = ::Rpush::App.find_by(name: "webpush")
|
||||
target_user.push_notification(webpush_app, inbox) if webpush_app
|
||||
end
|
||||
|
||||
def increment_asked_count
|
||||
unless source_user_id && !anonymous && !direct
|
||||
# Only increment the asked count of the source user if the question
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
self.addEventListener('push', function (event) {
|
||||
if (event.data) {
|
||||
const notification = event.data.json();
|
||||
|
||||
event.waitUntil(self.registration.showNotification(notification.title, {
|
||||
body: notification.body,
|
||||
tag: notification.type,
|
||||
icon: notification.icon,
|
||||
}));
|
||||
} else {
|
||||
console.error("Push event received, but it didn't contain any data.", event);
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', async event => {
|
||||
if (event.notification.tag === 'inbox') {
|
||||
event.preventDefault();
|
||||
return clients.openWindow("/inbox", "_blank").then(result => {
|
||||
event.notification.close();
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unhandled notification tag: ${event.notification.tag}`);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,263 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
describe Ajax::WebPushController, :ajax_controller, type: :controller do
|
||||
before do
|
||||
Rpush::Webpush::App.create(
|
||||
name: "webpush",
|
||||
certificate: { public_key: "AAAA", private_key: "BBBB", subject: "" }.to_json,
|
||||
connections: 1
|
||||
)
|
||||
end
|
||||
|
||||
describe "#key" do
|
||||
subject { get :key, format: :json }
|
||||
|
||||
let(:expected_response) do
|
||||
{
|
||||
"message" => "",
|
||||
"status" => "okay",
|
||||
"success" => true,
|
||||
"key" => "AAAA"
|
||||
}
|
||||
end
|
||||
|
||||
context "user signed in" do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
|
||||
before { sign_in user }
|
||||
|
||||
include_examples "returns the expected response"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#subscribe" do
|
||||
subject { post :subscribe, params: }
|
||||
|
||||
context "user signed in" do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
|
||||
before { sign_in user }
|
||||
|
||||
context "given a valid subscription" do
|
||||
let(:params) do
|
||||
{
|
||||
subscription: {
|
||||
endpoint: "https://some.webpush/endpoint",
|
||||
keys: {}
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:expected_response) do
|
||||
{
|
||||
"message" => I18n.t("settings.push_notifications.subscription_count.one"),
|
||||
"status" => "okay",
|
||||
"success" => true
|
||||
}
|
||||
end
|
||||
|
||||
it "stores the subscription" do
|
||||
expect { subject }
|
||||
.to(
|
||||
change { WebPushSubscription.count }
|
||||
.by(1)
|
||||
)
|
||||
end
|
||||
|
||||
include_examples "returns the expected response"
|
||||
end
|
||||
end
|
||||
|
||||
context "given no subscription param" do
|
||||
let(:params) do
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#unsubscribe" do
|
||||
subject { delete :unsubscribe, params: }
|
||||
|
||||
shared_examples_for "does not remove any subscriptions" do
|
||||
it "does not remove any subscriptions" do
|
||||
expect { subject }.not_to(change { WebPushSubscription.count })
|
||||
end
|
||||
end
|
||||
|
||||
context "user signed in" do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
|
||||
before { sign_in user }
|
||||
|
||||
context "valid subscription" do
|
||||
let(:endpoint) { "some endpoint" }
|
||||
let!(:subscription) do
|
||||
WebPushSubscription.create(
|
||||
user:,
|
||||
subscription: { endpoint:, keys: {} }
|
||||
)
|
||||
end
|
||||
let!(:other_subscription) do
|
||||
WebPushSubscription.create(
|
||||
user:,
|
||||
subscription: { endpoint: "other endpoint", keys: {} }
|
||||
)
|
||||
end
|
||||
let(:params) do
|
||||
{ endpoint: }
|
||||
end
|
||||
let(:expected_response) do
|
||||
{
|
||||
"status" => "okay",
|
||||
"success" => true,
|
||||
"message" => I18n.t("ajax.web_push.subscription_count.one"),
|
||||
"count" => 1
|
||||
}
|
||||
end
|
||||
|
||||
it "removes the subscription" do
|
||||
subject
|
||||
expect { subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { other_subscription.reload }.not_to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
include_examples "returns the expected response"
|
||||
end
|
||||
|
||||
context "invalid subscription" do
|
||||
let!(:subscription) do
|
||||
WebPushSubscription.create(
|
||||
user:,
|
||||
subscription: { endpoint: "other endpoint", keys: {} }
|
||||
)
|
||||
end
|
||||
let(:params) do
|
||||
{ endpoint: "some endpoint" }
|
||||
end
|
||||
let(:expected_response) do
|
||||
{
|
||||
"status" => "err",
|
||||
"success" => false,
|
||||
"message" => I18n.t("ajax.web_push.subscription_count.one"),
|
||||
"count" => 1
|
||||
}
|
||||
end
|
||||
|
||||
include_examples "does not remove any subscriptions"
|
||||
include_examples "returns the expected response"
|
||||
end
|
||||
|
||||
context "someone else's subscription" do
|
||||
let(:endpoint) { "some endpoint" }
|
||||
let(:other_user) { FactoryBot.create(:user) }
|
||||
let!(:subscription) do
|
||||
WebPushSubscription.create(
|
||||
user: other_user,
|
||||
subscription: { endpoint:, keys: {} }
|
||||
)
|
||||
end
|
||||
let(:params) do
|
||||
{ endpoint: }
|
||||
end
|
||||
let(:expected_response) do
|
||||
{
|
||||
"status" => "err",
|
||||
"success" => false,
|
||||
"message" => I18n.t("ajax.web_push.subscription_count.zero"),
|
||||
"count" => 0
|
||||
}
|
||||
end
|
||||
|
||||
include_examples "does not remove any subscriptions"
|
||||
include_examples "returns the expected response"
|
||||
end
|
||||
|
||||
context "no subscription provided" do
|
||||
let(:other_user) { FactoryBot.create(:user) }
|
||||
let(:params) { {} }
|
||||
|
||||
before do
|
||||
4.times do |i|
|
||||
WebPushSubscription.create(
|
||||
user: i.zero? ? other_user : user,
|
||||
subscription: { endpoint: i, keys: {} }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "removes all own subscriptions" do
|
||||
expect { subject }
|
||||
.to(
|
||||
change { WebPushSubscription.count }
|
||||
.from(4)
|
||||
.to(1)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#check" do
|
||||
subject { post :check, params: }
|
||||
|
||||
context "user signed in" do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
let(:endpoint) { "https://some.domain/some/webpush/endpoint" }
|
||||
let!(:subscription) do
|
||||
WebPushSubscription.create(
|
||||
user:,
|
||||
subscription: { endpoint:, keys: {} },
|
||||
failures:
|
||||
)
|
||||
end
|
||||
let(:expected_response) do
|
||||
{
|
||||
"message" => "",
|
||||
"status" => expected_status,
|
||||
"success" => true
|
||||
}
|
||||
end
|
||||
|
||||
before { sign_in user }
|
||||
|
||||
context "subscription exists" do
|
||||
let(:params) do
|
||||
{ endpoint: }
|
||||
end
|
||||
|
||||
context "without failures" do
|
||||
let(:failures) { 0 }
|
||||
let(:expected_status) { "subscribed" }
|
||||
|
||||
it_behaves_like "returns the expected response"
|
||||
end
|
||||
|
||||
context "with 2 failures" do
|
||||
let(:failures) { 2 }
|
||||
let(:expected_status) { "subscribed" }
|
||||
|
||||
it_behaves_like "returns the expected response"
|
||||
end
|
||||
|
||||
context "with 3 failures" do
|
||||
let(:failures) { 3 }
|
||||
let(:expected_status) { "failed" }
|
||||
|
||||
it_behaves_like "returns the expected response"
|
||||
end
|
||||
end
|
||||
|
||||
context "subscription doesn't exist" do
|
||||
let(:params) do
|
||||
{ endpoint: "https;//some.domain/some/other/endpoint" }
|
||||
end
|
||||
|
||||
let(:failures) { 0 }
|
||||
let(:expected_status) { "unsubscribed" }
|
||||
|
||||
it_behaves_like "returns the expected response"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
describe Settings::PushNotificationsController, type: :controller do
|
||||
describe "#index" do
|
||||
subject { get :index }
|
||||
|
||||
context "user signed in" do
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
|
||||
before { sign_in user }
|
||||
|
||||
it "renders the index template" do
|
||||
subject
|
||||
expect(response).to render_template(:index)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -43,8 +43,7 @@ describe QuestionWorker do
|
|||
|
||||
it "respects inbox locks" do
|
||||
user.followers.first.update(privacy_lock_inbox: true)
|
||||
|
||||
|
||||
|
||||
expect { subject }
|
||||
.to(
|
||||
change { Inbox.where(user_id: user.followers.ids, question_id:, new: true).count }
|
||||
|
@ -52,7 +51,7 @@ describe QuestionWorker do
|
|||
.to(4)
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
it "does not send questions to banned users" do
|
||||
user.followers.first.ban
|
||||
|
||||
|
@ -63,5 +62,35 @@ describe QuestionWorker do
|
|||
.to(4)
|
||||
)
|
||||
end
|
||||
|
||||
context "receiver has push notifications enabled" do
|
||||
let(:receiver) { FactoryBot.create(:user) }
|
||||
|
||||
before do
|
||||
Rpush::Webpush::App.create(
|
||||
name: "webpush",
|
||||
certificate: { public_key: "AAAA", private_key: "AAAA", subject: "" }.to_json,
|
||||
connections: 1
|
||||
)
|
||||
|
||||
WebPushSubscription.create!(
|
||||
user: receiver,
|
||||
subscription: {
|
||||
endpoint: "This will not be used",
|
||||
keys: {}
|
||||
}
|
||||
)
|
||||
receiver.follow(user)
|
||||
end
|
||||
|
||||
it "sends notifications" do
|
||||
expect { subject }
|
||||
.to(
|
||||
change { Rpush::Webpush::Notification.count }
|
||||
.from(0)
|
||||
.to(1)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue