Compare commits

...

37 Commits

Author SHA1 Message Date
nachtjasmin efc3ec1e4a
Remove h1 conversion spec 2023-12-27 23:07:30 +01:00
nachtjasmin 458a9a6f24
Align html sanitizing closer to upstream 2023-12-27 23:07:30 +01:00
nachtjasmin aa986fb619
Fix ruby specs 2023-12-27 23:07:30 +01:00
nachtjasmin 17a4311617
Manually bump json-canonicalization to 0.3.3
0.3.2 does not exist anymore, therefore the GitHub Actions are failing
  right now.
2023-12-27 21:43:33 +01:00
nachtjasmin 8925306af0
Automatic rubocop fixing 2023-12-27 21:33:44 +01:00
nachtjasmin d0014788f6
Increase font size of page title on login screen 2023-12-27 21:18:59 +01:00
nachtjasmin 7fa01d2eb8
Remove all "older" custom hometown styles
Without any documentation, it's hard to figure out the reason for any
line, therefore I removed it.
2023-12-27 21:07:40 +01:00
nachtjasmin 7a71a11082
Hide the explore/onboarding banner 2023-12-27 21:04:54 +01:00
nachtjasmin a392a76660
Add Hometown menu entry to admin settings 2023-12-27 21:04:00 +01:00
nachtjasmin 27a1a044f7
Merge tag 'v4.2.2' into lets-bump-hometown-to-mastodon-4.2 2023-12-27 20:40:24 +01:00
nachtjasmin 8e906d0ea8
Ensure correct rendering of logo in sidebars 2023-12-27 20:34:06 +01:00
nachtjasmin e14ae645d6
Ensure correct rendering for profile links 2023-12-27 20:33:17 +01:00
nachtjasmin 47d203d8a2
Add a specific stylesheet for all the hometown customizations 2023-12-27 20:32:01 +01:00
Claire 4b8fe9df73 Bump version to v4.2.2 2023-12-04 15:28:15 +01:00
Claire 7b9496322f Change dismissed banners to be stored server-side (#27055) 2023-12-04 15:28:15 +01:00
Claire 09115731d6 Change GIF max matrix size error to explicitly mention GIF files (#27927) 2023-12-04 15:28:15 +01:00
Claire e11100d782 Clamp dates when serializing to Elasticsearch API (#28081) 2023-12-04 15:28:15 +01:00
Jonathan de Jong 252ea2fc67 Have `Follow` activities bypass availability (#27586)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-12-04 15:28:15 +01:00
Claire 8d02e58ff4 Fix upper border radius of onboarding columns (#27890) 2023-12-04 15:28:15 +01:00
Claire 1076a6cd62 Fix incoming status creation date not being restricted to standard ISO8601 (#27655) 2023-12-04 15:28:15 +01:00
Claire 54a07731d1 Fix posts from threads received out-of-order sometimes not being inserted into timelines (#27653) 2023-12-04 15:28:15 +01:00
Claire 81d7cfd544 Fix posts from force-sensitized accounts being able to trend (#27620) 2023-12-04 15:28:15 +01:00
Claire e6f4c91c5c Fix hashtag matching pattern matching some URLs (#27584) 2023-12-04 15:28:15 +01:00
Claire de86e822f4 Fix error when trying to delete already-deleted file with OpenStack Swift (#27569) 2023-12-04 15:28:15 +01:00
Claire 4c38706474 Fix batch attachment deletion when using OpenStack Swift (#27554) 2023-12-04 15:28:15 +01:00
Renaud Chaput 4fc2523546 Do not display the navigation banner in the logo container (#27476) 2023-12-04 15:28:15 +01:00
Renaud Chaput d5bc10b711 The `class` props should be `className` (#27462) 2023-12-04 15:28:15 +01:00
Claire c66ade7de8 Fix processing LDSigned activities from actors with unknown public keys (#27474) 2023-12-04 15:28:15 +01:00
Claire bece853e3c Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts (#27459) 2023-12-04 15:28:15 +01:00
Claire 700ae1f918 Fix report processing notice not mentioning the report number when performing a custom action (#27442) 2023-12-04 15:28:15 +01:00
Claire 13205b54fd Fix handling of `inLanguage` attribute in preview card processing (#27423) 2023-12-04 15:28:15 +01:00
KMY(雪あすか) 8be33d4316 Fix when unfollow a tag, my post also disappears from the home timeline (#27391) 2023-12-04 15:28:15 +01:00
Claire cdedae6d63 Fix some link anchors being recognized as hashtags (#27271) 2023-12-04 15:28:15 +01:00
Claire aa69ca74ed Fix incorrect serialization of regional languages in `contentMap` (#27207) 2023-12-04 15:28:15 +01:00
gunchleoc 156d32689b Only strip country code when language not listed in SUPPORTED_LOCALES (#27099) 2023-12-04 15:28:15 +01:00
Claire ef149674f0 Change Content-Security-Policy to be tighter on media paths (#26889) 2023-12-04 15:28:15 +01:00
Claire eea2654236
Fix format-dependent redirects being cached regardless of requested format (#27634) 2023-11-13 17:58:00 +01:00
56 changed files with 542 additions and 1163 deletions

View File

@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file.
## [4.2.2] - 2023-12-04
### Changed
- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055))
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476))
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207))
### Fixed
- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890))
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653))
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569))
- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554))
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423))
- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391))
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634))
## [4.2.1] - 2023-10-10
### Added

View File

@ -377,7 +377,7 @@ GEM
ipaddress (0.8.3)
jmespath (1.6.2)
json (2.6.3)
json-canonicalization (0.3.2)
json-canonicalization (0.3.3)
json-jwt (1.15.3)
activesupport (>= 4.2)
aes_key_wrap

View File

@ -17,6 +17,6 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| ------- | ---------------- |
| 4.2.x | Yes |
| 4.1.x | Yes |
| 4.0.x | Until 2023-10-31 |
| 4.0.x | No |
| 3.5.x | Until 2023-12-31 |
| < 3.5 | No |

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class AccountsIndex < Chewy::Index
include DatetimeClampingConcern
settings index: index_preset(refresh_interval: '30s'), analysis: {
filter: {
english_stop: {
@ -60,7 +62,7 @@ class AccountsIndex < Chewy::Index
field(:following_count, type: 'long')
field(:followers_count, type: 'long')
field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
field(:last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) })
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module DatetimeClampingConcern
extend ActiveSupport::Concern
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
class_methods do
def clamp_date(datetime)
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
end
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class PublicStatusesIndex < Chewy::Index
include DatetimeClampingConcern
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
filter: {
english_stop: {
@ -62,6 +64,6 @@ class PublicStatusesIndex < Chewy::Index
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
field(:language, type: 'keyword')
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
field(:created_at, type: 'date')
field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class StatusesIndex < Chewy::Index
include DatetimeClampingConcern
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
filter: {
english_stop: {
@ -60,6 +62,6 @@ class StatusesIndex < Chewy::Index
field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
field(:language, type: 'keyword')
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
field(:created_at, type: 'date')
field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class TagsIndex < Chewy::Index
include DatetimeClampingConcern
settings index: index_preset(refresh_interval: '30s'), analysis: {
analyzer: {
content: {
@ -42,6 +44,6 @@ class TagsIndex < Chewy::Index
field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') }
field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? })
field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts })
field(:last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at })
field(:last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) })
end
end

View File

@ -21,7 +21,7 @@ module Admin
account_action.save!
if account_action.with_report?
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
else
redirect_to admin_account_path(@account.id)
end

View File

@ -254,6 +254,7 @@ module LanguagesHelper
def valid_locale_or_nil(str)
return if str.blank?
return str if valid_locale?(str)
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP

View File

@ -1,9 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-call,
@typescript-eslint/no-unsafe-return,
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access
-- the settings store is not yet typed */
import type { PropsWithChildren } from 'react';
import { useCallback, useState } from 'react';
import { useCallback, useState, useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { changeSetting } from 'mastodon/actions/settings';
import { bannerSettings } from 'mastodon/settings';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { IconButton } from './icon_button';
@ -19,13 +26,25 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
id,
children,
}) => {
const [visible, setVisible] = useState(!bannerSettings.get(id));
const dismissed = useAppSelector((state) =>
state.settings.getIn(['dismissed_banners', id], false),
);
const dispatch = useAppDispatch();
const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed);
const intl = useIntl();
const handleDismiss = useCallback(() => {
setVisible(false);
bannerSettings.set(id, true);
}, [id]);
dispatch(changeSetting(['dismissed_banners', id], true));
}, [id, dispatch]);
useEffect(() => {
if (!visible && !dismissed) {
dispatch(changeSetting(['dismissed_banners', id], true));
}
}, [id, dispatch, visible, dismissed]);
if (!visible) {
return null;

View File

@ -419,7 +419,6 @@ class Header extends ImmutablePureComponent {
<div className='account__header__tabs__name'>
<h1>
<span dangerouslySetInnerHTML={displayNameHtml} />
<a href={account.get('url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={displayNameHtml} /></a>{isRemote ? <span> <IconButton icon='external-link' size={14} onClick={this.handleDisplayNameClick} /></span> : null}
<small>
<span>@{acct}</span> {lockedIcon}

View File

@ -53,24 +53,30 @@ class NavigationPanel extends Component {
const { intl } = this.props;
const { signedIn, disabledAccountId } = this.context.identity;
let banner = undefined;
if(transientSingleColumn)
banner = (<div className='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)}
{" "}
<a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>);
return (
<div className='navigation-panel'>
<div className='navigation-panel__logo'>
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
{transientSingleColumn ? (
<div class='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)}
{" "}
<a href={`/deck${location.pathname}`} class='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>
) : (
<hr />
)}
{!banner && <hr />}
</div>
{banner &&
<div class='navigation-panel__banner'>
{banner}
</div>
}
{signedIn && (
<>
<ColumnLink transparent to='/home' icon='home' text={intl.formatMessage(messages.home)} />

View File

@ -100,6 +100,15 @@ const initialState = ImmutableMap({
body: '',
}),
}),
dismissed_banners: ImmutableMap({
'public_timeline': false,
'community_timeline': false,
'home.explore_prompt': false,
'explore/links': false,
'explore/statuses': false,
'explore/tags': false,
}),
});
const defaultColumns = fromJS([

View File

@ -25,3 +25,5 @@
@import 'mastodon/rtl';
@import 'mastodon/accessibility';
@import 'mastodon/rich_text';
@import 'hometown';

View File

@ -0,0 +1,45 @@
// app/javascript/mastodon/features/account_timeline/components/header.jsx
// Because we provide a link to the profile instead of a plain <span>, we override
// the color to ensure it doesn't use the accent color.
.account__header__tabs__name a {
color: inherit;
}
// app/javascript/mastodon/features/ui/components/navigation_panel.jsx
// We hide the logo from the sidebar. It's kept in the JSX, to keep the modifications of the React
// codebase as small as possible.
.navigation-panel__logo {
display: none;
}
// app/views/layouts/admin.html.haml
// The logo is set to a size of 100x100px. We don't have a funny/beautiful icon for hometown in the
// admin sidebar, therefore we unset the dimensions [1], center the link [2] and unset the color for it [3].
.admin-wrapper .sidebar .logo {
// 1
width: unset;
height: unset;
// 2
h2 {
text-align: center;
font-size: 1.25rem;
}
// 3
.brand {
color: inherit;
}
}
// app/views/layouts/auth.html.haml
// Increase the default font size for the page title on the login screen.
.logo-container h1 a {
font-size: 1.5rem;
}
// app/javascript/mastodon/features/home_timeline/components/explore_prompt.tsx
// Hide the "this is your home base" banner.
.dismissable-banner:has([href='/explore']) {
display: none;
}

View File

@ -150,12 +150,6 @@ body {
height: auto;
margin-top: -120px;
}
h1 {
a.brand {
font-size: 36px;
}
}
}
h1 {

View File

@ -2239,8 +2239,7 @@ $ui-header-height: 55px;
> .scrollable {
background: $ui-base-color;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-radius: 0 0 4px 4px;
}
}
@ -2466,6 +2465,7 @@ $ui-header-height: 55px;
.navigation-panel__sign-in-banner,
.navigation-panel__logo,
.navigation-panel__banner,
.getting-started__trends {
display: none;
}

View File

@ -104,780 +104,3 @@
margin-inline-start: 10px;
}
}
.grid-3 {
display: grid;
grid-gap: 10px;
grid-template-columns: 3fr 1fr;
grid-auto-columns: 25%;
grid-auto-rows: max-content;
.column-0 {
grid-column: 1 / 3;
grid-row: 1;
}
.column-1 {
grid-column: 1;
grid-row: 2;
}
.column-2 {
grid-column: 2;
grid-row: 2;
}
.column-3 {
grid-column: 1 / 3;
grid-row: 3;
}
@media screen and (max-width: $no-gap-breakpoint-static) {
grid-gap: 0;
grid-template-columns: minmax(0, 100%);
.column-0 {
grid-column: 1;
}
.column-1 {
grid-column: 1;
grid-row: 3;
}
.column-2 {
grid-column: 1;
grid-row: 2;
}
.column-3 {
grid-column: 1;
grid-row: 4;
}
}
}
.grid-4 {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-auto-columns: 25%;
grid-auto-rows: max-content;
.column-0 {
grid-column: 1 / 5;
grid-row: 1;
}
.column-1 {
grid-column: 1 / 4;
grid-row: 2;
}
.column-2 {
grid-column: 4;
grid-row: 2;
}
.column-3 {
grid-column: 2 / 5;
grid-row: 3;
}
.column-4 {
grid-column: 1;
grid-row: 3;
}
.landing-page__call-to-action {
min-height: 100%;
}
.flash-message {
margin-bottom: 10px;
}
@media screen and (width <= 738px) {
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
.landing-page__call-to-action {
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.row__information-board {
width: 100%;
justify-content: center;
align-items: center;
}
.row__mascot {
display: none;
}
}
@media screen and (max-width: $no-gap-breakpoint-static) {
grid-gap: 0;
grid-template-columns: minmax(0, 100%);
.column-0 {
grid-column: 1;
}
.column-1 {
grid-column: 1;
grid-row: 3;
}
.column-2 {
grid-column: 1;
grid-row: 2;
}
.column-3 {
grid-column: 1;
grid-row: 5;
}
.column-4 {
grid-column: 1;
grid-row: 4;
}
}
}
.public-layout {
.originalheader {
padding: 50px 30px 30px;
.logo--icon {
width: 140px;
}
h1 {
margin-bottom: 20px;
a {
line-height: 1em;
}
}
#register {
margin-top: 10px;
}
.btn {
text-transform: uppercase;
}
.closed {
margin-top: 10px;
border-radius: 4px;
max-width: 400px;
}
}
.container {
max-width: 960px;
@media screen and (max-width: $no-gap-breakpoint-static) {
padding: 0;
}
}
.header {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
height: 48px;
margin: 10px 0;
display: flex;
align-items: stretch;
justify-content: center;
flex-wrap: nowrap;
overflow: hidden;
@media screen and (max-width: $no-gap-breakpoint-static) {
position: fixed;
width: 100%;
top: 0;
left: 0;
margin: 0;
border-radius: 0;
box-shadow: none;
z-index: 110;
}
& > div {
flex: 1 1 33.3%;
min-height: 1px;
}
.nav-left {
display: flex;
align-items: stretch;
justify-content: flex-start;
flex-wrap: nowrap;
}
.nav-center {
display: flex;
align-items: stretch;
justify-content: center;
flex-wrap: nowrap;
}
.nav-right {
display: flex;
align-items: stretch;
justify-content: flex-end;
flex-wrap: nowrap;
}
.brand {
display: block;
padding: 15px;
.logo {
display: block;
height: 18px;
width: auto;
position: relative;
bottom: -2px;
fill: $primary-text-color;
@media screen and (max-width: $no-gap-breakpoint-static) {
height: 20px;
}
}
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 12%);
}
}
.nav-link {
display: flex;
align-items: center;
padding: 0 1rem;
font-size: 12px;
font-weight: 500;
text-decoration: none;
color: $darker-text-color;
white-space: nowrap;
text-align: center;
&:hover,
&:focus,
&:active {
text-decoration: underline;
color: $primary-text-color;
}
@media screen and (width <= 550px) {
&.optional {
display: none;
}
}
}
.nav-button {
background: lighten($ui-base-color, 16%);
margin: 8px;
margin-left: 0;
border-radius: 4px;
&:hover,
&:focus,
&:active {
text-decoration: none;
background: lighten($ui-base-color, 20%);
}
}
}
$no-columns-breakpoint: 600px;
.grid {
display: grid;
grid-gap: 10px;
grid-template-columns: minmax(300px, 3fr) minmax(298px, 1fr);
grid-auto-columns: 25%;
grid-auto-rows: max-content;
.column-0 {
grid-row: 1;
grid-column: 1;
}
.column-1 {
grid-row: 1;
grid-column: 2;
}
@media screen and (max-width: $no-columns-breakpoint) {
grid-template-columns: 100%;
grid-gap: 0;
.column-1 {
display: none;
}
}
}
.page-header {
@media screen and (max-width: $no-gap-breakpoint-static) {
border-bottom: 0;
}
}
.public-account-header {
overflow: hidden;
margin-bottom: 10px;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
height: auto;
&.inactive {
opacity: 0.5;
.public-account-header__image,
.avatar {
filter: grayscale(100%);
}
.logo-button {
background-color: $secondary-text-color;
}
}
.logo-button {
padding: 3px 15px;
}
&--no-bar {
margin-bottom: 0;
.public-account-header__image,
.public-account-header__image img {
border-radius: 4px;
@media screen and (max-width: $no-gap-breakpoint-static) {
border-radius: 0;
}
}
}
@media screen and (max-width: $no-gap-breakpoint-static) {
margin-bottom: 0;
box-shadow: none;
&__image::after {
display: none;
}
&__image,
&__image img {
border-radius: 0;
}
}
&__bar {
position: relative;
margin-top: -80px;
display: flex;
justify-content: flex-start;
&::before {
content: '';
display: block;
background: lighten($ui-base-color, 4%);
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
border-radius: 0 0 4px 4px;
z-index: -1;
}
.avatar {
display: block;
width: 120px;
height: 120px;
padding-left: 20px - 4px;
flex: 0 0 auto;
img {
display: block;
width: 100%;
height: 100%;
margin: 0;
border-radius: 50%;
border: 4px solid lighten($ui-base-color, 4%);
background: darken($ui-base-color, 8%);
}
}
@media screen and (width <= 600px) {
margin-top: 0;
background: lighten($ui-base-color, 4%);
border-radius: 0 0 4px 4px;
padding: 5px;
&::before {
display: none;
}
.avatar {
width: 48px;
height: 48px;
padding: 7px 0;
padding-left: 10px;
img {
border: 0;
border-radius: 4px;
}
@media screen and (width <= 360px) {
display: none;
}
}
}
@media screen and (max-width: $no-gap-breakpoint-static) {
border-radius: 0;
}
@media screen and (max-width: $no-columns-breakpoint) {
flex-wrap: wrap;
}
}
&__tabs {
flex: 1 1 auto;
margin-left: 20px;
&__name {
padding-top: 20px;
padding-bottom: 8px;
h1 {
font-size: 20px;
line-height: 18px * 1.5;
color: $primary-text-color;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-shadow: 1px 1px 1px $base-shadow-color;
small {
display: block;
font-size: 14px;
color: $primary-text-color;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@media screen and (width <= 600px) {
margin-left: 15px;
display: flex;
justify-content: space-between;
align-items: center;
&__name {
padding-top: 0;
padding-bottom: 0;
h1 {
font-size: 16px;
line-height: 24px;
text-shadow: none;
small {
color: $darker-text-color;
}
}
}
}
&__tabs {
display: flex;
justify-content: flex-start;
align-items: stretch;
height: 58px;
.details-counters {
display: flex;
flex-direction: row;
min-width: 300px;
}
@media screen and (max-width: $no-columns-breakpoint) {
.details-counters {
display: none;
}
}
.counter {
min-width: 33.3%;
box-sizing: border-box;
flex: 0 0 auto;
color: $darker-text-color;
padding: 10px;
border-right: 1px solid lighten($ui-base-color, 4%);
cursor: default;
text-align: center;
position: relative;
a {
display: block;
}
&:last-child {
border-right: 0;
}
&::after {
display: block;
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
border-bottom: 4px solid $ui-primary-color;
opacity: 0.5;
transition: all 400ms ease;
}
&.active {
&::after {
border-bottom: 4px solid $highlight-text-color;
opacity: 1;
}
&.inactive::after {
border-bottom-color: $secondary-text-color;
}
}
&:hover {
&::after {
opacity: 1;
transition-duration: 100ms;
}
}
a {
text-decoration: none;
color: inherit;
}
.counter-label {
font-size: 12px;
display: block;
}
.counter-number {
font-weight: 500;
font-size: 18px;
margin-bottom: 5px;
color: $primary-text-color;
font-family: $font-display, sans-serif;
}
}
.spacer {
flex: 1 1 auto;
height: 1px;
}
&__buttons {
padding: 7px 8px;
}
}
}
&__extra {
display: none;
margin-top: 4px;
.public-account-bio {
border-radius: 0;
box-shadow: none;
background: transparent;
margin: 0 -5px;
.account__header__fields {
border-top: 1px solid lighten($ui-base-color, 12%);
}
.roles {
display: none;
}
}
&__links {
margin-top: -15px;
font-size: 14px;
color: $darker-text-color;
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
padding: 15px;
font-weight: 500;
strong {
font-weight: 700;
color: $primary-text-color;
}
}
}
@media screen and (max-width: $no-columns-breakpoint) {
display: block;
flex: 100%;
}
}
}
.account__section-headline {
border-radius: 4px 4px 0 0;
@media screen and (max-width: $no-gap-breakpoint-static) {
border-radius: 0;
}
}
.detailed-status__meta {
margin-top: 25px;
}
.public-account-bio {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
@media screen and (max-width: $no-gap-breakpoint-static) {
box-shadow: none;
margin-bottom: 0;
border-radius: 0;
}
.account__header__fields {
margin: 0;
border-top: 0;
a {
color: $highlight-text-color;
}
dl:first-child .verified {
border-radius: 0 4px 0 0;
}
.verified a {
color: $valid-value-color;
}
}
.account__header__content {
padding: 20px;
padding-bottom: 0;
color: $primary-text-color;
}
&__extra,
.roles {
padding: 20px;
font-size: 14px;
color: $darker-text-color;
}
.roles {
padding-bottom: 0;
}
}
.directory__list {
display: grid;
grid-gap: 10px;
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
.account-card {
display: flex;
flex-direction: column;
}
@media screen and (max-width: $no-gap-breakpoint-static) {
display: block;
.account-card {
margin-bottom: 10px;
display: block;
}
}
}
.card-grid {
display: flex;
flex-wrap: wrap;
min-width: 100%;
margin: 0 -5px;
& > div {
box-sizing: border-box;
flex: 1 0 auto;
width: 300px;
padding: 0 5px;
margin-bottom: 10px;
max-width: 33.333%;
@media screen and (width <= 900px) {
max-width: 50%;
}
@media screen and (width <= 600px) {
max-width: 100%;
}
}
@media screen and (max-width: $no-gap-breakpoint-static) {
margin: 0;
border-top: 1px solid lighten($ui-base-color, 8%);
& > div {
width: 100%;
padding: 0;
margin-bottom: 0;
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:last-child {
border-bottom: 0;
}
.card__bar {
background: $ui-base-color;
&:hover,
&:active,
&:focus {
background: lighten($ui-base-color, 4%);
}
}
}
}
}
}

View File

@ -89,9 +89,6 @@ $media-modal-media-max-height: 80%;
$no-gap-breakpoint: 1175px;
// Hometown: different breakpoint for static pages
$no-gap-breakpoint-static: 700px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;

View File

@ -114,13 +114,6 @@
}
}
.box-widget {
padding: 20px;
border-radius: 4px;
background: $ui-base-color;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
}
.placeholder-widget {
padding: 16px;
border-radius: 4px;
@ -130,51 +123,6 @@
margin-bottom: 10px;
}
.contact-widget {
min-height: 100%;
font-size: 15px;
color: $darker-text-color;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
padding: 0;
h4 {
padding: 10px;
text-transform: uppercase;
font-weight: 700;
font-size: 13px;
color: $darker-text-color;
}
.account {
border-bottom: 0;
padding: 10px 0;
padding-top: 5px;
.account__wrapper {
padding-left: 10px;
}
}
& > a {
display: inline-block;
padding: 10px;
padding-top: 0;
color: $darker-text-color;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
.moved-account-widget {
padding: 15px;
padding-bottom: 20px;
@ -255,37 +203,6 @@
margin-bottom: 10px;
}
.page-header {
background: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
padding: 60px 15px;
text-align: center;
margin: 10px 0;
h1 {
color: $primary-text-color;
font-size: 36px;
line-height: 1.1;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 15px;
color: $darker-text-color;
}
@media screen and (max-width: $no-gap-breakpoint-static) {
margin-top: 0;
background: lighten($ui-base-color, 4%);
h1 {
font-size: 24px;
}
}
}
.directory {
background: $ui-base-color;
border-radius: 4px;
@ -372,34 +289,6 @@
}
}
.avatar-stack {
display: flex;
justify-content: flex-end;
.account__avatar {
flex: 0 0 auto;
width: 36px;
height: 36px;
border-radius: 50%;
position: relative;
margin-left: -10px;
background: darken($ui-base-color, 8%);
border: 2px solid $ui-base-color;
&:nth-child(1) {
z-index: 1;
}
&:nth-child(2) {
z-index: 2;
}
&:nth-child(3) {
z-index: 3;
}
}
}
.accounts-table {
width: 100%;
@ -502,100 +391,14 @@
.moved-account-widget,
.memoriam-widget,
.box-widget,
.contact-widget,
.landing-page__information.contact-widget,
.directory,
.page-header {
@media screen and (max-width: $no-gap-breakpoint-static) {
.directory {
@media screen and (max-width: $no-gap-breakpoint) {
margin-bottom: 0;
box-shadow: none;
border-radius: 0;
}
}
$maximum-width: 1235px;
$fluid-breakpoint: $maximum-width + 20px;
.statuses-grid {
min-height: 600px;
@media screen and (width <= 640px) {
width: 100% !important; // Masonry layout is unnecessary at this width
}
&__item {
width: math.div(960px - 20px, 3);
@media screen and (max-width: $fluid-breakpoint) {
width: math.div(940px - 20px, 3);
}
@media screen and (width <= 640px) {
width: 100%;
}
@media screen and (max-width: $no-gap-breakpoint-static) {
width: 100vw;
}
}
.detailed-status {
border-radius: 4px;
@media screen and (max-width: $no-gap-breakpoint-static) {
border-top: 1px solid lighten($ui-base-color, 16%);
}
&.compact {
.detailed-status__meta {
margin-top: 15px;
}
.status__content {
font-size: 15px;
line-height: 20px;
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
.status__content__spoiler-link {
line-height: 20px;
margin: 0;
}
}
.media-gallery,
.status-card,
.video-player {
margin-top: 15px;
}
}
}
}
.notice-widget {
margin-bottom: 10px;
color: $darker-text-color;
p {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
a {
font-size: 14px;
line-height: 20px;
}
}
.notice-widget,
.placeholder-widget {
a {
text-decoration: none;
@ -609,37 +412,3 @@ $fluid-breakpoint: $maximum-width + 20px;
}
}
}
.table-of-contents {
background: darken($ui-base-color, 4%);
min-height: 100%;
font-size: 14px;
border-radius: 4px;
li a {
display: block;
font-weight: 500;
padding: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
color: $primary-text-color;
border-bottom: 1px solid lighten($ui-base-color, 4%);
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
li:last-child a {
border-bottom: 0;
}
li ul {
padding-left: 20px;
border-bottom: 1px solid lighten($ui-base-color, 4%);
}
}

View File

@ -129,15 +129,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
conversation: conversation_from_uri(@object['conversation']),
media_attachment_ids: process_attachments.take(4).map(&:id),
poll: process_poll,
activity_pub_type: @object['type']
activity_pub_type: @object['type'],
}
end
end
class Handler < ::Ox::Sax
attr_reader :srcs
attr_reader :alts
def initialize(block)
attr_reader :srcs, :alts
def initialize(_block)
@stack = []
@srcs = []
@alts = {}
@ -147,7 +147,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@stack << [element_name, {}]
end
def end_element(element_name)
def end_element(_element_name)
self_name, self_attributes = @stack[-1]
if self_name == :img && !self_attributes[:src].nil?
@srcs << self_attributes[:src]
@ -163,7 +163,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def process_inline_images
proc = Proc.new { |name| puts name }
proc = proc { |name| puts name }
handler = Handler.new(proc)
Ox.sax_parse(handler, @object['content'])
handler.srcs.each do |src|
@ -436,7 +436,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return article_format(@object['content']) if @object['content'].present? && @object['type'] == 'Article'
return @status_parser.text || ''
@status_parser.text || ''
end
def text_from_name

View File

@ -14,6 +14,8 @@ module ActivityPub::CaseTransform
when String
camel_lower_cache[value] ||= if value.start_with?('_:')
"_:#{value.delete_prefix('_:').underscore.camelize(:lower)}"
elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
value
else
value.underscore.camelize(:lower)
end

View File

@ -18,8 +18,8 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017'
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
return if creator.nil?
@ -28,6 +28,8 @@ class ActivityPub::LinkedDataSignature
to_be_verified = options_hash + document_hash
creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
rescue OpenSSL::PKey::RSAError
false
end
def sign!(creator, sign_with: nil)

View File

@ -75,7 +75,12 @@ class AttachmentBatch
end
when :fog
logger.debug { "Deleting #{attachment.path(style)}" }
attachment.directory.files.new(key: attachment.path(style)).destroy
begin
attachment.send(:directory).files.new(key: attachment.path(style)).destroy
rescue Fog::Storage::OpenStack::NotFound
# Ignore failure to delete a file that has already been deleted
end
when :azure
logger.debug { "Deleting #{attachment.path(style)}" }
attachment.destroy

View File

@ -192,6 +192,7 @@ class FeedManager
# also tagged with another followed hashtag or from a followed user
scope = from_tag.statuses
.where(id: timeline_status_ids)
.where.not(account: into_account)
.where.not(account: into_account.following)
.tagged_with_none(TagFollow.where(account: into_account).pluck(:tag_id))

View File

@ -36,7 +36,8 @@ class LinkDetailsExtractor
end
def language
json['inLanguage']
lang = json['inLanguage']
lang.is_a?(Hash) ? (lang['alternateName'] || lang['name']) : lang
end
def type

View File

@ -52,9 +52,13 @@ module Attachmentable
return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank?
width, height = FastImage.size(attachment.queued_for_write[:original].path)
matrix_limit = attachment.content_type == 'image/gif' ? GIF_MATRIX_LIMIT : MAX_MATRIX_LIMIT
return unless width.present? && height.present?
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height > matrix_limit)
if attachment.content_type == 'image/gif' && width * height > GIF_MATRIX_LIMIT
raise Mastodon::DimensionsValidationError, "#{width}x#{height} GIF files are not supported"
elsif width * height > MAX_MATRIX_LIMIT
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported"
end
end
def appropriate_extension(attachment)

View File

@ -35,7 +35,7 @@ class Tag < ApplicationRecord
HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}"
HASHTAG_RE = %r{(?:^|[^/)\w])#(#{HASHTAG_NAME_PAT})}i
HASHTAG_RE = %r{(?<![=/)\w])#(#{HASHTAG_NAME_PAT})}i
HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]#{HASHTAG_SEPARATORS}]/

View File

@ -106,7 +106,7 @@ class Trends::Statuses < Trends::Base
private
def eligible?(status)
status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language)
status.public_visibility? && status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language)
end
def calculate_scores(statuses, at_time)

View File

@ -10,7 +10,9 @@ class REST::FeaturedTagSerializer < ActiveModel::Serializer
end
def url
short_account_tag_url(object.account, object.tag)
# The path is hardcoded because we have to deal with both local and
# remote users, which are different routes
account_with_domain_url(object.account, "tagged/#{object.tag.to_param}")
end
def name

View File

@ -8,6 +8,7 @@ class FanOutOnWriteService < BaseService
# @param [Hash] options
# @option options [Boolean] update
# @option options [Array<Integer>] silenced_account_ids
# @option options [Boolean] skip_notifications
def call(status, options = {})
@status = status
@account = status.account
@ -37,8 +38,11 @@ class FanOutOnWriteService < BaseService
def fan_out_to_local_recipients!
deliver_to_self!
notify_mentioned_accounts!
notify_about_update! if update?
unless @options[:skip_notifications]
notify_mentioned_accounts!
notify_about_update! if update?
end
case @status.visibility.to_sym
when :public, :unlisted, :private

View File

@ -71,7 +71,7 @@ class FollowService < BaseService
if @target_account.local?
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
elsif @target_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, { 'bypass_availability' => true })
end
follow_request

View File

@ -122,13 +122,11 @@ class PostStatusService < BaseService
spoiler_text&.include?(':local_only')
return true
end
if local_only.nil?
if in_reply_to && in_reply_to.local_only
return true
end
if in_reply_to && !in_reply_to.local_only
return false
end
return true if in_reply_to&.local_only
return false if in_reply_to && !in_reply_to.local_only
return !federation_setting
end
local_only
@ -139,9 +137,7 @@ class PostStatusService < BaseService
Trends.tags.register(@status)
LinkCrawlWorker.perform_async(@status.id)
DistributionWorker.perform_async(@status.id)
unless @status.local_only?
ActivityPub::DistributionWorker.perform_async(@status.id)
end
ActivityPub::DistributionWorker.perform_async(@status.id) unless @status.local_only?
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
end

View File

@ -30,9 +30,7 @@ class ReblogService < BaseService
Trends.register!(reblog)
DistributionWorker.perform_async(reblog.id)
unless reblogged_status.local_only?
ActivityPub::DistributionWorker.perform_async(reblog.id)
end
ActivityPub::DistributionWorker.perform_async(reblog.id) unless reblogged_status.local_only?
create_notification(reblog)
bump_potential_friendship(account, reblog)

View File

@ -23,9 +23,10 @@ class ActivityPub::DeliveryWorker
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
def perform(json, source_account_id, inbox_url, options = {})
return unless DeliveryFailureTracker.available?(inbox_url)
@options = options.with_indifferent_access
return unless @options[:bypass_availability] || DeliveryFailureTracker.available?(inbox_url)
@json = json
@source_account = Account.find(source_account_id)
@inbox_url = inbox_url

View File

@ -7,13 +7,18 @@ class ThreadResolveWorker
sidekiq_options queue: 'pull', retry: 3
def perform(child_status_id, parent_url, options = {})
child_status = Status.find(child_status_id)
parent_status = FetchRemoteStatusService.new.call(parent_url, **options.deep_symbolize_keys)
child_status = Status.find(child_status_id)
return if child_status.in_reply_to_id.present?
parent_status = ActivityPub::TagManager.instance.uri_to_resource(parent_url, Status)
parent_status ||= FetchRemoteStatusService.new.call(parent_url, **options.deep_symbolize_keys)
return if parent_status.nil?
child_status.thread = parent_status
child_status.save!
DistributionWorker.perform_async(child_status_id, { 'skip_notifications' => true }) if child_status.within_realtime_window?
rescue ActiveRecord::RecordNotFound
true
end

View File

@ -5,7 +5,11 @@
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
def host_to_url(str)
"http#{Rails.configuration.x.use_https ? 's' : ''}://#{str.split('/').first}" if str.present?
return if str.blank?
uri = Addressable::URI.parse("http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}")
uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/')
uri.to_s
end
base_host = Rails.configuration.x.web_domain

View File

@ -4,7 +4,7 @@ require_relative '../../lib/mastodon/sidekiq_middleware'
Sidekiq.configure_server do |config|
if Rails.configuration.database_configuration.dig('production', 'adapter') == 'postgresql_makara'
STDERR.puts 'ERROR: Database replication is not currently supported in Sidekiq workers. Check your configuration.'
warn 'ERROR: Database replication is not currently supported in Sidekiq workers. Check your configuration.'
exit 1
end

View File

@ -3,6 +3,18 @@
require 'sidekiq_unique_jobs/web'
require 'sidekiq-scheduler/web'
class RedirectWithVary < ActionDispatch::Routing::PathRedirect
def serve(...)
super.tap do |_, headers, _|
headers['Vary'] = 'Origin, Accept'
end
end
end
def redirect_with_vary(path)
RedirectWithVary.new(301, path)
end
Rails.application.routes.draw do
# Paths of routes on the web app that to not require to be indexed or
# have alternative format representations requiring separate controllers
@ -90,10 +102,13 @@ Rails.application.routes.draw do
confirmations: 'auth/confirmations',
}
get '/users/:username', to: redirect('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
get '/users/:username/following', to: redirect('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? }
get '/users/:username/followers', to: redirect('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? }
get '/users/:username/statuses/:id', to: redirect('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
# rubocop:disable Style/FormatStringToken - those do not go through the usual formatting functions and are not safe to correct
get '/users/:username', to: redirect_with_vary('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
get '/users/:username/following', to: redirect_with_vary('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? }
get '/users/:username/followers', to: redirect_with_vary('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? }
get '/users/:username/statuses/:id', to: redirect_with_vary('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
# rubocop:enable Style/FormatStringToken
get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" }
resources :accounts, path: 'users', only: [:show], param: :username do
@ -134,7 +149,7 @@ Rails.application.routes.draw do
get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
end
get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: %r{([^/])+?} }, format: false
get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: %r{([^/])+?} }, as: :account_with_domain, format: false
get '/settings', to: redirect('/settings/profile')
draw(:settings)

View File

@ -50,6 +50,7 @@ namespace :admin do
resource :about, only: [:show, :update], controller: 'about'
resource :appearance, only: [:show, :update], controller: 'appearance'
resource :discovery, only: [:show, :update], controller: 'discovery'
resource :hometown, only: [:show, :update], controller: 'hometown'
end
resources :site_uploads, only: [:destroy]

View File

@ -56,7 +56,7 @@ services:
web:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.1
image: ghcr.io/mastodon/mastodon:v4.2.2
restart: always
env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
@ -77,7 +77,7 @@ services:
streaming:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.1
image: ghcr.io/mastodon/mastodon:v4.2.2
restart: always
env_file: .env.production
command: node ./streaming
@ -95,7 +95,7 @@ services:
sidekiq:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.1
image: ghcr.io/mastodon/mastodon:v4.2.2
restart: always
env_file: .env.production
command: bundle exec sidekiq

View File

@ -13,7 +13,7 @@ module Mastodon
end
def patch
1
2
end
def default_prerelease

View File

@ -68,13 +68,13 @@ class Sanitize
elements: %w(p br span a abbr del pre blockquote code b strong i em h1 h2 h3 h4 h5 ul ol li img u),
attributes: {
'abbr' => %w(title),
'abbr' => %w(title),
'blockquote' => %w(cite),
'img' => %w(src alt),
'a' => %w(href rel class translate title),
'span' => %w(class translate),
'ol' => %w(start reversed),
'li' => %w(value),
'img' => %w(src alt),
'a' => %w(href rel class translate title),
'span' => %w(class translate),
'ol' => %w(start reversed),
'li' => %w(value),
},
add_attributes: {
@ -84,10 +84,7 @@ class Sanitize
},
},
protocols: {
'a' => { 'href' => HTTP_PROTOCOLS },
'blockquote' => { 'cite' => HTTP_PROTOCOLS },
},
protocols: {},
transformers: [
CLASS_WHITELIST_TRANSFORMER,
@ -101,16 +98,15 @@ class Sanitize
elements: %w(audio embed iframe source video),
attributes: {
'audio' => %w(controls),
'embed' => %w(height src type width),
'audio' => %w(controls),
'embed' => %w(height src type width),
'iframe' => %w(allowfullscreen frameborder height scrolling src width),
'source' => %w(src type),
'video' => %w(controls height loop width),
'div' => [:data]
'video' => %w(controls height loop width),
},
protocols: {
'embed' => { 'src' => HTTP_PROTOCOLS },
'embed' => { 'src' => HTTP_PROTOCOLS },
'iframe' => { 'src' => HTTP_PROTOCOLS },
'source' => { 'src' => HTTP_PROTOCOLS },
},

View File

@ -1,23 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Api::V1::Accounts::FeaturedTagsController do
render_views
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
let(:account) { Fabricate(:account) }
before do
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
end
end

View File

@ -23,6 +23,109 @@ RSpec.describe ActivityPub::Activity::Create do
stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' })
end
describe 'processing posts received out of order' do
let(:follower) { Fabricate(:account, username: 'bob') }
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
type: 'Note',
to: [
'https://www.w3.org/ns/activitystreams#Public',
ActivityPub::TagManager.instance.uri_for(follower),
],
content: '@bob lorem ipsum',
published: 1.hour.ago.utc.iso8601,
updated: 1.hour.ago.utc.iso8601,
tag: {
type: 'Mention',
href: ActivityPub::TagManager.instance.uri_for(follower),
},
}
end
let(:reply_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), 'reply'].join('/'),
type: 'Note',
inReplyTo: object_json[:id],
to: [
'https://www.w3.org/ns/activitystreams#Public',
ActivityPub::TagManager.instance.uri_for(follower),
],
content: '@bob lorem ipsum',
published: Time.now.utc.iso8601,
updated: Time.now.utc.iso8601,
tag: {
type: 'Mention',
href: ActivityPub::TagManager.instance.uri_for(follower),
},
}
end
def activity_for_object(json)
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: [json[:id], 'activity'].join('/'),
type: 'Create',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: json,
}.with_indifferent_access
end
before do
follower.follow!(sender)
end
around do |example|
Sidekiq::Testing.fake! do
example.run
Sidekiq::Worker.clear_all
end
end
it 'correctly processes posts and inserts them in timelines', :aggregate_failures do
# Simulate a temporary failure preventing from fetching the parent post
stub_request(:get, object_json[:id]).to_return(status: 500)
# When receiving the reply…
described_class.new(activity_for_object(reply_json), sender, delivery: true).perform
# NOTE: Refering explicitly to the workers is a bit awkward
DistributionWorker.drain
FeedInsertWorker.drain
# …it creates a status with an unknown parent
reply = Status.find_by(uri: reply_json[:id])
expect(reply.reply?).to be true
expect(reply.in_reply_to_id).to be_nil
# …and creates a notification
expect(LocalNotificationWorker.jobs.size).to eq 1
# …but does not insert it into timelines
expect(redis.zscore(FeedManager.instance.key(:home, follower.id), reply.id)).to be_nil
# When receiving the parent…
described_class.new(activity_for_object(object_json), sender, delivery: true).perform
Sidekiq::Worker.drain_all
# …it creates a status and insert it into timelines
parent = Status.find_by(uri: object_json[:id])
expect(parent.reply?).to be false
expect(parent.in_reply_to_id).to be_nil
expect(reply.reload.in_reply_to_id).to eq parent.id
# Check that the both statuses have been inserted into the home feed
expect(redis.zscore(FeedManager.instance.key(:home, follower.id), parent.id)).to be_within(0.1).of(parent.id.to_f)
expect(redis.zscore(FeedManager.instance.key(:home, follower.id), reply.id)).to be_within(0.1).of(reply.id.to_f)
# Creates two notifications
expect(Notification.count).to eq 2
end
end
describe '#perform' do
context 'when fetching' do
subject { described_class.new(json, sender) }

View File

@ -38,6 +38,40 @@ RSpec.describe ActivityPub::LinkedDataSignature do
end
end
context 'when local account record is missing a public key' do
let(:raw_signature) do
{
'creator' => 'http://example.com/alice',
'created' => '2017-09-23T20:21:34Z',
}
end
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
let(:service_stub) { instance_double(ActivityPub::FetchRemoteKeyService) }
before do
# Ensure signature is computed with the old key
signature
# Unset key
old_key = sender.public_key
sender.update!(private_key: '', public_key: '')
allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
sender.update!(public_key: old_key)
sender
end
end
it 'fetches key and returns creator' do
expect(subject.verify_actor!).to eq sender
expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
end
end
context 'when signature is missing' do
let(:signature) { nil }

View File

@ -525,6 +525,44 @@ RSpec.describe FeedManager do
end
end
describe '#unmerge_tag_from_home' do
let(:receiver) { Fabricate(:account) }
let(:tag) { Fabricate(:tag) }
it 'leaves a tagged status' do
status = Fabricate(:status)
status.tags << tag
described_class.instance.push_to_home(receiver, status)
described_class.instance.unmerge_tag_from_home(tag, receiver)
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
end
it 'remains a tagged status written by receiver\'s followee' do
followee = Fabricate(:account)
receiver.follow!(followee)
status = Fabricate(:status, account: followee)
status.tags << tag
described_class.instance.push_to_home(receiver, status)
described_class.instance.unmerge_tag_from_home(tag, receiver)
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
end
it 'remains a tagged status written by receiver' do
status = Fabricate(:status, account: receiver)
status.tags << tag
described_class.instance.push_to_home(receiver, status)
described_class.instance.unmerge_tag_from_home(tag, receiver)
expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
end
end
describe '#clear_from_home' do
let(:account) { Fabricate(:account) }
let(:followed_account) { Fabricate(:account) }

View File

@ -82,6 +82,10 @@ RSpec.describe LinkDetailsExtractor do
'name' => 'Pet News',
'url' => 'https://example.com',
},
'inLanguage' => {
name: 'English',
alternateName: 'en',
},
}.to_json
end
@ -115,6 +119,12 @@ RSpec.describe LinkDetailsExtractor do
expect(subject.provider_name).to eq 'Pet News'
end
end
describe '#language' do
it 'returns the language from structured data' do
expect(subject.language).to eq 'en'
end
end
end
context 'when is wrapped in CDATA tags' do

View File

@ -6,10 +6,6 @@ describe Sanitize::Config do
describe '::MASTODON_STRICT' do
subject { Sanitize::Config::MASTODON_STRICT }
it 'converts h1 to p strong' do
expect(Sanitize.fragment('<h1>Foo</h1>', subject)).to eq '<p><strong>Foo</strong></p>'
end
it 'keeps ul' do
expect(Sanitize.fragment('<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>'
end

View File

@ -60,9 +60,9 @@ describe TagFeed, type: :service do
end
it 'excludes local-only posts when specified' do
status1.update(local_only: true)
results = described_class.new(tag1, nil, any: [tag2.name], without_local_only: true).get(20)
expect(results).to_not include status1
status_tagged_with_cats.update(local_only: true)
results = described_class.new(tag_cats, nil, any: [tag_dogs.name], without_local_only: true).get(20)
expect(results).to_not include status_tagged_with_cats
end
it 'allows replies to be included' do

View File

@ -32,44 +32,52 @@ RSpec.describe Tag do
expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil
end
it 'does not match URLs with hashtag-like anchors after a numeral' do
expect(subject.match('https://gcc.gnu.org/bugzilla/show_bug.cgi?id=111895#c4')).to be_nil
end
it 'does not match URLs with hashtag-like anchors after an empty query parameter' do
expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)?foo=#Lawsuit')).to be_nil
end
it 'matches #' do
expect(subject.match('this is #').to_s).to eq ' #'
expect(subject.match('this is #').to_s).to eq '#'
end
it 'matches digits at the start' do
expect(subject.match('hello #3d').to_s).to eq ' #3d'
expect(subject.match('hello #3d').to_s).to eq '#3d'
end
it 'matches digits in the middle' do
expect(subject.match('hello #l33ts35k').to_s).to eq ' #l33ts35k'
expect(subject.match('hello #l33ts35k').to_s).to eq '#l33ts35k'
end
it 'matches digits at the end' do
expect(subject.match('hello #world2016').to_s).to eq ' #world2016'
expect(subject.match('hello #world2016').to_s).to eq '#world2016'
end
it 'matches underscores at the beginning' do
expect(subject.match('hello #_test').to_s).to eq ' #_test'
expect(subject.match('hello #_test').to_s).to eq '#_test'
end
it 'matches underscores at the end' do
expect(subject.match('hello #test_').to_s).to eq ' #test_'
expect(subject.match('hello #test_').to_s).to eq '#test_'
end
it 'matches underscores in the middle' do
expect(subject.match('hello #one_two_three').to_s).to eq ' #one_two_three'
expect(subject.match('hello #one_two_three').to_s).to eq '#one_two_three'
end
it 'matches middle dots' do
expect(subject.match('hello #one·two·three').to_s).to eq ' #one·two·three'
expect(subject.match('hello #one·two·three').to_s).to eq '#one·two·three'
end
it 'matches ・unicode in ぼっち・ざ・ろっく correctly' do
expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq ' #ぼっち・ざ・ろっく'
expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq '#ぼっち・ざ・ろっく'
end
it 'matches ZWNJ' do
expect(subject.match('just add #نرم‌افزار and').to_s).to eq ' #نرم‌افزار'
expect(subject.match('just add #نرم‌افزار and').to_s).to eq '#نرم‌افزار'
end
it 'does not match middle dots at the start' do
@ -77,7 +85,7 @@ RSpec.describe Tag do
end
it 'does not match middle dots at the end' do
expect(subject.match('hello #one·two·three·').to_s).to eq ' #one·two·three'
expect(subject.match('hello #one·two·three·').to_s).to eq '#one·two·three'
end
it 'does not match purely-numeric hashtags' do

View File

@ -84,18 +84,18 @@ RSpec.describe StatusPolicy, type: :model do
expect(subject).to_not permit(viewer, status)
end
end
it 'denies access when local-only and the viewer is not logged in' do
allow(status).to receive(:local_only?) { true }
it 'denies access when local-only and the viewer is not logged in' do
allow(status).to receive(:local_only?).and_return(true)
expect(subject).to_not permit(nil, status)
end
expect(subject).to_not permit(nil, status)
end
it 'denies access when local-only and the viewer is from another domain' do
viewer = Fabricate(:account, domain: 'remote-domain')
allow(status).to receive(:local_only?) { true }
expect(subject).to_not permit(viewer, status)
it 'denies access when local-only and the viewer is from another domain' do
viewer = Fabricate(:account, domain: 'remote-domain')
allow(status).to receive(:local_only?).and_return(true)
expect(subject).to_not permit(viewer, status)
end
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'account featured tags API' do
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'read:accounts' }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
let(:account) { Fabricate(:account) }
describe 'GET /api/v1/accounts/:id/featured_tags' do
subject do
get "/api/v1/accounts/#{account.id}/featured_tags", headers: headers
end
before do
account.featured_tags.create!(name: 'foo')
account.featured_tags.create!(name: 'bar')
end
it 'returns the expected tags', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(body_as_json).to contain_exactly(a_hash_including({
name: 'bar',
url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/bar",
}), a_hash_including({
name: 'foo',
url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/foo",
}))
end
context 'when the account is remote' do
it 'returns the expected tags', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(body_as_json).to contain_exactly(a_hash_including({
name: 'bar',
url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/bar",
}), a_hash_including({
name: 'foo',
url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/foo",
}))
end
end
end
end

View File

@ -124,7 +124,7 @@ describe 'Caching behavior' do
expect(response.cookies).to be_empty
end
it 'sets public cache control' do
it 'sets public cache control', :aggregate_failures do
# expect(response.cache_control[:max_age]&.to_i).to be_positive
expect(response.cache_control[:public]).to be_truthy
expect(response.cache_control[:private]).to be_falsy
@ -141,11 +141,8 @@ describe 'Caching behavior' do
end
shared_examples 'non-cacheable error' do
it 'does not return HTTP success' do
it 'does not return HTTP success and does not have cache headers', :aggregate_failures do
expect(response).to_not have_http_status(200)
end
it 'does not have cache headers' do
expect(response.cache_control[:public]).to be_falsy
end
end
@ -182,6 +179,15 @@ describe 'Caching behavior' do
end
context 'when anonymously accessed' do
describe '/users/alice' do
it 'redirects with proper cache header', :aggregate_failures do
get '/users/alice'
expect(response).to redirect_to('/@alice')
expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept')
end
end
TestEndpoints::ALWAYS_CACHED.each do |endpoint|
describe endpoint do
before { get endpoint }

View File

@ -7,7 +7,7 @@ describe ActivityPub::NoteSerializer do
let!(:account) { Fabricate(:account) }
let!(:other) { Fabricate(:account) }
let!(:parent) { Fabricate(:status, account: account, visibility: :public) }
let!(:parent) { Fabricate(:status, account: account, visibility: :public, language: 'zh-TW') }
let!(:reply_by_account_first) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:reply_by_account_next) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:reply_by_other_first) { Fabricate(:status, account: other, thread: parent, visibility: :public) }
@ -18,8 +18,15 @@ describe ActivityPub::NoteSerializer do
@serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter)
end
it 'has a Note type' do
expect(subject['type']).to eql('Note')
it 'has the expected shape' do
expect(subject).to include({
'@context' => include('https://www.w3.org/ns/activitystreams'),
'type' => 'Note',
'attributedTo' => ActivityPub::TagManager.instance.uri_for(account),
'contentMap' => include({
'zh-TW' => a_kind_of(String),
}),
})
end
it 'has a replies collection' do