diff options
author | Shel R <Yiskah.Raphen@gmail.com> | 2017-04-07 22:44:40 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-07 22:44:40 -0400 |
commit | abab82ec1e086e257650ceed3d03a4d881e0a12e (patch) | |
tree | a8b7db83af6b84813c5bd951d38ca0334acb9af7 /app | |
parent | 07495cc13feb78ae979a8af69a401cce8dcd4e8d (diff) | |
parent | f0797bf8cecd7d8537747bff66fc0363ccc59760 (diff) |
Merge branch 'master' into master
Diffstat (limited to 'app')
42 files changed, 556 insertions, 290 deletions
diff --git a/app/assets/images/background-photo.jpeg b/app/assets/images/background-photo.jpeg index b0a88ff35..d7937fd4b 100644 --- a/app/assets/images/background-photo.jpeg +++ b/app/assets/images/background-photo.jpeg Binary files differdiff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index cbb7b85bc..00f20074d 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -47,6 +47,7 @@ import pt from 'react-intl/locale-data/pt'; import hu from 'react-intl/locale-data/hu'; import uk from 'react-intl/locale-data/uk'; import fi from 'react-intl/locale-data/fi'; +import eo from 'react-intl/locale-data/eo'; import getMessagesForLocale from '../locales'; import { hydrateStore } from '../actions/store'; import createStream from '../stream'; @@ -59,7 +60,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({ basename: '/web' }); -addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi]); +addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]); const Mastodon = React.createClass({ diff --git a/app/assets/javascripts/components/locales/eo.jsx b/app/assets/javascripts/components/locales/eo.jsx new file mode 100644 index 000000000..8c118b31f --- /dev/null +++ b/app/assets/javascripts/components/locales/eo.jsx @@ -0,0 +1,68 @@ +const eo = { + "column_back_button.label": "Reveni", + "lightbox.close": "Fermi", + "loading_indicator.label": "Ŝarĝanta...", + "status.mention": "Mencii @{name}", + "status.delete": "Forigi", + "status.reply": "Respondi", + "status.reblog": "Diskonigi", + "status.favourite": "Favori", + "status.reblogged_by": "{name} diskonigita", + "status.sensitive_warning": "Tikla enhavo", + "status.sensitive_toggle": "Alklaki por vidi", + "video_player.toggle_sound": "Aktivigi sonojn", + "account.mention": "Mencii @{name}", + "account.edit_profile": "Redakti la profilon", + "account.unblock": "Malbloki @{name}", + "account.unfollow": "Malsekvi", + "account.block": "Bloki @{name}", + "account.follow": "Sekvi", + "account.posts": "Mesaĝoj", + "account.follows": "Sekvatoj", + "account.followers": "Sekvantoj", + "account.follows_you": "Sekvas vin", + "account.requested": "Atendas aprobon", + "getting_started.heading": "Por komenci", + "getting_started.about_addressing": "Vi povas sekvi homojn se vi konas la uzantnomon kaj domajnon tajpinte retpoŝtecan adreson en la serĉilon.", + "getting_started.about_shortcuts": "Se la celita uzanto troviĝas en la sama domajno de vi, uzi nur la uzantnomon sufiĉos. La sama regulo validas por mencii aliajn uzantojn en mesaĝo.", + "getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}. {apps}.", + "column.home": "Hejmo", + "column.community": "Loka tempolinio", + "column.public": "Fratara tempolinio", + "column.notifications": "Sciigoj", + "tabs_bar.compose": "Ekskribi", + "tabs_bar.home": "Hejmo", + "tabs_bar.mentions": "Sciigoj", + "tabs_bar.public": "Fratara tempolinio", + "tabs_bar.notifications": "Sciigoj", + "compose_form.placeholder": "Pri kio vi pensas?", + "compose_form.publish": "Hup", + "compose_form.sensitive": "Marki ke la enhavo estas tikla", + "compose_form.spoiler": "Kaŝi la tekston malantaŭ averto", + "compose_form.private": "Marki ke la enhavo estas privata", + "compose_form.privacy_disclaimer": "Via privata mesaĝo estos sendita nur al menciitaj uzantoj en {domains}. Ĉu vi fidas {domainsCount, plural, one {tiun servilon} other {tiujn servilojn}}? Mesaĝa privateco funkcias nur en aperaĵoj de Mastodon. Se {domains} {domainsCount, plural, one {ne estas aperaĵo de Mastodon} other {ne estas aperaĵoj de Mastodon}}, estos neniu indiko ke via mesaĝo estas privata, kaj ĝi povus esti diskonigita aŭ videbligita al necelitaj ricevantoj.", + "compose_form.unlisted": "Ne afiŝi en publikaj tempolinioj", + "navigation_bar.edit_profile": "Redakti la profilon", + "navigation_bar.preferences": "Preferoj", + "navigation_bar.community_timeline": "Loka tempolinio", + "navigation_bar.public_timeline": "Fratara tempolinio", + "navigation_bar.logout": "Elsaluti", + "reply_indicator.cancel": "Rezigni", + "search.placeholder": "Serĉi", + "search.account": "Konto", + "search.hashtag": "Kradvorto", + "upload_button.label": "Aldoni enhavaĵon", + "upload_form.undo": "Malfari", + "notification.follow": "{name} sekvis vin", + "notification.favourite": "{name} favoris vian mesaĝon", + "notification.reblog": "{name} diskonigis vian mesaĝon", + "notification.mention": "{name} menciis vin", + "notifications.column_settings.alert": "Retumilaj atentigoj", + "notifications.column_settings.show": "Montri en kolono", + "notifications.column_settings.follow": "Novaj sekvantoj:", + "notifications.column_settings.favourite": "Favoroj:", + "notifications.column_settings.mention": "Mencioj:", + "notifications.column_settings.reblog": "Diskonigoj:", +}; + +export default eo; diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index 72b8a5df5..1e7b8b548 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -6,6 +6,7 @@ import fr from './fr'; import pt from './pt'; import uk from './uk'; import fi from './fi'; +import eo from './eo'; const locales = { en, @@ -15,7 +16,8 @@ const locales = { fr, pt, uk, - fi + fi, + eo }; export default function getMessagesForLocale (locale) { diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index d233b3471..696e89418 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1,5 +1,9 @@ @import 'variables'; +.app-body{ + -ms-overflow-style: -ms-autohiding-scrollbar; +} + .button { background-color: darken($color4, 3%); font-family: inherit; diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index dc1aeb5ea..619c04be2 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -16,7 +16,8 @@ class AccountsController < ApplicationController end format.atom do - @entries = @account.stream_entries.order('id desc').where(activity_type: 'Status').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id]) + @entries = @account.stream_entries.order('id desc').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id]) + render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a)) end format.activitystreams2 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c06142fd4..f00f9c1e3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ApplicationController < ActionController::Base + include Localized + # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception @@ -14,7 +16,6 @@ class ApplicationController < ActionController::Base rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? - before_action :set_locale before_action :set_user_activity before_action :check_suspension, if: :user_signed_in? @@ -28,12 +29,6 @@ class ApplicationController < ActionController::Base store_location_for(:user, request.url) end - def set_locale - I18n.locale = current_user.try(:locale) || I18n.default_locale - rescue I18n::InvalidLocale - I18n.locale = I18n.default_locale - end - def require_admin! redirect_to root_path unless current_user&.admin? end diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb new file mode 100644 index 000000000..b6f868090 --- /dev/null +++ b/app/controllers/concerns/localized.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Localized + extend ActiveSupport::Concern + + included do + before_action :set_locale + end + + def set_locale + I18n.locale = current_user.try(:locale) || default_locale + rescue I18n::InvalidLocale + I18n.locale = default_locale + end + + def default_locale + ENV.fetch('DEFAULT_LOCALE') { I18n.default_locale } + end +end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 7c25266d8..cdbfde0fb 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController + include Localized + skip_before_action :authenticate_resource_owner! - before_action :set_locale before_action :store_current_location before_action :authenticate_resource_owner! @@ -12,10 +13,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController def store_current_location store_location_for(:user, request.url) end - - def set_locale - I18n.locale = current_user.try(:locale) || I18n.default_locale - rescue I18n::InvalidLocale - I18n.locale = I18n.default_locale - end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb new file mode 100644 index 000000000..09dd5d3c4 --- /dev/null +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController + include Localized + + skip_before_action :authenticate_resource_owner! + + before_action :store_current_location + before_action :authenticate_resource_owner! + + private + + def store_current_location + store_location_for(:user, request.url) + end +end diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index de38b3602..469a8c33e 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -19,7 +19,9 @@ class StreamEntriesController < ApplicationController end end - format.atom + format.atom do + render xml: AtomSerializer.render(AtomSerializer.new.entry(@stream_entry, true)) + end end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index e01f7d0cc..74dc0e11d 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -11,6 +11,7 @@ module SettingsHelper uk: 'Українська', 'zh-CN': '简体中文', fi: 'Suomi', + eo: 'Esperanto', }.freeze def human_locale(locale) diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index a26e912a3..38e63ed8d 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -34,10 +34,6 @@ module StreamEntriesHelper user_signed_in? && @favourited.key?(status.id) ? 'favourited' : '' end - def proper_status(status) - status.reblog? ? status.reblog : status - end - def rtl?(text) return false if text.empty? diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb new file mode 100644 index 000000000..b9dcee6b3 --- /dev/null +++ b/app/lib/atom_serializer.rb @@ -0,0 +1,351 @@ +# frozen_string_literal: true + +class AtomSerializer + include RoutingHelper + + class << self + def render(element) + document = Ox::Document.new(version: '1.0') + document << element + ('<?xml version="1.0"?>' + Ox.dump(element)).force_encoding('UTF-8') + end + end + + def author(account) + author = Ox::Element.new('author') + + uri = TagManager.instance.uri_for(account) + + append_element(author, 'id', uri) + append_element(author, 'activity:object-type', TagManager::TYPES[:person]) + append_element(author, 'uri', uri) + append_element(author, 'name', account.username) + append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct) + append_element(author, 'summary', account.note) + append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account)) + append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original))) + append_element(author, 'link', nil, rel: :header, type: account.header_content_type, 'media:width': 700, 'media:height': 335, href: full_asset_url(account.header.url(:original))) + append_element(author, 'poco:preferredUsername', account.username) + append_element(author, 'poco:displayName', account.display_name) unless account.display_name.blank? + append_element(author, 'poco:note', Formatter.instance.simplified_format(account).to_str) unless account.note.blank? + append_element(author, 'mastodon:scope', account.locked? ? :private : :public) + + author + end + + def feed(account, stream_entries) + feed = Ox::Element.new('feed') + + add_namespaces(feed) + + append_element(feed, 'id', account_url(account, format: 'atom')) + append_element(feed, 'title', account.display_name) + append_element(feed, 'subtitle', account.note) + append_element(feed, 'updated', account.updated_at.iso8601) + append_element(feed, 'logo', full_asset_url(account.avatar.url(:original))) + + feed << author(account) + + append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account)) + append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom')) + append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20 + append_element(feed, 'link', nil, rel: :hub, href: api_push_url) + append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id)) + + stream_entries.each do |stream_entry| + feed << entry(stream_entry) + end + + feed + end + + def entry(stream_entry, root = false) + entry = Ox::Element.new('entry') + + add_namespaces(entry) if root + + append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type)) + append_element(entry, 'published', stream_entry.created_at.iso8601) + append_element(entry, 'updated', stream_entry.updated_at.iso8601) + append_element(entry, 'title', stream_entry&.status&.title) + + entry << author(stream_entry.account) if root + + append_element(entry, 'activity:object-type', TagManager::TYPES[stream_entry.object_type]) + append_element(entry, 'activity:verb', TagManager::VERBS[stream_entry.verb]) + + entry << object(stream_entry.target) if stream_entry.targeted? + + serialize_status_attributes(entry, stream_entry.status) unless stream_entry.status.nil? + + append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: account_stream_entry_url(stream_entry.account, stream_entry)) + append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')) + append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded? + + entry + end + + def object(status) + object = Ox::Element.new('activity:object') + + append_element(object, 'id', TagManager.instance.uri_for(status)) + append_element(object, 'published', status.created_at.iso8601) + append_element(object, 'updated', status.updated_at.iso8601) + append_element(object, 'title', status.title) + + object << author(status.account) + + append_element(object, 'activity:object-type', TagManager::TYPES[status.object_type]) + append_element(object, 'activity:verb', TagManager::VERBS[status.verb]) + + serialize_status_attributes(object, status) + + append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(status)) + append_element(object, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(status.thread), href: TagManager.instance.url_for(status.thread)) if status.reply? && !status.thread.nil? + + object + end + + def follow_salmon(follow) + entry = Ox::Element.new('entry') + add_namespaces(entry) + + description = "#{follow.account.acct} started following #{follow.target_account.acct}" + + append_element(entry, 'id', TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow')) + append_element(entry, 'title', description) + append_element(entry, 'content', description, type: :html) + + entry << author(follow.account) + + append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', TagManager::VERBS[:follow]) + + object = author(follow.target_account) + object.value = 'activity:object' + + entry << object + entry + end + + def follow_request_salmon(follow_request) + entry = Ox::Element.new('entry') + add_namespaces(entry) + + append_element(entry, 'id', TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest')) + append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}") + + entry << author(follow_request.account) + + append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', TagManager::VERBS[:request_friend]) + + object = author(follow_request.target_account) + object.value = 'activity:object' + + entry << object + entry + end + + def authorize_follow_request_salmon(follow_request) + entry = Ox::Element.new('entry') + add_namespaces(entry) + + append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) + append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}") + + entry << author(follow_request.target_account) + + append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', TagManager::VERBS[:authorize]) + + object = Ox::Element.new('activity:object') + object << author(follow_request.account) + + append_element(object, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(object, 'activity:verb', TagManager::VERBS[:request_friend]) + + inner_object = author(follow_request.target_account) + inner_object.value = 'activity:object' + + object << inner_object + entry << object + entry + end + + def reject_follow_request_salmon(follow_request) + entry = Ox::Element.new('entry') + add_namespaces(entry) + + append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) + append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}") + + entry << author(follow_request.target_account) + + append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', TagManager::VERBS[:reject]) + + object = Ox::Element.new('activity:object') + object << author(follow_request.account) + + append_element(object, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(object, 'activity:verb', TagManager::VERBS[:request_friend]) + + inner_object = author(follow_request.target_account) + inner_object.value = 'activity:object' + + object << inner_object + entry << object + entry + end + + def unfollow_salmon(follow) + entry = Ox::Element.new('entry') + add_namespaces(entry) + + description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}" + + append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow')) + append_element(entry, 'title', description) + append_element(entry, 'content', description, type: :html) + + entry << author(follow.account) + + append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', TagManager::VERBS[:unfollow]) + + object = author(follow.target_account) + object.value = 'activity:object' + + entry << object + entry + end + + def block_salmon(block) + entry = Ox::Element.new('entry') + add_namespaces(entry) + + description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}" + + append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) + append_element(entry, 'title', description) + + entry << author(block.account) + + append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', TagManager::VERBS[:block]) + + object = author(block.target_account) + object.value = 'activity:object' + + entry << object + entry + end + + def unblock_salmon(block) + entry = Ox::Element.new('entry') + add_namespaces(entry) + + description = "#{block.account.acct} no longer blocks #{block.target_account.acct}" + + append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) + append_element(entry, 'title', description) + + entry << author(block.account) + + append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', TagManager::VERBS[:unblock]) + + object = author(block.target_account) + object.value = 'activity:object' + + entry << object + entry + end + + def favourite_salmon(favourite) + entry = Ox::Element.new('entry') + add_namespaces(entry) + + description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}" + + append_element(entry, 'id', TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite')) + append_element(entry, 'title', description) + append_element(entry, 'content', description, type: :html) + + entry << author(favourite.account) + + append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', TagManager::VERBS[:favorite]) + + entry << object(favourite.status) + + append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status)) + + entry + end + + def unfavourite_salmon(favourite) + entry = Ox::Element.new('entry') + add_namespaces(entry) + + description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}" + + append_element(entry, 'id', TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite')) + append_element(entry, 'title', description) + append_element(entry, 'content', description, type: :html) + + entry << author(favourite.account) + + append_element(entry, 'activity:object-type', TagManager::TYPES[:activity]) + append_element(entry, 'activity:verb', TagManager::VERBS[:unfavorite]) + + entry << object(favourite.status) + + append_element(entry, 'thr:in-reply-to', nil, ref: TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status)) + + entry + end + + private + + def append_element(parent, name, content = nil, attributes = {}) + element = Ox::Element.new(name) + attributes.each { |k, v| element[k] = v.to_s } + element << content.to_s unless content.nil? + parent << element + end + + def add_namespaces(parent) + parent['xmlns'] = TagManager::XMLNS + parent['xmlns:thr'] = TagManager::THR_XMLNS + parent['xmlns:activity'] = TagManager::AS_XMLNS + parent['xmlns:poco'] = TagManager::POCO_XMLNS + parent['xmlns:media'] = TagManager::MEDIA_XMLNS + parent['xmlns:ostatus'] = TagManager::OS_XMLNS + parent['xmlns:mastodon'] = TagManager::MTDN_XMLNS + end + + def serialize_status_attributes(entry, status) + append_element(entry, 'summary', status.spoiler_text) unless status.spoiler_text.blank? + append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html') + + status.mentions.each do |mentioned| + append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account)) + end + + append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:collection], href: TagManager::COLLECTIONS[:public]) if status.public_visibility? + + status.tags.each do |tag| + append_element(entry, 'category', nil, term: tag.name) + end + + append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive? + + status.media_attachments.each do |media| + append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false))) + end + + append_element(entry, 'mastodon:scope', status.visibility) + end +end diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index 2a5e7a409..07b2fb91e 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -78,6 +78,8 @@ class TagManager case target.object_type when :person account_url(target) + when :note, :comment, :activity + unique_tag(target.created_at, target.id, 'Status') else unique_tag(target.stream_entry.created_at, target.stream_entry.activity_id, target.stream_entry.activity_type) end diff --git a/app/models/account.rb b/app/models/account.rb index 6968607a2..cbba8b5b6 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -125,11 +125,11 @@ class Account < ApplicationRecord end def favourited?(status) - (status.reblog? ? status.reblog : status).favourites.where(account: self).count.positive? + status.proper.favourites.where(account: self).count.positive? end def reblogged?(status) - (status.reblog? ? status.reblog : status).reblogs.where(account: self).count.positive? + status.proper.reblogs.where(account: self).count.positive? end def keypair diff --git a/app/models/status.rb b/app/models/status.rb index 6948ad77c..7e3dd3e28 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -62,8 +62,12 @@ class Status < ApplicationRecord reply? ? :comment : :note end + def proper + reblog? ? reblog : self + end + def content - reblog? ? reblog.text : text + proper.text end def target diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb index ae7ae446e..8aff5ae06 100644 --- a/app/models/stream_entry.rb +++ b/app/models/stream_entry.rb @@ -5,25 +5,21 @@ class StreamEntry < ApplicationRecord belongs_to :account, inverse_of: :stream_entries belongs_to :activity, polymorphic: true - belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry validates :account, :activity, presence: true - STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, mentions: :account], thread: [:stream_entry, :account]].freeze + STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account], thread: [:stream_entry, :account]].freeze + default_scope { where(activity_type: 'Status') } scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) } def object_type - if orphaned? - :activity - else - targeted? ? :activity : activity.object_type - end + orphaned? || targeted? ? :activity : status.object_type end def verb - orphaned? ? :delete : activity.verb + orphaned? ? :delete : status.verb end def targeted? @@ -31,15 +27,15 @@ class StreamEntry < ApplicationRecord end def target - orphaned? ? nil : activity.target + orphaned? ? nil : status.target end def title - orphaned? ? nil : activity.title + orphaned? ? nil : status.title end def content - orphaned? ? nil : activity.content + orphaned? ? nil : status.content end def threaded? @@ -47,20 +43,16 @@ class StreamEntry < ApplicationRecord end def thread - orphaned? ? nil : activity.thread + orphaned? ? nil : status.thread end def mentions - activity.respond_to?(:mentions) ? activity.mentions.map(&:account) : [] - end - - def activity - !new_record? ? send(activity_type.underscore) || super : super + orphaned? ? [] : status.mentions.map(&:account) end private def orphaned? - activity.nil? + status.nil? end end diff --git a/app/services/after_block_service.rb b/app/services/after_block_service.rb index 8c6197f2c..0f478bcb7 100644 --- a/app/services/after_block_service.rb +++ b/app/services/after_block_service.rb @@ -9,20 +9,20 @@ class AfterBlockService < BaseService private def clear_timelines(account, target_account) - mentions_key = FeedManager.instance.key(:mentions, account.id) - home_key = FeedManager.instance.key(:home, account.id) + home_key = FeedManager.instance.key(:home, account.id) - target_account.statuses.select('id').find_each do |status| - redis.zrem(mentions_key, status.id) - redis.zrem(home_key, status.id) + redis.pipelined do + target_account.statuses.select('id').find_each do |status| + redis.zrem(home_key, status.id) + end end end def clear_notifications(account, target_account) - Notification.where(account: account).joins(:follow).where(activity_type: 'Follow', follows: { account_id: target_account.id }).destroy_all - Notification.where(account: account).joins(mention: :status).where(activity_type: 'Mention', statuses: { account_id: target_account.id }).destroy_all - Notification.where(account: account).joins(:favourite).where(activity_type: 'Favourite', favourites: { account_id: target_account.id }).destroy_all - Notification.where(account: account).joins(:status).where(activity_type: 'Status', statuses: { account_id: target_account.id }).destroy_all + Notification.where(account: account).joins(:follow).where(activity_type: 'Follow', follows: { account_id: target_account.id }).delete_all + Notification.where(account: account).joins(mention: :status).where(activity_type: 'Mention', statuses: { account_id: target_account.id }).delete_all + Notification.where(account: account).joins(:favourite).where(activity_type: 'Favourite', favourites: { account_id: target_account.id }).delete_all + Notification.where(account: account).joins(:status).where(activity_type: 'Status', statuses: { account_id: target_account.id }).delete_all end def redis diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index ac465bdb2..97c76bee1 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -10,31 +10,6 @@ class AuthorizeFollowService < BaseService private def build_xml(follow_request) - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest' - title xml, "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}" - - author(xml) do - include_author xml, follow_request.target_account - end - - object_type xml, :activity - verb xml, :authorize - - target(xml) do - author(xml) do - include_author xml, follow_request.account - end - - object_type xml, :activity - verb xml, :request_friend - - target(xml) do - include_author xml, follow_request.target_account - end - end - end - end.to_xml + AtomSerializer.render(AtomSerializer.new.authorize_follow_request_salmon(follow_request)) end end diff --git a/app/services/block_service.rb b/app/services/block_service.rb index bd914d8be..d59b47afb 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -18,22 +18,6 @@ class BlockService < BaseService private def build_xml(block) - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - unique_id xml, block.created_at, block.id, 'Block' - title xml, "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}" - - author(xml) do - include_author xml, block.account - end - - object_type xml, :activity - verb xml, :block - - target(xml) do - include_author xml, block.target_account - end - end - end.to_xml + AtomSerializer.render(AtomSerializer.new.block_salmon(block)) end end diff --git a/app/services/concerns/stream_entry_renderer.rb b/app/services/concerns/stream_entry_renderer.rb index a4255daea..ef176d8a6 100644 --- a/app/services/concerns/stream_entry_renderer.rb +++ b/app/services/concerns/stream_entry_renderer.rb @@ -2,7 +2,6 @@ module StreamEntryRenderer def stream_entry_to_xml(stream_entry) - renderer = StreamEntriesController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https) - renderer.render(:show, assigns: { stream_entry: stream_entry }, formats: [:atom]) + AtomSerializer.render(AtomSerializer.new.entry(stream_entry, true)) end end diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 5cc96403c..e92aada64 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -22,26 +22,6 @@ class FavouriteService < BaseService private def build_xml(favourite) - description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}" - - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - unique_id xml, favourite.created_at, favourite.id, 'Favourite' - title xml, description - content xml, description - - author(xml) do - include_author xml, favourite.account - end - - object_type xml, :activity - verb xml, :favorite - in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status) - - target(xml) do - include_target xml, favourite.status - end - end - end.to_xml + AtomSerializer.render(AtomSerializer.new.favourite_salmon(favourite)) end end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 17b3b2542..844f5282d 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -10,7 +10,7 @@ class FollowService < BaseService target_account = FollowRemoteAccountService.new.call(uri) raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? - raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) + raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) if target_account.locked? request_follow(source_account, target_account) @@ -55,48 +55,10 @@ class FollowService < BaseService end def build_follow_request_xml(follow_request) - description = "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}" - - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - unique_id xml, follow_request.created_at, follow_request.id, 'FollowRequest' - title xml, description - content xml, description - - author(xml) do - include_author xml, follow_request.account - end - - object_type xml, :activity - verb xml, :request_friend - - target(xml) do - include_author xml, follow_request.target_account - end - end - end.to_xml + AtomSerializer.render(AtomSerializer.new.follow_request_salmon(follow_request)) end def build_follow_xml(follow) - description = "#{follow.account.acct} started following #{follow.target_account.acct}" - - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - unique_id xml, follow.created_at, follow.id, 'Follow' - title xml, description - content xml, description - - author(xml) do - include_author xml, follow.account - end - - object_type xml, :activity - verb xml, :follow - - target(xml) do - include_author xml, follow.target_account - end - end - end.to_xml + AtomSerializer.render(AtomSerializer.new.follow_salmon(follow)) end end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index b8179f7dc..221aa42a3 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -37,11 +37,11 @@ class PostStatusService < BaseService def validate_media!(media_ids) return if media_ids.nil? || !media_ids.is_a?(Enumerable) - raise Mastodon::ValidationError, 'Cannot attach more than 4 files' if media_ids.size > 4 + raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if media_ids.size > 4 media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map(&:to_i)) - raise Mastodon::ValidationError, 'Cannot attach a video to a toot that already contains images' if media.size > 1 && media.find(&:video?) + raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if media.size > 1 && media.find(&:video?) media end diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb index 1b03d62e6..675007938 100644 --- a/app/services/reject_follow_service.rb +++ b/app/services/reject_follow_service.rb @@ -10,31 +10,6 @@ class RejectFollowService < BaseService private def build_xml(follow_request) - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest' - title xml, "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}" - - author(xml) do - include_author xml, follow_request.target_account - end - - object_type xml, :activity - verb xml, :reject - - target(xml) do - author(xml) do - include_author xml, follow_request.account - end - - object_type xml, :activity - verb xml, :request_friend - - target(xml) do - include_author xml, follow_request.target_account - end - end - end - end.to_xml + AtomSerializer.render(AtomSerializer.new.reject_follow_request_salmon(follow_request)) end end diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index c4f789f74..3a3fd2d8c 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -11,22 +11,6 @@ class UnblockService < BaseService private def build_xml(block) - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - unique_id xml, Time.now.utc, block.id, 'Block' - title xml, "#{block.account.acct} no longer blocks #{block.target_account.acct}" - - author(xml) do - include_author xml, block.account - end - - object_type xml, :activity - verb xml, :unblock - - target(xml) do - include_author xml, block.target_account - end - end - end.to_xml + AtomSerializer.render(AtomSerializer.new.unblock_salmon(block)) end end diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index 5f0ba4254..a32e87bff 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -13,26 +13,6 @@ class UnfavouriteService < BaseService private def build_xml(favourite) - description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}" - - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - unique_id xml, Time.now.utc, favourite.id, 'Favourite' - title xml, description - content xml, description - - author(xml) do - include_author xml, favourite.account - end - - object_type xml, :activity - verb xml, :unfavorite - in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status) - - target(xml) do - include_target xml, favourite.status - end - end - end.to_xml + AtomSerializer.render(AtomSerializer.new.unfavourite_salmon(favourite)) end end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index 3440da364..244c9b529 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -13,25 +13,6 @@ class UnfollowService < BaseService private def build_xml(follow) - description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}" - - Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - unique_id xml, Time.now.utc, follow.id, 'Follow' - title xml, description - content xml, description - - author(xml) do - include_author xml, follow.account - end - - object_type xml, :activity - verb xml, :unfollow - - target(xml) do - include_author xml, follow.target_account - end - end - end.to_xml + AtomSerializer.render(AtomSerializer.new.unfollow_salmon(follow)) end end diff --git a/app/views/accounts/show.atom.ruby b/app/views/accounts/show.atom.ruby deleted file mode 100644 index e15021178..000000000 --- a/app/views/accounts/show.atom.ruby +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -Nokogiri::XML::Builder.new do |xml| - feed(xml) do - simple_id xml, account_url(@account, format: 'atom') - title xml, @account.display_name - subtitle xml, @account.note - updated_at xml, stream_updated_at - logo xml, full_asset_url(@account.avatar.url(:original)) - - author(xml) do - include_author xml, @account - end - - link_alternate xml, TagManager.instance.url_for(@account) - link_self xml, account_url(@account, format: 'atom') - link_next xml, account_url(@account, format: 'atom', max_id: @entries.last.id) if @entries.size == 20 - link_hub xml, api_push_url - link_salmon xml, api_salmon_url(@account.id) - - @entries.each do |stream_entry| - entry(xml, false) do - include_entry xml, stream_entry - end - end - end -end.to_xml diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7eae6982b..abab14a28 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -11,8 +11,10 @@ %meta{:name => "theme-color", :content => "#282c37"}/ %meta{:name => "apple-mobile-web-app-capable", :content => "yes"}/ - %title - = "#{yield(:page_title)} - " if content_for?(:page_title) + %title< + - if content_for?(:page_title) + = yield(:page_title) + = ' - ' = Setting.site_title = stylesheet_link_tag 'application', media: 'all' diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index d4719881c..d4719881c 100644 --- a/app/views/doorkeeper/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index cdd0dde3b..434c5c8da 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -16,7 +16,7 @@ %strong= display_name(status.account) = t('stream_entries.reblogged') - = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: proper_status(status) } + = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper } - if include_threads = render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true } diff --git a/app/views/stream_entries/show.atom.ruby b/app/views/stream_entries/show.atom.ruby deleted file mode 100644 index a298f3269..000000000 --- a/app/views/stream_entries/show.atom.ruby +++ /dev/null @@ -1,9 +0,0 @@ -Nokogiri::XML::Builder.new do |xml| - entry(xml, true) do - author(xml) do - include_author xml, @stream_entry.account - end - - include_entry xml, @stream_entry - end -end.to_xml diff --git a/app/views/user_mailer/confirmation_instructions.fi.html.erb b/app/views/user_mailer/confirmation_instructions.fi.html.erb new file mode 100644 index 000000000..8b72722da --- /dev/null +++ b/app/views/user_mailer/confirmation_instructions.fi.html.erb @@ -0,0 +1,5 @@ +<p>Tervetuloa <%= @resource.email %>!</p> + +<p>Voit vahvistaa Mastodon tilisi klikkaamalla alla olevaa linkkiä:</p> + +<p><%= link_to 'Varmista tilini', confirmation_url(@resource, confirmation_token: @token) %></p> diff --git a/app/views/user_mailer/confirmation_instructions.fi.text.erb b/app/views/user_mailer/confirmation_instructions.fi.text.erb new file mode 100644 index 000000000..796913abb --- /dev/null +++ b/app/views/user_mailer/confirmation_instructions.fi.text.erb @@ -0,0 +1,5 @@ +Tervetuloa <%= @resource.email %>! + +Voit vahvistaa Mastodon tilisi klikkaamalla alla olevaa linkkiä: + +<%= confirmation_url(@resource, confirmation_token: @token) %> diff --git a/app/views/user_mailer/password_change.fi.html.erb b/app/views/user_mailer/password_change.fi.html.erb new file mode 100644 index 000000000..c56b96593 --- /dev/null +++ b/app/views/user_mailer/password_change.fi.html.erb @@ -0,0 +1,3 @@ +<p>Hei <%= @resource.email %>!</p> + +<p>Lähetämme tämän viestin ilmoittaaksemme että salasanasi on vaihdettu.</p> diff --git a/app/views/user_mailer/password_change.fi.text.erb b/app/views/user_mailer/password_change.fi.text.erb new file mode 100644 index 000000000..d90c3fdeb --- /dev/null +++ b/app/views/user_mailer/password_change.fi.text.erb @@ -0,0 +1,3 @@ +Hei <%= @resource.email %>! + +Lähetämme tämän viestin ilmoittaaksemme että salasanasi on vaihdettu. diff --git a/app/views/user_mailer/reset_password_instructions.fi.html.erb b/app/views/user_mailer/reset_password_instructions.fi.html.erb new file mode 100644 index 000000000..53be0b62b --- /dev/null +++ b/app/views/user_mailer/reset_password_instructions.fi.html.erb @@ -0,0 +1,8 @@ +<p>Hei <%= @resource.email %>!</p> + +<p>Joku on pyytänyt salasanvaihto Mastodonissa. Voit tehdä sen allaolevassa linkissä.</p> + +<p><%= link_to 'Vaihda salasanani', edit_password_url(@resource, reset_password_token: @token) %></p> + +<p>Jos et pyytänyt vaihtoa, poista tämä viesti.</p> +<p>Salasanaasi ei vaihdeta ennen kuin menet ylläolevaan linkkiin ja luot uuden.</p> diff --git a/app/views/user_mailer/reset_password_instructions.fi.text.erb b/app/views/user_mailer/reset_password_instructions.fi.text.erb new file mode 100644 index 000000000..c826d5fc8 --- /dev/null +++ b/app/views/user_mailer/reset_password_instructions.fi.text.erb @@ -0,0 +1,8 @@ +Hei <%= @resource.email %>! + +Joku on pyytänyt salasanvaihto Mastodonissa. Voit tehdä sen allaolevassa linkissä. + +<%= edit_password_url(@resource, reset_password_token: @token) %> + +Jos et pyytänyt vaihtoa, poista tämä viesti. +Salasanaasi ei vaihdeta ennen kuin menet ylläolevaan linkkiin ja luot uuden. diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb index 466def3a8..8412be4b7 100644 --- a/app/workers/pubsubhubbub/delivery_worker.rb +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -13,6 +13,9 @@ class Pubsubhubbub::DeliveryWorker def perform(subscription_id, payload) subscription = Subscription.find(subscription_id) headers = {} + host = Addressable::URI.parse(subscription.callback_url).host + + return if DomainBlock.blocked?(host) headers['User-Agent'] = 'Mastodon/PubSubHubbub' headers['Link'] = LinkHeader.new([[api_push_url, [%w(rel hub)]], [account_url(subscription.account, format: :atom), [%w(rel self)]]]).to_s diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb index 82ff257af..68ca0f870 100644 --- a/app/workers/pubsubhubbub/distribution_worker.rb +++ b/app/workers/pubsubhubbub/distribution_worker.rb @@ -10,14 +10,10 @@ class Pubsubhubbub::DistributionWorker return if stream_entry.hidden? - account = stream_entry.account - renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https) - payload = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom]) - # domains = account.followers_domains + account = stream_entry.account + payload = AtomSerializer.render(AtomSerializer.new.feed(account, [stream_entry])) Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription| - host = Addressable::URI.parse(subscription.callback_url).host - next if DomainBlock.blocked?(host) # || !domains.include?(host) Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload) end rescue ActiveRecord::RecordNotFound |