about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock4
-rw-r--r--ISSUE_TEMPLATE.md1
-rw-r--r--README.md8
-rw-r--r--app/assets/javascripts/components/features/ui/containers/status_list_container.jsx6
-rw-r--r--app/assets/javascripts/components/locales/fr.jsx1
-rw-r--r--app/assets/javascripts/components/locales/ja.jsx194
-rw-r--r--app/assets/javascripts/components/reducers/statuses.jsx3
-rw-r--r--app/assets/stylesheets/components.scss12
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb14
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/models/concerns/.keep0
-rw-r--r--app/models/domain_block.rb2
-rw-r--r--app/models/import.rb8
-rw-r--r--app/models/status.rb4
-rw-r--r--app/services/block_domain_service.rb32
-rw-r--r--app/services/follow_remote_account_service.rb6
-rw-r--r--app/services/process_feed_service.rb10
-rw-r--r--app/services/suspend_account_service.rb3
-rw-r--r--app/services/unblock_domain_service.rb15
-rw-r--r--app/services/unfollow_service.rb1
-rw-r--r--app/views/about/terms.no.html.haml2
-rw-r--r--app/views/accounts/_header.html.haml40
-rw-r--r--app/views/accounts/followers.html.haml2
-rw-r--r--app/views/accounts/following.html.haml2
-rw-r--r--app/views/accounts/show.html.haml2
-rw-r--r--app/views/admin/domain_blocks/index.html.haml9
-rw-r--r--app/views/admin/domain_blocks/new.html.haml3
-rw-r--r--app/views/admin/domain_blocks/show.html.haml9
-rw-r--r--app/views/api/v1/statuses/_media.rabl4
-rw-r--r--app/views/settings/two_factor_auths/_recovery_codes.html.haml2
-rw-r--r--app/views/settings/two_factor_auths/create.html.haml2
-rw-r--r--app/views/settings/two_factor_auths/recovery_codes.html.haml2
-rw-r--r--app/views/stream_entries/_og_description.html.haml4
-rw-r--r--app/views/stream_entries/_og_image.html.haml6
-rw-r--r--app/views/stream_entries/show.html.haml13
-rwxr-xr-xbin/rspec17
-rw-r--r--config/deploy.rb5
-rw-r--r--config/locales/en.yml16
-rw-r--r--config/locales/ja.yml23
-rw-r--r--config/locales/pt.yml77
-rw-r--r--config/locales/simple_form.ja.yml7
-rw-r--r--config/locales/simple_form.pt.yml12
-rw-r--r--config/routes.rb2
-rw-r--r--lib/tasks/mastodon.rake35
-rw-r--r--package.json1
-rw-r--r--spec/views/stream_entries/show.html.haml_spec.rb1
48 files changed, 450 insertions, 181 deletions
diff --git a/.travis.yml b/.travis.yml
index 45a71d83c..a91d70cf5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,6 +14,7 @@ addons:
   postgresql: 9.4
 
 rvm:
+  - 2.3.4
   - 2.4.1
 
 services:
diff --git a/Gemfile b/Gemfile
index 57600ed9f..0165219d3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 source 'https://rubygems.org'
-ruby '2.4.1'
+ruby '>= 2.3.0', '< 2.5.0'
 
 gem 'pkg-config'
 
@@ -88,7 +88,7 @@ group :development do
   gem 'bullet'
   gem 'active_record_query_trace'
 
-  gem 'capistrano'
+  gem 'capistrano', '3.8.0'
   gem 'capistrano-rails'
   gem 'capistrano-rbenv'
   gem 'capistrano-yarn'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2f55abec7..4b4dd105c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -41,7 +41,7 @@ GEM
       tzinfo (~> 1.1)
     addressable (2.5.1)
       public_suffix (~> 2.0, >= 2.0.2)
-    airbrussh (1.1.2)
+    airbrussh (1.2.0)
       sshkit (>= 1.6.1, != 1.7.0)
     arel (7.1.4)
     ast (2.3.0)
@@ -469,7 +469,7 @@ DEPENDENCIES
   binding_of_caller
   browserify-rails
   bullet
-  capistrano
+  capistrano (= 3.8.0)
   capistrano-faster-assets (~> 1.0)
   capistrano-rails
   capistrano-rbenv
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
index 8394b2424..c78bcb492 100644
--- a/ISSUE_TEMPLATE.md
+++ b/ISSUE_TEMPLATE.md
@@ -3,3 +3,4 @@
 * * * *
 
 - [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.
+- [ ] This bug happens on a [tagged release](https://github.com/tootsuite/mastodon/releases) and not on `master` (If you're a user, don't worry about this).
diff --git a/README.md b/README.md
index 0a28170b7..ea7b20a5c 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,14 @@ If you would like, you can [support the development of this project on Patreon][
 - **Deployable via Docker**
   You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
 
+## Checking out
+
+If you want a stable release for production use, you should use tagged releases. To checkout the latest available tagged version:
+
+    git clone https://github.com/tootsuite/mastodon.git
+    cd mastodon
+    git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
+
 ## Configuration
 
 - `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related
diff --git a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
index f249240d8..4c33f2b61 100644
--- a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
@@ -24,8 +24,10 @@ const makeGetStatusIds = () => createSelector([
 
   if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
     try {
-      const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
-      showStatus = showStatus && !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'content']) : statusForId.get('content'));
+      if (showStatus) {
+        const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
+        showStatus = !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'unescaped_content']) : statusForId.get('unescaped_content'));
+      }
     } catch(e) {
       // Bad regex, don't affect filters
     }
diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx
index 8838c264f..0a1dd38ae 100644
--- a/app/assets/javascripts/components/locales/fr.jsx
+++ b/app/assets/javascripts/components/locales/fr.jsx
@@ -75,6 +75,7 @@ const fr = {
   "navigation_bar.favourites": "Favoris",
   "navigation_bar.info": "Plus d'informations",
   "navigation_bar.logout": "Déconnexion",
+  "navigation_bar.mutes": "Utilisateurs muets",
   "navigation_bar.follow_requests": "Demandes de suivi",
   "reply_indicator.cancel": "Annuler",
   "search.placeholder": "Rechercher",
diff --git a/app/assets/javascripts/components/locales/ja.jsx b/app/assets/javascripts/components/locales/ja.jsx
index c64d7ecc6..79defd325 100644
--- a/app/assets/javascripts/components/locales/ja.jsx
+++ b/app/assets/javascripts/components/locales/ja.jsx
@@ -1,121 +1,125 @@
 const ja = {
-  "column_back_button.label": "戻る",
-  "lightbox.close": "閉じる",
-  "loading_indicator.label": "読み込み中...",
-  "status.mention": "@{name} さんへの返信",
-  "status.delete": "削除",
-  "status.reply": "返信",
-  "status.reblog": "ブースト",
-  "status.favourite": "お気に入り",
-  "status.reblogged_by": "{name} さんにブーストされました",
-  "status.sensitive_warning": "不適切なコンテンツ",
-  "status.sensitive_toggle": "クリックして表示",
-  "status.show_more": "もっと見る",
-  "status.load_more": "もっと見る",
-  "status.show_less": "隠す",
-  "status.open": "Expand this status",
-  "status.report": "@{name} さんを通報",
-  "status.media_hidden": "非表示のメデイア",
-  "video_player.toggle_sound": "音の切り替え",
-  "account.mention": "@{name} さんに返信",
-  "account.edit_profile": "プロフィールを編集",
-  "account.unblock": "@{name} さんのブロックを解除",
-  "account.unfollow": "フォロー解除",
   "account.block": "@{name} さんをブロック",
-  "account.mute": "ミュート",
-  "account.unmute": "ミュート解除",
+  "account.disclaimer": "このユーザーは他のインスタンスに所属しているため、数字が正確で無い場合があります。",
+  "account.edit_profile": "プロフィールを編集",
   "account.follow": "フォロー",
-  "account.report": "@{name}を通報する",
-  "account.posts": "投稿",
-  "account.follows": "フォロー",
   "account.followers": "フォロワー",
+  "account.follows": "フォロー",
   "account.follows_you": "フォローされています",
+  "account.mention": "@{name} さんに返信",
+  "account.mute": "ミュート",
+  "account.posts": "投稿",
+  "account.report": "@{name}を通報する",
   "account.requested": "承認待ち",
-  "follow_request.authorize": "許可",
-  "follow_request.reject": "拒否",
-  "getting_started.heading": "スタート",
-  "getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
-  "getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
-  "getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
-  "getting_started.apps": "さまざまなアプリで利用できます。",
-  "column.home": "ホーム",
+  "account.unblock": "@{name} さんのブロックを解除",
+  "account.unfollow": "フォロー解除",
+  "account.unmute": "ミュート解除",
+  "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
+  "column.blocks": "ブロックしたユーザー",
   "column.community": "ローカルタイムライン",
-  "column.public": "連合タイムライン",
-  "column.notifications": "通知",
   "column.favourites": "お気に入り",
-  "tabs_bar.compose": "投稿",
-  "tabs_bar.home": "ホーム",
-  "tabs_bar.mentions": "返信",
-  "tabs_bar.local_timeline": "ローカル",
-  "tabs_bar.federated_timeline": "連合",
-  "tabs_bar.notifications": "通知",
+  "column.follow_requests": "フォローリクエスト",
+  "column.home": "ホーム",
+  "column.mutes": "ミュートしたユーザー",
+  "column.notifications": "通知",
+  "column.public": "連合タイムライン",
+  "column_back_button.label": "戻る",
   "compose_form.placeholder": "今なにしてる?",
+  "compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザー(at {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか?投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
   "compose_form.publish": "トゥート",
   "compose_form.sensitive": "メディアを不適切なコンテンツとしてマークする",
   "compose_form.spoiler": "テキストを隠す",
-  "compose_form.spoiler_placeholder": "内容注意メッセージ",
-  "compose_form.private": "非公開にする",
-  "compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザー(at {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか?投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
-  "compose_form.unlisted": "公開タイムラインに表示しない",
-  "privacy.public.short": "公開",
-  "privacy.public.long": "公開TLに投稿する",
-  "privacy.unlisted.short": "未収載",
-  "privacy.unlisted.long": "公開TLで表示しない",
-  "privacy.private.short": "非公開",
-  "privacy.private.long": "フォロワーだけに公開",
-  "privacy.direct.short": "ダイレクト",
-  "privacy.direct.long": "含んだユーザーだけに公開",
-  "privacy.change": "投稿のプライバシーを変更",
-  "report.heading": "新規通報",
-  "report.placeholder": "コメント",
-  "report.target": "問題のユーザー",
-  "report.submit": "通報する",
-  "navigation_bar.edit_profile": "プロフィールを編集",
-  "navigation_bar.preferences": "ユーザー設定",
+  "compose_form.spoiler_placeholder": "閲覧注意",
+  "emoji_button.label": "絵文字を追加",
+  "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
+  "empty_column.hashtag": "このハッシュタグはまだ使われていません。",
+  "empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
+  "empty_column.home.public_timeline": "連合タイムライン",
+  "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
+  "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
+  "follow_request.authorize": "許可",
+  "follow_request.reject": "拒否",
+  "getting_started.apps": "さまざまなアプリで利用できます。",
+  "getting_started.heading": "スタート",
+  "getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
+  "home.column_settings.advanced": "上級者向け",
+  "home.column_settings.basic": "シンプル",
+  "home.column_settings.filter_regex": "正規表現でフィルター",
+  "home.column_settings.show_reblogs": "ブースト表示",
+  "home.column_settings.show_replies": "返信表示",
+  "home.settings": "カラム設定",
+  "lightbox.close": "閉じる",
+  "loading_indicator.label": "読み込み中...",
+  "media_gallery.toggle_visible": "表示切り替え",
+  "missing_indicator.label": "見つかりません",
+  "navigation_bar.blocks": "ブロックしたユーザー",
   "navigation_bar.community_timeline": "ローカルタイムライン",
-  "navigation_bar.public_timeline": "連合タイムライン",
-  "navigation_bar.logout": "ログアウト",
+  "navigation_bar.edit_profile": "プロフィールを編集",
   "navigation_bar.favourites": "お気に入り",
-  "navigation_bar.blocks": "ブロックしたユーザー",
+  "navigation_bar.follow_requests": "フォローリクエスト",
   "navigation_bar.info": "サーバー情報",
-  "reply_indicator.cancel": "キャンセル",
-  "search.placeholder": "検索",
-  "search.account": "アカウント",
-  "search.hashtag": "ハッシュタグ",
-  "search.status_by": "{uuuname}からの投稿",
-  "search_results.total": "{count} 件",
-  "upload_area.title": "ファイルをこちらにドラッグしてください",
-  "upload_button.label": "メディアを追加",
-  "upload_form.undo": "やり直す",
-  "notification.follow": "{name} さんにフォローされました",
+  "navigation_bar.logout": "ログアウト",
+  "navigation_bar.mutes": "ミュートしたユーザー",
+  "navigation_bar.preferences": "ユーザー設定",
+  "navigation_bar.public_timeline": "連合タイムライン",
   "notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
-  "notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
+  "notification.follow": "{name} さんにフォローされました",
   "notification.mention": "{name} さんがあなたに返信しました",
-  "notifications.clear": "通知を片付ける",
-  "notifications.clear_confirmation": "通知を全部片付けます。大丈夫ですか?",
+  "notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
+  "notifications.clear": "通知を消去",
+  "notifications.clear_confirmation": "本当に通知を消去しますか?",
   "notifications.column_settings.alert": "デスクトップ通知",
-  "notifications.column_settings.show": "カラムに表示",
-  "notifications.column_settings.follow": "新しいフォロワー",
   "notifications.column_settings.favourite": "お気に入り",
+  "notifications.column_settings.follow": "新しいフォロワー",
   "notifications.column_settings.mention": "返信",
   "notifications.column_settings.reblog": "ブースト",
+  "notifications.column_settings.show": "カラムに表示",
   "notifications.column_settings.sound": "通知音を再生",
-  "empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
-  "empty_column.home.public_timeline": "連合タイムライン",
-  "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
-  "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
-  "empty_column.hashtag": "このハッシュタグはまだ使っていません。",
-  "upload_progress.label": "アップロード中…",
-  "emoji_button.label": "絵文字を追加",
-  "home.column_settings.basic": "シンプル",
-  "home.column_settings.advanced": "エキスパート",
-  "home.column_settings.show_reblogs": "ブースト表示",
-  "home.column_settings.show_replies": "返信表示",
-  "home.column_settings.filter_regex": "正規表現でフィルター",
-  "home.settings": "カラム設定",
   "notifications.settings": "カラム設定",
-  "missing_indicator.label": "見つかりません",
-  "boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
+  "privacy.change": "投稿のプライバシーを変更",
+  "privacy.direct.long": "メンションしたユーザーだけに公開",
+  "privacy.direct.short": "ダイレクト",
+  "privacy.private.long": "フォロワーだけに公開",
+  "privacy.private.short": "非公開",
+  "privacy.public.long": "公開TLに投稿する",
+  "privacy.public.short": "公開",
+  "privacy.unlisted.long": "公開TLで表示しない",
+  "privacy.unlisted.short": "未収載",
+  "reply_indicator.cancel": "キャンセル",
+  "report.heading": "新規通報",
+  "report.placeholder": "コメント",
+  "report.submit": "通報する",
+  "report.target": "問題のユーザー",
+  "search.placeholder": "検索",
+  "search.status_by": "{name}からの投稿",
+  "search_results.total": "{count} {count, plural, one {result} other {results}} 件",
+  "status.delete": "削除",
+  "status.favourite": "お気に入り",
+  "status.load_more": "もっと見る",
+  "status.media_hidden": "非表示のメデイア",
+  "status.mention": "@{name} さんへの返信",
+  "status.open": "詳細を表示",
+  "status.reblog": "ブースト",
+  "status.reblogged_by": "{name} さんにブーストされました",
+  "status.reply": "返信",
+  "status.report": "@{name} さんを通報",
+  "status.sensitive_toggle": "クリックして表示",
+  "status.sensitive_warning": "不適切なコンテンツ",
+  "status.show_less": "隠す",
+  "status.show_more": "もっと見る",
+  "tabs_bar.compose": "投稿",
+  "tabs_bar.federated_timeline": "連合",
+  "tabs_bar.home": "ホーム",
+  "tabs_bar.local_timeline": "ローカル",
+  "tabs_bar.notifications": "通知",
+  "upload_area.title": "ドラッグ&ドロップでアップロード",
+  "upload_button.label": "メディアを追加",
+  "upload_form.undo": "やり直す",
+  "upload_progress.label": "アップロード中…",
+  "video_player.expand": "動画の詳細",
+  "video_player.toggle_sound": "音の切り替え",
+  "video_player.toggle_visible": "表示切り替え",
+  "video_player.video_error": "動画の再生に失敗しました",
 };
 
 export default ja;
diff --git a/app/assets/javascripts/components/reducers/statuses.jsx b/app/assets/javascripts/components/reducers/statuses.jsx
index ca8fa7a01..2002d2223 100644
--- a/app/assets/javascripts/components/reducers/statuses.jsx
+++ b/app/assets/javascripts/components/reducers/statuses.jsx
@@ -48,6 +48,9 @@ const normalizeStatus = (state, status) => {
     normalStatus.reblog = status.reblog.id;
   }
 
+  const linebreakComplemented = status.content.replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
+  normalStatus.unescaped_content = new DOMParser().parseFromString(linebreakComplemented, 'text/html').documentElement.textContent;
+
   return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus)));
 };
 
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index d1c4d2bb2..8bd35819a 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -1203,6 +1203,10 @@ a.status__content__spoiler-link {
   &:focus {
     outline: 0;
   }
+
+  @media screen and (max-width: 600px) {
+    font-size: 16px;
+  }
 }
 
 .spoiler-input__input {
@@ -1267,6 +1271,10 @@ a.status__content__spoiler-link {
     color: $color5;
     border-bottom-color: $color4;
   }
+
+  @media screen and (max-width: 600px) {
+    font-size: 16px;
+  }
 }
 
 @import 'boost';
@@ -1906,6 +1914,10 @@ button.icon-button.active i.fa-retweet {
   &:focus {
     background: lighten($color1, 4%);
   }
+
+  @media screen and (max-width: 600px) {
+    font-size: 16px;
+  }
 }
 
 .search__icon {
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index a8b56c085..5d146d946 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -15,16 +15,26 @@ module Admin
 
       if @domain_block.save
         DomainBlockWorker.perform_async(@domain_block.id)
-        redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
+        redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_block.created_msg')
       else
         render action: :new
       end
     end
 
+    def show
+      @domain_block = DomainBlock.find(params[:id])
+    end
+
+    def destroy
+      @domain_block = DomainBlock.find(params[:id])
+      UnblockDomainService.new.call(@domain_block, resource_params[:retroactive])
+      redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_block.destroyed_msg')
+    end
+
     private
 
     def resource_params
-      params.require(:domain_block).permit(:domain, :severity)
+      params.require(:domain_block).permit(:domain, :severity, :reject_media, :retroactive)
     end
   end
 end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 0c320177d..e8d7de218 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -8,7 +8,9 @@ class ApplicationController < ActionController::Base
   force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
 
   include Localized
-  helper_method :current_account, :single_user_mode?
+
+  helper_method :current_account
+  helper_method :single_user_mode?
 
   rescue_from ActionController::RoutingError, with: :not_found
   rescue_from ActiveRecord::RecordNotFound, with: :not_found
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
deleted file mode 100644
index e69de29bb..000000000
--- a/app/models/concerns/.keep
+++ /dev/null
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 3548ccd69..89c81f766 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -3,6 +3,8 @@
 class DomainBlock < ApplicationRecord
   enum severity: [:silence, :suspend]
 
+  attr_accessor :retroactive
+
   validates :domain, presence: true, uniqueness: true
 
   def self.blocked?(domain)
diff --git a/app/models/import.rb b/app/models/import.rb
index 3013bc50e..85f6ca4bd 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -1,13 +1,15 @@
 # frozen_string_literal: true
 
 class Import < ApplicationRecord
+  FILE_TYPES = ['text/plain', 'text/csv'].freeze
+
   self.inheritance_column = false
 
-  enum type: [:following, :blocking, :muting]
+  belongs_to :account, required: true
 
-  belongs_to :account
+  enum type: [:following, :blocking, :muting]
 
-  FILE_TYPES = ['text/plain', 'text/csv'].freeze
+  validates :type, presence: true
 
   has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
   validates_attachment_content_type :data, content_type: FILE_TYPES
diff --git a/app/models/status.rb b/app/models/status.rb
index 22d93947a..5393acfcc 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -110,6 +110,10 @@ class Status < ApplicationRecord
     results
   end
 
+  def non_sensitive_with_media?
+    !sensitive? && media_attachments.any?
+  end
+
   class << self
     def as_home_timeline(account)
       where(account: [account] + account.following)
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index 6c131bd34..97d2ebcd7 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -3,12 +3,34 @@
 class BlockDomainService < BaseService
   def call(domain_block)
     if domain_block.silence?
-      Account.where(domain: domain_block.domain).update_all(silenced: true)
+      silence_accounts!(domain_block.domain)
+      clear_media!(domain_block.domain) if domain_block.reject_media?
     else
-      Account.where(domain: domain_block.domain).find_each do |account|
-        account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed?
-        SuspendAccountService.new.call(account)
-      end
+      suspend_accounts!(domain_block.domain)
+    end
+  end
+
+  private
+
+  def silence_accounts!(domain)
+    Account.where(domain: domain).update_all(silenced: true)
+  end
+
+  def clear_media!(domain)
+    Account.where(domain: domain).find_each do |account|
+      account.avatar.destroy
+      account.header.destroy
+    end
+
+    MediaAttachment.where(account: Account.where(domain: domain)).find_each do |attachment|
+      attachment.file.destroy
+    end
+  end
+
+  def suspend_accounts!(domain)
+    Account.where(domain: domain).where(suspended: false).find_each do |account|
+      account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed?
+      SuspendAccountService.new.call(account)
     end
   end
 end
diff --git a/app/services/follow_remote_account_service.rb b/app/services/follow_remote_account_service.rb
index dce712b40..14bc064d5 100644
--- a/app/services/follow_remote_account_service.rb
+++ b/app/services/follow_remote_account_service.rb
@@ -16,7 +16,7 @@ class FollowRemoteAccountService < BaseService
     return Account.find_local(username) if TagManager.instance.local_domain?(domain)
 
     account = Account.find_remote(username, domain)
-    return account unless account&.last_webfingered_at.nil? || 1.day.from_now(account.last_webfingered_at) < Time.now.utc
+    return account unless account_needs_webfinger_update?(account)
 
     Rails.logger.debug "Looking up webfinger for #{uri}"
 
@@ -62,6 +62,10 @@ class FollowRemoteAccountService < BaseService
 
   private
 
+  def account_needs_webfinger_update?(account)
+    account&.last_webfingered_at.nil? || account.last_webfingered_at <= 1.day.ago
+  end
+
   def get_feed(url)
     response = http_client.get(Addressable::URI.parse(url))
     [response.to_s, Nokogiri::XML(response)]
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index 321f53f22..fa0633b27 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -179,12 +179,12 @@ class ProcessFeedService < BaseService
     end
 
     def hashtags_from_xml(parent, xml)
-      tags = xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select { |t| !t.blank? }
+      tags = xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
       ProcessHashtagsService.new.call(parent, tags)
     end
 
     def media_from_xml(parent, xml)
-      return if DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
+      do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
 
       xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link|
         next unless link['href']
@@ -192,7 +192,11 @@ class ProcessFeedService < BaseService
         media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
         parsed_url = URI.parse(link['href'])
 
-        next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty?
+        next if !%w[http https].include?(parsed_url.scheme) || parsed_url.host.empty?
+
+        media.save
+
+        next if do_not_download
 
         begin
           media.file_remote_url = link['href']
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 42ff4dcb7..66517470e 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -13,6 +13,7 @@ class SuspendAccountService < BaseService
 
   def purge_content
     @account.statuses.reorder(nil).find_each do |status|
+      # This federates out deletes to previous followers
       RemoveStatusService.new.call(status)
     end
 
@@ -29,9 +30,7 @@ class SuspendAccountService < BaseService
     @account.display_name = ''
     @account.note         = ''
     @account.avatar.destroy
-    @account.avatar.clear
     @account.header.destroy
-    @account.header.clear
     @account.save!
   end
 
diff --git a/app/services/unblock_domain_service.rb b/app/services/unblock_domain_service.rb
new file mode 100644
index 000000000..9794e439d
--- /dev/null
+++ b/app/services/unblock_domain_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class UnblockDomainService < BaseService
+  def call(domain_block, retroactive)
+    if retroactive
+      if domain_block.silence?
+        Account.where(domain: domain_block.domain).update_all(silenced: false)
+      else
+        Account.where(domain: domain_block.domain).update_all(suspended: false)
+      end
+    end
+
+    domain_block.destroy
+  end
+end
diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb
index 244c9b529..9b39f4945 100644
--- a/app/services/unfollow_service.rb
+++ b/app/services/unfollow_service.rb
@@ -6,6 +6,7 @@ class UnfollowService < BaseService
   # @param [Account] target_account Which to unfollow
   def call(source_account, target_account)
     follow = source_account.unfollow!(target_account)
+    return unless follow
     NotificationWorker.perform_async(build_xml(follow), source_account.id, target_account.id) unless target_account.local?
     UnmergeWorker.perform_async(target_account.id, source_account.id)
   end
diff --git a/app/views/about/terms.no.html.haml b/app/views/about/terms.no.html.haml
index 5506cd863..32ec57ed1 100644
--- a/app/views/about/terms.no.html.haml
+++ b/app/views/about/terms.no.html.haml
@@ -71,6 +71,6 @@
   %p Dette dokumentet er lisensiert under CC-BY-SA. De ble sist oppdatert 12. april 2017.
 
   %p
-    Dokumentet er en adoptert og endret versjon fra 
+    Dokumentet er en adoptert og endret versjon fra
     = succeed '.' do
       = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse'
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
index beee96cd8..6538885a0 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -1,34 +1,34 @@
-.card.h-card.p-author{ style: "background-image: url(#{@account.header.url( :original)})" }
-  - if user_signed_in? && current_account.id != @account.id && !current_account.requested?(@account)
+.card.h-card.p-author{ style: "background-image: url(#{account.header.url( :original)})" }
+  - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account)
     .controls
-      - if current_account.following?(@account)
-        = link_to t('accounts.unfollow'), unfollow_account_path(@account), data: { method: :post }, class: 'button'
+      - if current_account.following?(account)
+        = link_to t('accounts.unfollow'), unfollow_account_path(account), data: { method: :post }, class: 'button'
       - else
-        = link_to t('accounts.follow'), follow_account_path(@account), data: { method: :post }, class: 'button'
+        = link_to t('accounts.follow'), follow_account_path(account), data: { method: :post }, class: 'button'
   - elsif !user_signed_in?
     .controls
       .remote-follow
-        = link_to t('accounts.remote_follow'), account_remote_follow_path(@account), class: 'button'
-  .avatar= image_tag @account.avatar.url(:original), class: 'u-photo'
+        = link_to t('accounts.remote_follow'), account_remote_follow_path(account), class: 'button'
+  .avatar= image_tag account.avatar.url(:original), class: 'u-photo'
   %h1.name
-    %span.p-name.emojify= display_name(@account)
+    %span.p-name.emojify= display_name(account)
     %small
-      %span= "@#{@account.username}"
-      = fa_icon('lock') if @account.locked?
+      %span= "@#{account.username}"
+      = fa_icon('lock') if account.locked?
   .details
     .bio
-      .account__header__content.p-note.emojify= Formatter.instance.simplified_format(@account)
+      .account__header__content.p-note.emojify= Formatter.instance.simplified_format(account)
 
     .details-counters
-      .counter{ class: active_nav_class(short_account_url(@account)) }
-        = link_to short_account_url(@account), class: 'u-url u-uid' do
+      .counter{ class: active_nav_class(short_account_url(account)) }
+        = link_to short_account_url(account), class: 'u-url u-uid' do
           %span.counter-label= t('accounts.posts')
-          %span.counter-number= number_with_delimiter @account.statuses_count
-      .counter{ class: active_nav_class(following_account_url(@account)) }
-        = link_to following_account_url(@account) do
+          %span.counter-number= number_with_delimiter account.statuses_count
+      .counter{ class: active_nav_class(following_account_url(account)) }
+        = link_to following_account_url(account) do
           %span.counter-label= t('accounts.following')
-          %span.counter-number= number_with_delimiter @account.following_count
-      .counter{ class: active_nav_class(followers_account_url(@account)) }
-        = link_to followers_account_url(@account) do
+          %span.counter-number= number_with_delimiter account.following_count
+      .counter{ class: active_nav_class(followers_account_url(account)) }
+        = link_to followers_account_url(account) do
           %span.counter-label= t('accounts.followers')
-          %span.counter-number= number_with_delimiter @account.followers_count
+          %span.counter-number= number_with_delimiter account.followers_count
diff --git a/app/views/accounts/followers.html.haml b/app/views/accounts/followers.html.haml
index fa5071f38..4b53aef0c 100644
--- a/app/views/accounts/followers.html.haml
+++ b/app/views/accounts/followers.html.haml
@@ -1,7 +1,7 @@
 - content_for :page_title do
   = t('accounts.people_who_follow', name: display_name(@account))
 
-= render partial: 'header'
+= render 'header', account: @account
 
 .accounts-grid
   - if @followers.empty?
diff --git a/app/views/accounts/following.html.haml b/app/views/accounts/following.html.haml
index 987dcba1f..4711997d9 100644
--- a/app/views/accounts/following.html.haml
+++ b/app/views/accounts/following.html.haml
@@ -1,7 +1,7 @@
 - content_for :page_title do
   = t('accounts.people_followed_by', name: display_name(@account))
 
-= render partial: 'header'
+= render 'header', account: @account
 
 .accounts-grid
   - if @following.empty?
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index fd7ff9653..9a70fd16f 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -20,7 +20,7 @@
 .h-feed
   %data.p-name{ value: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
 
-  = render partial: 'header'
+  = render 'header', account: @account
 
   - if @statuses.empty?
     .accounts-grid
diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml
index 6f4ba9b57..da9a07bbc 100644
--- a/app/views/admin/domain_blocks/index.html.haml
+++ b/app/views/admin/domain_blocks/index.html.haml
@@ -6,12 +6,19 @@
     %tr
       %th= t('admin.domain_block.domain')
       %th= t('admin.domain_block.severity')
+      %th= t('admin.domain_block.reject_media')
+      %th
   %tbody
     - @blocks.each do |block|
       %tr
         %td
           %samp= block.domain
-        %td= block.severity
+        %td= t("admin.domain_block.severities.#{block.severity}")
+        %td
+          - if block.reject_media? || block.suspend?
+            %i.fa.fa-check
+        %td
+          = table_link_to 'undo', t('admin.domain_block.undo'), admin_domain_block_path(block)
 
 = paginate @blocks
 = link_to t('admin.domain_block.add_new'), new_admin_domain_block_path, class: 'button'
diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml
index 53aab21ff..603faeb55 100644
--- a/app/views/admin/domain_blocks/new.html.haml
+++ b/app/views/admin/domain_blocks/new.html.haml
@@ -10,5 +10,8 @@
   = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("admin.domain_block.new.severity.#{type}") }
 
   %p.hint= t('admin.domain_block.new.severity.desc_html')
+
+  = f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_block.reject_media'), hint: I18n.t('admin.domain_block.reject_media_hint')
+
   .actions
     = f.button :button, t('admin.domain_block.new.create'), type: :submit
diff --git a/app/views/admin/domain_blocks/show.html.haml b/app/views/admin/domain_blocks/show.html.haml
new file mode 100644
index 000000000..bf9011c52
--- /dev/null
+++ b/app/views/admin/domain_blocks/show.html.haml
@@ -0,0 +1,9 @@
+- content_for :page_title do
+  = t('admin.domain_block.show.title', domain: @domain_block.domain)
+
+= simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f|
+
+  = f.input :retroactive, as: :boolean, wrapper: :with_label, label: I18n.t("admin.domain_block.show.retroactive.#{@domain_block.severity}"), hint: I18n.t('admin.domain_block.show.affected_accounts', count: Account.where(domain: @domain_block.domain).count)
+
+  .actions
+    = f.button :button, t('admin.domain_block.show.undo'), type: :submit
diff --git a/app/views/api/v1/statuses/_media.rabl b/app/views/api/v1/statuses/_media.rabl
index 2f56c6d07..80d80ea05 100644
--- a/app/views/api/v1/statuses/_media.rabl
+++ b/app/views/api/v1/statuses/_media.rabl
@@ -1,5 +1,5 @@
 attributes :id, :remote_url, :type
 
-node(:url)         { |media| full_asset_url(media.file.url(:original)) }
-node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
+node(:url)         { |media| media.file.blank? ? media.remote_url : full_asset_url(media.file.url(:original)) }
+node(:preview_url) { |media| media.file.blank? ? media.remote_url : full_asset_url(media.file.url(:small)) }
 node(:text_url)    { |media| media.local? ? medium_url(media) : nil }
diff --git a/app/views/settings/two_factor_auths/_recovery_codes.html.haml b/app/views/settings/two_factor_auths/_recovery_codes.html.haml
index 719a1e01b..054588b97 100644
--- a/app/views/settings/two_factor_auths/_recovery_codes.html.haml
+++ b/app/views/settings/two_factor_auths/_recovery_codes.html.haml
@@ -1,6 +1,6 @@
 %p.hint= t('two_factor_auth.recovery_instructions')
 
 %ol.recovery-codes
-  - @codes.each do |code|
+  - recovery_codes.each do |code|
     %li
       %samp= code
diff --git a/app/views/settings/two_factor_auths/create.html.haml b/app/views/settings/two_factor_auths/create.html.haml
index 8710b6e02..138a930fd 100644
--- a/app/views/settings/two_factor_auths/create.html.haml
+++ b/app/views/settings/two_factor_auths/create.html.haml
@@ -1,4 +1,4 @@
 - content_for :page_title do
   = t('settings.two_factor_auth')
 
-= render 'recovery_codes'
+= render partial: 'recovery_codes', object: @codes
diff --git a/app/views/settings/two_factor_auths/recovery_codes.html.haml b/app/views/settings/two_factor_auths/recovery_codes.html.haml
index 8710b6e02..138a930fd 100644
--- a/app/views/settings/two_factor_auths/recovery_codes.html.haml
+++ b/app/views/settings/two_factor_auths/recovery_codes.html.haml
@@ -1,4 +1,4 @@
 - content_for :page_title do
   = t('settings.two_factor_auth')
 
-= render 'recovery_codes'
+= render partial: 'recovery_codes', object: @codes
diff --git a/app/views/stream_entries/_og_description.html.haml b/app/views/stream_entries/_og_description.html.haml
new file mode 100644
index 000000000..5762aca04
--- /dev/null
+++ b/app/views/stream_entries/_og_description.html.haml
@@ -0,0 +1,4 @@
+- if activity.is_a?(Status) && activity.spoiler_text?
+  %meta{ property: 'og:description', content: activity.spoiler_text }/
+- else
+  %meta{ property: 'og:description', content: activity.content }/
diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml
new file mode 100644
index 000000000..f725209d8
--- /dev/null
+++ b/app/views/stream_entries/_og_image.html.haml
@@ -0,0 +1,6 @@
+- if activity.is_a?(Status) && activity.non_sensitive_with_media?
+  %meta{ property: 'og:image', content: full_asset_url(activity.media_attachments.first.file.url(:small)) }/
+- else
+  %meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/
+  %meta{ property: 'og:image:width', content: '120' }/
+  %meta{ property: 'og:image:height', content: '120' }/
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index eb8387ccb..dea5e9d40 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -6,17 +6,8 @@
   %meta{ property: 'og:type', content: 'article' }/
   %meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
 
-  - if @stream_entry.activity.is_a?(Status) && !@stream_entry.activity.spoiler_text.blank?
-    %meta{ property: 'og:description', content: @stream_entry.activity.spoiler_text }/
-  - else
-    %meta{ property: 'og:description', content: @stream_entry.activity.content }/
-
-  - if @stream_entry.activity.is_a?(Status) && !@stream_entry.activity.sensitive? && @stream_entry.activity.media_attachments.size > 0
-    %meta{ property: 'og:image', content: full_asset_url(@stream_entry.activity.media_attachments.first.file.url(:small)) }/
-  - else
-    %meta{ property: 'og:image', content: full_asset_url(@account.avatar.url(:original)) }/
-    %meta{ property: 'og:image:width', content: '120' }/
-    %meta{ property: 'og:image:height', content: '120' }/
+  = render 'stream_entries/og_description', activity: @stream_entry.activity
+  = render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account
 
   %meta{ property: 'twitter:card', content: 'summary' }/
 
diff --git a/bin/rspec b/bin/rspec
new file mode 100755
index 000000000..d738b23c0
--- /dev/null
+++ b/bin/rspec
@@ -0,0 +1,17 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+#
+# This file was generated by Bundler.
+#
+# The application 'rspec' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require "pathname"
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
+  Pathname.new(__FILE__).realpath)
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("rspec-core", "rspec")
diff --git a/config/deploy.rb b/config/deploy.rb
index 9bcf907be..b1cade49d 100644
--- a/config/deploy.rb
+++ b/config/deploy.rb
@@ -1,4 +1,7 @@
-lock '3.7.2'
+# frozen_string_literal: true
+
+lock '3.8.0'
+
 set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git')
 set :branch, ENV.fetch('BRANCH', 'master')
 
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 474de3985..325df5045 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -81,6 +81,8 @@ en:
       web: Web
     domain_block:
       add_new: Add new
+      created_msg: Domain block is now being processed
+      destroyed_msg: Domain block has been undone
       domain: Domain
       new:
         create: Create block
@@ -90,8 +92,22 @@ en:
           silence: Silence
           suspend: Suspend
         title: New domain block
+      reject_media: Reject media files
+      reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions
+      severities:
+        silence: Silence
+        suspend: Suspend
       severity: Severity
+      show:
+        affected_accounts:
+          one: One account in the database affected
+          other: "%{count} accounts in the database affected"
+        retroactive:
+          silence: Unsilence all existing accounts from this domain
+          suspend: Unsuspend all existing accounts from this domain
+        title: Undo domain block for %{domain}
       title: Domain Blocks
+      undo: Undo
     pubsubhubbub:
       callback_url: Callback URL
       confirmed: Confirmed
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 3c7b342e2..5483e63b5 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -71,6 +71,7 @@ ja:
       profile_url: プロフィールURL
       public: パブリック
       push_subscription_expires: PuSH購読期限切れ
+      reset_password: パスワード再設定
       salmon_url: Salmon URL
       silence: サイレンス
       statuses: トゥート数
@@ -81,6 +82,8 @@ ja:
       web: Web
     domain_block:
       add_new: 新規追加
+      created_msg: ドメインブロック処理を完了しました
+      destroyed_msg: ドメインブロックを外しました
       domain: ドメイン
       new:
         create: ブロックを作成
@@ -90,8 +93,21 @@ ja:
           silence: サイレンス
           suspend: 停止
         title: 新規ドメインブロック
+      reject_media: メディアファイルを拒否
+      reject_media_hint: ローカルに保村されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です。
+      severities:
+        silence: サイレンス
+        suspend: 停止
       severity: 深刻度
+      show:
+        affected_accounts: "データベース中の%{count}個のアカウントに影響します"
+        retroactive:
+          silence: このドメインからの存在するすべてのアカウントのサイレンスを戻す
+          suspend: このドメインからの存在するすべてのアカウントの停止を戻す
+        title: "%{domain}のドメインブロックを戻す"
+        undo: 元に戻す
       title: ドメインブロック
+      undo: 元に戻す
     pubsubhubbub:
       callback_url: コールバックURL
       confirmed: 確認済み
@@ -106,7 +122,7 @@ ja:
       delete: 削除
       id: ID
       mark_as_resolved: 解決済みとしてマーク
-      report: 'レポート#%{id}'
+      report: レポート#%{id}
       reported_account: 報告対象アカウント
       reported_by: 報告者
       resolved: 解決済み
@@ -290,8 +306,13 @@ ja:
     disable: 無効
     enable: 有効
     enabled_success: 二段階認証が有効になりました
+    generate_recovery_codes: 復元コードを生成
     instructions_html: "<strong>Google Authenticatorか、もしくはほかのTOTPアプリでこのQRコードをスキャンしてください。</strong>これ以降、ログインするときはそのアプリで生成されるコードが必要になります。"
+    lost_recovery_codes: リカバリコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリコードは無効になります。
     manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:'
+    recovery_codes: リカバリーコード
+    recovery_codes_regenerated: リカバリーコードが再生成されました。
+    recovery_instructions: 携帯電話を紛失した場合、以下の内どれかのリカバリコードを使用してアカウントへアクセスすることができます。 リカバリコードは印刷して安全に保管してください。
     setup: 初期設定
     warning: 現在認証アプリを設定できない場合、無効に設定して、有効にしないでください。
     wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index f2c7458f7..703c8467f 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -2,28 +2,97 @@
 pt:
   about:
     about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas.
+    about_this: Sobre essa instância
     get_started: Como começar
+    apps: Aplicações
+    business_email: 'Email comercial:'
+    closed_registrations: Registros estão fechadas para essa instância.
+    contact: Contato
+    description_headline: O que é %{domain}?
+    domain_count_after: outras instâncias
+    domain_count_before: Conectado a
+    features:
+      api: Aberto para API de aplicações e serviços
+      blocks: Bloqueos e ferramentas para mudar
+      characters: 500 caracteres por post
+      chronology: Timeline são cronologicas
+      ethics: 'Design ético: sem propaganda, sem tracking'
+      gifv: GIFV e vídeos curtos
+      privacy: Granular, privacidade setada por post
+      public: Timelines públicas
+    features_headline: O que torna Mastodon diferente
+    get_started: Comece aqui
+    links: Links
     source_code: Source code
+    other_instances: Outras instâncias
     terms: Termos
+    user_count_after: usuários
+    user_count_before: Lugar de
   accounts:
     follow: Seguir
     followers: Seguidores
-    following: Following
+    following: Seguindo
     nothing_here: Não há nada aqui!
     people_followed_by: Pessoas seguidas por %{name}
     people_who_follow: Pessoas que seguem %{name}
     posts: Posts
+    remote_follow: Acesso remoto
     unfollow: Unfollow
+  admin:
+    accounts:
+      are_you_sure: Você tem certeza?
+      display_name: Nome mostrado
+      domain: Domain
+      edit: Editar
+      email: E-mail
+      feed_url: URL do Feed
+      followers: Seguidores
+      follows: Seguindo
+      location:
+        all: Todos
+        local: Local
+        remote: Remoto
+        title: Local
+      media_attachments: Mídia anexadas
+      moderation:
+        all: Todos
+        silenced: Silenciado
+        suspended: Supenso
+        title: Moderação
+      most_recent_activity: Atividade mais recente
+      most_recent_ip: IP mais recente
+      not_subscribed: Não inscrito
+      order:
+        alphabetic: Alfabética
+        most_recent: Mais recente
+        title: Ordem
+      perform_full_suspension: Fazer suspensão completa
+      profile_url: URL do perfil
+      public: Público
+      push_subscription_expires: PuSH subscription expires
+      salmon_url: Salmon URL
+      silence: Silêncio
+      statuses: Status
+      title: Contas
+      undo_silenced: Desfazer silenciar
+      undo_suspension: Desfazer supensão
+      username: Usuário
+      web: Web
+    domain_block:
+      add_new: Adicionar nova
+      created_msg: Bloqueio do domínio está sendo processado
+      destroyed_msg: Bloqueio de domínio está sendo desfeito
+      domain: Domínio
   application_mailer:
     signature: notificações Mastodon de %{instance}
   auth:
-    change_password: Mudar password
+    change_password: Mudar senha
     didnt_get_confirmation: Não recebeu instruções de confirmação?
-    forgot_password: Esqueceu a password?
+    forgot_password: Esqueceu a senha?
     login: Entrar
     register: Registar
     resend_confirmation: Reenviar instruções de confirmação
-    reset_password: Reset password
+    reset_password: Resetar senha
     set_new_password: Editar password
   generic:
     changes_saved_msg: Mudanças guardadas!
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index 103001b7e..b9257ec35 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -10,11 +10,13 @@ ja:
         note: プロフィールは160文字まで設定することができます。
       imports:
         data: 他の Mastodon サーバーからエクスポートしたCSVファイルを選択して下さい
+      sessions:
+        otp: 携帯電話に表示された2段階認証コードを入力するか、生成したリカバリーコードを使用してください。
     labels:
       defaults:
         avatar: アイコン
         confirm_new_password: 新しいパスワード(確認用)
-        confirm_password: 新しいパスワード
+        confirm_password: パスワード(確認用)
         current_password: 現在のパスワード
         data: データ
         display_name: 表示名
@@ -22,12 +24,13 @@ ja:
         header: ヘッダー
         locale: 言語
         locked: 非公開アカウントにする
-        new_password: パスワード
+        new_password: 新しいパスワード
         note: プロフィール
         otp_attempt: 二段階認証コード
         password: パスワード
         setting_boost_modal: ブーストする前に確認ダイアログを表示する
         setting_default_privacy: 投稿の公開範囲
+        severity: 重大性
         type: インポートする項目
         username: ユーザー名
       interactions:
diff --git a/config/locales/simple_form.pt.yml b/config/locales/simple_form.pt.yml
index 07099c1f0..e8b5e2d7f 100644
--- a/config/locales/simple_form.pt.yml
+++ b/config/locales/simple_form.pt.yml
@@ -4,17 +4,17 @@ pt:
     labels:
       defaults:
         avatar: Avatar
-        confirm_new_password: Confirme nova password
-        confirm_password: Confirme a password
-        current_password: Password atual
+        confirm_new_password: Confirme nova senha
+        confirm_password: Confirme a senha
+        current_password: Senha atual
         display_name: Nome
         email: Endereço de email
         header: Header
         locale: Linguagem
-        new_password: Nova password
+        new_password: Nova senha
         note: Biografia
-        password: Password
-        username: Username
+        password: Senha
+        username: Usuário
       interactions:
         must_be_follower: Bloquear notificações de não-seguidores
         must_be_following: Bloquear notificações de pessoas que você
diff --git a/config/routes.rb b/config/routes.rb
index 31909a4f4..fd186c320 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -78,7 +78,7 @@ Rails.application.routes.draw do
 
   namespace :admin do
     resources :pubsubhubbub, only: [:index]
-    resources :domain_blocks, only: [:index, :new, :create]
+    resources :domain_blocks, only: [:index, :new, :create, :show, :destroy]
     resources :settings, only: [:index, :update]
 
     resources :reports, only: [:index, :show, :update] do
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 54980634d..b47730274 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -1,6 +1,16 @@
 # frozen_string_literal: true
 
 namespace :mastodon do
+  desc 'Execute daily tasks'
+  task :daily do
+    Rake::Task['mastodon:feeds:clear'].invoke
+    Rake::Task['mastodon:media:clear'].invoke
+    Rake::Task['mastodon:users:clear'].invoke
+
+    Rake::Task['mastodon:push:refresh'].invoke
+  end
+
+  desc 'Turn a user into an admin, identified by the USERNAME environment variable'
   task make_admin: :environment do
     include RoutingHelper
 
@@ -13,12 +23,13 @@ namespace :mastodon do
   desc 'Manually confirms a user with associated user email address stored in USER_EMAIL environment variable.'
   task confirm_email: :environment do
     email = ENV.fetch('USER_EMAIL')
-    user = User.where(email: email).first
+    user  = User.find_by(email: email)
+
     if user
       user.update(confirmed_at: Time.now.utc)
-      puts "User #{email} confirmed."
+      puts "#{email} confirmed"
     else
-      abort "User #{email} not found."
+      abort "#{email} not found"
     end
   end
 
@@ -32,6 +43,13 @@ namespace :mastodon do
     task remove_silenced: :environment do
       MediaAttachment.where(account: Account.silenced).find_each(&:destroy)
     end
+
+    desc 'Remove cached remote media attachments that are older than a week'
+    task remove_remote: :environment do
+      MediaAttachment.where.not(remote_url: '').where('created_at < ?', 1.week.ago).find_each do |media|
+        media.file.destroy
+      end
+    end
   end
 
   namespace :push do
@@ -60,7 +78,7 @@ namespace :mastodon do
       end
     end
 
-    desc 'Clears all timelines so that they would be regenerated on next hit'
+    desc 'Clears all timelines'
     task clear_all: :environment do
       Redis.current.keys('feed:*').each { |key| Redis.current.del(key) }
     end
@@ -126,8 +144,13 @@ namespace :mastodon do
       Rails.logger.debug 'Generating static avatars/headers for GIF ones...'
 
       Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account|
-        account.avatar.reprocess!
-        account.header.reprocess!
+        begin
+          account.avatar.reprocess!
+          account.header.reprocess!
+        rescue StandardError => e
+          Rails.logger.error "Error while generating static avatars/headers for account #{account.id}: #{e}"
+          next
+        end
       end
 
       Rails.logger.debug 'Done!'
diff --git a/package.json b/package.json
index 6f448238b..0ced631a9 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,6 @@
 {
   "name": "mastodon",
+  "license" : "AGPL-3.0",
   "scripts": {
     "start": "babel-node ./streaming/index.js --presets es2015,stage-2",
     "storybook": "start-storybook -p 9001 -c storybook",
diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb
index 5526d6780..acc0c39f5 100644
--- a/spec/views/stream_entries/show.html.haml_spec.rb
+++ b/spec/views/stream_entries/show.html.haml_spec.rb
@@ -61,5 +61,4 @@ describe 'stream_entries/show.html.haml' do
     expect(mf2.entry.in_reply_to.format.author.format.name.to_s).to eq alice.display_name
     expect(mf2.entry.in_reply_to.format.author.format.url.to_s).not_to be_empty
   end
-
 end