about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/background-photo.jpegbin894792 -> 214464 bytes
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx3
-rw-r--r--app/assets/javascripts/components/locales/eo.jsx68
-rw-r--r--app/assets/javascripts/components/locales/index.jsx4
-rw-r--r--app/assets/stylesheets/components.scss4
-rw-r--r--app/controllers/accounts_controller.rb3
-rw-r--r--app/controllers/application_controller.rb9
-rw-r--r--app/controllers/concerns/localized.rb19
-rw-r--r--app/controllers/oauth/authorizations_controller.rb9
-rw-r--r--app/controllers/oauth/authorized_applications_controller.rb16
-rw-r--r--app/controllers/stream_entries_controller.rb4
-rw-r--r--app/helpers/settings_helper.rb1
-rw-r--r--app/helpers/stream_entries_helper.rb4
-rw-r--r--app/lib/atom_serializer.rb351
-rw-r--r--app/lib/tag_manager.rb2
-rw-r--r--app/models/account.rb4
-rw-r--r--app/models/status.rb6
-rw-r--r--app/models/stream_entry.rb28
-rw-r--r--app/services/after_block_service.rb18
-rw-r--r--app/services/authorize_follow_service.rb27
-rw-r--r--app/services/block_service.rb18
-rw-r--r--app/services/concerns/stream_entry_renderer.rb3
-rw-r--r--app/services/fan_out_on_write_service.rb9
-rw-r--r--app/services/favourite_service.rb22
-rw-r--r--app/services/follow_service.rb44
-rw-r--r--app/services/notify_service.rb2
-rw-r--r--app/services/post_status_service.rb4
-rw-r--r--app/services/reject_follow_service.rb27
-rw-r--r--app/services/remove_status_service.rb28
-rw-r--r--app/services/unblock_service.rb18
-rw-r--r--app/services/unfavourite_service.rb22
-rw-r--r--app/services/unfollow_service.rb21
-rw-r--r--app/views/accounts/show.atom.ruby27
-rw-r--r--app/views/layouts/application.html.haml6
-rw-r--r--app/views/oauth/authorized_applications/index.html.haml (renamed from app/views/doorkeeper/authorized_applications/index.html.haml)0
-rw-r--r--app/views/stream_entries/_status.html.haml2
-rw-r--r--app/views/stream_entries/show.atom.ruby9
-rw-r--r--app/views/user_mailer/confirmation_instructions.fi.html.erb5
-rw-r--r--app/views/user_mailer/confirmation_instructions.fi.text.erb5
-rw-r--r--app/views/user_mailer/password_change.fi.html.erb3
-rw-r--r--app/views/user_mailer/password_change.fi.text.erb3
-rw-r--r--app/views/user_mailer/reset_password_instructions.fi.html.erb8
-rw-r--r--app/views/user_mailer/reset_password_instructions.fi.text.erb8
-rw-r--r--app/workers/import_worker.rb2
-rw-r--r--app/workers/pubsubhubbub/delivery_worker.rb3
-rw-r--r--app/workers/pubsubhubbub/distribution_worker.rb8
46 files changed, 578 insertions, 309 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/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index c63fcc1fe..19eedc0a7 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -51,21 +51,22 @@ class FanOutOnWriteService < BaseService
 
   def render_anonymous_payload(status)
     @payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show')
+    @payload = Oj.dump(event: :update, payload: @payload)
   end
 
   def deliver_to_hashtags(status)
     Rails.logger.debug "Delivering status #{status.id} to hashtags"
 
     status.tags.pluck(:name).each do |hashtag|
-      Redis.current.publish("hashtag:#{hashtag}", Oj.dump(event: :update, payload: @payload))
-      Redis.current.publish("hashtag:#{hashtag}:local", Oj.dump(event: :update, payload: @payload)) if status.account.local?
+      Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
+      Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
     end
   end
 
   def deliver_to_public(status)
     Rails.logger.debug "Delivering status #{status.id} to public timeline"
 
-    Redis.current.publish('public', Oj.dump(event: 'update', payload: @payload))
-    Redis.current.publish('public:local', Oj.dump(event: 'update', payload: @payload)) if status.account.local?
+    Redis.current.publish('timeline:public', @payload)
+    Redis.current.publish('timeline:public:local', @payload) if status.local?
   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/notify_service.rb b/app/services/notify_service.rb
index 62508a049..ffeee5fcf 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -50,7 +50,7 @@ class NotifyService < BaseService
   def create_notification
     @notification.save!
     return unless @notification.browserable?
-    Redis.current.publish(@recipient.id, Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show')))
+    Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show')))
   end
 
   def send_email
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/remove_status_service.rb b/app/services/remove_status_service.rb
index e19fdd030..50bb7fc97 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -4,6 +4,8 @@ class RemoveStatusService < BaseService
   include StreamEntryRenderer
 
   def call(status)
+    @payload = Oj.dump(event: :delete, payload: status.id)
+
     remove_from_self(status) if status.account.local?
     remove_from_followers(status)
     remove_from_mentioned(status)
@@ -25,25 +27,23 @@ class RemoveStatusService < BaseService
   end
 
   def remove_from_followers(status)
-    status.account.followers.each do |follower|
-      next unless follower.local?
+    status.account.followers.where(domain: nil).each do |follower|
       unpush(:home, follower, status)
     end
   end
 
   def remove_from_mentioned(status)
+    return unless status.local?
     notified_domains = []
 
     status.mentions.each do |mention|
       mentioned_account = mention.account
 
-      if mentioned_account.local?
-        unpush(:mentions, mentioned_account, status)
-      else
-        next if notified_domains.include?(mentioned_account.domain)
-        notified_domains << mentioned_account.domain
-        send_delete_salmon(mentioned_account, status)
-      end
+      next if mentioned_account.local?
+      next if notified_domains.include?(mentioned_account.domain)
+
+      notified_domains << mentioned_account.domain
+      send_delete_salmon(mentioned_account, status)
     end
   end
 
@@ -65,17 +65,19 @@ class RemoveStatusService < BaseService
       redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id)
     end
 
-    Redis.current.publish(receiver.id, Oj.dump(event: :delete, payload: status.id))
+    Redis.current.publish("timeline:#{receiver.id}", @payload)
   end
 
   def remove_from_hashtags(status)
-    status.tags.each do |tag|
-      Redis.current.publish("hashtag:#{tag.name}", Oj.dump(event: :delete, payload: status.id))
+    status.tags.pluck(:name) do |hashtag|
+      Redis.current.publish("timeline:hashtag:#{hashtag}", @payload)
+      Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if status.local?
     end
   end
 
   def remove_from_public(status)
-    Redis.current.publish('public', Oj.dump(event: :delete, payload: status.id))
+    Redis.current.publish('timeline:public', @payload)
+    Redis.current.publish('timeline:public:local', @payload) if status.local?
   end
 
   def redis
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/import_worker.rb b/app/workers/import_worker.rb
index 7cf29fb53..d5a33cada 100644
--- a/app/workers/import_worker.rb
+++ b/app/workers/import_worker.rb
@@ -46,7 +46,7 @@ class ImportWorker
 
       begin
         FollowService.new.call(from_account, row[0])
-      rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
+      rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
         next
       end
     end
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