Skip to content

Commit

Permalink
Merge pull request #2580 from ClearlyClaire/glitch-soc/merge-upstream
Browse files Browse the repository at this point in the history
Merge upstream changes up to e2d9635
  • Loading branch information
ClearlyClaire authored Jan 24, 2024
2 parents 915cd36 + 72bae7e commit 634acd0
Show file tree
Hide file tree
Showing 108 changed files with 1,444 additions and 596 deletions.
3 changes: 0 additions & 3 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,12 @@ Rails/WhereExists:
- 'app/lib/activitypub/activity/create.rb'
- 'app/lib/delivery_failure_tracker.rb'
- 'app/lib/feed_manager.rb'
- 'app/lib/status_cache_hydrator.rb'
- 'app/lib/suspicious_sign_in_detector.rb'
- 'app/models/poll.rb'
- 'app/models/session_activation.rb'
- 'app/models/status.rb'
- 'app/policies/status_policy.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'
Expand Down Expand Up @@ -137,7 +135,6 @@ Style/FetchEnvVar:
# AllowedMethods: redirect
Style/FormatStringToken:
Exclude:
- 'app/models/privacy_policy.rb'
- 'config/initializers/devise.rb'
- 'lib/paperclip/color_extractor.rb'

Expand Down
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.2.2
3.2.3
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM}

# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"]
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.3"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
ARG NODE_MAJOR_VERSION="20"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm"
# 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
# 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

# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
Expand Down
36 changes: 27 additions & 9 deletions FEDERATION.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,49 @@
## 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.

Supported vocabulary: https://docs.joinmastodon.org/spec/activitypub/
- [Supported ActivityPub vocabulary](https://docs.joinmastodon.org/spec/activitypub/)

### Required extensions

#### Webfinger
#### WebFinger

In Mastodon, users are identified by a `username` and `domain` pair (e.g., `[email protected]`).
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.

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

In order to authenticate activities, Mastodon relies on HTTP Signatures, signing every `POST` and `GET` request to other ActivityPub implementations on behalf of the user authoring an activity (for `POST` requests) or an actor representing the Mastodon server itself (for most `GET` requests).

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

- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld
- 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
- [Linked-Data Signatures](https://docs.joinmastodon.org/spec/security/#ld)
- [Bearcaps](https://docs.joinmastodon.org/spec/bearcaps/)

### Additional documentation

- [Mastodon documentation](https://docs.joinmastodon.org/)
10 changes: 5 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ GEM
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.1.5)
bigdecimal (3.1.6)
bindata (2.4.15)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
Expand Down Expand Up @@ -398,12 +398,12 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
kt-paperclip (7.2.1)
kt-paperclip (7.2.2)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
marcel (~> 1.0.1)
mime-types
terrapin (~> 0.6.0)
terrapin (>= 0.6.0, < 2.0)
language_server-protocol (3.17.0.3)
launchy (2.5.2)
addressable (~> 2.8)
Expand Down Expand Up @@ -600,8 +600,8 @@ GEM
rdf (3.3.1)
bcp47_spec (~> 0.2)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.6.1)
rdf (~> 3.2)
rdf-normalize (0.7.0)
rdf (~> 3.3)
rdoc (6.6.2)
psych (>= 4.0.0)
redcarpet (3.6.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def uri_prefix
end

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

def collection_presenter
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/admin/action_logs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class ActionLogsController < BaseController

def 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

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def load_accounts
return [] if hide_results?

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
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def load_accounts
return [] if hide_results?

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
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def index

def load_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
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def index

def load_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
end

Expand Down
27 changes: 27 additions & 0 deletions app/controllers/auth/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# frozen_string_literal: true

class Auth::SessionsController < Devise::SessionsController
include Redisable

MAX_2FA_ATTEMPTS_PER_HOUR = 10

layout 'auth'

skip_before_action :check_self_destruct!
Expand Down Expand Up @@ -135,9 +139,23 @@ def clear_attempt_from_session
session.delete(:attempt_user_updated_at)
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)
@on_authentication_success_called = true

clear_2fa_attempt_from_user(user)
clear_attempt_from_session

user.update_sign_in!(new_sign_in: true)
Expand Down Expand Up @@ -168,5 +186,14 @@ def on_authentication_failure(user, security_measure, failure_reason)
ip: request.remote_ip,
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
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ def authenticate_with_two_factor_via_webauthn(user)
end

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)
on_authentication_success(user, :otp)
else
Expand Down
12 changes: 6 additions & 6 deletions app/helpers/jsonld_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def safe_for_forwarding?(original, compacted)
end
end

def fetch_resource(uri, id, on_behalf_of = nil)
def fetch_resource(uri, id, on_behalf_of = nil, request_options: {})
unless id
json = fetch_resource_without_id_validation(uri, on_behalf_of)

Expand All @@ -164,14 +164,14 @@ def fetch_resource(uri, id, on_behalf_of = nil)
uri = json['id']
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
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

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

body_to_json(response.body_with_limit) if response.code == 200
Expand Down Expand Up @@ -204,8 +204,8 @@ def response_error_unsalvageable?(response)
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
end

def build_request(uri, on_behalf_of = nil)
Request.new(:get, uri).tap do |request|
def build_request(uri, on_behalf_of = nil, options: {})
Request.new(:get, uri, **options).tap do |request|
request.on_behalf_of(on_behalf_of) if on_behalf_of
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
end
Expand Down
7 changes: 6 additions & 1 deletion app/javascript/flavours/glitch/actions/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ export const openURL = routerHistory => (dispatch, getState) => {

export const clickSearchResult = (q, type) => (dispatch, getState) => {
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 current = previous.add(fromJS({ type, q })).takeLast(4);

Expand Down Expand Up @@ -198,4 +203,4 @@ export const hydrateSearch = () => (dispatch, getState) => {
if (history !== null) {
dispatch(updateSearchHistory(history));
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ class Search extends PureComponent {
};

defaultOptions = [
{ 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:'); } },
{ 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:'); } },
{ 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:'); } },
{ 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-has', label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
{ key: 'prompt-is', label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
{ key: 'prompt-language', label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
{ key: 'prompt-from', label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
{ key: 'prompt-before', label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
{ key: 'prompt-during', label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
{ key: 'prompt-after', label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
{ key: 'prompt-in', label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } }
];

setRef = c => {
Expand Down Expand Up @@ -263,6 +263,8 @@ class Search extends PureComponent {
const { recent } = this.props;

return recent.toArray().map(search => ({
key: `${search.get('type')}/${search.get('q')}`,

label: labelForRecentSearch(search),

action: () => this.handleRecentSearchClick(search),
Expand Down Expand Up @@ -347,8 +349,8 @@ class Search extends PureComponent {
<h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4>

<div className='search__popout__menu'>
{recent.size > 0 ? this._getOptions().map(({ label, action, forget }, i) => (
<button key={label} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
{recent.size > 0 ? this._getOptions().map(({ label, key, action, forget }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
<span>{label}</span>
<button className='icon-button' onMouseDown={forget}><Icon id='times' icon={CloseIcon} /></button>
</button>
Expand Down
5 changes: 2 additions & 3 deletions app/javascript/flavours/glitch/store/typed_functions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import type { TypedUseSelectorHook } from 'react-redux';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';

import type { AppDispatch, RootState } from './store';

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();

export const createAppAsyncThunk = createAsyncThunk.withTypes<{
state: RootState;
Expand Down
7 changes: 6 additions & 1 deletion app/javascript/mastodon/actions/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {

export const clickSearchResult = (q, type) => (dispatch, getState) => {
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 current = previous.add(fromJS({ type, q })).takeLast(4);

Expand Down Expand Up @@ -207,4 +212,4 @@ export const hydrateSearch = () => (dispatch, getState) => {
if (history !== null) {
dispatch(updateSearchHistory(history));
}
};
};
Loading

0 comments on commit 634acd0

Please sign in to comment.