From ba84b6d4d7b2e1ccdcbfcdd5e0d09fbfdb241f45 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 15 Mar 2019 04:36:41 +0100 Subject: Add `visibility` param to reblog REST API (#9851) Use async worker for creating reblog notification to improve performance --- app/models/status.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'app/models') diff --git a/app/models/status.rb b/app/models/status.rb index f33130dd6..d029ff3cd 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -70,6 +70,7 @@ class Status < ApplicationRecord validates_with StatusLengthValidator validates_with DisallowedHashtagsValidator validates :reblog, uniqueness: { scope: :account }, if: :reblog? + validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? validates_associated :owned_poll default_scope { recent } -- cgit From 3ad3223b466d8afbe8d11160a7351b34fe12c97a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 15 Mar 2019 13:36:38 +0100 Subject: Fix detailed poll validation errors not being returned in the API (#10261) No more "Owned poll is invalid" --- app/models/status.rb | 3 ++- app/services/post_status_service.rb | 18 +++++++++--------- config/locales/activerecord.en.yml | 3 +++ 3 files changed, 14 insertions(+), 10 deletions(-) (limited to 'app/models') diff --git a/app/models/status.rb b/app/models/status.rb index d029ff3cd..571167943 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -71,7 +71,8 @@ class Status < ApplicationRecord validates_with DisallowedHashtagsValidator validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? - validates_associated :owned_poll + + accepts_nested_attributes_for :owned_poll default_scope { recent } diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index a1705a6ad..3f392a6e6 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -29,7 +29,6 @@ class PostStatusService < BaseService return idempotency_duplicate if idempotency_given? && idempotency_duplicate? validate_media! - validate_poll! preprocess_attributes! if scheduled? @@ -71,6 +70,7 @@ class PostStatusService < BaseService def schedule_status! status_for_validation = @account.statuses.build(status_attributes) + if status_for_validation.valid? status_for_validation.destroy @@ -103,12 +103,6 @@ class PostStatusService < BaseService raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?) end - def validate_poll! - return if @options[:poll].blank? - - @poll = @account.polls.new(@options[:poll]) - end - def language_from_option(str) ISO_639.find(str)&.alpha2 end @@ -161,13 +155,13 @@ class PostStatusService < BaseService text: @text, media_attachments: @media || [], thread: @in_reply_to, - owned_poll: @poll, + owned_poll_attributes: poll_attributes, sensitive: (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?, spoiler_text: @options[:spoiler_text] || '', visibility: @visibility, language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account), application: @options[:application], - } + }.compact end def scheduled_status_attributes @@ -178,6 +172,12 @@ class PostStatusService < BaseService } end + def poll_attributes + return if @options[:poll].blank? + + @options[:poll].merge(account: @account) + end + def scheduled_options @options.tap do |options_hash| options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index 428aaf727..561ce68b8 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -1,6 +1,9 @@ --- en: activerecord: + attributes: + status: + owned_poll: Poll errors: models: account: -- cgit From 1c113fd72df18999de1d6f09fa3790dd1f715506 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 16 Mar 2019 11:23:22 +0100 Subject: Add relationship manager UI (#10268) --- app/controllers/relationships_controller.rb | 98 ++++++++++++++++++++++ .../settings/follower_domains_controller.rb | 28 ------- app/helpers/admin/filter_helper.rb | 3 +- app/javascript/styles/mastodon/tables.scss | 19 +++++ app/models/form/account_batch.rb | 60 +++++++++++++ app/views/relationships/_account.html.haml | 20 +++++ app/views/relationships/show.html.haml | 43 ++++++++++ app/views/settings/follower_domains/show.html.haml | 34 -------- config/locales/ar.yml | 10 --- config/locales/ast.yml | 5 -- config/locales/ca.yml | 13 --- config/locales/co.yml | 13 --- config/locales/cs.yml | 14 ---- config/locales/cy.yml | 17 ---- config/locales/da.yml | 13 --- config/locales/de.yml | 13 --- config/locales/el.yml | 13 --- config/locales/en.yml | 24 +++--- config/locales/eo.yml | 13 --- config/locales/es.yml | 13 --- config/locales/eu.yml | 13 --- config/locales/fa.yml | 13 --- config/locales/fi.yml | 13 --- config/locales/fr.yml | 13 --- config/locales/gl.yml | 13 --- config/locales/he.yml | 13 --- config/locales/hu.yml | 13 --- config/locales/id.yml | 13 --- config/locales/it.yml | 10 --- config/locales/ja.yml | 13 --- config/locales/ka.yml | 13 --- config/locales/kk.yml | 13 --- config/locales/ko.yml | 13 --- config/locales/lt.yml | 14 ---- config/locales/ms.yml | 4 - config/locales/nl.yml | 13 --- config/locales/no.yml | 13 --- config/locales/oc.yml | 13 --- config/locales/pl.yml | 15 ---- config/locales/pt-BR.yml | 13 --- config/locales/pt.yml | 13 --- config/locales/ro.yml | 6 -- config/locales/ru.yml | 15 ---- config/locales/sk.yml | 14 ---- config/locales/sq.yml | 13 --- config/locales/sr-Latn.yml | 15 ---- config/locales/sr.yml | 15 ---- config/locales/sv.yml | 13 --- config/locales/th.yml | 13 --- config/locales/tr.yml | 13 --- config/locales/uk.yml | 11 --- config/locales/zh-CN.yml | 11 --- config/locales/zh-HK.yml | 13 --- config/locales/zh-TW.yml | 11 --- config/navigation.rb | 2 +- config/routes.rb | 3 +- spec/controllers/relationships_controller_spec.rb | 77 +++++++++++++++++ .../settings/follower_domains_controller_spec.rb | 85 ------------------- 58 files changed, 332 insertions(+), 728 deletions(-) create mode 100644 app/controllers/relationships_controller.rb delete mode 100644 app/controllers/settings/follower_domains_controller.rb create mode 100644 app/models/form/account_batch.rb create mode 100644 app/views/relationships/_account.html.haml create mode 100644 app/views/relationships/show.html.haml delete mode 100644 app/views/settings/follower_domains/show.html.haml create mode 100644 spec/controllers/relationships_controller_spec.rb delete mode 100644 spec/controllers/settings/follower_domains_controller_spec.rb (limited to 'app/models') diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb new file mode 100644 index 000000000..e6dd65e44 --- /dev/null +++ b/app/controllers/relationships_controller.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +class RelationshipsController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + before_action :set_accounts, only: :show + before_action :set_body_classes + + helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? + + def show + @form = Form::AccountBatch.new + end + + def update + @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + # Do nothing + ensure + redirect_to relationships_path(current_params) + end + + private + + def set_accounts + @accounts = relationships_scope.page(params[:page]).per(40) + end + + def relationships_scope + scope = begin + if following_relationship? + current_account.following.includes(:account_stat) + else + current_account.followers.includes(:account_stat) + end + end + + scope.merge!(Follow.recent) + scope.merge!(mutual_relationship_scope) if mutual_relationship? + scope.merge!(abandoned_account_scope) if params[:status] == 'abandoned' + scope.merge!(active_account_scope) if params[:status] == 'active' + scope.merge!(by_domain_scope) if params[:by_domain].present? + + scope + end + + def mutual_relationship_scope + Account.where(id: current_account.following) + end + + def abandoned_account_scope + Account.where.not(moved_to_account_id: nil) + end + + def active_account_scope + Account.where(moved_to_account_id: nil) + end + + def by_domain_scope + Account.where(domain: params[:by_domain]) + end + + def form_account_batch_params + params.require(:form_account_batch).permit(:action, account_ids: []) + end + + def following_relationship? + params[:relationship].blank? || params[:relationship] == 'following' + end + + def mutual_relationship? + params[:relationship] == 'mutual' + end + + def followed_by_relationship? + params[:relationship] == 'followed_by' + end + + def current_params + params.slice(:page, :status, :relationship, :by_domain).permit(:page, :status, :relationship, :by_domain) + end + + def action_from_button + if params[:unfollow] + 'unfollow' + elsif params[:remove_from_followers] + 'remove_from_followers' + elsif params[:block_domains] + 'block_domains' + end + end + + def set_body_classes + @body_classes = 'admin' + end +end diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb deleted file mode 100644 index ce8ec985d..000000000 --- a/app/controllers/settings/follower_domains_controller.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -class Settings::FollowerDomainsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! - - def show - @account = current_account - @domains = current_account.followers.reorder(Arel.sql('MIN(follows.id) DESC')).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10) - end - - def update - domains = bulk_params[:select] || [] - - AfterAccountDomainBlockWorker.push_bulk(domains) do |domain| - [current_account.id, domain] - end - - redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size) - end - - private - - def bulk_params - params.permit(select: []) - end -end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 8f78bf5f8..09a356296 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -7,8 +7,9 @@ module Admin::FilterHelper CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze TAGS_FILTERS = %i(hidden).freeze INSTANCES_FILTERS = %i(limited by_domain).freeze + FOLLOWERS_FILTERS = %i(relationship status by_domain).freeze - FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS def filter_link_to(text, link_to_params, link_class_params = link_to_params) new_url = filtered_url_for(link_to_params) diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 9e8785679..d3a0ea03d 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -140,6 +140,15 @@ a.table-action-link { input { margin-top: 8px; } + + &--aligned { + display: flex; + align-items: center; + + input { + margin-top: 0; + } + } } &__actions, @@ -183,6 +192,10 @@ a.table-action-link { &__content { padding-top: 12px; padding-bottom: 16px; + + &--unpadded { + padding: 0; + } } } @@ -197,4 +210,10 @@ a.table-action-link { font-weight: 700; } } + + .nothing-here { + border: 1px solid darken($ui-base-color, 8%); + border-top: 0; + box-shadow: none; + } } diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb new file mode 100644 index 000000000..60eaaf0e2 --- /dev/null +++ b/app/models/form/account_batch.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +class Form::AccountBatch + include ActiveModel::Model + + attr_accessor :account_ids, :action, :current_account + + def save + case action + when 'unfollow' + unfollow! + when 'remove_from_followers' + remove_from_followers! + when 'block_domains' + block_domains! + end + end + + private + + def unfollow! + accounts.find_each do |target_account| + UnfollowService.new.call(current_account, target_account) + end + end + + def remove_from_followers! + current_account.passive_relationships.where(account_id: account_ids).find_each do |follow| + reject_follow!(follow) + end + end + + def block_domains! + AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain| + [current_account.id, domain] + end + end + + def account_domains + accounts.pluck(Arel.sql('distinct domain')).compact + end + + def accounts + Account.where(id: account_ids) + end + + def reject_follow!(follow) + follow.destroy + + return unless follow.account.activitypub? + + json = ActiveModelSerializers::SerializableResource.new( + follow, + serializer: ActivityPub::RejectFollowSerializer, + adapter: ActivityPub::Adapter + ).to_json + + ActivityPub::DeliveryWorker.perform_async(json, current_account.id, follow.account.inbox_url) + end +end diff --git a/app/views/relationships/_account.html.haml b/app/views/relationships/_account.html.haml new file mode 100644 index 000000000..6c22deb51 --- /dev/null +++ b/app/views/relationships/_account.html.haml @@ -0,0 +1,20 @@ +.batch-table__row + %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox + = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id + .batch-table__row__content.batch-table__row__content--unpadded + %table.accounts-table + %tbody + %tr + %td= account_link_to account + %td.accounts-table__count.optional + = number_to_human account.statuses_count, strip_insignificant_zeros: true + %small= t('accounts.posts', count: account.statuses_count).downcase + %td.accounts-table__count.optional + = number_to_human account.followers_count, strip_insignificant_zeros: true + %small= t('accounts.followers', count: account.followers_count).downcase + %td.accounts-table__count + - if account.last_status_at.present? + %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at + - else + \- + %small= t('accounts.last_active') diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml new file mode 100644 index 000000000..33a43f1a8 --- /dev/null +++ b/app/views/relationships/show.html.haml @@ -0,0 +1,43 @@ +- content_for :page_title do + = t('settings.relationships') + +- content_for :header_tags do + = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' + +.filters + .filter-subset + %strong= t 'relationships.relationship' + %ul + %li= filter_link_to t('accounts.following', count: current_account.following_count), relationship: nil + %li= filter_link_to t('accounts.followers', count: current_account.followers_count), relationship: 'followed_by' + %li= filter_link_to t('relationships.mutual'), relationship: 'mutual' + + .filter-subset + %strong= t 'relationships.status' + %ul + %li= filter_link_to t('generic.all'), status: nil + %li= filter_link_to t('relationships.active'), status: 'active' + %li= filter_link_to t('relationships.abandoned'), status: 'abandoned' + += form_for(@form, url: relationships_path, method: :patch) do |f| + = hidden_field_tag :page, params[:page] || 1 + = hidden_field_tag :relationship, params[:relationship] + = hidden_field_tag :status, params[:status] + + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all + = check_box_tag :batch_checkbox_all, nil, false + .batch-table__toolbar__actions + = f.button safe_join([fa_icon('user-times'), t('relationships.remove_selected_follows')]), name: :unfollow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless followed_by_relationship? + + = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless following_relationship? + + = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? + .batch-table__body + - if @accounts.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'account', collection: @accounts, locals: { f: f } + += paginate @accounts diff --git a/app/views/settings/follower_domains/show.html.haml b/app/views/settings/follower_domains/show.html.haml deleted file mode 100644 index f1687d4d2..000000000 --- a/app/views/settings/follower_domains/show.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -- content_for :page_title do - = t('settings.followers') - -= form_tag settings_follower_domains_path, method: :patch, class: 'table-form' do - - unless @account.locked? - .warning - %strong - = fa_icon('warning') - = t('followers.unlocked_warning_title') - = t('followers.unlocked_warning_html', lock_link: link_to(t('followers.lock_link'), settings_profile_url)) - - %p= t('followers.explanation_html') - %p= t('followers.true_privacy_html') - - .table-wrapper - %table.table - %thead - %tr - %th - %th= t('followers.domain') - %th= t('followers.followers_count') - %tbody - - @domains.each do |domain| - %tr - %td - = check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil? - %td - %samp= domain.domain.presence || Rails.configuration.x.local_domain - %td= number_with_delimiter domain.accounts_from_domain - - .action-pagination - .actions - = button_tag t('followers.purge'), type: :submit, class: 'button', disabled: !@account.locked? - = paginate @domains diff --git a/config/locales/ar.yml b/config/locales/ar.yml index b0b8d8b40..d409ad99a 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -607,15 +607,6 @@ ar: title: عوامل التصفية new: title: إضافة عامل تصفية جديد - followers: - domain: النطاق - followers_count: عدد المتابِعين - lock_link: قم بتجميد حسابك - purge: تنحية من بين متابعيك - success: جارية عملية حظر المتابِعين بسلاسة من %{count} نطاقات أخرى ... - true_privacy_html: تذكر دائمًا أنّ الخصوصية التامة لا يمكن بلوغها إلّا بالتعمية و التشفير من طرف إلى آخَر. - unlocked_warning_html: يمكن لأي كان متابعة حسابك و الإطلاع مباشرة على تبويقاتك. إستخدِم %{lock_link} لمُعاينة أو رفض طلبات المتابِعين الجُدُد. - unlocked_warning_title: إنّ حسابك غير مقفل footer: developers: المطورون more: المزيد … @@ -818,7 +809,6 @@ ar: development: التطوير edit_profile: تعديل الملف الشخصي export: تصدير البيانات - followers: المتابِعون المُرَخّصون import: إستيراد migrate: تهجير الحساب notifications: الإخطارات diff --git a/config/locales/ast.yml b/config/locales/ast.yml index ebf6a3799..cbfd27b04 100644 --- a/config/locales/ast.yml +++ b/config/locales/ast.yml @@ -182,10 +182,6 @@ ast: title: Peñeres new: title: Amestar una peñera nueva - followers: - domain: Dominiu - followers_count: Númberu de siguidores - purge: Desaniciar de los siguidores generic: changes_saved_msg: "¡Los cambeos guardáronse con ésitu!" save_changes: Guardar cambeos @@ -302,7 +298,6 @@ ast: back: Volver a Mastodon edit_profile: Edición del perfil export: Esportación de datos - followers: Siguidores autorizaos import: Importación notifications: Avisos preferences: Preferencies diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 417ba95f7..4f5012d56 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -588,18 +588,6 @@ ca: title: Filtres new: title: Afegir nou filtre - followers: - domain: Domini - explanation_html: Si desitges garantir la privacitat de les teves publicacions, has de ser conscient de qui t'està seguint. Les publicacions privades es lliuren a totes les instàncies on tens seguidors . És possible que vulguis revisar-los i eliminar seguidors si no confies en que la teva privacitat sigui respectada pel personal o el programari d'aquestes instàncies. - followers_count: Nombre de seguidors - lock_link: Bloca el teu compte - purge: Elimina dels seguidors - success: - one: En el procés de bloqueig suau de seguidors d'un domini... - other: En el procés de bloqueig suau de seguidors de %{count} dominis... - true_privacy_html: Considera que la autèntica privacitat només es pot aconseguir amb xifratge d'extrem a extrem. - unlocked_warning_html: Tothom pot seguir-te per a veure inmediatament les teves publicacions privades. %{lock_link} per poder revisar i rebutjar seguidors. - unlocked_warning_title: El teu compte no està blocat footer: developers: Desenvolupadors more: Més… @@ -785,7 +773,6 @@ ca: development: Desenvolupament edit_profile: Editar perfil export: Exportar informació - followers: Seguidors autoritzats import: Importar migrate: Migració del compte notifications: Notificacions diff --git a/config/locales/co.yml b/config/locales/co.yml index 77c3efeda..651d29781 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -593,18 +593,6 @@ co: title: Filtri new: title: Aghjustà un novu filtru - followers: - domain: Duminiu - explanation_html: Per assicuravi di a cunfidenzialità di i vostri statuti, duvete avè primura di quale vi seguita. I vostri statuti privati sò mandati à tutti i servori induve avete abbunati. Pensate à u vostru livellu di cunfidenza in i so amministratori. - followers_count: Numeru d’abbunati - lock_link: Rendete u contu privatu - purge: Toglie di a lista d’abbunati - success: - one: Suppressione di l’abbunati d’un duminiu... - other: Suppressione di l’abbunati da %{count} duminii... - true_privacy_html: Ùn vi scurdate chì una vera cunfidenzialità pò solu esse ottenuta cù crittografia da un capu à l’altru. - unlocked_warning_html: Tuttu u mondu pò seguitavi è vede i vostri statuti privati. %{lock_link} per pudè cunfirmà o righjittà abbunamenti. - unlocked_warning_title: U vostru contu hè pubblicu footer: developers: Sviluppatori more: Di più… @@ -807,7 +795,6 @@ co: edit_profile: Mudificà u prufile export: Spurtazione d’infurmazione featured_tags: Hashtag in vista - followers: Abbunati auturizati import: Impurtazione migrate: Migrazione di u contu notifications: Nutificazione diff --git a/config/locales/cs.yml b/config/locales/cs.yml index b0b317ac8..97e68eb4e 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -628,19 +628,6 @@ cs: title: Filtry new: title: Přidat nový filtr - followers: - domain: Doména - explanation_html: Chcete-li zaručit soukromí vašich tootů, musíte mít na vědomí, kdo vás sleduje. Vaše soukromé tooty jsou doručeny na všechny servery, kde máte sledující. Nejspíš si je budete chtít zkontrolovat a odstranit sledující na serverech, jejichž provozovatelům či softwaru nedůvěřujete s respektováním vašeho soukromí. - followers_count: Počet sledujících - lock_link: Uzamkněte svůj účet - purge: Odstranit ze sledujících - success: - few: V průběhu blokování sledujících ze %{count} domén... - one: V průběhu blokování sledujících z jedné domény... - other: V průběhu blokování sledujících z %{count} domén... - true_privacy_html: Berte prosím na vědomí, že skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování. - unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé tooty. %{lock_link}, abyste mohl/a kontrolovat a odmítat sledující. - unlocked_warning_title: Váš účet není uzamčen footer: developers: Vývojáři more: Více… @@ -846,7 +833,6 @@ cs: edit_profile: Upravit profil export: Export dat featured_tags: Zvýrazněné hashtagy - followers: Autorizovaní sledující import: Import migrate: Přesunutí účtu notifications: Oznámení diff --git a/config/locales/cy.yml b/config/locales/cy.yml index b6f94606d..900aedd57 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -614,22 +614,6 @@ cy: title: Hidlyddion new: title: Ychwanegu hidlydd newydd - followers: - domain: Parth - explanation_html: Os ydych am sicrhau preifatrwydd eich tŵtiau, rhaid i chi fod yn ymwybodol o bwy sy'n eich dilyn. Mae eich tŵtiau preifat yn cael eu hanfon at bob achos lle mae gennych ddilynwyr. Efallai hoffech chi i'w hadolygu o bryd i'w gilydd, a chael gwared ar ddilynwyr os nad ydych yn credu i'r staff neu'r meddalwedd ar yr achosion hynny barchu eich preifatrwydd. - followers_count: Nifer y dilynwyr - lock_link: Cloi eich cyfrif - purge: Dileu o dilynwyr - success: - few: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... - many: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... - one: Yn y broses o ysgafn-flocio dilynwyr o un parth... - other: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... - two: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... - zero: Yn y broses o ysgafn-flocio defnyddwyr o %{count} parth... - true_privacy_html: Cofiwch mai ond amgryptio pen-i-ben all sicrhau gwir breifatrwydd. - unlocked_warning_html: Gall unrhywun eich dilyn yn syth i weld eich tŵtiau preifat. %{lock_link} i gael adolygu a gwrthod dilynwyr. - unlocked_warning_title: Nid yw eich cyfrif wedi ei gloi footer: developers: Datblygwyr more: Mwy… @@ -815,7 +799,6 @@ cy: development: Datblygu edit_profile: Golygu proffil export: Allforio data - followers: Dilynwyr awdurdodedig import: Mewnforio migrate: Mudo cyfrif notifications: Hysbysiadau diff --git a/config/locales/da.yml b/config/locales/da.yml index a44a345d7..4953f70df 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -526,18 +526,6 @@ da: title: Filtrer new: title: Tilføj nyt filter - followers: - domain: Domæne - explanation_html: Hvis du vil sikre dig privatliv over dine statusser, skal du være klar over hvem der følger dig. Dine private statusser leveres til alle instanser som du har følger fra. Det kan være en ide at gennemgå dem, og fjerne følgere hvis du ikke føler dit privatliv respekteres af personalet eller software fra disse instanser. - followers_count: Antal følgere - lock_link: Lås din konto - purge: Fjern fra følgere - success: - one: I gang med at soft-blokere følgere fra et domæne... - other: I gang med at soft-blokere følgere fra %{count} domæner... - true_privacy_html: Husk på, at sand privatliv kan kun opnås via end-to-end kryptering. - unlocked_warning_html: Alle kan følge dig med det samme for at se dine private statusser. %{lock_link} for at være i stand til at gennemse og afvise følgere. - unlocked_warning_title: Din konto er ikke låst footer: developers: Udviklere more: Mere… @@ -708,7 +696,6 @@ da: development: Udvikling edit_profile: Rediger profil export: Data eksportering - followers: Godkendte følgere import: Importer migrate: Konto migrering notifications: Notifikationer diff --git a/config/locales/de.yml b/config/locales/de.yml index ae2948fb5..5c095c58a 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -592,18 +592,6 @@ de: title: Filter new: title: Neuen Filter hinzufügen - followers: - domain: Instanz - explanation_html: Wenn du sicherstellen willst, dass deine Beiträge privat sind, musst du wissen, wer dir folgt. Deine privaten Beiträge werden an alle Server weitergegeben, auf denen Menschen registriert sind, die dir folgen. Wenn du den Betreibenden eines Servers misstraust und du befürchtest, dass sie deine Privatsphäre missachten könnten, kannst du sie hier entfernen. - followers_count: Zahl der Folgenden - lock_link: dein Konto sperrst - purge: Von der Liste deiner Folgenden löschen - success: - one: Folgende von einer Domain werden soft-geblockt … - other: Folgende von %{count} Domains werden soft-geblockt … - true_privacy_html: Bitte beachte, dass wirklicher Schutz deiner Privatsphäre nur durch Ende-zu-Ende-Verschlüsselung erreicht werden kann.. - unlocked_warning_html: Wer dir folgen will, kann dies jederzeit ohne deine vorige Einverständnis tun und erhält damit automatisch Zugriff auf deine privaten Beiträge. Wenn du %{lock_link}, kannst du vorab entscheiden, wer dir folgen darf und wer nicht. - unlocked_warning_title: Dein Konto ist nicht gesperrt footer: developers: Entwickler more: Mehr… @@ -796,7 +784,6 @@ de: edit_profile: Profil bearbeiten export: Datenexport featured_tags: Empfohlene Hashtags - followers: Autorisierte Folgende import: Datenimport migrate: Konto-Umzug notifications: Benachrichtigungen diff --git a/config/locales/el.yml b/config/locales/el.yml index f5a2c5d4b..d78d63955 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -593,18 +593,6 @@ el: title: Φίλτρα new: title: Πρόσθεσε νέο φίλτρο - followers: - domain: Τομέας - explanation_html: Αν θέλεις να διασφαλίσεις την ιδιωτικότητα των ενημερώσεών σου, πρέπει να ξέρεις ποιος σε ακολουθεί. Οι ιδιωτικές ενημερώσεις σου μεταφέρονται σε όλους τους κόμβους στους οποίους έχεις ακόλουθους. Ίσως να θέλεις να κάνεις μια ανασκόπηση σε αυτούς και να αφαιρέσεις ακολούθους αν δεν εμπιστεύεσαι το προσωπικό αυτών των κόμβων πως θα σεβαστούν την ιδιωτικότητά σου. - followers_count: Πλήθος ακολούθων - lock_link: Κλείδωσε το λογαριασμό σου - purge: Αφαίρεσε από ακόλουθο - success: - one: Ημι-μπλοκάροντας τους ακόλουθους από έναν τομέα... - other: Ημι-μπλοκάροντας τους ακόλουθους από %{count} τομείς... - true_privacy_html: Έχε υπ' όψιν σου πως η πραγματική ιδιωτικότητα επιτυγχάνεται μόνο με κρυπτογράφηση από άκρη σε άκρη. - unlocked_warning_html: Μπορεί ο οποιοσδήποτε να σε ακολουθήσει και να βλέπει κατευθείαν τις ιδιωτικές ενημερώσεις σου. %{lock_link} για να αναθεωρήσεις και απορρίψεις ακόλουθους. - unlocked_warning_title: Ο λογαριασμός σου δεν είναι κλειδωμένος footer: developers: Ανάπτυξη more: Περισσότερα… @@ -806,7 +794,6 @@ el: edit_profile: Επεξεργασία προφίλ export: Εξαγωγή δεδομένων featured_tags: Χαρακτηριστικές ταμπέλες - followers: Εγκεκριμένοι ακόλουθοι import: Εισαγωγή migrate: Μετακόμιση λογαριασμού notifications: Ειδοποιήσεις diff --git a/config/locales/en.yml b/config/locales/en.yml index d11aa9262..4f9104eea 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -621,23 +621,12 @@ en: title: Filters new: title: Add new filter - followers: - domain: Domain - explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. Your private statuses are delivered to all servers where you have followers. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those servers. - followers_count: Number of followers - lock_link: Lock your account - purge: Remove from followers - success: - one: In the process of soft-blocking followers from one domain... - other: In the process of soft-blocking followers from %{count} domains... - true_privacy_html: Please mind that true privacy can only be achieved with end-to-end encryption. - unlocked_warning_html: Anyone can follow you to immediately view your private statuses. %{lock_link} to be able to review and reject followers. - unlocked_warning_title: Your account is not locked footer: developers: Developers more: More… resources: Resources generic: + all: All changes_saved_msg: Changes successfully saved! copy: Copy save_changes: Save changes @@ -761,6 +750,15 @@ en: other: Other publishing: Publishing web: Web + relationships: + abandoned: Abandoned + active: Active + mutual: Mutual + relationship: Relationship + remove_selected_domains: Remove all followers from the selected domains + remove_selected_followers: Remove selected followers + remove_selected_follows: Unfollow selected users + status: Account status remote_follow: acct: Enter your username@domain you want to act from missing_resource: Could not find the required redirect URL for your account @@ -835,11 +833,11 @@ en: edit_profile: Edit profile export: Data export featured_tags: Featured hashtags - followers: Authorized followers import: Import migrate: Account migration notifications: Notifications preferences: Preferences + relationships: Follows and followers settings: Settings two_factor_authentication: Two-factor Auth your_apps: Your applications diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 967396326..58d898fab 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -595,18 +595,6 @@ eo: title: Filtriloj new: title: Aldoni novan filtrilon - followers: - domain: Domajno - explanation_html: Se vi volas esti certa pri la privateco de viaj mesaĝoj, vi bezonas esti atenta pri tiuj, kiuj sekvas vin. Viaj privataj mesaĝoj estas liveritaj al ĉiuj serviloj, kie vi havas sekvantojn. Eble vi ŝatus kontroli ilin, kaj forigi la sekvantojn de la serviloj, kie vi ne certas ĉu via privateco estos respektita de la tiea teamo aŭ programo. - followers_count: Nombro de sekvantoj - lock_link: Ŝlosu vian konton - purge: Forigi el la sekvantoj - success: - one: Forigado de sekvantoj el iu domajno... - other: Forigado de sekvantoj el %{count} domajnoj... - true_privacy_html: Bonvolu atenti, ke vera privateco povas esti atingita nur per ĉifrado de komenco al fino. - unlocked_warning_html: Iu ajn povas eksekvi vin por tuj vidi viajn privatajn mesaĝojn. %{lock_link} por povi akcepti kaj rifuzi petojn de sekvado. - unlocked_warning_title: Via konto ne estas ŝlosita footer: developers: Programistoj more: Pli… @@ -799,7 +787,6 @@ eo: edit_profile: Redakti profilon export: Eksporti datumojn featured_tags: Elstarigitaj kradvortoj - followers: Rajtigitaj sekvantoj import: Importi migrate: Konta migrado notifications: Sciigoj diff --git a/config/locales/es.yml b/config/locales/es.yml index 648541eda..a79c3fb5d 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -529,18 +529,6 @@ es: title: Filtros new: title: Añadir un nuevo filtro - followers: - domain: Dominio - explanation_html: Si deseas asegurar la privacidad de tus estados, tienes que cuidarte de quién te sigue. Tus estados privados son enviados a todas las instancias de tus seguidores. Puede que desees revisarlas, y remover seguidores si no confías en tu privacidad para ser respetado por el staff o software de esas instancias. - followers_count: Número de seguidores - lock_link: Bloquear tu cuenta - purge: Remover de los seguidores - success: - one: En el proceso de bloquear suavemente usuarios de un solo dominio... - other: En el proceso de bloquear suavemente usuarios de %{count} dominios... - true_privacy_html: Por favor ten en cuenta que la verdadera privacidad se consigue con encriptación de punto a punto. - unlocked_warning_html: Todos pueden seguirte para ver tus estados privados inmediatamente. %{lock_link} para poder chequear y rechazar seguidores. - unlocked_warning_title: Tu cuenta no está bloqueada footer: developers: Desarrolladores more: Mas… @@ -711,7 +699,6 @@ es: development: Desarrollo edit_profile: Editar perfil export: Exportar información - followers: Seguidores autorizados import: Importar migrate: Migración de cuenta notifications: Notificaciones diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 59cba6287..187a5325b 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -592,18 +592,6 @@ eu: title: Iragazkiak new: title: Gehitu iragazki berria - followers: - domain: Domeinua - explanation_html: Zure mezuen pribatutasuna bermatu nahi baduzu, nork jarraitzen zaituen jakin behar duzu. Zure mezu pribatuak zure jarraitzaileak dituzten zerbitzari guztietara bidaltzen dira. Zerbitzari bateko langileek edo softwareak zure pribatutasunari dagokion begirunea ez dutela izango uste baduzu, berrikusi eta kendu jarraitzaileak. - followers_count: Jarraitzaile kopurua - lock_link: Giltzapetu zure kontua - purge: Kendu jarraitzaileetatik - success: - one: Domeinu bateko jarraitzaileei blokeo leuna ezartzen... - other: "%{count} domeinuetako jarraitzaileei blokeo leuna ezartzen..." - true_privacy_html: Kontuan izan egiazko pribatutasuna lortzeko muturretik muturrerako zifratzea ezinbestekoa dela. - unlocked_warning_html: Edonork jarraitu zaitzake eta berehala zure mezu pribatuak ikusi. %{lock_link} jarraitzaileak berrikusi eta ukatu ahal izateko. - unlocked_warning_title: Zure kontua ez dago giltzapetuta footer: developers: Garatzaileak more: Gehiago… @@ -796,7 +784,6 @@ eu: edit_profile: Aldatu profila export: Datuen esportazioa featured_tags: Nabarmendutako traolak - followers: Baimendutako jarraitzaileak import: Inportazioa migrate: Kontuaren migrazioa notifications: Jakinarazpenak diff --git a/config/locales/fa.yml b/config/locales/fa.yml index a1c891bc7..be19ff3da 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -593,18 +593,6 @@ fa: title: فیلترها new: title: افزودن فیلتر تازه - followers: - domain: دامین - explanation_html: اگر می‌خواهید از خصوصی‌بودن نوشته‌های خود مطمئن شوید، باید بدانید که چه کسانی پیگیر شما هستند. نوشته‌های خصوصی شما به همهٔ سرورهایی که در آن‌ها پیگیر دارید فرستاده می‌شود. شاید بخواهید این سرورها را بررسی کنید، و اگر به مسئولان یا نرم‌افزارهای آن‌ها در رعایت حریم خصوصی خود اعتماد ندارید، می‌توانید آن‌ها را حذف کنید. - followers_count: تعداد پیگیران - lock_link: حساب خود را خصوصی کنید - purge: برداشتن پیگیری - success: - one: در حال انجام مسدودسازی نرم روی کاربران یک دامین... - other: در حال انجام مسدودسازی نرم روی کاربران %{count} دامین... - true_privacy_html: لطفاً بدانید که داشتن حریم خصوصی واقعی تنها با رمزگذاری سرتاسر (end-to-end encryption) ممکن است. - unlocked_warning_html: هر کسی می‌تواند پیگیر شما شود تا بلافاصله نوشته‌های خصوصی شما را ببیند. اگر %{lock_link} خواهید توانست درخواست‌های پیگیری را بررسی کرده و نپذیرید. - unlocked_warning_title: حساب شما خصوصی نیست footer: developers: برنامه‌نویسان more: بیشتر… @@ -807,7 +795,6 @@ fa: edit_profile: ویرایش نمایه export: برون‌سپاری داده‌ها featured_tags: برچسب‌های منتخب - followers: پیگیران مورد تأیید import: درون‌ریزی migrate: انتقال حساب notifications: اعلان‌ها diff --git a/config/locales/fi.yml b/config/locales/fi.yml index deacd351a..029696f7d 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -449,18 +449,6 @@ fi: follows: Seurattavat mutes: Mykistetyt storage: Media-arkisto - followers: - domain: Verkkotunnus - explanation_html: Jos haluat olla varma tilapäivitystesi yksityisyydestä, sinun täytyy tietää, ketkä seuraavat sinua. Yksityiset tilapäivityksesi lähetetään kaikkiin niihin instansseihin, joissa sinulla on seuraajia. Jos et luota siihen, että näiden instanssien ylläpitäjät tai ohjelmisto kunnioittavat yksityisyyttäsi, käy läpi seuraajaluettelosi ja poista tarvittaessa käyttäjiä. - followers_count: Seuraajien määrä - lock_link: Lukitse tili - purge: Poista seuraajista - success: - one: Estetään kevyesti seuraajia yhdestä verkkotunnuksesta... - other: Estetään kevyesti seuraajia %{count} verkkotunnuksesta... - true_privacy_html: Muista, että kunnollinen yksityisyys voidaan varmistaa vain päästä päähän -salauksella. - unlocked_warning_html: Kuka tahansa voi seurata sinua ja nähdä saman tien yksityiset tilapäivityksesi. %{lock_link}, niin voit tarkastaa ja torjua seuraajia. - unlocked_warning_title: Tiliäsi ei ole lukittu generic: changes_saved_msg: Muutosten tallennus onnistui! save_changes: Tallenna muutokset @@ -622,7 +610,6 @@ fi: development: Kehittäminen edit_profile: Muokkaa profiilia export: Vie tietoja - followers: Valtuutetut seuraajat import: Tuo migrate: Tilin muutto muualle notifications: Ilmoitukset diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 1694fda82..cf5b768d3 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -593,18 +593,6 @@ fr: title: Filtres new: title: Ajouter un nouveau filtre - followers: - domain: Domaine - explanation_html: Si vous voulez vous assurer que vos statuts restent privés, vous devez savoir qui vous suit. Vos statuts privés seront diffusés sur toutes les instances où vous avez des abonné·e·s. Vous voudrez peut-être les passer en revue et les supprimer si vous pensez que votre vie privée ne sera pas respectée par l’administration ou le logiciel de ces instances. - followers_count: Nombre d’abonné⋅e⋅s - lock_link: Rendez votre compte privé - purge: Retirer de la liste d’abonné⋅e⋅s - success: - one: Suppression des abonné⋅e⋅s venant d’un domaine en cours… - other: Suppression des abonné⋅e⋅s venant de %{count} domaines en cours… - true_privacy_html: Soyez conscient⋅e⋅s qu’une vraie confidentialité ne peut être atteinte que par un chiffrement de bout-en-bout. - unlocked_warning_html: N’importe qui peut vous suivre et voir vos statuts privés. %{lock_link} afin de pouvoir vérifier et rejeter des abonné⋅e⋅s. - unlocked_warning_title: Votre compte n’est pas privé footer: developers: Développeurs more: Davantage… @@ -807,7 +795,6 @@ fr: edit_profile: Modifier le profil export: Export de données featured_tags: Hashtags mis en avant - followers: Abonné⋅es autorisé⋅es import: Import de données migrate: Migration de compte notifications: Notifications diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 249128426..1a1f6c590 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -593,18 +593,6 @@ gl: title: Filtros new: title: Engadir novo filtro - followers: - domain: Dominio - explanation_html: Se quere asegurar a intimidade dos seus estados, debe ser consciente de quen a está a seguir. Os seus estados privados son enviados a todas os servidores onde ten seguidoras. Podería querer revisalas, e elminar seguidoras si non confía que a súa intimidade sexa respetada polos administradores ou o software de ese servidor. - followers_count: Número de seguidoras - lock_link: Bloquear a súa conta - purge: Eliminar das seguidoras - success: - one: En proceso de bloquear seguidoras de un dominio... - other: No proceso de bloquear seguidoras de %{count} dominios... - true_privacy_html: Por favor teña en conta que a verdadeira intimidade só pode ser conseguida con cifrado de extremo-a-extremo. - unlocked_warning_html: Calquera pode seguila para inmediatamente ver os seus estados privados. %{lock_link} para poder revisar e rexeitar seguidoras. - unlocked_warning_title: A súa conta non está pechada footer: developers: Desenvolvedoras more: Máis… @@ -807,7 +795,6 @@ gl: edit_profile: Editar perfil export: Exportar datos featured_tags: Etiquetas destacadas - followers: Seguidoras autorizadas import: Importar migrate: Migrar conta notifications: Notificacións diff --git a/config/locales/he.yml b/config/locales/he.yml index 1ddb1361d..089af2beb 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -240,18 +240,6 @@ he: follows: רשימת נעקבים mutes: רשימת השתקות storage: אחסון מדיה - followers: - domain: קהילה - explanation_html: אם ברצונך להבטיח את הפרטיות של הודעותיך, יש לשים לב מי עוקב אחריך. הודעותיך הפרטיות יועברו לכל השרתים בהם יש לך עוקבים. כדאי לעבור על הרשימה ולהסיר עוקבים אם אין לך אמון בתוכנה או בצוות המפעילים של השרת הרחוק שיכבד את פרטיותך. - followers_count: מספר העוקבים - lock_link: לנעול את חשבונך - purge: הסרה מהעוקבים - success: - one: בתהליך חסימה של עוקבים ממתחם אחד... - other: בתהליך חסימה של עוקבים המגיעים מ־%{count} מתחמים... - true_privacy_html: 'לתשומת ליבך: פרטיות אמיתית ניתן להשיג אך ורק על ידי הצפנה מקצה לקצה.' - unlocked_warning_html: כל אחד יכול לעקוב אחריך כדי לראות מיידית את חצרוציך הפרטיים. %{lock_link} כדי לבחון ולדחות עוקבים. - unlocked_warning_title: חשבונך אינו נעול generic: changes_saved_msg: השינויים נשמרו בהצלחה! save_changes: שמור שינויים @@ -320,7 +308,6 @@ he: back: חזרה למסטודון edit_profile: עריכת פרופיל export: יצוא מידע - followers: עוקבים מאושרים import: יבוא preferences: העדפות settings: הגדרות diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 44399778c..04318f5e4 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -374,18 +374,6 @@ hu: follows: Követettjeid mutes: Némításaid storage: Médiatároló - followers: - domain: Domain - explanation_html: Ahhoz, hogy biztosítsd a tülkjeid adatvédelmét, tudnod kell, kik követnek téged. Még privátnak jelölt tülkjeid is továbbítódnak minden instanciára, ahol követőid vannak. Az alábbi listában láthatod, melyek ezek az instanciák; eltávolíthatod őket, ha nem vagy biztos benne, hogy az adott instancia üzemeltetői tiszteletben tartják az adatvédelmi beállításaidat. - followers_count: Követők száma - lock_link: Fiókod priváttá tétele - purge: Eltávolítás a követőid közül - success: - one: Egy domainen található követőid tiltása folyamatban... - other: "%{count} domainen található követőid tiltása folyamatban..." - true_privacy_html: Tartsd észben, hogy valódi biztonság csak végponttól-végpontig titkosítással érhető el. - unlocked_warning_html: Bárki követhet és így azonnal láthatja a privát tülkjeid. A %{lock_link} funkció bekapcsolásával lehetőséged van egyenként felülvizsgálni a követési kérelmeket. - unlocked_warning_title: A fiókod jelenleg nem privát generic: changes_saved_msg: Változások sikeresen elmentve! save_changes: Változások mentése @@ -542,7 +530,6 @@ hu: development: Fejlesztőknek edit_profile: Profil szerkesztése export: Adatok exportálása - followers: Jóváhagyott követők import: Importálás migrate: Fiók átirányítása notifications: Értesítések diff --git a/config/locales/id.yml b/config/locales/id.yml index a91f459a4..a27f1f008 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -266,18 +266,6 @@ id: follows: Anda ikuti mutes: Anda bisukan storage: Penyimpanan media - followers: - domain: Domain - explanation_html: Jika anda ingin memastikan privasi dari status anda, anda harus tahu siapa yang mengikuti anda. Status pribadi anda dikirim ke semua server dimana pengikut anda berada. Anda mungkin ingin untuk mengkaji ulang dan menghapus pengikut jika anda tidak mempercayai bahwa privasi anda di tangan staf atau software di server tersebut. - followers_count: Jumlah pengikut - lock_link: Kunci akun anda - purge: Hapus dari pengikut - success: - one: Dalam proses memblokir pengikut dari satu domain... - other: Dalam proses memblokir pengikut dari %{count} domain... - true_privacy_html: Mohon diingat bahwa privasi yang sebenarnya hanya dapat dicapai dengan enkripsi end-to-end. - unlocked_warning_html: Semua orang dapat mengikuti anda untuk langsung dapat melihat status pribadi anda. %{lock_link} untuk dapat meninjau dan menolak calon pengikut. - unlocked_warning_title: Akun anda tidak dikunci generic: changes_saved_msg: Perubahan berhasil disimpan! save_changes: Simpan perubahan @@ -344,7 +332,6 @@ id: back: Kembali ke Mastodon edit_profile: Ubah profil export: Expor data - followers: Pengikut yang diizinkan import: Impor preferences: Pilihan settings: Pengaturan diff --git a/config/locales/it.yml b/config/locales/it.yml index 1af8bc08c..7d2e1dd29 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -554,15 +554,6 @@ it: title: Filtri new: title: Aggiungi filtro - followers: - domain: Dominio - explanation_html: Se vuoi garantire la privacy dei tuoi status, devi sapere chi ti sta seguendo. I tuoi status privati vengono inviati a tutti i server su cui hai dei seguaci. Puoi controllare chi sono i tuoi seguaci, ed eliminarli se non hai fiducia che la tua privacy venga rispettata dallo staff o dal software di quei server. - followers_count: Numero di seguaci - lock_link: Blocca il tuo account - purge: Elimina dai seguaci - true_privacy_html: Tieni presente che l'effettiva riservatezza si può ottenere solo con la crittografia end-to-end. - unlocked_warning_html: Chiunque può seguirti per vedere immediatamente i tuoi status privati. %{lock_link} per poter esaminare e respingere gli utenti che vogliono seguirti. - unlocked_warning_title: Il tuo account non è bloccato footer: developers: Sviluppatori more: Altro… @@ -722,7 +713,6 @@ it: development: Sviluppo edit_profile: Modifica profilo export: Esporta impostazioni - followers: Seguaci autorizzati import: Importa migrate: Migrazione dell'account notifications: Notifiche diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 19845caa7..c3fa76530 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -607,18 +607,6 @@ ja: title: フィルター new: title: 新規フィルターを追加 - followers: - domain: ドメイン - explanation_html: あなたの投稿のプライバシーを確保したい場合、誰があなたをフォローしているのかを把握している必要があります。 プライベート投稿は、あなたのフォロワーがいる全てのサーバーに配信されます。 フォロワーのサーバーの管理者やソフトウェアがあなたのプライバシーを尊重してくれるかどうか怪しい場合は、そのフォロワーを削除した方がよいかもしれません。 - followers_count: フォロワー数 - lock_link: 承認制アカウントにする - purge: フォロワーから削除する - success: - one: 1個のドメインからソフトブロックするフォロワーを処理中... - other: "%{count} 個のドメインからソフトブロックするフォロワーを処理中..." - true_privacy_html: "プライバシーの保護はエンドツーエンドの暗号化でのみ実現可能であることに留意ください。" - unlocked_warning_html: 誰でもあなたをフォローすることができ、フォロワー限定の投稿をすぐに見ることができます。フォローする人を限定したい場合は%{lock_link}に設定してください。 - unlocked_warning_title: このアカウントは承認制アカウントに設定されていません footer: developers: 開発者向け more: さらに… @@ -820,7 +808,6 @@ ja: edit_profile: プロフィールを編集 export: データのエクスポート featured_tags: 注目のハッシュタグ - followers: 信頼済みのサーバー import: データのインポート migrate: アカウントの引っ越し notifications: 通知 diff --git a/config/locales/ka.yml b/config/locales/ka.yml index 5d0bba510..8e537c745 100644 --- a/config/locales/ka.yml +++ b/config/locales/ka.yml @@ -496,18 +496,6 @@ ka: title: ფილტრები new: title: ახალი ფილტრის დამატება - followers: - domain: დომენი - explanation_html: თუ გსურთ უზრუნველყოთ თქვენი სტატუსების კონფიდენციალურობა, უნდა იცოდეთ თუ ვინ მოგყვებათ. კერძო სტატუსები მიეწოდება ყველა ინსტანციას, სადაც გყავთ მიმდევრები. შესაძლოა გსურდეთ განიხილოთ ისინი და ამოშალოთ მიმდევრები თუ არ ენდობით თქვენი კონფიდენციალურობის პატივისცემას სტაფისა თუ პროგრამისგან იმ ინსტანციებში. - followers_count: მიმდევრების რაოდენობა - lock_link: თქვენი ანგარიშის ჩაკეტვა - purge: მიმდევრებიდან ამოშლა - success: - one: მიმდევრების სოფტ-ბლოკირების პროცესი ერთი დომენზე... - other: მიმდევრების სოფტ-ბლოკირების პროცესი %{count} დომენზე... - true_privacy_html: გთხოვთ გაითვალისწინეთ, ჭეშმარიტი კონფიდენციალურობა მიღწევადია მხოლოდ ენდ-თუ-ენდ შიფრაციით. - unlocked_warning_html: ყველას შეუძლია გამოგყვეთ, რომ უცბად იხილოს თქვენი სტატუსები. %{lock_link} რომ შეძლოთ განიხილოთ და უარყოთ მიმდევრები. - unlocked_warning_title: თქვენი ანგარიში არაა ჩაკეტილი footer: developers: დეველოპერები more: მეტი… @@ -677,7 +665,6 @@ ka: development: დეველოპმენტი edit_profile: პროფილის ცვლილება export: მონაცემის ექსპორტი - followers: ავტორიზირებული მიმდევრები import: იმპორტი migrate: ანგარიშის მიგრაცია notifications: შეტყობინებები diff --git a/config/locales/kk.yml b/config/locales/kk.yml index 4897bc095..aeea25939 100644 --- a/config/locales/kk.yml +++ b/config/locales/kk.yml @@ -593,18 +593,6 @@ kk: title: Фильтрлер new: title: Жаңа фильтр қосу - followers: - domain: Домен - explanation_html: Егер сіз жазбаларыңыздың құпиялылығын қамтамасыз еткіңіз келсе, сізді кім іздейтінін білуіңіз керек. Сіздің жазбаларыңыз оқырмандарыңыз бар барлық серверлерге жеткізіледі . Оларды оқырмандарыңызға және админдерге немесе осы серверлердің бағдарламалық жасақтамасына жауапты қызметкерлерге сенбесеңіз, оқырмандарыңызды алып тастауыңызға болады. - followers_count: Оқырман саны - lock_link: Аккаунтыңызды құлыптау - purge: Оқырмандар тізімінен шығару - success: - one: Бір доменнен оқырмандарды бұғаттау барысында... - other: "%{count} доменнен оқырмандарды бұғаттау барысында..." - true_privacy_html: Ұмытпаңыз, нақты құпиялылықты шифрлаудан соң ғана қол жеткізуге болатындығын ескеріңіз.. - unlocked_warning_html: Кез келген адам жазбаларыңызды оқу үшін сізге жазыла алады. Жазылушыларды қарап, қабылдамау үшін %{lock_link}. - unlocked_warning_title: Аккаунтыңыз қазір құлыпталды footer: developers: Жасаушылар more: Тағы… @@ -796,7 +784,6 @@ kk: edit_profile: Профиль өңдеу export: Экспорт уақыты featured_tags: Таңдаулы хэштегтер - followers: Авторизацияланған оқырмандар import: Импорт migrate: Аккаунт көшіру notifications: Ескертпелер diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 9d480e7bc..52042ae1a 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -595,18 +595,6 @@ ko: title: 필터 new: title: 필터 추가 - followers: - domain: 도메인 - explanation_html: 프라이버시를 확보하고 싶은 경우, 누가 여러분을 팔로우 하고 있는지 파악해둘 필요가 있습니다. 프라이빗 포스팅은 여러분의 팔로워가 소속하는 모든 서버로 배달됩니다. 팔로워가 소속된 서버 관리자나 소프트웨어가 여러분의 프라이버시를 존중하고 있는지 잘 모를 경우, 그 팔로워를 삭제하는 것이 좋을 수도 있습니다. - followers_count: 팔로워 수 - lock_link: 비공개 계정 - purge: 팔로워에서 삭제 - success: - one: 1개 도메인에서 팔로워를 soft-block 처리 중... - other: "%{count}개 도메인에서 팔로워를 soft-block 처리 중..." - true_privacy_html: "프라이버시 보호는 End-to-End 암호화로만 이루어 질 수 있다는 것에 유의해 주십시오." - unlocked_warning_html: 누구든 여러분을 팔로우 할 수 있으며, 여러분의 프라이빗 투고를 볼 수 있습니다. 팔로우 할 수 있는 사람을 제한하고 싶은 경우 %{lock_link}에서 설정해 주십시오. - unlocked_warning_title: 이 계정은 비공개로 설정되어 있지 않습니다 footer: developers: 개발자 more: 더 보기… @@ -809,7 +797,6 @@ ko: edit_profile: 프로필 편집 export: 데이터 내보내기 featured_tags: 추천 해시태그 - followers: 신뢰 중인 인스턴스 import: 데이터 가져오기 migrate: 계정 이동 notifications: 알림 diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 4f8fd5825..0f5ca3091 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -602,19 +602,6 @@ lt: title: Filtrai new: title: Pridėti naują filtrą - followers: - domain: Domenas - explanation_html: Jeigu norite garantuoti savo statusų privatumą, turite žinoti, kas jus seka. Jūsų privatūs statusai yra pristatyti visiems serveriams, kur jūs turite sekėju. Galbūt jūs norite juos peržiūrėti ir panaikinti sekėjus, kuriais nepasitikite. - followers_count: Sekėjų skaičius - lock_link: Užrakinti savo paskyrą - purge: Panaikint iš sekėju - success: - few: Švelnaus sekėjų blokavimo procedūroje iš %{count} domenų... - one: Švelnaus sekėjų blokavimo procedūroje iš vieno domeno... - other: Švelnaus sekėjų blokavimo procedūroje iš %{count} domenų... - true_privacy_html: Prašau prisiminti, kad tikras privatumas gali būti pasiekamas tik su end-to-end užsifravimu. - unlocked_warning_html: Visi, kurie nori matyti Jūsų privatų statusą, gali jus sekti. %{lock_link} kad galėtumėte peržiurėti ir pašalinti sekėjus. - unlocked_warning_title: Jūsų paskyra neužrakinta footer: developers: Programuotojai more: Daugiau… @@ -810,7 +797,6 @@ lt: edit_profile: Keisti profilį export: Informacijos eksportas featured_tags: Rodomi saitažodžiai(#) - followers: Autorizuoti sekėjai import: Importuoti migrate: Paskyros migracija notifications: Pranešimai diff --git a/config/locales/ms.yml b/config/locales/ms.yml index 0b1269fb2..fbadd80fd 100644 --- a/config/locales/ms.yml +++ b/config/locales/ms.yml @@ -317,10 +317,6 @@ ms: exports: archive_takeout: in_progress: Mengkompil arkib anda... - followers: - success: - one: Dalam proses menyekat-lembut pengikut daripada satu domain... - other: Dalam proses menyekat-lembut pengikut daripada %{count} domain... notification_mailer: digest: title: Ketika anda tiada di sini... diff --git a/config/locales/nl.yml b/config/locales/nl.yml index f92ae3bf1..e75d684f1 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -593,18 +593,6 @@ nl: title: Filters new: title: Nieuw filter toevoegen - followers: - domain: Domein - explanation_html: Wanneer je de privacy van jouw toots wilt garanderen, moet je goed weten wie jouw volgers zijn. Toots die alleen aan jouw volgers zijn gericht, worden aan de Mastodonservers van jouw volgers afgeleverd. Daarom wil je ze misschien controleren en desnoods volgers verwijderen die zich op een Mastodonserver bevinden die jij niet vertrouwd. Bijvoorbeeld omdat de beheerder(s) of de software van zo'n server jouw privacy niet respecteert. - followers_count: Aantal volgers - lock_link: Maak jouw account besloten - purge: Volgers verwijderen - success: - one: Bezig om volgers van één domein te verwijderen... - other: Bezig om volgers van %{count} domeinen te verwijderen... - true_privacy_html: Hou er wel rekening mee dat echte privacy alleen gegarandeerd kan worden met behulp van end-to-end-encryptie. - unlocked_warning_html: Iedereen kan jou volgen en daarmee meteen toots zien die je alleen aan jouw volgers hebt gericht. %{lock_link} om volgers te kunnen beoordelen en desnoods te weigeren. - unlocked_warning_title: Jouw account is niet besloten footer: developers: Ontwikkelaars more: Meer… @@ -797,7 +785,6 @@ nl: edit_profile: Profiel bewerken export: Exporteren featured_tags: Uitgelichte hashtags - followers: Geautoriseerde volgers import: Importeren migrate: Accountmigratie notifications: Meldingen diff --git a/config/locales/no.yml b/config/locales/no.yml index 6ee42a7ca..773f2d060 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -374,18 +374,6 @@ follows: Du følger mutes: Du demper storage: Medialagring - followers: - domain: Domene - explanation_html: Hvis du vil styre hvem som ser statusene dine, må du være klar over hvem som følger deg. Dine private statuser leveres til alle instanser der du har følgere. Du bør kanskje se over dem, og fjerne følgere hvis du ikke stoler på at ditt privatliv vil bli respektert av staben eller programvaren på de instansene. - followers_count: Antall følgere - lock_link: Lås kontoen din - purge: Fjern fra følgere - success: - one: I ferd med å mykblokkere følgere fra ett domene... - other: I ferd med å mykblokkere følgere fra %{count} domener... - true_privacy_html: Merk deg at virkelig privatliv kun kan oppnås med ende-til-ende-kryptering. - unlocked_warning_html: Alle kan følge deg for å umiddelbart se dine private statuser. %{lock_link} for å kunne se over og avvise følgere. - unlocked_warning_title: Din konto er ikke låst generic: changes_saved_msg: Vellykket lagring av endringer! save_changes: Lagre endringer @@ -542,7 +530,6 @@ development: Utvikling edit_profile: Endre profil export: Dataeksport - followers: Godkjente følgere import: Importér migrate: Kontomigrering notifications: Varslinger diff --git a/config/locales/oc.yml b/config/locales/oc.yml index b1d7c46d6..d87f7446f 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -649,18 +649,6 @@ oc: title: Filtres new: title: Ajustar un nòu filtre - followers: - domain: Domeni - explanation_html: Se volètz vos assegurar de la confidencialitat de vòstres estatuts, vos cal saber qual sèc vòstre compte. Vòstres estatuts privats son enviats a totas las instàncias qu’an de monde que vos sègon.. Benlèu que volètz repassar vòstra lista e tirar los seguidors s’avètz de dobtes tocant las politicas de confidencialitat dels gestionaris de lor instància o sul logicial qu’utilizan. - followers_count: Nombre de seguidors - lock_link: Clavar vòstre compte - purge: Tirar dels seguidors - success: - one: Soi a blocar los seguidors d’un domeni… - other: Soi a blocar los seguidors de %{count} domenis… - true_privacy_html: Mèfi que la vertadièra confidencialitat pòt solament èsser amb un chiframent del cap a la fin (end-to-end). - unlocked_warning_html: Tot lo monde pòt vos sègre e veire sulpic vòstres estatuts privats. %{lock_link} per poder repassar e regetar los seguidors. - unlocked_warning_title: Vòstre compte es pas clavat footer: developers: Desvolopaires more: Mai… @@ -853,7 +841,6 @@ oc: edit_profile: Modificar lo perfil export: Exportar de donadas featured_tags: Etiquetas en avant - followers: Seguidors autorizats import: Importar de donadas migrate: Migracion de compte notifications: Notificacions diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 6a2b15ba5..878416dcf 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -606,20 +606,6 @@ pl: title: Filtry new: title: Dodaj nowy filtr - followers: - domain: Domena - explanation_html: Jeżeli chcesz mieć pewność, kto może przeczytać Twoje wpisy, musisz kontrolować, kto śledzi Twój profil. Twoje prywatne wpisy są dostarczane na te instancje, na których jesteś śledzony. Możesz sprawdzać, kto Cię śledzi i blokować ich, jeśli nie ufasz właścicielom lub oprogramowaniu danej instancji. - followers_count: Liczba śledzących - lock_link: Zablokuj swoje konto - purge: Przestań śledzić - success: - few: W trakcie usuwania śledzących z %{count} domen… - many: W trakcie usuwania śledzących z %{count} domen… - one: W trakcie usuwania śledzących z jednej domeny… - other: W trakcie usuwania śledzących z %{count} domen… - true_privacy_html: Pamiętaj, że rzeczywista prywatność może zostać uzyskana wyłącznie dzięki szyfrowaniu end-to-end. - unlocked_warning_html: Każdy może Cię śledzić, dzięki czemu może zobaczyć Twoje niepubliczne wpisy. %{lock_link} aby móc kontrolować, kto Cię śledzi. - unlocked_warning_title: Twoje konto nie jest zablokowane footer: developers: Dla programistów more: Więcej… @@ -820,7 +806,6 @@ pl: edit_profile: Edytuj profil export: Eksportowanie danych featured_tags: Wyróżnione hashtagi - followers: Autoryzowani śledzący import: Importowanie danych migrate: Migracja konta notifications: Powiadomienia diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index ae4b0a271..be1ea6155 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -585,18 +585,6 @@ pt-BR: title: Filtros new: title: Adicionar novo filtro - followers: - domain: Domínio - explanation_html: Se você quer garantir a privacidade de suas postagens, você deve ficar atento a quem está te seguindo.Suas postagens privadas são enviadas para todas as instâncias em que você tem seguidores. Convém revisá-las e remover seguidores se você acredita que a sua privacidade não será respeitada pela equipe ou software destas instâncias. - followers_count: Número de seguidores - lock_link: Tranque a sua conta - purge: Remover de seus seguidores - success: - one: No processo de bloqueio suave de seguidores de outro domínio... - other: No processo de bloqueio suave de seguidores de outros %{count} domínios... - true_privacy_html: Lembre-se de que a verdadeira privacidade só pode ser alcançada através de encriptação ponto-a-ponto. - unlocked_warning_html: Qualquer pessoa pode te seguir e ver as suas postagens privadas. %{lock_link} para ser capaz de revisar e rejeitar seguidores. - unlocked_warning_title: A sua conta não está trancada footer: developers: Desenvolvedores more: Mais… @@ -782,7 +770,6 @@ pt-BR: development: Desenvolvimento edit_profile: Editar perfil export: Exportar dados - followers: Seguidores autorizados import: Importar migrate: Migração de conta notifications: Notificações diff --git a/config/locales/pt.yml b/config/locales/pt.yml index c2a7c36f0..a024d12b5 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -377,18 +377,6 @@ pt: follows: Segues mutes: Tens em silêncio storage: Armazenamento de média - followers: - domain: Domínio - explanation_html: Se queres garantir a privacidade das tuas publicações, deves ficar atento a quem te está a seguir.As tuas publicações privadas são enviadas para todas as instâncias nas que tens seguidores. Convém revisá-las e remover seguidores se achares que a tua privacidade não será respeitada pela equipa ou software destas instâncias. - followers_count: Número de seguidores - lock_link: Bloquear a tua conta - purge: Eliminar dos seguidores - success: - one: No processo de bloqueio suave de seguidores de outro domínio... - other: No processo de bloqueio suave de seguidores de outros %{count} domínios... - true_privacy_html: Por favor leva em conta que a verdadeira privacidade só pode ser alcançada através de encriptação ponto-a-ponto. - unlocked_warning_html: Qualquer pessoa pode seguir-te e ver as tuas publicações privadas. %{lock_link} para ser capaz de revisar e rejeitar seguidores. - unlocked_warning_title: A tua conta não está bloqueada generic: changes_saved_msg: Alterações guardadas! save_changes: Guardar alterações @@ -544,7 +532,6 @@ pt: development: Desenvolvimento edit_profile: Editar perfil export: Exportar dados - followers: Seguidores autorizados import: Importar migrate: Migração de conta notifications: Notificações diff --git a/config/locales/ro.yml b/config/locales/ro.yml index 82872e651..0331f002f 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -109,9 +109,3 @@ ro: title: Filtre new: title: Adaugă un filtru nou - followers: - domain: Domeniu - explanation_html: Dacă vrei să fi sigur de confidențialitatea statusurilor tale, ar trebui să fi conștient de cine te urmărește. Statusurile tale private sunt livrate către toate instanțele unde ai urmăritori. Este recomandabil să verifici și să ștergi urmăritorii în care nu ai încredere că îți vor respecta intimitatea. - followers_count: Numărul de urmăritori - lock_link: Privează contul tău - purge: Elimină de la urmăritori diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 72513e58c..ffc9471cd 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -519,20 +519,6 @@ ru: title: Фильтры new: title: Добавить фильтр - followers: - domain: Домен - explanation_html: Если Вы хотите быть уверены в приватности Ваших статусов, Вы должны иметь четкое представление о том, кто на Вас подписан. Ваши приватные статусы отправляются всем узлам, на которых у Вас есть подписчики. Рекомендуем удалить из подписчиков пользователей узлов, администрации или программному обеспечению которых Вы не доверяете. - followers_count: Количество подписчиков - lock_link: Закройте аккаунт - purge: Удалить из подписчиков - success: - few: В процессе мягкой блокировки подписчиков с %{count} доменов... - many: В процессе мягкой блокировки подписчиков с %{count} доменов... - one: В процессе мягкой блокировки подписчиков с одного домена... - other: В процессе мягкой блокировки подписчиков с %{count} доменов... - true_privacy_html: Пожалуйста, заметьте, что настоящая конфиденциальность может быть достигнута только при помощи end-to-end шифрования. - unlocked_warning_html: Кто угодно может подписаться на Вас и получить доступ к просмотру Ваших приватных статусов. %{lock_link}, чтобы получить возможность рассматривать и вручную подтверждать запросы о подписке. - unlocked_warning_title: Ваш аккаунт не закрыт для подписки footer: developers: Разработчикам more: Ещё… @@ -709,7 +695,6 @@ ru: development: Разработка edit_profile: Изменить профиль export: Экспорт данных - followers: Авторизованные подписчики import: Импорт migrate: Перенос аккаунта notifications: Уведомления diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 565b2e8a8..550fc4fe8 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -600,19 +600,6 @@ sk: title: Triedenia new: title: Pridaj nové triedenie - followers: - domain: Doména - explanation_html: Pokiaľ chceš zaručiť súkromie svojích príspevkov, musíš mať na vedomí, kto ťa sleduje. Tvoje súkromné príspevky sú doručené na každý server z ktorého ťa niekto následuje. Takže možno by si ich chcel/a skontrolovať, a odstrániť tých následovníkov, čo sú na serveroch ktorím dostatočne nedôveruješ v zmysle, že ich moderátori, alebo ich softvérové úpravy, budú tiež rešpektovať tvoje súkromie. - followers_count: Počet následovateľov - lock_link: Zamkni svoj účet - purge: Odstráň sledovateľa - success: - few: Počas utišovania sledovateľov z %{count} domén... - one: Počas utišovania sledovateľov z jednej domény... - other: Počas utišovania sledovateľov z %{count} domén... - true_privacy_html: Prosím ber na vedomie, že ozajstné súkromie sa dá dosiahnúť iba za pomoci end-to-end enkrypcie. - unlocked_warning_html: Hocikto ťa môže následovať aby mohol/a ihneď vidieť tvoje súkromné príspevky. %{lock_link} aby si mohla skontrolovať a odmietať sledovateľov. - unlocked_warning_title: Tvoj účet nieje zamknutý footer: developers: Vývojári more: Viac… @@ -818,7 +805,6 @@ sk: edit_profile: Uprav profil export: Exportovať dáta featured_tags: Popredne zvýraznené haštagy - followers: Povolení následovatelia import: Importovať migrate: Presunutie účtu notifications: Oznámenia diff --git a/config/locales/sq.yml b/config/locales/sq.yml index b29564e74..f02c994eb 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -590,18 +590,6 @@ sq: title: Filtra new: title: Shtoni filtër të ri - followers: - domain: Përkatësi - explanation_html: Nëse doni të garantoni privatësinë e gjendjeve tuaja, duhet të jeni në dijeni se cilët ju ndjekin. Gjendjet tuaja private u dërgohen krejt shërbyes ku keni ndjekës. Mund të donit t’i rishqyrtoni ato, dhe të hiqni ndjekës, nëse nuk besoni se privatësia juaj respektohet nga stafi apo software-i i këtyre shërbyesve. - followers_count: Numër ndjekësish - lock_link: Kyçeni llogarinë tuaj - purge: Hiqe nga ndjekësit - success: - one: Në përmbushje e sipër të bllokimit të butë të ndjekësve nga një përkatësi… - other: Në përmbushje e sipër të bllokimit të butë të ndjekësve nga %{count} përkatësi… - true_privacy_html: Ju lutemi, kini parasysh se privatësi e vërtetë mund të arrihet vetëm me fshehtëzim skaj-më-skaj. - unlocked_warning_html: Mund t’ju ndjekë cilido, që të shohë menjëherë gjendjet tuaja private. %{lock_link} që të jeni në gjendje të shqyrtoni dhe hidhni poshtë ndjekës. - unlocked_warning_title: Llogaria juaj s’është kyçur footer: developers: Zhvillues more: Më tepër… @@ -793,7 +781,6 @@ sq: edit_profile: Përpunoni profilin export: Eksportim të dhënash featured_tags: Hashtagë të zgjedhur - followers: Ndjekës të autorizuar import: Importo migrate: Migrim llogarie notifications: Njoftime diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index a43c639c0..a2d57ce29 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -367,20 +367,6 @@ sr-Latn: follows: Pratite mutes: Ućutkali ste storage: Multimedijalno skladište - followers: - domain: Domen - explanation_html: Ako želite da osigurate privatnost Vaših statusa, morate biti svesni ko Vas prati. Vaši privatni statusi se šalju na sve instance na kojima imate pratioce. Možda želite da ih pregledate i da uklonite one pratioce na onim instancama za koje nemate poverenja da će poštovati Vašu privatnost. - followers_count: Broj pratilaca - lock_link: Zaključajte nalog - purge: Ukloni iz pratioca - success: - few: U procesu blokiranja pratioca sa %{count} domena... - many: U procesu blokiranja pratioca sa %{count} domena... - one: U procesu blokiranja pratioca sa jednog domena... - other: U procesu blokiranja pratioca sa %{count} domena... - true_privacy_html: Zapamtite da se prava privatnost može postići samo šifrovanjem sa kraja na kraj. - unlocked_warning_html: Svako može da Vas zaprati da odmah vidi Vaše privatne statuse. %{lock_link} da biste pregledali i odbacili pratioce. - unlocked_warning_title: Vaš nalog nije zaključan generic: changes_saved_msg: Izmene uspešno sačuvane! save_changes: Snimi izmene @@ -534,7 +520,6 @@ sr-Latn: development: Razvoj edit_profile: Izmena profila export: Izvoz podataka - followers: Autorizovani pratioci import: Uvoz migrate: Prebacivanje naloga notifications: Obaveštenja diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 5f7533ee1..45a59bcb1 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -599,20 +599,6 @@ sr: title: Филтери new: title: Додај нови филтер - followers: - domain: Домен - explanation_html: Ако желите да осигурате приватност Ваших статуса, морате бити свесни ко Вас прати. Ваши приватни статуси се шаљу на све инстанце на којима имате пратиоце. Можда желите да их прегледате и да уклоните оне пратиоце на оним инстанцама за које немате поверења да ће поштовати Вашу приватност. - followers_count: Број пратилаца - lock_link: Закључајте налог - purge: Уклони из пратиоца - success: - few: У процесу блокирања пратиоца са %{count} домена... - many: У процесу блокирања пратиоца са %{count} домена... - one: У процесу блокирања пратиоца са једног домена... - other: У процесу блокирања пратиоца са %{count} домена... - true_privacy_html: Запамтите да се права приватност може постићи само шифровањем са краја на крај. - unlocked_warning_html: Свако може да Вас запрати да одмах види Ваше приватне статусе. %{lock_link} да бисте прегледали и одбацили пратиоце. - unlocked_warning_title: Ваш налог није закључан footer: developers: Програмери more: Више… @@ -803,7 +789,6 @@ sr: development: Развој edit_profile: Измена профила export: Извоз података - followers: Ауторизовани пратиоци import: Увоз migrate: Пребацивање налога notifications: Обавештења diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 7478bef6c..b0c04329a 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -433,18 +433,6 @@ sv: follows: Du följer mutes: Du tystar storage: Medialagring - followers: - domain: Domän - explanation_html: Om du vill försäkra integriteten av dina statusar måste du vara medveten om vem som följer dig. Dina privata statusar levereras till alla instanser där du har följare. Du kanske vill granska och eventuellt ta bort följare om du inte litar på att din integritet respekteras hos medarbetarna eller programvara i dessa instanser. - followers_count: Antal följare - lock_link: Lås ditt konto - purge: Ta bort från följare - success: - one: I processen med soft-blocking följare från en domän ... - other: I processen med soft-blocking följare från %{count} domäner... - true_privacy_html: Kom ihåg att sann integritet kan bara uppnås med end-to-end kryptering. - unlocked_warning_html: Vem som helst kan följa dig för att omedelbart se dina privata statusar. %{lock_link} för att kunna granska och avvisa följare. - unlocked_warning_title: Ditt konto är inte låst generic: changes_saved_msg: Ändringar sparades framgångsrikt! save_changes: Spara ändringar @@ -609,7 +597,6 @@ sv: development: Utveckling edit_profile: Redigera profil export: Exportera data - followers: Auktoriserade följare import: Import migrate: Kontoflytt notifications: Meddelanden diff --git a/config/locales/th.yml b/config/locales/th.yml index 5e9be4da7..788bf62eb 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -176,18 +176,6 @@ th: follows: คุณติดตาม mutes: คุณปิดเสียง storage: ที่เก็บสื่อ - followers: - domain: โดเมน - explanation_html: If you want to ensure the privacy of your statuses, you must be aware of who is following you. Your private statuses are delivered to all instances where you have followers. You may wish to review them, and remove followers if you do not trust your privacy to be respected by the staff or software of those instances. - followers_count: จำนวนผู้ติดตาม - lock_link: ล๊อคแอคเค๊าท์ของคุณ - purge: นำผู้ติดตามออก - success: - one: In the process of soft-blocking followers from one domain... - other: In the process of soft-blocking followers from %{count} domains... - true_privacy_html: Please mind that true privacy can only be achieved with end-to-end encryption. - unlocked_warning_html: Anyone can follow you to immediately view your private statuses. %{lock_link} to be able to review and reject followers. - unlocked_warning_title: แอคเค๊าท์ของคุณไม่ได้ล๊อค generic: changes_saved_msg: บันทึกการแก้ไขแล้ว! save_changes: บันทึกการเปลี่ยนแปลง @@ -256,7 +244,6 @@ th: back: กลับไปที่แมสโทดอน edit_profile: แก้ไขโปรไฟล์ export: นำข้อมูลออก - followers: Authorized followers import: นำเข้า preferences: Preferences settings: ตั้งค่า diff --git a/config/locales/tr.yml b/config/locales/tr.yml index d5f48ee45..14e7f34df 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -277,18 +277,6 @@ tr: follows: Takip ettikleriniz mutes: Susturduklarınız storage: Ortam deposu - followers: - domain: Domain - explanation_html: Eğer gönderilerinizin gizliliğini garanti altına almak istiyorsanız, sizi kimin takip ettiğinden emin olmak zorundasınız. Gizli gönderileriniz, takipçilerinizin olduğu bütün sunuculara iletilir. Gönderilerinizi gözden geçirmek isteyebilir, ve o sunuculardaki yazılımın veya ilgili çalışanın, gizliliğinizi suistimal edeceğinizi düşünüyorsanız, o sunucudaki takipçilerinizi silebilirsiniz. - followers_count: Takipçi sayısı - lock_link: Hesabımı kilitle - purge: Takipçilerimden çıkar - success: - one: Domaindeki takipçilerin engellenmesi sürüyor... - other: "%{count} domaindeki takipçilerin engellenmesi sürüyor..." - true_privacy_html: 'Lütfen aklınızda bulundurun: gerçek gizlilik yalnızca uçtan-uca şifreleme ile sağlanır.' - unlocked_warning_html: Herhangi bir kişi sizi takip edebilir ve paylaştığınız gizli gönderilerinizi görebilir. %{lock_link}'e tıklayarak takipçilerinizi gözden geçirebilir ve reddedebilirsiniz. - unlocked_warning_title: Hesabınız kilitlendi generic: changes_saved_msg: Değişiklikler başarıyla kaydedildi! save_changes: Değişiklikleri kaydet @@ -357,7 +345,6 @@ tr: back: Mastodon'a geri dön edit_profile: Profili düzenle export: Dışa aktar - followers: İzin verilmiş takipçiler import: İçe aktar preferences: Tercihler settings: Ayarlar diff --git a/config/locales/uk.yml b/config/locales/uk.yml index d8e2aa066..e72e2f461 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -481,16 +481,6 @@ uk: title: Фільтри new: title: Додати фільтр - followers: - domain: Домен - explanation_html: Якщо Ви хочете бути впевнені в приватності Ваших статусів, Ви повинні мати чітке уявлення про те, хто на Вас підписаний. Ваші приватні статусі відправляються усім сайтам, на яких у Вас є підписники. Рекомендуємо видалити з підписників користувачів інстанцій, адміністрації чи програмному забезпеченню яких Ви не довіряєте. - followers_count: Кількість підписників - lock_link: Закрийте акаунт - purge: Видалити з підписників - success: У процесі м'якого блокування підписників з %{count} доменів... - true_privacy_html: Будь ласка, помітьте, що справжняя конфіденційність може бути досягнена тільки за допомогою end-to-end шифрування. - unlocked_warning_html: Хто завгодно може підписатися на Вас та отримати доступ до перегляду Ваших приватних статусів. %{lock_link}, щоб отримати можливість роздивлятися та вручну підтверджувати запити щодо підписки. - unlocked_warning_title: Ваш аккаунт не закритий для підписки generic: changes_saved_msg: Зміни успішно збережені! save_changes: Зберегти зміни @@ -655,7 +645,6 @@ uk: development: Розробка edit_profile: Редагувати профіль export: Експорт даних - followers: Авторизовані підписники import: Імпорт migrate: Міграція акаунту notifications: Сповіщення diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index f91cef4a4..77cf32136 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -498,16 +498,6 @@ zh-CN: title: 过滤器 new: title: 添加新的过滤器 - followers: - domain: 域名 - explanation_html: 为保证你的嘟文的隐私安全,你应当经常检查你的关注者列表。受保护的嘟文将会发送到所有关注者所在的实例上。有些实例使用的软件代码或其管理员可能不会尊重你的隐私设置,因此你应当复查一下关注者列表,并移除那些你无法信任的关注者。 - followers_count: 关注者数量 - lock_link: 为你的帐户开启保护 - purge: 从关注者中移除 - success: 正在从 %{count} 个域名中移除关注者…… - true_privacy_html: 请始终铭记:真正的隐私只能靠端到端加密来实现! - unlocked_warning_html: 任何人都可以在关注你后立即查看非公开的嘟文。只要%{lock_link},你就可以审核并拒绝关注请求。 - unlocked_warning_title: 你的帐户未受到保护 generic: changes_saved_msg: 更改保存成功! save_changes: 保存更改 @@ -671,7 +661,6 @@ zh-CN: development: 开发 edit_profile: 更改个人资料 export: 导出 - followers: 已授权的关注者 import: 导入 migrate: 帐户迁移 notifications: 通知 diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index a2cfe56a9..7b200e91a 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -431,18 +431,6 @@ zh-HK: follows: 你所關注的用戶 mutes: 你所靜音的用戶 storage: 媒體容量大小 - followers: - domain: 網域 - explanation_html: 如果你想確保你的私隱,請留意是甚麼用戶在關注你。即使你的將文章設定「私人文章」,它仍為會被遞送至你所有關注者的服務站。如果你不信任某些用戶、或其服務站的管理者會尊重你私隱,請將他們自關注者名單私除。 - followers_count: 關注者數目 - lock_link: 將用戶轉為「私人」 - purge: 私除關注者 - success: - one: 正準備軟性阻擋 1 個網域的關注者…… - other: 正準備軟性阻擋 %{count} 個網域的關注者…… - true_privacy_html: 請謹記,唯有點對點加密方可以真正確保你的私隱。 - unlocked_warning_html: 目前任何人都可以看到你的私人文章,若%{lock_link}的話,你將可以審批關注者。 - unlocked_warning_title: 你的用戶目前為「公共」 generic: changes_saved_msg: 已成功儲存修改。 save_changes: 儲存修改 @@ -606,7 +594,6 @@ zh-HK: development: 開發 edit_profile: 修改個人資料 export: 匯出 - followers: 授權關注 import: 匯入 migrate: 帳戶遷移 notifications: 通知 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 4498eff95..d05514b83 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -436,16 +436,6 @@ zh-TW: follows: 您關注的使用者 mutes: 您靜音的使用者 storage: 儲存空間大小 - followers: - domain: 網域 - explanation_html: 為確保個人隱私,您必須知道有哪些使用者正關注你。您的私密內容會被發送到所有您有被關注的站點上。如果您不信任這些站點的管理者,您可以選擇檢查或刪除您的關注者。 - followers_count: 關注者數量 - lock_link: 將你的帳戶設定為私人 - purge: 移除關注者 - success: 正準備軟性封鎖 %{count} 個網域的關注者…… - true_privacy_html: 請謹記,唯有點對點加密方可以真正確保你的隱私。 - unlocked_warning_html: 任何人都可以在關注你後立即查看非公開的嘟文。只要%{lock_link},你就可以審核並拒絕關注請求。 - unlocked_warning_title: 你的帳戶是公開的 generic: changes_saved_msg: 已成功儲存修改! save_changes: 儲存修改 @@ -594,7 +584,6 @@ zh-TW: development: 開發 edit_profile: 編輯使用者資訊 export: 匯出 - followers: 授權關注者 import: 匯入 migrate: 帳戶搬遷 notifications: 通知 diff --git a/config/navigation.rb b/config/navigation.rb index 1be621ac2..77a300bbf 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -14,9 +14,9 @@ SimpleNavigation::Configuration.run do |navigation| settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url - settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url end + primary.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_url primary.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters} primary.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: proc { Setting.min_invite_role == 'user' } diff --git a/config/routes.rb b/config/routes.rb index 1bb875264..dc5633a68 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -106,8 +106,6 @@ Rails.application.routes.draw do resource :confirmation, only: [:new, :create] end - resource :follower_domains, only: [:show, :update] - resources :applications, except: [:edit] do member do post :regenerate @@ -129,6 +127,7 @@ Rails.application.routes.draw do resources :emojis, only: [:show] resources :invites, only: [:index, :create, :destroy] resources :filters, except: [:show] + resource :relationships, only: [:show, :update] get '/public', to: 'public_timelines#show', as: :public_timeline get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy diff --git a/spec/controllers/relationships_controller_spec.rb b/spec/controllers/relationships_controller_spec.rb new file mode 100644 index 000000000..16e255afe --- /dev/null +++ b/spec/controllers/relationships_controller_spec.rb @@ -0,0 +1,77 @@ +require 'rails_helper' + +describe RelationshipsController do + render_views + + let(:user) { Fabricate(:user) } + + shared_examples 'authenticate user' do + it 'redirects when not signed in' do + is_expected.to redirect_to '/auth/sign_in' + end + end + + describe 'GET #show' do + subject { get :show, params: { page: 2, relationship: 'followed_by' } } + + it 'assigns @accounts' do + Fabricate(:account, domain: 'old').follow!(user.account) + Fabricate(:account, domain: 'recent').follow!(user.account) + + sign_in user, scope: :user + subject + + assigned = assigns(:accounts).per(1).to_a + expect(assigned.size).to eq 1 + expect(assigned[0].domain).to eq 'old' + end + + it 'returns http success' do + sign_in user, scope: :user + subject + expect(response).to have_http_status(200) + end + + include_examples 'authenticate user' + end + + describe 'PATCH #update' do + let(:poopfeast) { Fabricate(:account, username: 'poopfeast', domain: 'example.com', salmon_url: 'http://example.com/salmon') } + + before do + stub_request(:post, 'http://example.com/salmon').to_return(status: 200) + end + + shared_examples 'redirects back to followers page' do + it 'redirects back to followers page' do + poopfeast.follow!(user.account) + + sign_in user, scope: :user + subject + + expect(response).to redirect_to(relationships_path) + end + end + + context 'when select parameter is not provided' do + subject { patch :update } + include_examples 'redirects back to followers page' + end + + context 'when select parameter is provided' do + subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, block_domains: '' } } + + it 'soft-blocks followers from selected domains' do + poopfeast.follow!(user.account) + + sign_in user, scope: :user + subject + + expect(poopfeast.following?(user.account)).to be false + end + + include_examples 'authenticate user' + include_examples 'redirects back to followers page' + end + end +end diff --git a/spec/controllers/settings/follower_domains_controller_spec.rb b/spec/controllers/settings/follower_domains_controller_spec.rb deleted file mode 100644 index 6d415a654..000000000 --- a/spec/controllers/settings/follower_domains_controller_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -require 'rails_helper' - -describe Settings::FollowerDomainsController do - render_views - - let(:user) { Fabricate(:user) } - - shared_examples 'authenticate user' do - it 'redirects when not signed in' do - is_expected.to redirect_to '/auth/sign_in' - end - end - - describe 'GET #show' do - subject { get :show, params: { page: 2 } } - - it 'assigns @account' do - sign_in user, scope: :user - subject - expect(assigns(:account)).to eq user.account - end - - it 'assigns @domains' do - Fabricate(:account, domain: 'old').follow!(user.account) - Fabricate(:account, domain: 'recent').follow!(user.account) - - sign_in user, scope: :user - subject - - assigned = assigns(:domains).per(1).to_a - expect(assigned.size).to eq 1 - expect(assigned[0].accounts_from_domain).to eq 1 - expect(assigned[0].domain).to eq 'old' - end - - it 'returns http success' do - sign_in user, scope: :user - subject - expect(response).to have_http_status(200) - end - - include_examples 'authenticate user' - end - - describe 'PATCH #update' do - let(:poopfeast) { Fabricate(:account, username: 'poopfeast', domain: 'example.com', salmon_url: 'http://example.com/salmon') } - - before do - stub_request(:post, 'http://example.com/salmon').to_return(status: 200) - end - - shared_examples 'redirects back to followers page' do |notice| - it 'redirects back to followers page' do - poopfeast.follow!(user.account) - - sign_in user, scope: :user - subject - - expect(flash[:notice]).to eq notice - expect(response).to redirect_to(settings_follower_domains_path) - end - end - - context 'when select parameter is not provided' do - subject { patch :update } - include_examples 'redirects back to followers page', 'In the process of soft-blocking followers from 0 domains...' - end - - context 'when select parameter is provided' do - subject { patch :update, params: { select: ['example.com'] } } - - it 'soft-blocks followers from selected domains' do - poopfeast.follow!(user.account) - - sign_in user, scope: :user - subject - - expect(poopfeast.following?(user.account)).to be false - end - - include_examples 'authenticate user' - include_examples 'redirects back to followers page', 'In the process of soft-blocking followers from one domain...' - end - end -end -- cgit From 5e38ef87a7b8bac59ffa0d98464086ab8a60a2e1 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 17 Mar 2019 14:54:09 +0100 Subject: Fix reblogs privacy (#10302) * Fix reblogs privacy * Fix Announce processing specs --- app/models/status.rb | 2 +- spec/lib/activitypub/activity/announce_spec.rb | 1 + spec/services/reblog_service_spec.rb | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) (limited to 'app/models') diff --git a/app/models/status.rb b/app/models/status.rb index 571167943..b9479c76b 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -453,8 +453,8 @@ class Status < ApplicationRecord end def set_visibility + self.visibility = reblog.visibility if reblog? && visibility.nil? self.visibility = (account.locked? ? :private : :public) if visibility.nil? - self.visibility = reblog.visibility if reblog? self.sensitive = false if sensitive.nil? end diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb index aa58d9e23..926083a4f 100644 --- a/spec/lib/activitypub/activity/announce_spec.rb +++ b/spec/lib/activitypub/activity/announce_spec.rb @@ -12,6 +12,7 @@ RSpec.describe ActivityPub::Activity::Announce do type: 'Announce', actor: 'https://example.com/actor', object: object_json, + to: 'http://example.com/followers', }.with_indifferent_access end diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb index 2755da772..9e66c6643 100644 --- a/spec/services/reblog_service_spec.rb +++ b/spec/services/reblog_service_spec.rb @@ -3,6 +3,27 @@ require 'rails_helper' RSpec.describe ReblogService, type: :service do let(:alice) { Fabricate(:account, username: 'alice') } + context 'creates a reblog with appropriate visibility' do + let(:bob) { Fabricate(:account, username: 'bob') } + let(:visibility) { :public } + let(:reblog_visibility) { :public } + let(:status) { Fabricate(:status, account: bob, visibility: visibility) } + + subject { ReblogService.new } + + before do + subject.call(alice, status, visibility: reblog_visibility) + end + + describe 'boosting privately' do + let(:reblog_visibility) { :private } + + it 'reblogs privately' do + expect(status.reblogs.first.visibility).to eq 'private' + end + end + end + context 'OStatus' do let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com') } let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com;something:something') } -- cgit From a20354a20b9dffada0e8d6170ebc2ff13c79baea Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 17 Mar 2019 15:34:56 +0100 Subject: Set and store report URIs (#10303) Fixes #10271 --- app/lib/activitypub/activity/flag.rb | 7 ++++++- app/models/report.rb | 11 +++++++++++ app/serializers/activitypub/flag_serializer.rb | 1 - app/services/report_service.rb | 3 ++- db/migrate/20190317135723_add_uri_to_reports.rb | 5 +++++ db/schema.rb | 3 ++- spec/lib/activitypub/activity/flag_spec.rb | 23 +++++++++++++++++++++-- spec/services/report_service_spec.rb | 5 +++++ 8 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20190317135723_add_uri_to_reports.rb (limited to 'app/models') diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb index 0d10d6c3c..f73b93058 100644 --- a/app/lib/activitypub/activity/flag.rb +++ b/app/lib/activitypub/activity/flag.rb @@ -14,7 +14,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity @account, target_account, status_ids: target_statuses.nil? ? [] : target_statuses.map(&:id), - comment: @json['content'] || '' + comment: @json['content'] || '', + uri: report_uri ) end end @@ -28,4 +29,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity def object_uris @object_uris ||= Array(@object.is_a?(Array) ? @object.map { |item| value_or_id(item) } : value_or_id(@object)) end + + def report_uri + @json['id'] unless @json['id'].nil? || invalid_origin?(@json['id']) + end end diff --git a/app/models/report.rb b/app/models/report.rb index 2804020f5..86c303798 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -13,6 +13,7 @@ # action_taken_by_account_id :bigint(8) # target_account_id :bigint(8) not null # assigned_account_id :bigint(8) +# uri :string # class Report < ApplicationRecord @@ -28,6 +29,12 @@ class Report < ApplicationRecord validates :comment, length: { maximum: 1000 } + def local? + false # Force uri_for to use uri attribute + end + + before_validation :set_uri, only: :create + def object_type :flag end @@ -89,4 +96,8 @@ class Report < ApplicationRecord Admin::ActionLog.from("(#{sql}) AS admin_action_logs") end + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local? + end end diff --git a/app/serializers/activitypub/flag_serializer.rb b/app/serializers/activitypub/flag_serializer.rb index 53e8f726d..1e7a46dd9 100644 --- a/app/serializers/activitypub/flag_serializer.rb +++ b/app/serializers/activitypub/flag_serializer.rb @@ -5,7 +5,6 @@ class ActivityPub::FlagSerializer < ActiveModel::Serializer attribute :virtual_object, key: :object def id - # This is nil for now ActivityPub::TagManager.instance.uri_for(object) end diff --git a/app/services/report_service.rb b/app/services/report_service.rb index 1bcc1c0d5..73bd6694f 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -21,7 +21,8 @@ class ReportService < BaseService @report = @source_account.reports.create!( target_account: @target_account, status_ids: @status_ids, - comment: @comment + comment: @comment, + uri: @options[:uri] ) end diff --git a/db/migrate/20190317135723_add_uri_to_reports.rb b/db/migrate/20190317135723_add_uri_to_reports.rb new file mode 100644 index 000000000..47c0f2a21 --- /dev/null +++ b/db/migrate/20190317135723_add_uri_to_reports.rb @@ -0,0 +1,5 @@ +class AddUriToReports < ActiveRecord::Migration[5.2] + def change + add_column :reports, :uri, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 23ec08238..790e347c3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_03_14_181829) do +ActiveRecord::Schema.define(version: 2019_03_17_135723) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -525,6 +525,7 @@ ActiveRecord::Schema.define(version: 2019_03_14_181829) do t.bigint "action_taken_by_account_id" t.bigint "target_account_id", null: false t.bigint "assigned_account_id" + t.string "uri" t.index ["account_id"], name: "index_reports_on_account_id" t.index ["target_account_id"], name: "index_reports_on_target_account_id" end diff --git a/spec/lib/activitypub/activity/flag_spec.rb b/spec/lib/activitypub/activity/flag_spec.rb index 3f082a813..ec7359f2f 100644 --- a/spec/lib/activitypub/activity/flag_spec.rb +++ b/spec/lib/activitypub/activity/flag_spec.rb @@ -1,14 +1,15 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Flag do - let(:sender) { Fabricate(:account, domain: 'example.com') } + let(:sender) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') } let(:flagged) { Fabricate(:account) } let(:status) { Fabricate(:status, account: flagged, uri: 'foobar') } + let(:flag_id) { nil } let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', - id: nil, + id: flag_id, type: 'Flag', content: 'Boo!!', actor: ActivityPub::TagManager.instance.uri_for(sender), @@ -34,4 +35,22 @@ RSpec.describe ActivityPub::Activity::Flag do expect(report.status_ids).to eq [status.id] end end + + describe '#perform with a defined uri' do + subject { described_class.new(json, sender) } + let (:flag_id) { 'http://example.com/reports/1' } + + before do + subject.perform + end + + it 'creates a report' do + report = Report.find_by(account: sender, target_account: flagged) + + expect(report).to_not be_nil + expect(report.comment).to eq 'Boo!!' + expect(report.status_ids).to eq [status.id] + expect(report.uri).to eq flag_id + end + end end diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb index e8b094c89..454e4d896 100644 --- a/spec/services/report_service_spec.rb +++ b/spec/services/report_service_spec.rb @@ -21,6 +21,11 @@ RSpec.describe ReportService, type: :service do subject.call(source_account, remote_account, forward: false) expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made end + + it 'has an uri' do + report = subject.call(source_account, remote_account, forward: true) + expect(report.uri).to_not be_nil + end end context 'when other reports already exist for the same target' do -- cgit From 9c4cbdbafb0324ae259e10865b90ed1ed0255bdd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 18 Mar 2019 21:00:55 +0100 Subject: Add Keybase integration (#10297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * create account_identity_proofs table * add endpoint for keybase to check local proofs * add async task to update validity and liveness of proofs from keybase * first pass keybase proof CRUD * second pass keybase proof creation * clean up proof list and add badges * add avatar url to keybase api * Always highlight the “Identity Proofs” navigation item when interacting with proofs. * Update translations. * Add profile URL. * Reorder proofs. * Add proofs to bio. * Update settings/identity_proofs front-end. * Use `link_to`. * Only encode query params if they exist. URLs without params had a trailing `?`. * Only show live proofs. * change valid to active in proof list and update liveness before displaying * minor fixes * add keybase config at well-known path * extremely naive feature flagging off the identity proof UI * fixes for rubocop * make identity proofs page resilient to potential keybase issues * normalize i18n * tweaks for brakeman * remove two unused translations * cleanup and add more localizations * make keybase_contacts an admin setting * fix ExternalProofService my_domain * use Addressable::URI in identity proofs * use active model serializer for keybase proof config * more cleanup of keybase proof config * rename proof is_valid and is_live to proof_valid and proof_live * cleanup * assorted tweaks for more robust communication with keybase * Clean up * Small fixes * Display verified identity identically to verified links * Clean up unused CSS * Add caching for Keybase avatar URLs * Remove keybase_contacts setting --- app/controllers/api/proofs_controller.rb | 30 ++++++ .../settings/identity_proofs_controller.rb | 45 +++++++++ .../well_known/keybase_proof_config_controller.rb | 9 ++ app/javascript/images/logo_transparent_black.svg | 1 + app/javascript/images/proof_providers/keybase.png | Bin 0 -> 12665 bytes app/javascript/styles/mastodon/forms.scss | 55 ++++++++++ app/lib/proof_provider.rb | 12 +++ app/lib/proof_provider/keybase.rb | 59 +++++++++++ app/lib/proof_provider/keybase/badge.rb | 48 +++++++++ .../proof_provider/keybase/config_serializer.rb | 70 +++++++++++++ app/lib/proof_provider/keybase/serializer.rb | 25 +++++ app/lib/proof_provider/keybase/verifier.rb | 62 ++++++++++++ app/lib/proof_provider/keybase/worker.rb | 33 ++++++ app/models/account_identity_proof.rb | 46 +++++++++ app/models/concerns/account_associations.rb | 3 + app/views/accounts/_bio.html.haml | 15 ++- .../settings/identity_proofs/_proof.html.haml | 20 ++++ app/views/settings/identity_proofs/index.html.haml | 17 ++++ app/views/settings/identity_proofs/new.html.haml | 31 ++++++ config/locales/en.yml | 16 +++ config/navigation.rb | 1 + config/routes.rb | 7 ++ ...0190316190352_create_account_identity_proofs.rb | 16 +++ db/schema.rb | 14 +++ spec/controllers/api/proofs_controller_spec.rb | 96 ++++++++++++++++++ .../settings/identity_proofs_controller_spec.rb | 112 +++++++++++++++++++++ .../keybase_proof_config_controller_spec.rb | 15 +++ .../account_identity_proof_fabricator.rb | 8 ++ spec/lib/proof_provider/keybase/verifier_spec.rb | 82 +++++++++++++++ 29 files changed, 946 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/proofs_controller.rb create mode 100644 app/controllers/settings/identity_proofs_controller.rb create mode 100644 app/controllers/well_known/keybase_proof_config_controller.rb create mode 100644 app/javascript/images/logo_transparent_black.svg create mode 100644 app/javascript/images/proof_providers/keybase.png create mode 100644 app/lib/proof_provider.rb create mode 100644 app/lib/proof_provider/keybase.rb create mode 100644 app/lib/proof_provider/keybase/badge.rb create mode 100644 app/lib/proof_provider/keybase/config_serializer.rb create mode 100644 app/lib/proof_provider/keybase/serializer.rb create mode 100644 app/lib/proof_provider/keybase/verifier.rb create mode 100644 app/lib/proof_provider/keybase/worker.rb create mode 100644 app/models/account_identity_proof.rb create mode 100644 app/views/settings/identity_proofs/_proof.html.haml create mode 100644 app/views/settings/identity_proofs/index.html.haml create mode 100644 app/views/settings/identity_proofs/new.html.haml create mode 100644 db/migrate/20190316190352_create_account_identity_proofs.rb create mode 100644 spec/controllers/api/proofs_controller_spec.rb create mode 100644 spec/controllers/settings/identity_proofs_controller_spec.rb create mode 100644 spec/controllers/well_known/keybase_proof_config_controller_spec.rb create mode 100644 spec/fabricators/account_identity_proof_fabricator.rb create mode 100644 spec/lib/proof_provider/keybase/verifier_spec.rb (limited to 'app/models') diff --git a/app/controllers/api/proofs_controller.rb b/app/controllers/api/proofs_controller.rb new file mode 100644 index 000000000..a84ad2014 --- /dev/null +++ b/app/controllers/api/proofs_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::ProofsController < Api::BaseController + before_action :set_account + before_action :set_provider + before_action :check_account_approval + before_action :check_account_suspension + + def index + render json: @account, serializer: @provider.serializer_class + end + + private + + def set_provider + @provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound) + end + + def set_account + @account = Account.find_local!(params[:username]) + end + + def check_account_approval + not_found if @account.user_pending? + end + + def check_account_suspension + gone if @account.suspended? + end +end diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb new file mode 100644 index 000000000..4a3b89a5e --- /dev/null +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Settings::IdentityProofsController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + before_action :check_required_params, only: :new + + def index + @proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc) + @proofs.each(&:refresh!) + end + + def new + @proof = current_account.identity_proofs.new( + token: params[:token], + provider: params[:provider], + provider_username: params[:provider_username] + ) + + render layout: 'auth' + end + + def create + @proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params) + @proof.token = resource_params[:token] + + if @proof.save + redirect_to @proof.on_success_path(params[:user_agent]) + else + flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) + redirect_to settings_identity_proofs_path + end + end + + private + + def check_required_params + redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :token].all? { |k| params[k].present? } + end + + def resource_params + params.require(:account_identity_proof).permit(:provider, :provider_username, :token) + end +end diff --git a/app/controllers/well_known/keybase_proof_config_controller.rb b/app/controllers/well_known/keybase_proof_config_controller.rb new file mode 100644 index 000000000..eb41e586f --- /dev/null +++ b/app/controllers/well_known/keybase_proof_config_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module WellKnown + class KeybaseProofConfigController < ActionController::Base + def show + render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer + end + end +end diff --git a/app/javascript/images/logo_transparent_black.svg b/app/javascript/images/logo_transparent_black.svg new file mode 100644 index 000000000..e44bcf5e1 --- /dev/null +++ b/app/javascript/images/logo_transparent_black.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/images/proof_providers/keybase.png b/app/javascript/images/proof_providers/keybase.png new file mode 100644 index 000000000..7e3ac657f Binary files /dev/null and b/app/javascript/images/proof_providers/keybase.png differ diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 6051c1d00..9ef45e425 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -801,3 +801,58 @@ code { } } } + +.connection-prompt { + margin-bottom: 25px; + + .fa-link { + background-color: darken($ui-base-color, 4%); + border-radius: 100%; + font-size: 24px; + padding: 10px; + } + + &__column { + align-items: center; + display: flex; + flex: 1; + flex-direction: column; + flex-shrink: 1; + + &-sep { + flex-grow: 0; + overflow: visible; + position: relative; + z-index: 1; + } + } + + .account__avatar { + margin-bottom: 20px; + } + + &__connection { + background-color: lighten($ui-base-color, 8%); + box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + border-radius: 4px; + padding: 25px 10px; + position: relative; + text-align: center; + + &::after { + background-color: darken($ui-base-color, 4%); + content: ''; + display: block; + height: 100%; + left: 50%; + position: absolute; + width: 1px; + } + } + + &__row { + align-items: center; + display: flex; + flex-direction: row; + } +} diff --git a/app/lib/proof_provider.rb b/app/lib/proof_provider.rb new file mode 100644 index 000000000..102c50f4f --- /dev/null +++ b/app/lib/proof_provider.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module ProofProvider + SUPPORTED_PROVIDERS = %w(keybase).freeze + + def self.find(identifier, proof = nil) + case identifier + when 'keybase' + ProofProvider::Keybase.new(proof) + end + end +end diff --git a/app/lib/proof_provider/keybase.rb b/app/lib/proof_provider/keybase.rb new file mode 100644 index 000000000..96322a265 --- /dev/null +++ b/app/lib/proof_provider/keybase.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class ProofProvider::Keybase + BASE_URL = 'https://keybase.io' + + class Error < StandardError; end + + class ExpectedProofLiveError < Error; end + + class UnexpectedResponseError < Error; end + + def initialize(proof = nil) + @proof = proof + end + + def serializer_class + ProofProvider::Keybase::Serializer + end + + def worker_class + ProofProvider::Keybase::Worker + end + + def validate! + unless @proof.token&.size == 66 + @proof.errors.add(:base, I18n.t('identity_proofs.errors.keybase.invalid_token')) + return + end + + return if @proof.provider_username.blank? + + if verifier.valid? + @proof.verified = true + @proof.live = false + else + @proof.errors.add(:base, I18n.t('identity_proofs.errors.keybase.verification_failed', kb_username: @proof.provider_username)) + end + end + + def refresh! + worker_class.new.perform(@proof) + rescue ProofProvider::Keybase::Error + nil + end + + def on_success_path(user_agent = nil) + verifier.on_success_path(user_agent) + end + + def badge + @badge ||= ProofProvider::Keybase::Badge.new(@proof.account.username, @proof.provider_username, @proof.token) + end + + private + + def verifier + @verifier ||= ProofProvider::Keybase::Verifier.new(@proof.account.username, @proof.provider_username, @proof.token) + end +end diff --git a/app/lib/proof_provider/keybase/badge.rb b/app/lib/proof_provider/keybase/badge.rb new file mode 100644 index 000000000..3aa067ecf --- /dev/null +++ b/app/lib/proof_provider/keybase/badge.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class ProofProvider::Keybase::Badge + include RoutingHelper + + def initialize(local_username, provider_username, token) + @local_username = local_username + @provider_username = provider_username + @token = token + end + + def proof_url + "#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}/sigchain\##{@token}" + end + + def profile_url + "#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}" + end + + def icon_url + "#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}/proof_badge/#{@token}?username=#{@local_username}&domain=#{domain}" + end + + def avatar_url + Rails.cache.fetch("proof_providers/keybase/#{@provider_username}/avatar_url", expires_in: 5.minutes) { remote_avatar_url } || default_avatar_url + end + + private + + def remote_avatar_url + request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/user/pic_url.json", params: { username: @provider_username }) + + request.perform do |res| + json = Oj.load(res.body_with_limit, mode: :strict) + json['pic_url'] if json.is_a?(Hash) + end + rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError + nil + end + + def default_avatar_url + asset_pack_path('media/images/proof_providers/keybase.png') + end + + def domain + Rails.configuration.x.local_domain + end +end diff --git a/app/lib/proof_provider/keybase/config_serializer.rb b/app/lib/proof_provider/keybase/config_serializer.rb new file mode 100644 index 000000000..474ea74e2 --- /dev/null +++ b/app/lib/proof_provider/keybase/config_serializer.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :version, :domain, :display_name, :username, + :brand_color, :logo, :description, :prefill_url, + :profile_url, :check_url, :check_path, :avatar_path, + :contact + + def version + 1 + end + + def domain + Rails.configuration.x.local_domain + end + + def display_name + Setting.site_title + end + + def logo + { svg_black: full_asset_url(asset_pack_path('media/images/logo_transparent_black.svg')), svg_full: full_asset_url(asset_pack_path('media/images/logo.svg')) } + end + + def brand_color + '#282c37' + end + + def description + Setting.site_short_description.presence || Setting.site_description.presence || I18n.t('about.about_mastodon_html') + end + + def username + { min: 1, max: 30, re: Account::USERNAME_RE.inspect } + end + + def prefill_url + params = { + provider: 'keybase', + token: '%{sig_hash}', + provider_username: '%{kb_username}', + username: '%{username}', + user_agent: '%{kb_ua}', + } + + CGI.unescape(new_settings_identity_proof_url(params)) + end + + def profile_url + CGI.unescape(short_account_url('%{username}')) # rubocop:disable Style/FormatStringToken + end + + def check_url + CGI.unescape(api_proofs_url(username: '%{username}', provider: 'keybase')) + end + + def check_path + ['signatures'] + end + + def avatar_path + ['avatar'] + end + + def contact + [Setting.site_contact_email.presence].compact + end +end diff --git a/app/lib/proof_provider/keybase/serializer.rb b/app/lib/proof_provider/keybase/serializer.rb new file mode 100644 index 000000000..d29283600 --- /dev/null +++ b/app/lib/proof_provider/keybase/serializer.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class ProofProvider::Keybase::Serializer < ActiveModel::Serializer + include RoutingHelper + + attribute :avatar + + has_many :identity_proofs, key: :signatures + + def avatar + full_asset_url(object.avatar_original_url) + end + + class AccountIdentityProofSerializer < ActiveModel::Serializer + attributes :sig_hash, :kb_username + + def sig_hash + object.token + end + + def kb_username + object.provider_username + end + end +end diff --git a/app/lib/proof_provider/keybase/verifier.rb b/app/lib/proof_provider/keybase/verifier.rb new file mode 100644 index 000000000..86f249dd7 --- /dev/null +++ b/app/lib/proof_provider/keybase/verifier.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class ProofProvider::Keybase::Verifier + def initialize(local_username, provider_username, token) + @local_username = local_username + @provider_username = provider_username + @token = token + end + + def valid? + request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/sig/proof_valid.json", params: query_params) + + request.perform do |res| + json = Oj.load(res.body_with_limit, mode: :strict) + + if json.is_a?(Hash) + json.fetch('proof_valid', false) + else + false + end + end + rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError + false + end + + def on_success_path(user_agent = nil) + url = Addressable::URI.parse("#{ProofProvider::Keybase::BASE_URL}/_/proof_creation_success") + url.query_values = query_params.merge(kb_ua: user_agent || 'unknown') + url.to_s + end + + def status + request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/sig/proof_live.json", params: query_params) + + request.perform do |res| + raise ProofProvider::Keybase::UnexpectedResponseError unless res.code == 200 + + json = Oj.load(res.body_with_limit, mode: :strict) + + raise ProofProvider::Keybase::UnexpectedResponseError unless json.is_a?(Hash) && json.key?('proof_valid') && json.key?('proof_live') + + json + end + rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError + raise ProofProvider::Keybase::UnexpectedResponseError + end + + private + + def query_params + { + domain: domain, + kb_username: @provider_username, + username: @local_username, + sig_hash: @token, + } + end + + def domain + Rails.configuration.x.local_domain + end +end diff --git a/app/lib/proof_provider/keybase/worker.rb b/app/lib/proof_provider/keybase/worker.rb new file mode 100644 index 000000000..2872f59c1 --- /dev/null +++ b/app/lib/proof_provider/keybase/worker.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class ProofProvider::Keybase::Worker + include Sidekiq::Worker + + sidekiq_options queue: 'pull', retry: 20, unique: :until_executed + + sidekiq_retry_in do |count, exception| + # Retry aggressively when the proof is valid but not live in Keybase. + # This is likely because Keybase just hasn't noticed the proof being + # served from here yet. + + if exception.class == ProofProvider::Keybase::ExpectedProofLiveError + case count + when 0..2 then 0.seconds + when 2..6 then 1.second + end + end + end + + def perform(proof_id) + proof = proof_id.is_a?(AccountIdentityProof) ? proof_id : AccountIdentityProof.find(proof_id) + verifier = ProofProvider::Keybase::Verifier.new(proof.account.username, proof.provider_username, proof.token) + status = verifier.status + + # If Keybase thinks the proof is valid, and it exists here in Mastodon, + # then it should be live. Keybase just has to notice that it's here + # and then update its state. That might take a couple seconds. + raise ProofProvider::Keybase::ExpectedProofLiveError if status['proof_valid'] && !status['proof_live'] + + proof.update!(verified: status['proof_valid'], live: status['proof_live']) + end +end diff --git a/app/models/account_identity_proof.rb b/app/models/account_identity_proof.rb new file mode 100644 index 000000000..e7a3f97e5 --- /dev/null +++ b/app/models/account_identity_proof.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: account_identity_proofs +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# provider :string default(""), not null +# provider_username :string default(""), not null +# token :text default(""), not null +# verified :boolean default(FALSE), not null +# live :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class AccountIdentityProof < ApplicationRecord + belongs_to :account + + validates :provider, inclusion: { in: ProofProvider::SUPPORTED_PROVIDERS } + validates :provider_username, format: { with: /\A[a-z0-9_]+\z/i }, length: { minimum: 2, maximum: 15 } + validates :provider_username, uniqueness: { scope: [:account_id, :provider] } + validates :token, format: { with: /\A[a-f0-9]+\z/ }, length: { maximum: 66 } + + validate :validate_with_provider, if: :token_changed? + + scope :active, -> { where(verified: true, live: true) } + + after_create_commit :queue_worker + + delegate :refresh!, :on_success_path, :badge, to: :provider_instance + + private + + def provider_instance + @provider_instance ||= ProofProvider.find(provider, self) + end + + def queue_worker + provider_instance.worker_class.perform_async(id) + end + + def validate_with_provider + provider_instance.validate! + end +end diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index a8ba8fef1..70855e054 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -7,6 +7,9 @@ module AccountAssociations # Local users has_one :user, inverse_of: :account, dependent: :destroy + # Identity proofs + has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account + # Timelines has_many :stream_entries, inverse_of: :account, dependent: :destroy has_many :statuses, inverse_of: :account, dependent: :destroy diff --git a/app/views/accounts/_bio.html.haml b/app/views/accounts/_bio.html.haml index 2ea34a048..efc26d136 100644 --- a/app/views/accounts/_bio.html.haml +++ b/app/views/accounts/_bio.html.haml @@ -1,7 +1,17 @@ +- proofs = account.identity_proofs.active +- fields = account.fields + .public-account-bio - - unless account.fields.empty? + - unless fields.empty? && proofs.empty? .account__header__fields - - account.fields.each do |field| + - proofs.each do |proof| + %dl + %dt= proof.provider.capitalize + %dd.verified + = link_to fa_icon('check'), proof.badge.proof_url, class: 'verified__mark', title: t('accounts.link_verified_on', date: l(proof.updated_at)) + = link_to proof.provider_username, proof.badge.profile_url + + - fields.each do |field| %dl %dt.emojify{ title: field.name }= Formatter.instance.format_field(account, field.name, custom_emojify: true) %dd{ title: field.value, class: custom_field_classes(field) } @@ -9,6 +19,7 @@ %span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) } = fa_icon 'check' = Formatter.instance.format_field(account, field.value, custom_emojify: true) + = account_badge(account) - if account.note.present? diff --git a/app/views/settings/identity_proofs/_proof.html.haml b/app/views/settings/identity_proofs/_proof.html.haml new file mode 100644 index 000000000..524827ad7 --- /dev/null +++ b/app/views/settings/identity_proofs/_proof.html.haml @@ -0,0 +1,20 @@ +%tr + %td + = link_to proof.badge.profile_url, class: 'name-tag' do + = image_tag proof.badge.avatar_url, width: 15, height: 15, alt: '', class: 'avatar' + %span.username + = proof.provider_username + %span= "(#{proof.provider.capitalize})" + + %td + - if proof.live? + %span.positive-hint + = fa_icon 'check-circle fw' + = t('identity_proofs.active') + - else + %span.negative-hint + = fa_icon 'times-circle fw' + = t('identity_proofs.inactive') + + %td + = table_link_to 'external-link', t('identity_proofs.view_proof'), proof.badge.proof_url if proof.badge.proof_url diff --git a/app/views/settings/identity_proofs/index.html.haml b/app/views/settings/identity_proofs/index.html.haml new file mode 100644 index 000000000..d0ea03ecd --- /dev/null +++ b/app/views/settings/identity_proofs/index.html.haml @@ -0,0 +1,17 @@ +- content_for :page_title do + = t('settings.identity_proofs') + +%p= t('identity_proofs.explanation_html') + +- unless @proofs.empty? + %hr.spacer/ + + .table-wrapper + %table.table + %thead + %tr + %th= t('identity_proofs.identity') + %th= t('identity_proofs.status') + %th + %tbody + = render partial: 'settings/identity_proofs/proof', collection: @proofs, as: :proof diff --git a/app/views/settings/identity_proofs/new.html.haml b/app/views/settings/identity_proofs/new.html.haml new file mode 100644 index 000000000..8ce6e61c9 --- /dev/null +++ b/app/views/settings/identity_proofs/new.html.haml @@ -0,0 +1,31 @@ +- content_for :page_title do + = t('identity_proofs.authorize_connection_prompt') + +.form-container + .oauth-prompt + %h2= t('identity_proofs.authorize_connection_prompt') + + = simple_form_for @proof, url: settings_identity_proofs_url, html: { method: :post } do |f| + = f.input :provider, as: :hidden + = f.input :provider_username, as: :hidden + = f.input :token, as: :hidden + + = hidden_field_tag :user_agent, params[:user_agent] + + .connection-prompt + .connection-prompt__row.connection-prompt__connection + .connection-prompt__column + = image_tag current_account.avatar.url(:original), size: 96, class: 'account__avatar' + + %p= t('identity_proofs.i_am_html', username: content_tag(:strong,current_account.username), service: site_hostname) + + .connection-prompt__column.connection-prompt__column-sep + = fa_icon 'link' + + .connection-prompt__column + = image_tag @proof.badge.avatar_url, size: 96, class: 'account__avatar' + + %p= t('identity_proofs.i_am_html', username: content_tag(:strong, @proof.provider_username), service: @proof.provider.capitalize) + + = f.button :button, t('identity_proofs.authorize'), type: :submit + = link_to t('simple_form.no'), settings_identity_proofs_url, class: 'button negative' diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f1081fb8..ba42e7ce1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -633,6 +633,21 @@ en: validation_errors: one: Something isn't quite right yet! Please review the error below other: Something isn't quite right yet! Please review %{count} errors below + identity_proofs: + active: Active + authorize: Yes, authorize + authorize_connection_prompt: Authorize this cryptographic connection? + errors: + failed: The cryptographic connection failed. Please try again from %{provider}. + keybase: + invalid_token: Keybase tokens are hashes of signatures and must be 66 hex characters + verification_failed: Keybase does not recognize this token as a signature of Keybase user %{kb_username}. Please retry from Keybase. + explanation_html: Here you can cryptographically connect your other identities, such as a Keybase profile. This lets other people send you encrypted messages and trust content you send them. + i_am_html: I am %{username} on %{service}. + identity: Identity + inactive: Inactive + status: Verification status + view_proof: View proof imports: modes: merge: Merge @@ -835,6 +850,7 @@ en: edit_profile: Edit profile export: Data export featured_tags: Featured hashtags + identity_proofs: Identity proofs import: Import migrate: Account migration notifications: Notifications diff --git a/config/navigation.rb b/config/navigation.rb index 77a300bbf..07aec4b9d 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -14,6 +14,7 @@ SimpleNavigation::Configuration.run do |navigation| settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url + settings.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*} end primary.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_url diff --git a/config/routes.rb b/config/routes.rb index dc5633a68..194b4c09b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,8 @@ Rails.application.routes.draw do get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' } get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger get '.well-known/change-password', to: redirect('/auth/edit') + get '.well-known/keybase-proof-config', to: 'well_known/keybase_proof_config#show' + get 'manifest', to: 'manifests#show', defaults: { format: 'json' } get 'intent', to: 'intents#show' get 'custom.css', to: 'custom_css#show', as: :custom_css @@ -106,6 +108,8 @@ Rails.application.routes.draw do resource :confirmation, only: [:new, :create] end + resources :identity_proofs, only: [:index, :show, :new, :create, :update] + resources :applications, except: [:edit] do member do post :regenerate @@ -248,6 +252,9 @@ Rails.application.routes.draw do # OEmbed get '/oembed', to: 'oembed#show', as: :oembed + # Identity proofs + get :proofs, to: 'proofs#index' + # JSON / REST API namespace :v1 do resources :statuses, only: [:create, :show, :destroy] do diff --git a/db/migrate/20190316190352_create_account_identity_proofs.rb b/db/migrate/20190316190352_create_account_identity_proofs.rb new file mode 100644 index 000000000..ddcbce3f3 --- /dev/null +++ b/db/migrate/20190316190352_create_account_identity_proofs.rb @@ -0,0 +1,16 @@ +class CreateAccountIdentityProofs < ActiveRecord::Migration[5.2] + def change + create_table :account_identity_proofs do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade } + t.string :provider, null: false, default: '' + t.string :provider_username, null: false, default: '' + t.text :token, null: false, default: '' + t.boolean :verified, null: false, default: false + t.boolean :live, null: false, default: false + + t.timestamps null: false + end + + add_index :account_identity_proofs, [:account_id, :provider, :provider_username], unique: true, name: :index_account_proofs_on_account_and_provider_and_username + end +end diff --git a/db/schema.rb b/db/schema.rb index 790e347c3..11535d867 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -36,6 +36,19 @@ ActiveRecord::Schema.define(version: 2019_03_17_135723) do t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true end + create_table "account_identity_proofs", force: :cascade do |t| + t.bigint "account_id" + t.string "provider", default: "", null: false + t.string "provider_username", default: "", null: false + t.text "token", default: "", null: false + t.boolean "verified", default: false, null: false + t.boolean "live", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "provider", "provider_username"], name: "index_account_proofs_on_account_and_provider_and_username", unique: true + t.index ["account_id"], name: "index_account_identity_proofs_on_account_id" + end + create_table "account_moderation_notes", force: :cascade do |t| t.text "content", null: false t.bigint "account_id", null: false @@ -732,6 +745,7 @@ ActiveRecord::Schema.define(version: 2019_03_17_135723) do add_foreign_key "account_conversations", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "conversations", on_delete: :cascade add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade + add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade add_foreign_key "account_moderation_notes", "accounts" add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id" add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade diff --git a/spec/controllers/api/proofs_controller_spec.rb b/spec/controllers/api/proofs_controller_spec.rb new file mode 100644 index 000000000..dbde4927f --- /dev/null +++ b/spec/controllers/api/proofs_controller_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +describe Api::ProofsController do + let(:alice) { Fabricate(:account, username: 'alice') } + + before do + stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_valid.json?domain=cb6e6126.ngrok.io&kb_username=crypto_alice&sig_hash=111111111111111111111111111111111111111111111111111111111111111111&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":false}') + stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_live.json?domain=cb6e6126.ngrok.io&kb_username=crypto_alice&sig_hash=111111111111111111111111111111111111111111111111111111111111111111&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":true}') + stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_valid.json?domain=cb6e6126.ngrok.io&kb_username=hidden_alice&sig_hash=222222222222222222222222222222222222222222222222222222222222222222&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":true}') + stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_live.json?domain=cb6e6126.ngrok.io&kb_username=hidden_alice&sig_hash=222222222222222222222222222222222222222222222222222222222222222222&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":true}') + end + + describe 'GET #index' do + describe 'with a non-existent username' do + it '404s' do + get :index, params: { username: 'nonexistent', provider: 'keybase' } + + expect(response).to have_http_status(:not_found) + end + end + + describe 'with a user that has no proofs' do + it 'is an empty list of signatures' do + get :index, params: { username: alice.username, provider: 'keybase' } + + expect(body_as_json[:signatures]).to eq [] + end + end + + describe 'with a user that has a live, valid proof' do + let(:token1) { '111111111111111111111111111111111111111111111111111111111111111111' } + let(:kb_name1) { 'crypto_alice' } + + before do + Fabricate(:account_identity_proof, account: alice, verified: true, live: true, token: token1, provider_username: kb_name1) + end + + it 'is a list with that proof in it' do + get :index, params: { username: alice.username, provider: 'keybase' } + + expect(body_as_json[:signatures]).to eq [ + { kb_username: kb_name1, sig_hash: token1 }, + ] + end + + describe 'add one that is neither live nor valid' do + let(:token2) { '222222222222222222222222222222222222222222222222222222222222222222' } + let(:kb_name2) { 'hidden_alice' } + + before do + Fabricate(:account_identity_proof, account: alice, verified: false, live: false, token: token2, provider_username: kb_name2) + end + + it 'is a list with both proofs' do + get :index, params: { username: alice.username, provider: 'keybase' } + + expect(body_as_json[:signatures]).to eq [ + { kb_username: kb_name1, sig_hash: token1 }, + { kb_username: kb_name2, sig_hash: token2 }, + ] + end + end + end + + describe 'a user that has an avatar' do + let(:alice) { Fabricate(:account, username: 'alice', avatar: attachment_fixture('avatar.gif')) } + + context 'and a proof' do + let(:token1) { '111111111111111111111111111111111111111111111111111111111111111111' } + let(:kb_name1) { 'crypto_alice' } + + before do + Fabricate(:account_identity_proof, account: alice, verified: true, live: true, token: token1, provider_username: kb_name1) + get :index, params: { username: alice.username, provider: 'keybase' } + end + + it 'has two keys: signatures and avatar' do + expect(body_as_json.keys).to match_array [:signatures, :avatar] + end + + it 'has the correct signatures' do + expect(body_as_json[:signatures]).to eq [ + { kb_username: kb_name1, sig_hash: token1 }, + ] + end + + it 'has the correct avatar url' do + first_part = 'https://cb6e6126.ngrok.io/system/accounts/avatars/' + last_part = 'original/avatar.gif' + + expect(body_as_json[:avatar]).to match /#{Regexp.quote(first_part)}(?:\d{3,5}\/){3}#{Regexp.quote(last_part)}/ + end + end + end + end +end diff --git a/spec/controllers/settings/identity_proofs_controller_spec.rb b/spec/controllers/settings/identity_proofs_controller_spec.rb new file mode 100644 index 000000000..46af3ccf4 --- /dev/null +++ b/spec/controllers/settings/identity_proofs_controller_spec.rb @@ -0,0 +1,112 @@ +require 'rails_helper' + +describe Settings::IdentityProofsController do + render_views + + let(:user) { Fabricate(:user) } + let(:valid_token) { '1'*66 } + let(:kbname) { 'kbuser' } + let(:provider) { 'keybase' } + let(:findable_id) { Faker::Number.number(5) } + let(:unfindable_id) { Faker::Number.number(5) } + let(:postable_params) do + { account_identity_proof: { provider: provider, provider_username: kbname, token: valid_token } } + end + + before do + allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:status) { { 'proof_valid' => true, 'proof_live' => true } } + sign_in user, scope: :user + end + + describe 'new proof creation' do + context 'GET #new with no existing proofs' do + it 'redirects to :index' do + get :new + expect(response).to redirect_to settings_identity_proofs_path + end + end + + context 'POST #create' do + context 'when saving works' do + before do + allow(ProofProvider::Keybase::Worker).to receive(:perform_async) + allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true } + allow_any_instance_of(AccountIdentityProof).to receive(:on_success_path) { root_url } + end + + it 'serializes a ProofProvider::Keybase::Worker' do + expect(ProofProvider::Keybase::Worker).to receive(:perform_async) + post :create, params: postable_params + end + + it 'delegates redirection to the proof provider' do + expect_any_instance_of(AccountIdentityProof).to receive(:on_success_path) + post :create, params: postable_params + expect(response).to redirect_to root_url + end + end + + context 'when saving fails' do + before do + allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { false } + end + + it 'redirects to :index' do + post :create, params: postable_params + expect(response).to redirect_to settings_identity_proofs_path + end + + it 'flashes a helpful message' do + post :create, params: postable_params + expect(flash[:alert]).to eq I18n.t('identity_proofs.errors.failed', provider: 'Keybase') + end + end + + context 'it can also do an update if the provider and username match an existing proof' do + before do + allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true } + allow(ProofProvider::Keybase::Worker).to receive(:perform_async) + Fabricate(:account_identity_proof, account: user.account, provider: provider, provider_username: kbname) + allow_any_instance_of(AccountIdentityProof).to receive(:on_success_path) { root_url } + end + + it 'calls update with the new token' do + expect_any_instance_of(AccountIdentityProof).to receive(:save) do |proof| + expect(proof.token).to eq valid_token + end + + post :create, params: postable_params + end + end + end + end + + describe 'GET #index' do + context 'with no existing proofs' do + it 'shows the helpful explanation' do + get :index + expect(response.body).to match I18n.t('identity_proofs.explanation_html') + end + end + + context 'with two proofs' do + before do + allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true } + @proof1 = Fabricate(:account_identity_proof, account: user.account) + @proof2 = Fabricate(:account_identity_proof, account: user.account) + allow_any_instance_of(AccountIdentityProof).to receive(:badge) { double(avatar_url: '', profile_url: '', proof_url: '') } + allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) { } + end + + it 'has the first proof username on the page' do + get :index + expect(response.body).to match /#{Regexp.quote(@proof1.provider_username)}/ + end + + it 'has the second proof username on the page' do + get :index + expect(response.body).to match /#{Regexp.quote(@proof2.provider_username)}/ + end + end + end +end diff --git a/spec/controllers/well_known/keybase_proof_config_controller_spec.rb b/spec/controllers/well_known/keybase_proof_config_controller_spec.rb new file mode 100644 index 000000000..9067e676d --- /dev/null +++ b/spec/controllers/well_known/keybase_proof_config_controller_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +describe WellKnown::KeybaseProofConfigController, type: :controller do + render_views + + describe 'GET #show' do + it 'renders json' do + get :show + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'application/json' + expect { JSON.parse(response.body) }.not_to raise_exception + end + end +end diff --git a/spec/fabricators/account_identity_proof_fabricator.rb b/spec/fabricators/account_identity_proof_fabricator.rb new file mode 100644 index 000000000..94f40dfd6 --- /dev/null +++ b/spec/fabricators/account_identity_proof_fabricator.rb @@ -0,0 +1,8 @@ +Fabricator(:account_identity_proof) do + account + provider 'keybase' + provider_username { sequence(:provider_username) { |i| "#{Faker::Lorem.characters(15)}" } } + token { sequence(:token) { |i| "#{i}#{Faker::Crypto.sha1()*2}"[0..65] } } + verified false + live false +end diff --git a/spec/lib/proof_provider/keybase/verifier_spec.rb b/spec/lib/proof_provider/keybase/verifier_spec.rb new file mode 100644 index 000000000..4ce67da9c --- /dev/null +++ b/spec/lib/proof_provider/keybase/verifier_spec.rb @@ -0,0 +1,82 @@ +require 'rails_helper' + +describe ProofProvider::Keybase::Verifier do + let(:my_domain) { Rails.configuration.x.local_domain } + + let(:keybase_proof) do + local_proof = AccountIdentityProof.new( + provider: 'Keybase', + provider_username: 'cryptoalice', + token: '11111111111111111111111111' + ) + + described_class.new('alice', 'cryptoalice', '11111111111111111111111111') + end + + let(:query_params) do + "domain=#{my_domain}&kb_username=cryptoalice&sig_hash=11111111111111111111111111&username=alice" + end + + describe '#valid?' do + let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_valid.json' } + + context 'when valid' do + before do + json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":true}' + stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) + end + + it 'calls out to keybase and returns true' do + expect(keybase_proof.valid?).to eq true + end + end + + context 'when invalid' do + before do + json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":false}' + stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) + end + + it 'calls out to keybase and returns false' do + expect(keybase_proof.valid?).to eq false + end + end + + context 'with an unexpected api response' do + before do + json_response_body = '{"status":{"code":100,"desc":"wrong size hex_id","fields":{"sig_hash":"wrong size hex_id"},"name":"INPUT_ERROR"}}' + stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) + end + + it 'swallows the error and returns false' do + expect(keybase_proof.valid?).to eq false + end + end + end + + describe '#status' do + let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_live.json' } + + context 'with a normal response' do + before do + json_response_body = '{"status":{"code":0,"name":"OK"},"proof_live":false,"proof_valid":true}' + stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) + end + + it 'calls out to keybase and returns the status fields as proof_valid and proof_live' do + expect(keybase_proof.status).to include({ 'proof_valid' => true, 'proof_live' => false }) + end + end + + context 'with an unexpected keybase response' do + before do + json_response_body = '{"status":{"code":100,"desc":"missing non-optional field sig_hash","fields":{"sig_hash":"missing non-optional field sig_hash"},"name":"INPUT_ERROR"}}' + stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body) + end + + it 'raises a ProofProvider::Keybase::UnexpectedResponseError' do + expect { keybase_proof.status }.to raise_error ProofProvider::Keybase::UnexpectedResponseError + end + end + end +end -- cgit From 80f0910e2141b24082b9143266a9a6cf1ef6a516 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 20 Mar 2019 17:29:12 +0100 Subject: Add support for custom emojis in poll options (#10322) * Backend changes for custom emoji support in poll options * Serialize poll emojis in REST API * Render custom emojis in poll options * Render custom emoji in poll options on public pages --- app/javascript/mastodon/actions/importer/normalizer.js | 4 +++- app/javascript/mastodon/components/poll.js | 13 ++++++++++++- app/lib/formatter.rb | 6 ++++++ app/models/poll.rb | 4 ++++ app/models/status.rb | 6 +++++- app/serializers/rest/poll_serializer.rb | 1 + app/views/stream_entries/_detailed_status.html.haml | 2 +- app/views/stream_entries/_poll.html.haml | 4 ++-- app/views/stream_entries/_simple_status.html.haml | 2 +- 9 files changed, 35 insertions(+), 7 deletions(-) (limited to 'app/models') diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index ea80c0efb..5badb0c49 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -71,9 +71,11 @@ export function normalizeStatus(status, normalOldStatus) { export function normalizePoll(poll) { const normalPoll = { ...poll }; + const emojiMap = makeEmojiMap(normalPoll); + normalPoll.options = poll.options.map(option => ({ ...option, - title_emojified: emojify(escapeTextContentForBrowser(option.title)), + title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), })); return normalPoll; diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index a1b297ce7..56331cb29 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -44,6 +44,11 @@ const timeRemainingString = (intl, date, now) => { return relativeTime; }; +const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { + obj[`:${emoji.get('shortcode')}:`] = emoji.toJS(); + return obj; +}, {}); + export default @injectIntl class Poll extends ImmutablePureComponent { @@ -99,6 +104,12 @@ class Poll extends ImmutablePureComponent { const active = !!this.state.selected[`${optionIndex}`]; const showResults = poll.get('voted') || poll.get('expired'); + let titleEmojified = option.get('title_emojified'); + if (!titleEmojified) { + const emojiMap = makeEmojiMap(poll); + titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap); + } + return (
  • {showResults && ( @@ -122,7 +133,7 @@ class Poll extends ImmutablePureComponent { {!showResults && } {showResults && {Math.round(percent)}%} - +
  • ); diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 464e1ee7e..aadf03b2a 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -71,6 +71,12 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end + def format_poll_option(status, option, **options) + html = encode(option.title) + html = encode_custom_emojis(html, status.emojis, options[:autoplay]) + html.html_safe # rubocop:disable Rails/OutputSafety + end + def format_display_name(account, **options) html = encode(account.display_name.presence || account.username) html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify] diff --git a/app/models/poll.rb b/app/models/poll.rb index 6df230337..8f72c7b11 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -60,6 +60,10 @@ class Poll < ApplicationRecord !local? end + def emojis + @emojis ||= CustomEmoji.from_text(options.join(' '), account.domain) + end + class Option < ActiveModelSerializers::Model attributes :id, :title, :votes_count, :poll diff --git a/app/models/status.rb b/app/models/status.rb index b9479c76b..d3fb83cca 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -213,7 +213,11 @@ class Status < ApplicationRecord end def emojis - @emojis ||= CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain) + return @emojis if defined?(@emojis) + fields = [spoiler_text, text] + fields += owned_poll.options unless owned_poll.nil? + @emojis = CustomEmoji.from_text(fields.join(' '), account.domain) + @emojis end def mark_for_mass_destruction! diff --git a/app/serializers/rest/poll_serializer.rb b/app/serializers/rest/poll_serializer.rb index 4dae1c09f..356c45b83 100644 --- a/app/serializers/rest/poll_serializer.rb +++ b/app/serializers/rest/poll_serializer.rb @@ -5,6 +5,7 @@ class REST::PollSerializer < ActiveModel::Serializer :multiple, :votes_count has_many :loaded_options, key: :options + has_many :emojis, serializer: REST::CustomEmojiSerializer attribute :voted, if: :current_user? diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index b19d2452a..d18ecd37a 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -24,7 +24,7 @@ - if status.poll = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do - = render partial: 'stream_entries/poll', locals: { poll: status.poll } + = render partial: 'stream_entries/poll', locals: { status: status, poll: status.poll, autoplay: autoplay } - elsif !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first diff --git a/app/views/stream_entries/_poll.html.haml b/app/views/stream_entries/_poll.html.haml index d6b2c0cd9..ba34890df 100644 --- a/app/views/stream_entries/_poll.html.haml +++ b/app/views/stream_entries/_poll.html.haml @@ -10,11 +10,11 @@ %label.poll__text>< %span.poll__number= percent.round - = option.title + = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) - else %label.poll__text>< %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}>< - = option.title + = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) .poll__footer - unless show_results %button.button.button-secondary{ disabled: true } diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index 68e48edbb..a499a8634 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -28,7 +28,7 @@ - if status.poll = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do - = render partial: 'stream_entries/poll', locals: { poll: status.poll } + = render partial: 'stream_entries/poll', locals: { status: status, poll: status.poll, autoplay: autoplay } - elsif !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first -- cgit