about summary refs log tree commit diff
path: root/config
diff options
context:
space:
mode:
Diffstat (limited to 'config')
-rw-r--r--config/environments/production.rb12
-rw-r--r--config/i18n-tasks.yml1
-rw-r--r--config/initializers/0_duplicate_migrations.rb52
-rw-r--r--config/initializers/content_security_policy.rb64
-rw-r--r--config/initializers/cors.rb4
-rw-r--r--config/initializers/locale.rb7
-rw-r--r--config/locales-glitch/en.yml58
-rw-r--r--config/locales-glitch/es.yml23
-rw-r--r--config/locales-glitch/ja.yml23
-rw-r--r--config/locales-glitch/ko.yml50
-rw-r--r--config/locales-glitch/pl.yml6
-rw-r--r--config/locales-glitch/simple_form.en.yml24
-rw-r--r--config/locales-glitch/simple_form.es.yml20
-rw-r--r--config/locales-glitch/simple_form.ja.yml20
-rw-r--r--config/locales-glitch/simple_form.ko.yml24
-rw-r--r--config/locales-glitch/simple_form.pl.yml10
-rw-r--r--config/locales-glitch/simple_form.zh-CN.yml20
-rw-r--r--config/locales-glitch/zh-CN.yml23
-rw-r--r--config/navigation.rb6
-rw-r--r--config/routes.rb28
-rw-r--r--config/settings.yml20
-rw-r--r--config/themes.yml3
-rw-r--r--config/webpack/configuration.js54
-rw-r--r--config/webpack/generateLocalePacks.js94
-rw-r--r--config/webpack/rules/babel.js1
-rw-r--r--config/webpack/rules/css.js3
-rw-r--r--config/webpack/shared.js76
-rw-r--r--config/webpacker.yml1
28 files changed, 612 insertions, 115 deletions
diff --git a/config/environments/production.rb b/config/environments/production.rb
index f41a0f197..dd73bb4e1 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -118,11 +118,13 @@ Rails.application.configure do
   config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym
 
   config.action_dispatch.default_headers = {
-    'Server'                 => 'Mastodon',
-    'X-Frame-Options'        => 'DENY',
-    'X-Content-Type-Options' => 'nosniff',
-    'X-XSS-Protection'       => '0',
-    'Permissions-Policy'     => 'interest-cohort=()',
+    'Server'                  => 'Mastodon',
+    'X-Frame-Options'         => 'DENY',
+    'X-Content-Type-Options'  => 'nosniff',
+    'X-XSS-Protection'        => '0',
+    'Permissions-Policy'      => 'interest-cohort=()',
+    'Referrer-Policy'         => 'same-origin',
+    'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
   }
 
   config.x.otp_secret = ENV.fetch('OTP_SECRET')
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 1bebae5e9..cc607f72a 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -59,6 +59,7 @@ ignore_unused:
   - 'errors.429'
   - 'admin.accounts.roles.*'
   - 'admin.action_logs.actions.*'
+  - 'themes.*'
   - 'admin_mailer.new_appeal.actions.*'
   - 'statuses.attached.*'
   - 'move_handler.carry_{mutes,blocks}_over_text'
diff --git a/config/initializers/0_duplicate_migrations.rb b/config/initializers/0_duplicate_migrations.rb
new file mode 100644
index 000000000..6c45e4bd2
--- /dev/null
+++ b/config/initializers/0_duplicate_migrations.rb
@@ -0,0 +1,52 @@
+# Some migrations have been present in glitch-soc for a long time and have then
+# been merged in upstream Mastodon, under a different version number.
+#
+# This puts us in an uneasy situation in which if we remove upstream's
+# migration file, people migrating from upstream will end up having a conflict
+# with their already-ran migration.
+#
+# On the other hand, if we keep upstream's migration and remove our own,
+# any current glitch-soc user will have a conflict during migration.
+#
+# For lack of a better solution, as those migrations are indeed identical,
+# we decided monkey-patching Rails' Migrator to completely ignore the duplicate,
+# keeping only the one that has run, or an arbitrary one.
+
+ALLOWED_DUPLICATES = [20180410220657, 20180831171112].freeze
+
+module ActiveRecord
+  class Migrator
+    def self.new(direction, migrations, schema_migration, target_version = nil)
+      migrated = Set.new(Base.connection.migration_context.get_all_versions)
+
+      migrations.group_by(&:name).each do |name, duplicates|
+        if duplicates.length > 1 && duplicates.all? { |m| ALLOWED_DUPLICATES.include?(m.version) }
+          # We have a set of allowed duplicates. Keep the migrated one, if any.
+          non_migrated = duplicates.reject { |m| migrated.include?(m.version.to_i) }
+
+          if duplicates.length == non_migrated.length || non_migrated.length == 0
+            # There weren't any migrated one, so we have to pick one “canonical” migration
+            migrations = migrations - duplicates[1..-1]
+          else
+            # Just reject every duplicate which hasn't been migrated yet
+            migrations = migrations - non_migrated
+          end
+        end
+      end
+
+      super(direction, migrations, schema_migration, target_version)
+    end
+  end
+
+  class MigrationContext
+    def needs_migration?
+      # A set of duplicated migrations is considered migrated if at least one of
+      # them is migrated.
+      migrated = get_all_versions
+      migrations.group_by(&:name).each do |name, duplicates|
+        return true unless duplicates.any? { |m| migrated.include?(m.version.to_i) }
+      end
+      return false
+    end
+  end
+end
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index c113b0f8b..21f782b9c 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -2,43 +2,45 @@
 # For further information see the following documentation
 # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
 
-def host_to_url(str)
-  "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}" unless str.blank?
-end
+if Rails.env.production?
+  assets_host = Rails.configuration.action_controller.asset_host || "https://#{ENV['WEB_DOMAIN'] || ENV['LOCAL_DOMAIN']}"
+  data_hosts = [assets_host]
 
-base_host = Rails.configuration.x.web_domain
+  if ENV['S3_ENABLED'] == 'true'
+    attachments_host = "https://#{ENV['S3_ALIAS_HOST'] || ENV['S3_CLOUDFRONT_HOST'] || ENV['S3_HOSTNAME'] || "s3-#{ENV['S3_REGION'] || 'us-east-1'}.amazonaws.com"}"
+    attachments_host = "https://#{Addressable::URI.parse(attachments_host).host}"
+  elsif ENV['SWIFT_ENABLED'] == 'true'
+    attachments_host = ENV['SWIFT_OBJECT_URL']
+    attachments_host = "https://#{Addressable::URI.parse(attachments_host).host}"
+  else
+    attachments_host = nil
+  end
 
-assets_host   = Rails.configuration.action_controller.asset_host
-assets_host ||= host_to_url(base_host)
+  data_hosts << attachments_host unless attachments_host.nil?
 
-media_host   = host_to_url(ENV['S3_ALIAS_HOST'])
-media_host ||= host_to_url(ENV['S3_CLOUDFRONT_HOST'])
-media_host ||= host_to_url(ENV['S3_HOSTNAME']) if ENV['S3_ENABLED'] == 'true'
-media_host ||= assets_host
+  if ENV['PAPERCLIP_ROOT_URL']
+    url = Addressable::URI.parse(assets_host) + ENV['PAPERCLIP_ROOT_URL']
+    data_hosts << "https://#{url.host}"
+  end
 
-Rails.application.config.content_security_policy do |p|
-  p.base_uri        :none
-  p.default_src     :none
-  p.frame_ancestors :none
-  p.font_src        :self, assets_host
-  p.img_src         :self, :https, :data, :blob, assets_host
-  p.style_src       :self, assets_host
-  p.media_src       :self, :https, :data, assets_host
-  p.frame_src       :self, :https
-  p.manifest_src    :self, assets_host
+  data_hosts.concat(ENV['EXTRA_DATA_HOSTS'].split('|')) if ENV['EXTRA_DATA_HOSTS']
 
-  if Rails.env.development?
-    webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" }
+  data_hosts.uniq!
 
-    p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls
-    p.script_src  :self, :unsafe_inline, :unsafe_eval, assets_host
-    p.child_src   :self, :blob, assets_host
-    p.worker_src  :self, :blob, assets_host
-  else
-    p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url
-    p.script_src  :self, assets_host
-    p.child_src   :self, :blob, assets_host
-    p.worker_src  :self, :blob, assets_host
+  Rails.application.config.content_security_policy do |p|
+    p.base_uri        :none
+    p.default_src     :none
+    p.frame_ancestors :none
+    p.script_src      :self, assets_host
+    p.font_src        :self, assets_host
+    p.img_src         :self, :data, :blob, *data_hosts
+    p.style_src       :self, assets_host
+    p.media_src       :self, :data, *data_hosts
+    p.frame_src       :self, :https
+    p.child_src       :self, :blob, assets_host
+    p.worker_src      :self, :blob, assets_host
+    p.connect_src     :self, :blob, :data, Rails.configuration.x.streaming_api_base_url, *data_hosts
+    p.manifest_src    :self, assets_host
   end
 end
 
diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb
index 55f8c9c91..bc782bc76 100644
--- a/config/initializers/cors.rb
+++ b/config/initializers/cors.rb
@@ -30,5 +30,9 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
       headers: :any,
       methods: [:post],
       credentials: false
+    resource '/assets/*', headers: :any, methods: [:get, :head, :options]
+    resource '/stylesheets/*', headers: :any, methods: [:get, :head, :options]
+    resource '/javascripts/*', headers: :any, methods: [:get, :head, :options]
+    resource '/packs/*', headers: :any, methods: [:get, :head, :options]
   end
 end
diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb
new file mode 100644
index 000000000..fed182a71
--- /dev/null
+++ b/config/initializers/locale.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+I18n.load_path += Dir[Rails.root.join('app', 'javascript', 'flavours', '*', 'names.{rb,yml}').to_s]
+I18n.load_path += Dir[Rails.root.join('app', 'javascript', 'flavours', '*', 'names', '*.{rb,yml}').to_s]
+I18n.load_path += Dir[Rails.root.join('app', 'javascript', 'skins', '*', '*', 'names.{rb,yml}').to_s]
+I18n.load_path += Dir[Rails.root.join('app', 'javascript', 'skins', '*', '*', 'names', '*.{rb,yml}').to_s]
+I18n.load_path += Dir[Rails.root.join('config', 'locales-glitch', '*.{rb,yml}').to_s]
diff --git a/config/locales-glitch/en.yml b/config/locales-glitch/en.yml
new file mode 100644
index 000000000..4cdc81a24
--- /dev/null
+++ b/config/locales-glitch/en.yml
@@ -0,0 +1,58 @@
+---
+en:
+  admin:
+    custom_emojis:
+      batch_copy_error: 'An error occurred when copying some of the selected emoji: %{message}'
+      batch_error: 'An error occurred: %{message}'
+    domain_allows:
+      export: Export
+      import: Import
+    domain_blocks:
+      export: Export
+      import: Import
+    export_domain_allows:
+      new:
+        title: Import domain allows
+      no_file: No file selected
+    export_domain_blocks:
+      import:
+        description_html: You are about to import a list of domain blocks. Please review this list very carefully, especially if you have not authored this list yourself.
+        existing_relationships_warning: Existing follow relationships
+        private_comment_description_html: 'To help you track where imported blocks come from, imported blocks will be created with the following private comment: <q>%{comment}</q>'
+        private_comment_template: Imported from %{source} on %{date}
+        title: Import domain blocks
+      new:
+        title: Import domain blocks
+      no_file: No file selected
+    settings:
+      captcha_enabled:
+        desc_html: This relies on external scripts from hCaptcha, which may be a security and privacy concern. In addition, <strong>this can make the registration process significantly less accessible to some (especially disabled) people</strong>. For these reasons, please consider alternative measures such as approval-based or invite-based registration.<br>Users that have been invited through a limited-use invite will not need to solve a CAPTCHA
+        title: Require new users to solve a CAPTCHA to confirm their account
+      enable_keybase:
+        desc_html: Allow your users to prove their identity via keybase
+        title: Enable keybase integration
+      flavour_and_skin:
+        title: Flavour and skin
+      outgoing_spoilers:
+        desc_html: When federating toots, add this content warning to toots that do not have one. It is useful if your server is specialized in content other servers might want to have under a Content Warning. Media will also be marked as sensitive.
+        title: Content warning for outgoing toots
+      hide_followers_count:
+        desc_html: Do not show followers count on user profiles
+        title: Hide followers count
+      show_reblogs_in_public_timelines:
+        desc_html: Show public boosts of public toots in local and public timelines.
+        title: Show boosts in public timelines
+      show_replies_in_public_timelines:
+        desc_html: In addition to public self-replies (threads), show public replies in local and public timelines.
+        title: Show replies in public timelines
+      trending_status_cw:
+        desc_html: When trending posts are enabled, allow posts with Content Warnings to be eligible. Changes to this setting are not retroactive.
+        title: Allow posts with Content Warnings to trend
+  auth:
+    captcha_confirmation:
+      hint_html: Just one more step! To confirm your account, this server requires you to solve a CAPTCHA. You can <a href="/about/more">contact the server administrator</a> if you have questions or need assistance with confirming your account.
+      title: User verification
+  generic:
+    use_this: Use this
+  settings:
+    flavours: Flavours
diff --git a/config/locales-glitch/es.yml b/config/locales-glitch/es.yml
new file mode 100644
index 000000000..ad942a0c0
--- /dev/null
+++ b/config/locales-glitch/es.yml
@@ -0,0 +1,23 @@
+---
+es:
+  admin:
+    settings:
+      enable_keybase:
+        desc_html: Permite a tus usuarixs comprobar su identidad por medio de keybase
+        title: Habilitar la integración con keybase
+      outgoing_spoilers:
+        desc_html: Cuando los toots federen, agrega esta etiqueta de contenido a los toots que no tengan. Es útil si tu servidor se especializa en contenido que otros servidores desearían tener con una advertencia de contenido. Los medios también se marcarán como sensibles.
+        title: Advertencia de contenido para los toots salientes
+      hide_followers_count:
+        desc_html: No mostrar el conteo de seguidorxs en perfiles de usuarix
+        title: Ocultar conteo de seguidorxs
+      show_reblogs_in_public_timelines:
+        desc_html: Mostrar retoots públicos en las línea de tiempo local y pública.
+        title: Mostrar retoots en líneas de tiempo públicas
+      show_replies_in_public_timelines:
+        desc_html: Además de auto-respuestas públicas (hilos), mostrar respuestas públicas en las línea de tiempo local y pública.
+        title: Mostrar respuestas en líneas de tiempo públicas
+  generic:
+    use_this: Usar
+  settings:
+    flavours: Ediciones
diff --git a/config/locales-glitch/ja.yml b/config/locales-glitch/ja.yml
new file mode 100644
index 000000000..3ecb46ccd
--- /dev/null
+++ b/config/locales-glitch/ja.yml
@@ -0,0 +1,23 @@
+---
+ja:
+  admin:
+    settings:
+      enable_keybase:
+        desc_html: Keybaseにより身元の証明が可能となります
+        title: Keybase統合を有効にする
+      outgoing_spoilers:
+        desc_html: トゥートが連合される際、閲覧注意としてマークされていないトゥートにこの警告が追加されます。これはあなたのインスタンスが他のインスタンスに警告をして欲しいとされる投稿に特化している場合に便利です。 メディアは閲覧注意にマークされます。
+        title: 発信するトゥートへの警告
+      hide_followers_count:
+        desc_html: プロフィールページのフォロワー数を見られないようにします
+        title: フォロワー数を隠す
+      show_reblogs_in_public_timelines:
+        desc_html: ローカルタイムラインと連合タイムラインに公開投稿のブーストを表示します
+        title: 公開タイムラインにブーストを表示
+      show_replies_in_public_timelines:
+        desc_html: 自分への公開投稿の返信に加えて、すべての公開投稿の返信をローカルタイムラインと連合タイムラインに表示します。
+        title: 公開タイムラインに返信を表示
+  generic:
+    use_this: これを使う
+  settings:
+    flavours: フレーバー
diff --git a/config/locales-glitch/ko.yml b/config/locales-glitch/ko.yml
new file mode 100644
index 000000000..0e6890db7
--- /dev/null
+++ b/config/locales-glitch/ko.yml
@@ -0,0 +1,50 @@
+---
+ko:
+  admin:
+    custom_emojis:
+      batch_copy_error: '선택된 에모지를 복사하던 중 오류가 발생했습니다: %{message}'
+      batch_error: '에러가 발생했습니다: %{message}'
+    domain_allows:
+      export: 내보내기
+      import: 불러오기
+    domain_blocks:
+      export: 내보내기
+      import: 불러오기
+    export_domain_allows:
+      new:
+        title: 도메인 허용 불러오기
+      no_file: 파일이 선택되지 않았습니다
+    export_domain_blocks:
+      import:
+        description_html: 도메인 차단 목록을 불러오려고 합니다. 이 목록을 조심스럽게 검토하세요, 특히 자신이 직접 작성한 목록이 아니라면 더 조심하세요.
+        existing_relationships_warning: 팔로우 관계가 존재합니다
+        private_comment_description_html: '어디서 불러온 것인지 추적을 원활하게 하기 위해서, 불러온 차단들은 다음과 같은 비공개 주석과 함께 생성될 것입니다: <q>%{comment}</q>'
+        private_comment_template: '%{date}에 %{source}에서 불러옴'
+        title: 도메인 차단 불러오기
+      new:
+        title: 도메인 차단 불러오기
+      no_file: 파일이 선택되지 않았습니다
+    settings:
+      captcha_enabled:
+        desc_html: 이것은 hCaptcha의 외부 스크립트에 의존합니다, 이것은 개인정보 보호에 위협을 가할 수도 있습니다. 추가적으로, <strong>이것은 몇몇 사람들(특히나 장애인들)에게 가입 절차의 접근성을 심각하게 떨어트릴 수 있습니다</strong>. 이러한 이유로, 대체제로 승인 전용이나 초대제를 통한 가입을 고려해보세요.<br>한정된 사용만 가능한 초대장을 통한 가입자들은 CAPTCHA를 풀지 않아도 됩니다
+        title: 새로운 사용자가 계정 확인을 위해서는 CAPTCHA를 풀어야 하도록 합니다
+      hide_followers_count:
+        desc_html: 사용자 프로필에 팔로워 수를 표시하지 않습니다
+        title: 팔로워 수 숨기기
+      show_reblogs_in_public_timelines:
+        desc_html: 공개글의 공개적인 부스트를 로컬과 공개 타임라인에 표시합니다.
+        title: 부스트를 공개 타임라인에 표시
+      show_replies_in_public_timelines:
+        desc_html: 자기자신에 대한 답글(글타래)와 마찬가지로, 공개적인 답글을 로컬과 공개 타임라인에 표시합니다.
+        title: 답글을 공개 타임라인에 표시
+      trending_status_cw:
+        desc_html: 유행하는 게시물이 활성화 되었을 때, 열람주의가 설정된 글도 허용합니다. 이 설정의 변경은 소급 적용되지 않습니다.
+        title: 열람주의를 가진 글이 유행에 오를 수 있도록 허용
+  auth:
+    captcha_confirmation:
+      hint_html: 한가지 단계가 남았습니다! 계정을 확인하기 위해서는, CAPTCHA를 풀어야 합니다. 질문이 있거나 계정 확인 과정에서 도움을 받고 싶은 경우 <a href="/about/more">서버의 관리자에게 연락해서</a> 해결할 수 있습니다.
+      title: 사용자 확인
+  generic:
+    use_this: 사용하기
+  settings:
+    flavours: 풍미
diff --git a/config/locales-glitch/pl.yml b/config/locales-glitch/pl.yml
new file mode 100644
index 000000000..3fcdedcf3
--- /dev/null
+++ b/config/locales-glitch/pl.yml
@@ -0,0 +1,6 @@
+---
+pl:
+  generic:
+    use_this: Użyj tego
+  settings:
+    flavours: Odmiany
diff --git a/config/locales-glitch/simple_form.en.yml b/config/locales-glitch/simple_form.en.yml
new file mode 100644
index 000000000..c9ef40996
--- /dev/null
+++ b/config/locales-glitch/simple_form.en.yml
@@ -0,0 +1,24 @@
+---
+en:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: When writing toots, assume they are written in raw HTML, unless specified otherwise
+        setting_default_content_type_markdown: When writing toots, assume they are using Markdown for rich text formatting, unless specified otherwise
+        setting_default_content_type_plain: When writing toots, assume they are plain text with no special formatting, unless specified otherwise (default Mastodon behavior)
+        setting_default_language: The language of your toots can be detected automatically, but it's not always accurate
+        setting_skin: Reskins the selected Mastodon flavour
+    labels:
+      defaults:
+        setting_default_content_type: Default format for toots
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: Markdown
+        setting_default_content_type_plain: Plain text
+        setting_favourite_modal: Show confirmation dialog before favouriting (applies to Glitch flavour only)
+        setting_hide_followers_count: Hide your followers count
+        setting_skin: Skin
+        setting_system_emoji_font: Use system's default font for emojis (applies to Glitch flavour only)
+      notification_emails:
+        trending_tag: New trending tag requires review
+        trending_link: New trending link requires review
+        trending_status: New trending post requires review
diff --git a/config/locales-glitch/simple_form.es.yml b/config/locales-glitch/simple_form.es.yml
new file mode 100644
index 000000000..977775be6
--- /dev/null
+++ b/config/locales-glitch/simple_form.es.yml
@@ -0,0 +1,20 @@
+---
+es:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: Al escribir toots, asume que estás escritos en HTML, a menos que se especifique lo contrario
+        setting_default_content_type_markdown: Al escribir toots, asume que estás usando Markdown para dar formato de texto enriquecido, a menos que se especifique lo contrario
+        setting_default_content_type_plain: Al escribir toots, asume que estás usando texto sin formato, a menos que se especifique lo contrario (predeterminado de Mastodon)
+        setting_default_language: El idioma de tus toots se puede detectar automáticamente, pero no siempre es correcto
+        setting_skin: Cambia el diseño de la edición seleccionada de Mastodon
+    labels:
+      defaults:
+        setting_default_content_type: Formato predeterminado de tus toots
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: Markdown
+        setting_default_content_type_plain: Sin formato
+        setting_favourite_modal: Mostrar diálogo de confirmación antes de marcar como favorito (sólo aplica a la edición Glich)
+        setting_hide_followers_count: Ocultar tu conteo de seguidorxs
+        setting_skin: Diseño
+        setting_system_emoji_font: Usar la fuente predeterminada del sistema para emojis (sólo aplica a la edición Glitch)
diff --git a/config/locales-glitch/simple_form.ja.yml b/config/locales-glitch/simple_form.ja.yml
new file mode 100644
index 000000000..dc9e0b579
--- /dev/null
+++ b/config/locales-glitch/simple_form.ja.yml
@@ -0,0 +1,20 @@
+---
+ja:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: トゥートを作成するとき特に指定がない限り生のHTMLで書かれているとします
+        setting_default_content_type_markdown: トゥートを作成するとき特に指定がない限りリッチテキスト形式のマークダウンで書かれているとします
+        setting_default_content_type_plain: トゥートを作成するとき特に指定がない限りプレーンテキストで書かれているとします(Mastodon既定の動作)
+        setting_default_language: あなたのトゥートの言語を自動検出しますが必ずしも正確ではありません
+        setting_skin: 選択したMastodonフレーバーに変更します
+    labels:
+      defaults:
+        setting_default_content_type: 既定のトゥート形式
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: マークダウン
+        setting_default_content_type_plain: プレーンテキスト
+        setting_favourite_modal: お気に入りをする前に確認ダイアログを表示する
+        setting_hide_followers_count: フォロワー数を隠す
+        setting_skin: スキン
+        setting_system_emoji_font: 絵文字にシステム既定のフォントを使用する(Glitch Edition フレーバーのみに適用されます)
diff --git a/config/locales-glitch/simple_form.ko.yml b/config/locales-glitch/simple_form.ko.yml
new file mode 100644
index 000000000..f5575ed4c
--- /dev/null
+++ b/config/locales-glitch/simple_form.ko.yml
@@ -0,0 +1,24 @@
+---
+ko:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: 게시물을 작성할 때, 형식을 지정하지 않았다면, 생 HTML이라고 가정합니다
+        setting_default_content_type_markdown: 게시물을 작성할 때, 형식을 지정하지 않았다면, 마크다운이라고 가정합니다
+        setting_default_content_type_plain: 게시물을 작성할 때, 형식을 지정하지 않았다면, 일반적인 텍스트라고 가정합니다. (마스토돈의 기본 동작)
+        setting_default_language: 작성하는 게시물의 언어는 자동으로 설정될 수 있습니다, 하지만 언제나 정확하지는 않습니다
+        setting_skin: 선택한 마스토돈 풍미의 스킨을 바꿉니다
+    labels:
+      defaults:
+        setting_default_content_type: 게시물의 기본 포맷
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: 마크다운
+        setting_default_content_type_plain: 일반 텍스트
+        setting_favourite_modal: 관심글을 지정할 때 확인 창을 띄웁니다(글리치 풍미에만 적용됨)
+        setting_hide_followers_count: 내 팔로워 수 숨기기
+        setting_skin: 스킨
+        setting_system_emoji_font: 에모지에 시스템 기본 폰트 적용하기 (글리치 풍미에만 적용됨)
+      notification_emails:
+        trending_tag: 새로 유행중인 해시태그에 대한 리뷰가 필요할 때
+        trending_link: 새로 유행중인 링크에 대한 리뷰가 필요할 때
+        trending_status: 새로 유행중인 게시물에 대한 리뷰가 필요할 때
diff --git a/config/locales-glitch/simple_form.pl.yml b/config/locales-glitch/simple_form.pl.yml
new file mode 100644
index 000000000..264494c2d
--- /dev/null
+++ b/config/locales-glitch/simple_form.pl.yml
@@ -0,0 +1,10 @@
+---
+pl:
+  simple_form:
+    hints:
+      defaults:
+        setting_skin: Zmienia wygląd używanej odmiany Mastodona
+    labels:
+      defaults:
+        setting_favourite_modal: Pytaj o potwierdzenie przed dodaniem do ulubionych
+        setting_skin: Motyw
diff --git a/config/locales-glitch/simple_form.zh-CN.yml b/config/locales-glitch/simple_form.zh-CN.yml
new file mode 100644
index 000000000..a82617fdb
--- /dev/null
+++ b/config/locales-glitch/simple_form.zh-CN.yml
@@ -0,0 +1,20 @@
+---
+zh-CN:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: 在撰写嘟文时,除非另有指定,假定它们使用原始 HTML 语言撰写
+        setting_default_content_type_markdown: 在撰写嘟文时,除非另有指定,假定它们使用 Markdown 进行富文本格式化
+        setting_default_content_type_plain: 在撰写嘟文时,除非另有指定,假定它们是没有特殊格式的纯文本(默认的 Mastodon 行为)
+        setting_default_language: 你的嘟文语言可以自动检测,但不一定准确
+        setting_skin: 更换为所选择的 Mastodon 风味
+    labels:
+      defaults:
+        setting_default_content_type: 嘟文的默认格式
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: Markdown
+        setting_default_content_type_plain: 纯文本
+        setting_favourite_modal: 在喜欢嘟文前询问我 (仅限于 Glitch 风味)
+        setting_hide_followers_count: 隐藏你的关注者人数
+        setting_skin: 皮肤
+        setting_system_emoji_font: 表情符号使用系统默认字体 (仅限于 Glitch 风味)
\ No newline at end of file
diff --git a/config/locales-glitch/zh-CN.yml b/config/locales-glitch/zh-CN.yml
new file mode 100644
index 000000000..37a8307e8
--- /dev/null
+++ b/config/locales-glitch/zh-CN.yml
@@ -0,0 +1,23 @@
+---
+zh-CN:
+  admin:
+    settings:
+      enable_keybase:
+        desc_html: 允许你的用户使用 Keybase 证明身份
+        title: 启用 Keybase 集成
+      outgoing_spoilers:
+        desc_html: 在联邦化嘟文的时候,将这个内容警告添加到没有内容警告的嘟文中。如果你的服务器专用于其他服务器可能希望有内容警告的内容,它会很有用。媒体也将被标记为敏感。
+        title: 对外嘟文的内容警告
+      hide_followers_count:
+        desc_html: 不要在用户资料中显示关注者人数
+        title: 隐藏关注者人数
+      show_reblogs_in_public_timelines:
+        desc_html: 在本地和跨站时间线中显示公开嘟文的公开转嘟。
+        title: 在公共时间线中显示转嘟
+      show_replies_in_public_timelines:
+        desc_html: 除了公开的自我回复(线程模式),在本地和跨站时间轴中显示公开回复。
+        title: 在公共时间轴中显示回复
+  generic:
+    use_this: 使用这个
+  settings:
+    flavours: 风味
diff --git a/config/navigation.rb b/config/navigation.rb
index 706de0471..7a1aee078 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -15,6 +15,12 @@ SimpleNavigation::Configuration.run do |navigation|
       s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_path
     end
 
+    n.item :flavours, safe_join([fa_icon('paint-brush fw'), t('settings.flavours')]), settings_flavours_path do |flavours|
+      Themes.instance.flavours.each do |flavour|
+        flavours.item flavour.to_sym, safe_join([fa_icon('star fw'), t("flavours.#{flavour}.name", default: flavour)]), settings_flavour_path(flavour)
+      end
+    end
+
     n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_path, if: -> { current_user.functional? }
     n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? }
     n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional? }
diff --git a/config/routes.rb b/config/routes.rb
index 5d0b3004b..7330bb8df 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -44,6 +44,7 @@ Rails.application.routes.draw do
       resource :setup, only: [:show, :update], controller: :setup
       resource :challenge, only: [:create], controller: :challenges
       get 'sessions/security_key_options', to: 'sessions#webauthn_options'
+      post 'captcha_confirmation', to: 'confirmations#confirm_captcha', as: :captcha_confirmation
     end
   end
 
@@ -151,6 +152,8 @@ Rails.application.routes.draw do
       end
     end
 
+    resources :flavours, only: [:index, :show, :update], param: :flavour
+
     resource :delete, only: [:show, :destroy]
     resource :migration, only: [:show, :create]
 
@@ -198,7 +201,25 @@ Rails.application.routes.draw do
     get '/dashboard', to: 'dashboard#index'
 
     resources :domain_allows, only: [:new, :create, :show, :destroy]
-    resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit]
+    resources :domain_blocks, only: [:new, :create, :show, :destroy, :update, :edit] do
+      collection do
+        post :batch
+      end
+    end
+
+    resources :export_domain_allows, only: [:new] do
+      collection do
+        get :export, constraints: { format: :csv }
+        post :import
+      end
+    end
+
+    resources :export_domain_blocks, only: [:new] do
+      collection do
+        get :export, constraints: { format: :csv }
+        post :import
+      end
+    end
 
     resources :email_domain_blocks, only: [:index, :new, :create] do
       collection do
@@ -403,6 +424,7 @@ Rails.application.routes.draw do
       end
 
       namespace :timelines do
+        resource :direct, only: :show, controller: :direct
         resource :home, only: :show, controller: :home
         resource :public, only: :show, controller: :public
         resources :tag, only: :show
@@ -490,6 +512,7 @@ Rails.application.routes.draw do
       end
 
       resource :domain_blocks, only: [:show, :create, :destroy]
+
       resource :directory, only: [:show]
 
       resources :follow_requests, only: [:index] do
@@ -499,9 +522,10 @@ Rails.application.routes.draw do
         end
       end
 
-      resources :notifications, only: [:index, :show] do
+      resources :notifications, only: [:index, :show, :destroy] do
         collection do
           post :clear
+          delete :destroy_multiple
         end
 
         member do
diff --git a/config/settings.yml b/config/settings.yml
index 41742118b..16f870b33 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -2,7 +2,7 @@
 # important settings can be changed from the admin interface.
 
 defaults: &defaults
-  site_title: Mastodon
+  site_title: 'Mastodon Glitch Edition'
   site_short_description: ''
   site_description: ''
   site_extended_description: ''
@@ -13,12 +13,13 @@ defaults: &defaults
   profile_directory: true
   closed_registrations_message: ''
   open_deletion: true
+  timeline_preview: false
   min_invite_role: 'admin'
-  timeline_preview: true
   show_staff_badge: true
   default_sensitive: false
   unfollow_modal: false
   boost_modal: false
+  favourite_modal: false
   delete_modal: true
   auto_play_gif: false
   display_media: 'default'
@@ -26,16 +27,20 @@ defaults: &defaults
   preview_sensitive_media: false
   reduce_motion: false
   disable_swiping: false
-  show_application: true
+  show_application: false
   system_font_ui: false
+  system_emoji_font: false
   noindex: false
-  theme: 'default'
+  hide_followers_count: false
+  flavour: 'glitch'
+  skin: 'default'
   aggregate_reblogs: true
   advanced_layout: false
   use_blurhash: true
   use_pending_items: false
   trends: true
   trendable_by_default: false
+  trending_status_cw: true
   crop_images: true
   notification_emails:
     follow: true
@@ -47,6 +52,8 @@ defaults: &defaults
     report: true
     pending_account: true
     trending_tag: true
+    trending_link: false
+    trending_status: false
     appeal: true
   always_send_emails: false
   interactions:
@@ -67,9 +74,14 @@ defaults: &defaults
   activity_api_enabled: true
   peers_api_enabled: true
   show_known_fediverse_at_about_page: true
+  show_reblogs_in_public_timelines: false
+  show_replies_in_public_timelines: false
+  default_content_type: 'text/plain'
   show_domain_blocks: 'disabled'
   show_domain_blocks_rationale: 'disabled'
+  outgoing_spoilers: ''
   require_invite_text: false
+  captcha_enabled: false
   backups_retention_period: 7
 
 development:
diff --git a/config/themes.yml b/config/themes.yml
deleted file mode 100644
index 9c21c9459..000000000
--- a/config/themes.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-default: styles/application.scss
-contrast: styles/contrast.scss
-mastodon-light: styles/mastodon-light.scss
diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js
index 25b6b7abd..55ee06c0c 100644
--- a/config/webpack/configuration.js
+++ b/config/webpack/configuration.js
@@ -1,15 +1,58 @@
 // Common configuration for webpacker loaded from config/webpacker.yml
 
-const { resolve } = require('path');
+const { basename, dirname, extname, join, resolve } = require('path');
 const { env } = require('process');
 const { load } = require('js-yaml');
-const { readFileSync } = require('fs');
+const { lstatSync, readFileSync } = require('fs');
+const glob = require('glob');
 
 const configPath = resolve('config', 'webpacker.yml');
 const settings = load(readFileSync(configPath), 'utf8')[env.RAILS_ENV || env.NODE_ENV];
+const flavourFiles = glob.sync('app/javascript/flavours/*/theme.yml');
+const skinFiles = glob.sync('app/javascript/skins/*/*');
+const flavours = {};
 
-const themePath = resolve('config', 'themes.yml');
-const themes = load(readFileSync(themePath), 'utf8');
+const core = function () {
+  const coreFile = resolve('app', 'javascript', 'core', 'theme.yml');
+  const data = load(readFileSync(coreFile), 'utf8');
+  if (!data.pack_directory) {
+    data.pack_directory = dirname(coreFile);
+  }
+  return data.pack ? data : {};
+}();
+
+flavourFiles.forEach((flavourFile) => {
+  const data = load(readFileSync(flavourFile), 'utf8');
+  data.name = basename(dirname(flavourFile));
+  data.skin = {};
+  if (!data.pack_directory) {
+    data.pack_directory = dirname(flavourFile);
+  }
+  if (data.locales) {
+    data.locales = join(dirname(flavourFile), data.locales);
+  }
+  if (data.pack && typeof data.pack === 'object') {
+    flavours[data.name] = data;
+  }
+});
+
+skinFiles.forEach((skinFile) => {
+  let skin = basename(skinFile);
+  const name = basename(dirname(skinFile));
+  if (!flavours[name]) {
+    return;
+  }
+  const data = flavours[name].skin;
+  if (lstatSync(skinFile).isDirectory()) {
+    data[skin] = {};
+    const skinPacks = glob.sync(join(skinFile, '*.{css,scss}'));
+    skinPacks.forEach((pack) => {
+      data[skin][basename(pack, extname(pack))] = pack;
+    });
+  } else if ((skin = skin.match(/^(.*)\.s?css$/i))) {
+    data[skin[1]] = { common: skinFile };
+  }
+});
 
 const output = {
   path: resolve('public', settings.public_output_path),
@@ -18,7 +61,8 @@ const output = {
 
 module.exports = {
   settings,
-  themes,
+  core,
+  flavours,
   env: {
     NODE_ENV: env.NODE_ENV,
     PUBLIC_OUTPUT_PATH: settings.public_output_path,
diff --git a/config/webpack/generateLocalePacks.js b/config/webpack/generateLocalePacks.js
index b71cf2ade..09fba4a18 100644
--- a/config/webpack/generateLocalePacks.js
+++ b/config/webpack/generateLocalePacks.js
@@ -1,52 +1,66 @@
+// A message from upstream:
+// ========================
 // To avoid adding a lot of boilerplate, locale packs are
 // automatically generated here. These are written into the tmp/
 // directory and then used to generate locale_en.js, locale_fr.js, etc.
 
-const fs = require('fs');
-const path = require('path');
+// Glitch note:
+// ============
+// This code has been entirely rewritten to support glitch flavours.
+// However, the underlying process is exactly the same.
+
+const { existsSync, readdirSync, writeFileSync } = require('fs');
+const { join, resolve } = require('path');
 const rimraf = require('rimraf');
 const mkdirp = require('mkdirp');
+const { flavours } = require('./configuration.js');
+
+module.exports = Object.keys(flavours).reduce(function (map, entry) {
+  const flavour = flavours[entry];
+  if (!flavour.locales) {
+    return map;
+  }
+  const locales = readdirSync(flavour.locales).filter(
+    filename => /\.js(?:on)?$/.test(filename) && !/defaultMessages|whitelist|index/.test(filename)
+  );
+  const outPath = resolve('tmp', 'locales', entry);
 
-const localesJsonPath = path.join(__dirname, '../../app/javascript/mastodon/locales');
-const locales = fs.readdirSync(localesJsonPath).filter(filename => {
-  return /\.json$/.test(filename) &&
-    !/defaultMessages/.test(filename) &&
-    !/whitelist/.test(filename);
-}).map(filename => filename.replace(/\.json$/, ''));
-
-const outPath = path.join(__dirname, '../../tmp/packs');
-
-rimraf.sync(outPath);
-mkdirp.sync(outPath);
-
-const outPaths = [];
-
-locales.forEach(locale => {
-  const localePath = path.join(outPath, `locale_${locale}.js`);
-  const baseLocale = locale.split('-')[0]; // e.g. 'zh-TW' -> 'zh'
-  const localeDataPath = [
-    // first try react-intl
-    `../../node_modules/react-intl/locale-data/${baseLocale}.js`,
-    // then check locales/locale-data
-    `../../app/javascript/mastodon/locales/locale-data/${baseLocale}.js`,
-    // fall back to English (this is what react-intl does anyway)
-    '../../node_modules/react-intl/locale-data/en.js',
-  ].filter(filename => fs.existsSync(path.join(outPath, filename)))
-    .map(filename => filename.replace(/..\/..\/node_modules\//, ''))[0];
-
-  const localeContent = `//
-// locale_${locale}.js
+  rimraf.sync(outPath);
+  mkdirp.sync(outPath);
+
+  locales.forEach(function (locale) {
+    const localeName = locale.replace(/\.js(?:on)?$/, '');
+    const localePath = join(outPath, `${localeName}.js`);
+    const baseLocale = localeName.split('-')[0]; // e.g. 'zh-TW' -> 'zh'
+    const localeDataPath = [
+      // first try react-intl
+      `node_modules/react-intl/locale-data/${baseLocale}.js`,
+      // then check locales/locale-data
+      `app/javascript/locales/locale-data/${baseLocale}.js`,
+      // fall back to English (this is what react-intl does anyway)
+      'node_modules/react-intl/locale-data/en.js',
+    ].filter(
+      filename => existsSync(filename)
+    ).map(
+      filename => filename.replace(/(?:node_modules|app\/javascript)\//, '')
+    )[0];
+    const localeContent = `//
+// locales/${entry}/${localeName}.js
 // automatically generated by generateLocalePacks.js
 //
-import messages from '../../app/javascript/mastodon/locales/${locale}.json';
-import localeData from ${JSON.stringify(localeDataPath)};
-import { setLocale } from '../../app/javascript/mastodon/locales';
-setLocale({messages, localeData});
-`;
-  fs.writeFileSync(localePath, localeContent, 'utf8');
-  outPaths.push(localePath);
-});
 
-module.exports = outPaths;
+import messages from '../../../${flavour.locales}/${locale.replace(/\.js$/, '')}';
+import localeData from '${localeDataPath}';
+import { setLocale } from 'locales';
 
+setLocale({
+  localeData,
+  messages,
+});
+`;
+    writeFileSync(localePath, localeContent, 'utf8');
+    map[`locales/${entry}/${localeName}`] = localePath;
+  });
 
+  return map;
+}, {});
diff --git a/config/webpack/rules/babel.js b/config/webpack/rules/babel.js
index 2fc245c43..4d25748ee 100644
--- a/config/webpack/rules/babel.js
+++ b/config/webpack/rules/babel.js
@@ -12,6 +12,7 @@ module.exports = {
     {
       loader: 'babel-loader',
       options: {
+        sourceRoot: 'app/javascript',
         cacheDirectory: join(settings.cache_path, 'babel-loader'),
         cacheCompression: env.NODE_ENV === 'production',
         compact: env.NODE_ENV === 'production',
diff --git a/config/webpack/rules/css.js b/config/webpack/rules/css.js
index bc1f42c13..6ecfb3164 100644
--- a/config/webpack/rules/css.js
+++ b/config/webpack/rules/css.js
@@ -20,6 +20,9 @@ module.exports = {
     {
       loader: 'sass-loader',
       options: {
+        sassOptions: {
+          includePaths: ['app/javascript'],
+        },
         implementation: require('sass'),
         sourceMap: true,
       },
diff --git a/config/webpack/shared.js b/config/webpack/shared.js
index 3447e711c..bbf9f51f1 100644
--- a/config/webpack/shared.js
+++ b/config/webpack/shared.js
@@ -5,33 +5,57 @@ const { basename, dirname, join, relative, resolve } = require('path');
 const { sync } = require('glob');
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 const AssetsManifestPlugin = require('webpack-assets-manifest');
-const extname = require('path-complete-extname');
-const { env, settings, themes, output } = require('./configuration');
+const { env, settings, core, flavours, output } = require('./configuration.js');
 const rules = require('./rules');
-const localePackPaths = require('./generateLocalePacks');
+const localePacks = require('./generateLocalePacks');
+
+function reducePacks (data, into = {}) {
+  if (!data.pack) return into;
+
+  for (const entry in data.pack) {
+    const pack = data.pack[entry];
+    if (!pack) continue;
+
+    let packFiles = [];
+    if (typeof pack === 'string')
+      packFiles = [pack];
+    else if (Array.isArray(pack))
+      packFiles = pack;
+    else
+      packFiles = [pack.filename];
+
+    if (packFiles) {
+      into[data.name ? `flavours/${data.name}/${entry}` : `core/${entry}`] = packFiles.map(packFile => resolve(data.pack_directory, packFile));
+    }
+  }
+
+  if (!data.name) return into;
+
+  for (const skinName in data.skin) {
+    const skin = data.skin[skinName];
+    if (!skin) continue;
+
+    for (const entry in skin) {
+      const packFile = skin[entry];
+      if (!packFile) continue;
+
+      into[`skins/${data.name}/${skinName}/${entry}`] = resolve(packFile);
+    }
+  }
+
+  return into;
+}
+
+const entries = Object.assign(
+  { locales: resolve('app', 'javascript', 'locales') },
+  localePacks,
+  reducePacks(core),
+  Object.values(flavours).reduce((map, data) => reducePacks(data, map), {})
+);
 
-const extensionGlob = `**/*{${settings.extensions.join(',')}}*`;
-const entryPath = join(settings.source_path, settings.source_entry_path);
-const packPaths = sync(join(entryPath, extensionGlob));
 
 module.exports = {
-  entry: Object.assign(
-    packPaths.reduce((map, entry) => {
-      const localMap = map;
-      const namespace = relative(join(entryPath), dirname(entry));
-      localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry);
-      return localMap;
-    }, {}),
-    localePackPaths.reduce((map, entry) => {
-      const localMap = map;
-      localMap[basename(entry, extname(entry, extname(entry)))] = resolve(entry);
-      return localMap;
-    }, {}),
-    Object.keys(themes).reduce((themePaths, name) => {
-      themePaths[name] = resolve(join(settings.source_path, themes[name]));
-      return themePaths;
-    }, {}),
-  ),
+  entry: entries,
 
   output: {
     filename: 'js/[name]-[chunkhash].js',
@@ -44,7 +68,7 @@ module.exports = {
 
   optimization: {
     runtimeChunk: {
-      name: 'common',
+      name: 'locales',
     },
     splitChunks: {
       cacheGroups: {
@@ -52,7 +76,9 @@ module.exports = {
         vendors: false,
         common: {
           name: 'common',
-          chunks: 'all',
+          chunks (chunk) {
+            return !(chunk.name in entries);
+          },
           minChunks: 2,
           minSize: 0,
           test: /^(?!.*[\\\/]node_modules[\\\/]react-intl[\\\/]).+$/,
diff --git a/config/webpacker.yml b/config/webpacker.yml
index 4ad78a190..9accd6152 100644
--- a/config/webpacker.yml
+++ b/config/webpacker.yml
@@ -26,6 +26,7 @@ default: &default
     - .tiff
     - .ico
     - .svg
+    - .gif
     - .eot
     - .otf
     - .ttf