Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dalite 2024-01-24 10:52:32 +01:00
commit d5769ec3e9
175 changed files with 2414 additions and 964 deletions

View file

@ -5,7 +5,7 @@
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": { "features": {
"ghcr.io/devcontainers/features/sshd:1": {} "ghcr.io/devcontainers/features/sshd:1": {},
}, },
"runServices": ["app", "db", "redis"], "runServices": ["app", "db", "redis"],
@ -15,16 +15,16 @@
"portsAttributes": { "portsAttributes": {
"3000": { "3000": {
"label": "web", "label": "web",
"onAutoForward": "notify" "onAutoForward": "notify",
}, },
"4000": { "4000": {
"label": "stream", "label": "stream",
"onAutoForward": "silent" "onAutoForward": "silent",
} },
}, },
"otherPortsAttributes": { "otherPortsAttributes": {
"onAutoForward": "silent" "onAutoForward": "silent",
}, },
"remoteEnv": { "remoteEnv": {
@ -33,7 +33,7 @@
"STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev", "STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev",
"DISABLE_FORGERY_REQUEST_PROTECTION": "true", "DISABLE_FORGERY_REQUEST_PROTECTION": "true",
"ES_ENABLED": "", "ES_ENABLED": "",
"LIBRE_TRANSLATE_ENDPOINT": "" "LIBRE_TRANSLATE_ENDPOINT": "",
}, },
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
@ -43,7 +43,7 @@
"customizations": { "customizations": {
"vscode": { "vscode": {
"settings": {}, "settings": {},
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"],
} },
} },
} }

View file

@ -5,7 +5,7 @@
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": { "features": {
"ghcr.io/devcontainers/features/sshd:1": {} "ghcr.io/devcontainers/features/sshd:1": {},
}, },
"forwardPorts": [3000, 4000], "forwardPorts": [3000, 4000],
@ -14,17 +14,17 @@
"3000": { "3000": {
"label": "web", "label": "web",
"onAutoForward": "notify", "onAutoForward": "notify",
"requireLocalPort": true "requireLocalPort": true,
}, },
"4000": { "4000": {
"label": "stream", "label": "stream",
"onAutoForward": "silent", "onAutoForward": "silent",
"requireLocalPort": true "requireLocalPort": true,
} },
}, },
"otherPortsAttributes": { "otherPortsAttributes": {
"onAutoForward": "silent" "onAutoForward": "silent",
}, },
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
@ -34,7 +34,7 @@
"customizations": { "customizations": {
"vscode": { "vscode": {
"settings": {}, "settings": {},
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"],
} },
} },
} }

View file

@ -80,23 +80,13 @@ Rails/WhereExists:
- 'app/lib/activitypub/activity/create.rb' - 'app/lib/activitypub/activity/create.rb'
- 'app/lib/delivery_failure_tracker.rb' - 'app/lib/delivery_failure_tracker.rb'
- 'app/lib/feed_manager.rb' - 'app/lib/feed_manager.rb'
- 'app/lib/status_cache_hydrator.rb'
- 'app/lib/suspicious_sign_in_detector.rb' - 'app/lib/suspicious_sign_in_detector.rb'
- 'app/models/concerns/account/interactions.rb'
- 'app/models/featured_tag.rb'
- 'app/models/poll.rb' - 'app/models/poll.rb'
- 'app/models/session_activation.rb' - 'app/models/session_activation.rb'
- 'app/models/status.rb' - 'app/models/status.rb'
- 'app/models/user.rb'
- 'app/policies/status_policy.rb' - 'app/policies/status_policy.rb'
- 'app/serializers/rest/announcement_serializer.rb' - 'app/serializers/rest/announcement_serializer.rb'
- 'app/serializers/rest/tag_serializer.rb'
- 'app/services/activitypub/fetch_remote_status_service.rb'
- 'app/services/vote_service.rb'
- 'app/validators/reaction_validator.rb'
- 'app/validators/vote_validator.rb'
- 'app/workers/move_worker.rb' - 'app/workers/move_worker.rb'
- 'lib/tasks/tests.rake'
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'
- 'spec/services/activitypub/process_collection_service_spec.rb' - 'spec/services/activitypub/process_collection_service_spec.rb'
- 'spec/services/purge_domain_service_spec.rb' - 'spec/services/purge_domain_service_spec.rb'
@ -140,7 +130,6 @@ Style/FetchEnvVar:
# AllowedMethods: redirect # AllowedMethods: redirect
Style/FormatStringToken: Style/FormatStringToken:
Exclude: Exclude:
- 'app/models/privacy_policy.rb'
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'lib/paperclip/color_extractor.rb' - 'lib/paperclip/color_extractor.rb'

View file

@ -1 +1 @@
3.2.2 3.2.3

View file

@ -7,15 +7,15 @@
ARG TARGETPLATFORM=${TARGETPLATFORM} ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM} ARG BUILDPLATFORM=${BUILDPLATFORM}
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"] # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.3"]
ARG RUBY_VERSION="3.2.2" ARG RUBY_VERSION="3.2.3"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
ARG NODE_MAJOR_VERSION="20" ARG NODE_MAJOR_VERSION="20"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm" ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim) # Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm) # Ruby image to use for base image based on combined variables (ex: 3.2.3-slim-bookworm)
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA

View file

@ -1,19 +1,35 @@
## ActivityPub federation in Mastodon # Federation
## Supported federation protocols and standards
- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server)
- [WebFinger](https://webfinger.net/)
- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
- [NodeInfo](https://nodeinfo.diaspora.software/)
## Supported FEPs
- [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md)
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
- [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md)
- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md)
## ActivityPub in Mastodon
Mastodon largely follows the ActivityPub server-to-server specification but it makes uses of some non-standard extensions, some of which are required for interacting with Mastodon at all. Mastodon largely follows the ActivityPub server-to-server specification but it makes uses of some non-standard extensions, some of which are required for interacting with Mastodon at all.
Supported vocabulary: https://docs.joinmastodon.org/spec/activitypub/ - [Supported ActivityPub vocabulary](https://docs.joinmastodon.org/spec/activitypub/)
### Required extensions ### Required extensions
#### Webfinger #### WebFinger
In Mastodon, users are identified by a `username` and `domain` pair (e.g., `Gargron@mastodon.social`). In Mastodon, users are identified by a `username` and `domain` pair (e.g., `Gargron@mastodon.social`).
This is used both for discovery and for unambiguously mentioning users across the fediverse. Furthermore, this is part of Mastodon's database design from its very beginnings. This is used both for discovery and for unambiguously mentioning users across the fediverse. Furthermore, this is part of Mastodon's database design from its very beginnings.
As a result, Mastodon requires that each ActivityPub actor uniquely maps back to an `acct:` URI that can be resolved via WebFinger. As a result, Mastodon requires that each ActivityPub actor uniquely maps back to an `acct:` URI that can be resolved via WebFinger.
More information and examples are available at: https://docs.joinmastodon.org/spec/webfinger/ - [WebFinger information and examples](https://docs.joinmastodon.org/spec/webfinger/)
#### HTTP Signatures #### HTTP Signatures
@ -21,11 +37,13 @@ In order to authenticate activities, Mastodon relies on HTTP Signatures, signing
Mastodon requires all `POST` requests to be signed, and MAY require `GET` requests to be signed, depending on the configuration of the Mastodon server. Mastodon requires all `POST` requests to be signed, and MAY require `GET` requests to be signed, depending on the configuration of the Mastodon server.
More information on HTTP Signatures, as well as examples, can be found here: https://docs.joinmastodon.org/spec/security/#http - [HTTP Signatures information and examples](https://docs.joinmastodon.org/spec/security/#http)
### Optional extensions ### Optional extensions
- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld - [Linked-Data Signatures](https://docs.joinmastodon.org/spec/security/#ld)
- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/ - [Bearcaps](https://docs.joinmastodon.org/spec/bearcaps/)
- Followers collection synchronization: https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md
- Search indexing consent for actors: https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md ### Additional documentation
- [Mastodon documentation](https://docs.joinmastodon.org/)

View file

@ -150,7 +150,7 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (3.1.5) bigdecimal (3.1.6)
bindata (2.4.15) bindata (2.4.15)
binding_of_caller (1.0.0) binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
@ -398,12 +398,12 @@ GEM
activerecord activerecord
kaminari-core (= 1.2.2) kaminari-core (= 1.2.2)
kaminari-core (1.2.2) kaminari-core (1.2.2)
kt-paperclip (7.2.1) kt-paperclip (7.2.2)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
marcel (~> 1.0.1) marcel (~> 1.0.1)
mime-types mime-types
terrapin (~> 0.6.0) terrapin (>= 0.6.0, < 2.0)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (2.5.2) launchy (2.5.2)
addressable (~> 2.8) addressable (~> 2.8)
@ -600,8 +600,8 @@ GEM
rdf (3.3.1) rdf (3.3.1)
bcp47_spec (~> 0.2) bcp47_spec (~> 0.2)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.6.1) rdf-normalize (0.7.0)
rdf (~> 3.2) rdf (~> 3.3)
rdoc (6.6.2) rdoc (6.6.2)
psych (>= 4.0.0) psych (>= 4.0.0)
redcarpet (3.6.0) redcarpet (3.6.0)

View file

@ -24,7 +24,7 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
end end
def set_items def set_items
@items = @account.followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(uri_prefix)}/%", false, true)).or(@account.followers.where(uri: uri_prefix)).pluck(:uri) @items = @account.followers.matches_uri_prefix(uri_prefix).pluck(:uri)
end end
def collection_presenter def collection_presenter

View file

@ -6,7 +6,7 @@ module Admin
def index def index
authorize :audit_log, :index? authorize :audit_log, :index?
@auditable_accounts = Account.where(id: Admin::ActionLog.select('distinct account_id')).select(:id, :username) @auditable_accounts = Account.auditable.select(:id, :username)
end end
private private

View file

@ -21,7 +21,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
return [] if hide_results? return [] if hide_results?
scope = default_accounts scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id
scope.merge(paginated_follows).to_a scope.merge(paginated_follows).to_a
end end
@ -30,7 +30,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
end end
def default_accounts def default_accounts
Account.includes(:active_relationships, :account_stat).references(:active_relationships) Account.includes(:active_relationships, :account_stat, :user).references(:active_relationships)
end end
def paginated_follows def paginated_follows

View file

@ -21,7 +21,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
return [] if hide_results? return [] if hide_results?
scope = default_accounts scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id
scope.merge(paginated_follows).to_a scope.merge(paginated_follows).to_a
end end
@ -30,7 +30,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end end
def default_accounts def default_accounts
Account.includes(:passive_relationships, :account_stat).references(:passive_relationships) Account.includes(:passive_relationships, :account_stat, :user).references(:passive_relationships)
end end
def paginated_follows def paginated_follows

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class Api::V1::AnnualReportsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
before_action :require_user!
before_action :set_annual_report, except: :index
def index
with_read_replica do
@presenter = AnnualReportsPresenter.new(GeneratedAnnualReport.where(account_id: current_account.id).pending)
@relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id)
end
render json: @presenter,
serializer: REST::AnnualReportsSerializer,
relationships: @relationships
end
def read
@annual_report.view!
render_empty
end
private
def set_annual_report
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: params[:id])
end
end

View file

@ -17,7 +17,7 @@ class Api::V1::BlocksController < Api::BaseController
end end
def paginated_blocks def paginated_blocks
@paginated_blocks ||= Block.eager_load(target_account: :account_stat) @paginated_blocks ||= Block.eager_load(target_account: [:account_stat, :user])
.joins(:target_account) .joins(:target_account)
.merge(Account.without_suspended) .merge(Account.without_suspended)
.where(account: current_account) .where(account: current_account)

View file

@ -27,7 +27,7 @@ class Api::V1::DirectoriesController < Api::BaseController
scope.merge!(local_account_scope) if local_accounts? scope.merge!(local_account_scope) if local_accounts?
scope.merge!(account_exclusion_scope) if current_account scope.merge!(account_exclusion_scope) if current_account
scope.merge!(account_domain_block_scope) if current_account && !local_accounts? scope.merge!(account_domain_block_scope) if current_account && !local_accounts?
end end.includes(:account_stat, user: :role)
end end
def local_accounts? def local_accounts?

View file

@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController
end end
def endorsed_accounts def endorsed_accounts
current_account.endorsed_accounts.includes(:account_stat).without_suspended current_account.endorsed_accounts.includes(:account_stat, :user).without_suspended
end end
def insert_pagination_headers def insert_pagination_headers

View file

@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
end end
def default_accounts def default_accounts
Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests) Account.without_suspended.includes(:follow_requests, :account_stat, :user).references(:follow_requests)
end end
def paginated_follow_requests def paginated_follow_requests

View file

@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController
def load_accounts def load_accounts
if unlimited? if unlimited?
@list.accounts.without_suspended.includes(:account_stat).all @list.accounts.without_suspended.includes(:account_stat, :user).all
else else
@list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) @list.accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end end
end end

View file

@ -17,7 +17,7 @@ class Api::V1::MutesController < Api::BaseController
end end
def paginated_mutes def paginated_mutes
@paginated_mutes ||= Mute.eager_load(:target_account) @paginated_mutes ||= Mute.eager_load(target_account: [:account_stat, :user])
.joins(:target_account) .joins(:target_account)
.merge(Account.without_suspended) .merge(Account.without_suspended)
.where(account: current_account) .where(account: current_account)

View file

@ -27,7 +27,7 @@ class Api::V1::Peers::SearchController < Api::BaseController
@domains = InstancesIndex.query(function_score: { @domains = InstancesIndex.query(function_score: {
query: { query: {
prefix: { prefix: {
domain: TagManager.instance.normalize_domain(params[:q].strip), domain: normalized_domain,
}, },
}, },
@ -37,11 +37,18 @@ class Api::V1::Peers::SearchController < Api::BaseController
}, },
}).limit(10).pluck(:domain) }).limit(10).pluck(:domain)
else else
domain = params[:q].strip domain = normalized_domain
domain = TagManager.instance.normalize_domain(domain) @domains = Instance.searchable.domain_starts_with(domain).limit(10).pluck(:domain)
@domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain)
end end
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
@domains = [] @domains = []
end end
def normalized_domain
TagManager.instance.normalize_domain(query_value)
end
def query_value
params[:q].strip
end
end end

View file

@ -14,14 +14,14 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::Bas
def load_accounts def load_accounts
scope = default_accounts scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? scope = scope.not_excluded_by_account(current_account) unless current_account.nil?
scope.merge(paginated_favourites).to_a scope.merge(paginated_favourites).to_a
end end
def default_accounts def default_accounts
Account Account
.without_suspended .without_suspended
.includes(:favourites, :account_stat) .includes(:favourites, :account_stat, :user)
.references(:favourites) .references(:favourites)
.where(favourites: { status_id: @status.id }) .where(favourites: { status_id: @status.id })
end end

View file

@ -14,12 +14,12 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base
def load_accounts def load_accounts
scope = default_accounts scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? scope = scope.not_excluded_by_account(current_account) unless current_account.nil?
scope.merge(paginated_statuses).to_a scope.merge(paginated_statuses).to_a
end end
def default_accounts def default_accounts
Account.without_suspended.includes(:statuses, :account_stat).references(:statuses) Account.without_suspended.includes(:statuses, :account_stat, :user).references(:statuses)
end end
def paginated_statuses def paginated_statuses

View file

@ -35,7 +35,7 @@ class Api::V2::FiltersController < Api::BaseController
private private
def set_filters def set_filters
@filters = current_account.custom_filters.includes(:keywords) @filters = current_account.custom_filters.includes(:keywords, :statuses)
end end
def set_filter def set_filter

View file

@ -1,6 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Auth::SessionsController < Devise::SessionsController class Auth::SessionsController < Devise::SessionsController
include Redisable
MAX_2FA_ATTEMPTS_PER_HOUR = 10
layout 'auth' layout 'auth'
skip_before_action :check_self_destruct! skip_before_action :check_self_destruct!
@ -130,9 +134,23 @@ class Auth::SessionsController < Devise::SessionsController
session.delete(:attempt_user_updated_at) session.delete(:attempt_user_updated_at)
end end
def clear_2fa_attempt_from_user(user)
redis.del(second_factor_attempts_key(user))
end
def check_second_factor_rate_limits(user)
attempts, = redis.multi do |multi|
multi.incr(second_factor_attempts_key(user))
multi.expire(second_factor_attempts_key(user), 1.hour)
end
attempts >= MAX_2FA_ATTEMPTS_PER_HOUR
end
def on_authentication_success(user, security_measure) def on_authentication_success(user, security_measure)
@on_authentication_success_called = true @on_authentication_success_called = true
clear_2fa_attempt_from_user(user)
clear_attempt_from_session clear_attempt_from_session
user.update_sign_in!(new_sign_in: true) user.update_sign_in!(new_sign_in: true)
@ -163,5 +181,14 @@ class Auth::SessionsController < Devise::SessionsController
ip: request.remote_ip, ip: request.remote_ip,
user_agent: request.user_agent user_agent: request.user_agent
) )
# Only send a notification email every hour at most
return if redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour, get: true).present?
UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later!
end
def second_factor_attempts_key(user)
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
end end
end end

View file

@ -66,6 +66,11 @@ module Auth::TwoFactorAuthenticationConcern
end end
def authenticate_with_two_factor_via_otp(user) def authenticate_with_two_factor_via_otp(user)
if check_second_factor_rate_limits(user)
flash.now[:alert] = I18n.t('users.rate_limited')
return prompt_for_two_factor(user)
end
if valid_otp_attempt?(user) if valid_otp_attempt?(user)
on_authentication_success(user, :otp) on_authentication_success(user, :otp)
else else

View file

@ -155,7 +155,7 @@ module JsonLdHelper
end end
end end
def fetch_resource(uri, id, on_behalf_of = nil) def fetch_resource(uri, id, on_behalf_of = nil, request_options: {})
unless id unless id
json = fetch_resource_without_id_validation(uri, on_behalf_of) json = fetch_resource_without_id_validation(uri, on_behalf_of)
@ -164,14 +164,14 @@ module JsonLdHelper
uri = json['id'] uri = json['id']
end end
json = fetch_resource_without_id_validation(uri, on_behalf_of) json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
json.present? && json['id'] == uri ? json : nil json.present? && json['id'] == uri ? json : nil
end end
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
on_behalf_of ||= Account.representative on_behalf_of ||= Account.representative
build_request(uri, on_behalf_of).perform do |response| build_request(uri, on_behalf_of, options: request_options).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
body_to_json(response.body_with_limit) if response.code == 200 body_to_json(response.body_with_limit) if response.code == 200
@ -204,8 +204,8 @@ module JsonLdHelper
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
end end
def build_request(uri, on_behalf_of = nil) def build_request(uri, on_behalf_of = nil, options: {})
Request.new(:get, uri).tap do |request| Request.new(:get, uri, **options).tap do |request|
request.on_behalf_of(on_behalf_of) if on_behalf_of request.on_behalf_of(on_behalf_of) if on_behalf_of
request.add_headers('Accept' => 'application/activity+json, application/ld+json') request.add_headers('Accept' => 'application/activity+json, application/ld+json')
end end

View file

@ -179,6 +179,11 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
export const clickSearchResult = (q, type) => (dispatch, getState) => { export const clickSearchResult = (q, type) => (dispatch, getState) => {
const previous = getState().getIn(['search', 'recent']); const previous = getState().getIn(['search', 'recent']);
if (previous.some(x => x.get('q') === q && x.get('type') === type)) {
return;
}
const me = getState().getIn(['meta', 'me']); const me = getState().getIn(['meta', 'me']);
const current = previous.add(fromJS({ type, q })).takeLast(4); const current = previous.add(fromJS({ type, q })).takeLast(4);

View file

@ -62,14 +62,14 @@ class Search extends PureComponent {
}; };
defaultOptions = [ defaultOptions = [
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } }, { key: 'prompt-has', label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } }, { key: 'prompt-is', label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } }, { key: 'prompt-language', label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } }, { key: 'prompt-from', label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } }, { key: 'prompt-before', label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } }, { key: 'prompt-during', label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } }, { key: 'prompt-after', label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } } { key: 'prompt-in', label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }
]; ];
setRef = c => { setRef = c => {
@ -262,6 +262,8 @@ class Search extends PureComponent {
const { recent } = this.props; const { recent } = this.props;
return recent.toArray().map(search => ({ return recent.toArray().map(search => ({
key: `${search.get('type')}/${search.get('q')}`,
label: labelForRecentSearch(search), label: labelForRecentSearch(search),
action: () => this.handleRecentSearchClick(search), action: () => this.handleRecentSearchClick(search),
@ -346,8 +348,8 @@ class Search extends PureComponent {
<h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4> <h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4>
<div className='search__popout__menu'> <div className='search__popout__menu'>
{recent.size > 0 ? this._getOptions().map(({ label, action, forget }, i) => ( {recent.size > 0 ? this._getOptions().map(({ label, key, action, forget }, i) => (
<button key={label} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}> <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
<span>{label}</span> <span>{label}</span>
<button className='icon-button' onMouseDown={forget}><Icon id='times' icon={CloseIcon} /></button> <button className='icon-button' onMouseDown={forget}><Icon id='times' icon={CloseIcon} /></button>
</button> </button>

View file

@ -1,3 +1,4 @@
import { createSelector } from '@reduxjs/toolkit';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
@ -12,10 +13,15 @@ import {
import Search from '../components/search'; import Search from '../components/search';
const getRecentSearches = createSelector(
state => state.getIn(['search', 'recent']),
recent => recent.reverse(),
);
const mapStateToProps = state => ({ const mapStateToProps = state => ({
value: state.getIn(['search', 'value']), value: state.getIn(['search', 'value']),
submitted: state.getIn(['search', 'submitted']), submitted: state.getIn(['search', 'submitted']),
recent: state.getIn(['search', 'recent']).reverse(), recent: getRecentSearches(state),
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({

View file

@ -116,7 +116,6 @@
"compose_form.publish_form": "Artículu nuevu", "compose_form.publish_form": "Artículu nuevu",
"compose_form.publish_loud": "¡{publish}!", "compose_form.publish_loud": "¡{publish}!",
"compose_form.save_changes": "Guardar los cambeos", "compose_form.save_changes": "Guardar los cambeos",
"compose_form.spoiler.unmarked": "Text is not hidden",
"confirmation_modal.cancel": "Encaboxar", "confirmation_modal.cancel": "Encaboxar",
"confirmations.block.block_and_report": "Bloquiar ya informar", "confirmations.block.block_and_report": "Bloquiar ya informar",
"confirmations.block.confirm": "Bloquiar", "confirmations.block.confirm": "Bloquiar",
@ -146,6 +145,7 @@
"dismissable_banner.community_timeline": "Esta seición contién los artículos públicos más actuales de los perfiles agospiaos nel dominiu {domain}.", "dismissable_banner.community_timeline": "Esta seición contién los artículos públicos más actuales de los perfiles agospiaos nel dominiu {domain}.",
"dismissable_banner.dismiss": "Escartar", "dismissable_banner.dismiss": "Escartar",
"dismissable_banner.explore_tags": "Esta seición contién les etiquetes del fediversu que tán ganando popularidá güei. Les etiquetes más usaes polos perfiles apaecen no cimero.", "dismissable_banner.explore_tags": "Esta seición contién les etiquetes del fediversu que tán ganando popularidá güei. Les etiquetes más usaes polos perfiles apaecen no cimero.",
"dismissable_banner.public_timeline": "Esta seición contién los artículos más nuevos de les persones na web social que les persones de {domain} siguen.",
"embed.instructions": "Empotra esti artículu nel to sitiu web pente la copia del códigu d'abaxo.", "embed.instructions": "Empotra esti artículu nel to sitiu web pente la copia del códigu d'abaxo.",
"embed.preview": "Va apaecer asina:", "embed.preview": "Va apaecer asina:",
"emoji_button.activity": "Actividá", "emoji_button.activity": "Actividá",
@ -155,6 +155,7 @@
"emoji_button.not_found": "Nun s'atoparon fustaxes que concasen", "emoji_button.not_found": "Nun s'atoparon fustaxes que concasen",
"emoji_button.objects": "Oxetos", "emoji_button.objects": "Oxetos",
"emoji_button.people": "Persones", "emoji_button.people": "Persones",
"emoji_button.recent": "D'usu frecuente",
"emoji_button.search": "Buscar…", "emoji_button.search": "Buscar…",
"emoji_button.search_results": "Resultaos de la busca", "emoji_button.search_results": "Resultaos de la busca",
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
@ -217,7 +218,6 @@
"hashtag.column_header.tag_mode.any": "o {additional}", "hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "ensin {additional}", "hashtag.column_header.tag_mode.none": "ensin {additional}",
"hashtag.column_settings.select.no_options_message": "Nun s'atopó nenguna suxerencia", "hashtag.column_settings.select.no_options_message": "Nun s'atopó nenguna suxerencia",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
"hashtag.follow": "Siguir a la etiqueta", "hashtag.follow": "Siguir a la etiqueta",
"hashtag.unfollow": "Dexar de siguir a la etiqueta", "hashtag.unfollow": "Dexar de siguir a la etiqueta",
@ -259,7 +259,6 @@
"keyboard_shortcuts.reply": "Responder a un artículu", "keyboard_shortcuts.reply": "Responder a un artículu",
"keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu", "keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu",
"keyboard_shortcuts.search": "Enfocar la barra de busca", "keyboard_shortcuts.search": "Enfocar la barra de busca",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "Abrir la columna «Entamar»", "keyboard_shortcuts.start": "Abrir la columna «Entamar»",
"keyboard_shortcuts.toggle_sensitivity": "Amosar/anubrir el conteníu multimedia", "keyboard_shortcuts.toggle_sensitivity": "Amosar/anubrir el conteníu multimedia",
"keyboard_shortcuts.toot": "Comenzar un artículu nuevu", "keyboard_shortcuts.toot": "Comenzar un artículu nuevu",
@ -412,12 +411,16 @@
"search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}", "search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}",
"search.quick_action.status_search": "Artículos que concasen con {x}", "search.quick_action.status_search": "Artículos que concasen con {x}",
"search.search_or_paste": "Busca o apiega una URL", "search.search_or_paste": "Busca o apiega una URL",
"search_popout.language_code": "códigu de llingua ISO",
"search_popout.quick_actions": "Aiciones rápides", "search_popout.quick_actions": "Aiciones rápides",
"search_popout.recent": "Busques de recién", "search_popout.recent": "Busques de recién",
"search_popout.specific_date": "data específica",
"search_popout.user": "perfil",
"search_results.accounts": "Perfiles", "search_results.accounts": "Perfiles",
"search_results.all": "Too", "search_results.all": "Too",
"search_results.hashtags": "Etiquetes", "search_results.hashtags": "Etiquetes",
"search_results.nothing_found": "Nun se pudo atopar nada con esos términos de busca", "search_results.nothing_found": "Nun se pudo atopar nada con esos términos de busca",
"search_results.see_all": "Ver too",
"search_results.statuses": "Artículos", "search_results.statuses": "Artículos",
"search_results.title": "Busca de: {q}", "search_results.title": "Busca de: {q}",
"server_banner.introduction": "{domain} ye parte de la rede social descentralizada que tien la teunoloxía de {mastodon}.", "server_banner.introduction": "{domain} ye parte de la rede social descentralizada que tien la teunoloxía de {mastodon}.",
@ -460,6 +463,7 @@
"status.replied_to": "En rempuesta a {name}", "status.replied_to": "En rempuesta a {name}",
"status.reply": "Responder", "status.reply": "Responder",
"status.replyAll": "Responder al filu", "status.replyAll": "Responder al filu",
"status.report": "Informar de @{name}",
"status.sensitive_warning": "Conteníu sensible", "status.sensitive_warning": "Conteníu sensible",
"status.show_filter_reason": "Amosar de toes toes", "status.show_filter_reason": "Amosar de toes toes",
"status.show_less": "Amosar menos", "status.show_less": "Amosar menos",

View file

@ -150,7 +150,7 @@
"compose_form.poll.duration": "Durada de l'enquesta", "compose_form.poll.duration": "Durada de l'enquesta",
"compose_form.poll.option_placeholder": "Opció {number}", "compose_form.poll.option_placeholder": "Opció {number}",
"compose_form.poll.remove_option": "Elimina aquesta opció", "compose_form.poll.remove_option": "Elimina aquesta opció",
"compose_form.poll.switch_to_multiple": "Canvia lenquesta per a permetre diverses opcions", "compose_form.poll.switch_to_multiple": "Canvia lenquesta per a permetre múltiples opcions",
"compose_form.poll.switch_to_single": "Canvia lenquesta per a permetre una única opció", "compose_form.poll.switch_to_single": "Canvia lenquesta per a permetre una única opció",
"compose_form.publish": "Tut", "compose_form.publish": "Tut",
"compose_form.publish_form": "Nou tut", "compose_form.publish_form": "Nou tut",
@ -607,7 +607,7 @@
"search.quick_action.status_search": "Tuts coincidint amb {x}", "search.quick_action.status_search": "Tuts coincidint amb {x}",
"search.search_or_paste": "Cerca o escriu l'URL", "search.search_or_paste": "Cerca o escriu l'URL",
"search_popout.full_text_search_disabled_message": "No disponible a {domain}.", "search_popout.full_text_search_disabled_message": "No disponible a {domain}.",
"search_popout.full_text_search_logged_out_message": "Només disponible en iniciar la sessió.", "search_popout.full_text_search_logged_out_message": "Només disponible amb la sessió iniciada.",
"search_popout.language_code": "Codi de llengua ISO", "search_popout.language_code": "Codi de llengua ISO",
"search_popout.options": "Opcions de cerca", "search_popout.options": "Opcions de cerca",
"search_popout.quick_actions": "Accions ràpides", "search_popout.quick_actions": "Accions ràpides",

View file

@ -683,7 +683,7 @@
"status.show_more": "펼치기", "status.show_more": "펼치기",
"status.show_more_all": "모두 펼치기", "status.show_more_all": "모두 펼치기",
"status.show_original": "원본 보기", "status.show_original": "원본 보기",
"status.title.with_attachments": "{user} 님이 {attachmentCount, plural, one {첨부} other {{attachmentCount}개 첨부}}하여 게시", "status.title.with_attachments": "{user} 님이 {attachmentCount, plural, one {첨부파일} other {{attachmentCount}개의 첨부파일}}과 함께 게시함",
"status.translate": "번역", "status.translate": "번역",
"status.translated_from_with": "{provider}에 의해 {lang}에서 번역됨", "status.translated_from_with": "{provider}에 의해 {lang}에서 번역됨",
"status.uncached_media_warning": "마리보기 허용되지 않음", "status.uncached_media_warning": "마리보기 허용되지 않음",

View file

@ -328,6 +328,7 @@
"interaction_modal.on_another_server": "En otro sirvidor", "interaction_modal.on_another_server": "En otro sirvidor",
"interaction_modal.on_this_server": "En este sirvidor", "interaction_modal.on_this_server": "En este sirvidor",
"interaction_modal.sign_in": "No estas konektado kon este sirvidor. Ande tyenes tu kuento?", "interaction_modal.sign_in": "No estas konektado kon este sirvidor. Ande tyenes tu kuento?",
"interaction_modal.sign_in_hint": "Konsejo: Akel es el sitio adonde te enrejistrates. Si no lo akodras, bushka el mesaj de posta elektronika de bienvenida en tu kuti de arivo. Tambien puedes eskrivir tu nombre de utilizador kompleto (por enshemplo @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Endika ke te plaze publikasyon de {name}", "interaction_modal.title.favourite": "Endika ke te plaze publikasyon de {name}",
"interaction_modal.title.follow": "Sige a {name}", "interaction_modal.title.follow": "Sige a {name}",
"interaction_modal.title.reblog": "Repartaja publikasyon de {name}", "interaction_modal.title.reblog": "Repartaja publikasyon de {name}",
@ -478,6 +479,7 @@
"onboarding.actions.go_to_explore": "Va a los trendes", "onboarding.actions.go_to_explore": "Va a los trendes",
"onboarding.actions.go_to_home": "Va a tu linya prinsipala", "onboarding.actions.go_to_home": "Va a tu linya prinsipala",
"onboarding.compose.template": "Ke haber, #Mastodon?", "onboarding.compose.template": "Ke haber, #Mastodon?",
"onboarding.follows.empty": "Malorozamente, no se pueden amostrar rezultados en este momento. Puedes aprovar uzar la bushkeda o navigar por la pajina de eksplorasyon para topar personas a las que segir, o aprovarlo de muevo mas tadre.",
"onboarding.follows.title": "Personaliza tu linya prinsipala", "onboarding.follows.title": "Personaliza tu linya prinsipala",
"onboarding.profile.discoverable": "Faz ke mi profil apareska en bushkedas", "onboarding.profile.discoverable": "Faz ke mi profil apareska en bushkedas",
"onboarding.profile.display_name": "Nombre amostrado", "onboarding.profile.display_name": "Nombre amostrado",
@ -497,7 +499,9 @@
"onboarding.start.title": "Lo logrates!", "onboarding.start.title": "Lo logrates!",
"onboarding.steps.follow_people.body": "El buto de Mastodon es segir a djente interesante.", "onboarding.steps.follow_people.body": "El buto de Mastodon es segir a djente interesante.",
"onboarding.steps.follow_people.title": "Personaliza tu linya prinsipala", "onboarding.steps.follow_people.title": "Personaliza tu linya prinsipala",
"onboarding.steps.publish_status.body": "Puedes introdusirte al mundo con teksto, fotos, videos o anketas {emoji}",
"onboarding.steps.publish_status.title": "Eskrive tu primera publikasyon", "onboarding.steps.publish_status.title": "Eskrive tu primera publikasyon",
"onboarding.steps.setup_profile.body": "Kompleta tu profil para aumentar tus enteraksyones.",
"onboarding.steps.setup_profile.title": "Personaliza tu profil", "onboarding.steps.setup_profile.title": "Personaliza tu profil",
"onboarding.steps.share_profile.body": "Informe a tus amigos komo toparte en Mastodon", "onboarding.steps.share_profile.body": "Informe a tus amigos komo toparte en Mastodon",
"onboarding.steps.share_profile.title": "Partaja tu profil de Mastodon", "onboarding.steps.share_profile.title": "Partaja tu profil de Mastodon",

View file

@ -32,6 +32,7 @@
"account.featured_tags.last_status_never": "Sem publicações", "account.featured_tags.last_status_never": "Sem publicações",
"account.featured_tags.title": "Hashtags em destaque de {name}", "account.featured_tags.title": "Hashtags em destaque de {name}",
"account.follow": "Seguir", "account.follow": "Seguir",
"account.follow_back": "Seguir de volta",
"account.followers": "Seguidores", "account.followers": "Seguidores",
"account.followers.empty": "Nada aqui.", "account.followers.empty": "Nada aqui.",
"account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
@ -52,6 +53,7 @@
"account.mute_notifications_short": "Silenciar notificações", "account.mute_notifications_short": "Silenciar notificações",
"account.mute_short": "Silenciar", "account.mute_short": "Silenciar",
"account.muted": "Silenciado", "account.muted": "Silenciado",
"account.mutual": "Mútuo",
"account.no_bio": "Nenhuma descrição fornecida.", "account.no_bio": "Nenhuma descrição fornecida.",
"account.open_original_page": "Abrir a página original", "account.open_original_page": "Abrir a página original",
"account.posts": "Toots", "account.posts": "Toots",

View file

@ -314,7 +314,7 @@
"home.explore_prompt.body": "ฟีดหน้าแรกของคุณจะมีการผสมผสานของโพสต์จากแฮชแท็กที่คุณได้เลือกติดตาม, ผู้คนที่คุณได้เลือกติดตาม และโพสต์ที่เขาดัน หากนั่นรู้สึกเงียบเกินไป คุณอาจต้องการ:", "home.explore_prompt.body": "ฟีดหน้าแรกของคุณจะมีการผสมผสานของโพสต์จากแฮชแท็กที่คุณได้เลือกติดตาม, ผู้คนที่คุณได้เลือกติดตาม และโพสต์ที่เขาดัน หากนั่นรู้สึกเงียบเกินไป คุณอาจต้องการ:",
"home.explore_prompt.title": "นี่คือฐานหน้าแรกของคุณภายใน Mastodon", "home.explore_prompt.title": "นี่คือฐานหน้าแรกของคุณภายใน Mastodon",
"home.hide_announcements": "ซ่อนประกาศ", "home.hide_announcements": "ซ่อนประกาศ",
"home.pending_critical_update.body": "โปรดอัปเดตเซิร์ฟเวอร์ Mastodon ของคุณโดยเร็วที่สุดเท่าที่จะทำได้!", "home.pending_critical_update.body": "โปรดอัปเดตเซิร์ฟเวอร์ Mastodon ของคุณโดยเร็วที่สุดเท่าที่จะเป็นไปได้!",
"home.pending_critical_update.link": "ดูการอัปเดต", "home.pending_critical_update.link": "ดูการอัปเดต",
"home.pending_critical_update.title": "มีการอัปเดตความปลอดภัยสำคัญพร้อมใช้งาน!", "home.pending_critical_update.title": "มีการอัปเดตความปลอดภัยสำคัญพร้อมใช้งาน!",
"home.show_announcements": "แสดงประกาศ", "home.show_announcements": "แสดงประกาศ",

View file

@ -358,7 +358,7 @@
"keyboard_shortcuts.my_profile": "mở hồ sơ của bạn", "keyboard_shortcuts.my_profile": "mở hồ sơ của bạn",
"keyboard_shortcuts.notifications": "mở thông báo", "keyboard_shortcuts.notifications": "mở thông báo",
"keyboard_shortcuts.open_media": "mở ảnh hoặc video", "keyboard_shortcuts.open_media": "mở ảnh hoặc video",
"keyboard_shortcuts.pinned": "mở những tút đã ghim", "keyboard_shortcuts.pinned": "Open pinned posts list",
"keyboard_shortcuts.profile": "mở trang của người đăng tút", "keyboard_shortcuts.profile": "mở trang của người đăng tút",
"keyboard_shortcuts.reply": "trả lời", "keyboard_shortcuts.reply": "trả lời",
"keyboard_shortcuts.requests": "mở danh sách yêu cầu theo dõi", "keyboard_shortcuts.requests": "mở danh sách yêu cầu theo dõi",

View file

@ -1,12 +1,11 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import type { TypedUseSelectorHook } from 'react-redux';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from './store'; import type { AppDispatch, RootState } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useAppSelector = useSelector.withTypes<RootState>();
export const createAppAsyncThunk = createAsyncThunk.withTypes<{ export const createAppAsyncThunk = createAsyncThunk.withTypes<{
state: RootState; state: RootState;

View file

@ -100,9 +100,8 @@ table + p {
border-top-right-radius: 12px; border-top-right-radius: 12px;
height: 140px; height: 140px;
vertical-align: bottom; vertical-align: bottom;
background-color: #f3f2f5; background-position: center !important;
background-position: center; background-size: cover !important;
background-size: cover;
} }
.email-account-banner-inner-td { .email-account-banner-inner-td {

43
app/lib/annual_report.rb Normal file
View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
class AnnualReport
include DatabaseHelper
SOURCES = [
AnnualReport::Archetype,
AnnualReport::TypeDistribution,
AnnualReport::TopStatuses,
AnnualReport::MostUsedApps,
AnnualReport::CommonlyInteractedWithAccounts,
AnnualReport::TimeSeries,
AnnualReport::TopHashtags,
AnnualReport::MostRebloggedAccounts,
AnnualReport::Percentiles,
].freeze
SCHEMA = 1
def initialize(account, year)
@account = account
@year = year
end
def generate
return if GeneratedAnnualReport.exists?(account: @account, year: @year)
GeneratedAnnualReport.create(
account: @account,
year: @year,
schema_version: SCHEMA,
data: data
)
end
private
def data
with_read_replica do
SOURCES.each_with_object({}) { |klass, hsh| hsh.merge!(klass.new(@account, @year).generate) }
end
end
end

View file

@ -0,0 +1,49 @@
# frozen_string_literal: true
class AnnualReport::Archetype < AnnualReport::Source
# Average number of posts (including replies and reblogs) made by
# each active user in a single year (2023)
AVERAGE_PER_YEAR = 113
def generate
{
archetype: archetype,
}
end
private
def archetype
if (standalone_count + replies_count + reblogs_count) < AVERAGE_PER_YEAR
:lurker
elsif reblogs_count > (standalone_count * 2)
:booster
elsif polls_count > (standalone_count * 0.1) # standalone_count includes posts with polls
:pollster
elsif replies_count > (standalone_count * 2)
:replier
else
:oracle
end
end
def polls_count
@polls_count ||= base_scope.where.not(poll_id: nil).count
end
def reblogs_count
@reblogs_count ||= base_scope.where.not(reblog_of_id: nil).count
end
def replies_count
@replies_count ||= base_scope.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count
end
def standalone_count
@standalone_count ||= base_scope.without_replies.without_reblogs.count
end
def base_scope
@account.statuses.where(id: year_as_snowflake_range)
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AnnualReport::CommonlyInteractedWithAccounts < AnnualReport::Source
SET_SIZE = 40
def generate
{
commonly_interacted_with_accounts: commonly_interacted_with_accounts.map do |(account_id, count)|
{
account_id: account_id,
count: count,
}
end,
}
end
private
def commonly_interacted_with_accounts
@account.statuses.reorder(nil).where(id: year_as_snowflake_range).where.not(in_reply_to_account_id: @account.id).group(:in_reply_to_account_id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('in_reply_to_account_id, count(*) AS total'))
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AnnualReport::MostRebloggedAccounts < AnnualReport::Source
SET_SIZE = 10
def generate
{
most_reblogged_accounts: most_reblogged_accounts.map do |(account_id, count)|
{
account_id: account_id,
count: count,
}
end,
}
end
private
def most_reblogged_accounts
@account.statuses.reorder(nil).where(id: year_as_snowflake_range).where.not(reblog_of_id: nil).joins(reblog: :account).group('accounts.id').having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('accounts.id, count(*) as total'))
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AnnualReport::MostUsedApps < AnnualReport::Source
SET_SIZE = 10
def generate
{
most_used_apps: most_used_apps.map do |(name, count)|
{
name: name,
count: count,
}
end,
}
end
private
def most_used_apps
@account.statuses.reorder(nil).where(id: year_as_snowflake_range).joins(:application).group('oauth_applications.name').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('oauth_applications.name, count(*) as total'))
end
end

View file

@ -0,0 +1,62 @@
# frozen_string_literal: true
class AnnualReport::Percentiles < AnnualReport::Source
def generate
{
percentiles: {
followers: (total_with_fewer_followers / (total_with_any_followers + 1.0)) * 100,
statuses: (total_with_fewer_statuses / (total_with_any_statuses + 1.0)) * 100,
},
}
end
private
def followers_gained
@followers_gained ||= @account.passive_relationships.where("date_part('year', follows.created_at) = ?", @year).count
end
def statuses_created
@statuses_created ||= @account.statuses.where(id: year_as_snowflake_range).count
end
def total_with_fewer_followers
@total_with_fewer_followers ||= Follow.find_by_sql([<<~SQL.squish, { year: @year, comparison: followers_gained }]).first.total
WITH tmp0 AS (
SELECT follows.target_account_id
FROM follows
INNER JOIN accounts ON accounts.id = follows.target_account_id
WHERE date_part('year', follows.created_at) = :year
AND accounts.domain IS NULL
GROUP BY follows.target_account_id
HAVING COUNT(*) < :comparison
)
SELECT count(*) AS total
FROM tmp0
SQL
end
def total_with_fewer_statuses
@total_with_fewer_statuses ||= Status.find_by_sql([<<~SQL.squish, { comparison: statuses_created, min_id: year_as_snowflake_range.first, max_id: year_as_snowflake_range.last }]).first.total
WITH tmp0 AS (
SELECT statuses.account_id
FROM statuses
INNER JOIN accounts ON accounts.id = statuses.account_id
WHERE statuses.id BETWEEN :min_id AND :max_id
AND accounts.domain IS NULL
GROUP BY statuses.account_id
HAVING count(*) < :comparison
)
SELECT count(*) AS total
FROM tmp0
SQL
end
def total_with_any_followers
@total_with_any_followers ||= Follow.where("date_part('year', follows.created_at) = ?", @year).joins(:target_account).merge(Account.local).count('distinct follows.target_account_id')
end
def total_with_any_statuses
@total_with_any_statuses ||= Status.where(id: year_as_snowflake_range).joins(:account).merge(Account.local).count('distinct statuses.account_id')
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AnnualReport::Source
attr_reader :account, :year
def initialize(account, year)
@account = account
@year = year
end
protected
def year_as_snowflake_range
(Mastodon::Snowflake.id_at(DateTime.new(year, 1, 1))..Mastodon::Snowflake.id_at(DateTime.new(year, 12, 31)))
end
end

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class AnnualReport::TimeSeries < AnnualReport::Source
def generate
{
time_series: (1..12).map do |month|
{
month: month,
statuses: statuses_per_month[month] || 0,
following: following_per_month[month] || 0,
followers: followers_per_month[month] || 0,
}
end,
}
end
private
def statuses_per_month
@statuses_per_month ||= @account.statuses.reorder(nil).where(id: year_as_snowflake_range).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h
end
def following_per_month
@following_per_month ||= @account.active_relationships.where("date_part('year', created_at) = ?", @year).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h
end
def followers_per_month
@followers_per_month ||= @account.passive_relationships.where("date_part('year', created_at) = ?", @year).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AnnualReport::TopHashtags < AnnualReport::Source
SET_SIZE = 40
def generate
{
top_hashtags: top_hashtags.map do |(name, count)|
{
name: name,
count: count,
}
end,
}
end
private
def top_hashtags
Tag.joins(:statuses).where(statuses: { id: @account.statuses.where(id: year_as_snowflake_range).reorder(nil).select(:id) }).group(:id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('COALESCE(tags.display_name, tags.name), count(*) AS total'))
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AnnualReport::TopStatuses < AnnualReport::Source
def generate
top_reblogs = base_scope.order(reblogs_count: :desc).first&.id
top_favourites = base_scope.where.not(id: top_reblogs).order(favourites_count: :desc).first&.id
top_replies = base_scope.where.not(id: [top_reblogs, top_favourites]).order(replies_count: :desc).first&.id
{
top_statuses: {
by_reblogs: top_reblogs,
by_favourites: top_favourites,
by_replies: top_replies,
},
}
end
def base_scope
@account.statuses.with_public_visibility.joins(:status_stat).where(id: year_as_snowflake_range).reorder(nil)
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AnnualReport::TypeDistribution < AnnualReport::Source
def generate
{
type_distribution: {
total: base_scope.count,
reblogs: base_scope.where.not(reblog_of_id: nil).count,
replies: base_scope.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count,
standalone: base_scope.without_replies.without_reblogs.count,
},
}
end
private
def base_scope
@account.statuses.where(id: year_as_snowflake_range)
end
end

View file

@ -26,11 +26,11 @@ class StatusCacheHydrator
def hydrate_non_reblog_payload(empty_payload, account_id) def hydrate_non_reblog_payload(empty_payload, account_id)
empty_payload.tap do |payload| empty_payload.tap do |payload|
payload[:favourited] = Favourite.where(account_id: account_id, status_id: @status.id).exists? payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.id)
payload[:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.id).exists? payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.id)
payload[:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.conversation_id).exists? payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.conversation_id)
payload[:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.id).exists? payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.id)
payload[:pinned] = StatusPin.where(account_id: account_id, status_id: @status.id).exists? if @status.account_id == account_id payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.id) if @status.account_id == account_id
payload[:filtered] = mapped_applied_custom_filter(account_id, @status) payload[:filtered] = mapped_applied_custom_filter(account_id, @status)
if payload[:poll] if payload[:poll]
@ -51,11 +51,11 @@ class StatusCacheHydrator
# used to create the status, we need to hydrate it here too # used to create the status, we need to hydrate it here too
payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id
payload[:reblog][:favourited] = Favourite.where(account_id: account_id, status_id: @status.reblog_of_id).exists? payload[:reblog][:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.reblog_of_id)
payload[:reblog][:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.reblog_of_id).exists? payload[:reblog][:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.reblog_of_id)
payload[:reblog][:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.reblog.conversation_id).exists? payload[:reblog][:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.reblog.conversation_id)
payload[:reblog][:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.reblog_of_id).exists? payload[:reblog][:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.reblog_of_id)
payload[:reblog][:pinned] = StatusPin.where(account_id: account_id, status_id: @status.reblog_of_id).exists? if @status.reblog.account_id == account_id payload[:reblog][:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.reblog_of_id) if @status.reblog.account_id == account_id
payload[:reblog][:filtered] = payload[:filtered] payload[:reblog][:filtered] = payload[:filtered]
if payload[:reblog][:poll] if payload[:reblog][:poll]

View file

@ -191,6 +191,18 @@ class UserMailer < Devise::Mailer
end end
end end
def failed_2fa(user, remote_ip, user_agent, timestamp)
@resource = user
@remote_ip = remote_ip
@user_agent = user_agent
@detection = Browser.new(user_agent)
@timestamp = timestamp.to_time.utc
I18n.with_locale(locale) do
mail subject: default_i18n_subject
end
end
private private
def default_devise_subject def default_devise_subject

View file

@ -123,10 +123,11 @@ class Account < ApplicationRecord
scope :bots, -> { where(actor_type: %w(Application Service)) } scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :groups, -> { where(actor_type: 'Group') } scope :groups, -> { where(actor_type: 'Group') }
scope :alphabetic, -> { order(domain: :asc, username: :asc) } scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :matches_uri_prefix, ->(value) { where(arel_table[:uri].matches("#{sanitize_sql_like(value)}/%", false, true)).or(where(uri: value)) }
scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") } scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) } scope :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) }
scope :auditable, -> { where(id: Admin::ActionLog.select(:account_id).distinct) }
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) } scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) } scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) }
scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) } scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) }

View file

@ -29,7 +29,7 @@ class AccountSuggestions
# a complicated query on this end. # a complicated query on this end.
account_ids = account_ids_with_sources[offset, limit] account_ids = account_ids_with_sources[offset, limit]
accounts_map = Account.where(id: account_ids.map(&:first)).includes(:account_stat).index_by(&:id) accounts_map = Account.where(id: account_ids.map(&:first)).includes(:account_stat, :user).index_by(&:id)
account_ids.filter_map do |(account_id, source)| account_ids.filter_map do |(account_id, source)|
next unless accounts_map.key?(account_id) next unless accounts_map.key?(account_id)

View file

@ -72,7 +72,7 @@ class Admin::ActionLogFilter
end end
def results def results
scope = latest_action_logs.includes(:target) scope = latest_action_logs.includes(:target, :account)
params.each do |key, value| params.each do |key, value|
next if key.to_s == 'page' next if key.to_s == 'page'

View file

@ -20,8 +20,11 @@ class Appeal < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id', inverse_of: :appeal belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id', inverse_of: :appeal
belongs_to :approved_by_account, class_name: 'Account', optional: true
belongs_to :rejected_by_account, class_name: 'Account', optional: true with_options class_name: 'Account', optional: true do
belongs_to :approved_by_account
belongs_to :rejected_by_account
end
validates :text, presence: true, length: { maximum: 2_000 } validates :text, presence: true, length: { maximum: 2_000 }
validates :account_warning_id, uniqueness: true validates :account_warning_id, uniqueness: true

View file

@ -183,7 +183,7 @@ module Account::Interactions
end end
def following?(other_account) def following?(other_account)
active_relationships.where(target_account: other_account).exists? active_relationships.exists?(target_account: other_account)
end end
def following_anyone? def following_anyone?
@ -199,51 +199,51 @@ module Account::Interactions
end end
def blocking?(other_account) def blocking?(other_account)
block_relationships.where(target_account: other_account).exists? block_relationships.exists?(target_account: other_account)
end end
def domain_blocking?(other_domain) def domain_blocking?(other_domain)
domain_blocks.where(domain: other_domain).exists? domain_blocks.exists?(domain: other_domain)
end end
def muting?(other_account) def muting?(other_account)
mute_relationships.where(target_account: other_account).exists? mute_relationships.exists?(target_account: other_account)
end end
def muting_conversation?(conversation) def muting_conversation?(conversation)
conversation_mutes.where(conversation: conversation).exists? conversation_mutes.exists?(conversation: conversation)
end end
def muting_notifications?(other_account) def muting_notifications?(other_account)
mute_relationships.where(target_account: other_account, hide_notifications: true).exists? mute_relationships.exists?(target_account: other_account, hide_notifications: true)
end end
def muting_reblogs?(other_account) def muting_reblogs?(other_account)
active_relationships.where(target_account: other_account, show_reblogs: false).exists? active_relationships.exists?(target_account: other_account, show_reblogs: false)
end end
def requested?(other_account) def requested?(other_account)
follow_requests.where(target_account: other_account).exists? follow_requests.exists?(target_account: other_account)
end end
def favourited?(status) def favourited?(status)
status.proper.favourites.where(account: self).exists? status.proper.favourites.exists?(account: self)
end end
def bookmarked?(status) def bookmarked?(status)
status.proper.bookmarks.where(account: self).exists? status.proper.bookmarks.exists?(account: self)
end end
def reblogged?(status) def reblogged?(status)
status.proper.reblogs.where(account: self).exists? status.proper.reblogs.exists?(account: self)
end end
def pinned?(status) def pinned?(status)
status_pins.where(status: status).exists? status_pins.exists?(status: status)
end end
def endorsed?(account) def endorsed?(account)
account_pins.where(target_account: account).exists? account_pins.exists?(target_account: account)
end end
def status_matches_filters(status) def status_matches_filters(status)

View file

@ -17,8 +17,6 @@ class DomainAllow < ApplicationRecord
validates :domain, presence: true, uniqueness: true, domain: true validates :domain, presence: true, uniqueness: true, domain: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
def to_log_human_identifier def to_log_human_identifier
domain domain
end end

View file

@ -28,7 +28,6 @@ class DomainBlock < ApplicationRecord
has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false, dependent: nil has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false, dependent: nil
delegate :count, to: :accounts, prefix: true delegate :count, to: :accounts, prefix: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]) } scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]) }
scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) } scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
scope :by_severity, -> { in_order_of(:severity, %w(noop silence suspend)).order(:domain) } scope :by_severity, -> { in_order_of(:severity, %w(noop silence suspend)).order(:domain) }

View file

@ -21,8 +21,10 @@ class EmailDomainBlock < ApplicationRecord
include DomainNormalizable include DomainNormalizable
include Paginable include Paginable
belongs_to :parent, class_name: 'EmailDomainBlock', optional: true with_options class_name: 'EmailDomainBlock' do
has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy belongs_to :parent, optional: true
has_many :children, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
end
validates :domain, presence: true, uniqueness: true, domain: true validates :domain, presence: true, uniqueness: true, domain: true

View file

@ -45,7 +45,7 @@ class FeaturedTag < ApplicationRecord
end end
def decrement(deleted_status_id) def decrement(deleted_status_id)
update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) update(statuses_count: [0, statuses_count - 1].max, last_status_at: visible_tagged_account_statuses.where.not(id: deleted_status_id).select(:created_at).first&.created_at)
end end
private private
@ -55,8 +55,8 @@ class FeaturedTag < ApplicationRecord
end end
def reset_data def reset_data
self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count self.statuses_count = visible_tagged_account_statuses.count
self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at self.last_status_at = visible_tagged_account_statuses.select(:created_at).first&.created_at
end end
def validate_featured_tags_limit def validate_featured_tags_limit
@ -66,6 +66,14 @@ class FeaturedTag < ApplicationRecord
end end
def validate_tag_uniqueness def validate_tag_uniqueness
errors.add(:name, :taken) if FeaturedTag.by_name(name).where(account_id: account_id).exists? errors.add(:name, :taken) if tag_already_featured_for_account?
end
def tag_already_featured_for_account?
FeaturedTag.by_name(name).exists?(account_id: account_id)
end
def visible_tagged_account_statuses
account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag)
end end
end end

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: generated_annual_reports
#
# id :bigint(8) not null, primary key
# account_id :bigint(8) not null
# year :integer not null
# data :jsonb not null
# schema_version :integer not null
# viewed_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
#
class GeneratedAnnualReport < ApplicationRecord
belongs_to :account
scope :pending, -> { where(viewed_at: nil) }
def viewed?
viewed_at.present?
end
def view!
update!(viewed_at: Time.now.utc)
end
def account_ids
data['most_reblogged_accounts'].pluck('account_id') + data['commonly_interacted_with_accounts'].pluck('account_id')
end
def status_ids
data['top_statuses'].values
end
end

View file

@ -23,6 +23,7 @@ class Instance < ApplicationRecord
scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) } scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :domain_starts_with, ->(value) { where(arel_table[:domain].matches("#{sanitize_sql_like(value)}%", false, true)) }
scope :by_domain_and_subdomains, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") } scope :by_domain_and_subdomains, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") }
def self.refresh def self.refresh

View file

@ -27,8 +27,11 @@ class Poll < ApplicationRecord
belongs_to :status belongs_to :status
has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all
has_many :voters, -> { group('accounts.id') }, through: :votes, class_name: 'Account', source: :account
has_many :local_voters, -> { group('accounts.id').merge(Account.local) }, through: :votes, class_name: 'Account', source: :account with_options class_name: 'Account', source: :account, through: :votes do
has_many :voters, -> { group('accounts.id') }
has_many :local_voters, -> { group('accounts.id').merge(Account.local) }
end
has_many :notifications, as: :activity, dependent: :destroy has_many :notifications, as: :activity, dependent: :destroy

View file

@ -1,66 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class PrivacyPolicy < ActiveModelSerializers::Model class PrivacyPolicy < ActiveModelSerializers::Model
DEFAULT_PRIVACY_POLICY = <<~TXT DEFAULT_PRIVACY_POLICY = Rails.root.join('config', 'templates', 'privacy-policy.md').read
This privacy policy describes how %{domain} ("%{domain}", "we", "us") collects, protects and uses the personally identifiable information you may provide through the %{domain} website or its API. The policy also describes the choices available to you regarding our use of your personal information and how you can access and update this information. This policy does not apply to the practices of companies that %{domain} does not own or control, or to individuals that %{domain} does not employ or manage.
# What information do we collect?
- **Basic account information**: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly.
- **Posts, following and other public information**: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public.
- **Direct and followers-only posts**: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it's important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. **Please keep in mind that the operators of the server and any receiving server may view such messages**, and that recipients may screenshot, copy or otherwise re-share them. **Do not share any sensitive information over Mastodon.**
- **IPs and other metadata**: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server.
# What do we use your information for?
Any of the information we collect from you may be used in the following ways:
- To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline.
- To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations.
- The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions.
# How do we protect your information?
We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account.
# What is our data retention policy?
We will make a good faith effort to:
- Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days.
- Retain the IP addresses associated with registered users no more than 12 months.
You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image.
You may irreversibly delete your account at any time.
# Do we use cookies?
Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.
We use cookies to understand and save your preferences for future visits.
# Do we disclose any information to outside parties?
We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety.
Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this.
When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password.
# Site usage by children
If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site.
If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.
Law requirements can be different if this server is in another jurisdiction.
___
This document is CC-BY-SA. Originally adapted from the [Discourse privacy policy](https://github.com/discourse/discourse).
TXT
DEFAULT_UPDATED_AT = DateTime.new(2022, 10, 7).freeze DEFAULT_UPDATED_AT = DateTime.new(2022, 10, 7).freeze
attributes :updated_at, :text attributes :updated_at, :text

View file

@ -29,16 +29,19 @@ class Report < ApplicationRecord
rate_limit by: :account, family: :reports rate_limit by: :account, family: :reports
belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account'
belongs_to :action_taken_by_account, class_name: 'Account', optional: true with_options class_name: 'Account' do
belongs_to :assigned_account, class_name: 'Account', optional: true belongs_to :target_account
belongs_to :action_taken_by_account, optional: true
belongs_to :assigned_account, optional: true
end
has_many :notes, class_name: 'ReportNote', inverse_of: :report, dependent: :destroy has_many :notes, class_name: 'ReportNote', inverse_of: :report, dependent: :destroy
has_many :notifications, as: :activity, dependent: :destroy has_many :notifications, as: :activity, dependent: :destroy
scope :unresolved, -> { where(action_taken_at: nil) } scope :unresolved, -> { where(action_taken_at: nil) }
scope :resolved, -> { where.not(action_taken_at: nil) } scope :resolved, -> { where.not(action_taken_at: nil) }
scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) } scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with([:account_stat, { user: [:invite_request, :invite, :ips] }])) }
# A report is considered local if the reporter is local # A report is considered local if the reporter is local
delegate :local?, to: :account delegate :local?, to: :account

View file

@ -59,8 +59,10 @@ class Status < ApplicationRecord
belongs_to :conversation, optional: true belongs_to :conversation, optional: true
belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true, inverse_of: false belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true, inverse_of: false
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true with_options class_name: 'Status', optional: true do
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true belongs_to :thread, foreign_key: 'in_reply_to_id', inverse_of: :replies
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
end
has_many :favourites, inverse_of: :status, dependent: :destroy has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :bookmarks, inverse_of: :status, dependent: :destroy has_many :bookmarks, inverse_of: :status, dependent: :destroy

View file

@ -39,6 +39,8 @@ class Tag < ApplicationRecord
HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]\u0E47-\u0E4E#{HASHTAG_SEPARATORS}]/ HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]\u0E47-\u0E4E#{HASHTAG_SEPARATORS}]/
RECENT_STATUS_LIMIT = 1000
validates :name, presence: true, format: { with: HASHTAG_NAME_RE } validates :name, presence: true, format: { with: HASHTAG_NAME_RE }
validates :display_name, format: { with: HASHTAG_NAME_RE } validates :display_name, format: { with: HASHTAG_NAME_RE }
validate :validate_name_change, if: -> { !new_record? && name_changed? } validate :validate_name_change, if: -> { !new_record? && name_changed? }
@ -53,7 +55,7 @@ class Tag < ApplicationRecord
scope :not_trendable, -> { where(trendable: false) } scope :not_trendable, -> { where(trendable: false) }
scope :recently_used, lambda { |account| scope :recently_used, lambda { |account|
joins(:statuses) joins(:statuses)
.where(statuses: { id: account.statuses.select(:id).limit(1000) }) .where(statuses: { id: account.statuses.select(:id).limit(RECENT_STATUS_LIMIT) })
.group(:id).order(Arel.sql('count(*) desc')) .group(:id).order(Arel.sql('count(*) desc'))
} }
scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(Tag.normalize(term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(Tag.normalize(term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index

View file

@ -434,7 +434,7 @@ class User < ApplicationRecord
end end
def sign_up_from_ip_requires_approval? def sign_up_from_ip_requires_approval?
!sign_up_ip.nil? && IpBlock.where(severity: :sign_up_requires_approval).where('ip >>= ?', sign_up_ip.to_s).exists? sign_up_ip.present? && IpBlock.sign_up_requires_approval.exists?(['ip >>= ?', sign_up_ip.to_s])
end end
def sign_up_email_requires_approval? def sign_up_email_requires_approval?

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class AnnualReportsPresenter
alias read_attribute_for_serialization send
attr_reader :annual_reports
def initialize(annual_reports)
@annual_reports = annual_reports
end
def accounts
@accounts ||= Account.where(id: @annual_reports.flat_map(&:account_ids)).includes(:account_stat, :moved_to_account, user: :role)
end
def statuses
@statuses ||= Status.where(id: @annual_reports.flat_map(&:status_ids)).with_includes
end
def self.model_name
@model_name ||= ActiveModel::Name.new(self)
end
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class REST::AnnualReportSerializer < ActiveModel::Serializer
attributes :year, :data, :schema_version
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class REST::AnnualReportsSerializer < ActiveModel::Serializer
has_many :annual_reports, serializer: REST::AnnualReportSerializer
has_many :accounts, serializer: REST::AccountSerializer
has_many :statuses, serializer: REST::StatusSerializer
end

View file

@ -19,7 +19,7 @@ class REST::TagSerializer < ActiveModel::Serializer
if instance_options && instance_options[:relationships] if instance_options && instance_options[:relationships]
instance_options[:relationships].following_map[object.id] || false instance_options[:relationships].following_map[object.id] || false
else else
TagFollow.where(tag_id: object.id, account_id: current_user.account_id).exists? TagFollow.exists?(tag_id: object.id, account_id: current_user.account_id)
end end
end end

View file

@ -23,9 +23,9 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
case collection['type'] case collection['type']
when 'Collection', 'CollectionPage' when 'Collection', 'CollectionPage'
collection['items'] as_array(collection['items'])
when 'OrderedCollection', 'OrderedCollectionPage' when 'OrderedCollection', 'OrderedCollectionPage'
collection['orderedItems'] as_array(collection['orderedItems'])
end end
end end

View file

@ -44,7 +44,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
# If we fetched a status that already exists, then we need to treat the # If we fetched a status that already exists, then we need to treat the
# activity as an update rather than create # activity as an update rather than create
activity_json['type'] = 'Update' if equals_or_includes_any?(activity_json['type'], %w(Create)) && Status.where(uri: object_uri, account_id: actor.id).exists? activity_json['type'] = 'Update' if equals_or_includes_any?(activity_json['type'], %w(Create)) && Status.exists?(uri: object_uri, account_id: actor.id)
with_redis do |redis| with_redis do |redis|
discoveries = redis.incr("status_discovery_per_request:#{@request_id}") discoveries = redis.incr("status_discovery_per_request:#{@request_id}")

View file

@ -26,9 +26,9 @@ class ActivityPub::FetchRepliesService < BaseService
case collection['type'] case collection['type']
when 'Collection', 'CollectionPage' when 'Collection', 'CollectionPage'
collection['items'] as_array(collection['items'])
when 'OrderedCollection', 'OrderedCollectionPage' when 'OrderedCollection', 'OrderedCollectionPage'
collection['orderedItems'] as_array(collection['orderedItems'])
end end
end end
@ -37,7 +37,20 @@ class ActivityPub::FetchRepliesService < BaseService
return unless @allow_synchronous_requests return unless @allow_synchronous_requests
return if non_matching_uri_hosts?(@account.uri, collection_or_uri) return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
fetch_resource_without_id_validation(collection_or_uri, nil, true) # NOTE: For backward compatibility reasons, Mastodon signs outgoing
# queries incorrectly by default.
#
# While this is relevant for all URLs with query strings, this is
# the only code path where this happens in practice.
#
# Therefore, retry with correct signatures if this fails.
begin
fetch_resource_without_id_validation(collection_or_uri, nil, true)
rescue Mastodon::UnexpectedResponseError => e
raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { with_query_string: true })
end
end end
def filtered_replies def filtered_replies

View file

@ -59,9 +59,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService
case collection['type'] case collection['type']
when 'Collection', 'CollectionPage' when 'Collection', 'CollectionPage'
collection['items'] as_array(collection['items'])
when 'OrderedCollection', 'OrderedCollectionPage' when 'OrderedCollection', 'OrderedCollectionPage'
collection['orderedItems'] as_array(collection['orderedItems'])
end end
end end

View file

@ -69,7 +69,7 @@ class Keys::QueryService < BaseService
return if json['items'].blank? return if json['items'].blank?
@devices = json['items'].map do |device| @devices = as_array(json['items']).map do |device|
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim']) Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
end end
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e

View file

@ -19,7 +19,7 @@ class VoteService < BaseService
already_voted = true already_voted = true
with_redis_lock("vote:#{@poll.id}:#{@account.id}") do with_redis_lock("vote:#{@poll.id}:#{@account.id}") do
already_voted = @poll.votes.where(account: @account).exists? already_voted = @poll.votes.exists?(account: @account)
ApplicationRecord.transaction do ApplicationRecord.transaction do
@choices.each do |choice| @choices.each do |choice|

View file

@ -19,7 +19,7 @@ class ReactionValidator < ActiveModel::Validator
end end
def new_reaction?(reaction) def new_reaction?(reaction)
!reaction.announcement.announcement_reactions.where(name: reaction.name).exists? !reaction.announcement.announcement_reactions.exists?(name: reaction.name)
end end
def limit_reached?(reaction) def limit_reached?(reaction)

View file

@ -35,7 +35,7 @@ class VoteValidator < ActiveModel::Validator
if vote.persisted? if vote.persisted?
account_votes_on_same_poll(vote).where(choice: vote.choice).where.not(poll_votes: { id: vote }).exists? account_votes_on_same_poll(vote).where(choice: vote.choice).where.not(poll_votes: { id: vote }).exists?
else else
account_votes_on_same_poll(vote).where(choice: vote.choice).exists? account_votes_on_same_poll(vote).exists?(choice: vote.choice)
end end
end end

View file

@ -0,0 +1,24 @@
= content_for :heading do
= render 'application/mailer/heading', heading_title: t('user_mailer.failed_2fa.title'), heading_subtitle: t('user_mailer.failed_2fa.explanation'), heading_image_url: frontend_asset_url('images/mailer-new/heading/login.png')
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
%tr
%td.email-body-padding-td
%table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
%tr
%td.email-inner-card-td.email-prose
%p= t 'user_mailer.failed_2fa.details'
%p
%strong #{t('sessions.ip')}:
= @remote_ip
%br/
%strong #{t('sessions.browser')}:
%span{ title: @user_agent }
= t 'sessions.description',
browser: t("sessions.browsers.#{@detection.id}", default: @detection.id.to_s),
platform: t("sessions.platforms.#{@detection.platform.id}", default: @detection.platform.id.to_s)
%br/
%strong #{t('sessions.date')}:
= l(@timestamp.in_time_zone(@resource.time_zone.presence), format: :with_time_zone)
= render 'application/mailer/button', text: t('settings.account_settings'), url: edit_user_registration_url
%p= t 'user_mailer.failed_2fa.further_actions_html',
action: link_to(t('user_mailer.suspicious_sign_in.change_password'), edit_user_registration_url)

View file

@ -0,0 +1,15 @@
<%= t 'user_mailer.failed_2fa.title' %>
===
<%= t 'user_mailer.failed_2fa.explanation' %>
<%= t 'user_mailer.failed_2fa.details' %>
<%= t('sessions.ip') %>: <%= @remote_ip %>
<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %>
<%= l(@timestamp.in_time_zone(@resource.time_zone.presence), format: :with_time_zone) %>
<%= t 'user_mailer.failed_2fa.further_actions_html', action: t('user_mailer.suspicious_sign_in.change_password') %>
=> <%= edit_user_registration_url %>

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class GenerateAnnualReportWorker
include Sidekiq::Worker
def perform(account_id, year)
AnnualReport.new(Account.find(account_id), year).generate
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordNotUnique
true
end
end

View file

@ -24,6 +24,8 @@ class Scheduler::IndexingScheduler
end end
end end
private
def indexes def indexes
[AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex] [AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex]
end end

View file

@ -909,6 +909,7 @@ ast:
users: users:
follow_limit_reached: Nun pues siguir a más de %{limit} persones follow_limit_reached: Nun pues siguir a más de %{limit} persones
invalid_otp_token: El códigu de l'autenticación en dos pasos nun ye válidu invalid_otp_token: El códigu de l'autenticación en dos pasos nun ye válidu
rate_limited: Fixéronse milenta intentos d'autenticación. Volvi tentalo dempués.
seamless_external_login: Aniciesti la sesión pente un serviciu esternu, polo que la configuración de la contraseña ya de la direición de corréu electrónicu nun tán disponibles. seamless_external_login: Aniciesti la sesión pente un serviciu esternu, polo que la configuración de la contraseña ya de la direición de corréu electrónicu nun tán disponibles.
signed_in_as: 'Aniciesti la sesión como:' signed_in_as: 'Aniciesti la sesión como:'
verification: verification:

View file

@ -1790,6 +1790,11 @@ bg:
extra: Вече е готово за теглене! extra: Вече е готово за теглене!
subject: Вашият архив е готов за изтегляне subject: Вашият архив е готов за изтегляне
title: Сваляне на архива title: Сваляне на архива
failed_2fa:
details: 'Ето подробности на опита за влизане:'
explanation: Някой се опита да влезе в акаунта ви, но предостави невалиден втори фактор за удостоверяване.
subject: Неуспешен втори фактор за удостоверяване
title: Провал на втория фактор за удостоверяване
suspicious_sign_in: suspicious_sign_in:
change_password: промяна на паролата ви change_password: промяна на паролата ви
details: 'Ето подробности при вход:' details: 'Ето подробности при вход:'
@ -1843,6 +1848,7 @@ bg:
go_to_sso_account_settings: Отидете при настройките на акаунта на своя доставчик на идентичност go_to_sso_account_settings: Отидете при настройките на акаунта на своя доставчик на идентичност
invalid_otp_token: Невалиден код invalid_otp_token: Невалиден код
otp_lost_help_html: Ако загубите достъп до двете, то може да се свържете с %{email} otp_lost_help_html: Ако загубите достъп до двете, то може да се свържете с %{email}
rate_limited: Премного опити за удостоверяване. Опитайте пак по-късно.
seamless_external_login: Влезли сте чрез външна услуга, така че настройките за парола и имейл не са налични. seamless_external_login: Влезли сте чрез външна услуга, така че настройките за парола и имейл не са налични.
signed_in_as: 'Влезли като:' signed_in_as: 'Влезли като:'
verification: verification:

View file

@ -425,7 +425,7 @@ ca:
view: Veure el bloqueig del domini view: Veure el bloqueig del domini
email_domain_blocks: email_domain_blocks:
add_new: Afegir nou add_new: Afegir nou
allow_registrations_with_approval: Registre permès amb validació allow_registrations_with_approval: Permet els registres amb validació
attempts_over_week: attempts_over_week:
one: "%{count} intent en la darrera setmana" one: "%{count} intent en la darrera setmana"
other: "%{count} intents de registre en la darrera setmana" other: "%{count} intents de registre en la darrera setmana"
@ -1046,6 +1046,7 @@ ca:
clicking_this_link: en clicar aquest enllaç clicking_this_link: en clicar aquest enllaç
login_link: inici de sessió login_link: inici de sessió
proceed_to_login_html: Ara pots passar a %{login_link}. proceed_to_login_html: Ara pots passar a %{login_link}.
redirect_to_app_html: Se us hauria d'haver redirigit a l'app <strong>%{app_name}</strong>. Si això no ha passat, intenteu %{clicking_this_link} o torneu manualment a l'app.
registration_complete: La teva inscripció a %{domain} ja és completa. registration_complete: La teva inscripció a %{domain} ja és completa.
welcome_title: Hola, %{name}! welcome_title: Hola, %{name}!
wrong_email_hint: Si aquesta adreça de correu electrònic no és correcte, pots canviar-la en els ajustos del compte. wrong_email_hint: Si aquesta adreça de correu electrònic no és correcte, pots canviar-la en els ajustos del compte.
@ -1109,6 +1110,7 @@ ca:
functional: El teu compte està completament operatiu. functional: El teu compte està completament operatiu.
pending: La vostra sol·licitud està pendent de revisió pel nostre personal. Això pot trigar una mica. Rebreu un correu electrònic quan sigui aprovada. pending: La vostra sol·licitud està pendent de revisió pel nostre personal. Això pot trigar una mica. Rebreu un correu electrònic quan sigui aprovada.
redirecting_to: El teu compte és inactiu perquè actualment està redirigint a %{acct}. redirecting_to: El teu compte és inactiu perquè actualment està redirigint a %{acct}.
self_destruct: Com que %{domain} tanca, només tindreu accés limitat al vostre compte.
view_strikes: Veure accions del passat contra el teu compte view_strikes: Veure accions del passat contra el teu compte
too_fast: Formulari enviat massa ràpid, torna a provar-ho. too_fast: Formulari enviat massa ràpid, torna a provar-ho.
use_security_key: Usa clau de seguretat use_security_key: Usa clau de seguretat
@ -1580,6 +1582,7 @@ ca:
over_total_limit: Has superat el límit de %{limit} tuts programats over_total_limit: Has superat el límit de %{limit} tuts programats
too_soon: La data programada ha de ser futura too_soon: La data programada ha de ser futura
self_destruct: self_destruct:
lead_html: Lamentablement, <strong>%{domain}</strong> tanca de forma definitiva. Si hi teníeu un compte, no el podreu continuar utilitzant, però podeu demanar una còpia de les vostres dades.
title: Aquest servidor tancarà title: Aquest servidor tancarà
sessions: sessions:
activity: Última activitat activity: Última activitat
@ -1784,9 +1787,15 @@ ca:
title: Apel·lació rebutjada title: Apel·lació rebutjada
backup_ready: backup_ready:
explanation: Heu demanat una còpia completa de les dades del vostre compte de Mastodon. explanation: Heu demanat una còpia completa de les dades del vostre compte de Mastodon.
extra: Ja us ho podeu baixar extra: Ja la podeu baixar
subject: L'arxiu està preparat per a descàrrega subject: L'arxiu està preparat per a descàrrega
title: Recollida de l'arxiu title: Recollida de l'arxiu
failed_2fa:
details: 'Aquests són els detalls de l''intent d''accés:'
explanation: Algú ha intentat accedir al vostre compte però no ha proporcionat un factor de doble autenticació correcte.
further_actions_html: Si no heu estat vosaltres, us recomanem que %{action} immediatament perquè pot estar compromès.
subject: Ha fallat el factor de doble autenticació
title: Ha fallat l'autenticació de doble factor
suspicious_sign_in: suspicious_sign_in:
change_password: canvia la teva contrasenya change_password: canvia la teva contrasenya
details: 'Aquest són els detalls de l''inici de sessió:' details: 'Aquest són els detalls de l''inici de sessió:'
@ -1840,6 +1849,7 @@ ca:
go_to_sso_account_settings: Ves a la configuració del compte del teu proveïdor d'identitat go_to_sso_account_settings: Ves a la configuració del compte del teu proveïdor d'identitat
invalid_otp_token: El codi de dos factors no és correcte invalid_otp_token: El codi de dos factors no és correcte
otp_lost_help_html: Si has perdut l'accés a tots dos pots contactar per %{email} otp_lost_help_html: Si has perdut l'accés a tots dos pots contactar per %{email}
rate_limited: Excessius intents d'autenticació, torneu-hi més tard.
seamless_external_login: Has iniciat sessió via un servei extern per tant els ajustos de contrasenya i correu electrònic no estan disponibles. seamless_external_login: Has iniciat sessió via un servei extern per tant els ajustos de contrasenya i correu electrònic no estan disponibles.
signed_in_as: 'Sessió iniciada com a:' signed_in_as: 'Sessió iniciada com a:'
verification: verification:

View file

@ -1843,6 +1843,7 @@ da:
go_to_sso_account_settings: Gå til identitetsudbyderens kontoindstillinger go_to_sso_account_settings: Gå til identitetsudbyderens kontoindstillinger
invalid_otp_token: Ugyldig tofaktorkode invalid_otp_token: Ugyldig tofaktorkode
otp_lost_help_html: Har du mistet adgang til begge, kan du kontakte %{email} otp_lost_help_html: Har du mistet adgang til begge, kan du kontakte %{email}
rate_limited: For mange godkendelsesforsøg. Prøv igen senere.
seamless_external_login: Du er logget ind via en ekstern tjeneste, så adgangskode- og e-mailindstillinger er utilgængelige. seamless_external_login: Du er logget ind via en ekstern tjeneste, så adgangskode- og e-mailindstillinger er utilgængelige.
signed_in_as: 'Logget ind som:' signed_in_as: 'Logget ind som:'
verification: verification:

View file

@ -1790,8 +1790,14 @@ de:
extra: Sie ist jetzt zum Herunterladen bereit! extra: Sie ist jetzt zum Herunterladen bereit!
subject: Dein persönliches Archiv kann heruntergeladen werden subject: Dein persönliches Archiv kann heruntergeladen werden
title: Archiv-Download title: Archiv-Download
failed_2fa:
details: 'Details zum Anmeldeversuch:'
explanation: Jemand hat versucht, sich bei deinem Konto anzumelden, aber die Zwei-Faktor-Authentisierung schlug fehl.
further_actions_html: Solltest du das nicht gewesen sein, empfehlen wir dir, sofort %{action}, da dein Konto möglicherweise kompromittiert ist.
subject: Zwei-Faktor-Authentisierung fehlgeschlagen
title: Zwei-Faktor-Authentisierung fehlgeschlagen
suspicious_sign_in: suspicious_sign_in:
change_password: dein Passwort ändern change_password: dein Passwort zu ändern
details: 'Hier sind die Details zu den Anmeldeversuchen:' details: 'Hier sind die Details zu den Anmeldeversuchen:'
explanation: Wir haben eine Anmeldung zu deinem Konto von einer neuen IP-Adresse festgestellt. explanation: Wir haben eine Anmeldung zu deinem Konto von einer neuen IP-Adresse festgestellt.
further_actions_html: Wenn du das nicht warst, empfehlen wir dir schnellstmöglich, %{action} und die Zwei-Faktor-Authentisierung (2FA) für dein Konto zu aktivieren, um es abzusichern. further_actions_html: Wenn du das nicht warst, empfehlen wir dir schnellstmöglich, %{action} und die Zwei-Faktor-Authentisierung (2FA) für dein Konto zu aktivieren, um es abzusichern.
@ -1843,6 +1849,7 @@ de:
go_to_sso_account_settings: Kontoeinstellungen des Identitätsanbieters aufrufen go_to_sso_account_settings: Kontoeinstellungen des Identitätsanbieters aufrufen
invalid_otp_token: Ungültiger Code der Zwei-Faktor-Authentisierung (2FA) invalid_otp_token: Ungültiger Code der Zwei-Faktor-Authentisierung (2FA)
otp_lost_help_html: Wenn du beides nicht mehr weißt, melde dich bitte bei uns unter der E-Mail-Adresse %{email} otp_lost_help_html: Wenn du beides nicht mehr weißt, melde dich bitte bei uns unter der E-Mail-Adresse %{email}
rate_limited: Zu viele Authentisierungsversuche. Bitte versuche es später noch einmal.
seamless_external_login: Du bist über einen externen Dienst angemeldet, daher sind Passwort- und E-Mail-Einstellungen nicht verfügbar. seamless_external_login: Du bist über einen externen Dienst angemeldet, daher sind Passwort- und E-Mail-Einstellungen nicht verfügbar.
signed_in_as: 'Angemeldet als:' signed_in_as: 'Angemeldet als:'
verification: verification:

View file

@ -49,19 +49,19 @@ ca:
subject: 'Mastodon: Instruccions per a reiniciar contrasenya' subject: 'Mastodon: Instruccions per a reiniciar contrasenya'
title: Contrasenya restablerta title: Contrasenya restablerta
two_factor_disabled: two_factor_disabled:
explanation: Només es pot accedir amb compte de correu i contrasenya. explanation: Ara es pot accedir amb només compte de correu i contrasenya.
subject: 'Mastodon: Autenticació de doble factor desactivada' subject: 'Mastodon: Autenticació de doble factor desactivada'
subtitle: S'ha deshabilitat l'autenticació de doble factor al vostre compte. subtitle: S'ha deshabilitat l'autenticació de doble factor al vostre compte.
title: A2F desactivada title: A2F desactivada
two_factor_enabled: two_factor_enabled:
explanation: Per accedir fa falta un token generat per l'aplicació TOTP aparellada. explanation: Per accedir cal un token generat per l'aplicació TOTP aparellada.
subject: 'Mastodon: Autenticació de doble factor activada' subject: 'Mastodon: Autenticació de doble factor activada'
subtitle: S'ha habilitat l'autenticació de doble factor al vostre compte. subtitle: S'ha habilitat l'autenticació de doble factor al vostre compte.
title: A2F activada title: A2F activada
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: Els codis de recuperació anteriors ja no són vàlids i se n'han generat de nous. explanation: Els codis de recuperació anteriors ja no són vàlids i se n'han generat de nous.
subject: 'Mastodon: codis de recuperació de doble factor regenerats' subject: 'Mastodon: codis de recuperació de doble factor regenerats'
subtitle: S'han invalidat els codis de recuperació anteriors i se n'ha generat de nous. subtitle: S'han invalidat els codis de recuperació anteriors i se n'han generat de nous.
title: Codis de recuperació A2F canviats title: Codis de recuperació A2F canviats
unlock_instructions: unlock_instructions:
subject: 'Mastodon: Instruccions per a desblocar' subject: 'Mastodon: Instruccions per a desblocar'
@ -76,7 +76,7 @@ ca:
title: Una de les teves claus de seguretat ha estat esborrada title: Una de les teves claus de seguretat ha estat esborrada
webauthn_disabled: webauthn_disabled:
explanation: S'ha deshabilitat l'autenticació amb claus de seguretat al vostre compte. explanation: S'ha deshabilitat l'autenticació amb claus de seguretat al vostre compte.
extra: Ara només podeu accedir amb el token generat amb l'aplicació TOTP aparellada. extra: Ara es pot accedir amb només el token generat amb l'aplicació TOTP aparellada.
subject: 'Mastodon: S''ha desactivat l''autenticació amb claus de seguretat' subject: 'Mastodon: S''ha desactivat l''autenticació amb claus de seguretat'
title: Claus de seguretat desactivades title: Claus de seguretat desactivades
webauthn_enabled: webauthn_enabled:

View file

@ -47,14 +47,19 @@ fi:
subject: 'Mastodon: ohjeet salasanan vaihtoon' subject: 'Mastodon: ohjeet salasanan vaihtoon'
title: Salasanan vaihto title: Salasanan vaihto
two_factor_disabled: two_factor_disabled:
explanation: Sisäänkirjautuminen on nyt mahdollista pelkällä sähköpostiosoitteella ja salasanalla.
subject: 'Mastodon: kaksivaiheinen todennus poistettu käytöstä' subject: 'Mastodon: kaksivaiheinen todennus poistettu käytöstä'
subtitle: Kaksivaiheinen todennus on poistettu käytöstä tililtäsi.
title: 2-vaiheinen todennus pois käytöstä title: 2-vaiheinen todennus pois käytöstä
two_factor_enabled: two_factor_enabled:
explanation: Sisäänkirjautuminen edellyttää liitetyn TOTP-sovelluksen luomaa aikarajattua kertatunnuslukua.
subject: 'Mastodon: kaksivaiheinen todennus otettu käyttöön' subject: 'Mastodon: kaksivaiheinen todennus otettu käyttöön'
subtitle: Kaksivaiheinen todennus on otettu käyttöön tilillesi.
title: 2-vaiheinen todennus käytössä title: 2-vaiheinen todennus käytössä
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: Uudet palautuskoodit on nyt luotu ja vanhat on mitätöity. explanation: Uudet palautuskoodit on nyt luotu ja vanhat on mitätöity.
subject: 'Mastodon: kaksivaiheisen todennuksen palautuskoodit luotiin uudelleen' subject: 'Mastodon: kaksivaiheisen todennuksen palautuskoodit luotiin uudelleen'
subtitle: Aiemmat palautuskoodit on mitätöity ja tilalle on luotu uudet.
title: 2-vaiheisen todennuksen palautuskoodit vaihdettiin title: 2-vaiheisen todennuksen palautuskoodit vaihdettiin
unlock_instructions: unlock_instructions:
subject: 'Mastodon: lukituksen poistamisen ohjeet' subject: 'Mastodon: lukituksen poistamisen ohjeet'
@ -68,9 +73,13 @@ fi:
subject: 'Mastodon: suojausavain poistettu' subject: 'Mastodon: suojausavain poistettu'
title: Yksi suojausavaimistasi on poistettu title: Yksi suojausavaimistasi on poistettu
webauthn_disabled: webauthn_disabled:
explanation: Turva-avaimin kirjautuminen on poistettu käytöstä tililtäsi.
extra: Sisäänkirjautuminen on nyt mahdollista pelkällä palveluun liitetyn TOTP-sovelluksen luomalla aikarajoitteisella kertatunnusluvulla.
subject: 'Mastodon: Todennus suojausavaimilla poistettu käytöstä' subject: 'Mastodon: Todennus suojausavaimilla poistettu käytöstä'
title: Suojausavaimet poistettu käytöstä title: Suojausavaimet poistettu käytöstä
webauthn_enabled: webauthn_enabled:
explanation: Turva-avaimella kirjautuminen on otettu käyttöön tilillesi.
extra: Voit nyt kirjautua sisään turva-avaimellasi.
subject: 'Mastodon: Todennus suojausavaimella on otettu käyttöön' subject: 'Mastodon: Todennus suojausavaimella on otettu käyttöön'
title: Suojausavaimet käytössä title: Suojausavaimet käytössä
omniauth_callbacks: omniauth_callbacks:

View file

@ -47,14 +47,19 @@ hu:
subject: 'Mastodon: Jelszóvisszaállítási utasítások' subject: 'Mastodon: Jelszóvisszaállítási utasítások'
title: Jelszó visszaállítása title: Jelszó visszaállítása
two_factor_disabled: two_factor_disabled:
explanation: A bejelentkezés most már csupán email címmel és jelszóval lehetséges.
subject: Kétlépcsős azonosítás kikapcsolva subject: Kétlépcsős azonosítás kikapcsolva
subtitle: A kétlépcsős hitelesítés a fiókodhoz ki lett kapcsolva.
title: Kétlépcsős hitelesítés kikapcsolva title: Kétlépcsős hitelesítés kikapcsolva
two_factor_enabled: two_factor_enabled:
explanation: Egy párosított TOTP appal generált tokenre lesz szükség a bejelentkezéshez.
subject: 'Mastodon: Kétlépcsős azonosítás engedélyezve' subject: 'Mastodon: Kétlépcsős azonosítás engedélyezve'
subtitle: A kétlépcsős hitelesítés a fiókodhoz aktiválva lett.
title: Kétlépcsős hitelesítés engedélyezve title: Kétlépcsős hitelesítés engedélyezve
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: A korábbi helyreállítási kódok letiltásra és újragenerálásra kerültek. explanation: A korábbi helyreállítási kódok letiltásra és újragenerálásra kerültek.
subject: 'Mastodon: Kétlépcsős helyreállítási kódok újból előállítva' subject: 'Mastodon: Kétlépcsős helyreállítási kódok újból előállítva'
subtitle: A korábbi helyreállítási kódokat letiltottuk, és újakat generáltunk.
title: A kétlépcsős kódok megváltoztak title: A kétlépcsős kódok megváltoztak
unlock_instructions: unlock_instructions:
subject: 'Mastodon: Feloldási utasítások' subject: 'Mastodon: Feloldási utasítások'
@ -68,9 +73,13 @@ hu:
subject: 'Mastodon: A biztonsági kulcs törlésre került' subject: 'Mastodon: A biztonsági kulcs törlésre került'
title: Az egyik biztonsági kulcsodat törölték title: Az egyik biztonsági kulcsodat törölték
webauthn_disabled: webauthn_disabled:
explanation: A biztonsági kulcsokkal történő hitelesítés a fiókodhoz ki lett kapcsolva.
extra: A bejelentkezés most már csak TOTP app által generált tokennel lehetséges.
subject: 'Mastodon: A biztonsági kulccsal történő hitelesítés letiltásra került' subject: 'Mastodon: A biztonsági kulccsal történő hitelesítés letiltásra került'
title: A biztonsági kulcsok letiltásra kerültek title: A biztonsági kulcsok letiltásra kerültek
webauthn_enabled: webauthn_enabled:
explanation: A biztonsági kulcsokkal történő hitelesítés a fiókodhoz aktiválva lett.
extra: A biztonsági kulcsodat mostantól lehet bejelentkezésre használni.
subject: 'Mastodon: A biztonsági kulcsos hitelesítés engedélyezésre került' subject: 'Mastodon: A biztonsági kulcsos hitelesítés engedélyezésre került'
title: A biztonsági kulcsok engedélyezésre kerültek title: A biztonsági kulcsok engedélyezésre kerültek
omniauth_callbacks: omniauth_callbacks:

View file

@ -52,6 +52,7 @@ ie:
subtitle: 2-factor autentication por tui conto ha esset desactivisat. subtitle: 2-factor autentication por tui conto ha esset desactivisat.
title: 2FA desvalidat title: 2FA desvalidat
two_factor_enabled: two_factor_enabled:
explanation: Un clave generat del acuplat TOTP-aplication nu va esser besonat por aperter session.
subject: 'Mastodon: 2-factor autentication activat' subject: 'Mastodon: 2-factor autentication activat'
subtitle: 2-factor autentication ha esset activisat por tui conto. subtitle: 2-factor autentication ha esset activisat por tui conto.
title: 2FA permisset title: 2FA permisset
@ -73,6 +74,7 @@ ie:
title: Un ex tui claves de securitá ha esset deletet title: Un ex tui claves de securitá ha esset deletet
webauthn_disabled: webauthn_disabled:
explanation: Autentication per clave de securitá ha esset desactivisat por tui conto. explanation: Autentication per clave de securitá ha esset desactivisat por tui conto.
extra: Aperter session es nu possibil solmen per li clave generat del acuplat TOTP-aplication.
subject: 'Mastodon: Autentication con claves de securitá desactivisat' subject: 'Mastodon: Autentication con claves de securitá desactivisat'
title: Claves de securitá desactivisat title: Claves de securitá desactivisat
webauthn_enabled: webauthn_enabled:

View file

@ -49,12 +49,12 @@ ja:
two_factor_disabled: two_factor_disabled:
explanation: メールアドレスとパスワードのみでログイン可能になりました。 explanation: メールアドレスとパスワードのみでログイン可能になりました。
subject: 'Mastodon: 二要素認証が無効になりました' subject: 'Mastodon: 二要素認証が無効になりました'
subtitle: 二要素認証が無効になっています subtitle: 今後、アカウントへのログインに二要素認証を要求しません
title: 二要素認証が無効化されました title: 二要素認証が無効化されました
two_factor_enabled: two_factor_enabled:
explanation: ログインには設定済みのTOTPアプリが生成したトークンが必要です。 explanation: ログインには設定済みのTOTPアプリが生成したトークンが必要です。
subject: 'Mastodon: 二要素認証が有効になりました' subject: 'Mastodon: 二要素認証が有効になりました'
subtitle: 二要素認証が有効になりました subtitle: 今後、アカウントへのログインに二要素認証が必要になります
title: 二要素認証が有効化されました title: 二要素認証が有効化されました
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: 以前のリカバリーコードが無効化され、新しいコードが生成されました。 explanation: 以前のリカバリーコードが無効化され、新しいコードが生成されました。
@ -73,7 +73,7 @@ ja:
subject: 'Mastodon: セキュリティキーが削除されました' subject: 'Mastodon: セキュリティキーが削除されました'
title: セキュリティキーが削除されました title: セキュリティキーが削除されました
webauthn_disabled: webauthn_disabled:
explanation: セキュリティキー認証が無効になっています explanation: セキュリティキー認証が無効になりました
extra: 設定済みのTOTPアプリが生成したトークンのみでログインできるようになりました。 extra: 設定済みのTOTPアプリが生成したトークンのみでログインできるようになりました。
subject: 'Mastodon: セキュリティキー認証が無効になりました' subject: 'Mastodon: セキュリティキー認証が無効になりました'
title: セキュリティキーは無効になっています title: セキュリティキーは無効になっています

View file

@ -47,14 +47,19 @@ ko:
subject: 'Mastodon: 암호 재설정 설명' subject: 'Mastodon: 암호 재설정 설명'
title: 암호 재설정 title: 암호 재설정
two_factor_disabled: two_factor_disabled:
explanation: 이제 이메일과 암호만 이용해서 로그인이 가능합니다.
subject: '마스토돈: 이중 인증 비활성화' subject: '마스토돈: 이중 인증 비활성화'
subtitle: 계정에 대한 2단계 인증이 비활성화되었습니다.
title: 2FA 비활성화 됨 title: 2FA 비활성화 됨
two_factor_enabled: two_factor_enabled:
explanation: 로그인 하기 위해서는 짝이 되는 TOTP 앱에서 생성한 토큰이 필요합니다.
subject: '마스토돈: 이중 인증 활성화' subject: '마스토돈: 이중 인증 활성화'
subtitle: 계정에 대한 2단계 인증이 활성화되었습니다.
title: 2FA 활성화 됨 title: 2FA 활성화 됨
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: 이전 복구 코드가 무효화되고 새 코드가 생성되었습니다 explanation: 이전 복구 코드가 무효화되고 새 코드가 생성되었습니다
subject: '마스토돈: 이중 인증 복구 코드 재생성 됨' subject: '마스토돈: 이중 인증 복구 코드 재생성 됨'
subtitle: 이전 복구 코드가 무효화되고 새 코드가 생성되었습니다.
title: 2FA 복구 코드 변경됨 title: 2FA 복구 코드 변경됨
unlock_instructions: unlock_instructions:
subject: '마스토돈: 잠금 해제 방법' subject: '마스토돈: 잠금 해제 방법'
@ -68,9 +73,13 @@ ko:
subject: '마스토돈: 보안 키 삭제' subject: '마스토돈: 보안 키 삭제'
title: 보안 키가 삭제되었습니다 title: 보안 키가 삭제되었습니다
webauthn_disabled: webauthn_disabled:
explanation: 계정의 보안 키 인증이 비활성화되었습니다
extra: 이제 TOTP 앱에서 생성한 토큰을 통해서만 로그인 가능합니다.
subject: '마스토돈: 보안 키를 이용한 인증이 비활성화 됨' subject: '마스토돈: 보안 키를 이용한 인증이 비활성화 됨'
title: 보안 키 비활성화 됨 title: 보안 키 비활성화 됨
webauthn_enabled: webauthn_enabled:
explanation: 계정에 대한 보안키 인증이 활성화되었습니다.
extra: 로그인시 보안키가 사용됩니다.
subject: '마스토돈: 보안 키 인증 활성화 됨' subject: '마스토돈: 보안 키 인증 활성화 됨'
title: 보안 키 활성화 됨 title: 보안 키 활성화 됨
omniauth_callbacks: omniauth_callbacks:

View file

@ -47,10 +47,14 @@ lad:
subject: 'Mastodon: Instruksyones para reinisyar kod' subject: 'Mastodon: Instruksyones para reinisyar kod'
title: Reinisyar kod title: Reinisyar kod
two_factor_disabled: two_factor_disabled:
explanation: Agora puedes konektarte kon tu kuento uzando solo tu adreso de posta i kod.
subject: 'Mastodon: La autentifikasyon de dos pasos esta inkapasitada' subject: 'Mastodon: La autentifikasyon de dos pasos esta inkapasitada'
subtitle: La autentifikasyon en dos pasos para tu kuento tiene sido inkapasitada.
title: Autentifikasyon 2FA inkapasitada title: Autentifikasyon 2FA inkapasitada
two_factor_enabled: two_factor_enabled:
explanation: Se rekierira un token djenerado por la aplikasyon TOTP konektada para entrar.
subject: 'Mastodon: La autentifikasyon de dos pasos esta kapasitada' subject: 'Mastodon: La autentifikasyon de dos pasos esta kapasitada'
subtitle: La autentifikasyon de dos pasos para tu kuento tiene sido kapasitada.
title: Autentifikasyon 2FA aktivada title: Autentifikasyon 2FA aktivada
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: Los kodiches de rekuperasyon previos tienen sido invalidados i se djeneraron kodiches muevos. explanation: Los kodiches de rekuperasyon previos tienen sido invalidados i se djeneraron kodiches muevos.
@ -69,9 +73,13 @@ lad:
subject: 'Mastodon: Yave de sigurita supremida' subject: 'Mastodon: Yave de sigurita supremida'
title: Una de tus yaves de sigurita tiene sido supremida title: Una de tus yaves de sigurita tiene sido supremida
webauthn_disabled: webauthn_disabled:
explanation: La autentifikasyon kon yaves de sigurita tiene sido inkapasitada para tu kuento.
extra: Agora el inisyo de sesyon solo es posivle utilizando el token djeenerado por la aplikasyon TOTP konektada.
subject: 'Mastodon: autentifikasyon kon yaves de sigurita inkapasitada' subject: 'Mastodon: autentifikasyon kon yaves de sigurita inkapasitada'
title: Yaves de sigurita inkapasitadas title: Yaves de sigurita inkapasitadas
webauthn_enabled: webauthn_enabled:
explanation: La autentifikasyon kon yave de sigurita tiene sido kapasitada para tu kuento.
extra: Agora tu yave de sigurita puede ser utilizada para konektarte kon tu kuento.
subject: 'Mastodon: Autentifikasyon de yave de sigurita aktivada' subject: 'Mastodon: Autentifikasyon de yave de sigurita aktivada'
title: Yaves de sigurita kapasitadas title: Yaves de sigurita kapasitadas
omniauth_callbacks: omniauth_callbacks:

View file

@ -47,14 +47,19 @@ nn:
subject: 'Mastodon: Instuksjonar for å endra passord' subject: 'Mastodon: Instuksjonar for å endra passord'
title: Attstilling av passord title: Attstilling av passord
two_factor_disabled: two_factor_disabled:
explanation: Innlogging er nå mulig med kun e-postadresse og passord.
subject: 'Mastodon: To-faktor-autentisering deaktivert' subject: 'Mastodon: To-faktor-autentisering deaktivert'
subtitle: To-faktor autentisering for din konto har blitt deaktivert.
title: 2FA deaktivert title: 2FA deaktivert
two_factor_enabled: two_factor_enabled:
explanation: En token generert av den sammenkoblede TOTP-appen vil være påkrevd for innlogging.
subject: 'Mastodon: To-faktor-autentisering aktivert' subject: 'Mastodon: To-faktor-autentisering aktivert'
subtitle: Tofaktorautentisering er aktivert for din konto.
title: 2FA aktivert title: 2FA aktivert
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: Dei førre gjenopprettingskodane er ugyldige og nye er genererte. explanation: Dei førre gjenopprettingskodane er ugyldige og nye er genererte.
subject: 'Mastodon: To-faktor-gjenopprettingskodar har vorte genererte på nytt' subject: 'Mastodon: To-faktor-gjenopprettingskodar har vorte genererte på nytt'
subtitle: De forrige gjenopprettingskodene er gjort ugyldige og nye er generert.
title: 2FA-gjenopprettingskodane er endra title: 2FA-gjenopprettingskodane er endra
unlock_instructions: unlock_instructions:
subject: 'Mastodon: Instruksjonar for å opne kontoen igjen' subject: 'Mastodon: Instruksjonar for å opne kontoen igjen'
@ -68,9 +73,13 @@ nn:
subject: 'Mastodon: Sikkerheitsnøkkel sletta' subject: 'Mastodon: Sikkerheitsnøkkel sletta'
title: Ein av sikkerheitsnøklane dine har blitt sletta title: Ein av sikkerheitsnøklane dine har blitt sletta
webauthn_disabled: webauthn_disabled:
explanation: Autentisering med sikkerhetsnøkler er deaktivert for kontoen din.
extra: Innlogging er nå mulig med kun tilgangstoken generert av den sammenkoblede TOTP-appen.
subject: 'Mastodon: Autentisering med sikkerheitsnøklar vart skrudd av' subject: 'Mastodon: Autentisering med sikkerheitsnøklar vart skrudd av'
title: Sikkerheitsnøklar deaktivert title: Sikkerheitsnøklar deaktivert
webauthn_enabled: webauthn_enabled:
explanation: Sikkerhetsnøkkelautentisering har blitt aktivert for kontoen din.
extra: Sikkerhetsnøkkelen din kan nå bli brukt for innlogging.
subject: 'Mastodon: Sikkerheitsnøkkelsautentisering vart skrudd på' subject: 'Mastodon: Sikkerheitsnøkkelsautentisering vart skrudd på'
title: Sikkerheitsnøklar aktivert title: Sikkerheitsnøklar aktivert
omniauth_callbacks: omniauth_callbacks:

View file

@ -47,14 +47,19 @@
subject: 'Mastodon: Hvordan nullstille passord' subject: 'Mastodon: Hvordan nullstille passord'
title: Nullstill passord title: Nullstill passord
two_factor_disabled: two_factor_disabled:
explanation: Innlogging er nå mulig med kun e-postadresse og passord.
subject: 'Mastodon: Tofaktorautentisering deaktivert' subject: 'Mastodon: Tofaktorautentisering deaktivert'
subtitle: To-faktor autentisering for din konto har blitt deaktivert.
title: 2FA deaktivert title: 2FA deaktivert
two_factor_enabled: two_factor_enabled:
explanation: En token generert av den sammenkoblede TOTP-appen vil være påkrevd for innlogging.
subject: 'Mastodon: Tofaktorautentisering aktivert' subject: 'Mastodon: Tofaktorautentisering aktivert'
subtitle: Tofaktorautentisering er aktivert for din konto.
title: 2FA aktivert title: 2FA aktivert
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: De forrige gjenopprettingskodene er gjort ugyldige og nye er generert. explanation: De forrige gjenopprettingskodene er gjort ugyldige og nye er generert.
subject: 'Mastodon: Tofaktor-gjenopprettingskoder har blitt generert på nytt' subject: 'Mastodon: Tofaktor-gjenopprettingskoder har blitt generert på nytt'
subtitle: De forrige gjenopprettingskodene er gjort ugyldige og nye er generert.
title: 2FA-gjenopprettingskodene ble endret title: 2FA-gjenopprettingskodene ble endret
unlock_instructions: unlock_instructions:
subject: 'Mastodon: Instruksjoner for å gjenåpne konto' subject: 'Mastodon: Instruksjoner for å gjenåpne konto'
@ -68,9 +73,13 @@
subject: 'Mastodon: Sikkerhetsnøkkel slettet' subject: 'Mastodon: Sikkerhetsnøkkel slettet'
title: En av sikkerhetsnøklene dine har blitt slettet title: En av sikkerhetsnøklene dine har blitt slettet
webauthn_disabled: webauthn_disabled:
explanation: Autentisering med sikkerhetsnøkler er deaktivert for kontoen din.
extra: Innlogging er nå mulig med kun tilgangstoken generert av den sammenkoblede TOTP-appen.
subject: 'Mastodon: Autentisering med sikkerhetsnøkler ble skrudd av' subject: 'Mastodon: Autentisering med sikkerhetsnøkler ble skrudd av'
title: Sikkerhetsnøkler deaktivert title: Sikkerhetsnøkler deaktivert
webauthn_enabled: webauthn_enabled:
explanation: Sikkerhetsnøkkelautentisering har blitt aktivert for kontoen din.
extra: Sikkerhetsnøkkelen din kan nå bli brukt for innlogging.
subject: 'Mastodon: Sikkerhetsnøkkelsautentisering ble skrudd på' subject: 'Mastodon: Sikkerhetsnøkkelsautentisering ble skrudd på'
title: Sikkerhetsnøkler aktivert title: Sikkerhetsnøkler aktivert
omniauth_callbacks: omniauth_callbacks:

View file

@ -47,14 +47,19 @@ sl:
subject: 'Mastodon: navodila za ponastavitev gesla' subject: 'Mastodon: navodila za ponastavitev gesla'
title: Ponastavitev gesla title: Ponastavitev gesla
two_factor_disabled: two_factor_disabled:
explanation: Prijava je sedaj mogoče le z uporabo e-poštnega naslova in gesla.
subject: 'Mastodon: dvojno preverjanje pristnosti je onemogočeno' subject: 'Mastodon: dvojno preverjanje pristnosti je onemogočeno'
subtitle: Dvo-faktorsko preverjanje pristnosti za vaš račun je bilo onemogočeno.
title: 2FA onemogočeno title: 2FA onemogočeno
two_factor_enabled: two_factor_enabled:
explanation: Za prijavo bo zahtevan žeton, ustvarjen s povezano aplikacijo TOTP.
subject: 'Mastodon: dvojno preverjanje pristnosti je omogočeno' subject: 'Mastodon: dvojno preverjanje pristnosti je omogočeno'
subtitle: Dvo-faktorsko preverjanje pristnosti za vaš račun je bilo omogočeno.
title: 2FA omogočeno title: 2FA omogočeno
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: Prejšnje obnovitvene kode so postale neveljavne in ustvarjene so bile nove. explanation: Prejšnje obnovitvene kode so postale neveljavne in ustvarjene so bile nove.
subject: 'Mastodon: varnostne obnovitvene kode za dvojno preverjanje pristnosti so ponovno izdelane' subject: 'Mastodon: varnostne obnovitvene kode za dvojno preverjanje pristnosti so ponovno izdelane'
subtitle: Prejšnje kode za obnovitev so bile razveljavljene, ustvarjene pa so bile nove.
title: obnovitvene kode 2FA spremenjene title: obnovitvene kode 2FA spremenjene
unlock_instructions: unlock_instructions:
subject: 'Mastodon: navodila za odklepanje' subject: 'Mastodon: navodila za odklepanje'
@ -68,9 +73,13 @@ sl:
subject: 'Mastodon: varnostna koda izbrisana' subject: 'Mastodon: varnostna koda izbrisana'
title: Ena od vaših varnostnih kod je bila izbrisana title: Ena od vaših varnostnih kod je bila izbrisana
webauthn_disabled: webauthn_disabled:
explanation: Preverjanje pristnosti z varnostnimi ključi za vaš račun je bilo onemogočeno.
extra: Prijava je sedaj mogoče le z uporabo žetona, ustvarjenega s povezano aplikacijo TOTP.
subject: 'Mastodon: overjanje pristnosti z varnosnimi kodami je onemogočeno' subject: 'Mastodon: overjanje pristnosti z varnosnimi kodami je onemogočeno'
title: Varnostne kode onemogočene title: Varnostne kode onemogočene
webauthn_enabled: webauthn_enabled:
explanation: Preverjanje pristnosti z varnostnimi ključi za vaš račun je bilo omogočeno.
extra: Za prijavo sedaj lahko uporabite svoj varnostni ključ.
subject: 'Mastodon: preverjanje pristnosti z varnostno kodo je omogočeno' subject: 'Mastodon: preverjanje pristnosti z varnostno kodo je omogočeno'
title: Varnostne kode omogočene title: Varnostne kode omogočene
omniauth_callbacks: omniauth_callbacks:

View file

@ -47,14 +47,19 @@ th:
subject: 'Mastodon: คำแนะนำการตั้งรหัสผ่านใหม่' subject: 'Mastodon: คำแนะนำการตั้งรหัสผ่านใหม่'
title: การตั้งรหัสผ่านใหม่ title: การตั้งรหัสผ่านใหม่
two_factor_disabled: two_factor_disabled:
explanation: ตอนนี้สามารถเข้าสู่ระบบได้โดยใช้เพียงที่อยู่อีเมลและรหัสผ่านเท่านั้น
subject: 'Mastodon: ปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยแล้ว' subject: 'Mastodon: ปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยแล้ว'
subtitle: ปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยสำหรับบัญชีของคุณแล้ว
title: ปิดใช้งาน 2FA แล้ว title: ปิดใช้งาน 2FA แล้ว
two_factor_enabled: two_factor_enabled:
explanation: จะต้องใช้โทเคนที่สร้างโดยแอป TOTP ที่จับคู่สำหรับการเข้าสู่ระบบ
subject: 'Mastodon: เปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยแล้ว' subject: 'Mastodon: เปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยแล้ว'
subtitle: เปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยสำหรับบัญชีของคุณแล้ว
title: เปิดใช้งาน 2FA แล้ว title: เปิดใช้งาน 2FA แล้ว
two_factor_recovery_codes_changed: two_factor_recovery_codes_changed:
explanation: ยกเลิกรหัสกู้คืนก่อนหน้านี้และสร้างรหัสใหม่แล้ว explanation: ยกเลิกรหัสกู้คืนก่อนหน้านี้และสร้างรหัสกู้คืนใหม่แล้ว
subject: 'Mastodon: สร้างรหัสกู้คืนสองปัจจัยใหม่แล้ว' subject: 'Mastodon: สร้างรหัสกู้คืนสองปัจจัยใหม่แล้ว'
subtitle: ยกเลิกรหัสกู้คืนก่อนหน้านี้และสร้างรหัสกู้คืนใหม่แล้ว
title: เปลี่ยนรหัสกู้คืน 2FA แล้ว title: เปลี่ยนรหัสกู้คืน 2FA แล้ว
unlock_instructions: unlock_instructions:
subject: 'Mastodon: คำแนะนำการปลดล็อค' subject: 'Mastodon: คำแนะนำการปลดล็อค'
@ -68,9 +73,13 @@ th:
subject: 'Mastodon: ลบกุญแจความปลอดภัยแล้ว' subject: 'Mastodon: ลบกุญแจความปลอดภัยแล้ว'
title: ลบหนึ่งในกุญแจความปลอดภัยของคุณแล้ว title: ลบหนึ่งในกุญแจความปลอดภัยของคุณแล้ว
webauthn_disabled: webauthn_disabled:
explanation: ปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยสำหรับบัญชีของคุณแล้ว
extra: ตอนนี้สามารถเข้าสู่ระบบได้โดยใช้เพียงโทเคนที่สร้างโดยแอป TOTP ที่จับคู่เท่านั้น
subject: 'Mastodon: ปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยแล้ว' subject: 'Mastodon: ปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยแล้ว'
title: ปิดใช้งานกุญแจความปลอดภัยแล้ว title: ปิดใช้งานกุญแจความปลอดภัยแล้ว
webauthn_enabled: webauthn_enabled:
explanation: เปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยสำหรับบัญชีของคุณแล้ว
extra: ตอนนี้สามารถใช้กุญแจความปลอดภัยของคุณสำหรับการเข้าสู่ระบบ
subject: 'Mastodon: เปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยแล้ว' subject: 'Mastodon: เปิดใช้งานการรับรองความถูกต้องด้วยกุญแจความปลอดภัยแล้ว'
title: เปิดใช้งานกุญแจความปลอดภัยแล้ว title: เปิดใช้งานกุญแจความปลอดภัยแล้ว
omniauth_callbacks: omniauth_callbacks:

View file

@ -17,6 +17,7 @@ ia:
index: index:
application: Application application: Application
delete: Deler delete: Deler
empty: Tu non ha applicationes.
name: Nomine name: Nomine
new: Nove application new: Nove application
show: Monstrar show: Monstrar
@ -47,6 +48,7 @@ ia:
title: title:
accounts: Contos accounts: Contos
admin/accounts: Gestion de contos admin/accounts: Gestion de contos
all: Accesso plen a tu conto de Mastodon
bookmarks: Marcapaginas bookmarks: Marcapaginas
conversations: Conversationes conversations: Conversationes
favourites: Favoritos favourites: Favoritos
@ -61,8 +63,15 @@ ia:
applications: Applicationes applications: Applicationes
oauth2_provider: Fornitor OAuth2 oauth2_provider: Fornitor OAuth2
scopes: scopes:
read:favourites: vider tu favoritos
read:lists: vider tu listas
read:notifications: vider tu notificationes
read:statuses: vider tote le messages
write:accounts: modificar tu profilo write:accounts: modificar tu profilo
write:blocks: blocar contos e dominios
write:favourites: messages favorite write:favourites: messages favorite
write:filters: crear filtros
write:lists: crear listas write:lists: crear listas
write:media: incargar files de medios
write:notifications: rader tu notificationes write:notifications: rader tu notificationes
write:statuses: publicar messages write:statuses: publicar messages

Some files were not shown because too many files have changed in this diff Show more