96 Commits

Author SHA1 Message Date
1a7157000a Theming and such 2023-07-06 13:23:31 -04:00
6c658d9680 Update to 4.1.3 2023-07-06 09:38:36 -04:00
0d5781ca76 Bump version to v4.1.3 2023-07-06 15:07:20 +02:00
32ebeed59b Merge pull request from GHSA-55j9-c3mp-6fcq 2023-07-06 15:06:50 +02:00
e75ad1de0f Merge pull request from GHSA-9pxv-6qvf-pjwc
* Fix timeout handling of outbound HTTP requests

* Use CLOCK_MONOTONIC instead of Time.now
2023-07-06 15:06:24 +02:00
0aa0b71f2c Merge pull request from GHSA-9928-3cp5-93fm
* Fix attachments getting processed despite failing content-type validation

* Add a restrictive ImageMagick security policy tailored for Mastodon

* Fix misdetection of MP3 files with large cover art

* Reject unprocessable audio/video files instead of keeping them unchanged
2023-07-06 15:05:05 +02:00
c4f2609f7a Merge pull request from GHSA-ccm4-vgcc-73hp
* Tighten allowed HTML in oEmbed-based preview cards

* Sanitize preview cards at render time

* Add `sandbox` attribute to preview card iframes
2023-07-06 15:03:33 +02:00
9b6c0cac7d Add hardened headers to user-uploaded files (#25756) 2023-07-06 14:32:26 +02:00
fac2c9eb7d Update rack, rails, nokogiri and doorkeeper gems 2023-07-06 13:45:40 +02:00
a3d69a2c5d Fix OAuth apps page crashing when listing apps with certain admin API scopes (#25713) 2023-07-06 13:45:40 +02:00
8eb1bb8ba6 Allow carets in URL search params (#25216) 2023-07-06 13:45:40 +02:00
652ff76462 Fix Redis client and type errors introduced in #24285 (#24342) 2023-07-06 13:45:40 +02:00
6f484fbbd2 IndexingScheduler: fetch and import in batches (#24285)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-07-06 13:45:40 +02:00
79f5b8f156 Fix ResolveURLService not resolving local URLs for remote content (#25637) 2023-07-06 13:45:40 +02:00
f8930a67a0 Change /api/v1/statuses/:id/history to always return at least one item (#25510) 2023-07-06 13:45:40 +02:00
e65e3a6d14 Add finer permission requirements for managing webhooks (#25463) 2023-07-06 13:45:40 +02:00
8acbfc6ab1 Fix wrong view being displayed when a webhook fails validation (#25464) 2023-07-06 13:45:40 +02:00
3ef53958b2 Prevent UserCleanupScheduler from overwhelming streaming (#25519) 2023-07-06 13:45:40 +02:00
fd1ffd72eb Fix incorrect pagination headers in /api/v2/admin/accounts (#25477) 2023-07-06 13:45:40 +02:00
7bd34f8b23 Fix infinite loop in AccountsStatusesCleanupScheduler (#24840) 2023-07-06 13:45:40 +02:00
7012bf6ed3 Improve automatic post cleanup worker performances (#24785) 2023-07-06 13:45:40 +02:00
d9e45f2fa9 Fix AccountsStatusesCleanupScheduler not spreading deletes across accounts correctly (#24607) 2023-07-06 13:45:40 +02:00
0e139e3c4d Change automatic post deletion thresholds and load detection (#24614) 2023-07-06 13:45:40 +02:00
23e7b4d28d Fix logging of messages that are binary before closing their connection (#25361) 2023-07-06 13:45:40 +02:00
e78ee582f7 Fix performance of streaming by parsing message JSON once (#25278) 2023-07-06 13:45:40 +02:00
a197fc094f Fix CSP headers when S3_ALIAS_HOST includes a path component (#25273) 2023-07-06 13:45:40 +02:00
bd7cbeeadf Fix tootctl accounts approve --number N not aproving N earliest registrations (#24605) 2023-07-06 13:45:40 +02:00
2779bce9a2 Add fallback redirection when getting a webfinger query LOCAL_DOMAIN@LOCAL_DOMAIN (#23600)
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
2023-07-06 13:45:40 +02:00
210ff36860 Change AccessTokensVacuum to also delete expired tokens (#24868) 2023-07-06 13:45:40 +02:00
99c2bbbec9 Change profile updates to be sent to recently-mentioned servers (#24852) 2023-07-06 13:45:40 +02:00
7e58779300 Fix reports not being closed when performing batch suspensions (#24988) 2023-07-06 13:45:40 +02:00
cca464bce3 Fix being able to vote on your own polls (#25015) 2023-07-06 13:45:40 +02:00
1301af60e0 Fix race condition when reblogging a status (#25016) 2023-07-06 13:45:40 +02:00
f962e83856 Change OpenGraph-based embeds to allow fullscreen (#25058) 2023-07-06 13:45:40 +02:00
b3cbcd7447 Fix “Authorized applications” inefficiently and incorrectly getting last use date (#25060) 2023-07-06 13:45:40 +02:00
72d96bf17a Remove invalid X-Frame-Options: ALLOWALL (#25070) 2023-07-06 13:45:40 +02:00
b1ac3562df Change Identity to not destroy associated User on destroy (#25098) 2023-07-06 13:45:40 +02:00
4c6c790f80 Fix /api/v1/conversations sometimes returning empty accounts (#25499) 2023-07-06 13:45:40 +02:00
036ac5b5c9 Fix ArgumentError when loading newer Private Mentions (#25399) 2023-07-06 13:45:40 +02:00
3e1724e972 Fix multiple N+1s in ConversationsController (#25134) 2023-07-06 13:45:40 +02:00
bc8592627b Fix user archive takeouts when using OpenStack Swift (#24431) 2023-07-06 13:45:40 +02:00
4b9e4f6398 Bump version to v4.1.2 2023-04-04 12:41:27 +02:00
b9f271364e Fix unescaped user input in LDAP query (#24379)
Fix CVE-2023-28853
2023-04-04 12:41:27 +02:00
4eaa6d58b2 Change root Chewy strategy to emit a warning instead of erroring out in production mode (#24327) 2023-04-04 12:41:27 +02:00
51572ac615 Fix invalid/expired invites being processed on sign-up (#24337) 2023-04-04 12:41:27 +02:00
Sai
01617534fa Update Ruby to 3.0.6 (#24334) 2023-04-04 12:41:27 +02:00
af6eb37c70 Wrap db:setup with Chewy.strategy(:mastodon) (#24302) 2023-04-04 12:41:27 +02:00
590df443f1 Bump blurhash from 0.1.6 to 0.1.7 (#23517) 2023-04-04 12:41:27 +02:00
ae64c5b7ec Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support (#24200) 2023-04-04 12:41:27 +02:00
3c82c4e780 Fix crash in tootctl commands making use of parallelization when Elasticsearch is enabled (#24182) 2023-04-04 12:41:27 +02:00
ab85f59c30 Bump version to v4.1.1 2023-03-16 22:48:42 +01:00
6a7b91a038 Add warning for object storage misconfiguration (#24137) 2023-03-16 22:48:42 +01:00
6db76875fd Change user backups to use expiring URLs for download when possible (#24136) 2023-03-16 22:48:42 +01:00
19def1a1f1 Update changelog 2023-03-16 22:03:22 +01:00
0e58e7f5d8 Update changelog 2023-03-16 11:51:36 +01:00
8c4ea7d715 Fix misleading error code when receiving invalid WebAuthn credentials (#23568) 2023-03-16 11:45:53 +01:00
cc65f32714 Fix incorrect post links in strikes when the account is remote (#23611) 2023-03-16 11:45:33 +01:00
0363064501 Fix dashboard crash on ElasticSearch server error (#23751) 2023-03-16 11:45:01 +01:00
46d6cb0f36 Skip pushing containers on forks (#24106) 2023-03-16 11:44:25 +01:00
4213907aaf Use Github Container Registry as the official container image source (#24113) 2023-03-16 11:44:11 +01:00
0891a8d4b0 Skip Docker CI Login/Push on forks (#23564) 2023-03-16 11:43:59 +01:00
0529fb0866 Push Docker images to Github Container Registry as well (#24101) 2023-03-16 11:43:46 +01:00
59a2fe32ff Add cache headers to static files served through Rails (#24120) 2023-03-16 11:43:18 +01:00
5cc39a3810 Add SENDFILE_HEADER environment variable (#24123) 2023-03-16 11:42:41 +01:00
4e02c7dc2c Support the PROXY protocol through the PROXY_PROTO_V1 env variable (#24064) 2023-03-16 11:42:27 +01:00
fe7752f4b8 Update changelog 2023-03-13 18:50:33 +01:00
6962d117b7 Change ActivityPub::DeliveryWorker retries to be spread out more (#21956) 2023-03-13 18:49:50 +01:00
2a37dc7967 Change unintended SMTP read timeout from 5 seconds to 20 seconds (#23750) 2023-03-13 18:49:38 +01:00
a54bd84690 Switched bookmark and favourites around (#23701) 2023-03-13 18:49:27 +01:00
68af19c328 Change auto-deletion throttling constants to better scale with server size (#23320) 2023-03-13 18:49:01 +01:00
a133570b26 Increase contrast of upload progress background (#23836) 2023-03-13 18:48:21 +01:00
9972eb41ae add modal message when editing toot (#23936)
Co-authored-by: PauloVilarinho <paulotarsobranco@hotmail.com>
2023-03-13 18:48:06 +01:00
9p4
78c7c79d78 Add refreshing many accounts at once with "tootctl accounts refresh" (#23304) 2023-03-13 18:47:52 +01:00
cec59417d7 Add mail headers to avoid auto-replies (#23597) 2023-03-13 18:47:28 +01:00
9377c4a87c Add lang tag to native language names in language picker (#23749) 2023-03-13 18:47:14 +01:00
40ae8d5e03 Fix paths with url-encoded @ to redirect to the correct path (#23593) 2023-03-13 18:46:57 +01:00
3f2e31800e Unescape HTML entities (#24019) 2023-03-13 18:45:42 +01:00
92a26638eb Do not strip tags from Setting.site_short_description (#23975) 2023-03-13 18:44:38 +01:00
479b66637b Fix sidekiq jobs not triggering Elasticsearch index updates (#24046) 2023-03-13 18:44:09 +01:00
14bcd14289 Center the text itself in upload area (#24029) 2023-03-13 18:43:54 +01:00
4bfbeb8139 Fix /api/v1/streaming sub-paths not being redirected (#23988) 2023-03-13 18:43:04 +01:00
2fed61a477 Fix pgBouncer resetting application name on every transaction (#23958) 2023-03-13 18:42:45 +01:00
37a28ba203 Do not leave Mastodon when clicking “Back” (#23953) 2023-03-13 18:42:29 +01:00
4cec3ad9b8 Fix original account being unfollowed on migration before the follow request could be sent (#21957) 2023-03-13 18:41:40 +01:00
675c24a34e Fix unconfirmed accounts being registered as active users (#23803) 2023-03-13 18:40:55 +01:00
f5f17e897b Fix tootctl accounts migrate error due to typo (#23567) 2023-03-13 18:40:18 +01:00
63532d9883 Fix error when displaying post history of a trendable post in the admin interface (#23574) 2023-03-13 18:39:51 +01:00
aff3f850de Fix server error when failing to follow back followers from /relationships (#23787) 2023-03-13 18:39:35 +01:00
b52746e64b Fix duplicate “Publish” button on mobile (#23804) 2023-03-13 18:38:18 +01:00
69564db447 Fix inefficiency when searching accounts per username in admin interface (#23801) 2023-03-13 18:38:01 +01:00
00208b23b1 Fix sidebar cut-off on small screens in admin UI (#23764) 2023-03-13 18:37:40 +01:00
900790184a Fix focus point of already-attached media not saving after edit (#23566) 2023-03-13 18:37:26 +01:00
11d6663025 Fix case-sensitive check for previously used hashtags (#23526) 2023-03-13 18:37:13 +01:00
ea1d55a64e fix metrics format (#23520) 2023-03-13 18:36:50 +01:00
ac7665193c dot is not allowed (#23519) 2023-03-13 18:36:36 +01:00
0dc342df81 Fix “Remove all followers from the selected domains” being more destructive than it claims (#23805) 2023-03-13 18:36:15 +01:00
140 changed files with 6338 additions and 400 deletions

View File

@ -12,6 +12,7 @@ on:
- Dockerfile - Dockerfile
permissions: permissions:
contents: read contents: read
packages: write
jobs: jobs:
build-image: build-image:
@ -26,15 +27,28 @@ jobs:
- uses: hadolint/hadolint-action@v3.1.0 - uses: hadolint/hadolint-action@v3.1.0
- uses: docker/setup-qemu-action@v2 - uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2 - uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
if: github.event_name != 'pull_request' if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request'
- name: Log in to the Github Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request'
- uses: docker/metadata-action@v4 - uses: docker/metadata-action@v4
id: meta id: meta
with: with:
images: tootsuite/mastodon images: |
tootsuite/mastodon
ghcr.io/mastodon/mastodon
flavor: | flavor: |
latest=auto latest=auto
tags: | tags: |
@ -42,13 +56,15 @@ jobs:
type=pep440,pattern={{raw}} type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}} type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=pr type=ref,event=pr
- uses: docker/build-push-action@v4 - uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
provenance: false provenance: false
builder: ${{ steps.buildx.outputs.name }} builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

View File

@ -70,3 +70,23 @@ docker-compose.override.yml
# Ignore locale files # Ignore locale files
/app/javascript/mastodon/locales /app/javascript/mastodon/locales
/config/locales /config/locales
<<<<<<< Updated upstream
=======
# Ignore vendored CSS reset
app/javascript/styles/mastodon/reset.scss
# Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631
*.js
*.jsx
*.ts
*.tsx
# Ignore HTML till cleaned and included in CI
*.html
# Ignore the generated AUTHORS.md
AUTHORS.md
/Mastodon-Modern
>>>>>>> Stashed changes

View File

@ -1 +1 @@
3.0.4 3.0.6

1
.stylelintignore Normal file
View File

@ -0,0 +1 @@
app/javascript/styles/modern/style.scss

View File

@ -3,6 +3,119 @@ Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.1.3] - 2023-07-06
### Added
- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600))
### Changed
- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058))
- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868))
- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852))
- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614))
- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510))
- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216))
### Removed
- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070))
### Fixed
- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464))
- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519))
- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477))
- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840))
- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361))
- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273))
- Fix `tootctl accounts approve --number N` not aproving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605))
- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988))
- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015))
- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016))
- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060))
- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713))
- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499))
- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431))
- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637))
- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342))
### Security
- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463))
- Update dependencies
- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756))
- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462)
- Fix timeout handling of outbound HTTP requests (CVE-2023-36461)
- Fix arbitrary file creation through media processing (CVE-2023-36460)
- Fix possible XSS in preview cards (CVE-2023-36459)
## [4.1.2] - 2023-04-04
### Fixed
- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377))
- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302))
- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200))
- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337))
### Security
- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24334))
- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379))
## [4.1.1] - 2023-03-16
### Added
- Add redirection from paths with url-encoded `@` to their decoded form ([thijskh](https://github.com/mastodon/mastodon/pull/23593))
- Add `lang` attribute to native language names in language picker in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23749))
- Add headers to outgoing mails to avoid auto-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23597))
- Add support for refreshing many accounts at once with `tootctl accounts refresh` ([9p4](https://github.com/mastodon/mastodon/pull/23304))
- Add confirmation modal when clicking to edit a post with a non-empty compose form ([PauloVilarinho](https://github.com/mastodon/mastodon/pull/23936))
- Add support for the HAproxy PROXY protocol through the `PROXY_PROTO_V1` environment variable ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24064))
- Add `SENDFILE_HEADER` environment variable ([Gargron](https://github.com/mastodon/mastodon/pull/24123))
- Add cache headers to static files served through Rails ([Gargron](https://github.com/mastodon/mastodon/pull/24120))
### Changed
- Increase contrast of upload progress bar background ([toolmantim](https://github.com/mastodon/mastodon/pull/23836))
- Change post auto-deletion throttling constants to better scale with server size ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23320))
- Change order of bookmark and favourite sidebar entries in single-column UI for consistency ([TerryGarcia](https://github.com/mastodon/mastodon/pull/23701))
- Change `ActivityPub::DeliveryWorker` retries to be spread out more ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21956))
### Fixed
- Fix “Remove all followers from the selected domains” also removing follows and notifications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805))
- Fix streaming metrics format ([emilweth](https://github.com/mastodon/mastodon/pull/23519), [emilweth](https://github.com/mastodon/mastodon/pull/23520))
- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526))
- Fix focus point of already-attached media not saving after edit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23566))
- Fix sidebar behavior in settings/admin UI on mobile ([wxt2005](https://github.com/mastodon/mastodon/pull/23764))
- Fix inefficiency when searching accounts per username in admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23801))
- Fix duplicate “Publish” button on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23804))
- Fix server error when failing to follow back followers from `/relationships` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23787))
- Fix server error when attempting to display the edit history of a trendable post in the admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23574))
- Fix `tootctl accounts migrate` crashing because of a typo ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23567))
- Fix original account being unfollowed on migration before the follow request to the new account could be sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21957))
- Fix the “Back” button in column headers sometimes leaving Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953))
- Fix pgBouncer resetting application name on every transaction ([Gargron](https://github.com/mastodon/mastodon/pull/23958))
- Fix unconfirmed accounts being counted as active users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23803))
- Fix `/api/v1/streaming` sub-paths not being redirected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23988))
- Fix drag'n'drop upload area text that spans multiple lines not being centered ([vintprox](https://github.com/mastodon/mastodon/pull/24029))
- Fix sidekiq jobs not triggering Elasticsearch index updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24046))
- Fix tags being unnecessarily stripped from plain-text short site description ([c960657](https://github.com/mastodon/mastodon/pull/23975))
- Fix HTML entities not being un-escaped in extracted plain-text from remote posts ([c960657](https://github.com/mastodon/mastodon/pull/24019))
- Fix dashboard crash on ElasticSearch server error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23751))
- Fix incorrect post links in strikes when the account is remote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23611))
- Fix misleading error code when receiving invalid WebAuthn credentials ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23568))
- Fix duplicate mails being sent when the SMTP server is too slow to close the connection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23750))
### Security
- Change user backups to use expiring URLs for download when possible ([Gargron](https://github.com/mastodon/mastodon/pull/24136))
- Add warning for object storage misconfiguration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24137))
## [4.1.0] - 2023-02-10 ## [4.1.0] - 2023-02-10
### Added ### Added

View File

@ -2,7 +2,7 @@
# This needs to be bullseye-slim because the Ruby image is built on bullseye-slim # This needs to be bullseye-slim because the Ruby image is built on bullseye-slim
ARG NODE_VERSION="16.18.1-bullseye-slim" ARG NODE_VERSION="16.18.1-bullseye-slim"
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.4-slim as ruby FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.6-slim as ruby
FROM node:${NODE_VERSION} as build FROM node:${NODE_VERSION} as build
COPY --link --from=ruby /opt/ruby /opt/ruby COPY --link --from=ruby /opt/ruby /opt/ruby

View File

@ -10,40 +10,40 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.7.2) actioncable (6.1.7.4)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.2) actionmailbox (6.1.7.4)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.4)
activejob (= 6.1.7.2) activejob (= 6.1.7.4)
activerecord (= 6.1.7.2) activerecord (= 6.1.7.4)
activestorage (= 6.1.7.2) activestorage (= 6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.7.2) actionmailer (6.1.7.4)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.4)
actionview (= 6.1.7.2) actionview (= 6.1.7.4)
activejob (= 6.1.7.2) activejob (= 6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.7.2) actionpack (6.1.7.4)
actionview (= 6.1.7.2) actionview (= 6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.7.2) actiontext (6.1.7.4)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.4)
activerecord (= 6.1.7.2) activerecord (= 6.1.7.4)
activestorage (= 6.1.7.2) activestorage (= 6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.7.2) actionview (6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -54,22 +54,22 @@ GEM
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (6.1.7.2) activejob (6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.7.2) activemodel (6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
activerecord (6.1.7.2) activerecord (6.1.7.4)
activemodel (= 6.1.7.2) activemodel (= 6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
activestorage (6.1.7.2) activestorage (6.1.7.4)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.4)
activejob (= 6.1.7.2) activejob (= 6.1.7.4)
activerecord (= 6.1.7.2) activerecord (= 6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (6.1.7.2) activesupport (6.1.7.4)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -120,8 +120,7 @@ GEM
bindata (2.4.14) bindata (2.4.14)
binding_of_caller (1.0.0) binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
blurhash (0.1.6) blurhash (0.1.7)
ffi (~> 1.14)
bootsnap (1.16.0) bootsnap (1.16.0)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (5.4.0) brakeman (5.4.0)
@ -174,7 +173,7 @@ GEM
cocoon (1.2.15) cocoon (1.2.15)
coderay (1.1.3) coderay (1.1.3)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.2.0) concurrent-ruby (1.2.2)
connection_pool (2.3.0) connection_pool (2.3.0)
cose (1.2.1) cose (1.2.1)
cbor (~> 0.5.9) cbor (~> 0.5.9)
@ -207,7 +206,7 @@ GEM
docile (1.4.0) docile (1.4.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.6.4) doorkeeper (5.6.6)
railties (>= 5) railties (>= 5)
dotenv (2.8.1) dotenv (2.8.1)
dotenv-rails (2.8.1) dotenv-rails (2.8.1)
@ -389,7 +388,7 @@ GEM
loofah (2.19.1) loofah (2.19.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.8.0.1) mail (2.8.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap net-imap
net-pop net-pop
@ -406,12 +405,12 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105) mime-types-data (3.2022.0105)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.1) mini_portile2 (2.8.2)
minitest (5.17.0) minitest (5.17.0)
msgpack (1.6.0) msgpack (1.6.0)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.1.1) multipart-post (2.1.1)
net-imap (0.3.4) net-imap (0.3.6)
date date
net-protocol net-protocol
net-ldap (0.17.1) net-ldap (0.17.1)
@ -424,8 +423,8 @@ GEM
net-smtp (0.3.3) net-smtp (0.3.3)
net-protocol net-protocol
net-ssh (7.0.1) net-ssh (7.0.1)
nio4r (2.5.8) nio4r (2.5.9)
nokogiri (1.14.1) nokogiri (1.14.5)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nsa (0.2.8) nsa (0.2.8)
@ -498,7 +497,7 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.2) racc (1.6.2)
rack (2.2.6.2) rack (2.2.7)
rack-attack (6.6.1) rack-attack (6.6.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (1.1.1)
@ -513,20 +512,20 @@ GEM
rack rack
rack-test (2.0.2) rack-test (2.0.2)
rack (>= 1.3) rack (>= 1.3)
rails (6.1.7.2) rails (6.1.7.4)
actioncable (= 6.1.7.2) actioncable (= 6.1.7.4)
actionmailbox (= 6.1.7.2) actionmailbox (= 6.1.7.4)
actionmailer (= 6.1.7.2) actionmailer (= 6.1.7.4)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.4)
actiontext (= 6.1.7.2) actiontext (= 6.1.7.4)
actionview (= 6.1.7.2) actionview (= 6.1.7.4)
activejob (= 6.1.7.2) activejob (= 6.1.7.4)
activemodel (= 6.1.7.2) activemodel (= 6.1.7.4)
activerecord (= 6.1.7.2) activerecord (= 6.1.7.4)
activestorage (= 6.1.7.2) activestorage (= 6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.7.2) railties (= 6.1.7.4)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
@ -542,9 +541,9 @@ GEM
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (6.1.7.2) railties (6.1.7.4)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.4)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.4)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@ -689,9 +688,9 @@ GEM
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (1.2.1) thor (1.2.2)
tilt (2.0.11) tilt (2.0.11)
timeout (0.3.1) timeout (0.3.2)
tpm-key_attestation (0.11.0) tpm-key_attestation (0.11.0)
bindata (~> 2.4) bindata (~> 2.4)
openssl (> 2.0, < 3.1) openssl (> 2.0, < 3.1)
@ -754,7 +753,7 @@ GEM
xorcist (1.1.3) xorcist (1.1.3)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.6) zeitwerk (2.6.8)
PLATFORMS PLATFORMS
ruby ruby

1
Mastodon-Modern Submodule

Submodule Mastodon-Modern added at 301d0033f5

View File

@ -8,13 +8,11 @@
[![Build Status](https://img.shields.io/circleci/project/github/mastodon/mastodon.svg)][circleci] [![Build Status](https://img.shields.io/circleci/project/github/mastodon/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/mastodon/mastodon.svg)][code_climate] [![Code Climate](https://img.shields.io/codeclimate/maintainability/mastodon/mastodon.svg)][code_climate]
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] [![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
[![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker]
[releases]: https://github.com/mastodon/mastodon/releases [releases]: https://github.com/mastodon/mastodon/releases
[circleci]: https://circleci.com/gh/mastodon/mastodon [circleci]: https://circleci.com/gh/mastodon/mastodon
[code_climate]: https://codeclimate.com/github/mastodon/mastodon [code_climate]: https://codeclimate.com/github/mastodon/mastodon
[crowdin]: https://crowdin.com/project/mastodon [crowdin]: https://crowdin.com/project/mastodon
[docker]: https://hub.docker.com/r/tootsuite/mastodon/
Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!) Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!)
@ -31,6 +29,7 @@ Click below to **learn more** in a video:
- [View sponsors](https://joinmastodon.org/sponsors) - [View sponsors](https://joinmastodon.org/sponsors)
- [Blog](https://blog.joinmastodon.org) - [Blog](https://blog.joinmastodon.org)
- [Documentation](https://docs.joinmastodon.org) - [Documentation](https://docs.joinmastodon.org)
- [Official Docker image](https://github.com/mastodon/mastodon/pkgs/container/mastodon)
- [Browse Mastodon servers](https://joinmastodon.org/communities) - [Browse Mastodon servers](https://joinmastodon.org/communities)
- [Browse Mastodon apps](https://joinmastodon.org/apps) - [Browse Mastodon apps](https://joinmastodon.org/apps)

View File

@ -20,6 +20,7 @@ module Admin
authorize :webhook, :create? authorize :webhook, :create?
@webhook = Webhook.new(resource_params) @webhook = Webhook.new(resource_params)
@webhook.current_account = current_account
if @webhook.save if @webhook.save
redirect_to admin_webhook_path(@webhook) redirect_to admin_webhook_path(@webhook)
@ -39,10 +40,12 @@ module Admin
def update def update
authorize @webhook, :update? authorize @webhook, :update?
@webhook.current_account = current_account
if @webhook.update(resource_params) if @webhook.update(resource_params)
redirect_to admin_webhook_path(@webhook) redirect_to admin_webhook_path(@webhook)
else else
render :show render :edit
end end
end end

View File

@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController
def index def index
@conversations = paginated_conversations @conversations = paginated_conversations
render json: @conversations, each_serializer: REST::ConversationSerializer render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id)
end end
def read def read
@ -32,6 +32,19 @@ class Api::V1::ConversationsController < Api::BaseController
def paginated_conversations def paginated_conversations
AccountConversation.where(account: current_account) AccountConversation.where(account: current_account)
.includes(
account: :account_stat,
last_status: [
:media_attachments,
:preview_cards,
:status_stat,
:tags,
{
active_mentions: [account: :account_stat],
account: :account_stat,
},
]
)
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) .to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end end

View File

@ -7,11 +7,15 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
before_action :set_status before_action :set_status
def show def show
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer render json: status_edits, each_serializer: REST::StatusEditSerializer
end end
private private
def status_edits
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
end
def set_status def set_status
@status = Status.find(params[:status_id]) @status = Status.find(params[:status_id])
authorize @status, :show? authorize @status, :show?

View File

@ -2,6 +2,8 @@
class Api::V1::Statuses::ReblogsController < Api::BaseController class Api::V1::Statuses::ReblogsController < Api::BaseController
include Authorization include Authorization
include Redisable
include Lockable
before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user! before_action :require_user!
@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :create, family: :statuses
def create def create
with_lock("reblog:#{current_account.id}:#{@reblog.id}") do
@status = ReblogService.new.call(current_account, @reblog, reblog_params) @status = ReblogService.new.call(current_account, @reblog, reblog_params)
end
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer
end end

View File

@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
private private
def next_path
api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end
def filtered_accounts def filtered_accounts
AccountFilter.new(translated_filter_params).results AccountFilter.new(translated_filter_params).results
end end

View File

@ -48,7 +48,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
super(hash) super(hash)
resource.locale = I18n.locale resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank? resource.invite_code = @invite&.code if resource.invite_code.blank?
resource.registration_form_time = session[:registration_form_time] resource.registration_form_time = session[:registration_form_time]
resource.sign_up_ip = request.remote_ip resource.sign_up_ip = request.remote_ip

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
class BackupsController < ApplicationController
include RoutingHelper
skip_before_action :require_functional!
before_action :authenticate_user!
before_action :set_backup
def download
case Paperclip::Attachment.default_options[:storage]
when :s3
redirect_to @backup.dump.expiring_url(10)
when :fog
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
else
redirect_to full_asset_url(@backup.dump.url)
end
when :filesystem
redirect_to full_asset_url(@backup.dump.url)
end
end
private
def set_backup
@backup = current_user.backups.find(params[:id])
end
end

View File

@ -46,6 +46,6 @@ class MediaController < ApplicationController
end end
def allow_iframing def allow_iframing
response.headers['X-Frame-Options'] = 'ALLOWALL' response.headers.delete('X-Frame-Options')
end end
end end

View File

@ -8,6 +8,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :require_not_suspended!, only: :destroy before_action :require_not_suspended!, only: :destroy
before_action :set_body_classes before_action :set_body_classes
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
skip_before_action :require_functional! skip_before_action :require_functional!
include Localized include Localized
@ -30,4 +32,14 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
def require_not_suspended! def require_not_suspended!
forbidden if current_account.suspended? forbidden if current_account.suspended?
end end
def set_last_used_at_by_app
@last_used_at_by_app = Doorkeeper::AccessToken
.select('DISTINCT ON (application_id) application_id, last_used_at')
.where(resource_owner_id: current_resource_owner.id)
.where.not(last_used_at: nil)
.order(application_id: :desc, last_used_at: :desc)
.pluck(:application_id, :last_used_at)
.to_h
end
end end

View File

@ -19,6 +19,8 @@ class RelationshipsController < ApplicationController
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing
# Do nothing # Do nothing
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound
flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow'
ensure ensure
redirect_to relationships_path(filter_params) redirect_to relationships_path(filter_params)
end end
@ -60,8 +62,8 @@ class RelationshipsController < ApplicationController
'unfollow' 'unfollow'
elsif params[:remove_from_followers] elsif params[:remove_from_followers]
'remove_from_followers' 'remove_from_followers'
elsif params[:block_domains] elsif params[:block_domains] || params[:remove_domains_from_followers]
'block_domains' 'remove_domains_from_followers'
end end
end end

View File

@ -52,7 +52,7 @@ module Settings
end end
else else
flash[:error] = I18n.t('webauthn_credentials.create.error') flash[:error] = I18n.t('webauthn_credentials.create.error')
status = :internal_server_error status = :unprocessable_entity
end end
else else
flash[:error] = t('webauthn_credentials.create.error') flash[:error] = t('webauthn_credentials.create.error')

View File

@ -43,7 +43,7 @@ class StatusesController < ApplicationController
return not_found if @status.hidden? || @status.reblog? return not_found if @status.hidden? || @status.reblog?
expires_in 180, public: true expires_in 180, public: true
response.headers['X-Frame-Options'] = 'ALLOWALL' response.headers.delete('X-Frame-Options')
render layout: 'embedded' render layout: 'embedded'
end end

View File

@ -18,7 +18,14 @@ module WellKnown
private private
def set_account def set_account
@account = Account.find_local!(username_from_resource) username = username_from_resource
@account = begin
if username == Rails.configuration.x.local_domain
Account.representative
else
Account.find_local!(username)
end
end
end end
def username_from_resource def username_from_resource

View File

@ -58,6 +58,10 @@ module FormattingHelper
end end
def account_field_value_format(field, with_rel_me: true) def account_field_value_format(field, with_rel_me: true)
if field.verified? && !field.account.local?
TextFormatter.shortened_link(field.value_for_verification)
else
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false) html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
end end
end
end end

View File

@ -165,11 +165,19 @@ export function submitCompose(routerHistory) {
// API call. // API call.
let media_attributes; let media_attributes;
if (statusId !== null) { if (statusId !== null) {
media_attributes = media.map(item => ({ media_attributes = media.map(item => {
let focus;
if (item.getIn(['meta', 'focus'])) {
focus = `${item.getIn(['meta', 'focus', 'x']).toFixed(2)},${item.getIn(['meta', 'focus', 'y']).toFixed(2)}`;
}
return {
id: item.get('id'), id: item.get('id'),
description: item.get('description'), description: item.get('description'),
focus: item.get('focus'), focus,
})); };
});
} }
api(getState).request({ api(getState).request({

View File

@ -15,10 +15,10 @@ export default class ColumnBackButton extends React.PureComponent {
}; };
handleClick = () => { handleClick = () => {
if (window.history && window.history.length === 1) { if (window.history && window.history.state) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack(); this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
} }
}; };

View File

@ -43,14 +43,6 @@ class ColumnHeader extends React.PureComponent {
animating: false, animating: false,
}; };
historyBack = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();
}
};
handleToggleClick = (e) => { handleToggleClick = (e) => {
e.stopPropagation(); e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true }); this.setState({ collapsed: !this.state.collapsed, animating: true });
@ -69,7 +61,11 @@ class ColumnHeader extends React.PureComponent {
}; };
handleBackClick = () => { handleBackClick = () => {
this.historyBack(); if (window.history && window.history.state) {
this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
}
}; };
handleTransitionEnd = () => { handleTransitionEnd = () => {

View File

@ -56,6 +56,8 @@ const messages = defineMessages({
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
}); });
@ -149,7 +151,18 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}, },
onEdit (status, history) { onEdit (status, history) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.editMessage),
confirm: intl.formatMessage(messages.editConfirm),
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
}));
} else {
dispatch(editStatus(status.get('id'), history)); dispatch(editStatus(status.get('id'), history));
}
});
}, },
onTranslate (status) { onTranslate (status) {

View File

@ -90,7 +90,7 @@ class ComposeForm extends ImmutablePureComponent {
const fulltext = this.getFulltextForCharacterCounting(); const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0; const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia)); return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 1500 || (isOnlyWhitespace && !anyMedia));
}; };
handleSubmit = (e) => { handleSubmit = (e) => {
@ -280,7 +280,7 @@ class ComposeForm extends ImmutablePureComponent {
</div> </div>
<div className='character-counter__wrapper'> <div className='character-counter__wrapper'>
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} /> <CharacterCounter max={1500} text={this.getFulltextForCharacterCounting()} />
</div> </div>
</div> </div>

View File

@ -210,7 +210,7 @@ class LanguageDropdownMenu extends React.PureComponent {
return ( return (
<div key={lang[0]} role='option' tabIndex='0' data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}> <div key={lang[0]} role='option' tabIndex='0' data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}>
<span className='language-dropdown__dropdown__results__item__native-name'>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span> <span className='language-dropdown__dropdown__results__item__native-name' lang={lang[0]}>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
</div> </div>
); );
}; };

View File

@ -22,8 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
}, },
}); });
export default @connect(null, mapDispatchToProps) export default @withRouter
@withRouter @connect(null, mapDispatchToProps)
class Header extends React.PureComponent { class Header extends React.PureComponent {
static contextTypes = { static contextTypes = {

View File

@ -82,8 +82,8 @@ class NavigationPanel extends React.Component {
{signedIn && ( {signedIn && (
<React.Fragment> <React.Fragment>
<ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} /> <ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} /> <ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} /> <ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
<ListPanel /> <ListPanel />

View File

@ -474,10 +474,10 @@ class UI extends React.PureComponent {
}; };
handleHotkeyBack = () => { handleHotkeyBack = () => {
if (window.history && window.history.length === 1) { if (window.history && window.history.state) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack(); this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
} }
}; };

View File

@ -162,6 +162,8 @@
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?", "confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.domain_block.confirm": "Block entire domain", "confirmations.domain_block.confirm": "Block entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"confirmations.edit.confirm": "Edit",
"confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.logout.confirm": "Log out", "confirmations.logout.confirm": "Log out",
"confirmations.logout.message": "Are you sure you want to log out?", "confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Mute",

View File

@ -186,11 +186,12 @@ const ignoreSuggestion = (state, position, token, completion, path) => {
}; };
const sortHashtagsByUse = (state, tags) => { const sortHashtagsByUse = (state, tags) => {
const personalHistory = state.get('tagHistory'); const personalHistory = state.get('tagHistory').map(tag => tag.toLowerCase());
return tags.sort((a, b) => { const tagsWithLowercase = tags.map(t => ({ ...t, lowerName: t.name.toLowerCase() }));
const usedA = personalHistory.includes(a.name); const sorted = tagsWithLowercase.sort((a, b) => {
const usedB = personalHistory.includes(b.name); const usedA = personalHistory.includes(a.lowerName);
const usedB = personalHistory.includes(b.lowerName);
if (usedA === usedB) { if (usedA === usedB) {
return 0; return 0;
@ -200,6 +201,8 @@ const sortHashtagsByUse = (state, tags) => {
return 1; return 1;
} }
}); });
sorted.forEach(tag => delete tag.lowerName);
return sorted;
}; };
const insertEmoji = (state, position, emojiData, needsSpace) => { const insertEmoji = (state, position, emojiData, needsSpace) => {

View File

@ -254,6 +254,10 @@ html {
border-color: $ui-base-color; border-color: $ui-base-color;
} }
.upload-progress__backdrop {
background: $ui-base-color;
}
// Change the background colors of statuses // Change the background colors of statuses
.focusable:focus { .focusable:focus {
background: $ui-base-color; background: $ui-base-color;

View File

@ -384,7 +384,7 @@ $content-width: 840px;
position: fixed; position: fixed;
z-index: 10; z-index: 10;
width: 100%; width: 100%;
height: calc(100vh - 56px); height: calc(100% - 56px);
left: 0; left: 0;
bottom: 0; bottom: 0;
overflow-y: auto; overflow-y: auto;

View File

@ -4482,6 +4482,7 @@ a.status-card.compact:hover {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center;
color: $secondary-text-color; color: $secondary-text-color;
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 500;
@ -4516,7 +4517,7 @@ a.status-card.compact:hover {
width: 100%; width: 100%;
height: 6px; height: 6px;
border-radius: 6px; border-radius: 6px;
background: $ui-base-lighter-color; background: darken($simple-background-color, 8%);
position: relative; position: relative;
margin-top: 5px; margin-top: 5px;
} }

View File

@ -0,0 +1,8 @@
@import 'wobbl/variables';
@import 'application';
@import 'modern/modern';
.layout-multiple-columns .column {
flex-grow: 1;
max-width: 500px;
}

View File

@ -0,0 +1,8 @@
@import 'mastodon/variables';
@import 'application';
@import 'modern/modern';
.layout-multiple-columns .column {
flex-grow: 1;
max-width: 500px;
}

View File

@ -0,0 +1,9 @@
@import 'wobbl-light/variables';
@import 'application';
@import 'modern/modern';
@import 'mastodon-light/diff';
.layout-multiple-columns .column {
flex-grow: 1;
max-width: 500px;
}

View File

@ -0,0 +1,9 @@
@import 'mastodon-light/variables';
@import 'application';
@import 'modern/modern';
@import 'mastodon-light/diff';
.layout-multiple-columns .column {
flex-grow: 1;
max-width: 500px;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
@import 'wobbl-light/variables';
@import 'application';
@import 'mastodon-light/diff';

View File

@ -0,0 +1,44 @@
// Dependent colors
$black: #000000;
$white: #ffffff;
$classic-base-color: #191919;// Dark Gray
$classic-primary-color: #E7E7E7;// Platinum
$classic-secondary-color: #FF8680; // Paler red
$classic-highlight-color: #850700; // Red highlights
// Differences
$success-green: lighten(#3c754d, 8%);
$base-overlay-background: $white !default;
$valid-value-color: $success-green !default;
$ui-base-color: $classic-primary-color !default;
$ui-base-lighter-color: #b0c0cf;
$ui-primary-color: #9bcbed;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: $classic-highlight-color !default;
$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
$dark-text-color: #444b5d;
$action-button-color: #606984;
$inverted-text-color: $black !default;
$lighter-text-color: $classic-base-color !default;
$light-text-color: #444b5d;
// Newly added colors
$account-background-color: $white !default;
// Invert darkened and lightened colors
@function darken($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) + $amount);
}
@function lighten($color, $amount) {
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
}
$emojis-requiring-inversion: 'chains';

View File

@ -0,0 +1,2 @@
@import 'wobbl/variables';
@import 'application';

View File

@ -0,0 +1,60 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$red-bookmark: $warning-red;
// Values from the classic Mastodon UI
$classic-base-color: #191919;// Dark Gray
$classic-primary-color: #E7E7E7;// Platinum
$classic-secondary-color: #FF8680; // Paler red
$classic-highlight-color: #850700; // Red highlights
// Variables for defaults in UI
$base-shadow-color: $black !default;
$base-overlay-background: $black !default;
$base-border-color: $white !default;
$simple-background-color: $white !default;
$valid-value-color: $success-green !default;
$error-value-color: $error-red !default;
// Tell UI to use selected colors
$ui-base-color: $classic-base-color !default; // Darkest
$ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkest
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
// Variables for texts
$primary-text-color: $white !default;
$darker-text-color: $ui-primary-color !default;
$dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: lighten($ui-highlight-color, 8%) !default;
$action-button-color: $ui-base-lighter-color !default;
$passive-text-color: $gold-star !default;
$active-passive-text-color: $success-green !default;
// For texts on inverted backgrounds
$inverted-text-color: $ui-base-color !default;
$lighter-text-color: $ui-base-lighter-color !default;
$light-text-color: $ui-primary-color !default;
// Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
// Variables for components
$media-modal-media-max-width: 100%;
// put margins on top and bottom of image to avoid the screen covered by image.
$media-modal-media-max-height: 80%;
$no-gap-breakpoint: 1175px;
$font-sans-serif: 'mastodon-font-sans-serif' !default;
$font-display: 'mastodon-font-display' !default;
$font-monospace: 'mastodon-font-monospace' !default;

View File

@ -6,7 +6,7 @@ class AccountReachFinder
end end
def inboxes def inboxes
(followers_inboxes + reporters_inboxes + relay_inboxes).uniq (followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + relay_inboxes).uniq
end end
private private
@ -19,6 +19,13 @@ class AccountReachFinder
Account.where(id: @account.targeted_reports.select(:account_id)).inboxes Account.where(id: @account.targeted_reports.select(:account_id)).inboxes
end end
def recently_mentioned_inboxes
cutoff_id = Mastodon::Snowflake.id_at(2.days.ago, with_random: false)
recent_statuses = @account.statuses.recent.where(id: cutoff_id...).limit(200)
Account.joins(:mentions).where(mentions: { status: recent_statuses }).inboxes.take(2000)
end
def relay_inboxes def relay_inboxes
Relay.enabled.pluck(:inbox_url) Relay.enabled.pluck(:inbox_url)
end end

View File

@ -2,6 +2,7 @@
class Admin::SystemCheck class Admin::SystemCheck
ACTIVE_CHECKS = [ ACTIVE_CHECKS = [
Admin::SystemCheck::MediaPrivacyCheck,
Admin::SystemCheck::DatabaseSchemaCheck, Admin::SystemCheck::DatabaseSchemaCheck,
Admin::SystemCheck::SidekiqProcessCheck, Admin::SystemCheck::SidekiqProcessCheck,
Admin::SystemCheck::RulesCheck, Admin::SystemCheck::RulesCheck,

View File

@ -31,7 +31,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
def running_version def running_version
@running_version ||= begin @running_version ||= begin
Chewy.client.info['version']['number'] Chewy.client.info['version']['number']
rescue Faraday::ConnectionFailed rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
nil nil
end end
end end

View File

@ -0,0 +1,105 @@
# frozen_string_literal: true
class Admin::SystemCheck::MediaPrivacyCheck < Admin::SystemCheck::BaseCheck
include RoutingHelper
def skip?
!current_user.can?(:view_devops)
end
def pass?
check_media_uploads!
@failure_message.nil?
end
def message
Admin::SystemCheck::Message.new(@failure_message, @failure_value, @failure_action, true)
end
private
def check_media_uploads!
if Rails.configuration.x.use_s3
check_media_listing_inaccessible_s3!
else
check_media_listing_inaccessible!
end
end
def check_media_listing_inaccessible!
full_url = full_asset_url(media_attachment.file.url(:original, false))
# Check if we can list the uploaded file. If true, that's an error
directory_url = Addressable::URI.parse(full_url)
directory_url.query = nil
filename = directory_url.path.gsub(%r{.*/}, '')
directory_url.path = directory_url.path.gsub(%r{/[^/]+\Z}, '/')
Request.new(:get, directory_url, allow_local: true).perform do |res|
if res.truncated_body&.include?(filename)
@failure_message = use_storage? ? :upload_check_privacy_error_object_storage : :upload_check_privacy_error
@failure_action = 'https://docs.joinmastodon.org/admin/optional/object-storage/#FS'
end
end
rescue
nil
end
def check_media_listing_inaccessible_s3!
urls_to_check = []
paperclip_options = Paperclip::Attachment.default_options
s3_protocol = paperclip_options[:s3_protocol]
s3_host_alias = paperclip_options[:s3_host_alias]
s3_host_name = paperclip_options[:s3_host_name]
bucket_name = paperclip_options.dig(:s3_credentials, :bucket)
urls_to_check << "#{s3_protocol}://#{s3_host_alias}/" if s3_host_alias.present?
urls_to_check << "#{s3_protocol}://#{s3_host_name}/#{bucket_name}/"
urls_to_check.uniq.each do |full_url|
check_s3_listing!(full_url)
break if @failure_message.present?
end
rescue
nil
end
def check_s3_listing!(full_url)
bucket_url = Addressable::URI.parse(full_url)
bucket_url.path = bucket_url.path.delete_suffix(media_attachment.file.path(:original))
bucket_url.query = "max-keys=1&x-random=#{SecureRandom.hex(10)}"
Request.new(:get, bucket_url, allow_local: true).perform do |res|
if res.truncated_body&.include?('ListBucketResult')
@failure_message = :upload_check_privacy_error_object_storage
@failure_action = 'https://docs.joinmastodon.org/admin/optional/object-storage/#S3'
end
end
end
def media_attachment
@media_attachment ||= begin
attachment = Account.representative.media_attachments.first
if attachment.present?
attachment.touch # rubocop:disable Rails/SkipsModelValidations
attachment
else
create_test_attachment!
end
end
end
def create_test_attachment!
Tempfile.create(%w(test-upload .jpg), binmode: true) do |tmp_file|
tmp_file.write(
Base64.decode64(
'/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' \
'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' \
'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' \
'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' \
'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' \
'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q=='
)
)
tmp_file.flush
Account.representative.media_attachments.create!(file: tmp_file)
end
end
end

View File

@ -1,11 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class Admin::SystemCheck::Message class Admin::SystemCheck::Message
attr_reader :key, :value, :action attr_reader :key, :value, :action, :critical
def initialize(key, value = nil, action = nil) def initialize(key, value = nil, action = nil, critical = false)
@key = key @key = key
@value = value @value = value
@action = action @action = action
@critical = critical
end end
end end

View File

@ -9,10 +9,6 @@ module ApplicationExtension
validates :redirect_uri, length: { maximum: 2_000 } validates :redirect_uri, length: { maximum: 2_000 }
end end
def most_recently_used_access_token
@most_recently_used_access_token ||= access_tokens.where.not(last_used_at: nil).order(last_used_at: :desc).first
end
def confirmation_redirect_uri def confirmation_redirect_uri
redirect_uri.lines.first.strip redirect_uri.lines.first.strip
end end

View File

@ -140,7 +140,7 @@ class LinkDetailsExtractor
end end
def html def html
player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil player_url.present? ? content_tag(:iframe, nil, src: player_url, width: width, height: height, allowfullscreen: 'true', allowtransparency: 'true', scrolling: 'no', frameborder: '0') : nil
end end
def width def width

View File

@ -18,7 +18,7 @@ class PlainTextFormatter
if local? if local?
text text
else else
strip_tags(insert_newlines).chomp html_entities.decode(strip_tags(insert_newlines)).chomp
end end
end end
@ -27,4 +27,8 @@ class PlainTextFormatter
def insert_newlines def insert_newlines
text.gsub(NEWLINE_TAGS_RE) { |match| "#{match}\n" } text.gsub(NEWLINE_TAGS_RE) { |match| "#{match}\n" }
end end
def html_entities
HTMLEntities.new
end
end end

View File

@ -7,11 +7,48 @@ require 'resolv'
# Monkey-patch the HTTP.rb timeout class to avoid using a timeout block # Monkey-patch the HTTP.rb timeout class to avoid using a timeout block
# around the Socket#open method, since we use our own timeout blocks inside # around the Socket#open method, since we use our own timeout blocks inside
# that method # that method
#
# Also changes how the read timeout behaves so that it is cumulative (closer
# to HTTP::Timeout::Global, but still having distinct timeouts for other
# operation types)
class HTTP::Timeout::PerOperation class HTTP::Timeout::PerOperation
def connect(socket_class, host, port, nodelay = false) def connect(socket_class, host, port, nodelay = false)
@socket = socket_class.open(host, port) @socket = socket_class.open(host, port)
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
end end
# Reset deadline when the connection is re-used for different requests
def reset_counter
@deadline = nil
end
# Read data from the socket
def readpartial(size, buffer = nil)
@deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @read_timeout
timeout = false
loop do
result = @socket.read_nonblock(size, buffer, exception: false)
return :eof if result.nil?
remaining_time = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
raise HTTP::TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout || remaining_time <= 0
return result if result != :wait_readable
# marking the socket for timeout. Why is this not being raised immediately?
# it seems there is some race-condition on the network level between calling
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
# for reads, and when waiting for x seconds, it returns nil suddenly without completing
# the x seconds. In a normal case this would be a timeout on wait/read, but it can
# also mean that the socket has been closed by the server. Therefore we "mark" the
# socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
# timeout. Else, the first timeout was a proper timeout.
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
timeout = true unless @socket.to_io.wait_readable(remaining_time)
end
end
end end
class Request class Request

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ScopeParser < Parslet::Parser class ScopeParser < Parslet::Parser
rule(:term) { match('[a-z]').repeat(1).as(:term) } rule(:term) { match('[a-z_]').repeat(1).as(:term) }
rule(:colon) { str(':') } rule(:colon) { str(':') }
rule(:access) { (str('write') | str('read')).as(:access) } rule(:access) { (str('write') | str('read')).as(:access) }
rule(:namespace) { str('admin').as(:namespace) } rule(:namespace) { str('admin').as(:namespace) }

View File

@ -48,6 +48,26 @@ class TextFormatter
html.html_safe # rubocop:disable Rails/OutputSafety html.html_safe # rubocop:disable Rails/OutputSafety
end end
class << self
include ERB::Util
def shortened_link(url, rel_me: false)
url = Addressable::URI.parse(url).to_s
rel = rel_me ? (DEFAULT_REL + %w(me)) : DEFAULT_REL
prefix = url.match(URL_PREFIX_REGEX).to_s
display_url = url[prefix.length, 30]
suffix = url[prefix.length + 30..-1]
cutoff = url[prefix.length..-1].length > 30
<<~HTML.squish
<a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a>
HTML
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
h(url)
end
end
private private
def rewrite def rewrite
@ -70,19 +90,7 @@ class TextFormatter
end end
def link_to_url(entity) def link_to_url(entity)
url = Addressable::URI.parse(entity[:url]).to_s TextFormatter.shortened_link(entity[:url], rel_me: with_rel_me?)
rel = with_rel_me? ? (DEFAULT_REL + %w(me)) : DEFAULT_REL
prefix = url.match(URL_PREFIX_REGEX).to_s
display_url = url[prefix.length, 30]
suffix = url[prefix.length + 30..-1]
cutoff = url[prefix.length..-1].length > 30
<<~HTML.squish
<a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a>
HTML
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
h(entity[:url])
end end
def link_to_hashtag(entity) def link_to_hashtag(entity)

View File

@ -9,10 +9,12 @@ class Vacuum::AccessTokensVacuum
private private
def vacuum_revoked_access_tokens! def vacuum_revoked_access_tokens!
Doorkeeper::AccessToken.where.not(revoked_at: nil).where('revoked_at < NOW()').delete_all Doorkeeper::AccessToken.where.not(expires_in: nil).where('created_at + make_interval(secs => expires_in) < NOW()').in_batches.delete_all
Doorkeeper::AccessToken.where.not(revoked_at: nil).where('revoked_at < NOW()').in_batches.delete_all
end end
def vacuum_revoked_access_grants! def vacuum_revoked_access_grants!
Doorkeeper::AccessGrant.where.not(revoked_at: nil).where('revoked_at < NOW()').delete_all Doorkeeper::AccessGrant.where.not(expires_in: nil).where('created_at + make_interval(secs => expires_in) < NOW()').in_batches.delete_all
Doorkeeper::AccessGrant.where.not(revoked_at: nil).where('revoked_at < NOW()').in_batches.delete_all
end end
end end

View File

@ -7,6 +7,8 @@ class ApplicationMailer < ActionMailer::Base
helper :instance helper :instance
helper :formatting helper :formatting
after_action :set_autoreply_headers!
protected protected
def locale_for_account(account) def locale_for_account(account)
@ -14,4 +16,10 @@ class ApplicationMailer < ActionMailer::Base
yield yield
end end
end end
def set_autoreply_headers!
headers['Precedence'] = 'list'
headers['X-Auto-Response-Suppress'] = 'All'
headers['Auto-Submitted'] = 'auto-generated'
end
end end

View File

@ -107,7 +107,7 @@ 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_username, ->(value) { where(arel_table[:username].matches("#{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 :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :without_unapproved, -> { left_outer_joins(:user).remote.or(left_outer_joins(:user).merge(User.approved.confirmed)) } scope :without_unapproved, -> { left_outer_joins(:user).remote.or(left_outer_joins(:user).merge(User.approved.confirmed)) }

View File

@ -16,29 +16,28 @@
class AccountConversation < ApplicationRecord class AccountConversation < ApplicationRecord
include Redisable include Redisable
attr_writer :participant_accounts
before_validation :set_last_status
after_commit :push_to_streaming_api after_commit :push_to_streaming_api
belongs_to :account belongs_to :account
belongs_to :conversation belongs_to :conversation
belongs_to :last_status, class_name: 'Status' belongs_to :last_status, class_name: 'Status'
before_validation :set_last_status
def participant_account_ids=(arr) def participant_account_ids=(arr)
self[:participant_account_ids] = arr.sort self[:participant_account_ids] = arr.sort
@participant_accounts = nil
end end
def participant_accounts def participant_accounts
if participant_account_ids.empty? @participant_accounts ||= Account.where(id: participant_account_ids).to_a
[account] @participant_accounts.presence || [account]
else
participants = Account.where(id: participant_account_ids)
participants.empty? ? [account] : participants
end
end end
class << self class << self
def to_a_paginated_by_id(limit, options = {}) def to_a_paginated_by_id(limit, options = {})
array = begin
if options[:min_id] if options[:min_id]
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
else else
@ -46,6 +45,17 @@ class AccountConversation < ApplicationRecord
end end
end end
# Preload participants
participant_ids = array.flat_map(&:participant_account_ids)
accounts_by_id = Account.where(id: participant_ids).index_by(&:id)
array.each do |conversation|
conversation.participant_accounts = conversation.participant_account_ids.filter_map { |id| accounts_by_id[id] }
end
array
end
def paginate_by_min_id(limit, min_id = nil, max_id = nil) def paginate_by_min_id(limit, min_id = nil, max_id = nil)
query = order(arel_table[:last_status_id].asc).limit(limit) query = order(arel_table[:last_status_id].asc).limit(limit)
query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present? query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present?

View File

@ -17,6 +17,6 @@
class Backup < ApplicationRecord class Backup < ApplicationRecord
belongs_to :user, inverse_of: :backups belongs_to :user, inverse_of: :backups
has_attached_file :dump has_attached_file :dump, s3_permissions: ->(*) { ENV['S3_PERMISSION'] == '' ? nil : 'private' }
do_not_validate_attachment_file_type :dump do_not_validate_attachment_file_type :dump
end end

View File

@ -22,15 +22,14 @@ module Attachmentable
included do included do
def self.has_attached_file(name, options = {}) # rubocop:disable Naming/PredicateName def self.has_attached_file(name, options = {}) # rubocop:disable Naming/PredicateName
options = { validate_media_type: false }.merge(options)
super(name, options) super(name, options)
send(:"before_#{name}_post_process") do
send(:"before_#{name}_validate") do
attachment = send(name) attachment = send(name)
check_image_dimension(attachment) check_image_dimension(attachment)
set_file_content_type(attachment) set_file_content_type(attachment)
obfuscate_file_name(attachment) obfuscate_file_name(attachment)
set_file_extension(attachment) set_file_extension(attachment)
Paperclip::Validators::MediaTypeSpoofDetectionValidator.new(attributes: [name]).validate(self)
end end
end end
end end

View File

@ -6,7 +6,7 @@ module LdapAuthenticable
class_methods do class_methods do
def authenticate_with_ldap(params = {}) def authenticate_with_ldap(params = {})
ldap = Net::LDAP.new(ldap_options) ldap = Net::LDAP.new(ldap_options)
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: params[:email]) filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: Net::LDAP::Filter.escape(params[:email]))
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password])) if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
ldap_get_user(user_info.first) ldap_get_user(user_info.first)

View File

@ -17,8 +17,8 @@ class Form::AccountBatch
unfollow! unfollow!
when 'remove_from_followers' when 'remove_from_followers'
remove_from_followers! remove_from_followers!
when 'block_domains' when 'remove_domains_from_followers'
block_domains! remove_domains_from_followers!
when 'approve' when 'approve'
approve! approve!
when 'reject' when 'reject'
@ -35,9 +35,15 @@ class Form::AccountBatch
private private
def follow! def follow!
error = nil
accounts.each do |target_account| accounts.each do |target_account|
FollowService.new.call(current_account, target_account) FollowService.new.call(current_account, target_account)
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound => e
error ||= e
end end
raise error if error.present?
end end
def unfollow! def unfollow!
@ -50,10 +56,8 @@ class Form::AccountBatch
RemoveFromFollowersService.new.call(current_account, account_ids) RemoveFromFollowersService.new.call(current_account, account_ids)
end end
def block_domains! def remove_domains_from_followers!
AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain| RemoveDomainsFromFollowersService.new.call(current_account, account_domains)
[current_account.id, domain]
end
end end
def account_domains def account_domains
@ -119,7 +123,18 @@ class Form::AccountBatch
account: current_account, account: current_account,
action: :suspend action: :suspend
) )
Admin::SuspensionWorker.perform_async(account.id) Admin::SuspensionWorker.perform_async(account.id)
# Suspending a single account closes their associated reports, so
# mass-suspending would be consistent.
Report.where(target_account: account).unresolved.find_each do |report|
authorize(report, :update?)
log_action(:resolve, report)
report.resolve!(current_account)
rescue Mastodon::NotPermittedError
# This should not happen, but just in case, do not fail early
end
end end
def approve_account(account) def approve_account(account)

View File

@ -12,7 +12,7 @@
# #
class Identity < ApplicationRecord class Identity < ApplicationRecord
belongs_to :user, dependent: :destroy belongs_to :user
validates :uid, presence: true, uniqueness: { scope: :provider } validates :uid, presence: true, uniqueness: { scope: :provider }
validates :provider, presence: true validates :provider, presence: true

View File

@ -504,11 +504,14 @@ class User < ApplicationRecord
def prepare_new_user! def prepare_new_user!
BootstrapTimelineWorker.perform_async(account_id) BootstrapTimelineWorker.perform_async(account_id)
ActivityTracker.increment('activity:accounts:local') ActivityTracker.increment('activity:accounts:local')
ActivityTracker.record('activity:logins', id)
UserMailer.welcome(self).deliver_later UserMailer.welcome(self).deliver_later
TriggerWebhookWorker.perform_async('account.approved', 'Account', account_id) TriggerWebhookWorker.perform_async('account.approved', 'Account', account_id)
end end
def prepare_returning_user! def prepare_returning_user!
return unless confirmed?
ActivityTracker.record('activity:logins', id) ActivityTracker.record('activity:logins', id)
regenerate_feed! if needs_feed_update? regenerate_feed! if needs_feed_update?
end end

View File

@ -20,6 +20,8 @@ class Webhook < ApplicationRecord
report.created report.created
).freeze ).freeze
attr_writer :current_account
scope :enabled, -> { where(enabled: true) } scope :enabled, -> { where(enabled: true) }
validates :url, presence: true, url: true validates :url, presence: true, url: true
@ -27,6 +29,7 @@ class Webhook < ApplicationRecord
validates :events, presence: true validates :events, presence: true
validate :validate_events validate :validate_events
validate :validate_permissions
before_validation :strip_events before_validation :strip_events
before_validation :generate_secret before_validation :generate_secret
@ -43,12 +46,29 @@ class Webhook < ApplicationRecord
update!(enabled: false) update!(enabled: false)
end end
def required_permissions
events.map { |event| Webhook.permission_for_event(event) }
end
def self.permission_for_event(event)
case event
when 'account.approved', 'account.created', 'account.updated'
:manage_users
when 'report.created'
:manage_reports
end
end
private private
def validate_events def validate_events
errors.add(:events, :invalid) if events.any? { |e| !EVENTS.include?(e) } errors.add(:events, :invalid) if events.any? { |e| !EVENTS.include?(e) }
end end
def validate_permissions
errors.add(:events, :invalid_permissions) if defined?(@current_account) && required_permissions.any? { |permission| !@current_account.user_role.can?(permission) }
end
def strip_events def strip_events
self.events = events.map { |str| str.strip.presence }.compact if events.present? self.events = events.map { |str| str.strip.presence }.compact if events.present?
end end

View File

@ -14,7 +14,7 @@ class WebhookPolicy < ApplicationPolicy
end end
def update? def update?
role.can?(:manage_webhooks) role.can?(:manage_webhooks) && record.required_permissions.all? { |permission| role.can?(permission) }
end end
def enable? def enable?
@ -30,6 +30,6 @@ class WebhookPolicy < ApplicationPolicy
end end
def destroy? def destroy?
role.can?(:manage_webhooks) role.can?(:manage_webhooks) && record.required_permissions.all? { |permission| role.can?(permission) }
end end
end end

View File

@ -11,7 +11,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :domain, :title, :version, :source_url, :description, attributes :domain, :title, :version, :source_url, :description,
:usage, :thumbnail, :languages, :configuration, :usage, :thumbnail, :languages, :configuration,
:registrations :registrations, :max_toot_chars
has_one :contact, serializer: ContactSerializer has_one :contact, serializer: ContactSerializer
has_many :rules, serializer: REST::RuleSerializer has_many :rules, serializer: REST::RuleSerializer
@ -88,6 +88,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
} }
end end
def max_toot_chars
1500
end
private private
def registrations_enabled? def registrations_enabled?

View File

@ -11,4 +11,8 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
def image def image
object.image? ? full_asset_url(object.image.url(:original)) : nil object.image? ? full_asset_url(object.image.url(:original)) : nil
end end
def html
Sanitize.fragment(object.html, Sanitize::Config::MASTODON_OEMBED)
end
end end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class FollowMigrationService < FollowService
# Follow an account with the same settings as another account, and unfollow the old account once the request is sent
# @param [Account] source_account From which to follow
# @param [Account] target_account Account to follow
# @param [Account] old_target_account Account to unfollow once the follow request has been sent to the new one
# @option [Boolean] bypass_locked Whether to immediately follow the new account even if it is locked
def call(source_account, target_account, old_target_account, bypass_locked: false)
@old_target_account = old_target_account
follow = source_account.active_relationships.find_by(target_account: old_target_account)
reblogs = follow&.show_reblogs?
notify = follow&.notify?
languages = follow&.languages
super(source_account, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
end
private
def request_follow!
follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))
if @target_account.local?
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
elsif @target_account.activitypub?
ActivityPub::MigratedFollowDeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, @old_target_account.id)
end
follow_request
end
def direct_follow!
follow = super
UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
follow
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class RemoveDomainsFromFollowersService < BaseService
include Payloadable
def call(source_account, target_domains)
source_account.passive_relationships.where(account_id: Account.where(domain: target_domains)).find_each do |follow|
follow.destroy
create_notification(follow) if source_account.local? && !follow.account.local? && follow.account.activitypub?
end
end
private
def create_notification(follow)
ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.target_account_id, follow.account.inbox_url)
end
def build_json(follow)
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
end
end

View File

@ -12,6 +12,7 @@ class RemoveStatusService < BaseService
# @option [Boolean] :immediate # @option [Boolean] :immediate
# @option [Boolean] :preserve # @option [Boolean] :preserve
# @option [Boolean] :original_removed # @option [Boolean] :original_removed
# @option [Boolean] :skip_streaming
def call(status, **options) def call(status, **options)
@payload = Oj.dump(event: :delete, payload: status.id.to_s) @payload = Oj.dump(event: :delete, payload: status.id.to_s)
@status = status @status = status
@ -52,6 +53,9 @@ class RemoveStatusService < BaseService
private private
# The following FeedManager calls all do not result in redis publishes for
# streaming, as the `:update` option is false
def remove_from_self def remove_from_self
FeedManager.instance.unpush_from_home(@account, @status) FeedManager.instance.unpush_from_home(@account, @status)
end end
@ -75,6 +79,8 @@ class RemoveStatusService < BaseService
# followers. Here we send a delete to actively mentioned accounts # followers. Here we send a delete to actively mentioned accounts
# that may not follow the account # that may not follow the account
return if skip_streaming?
@status.active_mentions.find_each do |mention| @status.active_mentions.find_each do |mention|
redis.publish("timeline:#{mention.account_id}", @payload) redis.publish("timeline:#{mention.account_id}", @payload)
end end
@ -103,7 +109,7 @@ class RemoveStatusService < BaseService
# without us being able to do all the fancy stuff # without us being able to do all the fancy stuff
@status.reblogs.rewhere(deleted_at: [nil, @status.deleted_at]).includes(:account).reorder(nil).find_each do |reblog| @status.reblogs.rewhere(deleted_at: [nil, @status.deleted_at]).includes(:account).reorder(nil).find_each do |reblog|
RemoveStatusService.new.call(reblog, original_removed: true) RemoveStatusService.new.call(reblog, original_removed: true, skip_streaming: skip_streaming?)
end end
end end
@ -114,6 +120,8 @@ class RemoveStatusService < BaseService
return unless @status.public_visibility? return unless @status.public_visibility?
return if skip_streaming?
@status.tags.map(&:name).each do |hashtag| @status.tags.map(&:name).each do |hashtag|
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload) redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local? redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local?
@ -123,6 +131,8 @@ class RemoveStatusService < BaseService
def remove_from_public def remove_from_public
return unless @status.public_visibility? return unless @status.public_visibility?
return if skip_streaming?
redis.publish('timeline:public', @payload) redis.publish('timeline:public', @payload)
redis.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', @payload) redis.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', @payload)
end end
@ -130,6 +140,8 @@ class RemoveStatusService < BaseService
def remove_from_media def remove_from_media
return unless @status.public_visibility? return unless @status.public_visibility?
return if skip_streaming?
redis.publish('timeline:public:media', @payload) redis.publish('timeline:public:media', @payload)
redis.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', @payload) redis.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', @payload)
end end
@ -143,4 +155,8 @@ class RemoveStatusService < BaseService
def permanently? def permanently?
@options[:immediate] || !(@options[:preserve] || @status.reported?) @options[:immediate] || !(@options[:preserve] || @status.reported?)
end end
def skip_streaming?
!!@options[:skip_streaming]
end
end end

View File

@ -89,13 +89,28 @@ class ResolveURLService < BaseService
def process_local_url def process_local_url
recognized_params = Rails.application.routes.recognize_path(@url) recognized_params = Rails.application.routes.recognize_path(@url)
case recognized_params[:controller]
when 'statuses'
return unless recognized_params[:action] == 'show' return unless recognized_params[:action] == 'show'
if recognized_params[:controller] == 'statuses'
status = Status.find_by(id: recognized_params[:id]) status = Status.find_by(id: recognized_params[:id])
check_local_status(status) check_local_status(status)
elsif recognized_params[:controller] == 'accounts' when 'accounts'
return unless recognized_params[:action] == 'show'
Account.find_local(recognized_params[:username]) Account.find_local(recognized_params[:username])
when 'home'
return unless recognized_params[:action] == 'index' && recognized_params[:username_with_domain].present?
if recognized_params[:any]&.match?(/\A[0-9]+\Z/)
status = Status.find_by(id: recognized_params[:any])
check_local_status(status)
elsif recognized_params[:any].blank?
username, domain = recognized_params[:username_with_domain].gsub(/\A@/, '').split('@')
return unless username.present? && domain.present?
Account.find_remote(username, domain)
end
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class StatusLengthValidator < ActiveModel::Validator class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 500 MAX_CHARS = 1500
URL_PLACEHOLDER_CHARS = 23 URL_PLACEHOLDER_CHARS = 23
URL_PLACEHOLDER = 'x' * 23 URL_PLACEHOLDER = 'x' * 23

View File

@ -3,8 +3,8 @@
class VoteValidator < ActiveModel::Validator class VoteValidator < ActiveModel::Validator
def validate(vote) def validate(vote)
vote.errors.add(:base, I18n.t('polls.errors.expired')) if vote.poll.expired? vote.errors.add(:base, I18n.t('polls.errors.expired')) if vote.poll.expired?
vote.errors.add(:base, I18n.t('polls.errors.invalid_choice')) if invalid_choice?(vote) vote.errors.add(:base, I18n.t('polls.errors.invalid_choice')) if invalid_choice?(vote)
vote.errors.add(:base, I18n.t('polls.errors.self_vote')) if self_vote?(vote)
if vote.poll.multiple? && vote.poll.votes.where(account: vote.account, choice: vote.choice).exists? if vote.poll.multiple? && vote.poll.votes.where(account: vote.account, choice: vote.choice).exists?
vote.errors.add(:base, I18n.t('polls.errors.already_voted')) vote.errors.add(:base, I18n.t('polls.errors.already_voted'))
@ -18,4 +18,8 @@ class VoteValidator < ActiveModel::Validator
def invalid_choice?(vote) def invalid_choice?(vote)
vote.choice.negative? || vote.choice >= vote.poll.options.size vote.choice.negative? || vote.choice >= vote.poll.options.size
end end
def self_vote?(vote)
vote.account_id == vote.poll.account_id
end
end end

View File

@ -12,7 +12,7 @@
- unless @system_checks.empty? - unless @system_checks.empty?
.flash-message-stack .flash-message-stack
- @system_checks.each do |message| - @system_checks.each do |message|
.flash-message.warning .flash-message{ class: message.critical ? 'alert' : 'warning' }
= t("admin.system_checks.#{message.key}.message_html", value: message.value ? content_tag(:strong, message.value) : nil) = t("admin.system_checks.#{message.key}.message_html", value: message.value ? content_tag(:strong, message.value) : nil)
- if message.action - if message.action
= link_to t("admin.system_checks.#{message.key}.action"), message.action = link_to t("admin.system_checks.#{message.key}.action"), message.action

View File

@ -54,14 +54,14 @@
.strike-card__statuses-list__item .strike-card__statuses-list__item
- if (status = status_map[status_id.to_i]) - if (status = status_map[status_id.to_i])
.one-liner .one-liner
= link_to short_account_status_url(@report.target_account, status_id), class: 'emojify' do .emojify= one_line_preview(status)
= one_line_preview(status)
- status.ordered_media_attachments.each do |media_attachment| - status.ordered_media_attachments.each do |media_attachment|
%abbr{ title: media_attachment.description } %abbr{ title: media_attachment.description }
= fa_icon 'link' = fa_icon 'link'
= media_attachment.file_file_name = media_attachment.file_file_name
.strike-card__statuses-list__item__meta .strike-card__statuses-list__item__meta
= link_to ActivityPub::TagManager.instance.url_for(status), target: '_blank' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
- unless status.application.nil? - unless status.application.nil?
· ·

View File

@ -34,7 +34,7 @@
%td %td
- if @status.trend.allowed? - if @status.trend.allowed?
%abbr{ title: t('admin.trends.tags.current_score', score: @status.trend.score) }= t('admin.trends.tags.trending_rank', rank: @status.trend.rank) %abbr{ title: t('admin.trends.tags.current_score', score: @status.trend.score) }= t('admin.trends.tags.trending_rank', rank: @status.trend.rank)
- elsif @status.trend.requires_review? - elsif @status.requires_review?
= t('admin.trends.pending_review') = t('admin.trends.pending_review')
- else - else
= t('admin.trends.not_allowed_to_trend') = t('admin.trends.not_allowed_to_trend')

View File

@ -5,7 +5,7 @@
= f.input :url, wrapper: :with_block_label, input_html: { placeholder: 'https://' } = f.input :url, wrapper: :with_block_label, input_html: { placeholder: 'https://' }
.fields-group .fields-group
= f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' = f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', disabled: Webhook::EVENTS.filter { |event| !current_user.role.can?(Webhook.permission_for_event(event)) }
.actions .actions
= f.button :button, @webhook.new_record? ? t('admin.webhooks.add_new') : t('generic.save_changes'), type: :submit = f.button :button, @webhook.new_record? ? t('admin.webhooks.add_new') : t('generic.save_changes'), type: :submit

View File

@ -3,7 +3,7 @@
= image_tag @instance_presenter.thumbnail&.file&.url(:'@1x') || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title = image_tag @instance_presenter.thumbnail&.file&.url(:'@1x') || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title
.hero-widget__text .hero-widget__text
%p= @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html') %p= @instance_presenter.description.presence || t('about.about_mastodon_html')
- if Setting.trends && !(user_signed_in? && !current_user.setting_trends) - if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
- trends = Trends.tags.query.allowed.limit(3) - trends = Trends.tags.query.allowed.limit(3)

View File

@ -50,14 +50,14 @@
.strike-card__statuses-list__item .strike-card__statuses-list__item
- if (status = status_map[status_id.to_i]) - if (status = status_map[status_id.to_i])
.one-liner .one-liner
= link_to short_account_status_url(@strike.target_account, status_id), class: 'emojify' do .emojify= one_line_preview(status)
= one_line_preview(status)
- status.ordered_media_attachments.each do |media_attachment| - status.ordered_media_attachments.each do |media_attachment|
%abbr{ title: media_attachment.description } %abbr{ title: media_attachment.description }
= fa_icon 'link' = fa_icon 'link'
= media_attachment.file_file_name = media_attachment.file_file_name
.strike-card__statuses-list__item__meta .strike-card__statuses-list__item__meta
= link_to ActivityPub::TagManager.instance.url_for(status), target: '_blank' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
- unless status.application.nil? - unless status.application.nil?
· ·

View File

@ -18,8 +18,8 @@
.announcements-list__item__action-bar .announcements-list__item__action-bar
.announcements-list__item__meta .announcements-list__item__meta
- if application.most_recently_used_access_token - if @last_used_at_by_app[application.id]
= t('doorkeeper.authorized_applications.index.last_used_at', date: l(application.most_recently_used_access_token.last_used_at.to_date)) = t('doorkeeper.authorized_applications.index.last_used_at', date: l(@last_used_at_by_app[application.id].to_date))
- else - else
= t('doorkeeper.authorized_applications.index.never_used') = t('doorkeeper.authorized_applications.index.never_used')

View File

@ -48,7 +48,7 @@
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('relationships.confirm_remove_selected_followers') } unless following_relationship? = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('relationships.confirm_remove_selected_followers') } unless following_relationship?
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :remove_domains_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
.batch-table__body .batch-table__body
- if @accounts.empty? - if @accounts.empty?
= nothing_here 'nothing-here--under-tabs' = nothing_here 'nothing-here--under-tabs'

View File

@ -64,6 +64,6 @@
%td= l backup.created_at %td= l backup.created_at
- if backup.processed? - if backup.processed?
%td= number_to_human_size backup.dump_file_size %td= number_to_human_size backup.dump_file_size
%td= table_link_to 'download', t('exports.archive_takeout.download'), backup.dump.url %td= table_link_to 'download', t('exports.archive_takeout.download'), download_backup_url(backup)
- else - else
%td{ colspan: 2 }= t('exports.archive_takeout.in_progress') %td{ colspan: 2 }= t('exports.archive_takeout.in_progress')

View File

@ -1,5 +1,5 @@
- thumbnail = @instance_presenter.thumbnail - thumbnail = @instance_presenter.thumbnail
- description ||= strip_tags(@instance_presenter.description.presence || t('about.about_mastodon_html')) - description ||= @instance_presenter.description.presence || strip_tags(t('about.about_mastodon_html'))
%meta{ name: 'description', content: description }/ %meta{ name: 'description', content: description }/

View File

@ -55,5 +55,5 @@
%tbody %tbody
%tr %tr
%td.button-primary %td.button-primary
= link_to full_asset_url(@backup.dump.url) do = link_to download_backup_url(@backup) do
%span= t 'exports.archive_takeout.download' %span= t 'exports.archive_takeout.download'

View File

@ -4,4 +4,4 @@
<%= t 'user_mailer.backup_ready.explanation' %> <%= t 'user_mailer.backup_ready.explanation' %>
=> <%= full_asset_url(@backup.dump.url) %> => <%= download_backup_url(@backup) %>

View File

@ -10,6 +10,16 @@ class ActivityPub::DeliveryWorker
sidekiq_options queue: 'push', retry: 16, dead: false sidekiq_options queue: 'push', retry: 16, dead: false
# Unfortunately, we cannot control Sidekiq's jitter, so add our own
sidekiq_retry_in do |count|
# This is Sidekiq's default delay
delay = (count**4) + 15
# Our custom jitter, that will be added to Sidekiq's built-in one.
# Sidekiq's built-in jitter is `rand(10) * (count + 1)`
jitter = rand(0.5 * (count**4))
delay + jitter
end
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
def perform(json, source_account_id, inbox_url, options = {}) def perform(json, source_account_id, inbox_url, options = {})

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker
def perform(json, source_account_id, inbox_url, old_target_account_id, options = {})
super(json, source_account_id, inbox_url, options)
unfollow_old_account!(old_target_account_id)
end
private
def unfollow_old_account!(old_target_account_id)
old_target_account = Account.find(old_target_account_id)
UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true)
rescue StandardError
true
end
end

View File

@ -7,52 +7,68 @@ class Scheduler::AccountsStatusesCleanupScheduler
# This limit is mostly to be nice to the fediverse at large and not # This limit is mostly to be nice to the fediverse at large and not
# generate too much traffic. # generate too much traffic.
# This also helps limiting the running time of the scheduler itself. # This also helps limiting the running time of the scheduler itself.
MAX_BUDGET = 50 MAX_BUDGET = 300
# This is an attempt to spread the load across instances, as various # This is an attempt to spread the load across remote servers, as
# accounts are likely to have various followers. # spreading deletions across diverse accounts is likely to spread
# the deletion across diverse followers. It also helps each individual
# user see some effect sooner.
PER_ACCOUNT_BUDGET = 5 PER_ACCOUNT_BUDGET = 5
# This is an attempt to limit the workload generated by status removal # This is an attempt to limit the workload generated by status removal
# jobs to something the particular instance can handle. # jobs to something the particular server can handle.
PER_THREAD_BUDGET = 5 PER_THREAD_BUDGET = 5
# Those avoid loading an instance that is already under load # These are latency limits on various queues above which a server is
MAX_DEFAULT_SIZE = 2 # considered to be under load, causing the auto-deletion to be entirely
MAX_DEFAULT_LATENCY = 5 # skipped for that run.
MAX_PUSH_SIZE = 5 LOAD_LATENCY_THRESHOLDS = {
MAX_PUSH_LATENCY = 10 default: 5,
# 'pull' queue has lower priority jobs, and it's unlikely that pushing push: 10,
# deletes would cause much issues with this queue if it didn't cause issues # The `pull` queue has lower priority jobs, and it's unlikely that
# with default and push. Yet, do not enqueue deletes if the instance is # pushing deletes would cause much issues with this queue if it didn't
# lagging behind too much. # cause issues with `default` and `push`. Yet, do not enqueue deletes
MAX_PULL_SIZE = 500 # if the instance is lagging behind too much.
MAX_PULL_LATENCY = 300 pull: 5.minutes.to_i,
}.freeze
# This is less of an issue in general, but deleting old statuses is likely sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.day.to_i
# to cause delivery errors, and thus increase the number of jobs to be retried.
# This doesn't directly translate to load, but connection errors and a high
# number of dead instances may lead to this spiraling out of control if
# unchecked.
MAX_RETRY_SIZE = 50_000
sidekiq_options retry: 0, lock: :until_executed
def perform def perform
return if under_load? return if under_load?
budget = compute_budget budget = compute_budget
first_policy_id = last_processed_id
# If the budget allows it, we want to consider all accounts with enabled
# auto cleanup at least once.
#
# We start from `first_policy_id` (the last processed id in the previous
# run) and process each policy until we loop to `first_policy_id`,
# recording into `affected_policies` any policy that caused posts to be
# deleted.
#
# After that, we set `full_iteration` to `false` and continue looping on
# policies from `affected_policies`.
first_policy_id = last_processed_id || 0
first_iteration = true
full_iteration = true
affected_policies = []
loop do loop do
num_processed_accounts = 0 num_processed_accounts = 0
scope = AccountStatusesCleanupPolicy.where(enabled: true) scope = cleanup_policies(first_policy_id, affected_policies, first_iteration, full_iteration)
scope.where(Account.arel_table[:id].gt(first_policy_id)) if first_policy_id.present?
scope.find_each(order: :asc) do |policy| scope.find_each(order: :asc) do |policy|
num_deleted = AccountStatusesCleanupService.new.call(policy, [budget, PER_ACCOUNT_BUDGET].min) num_deleted = AccountStatusesCleanupService.new.call(policy, [budget, PER_ACCOUNT_BUDGET].min)
num_processed_accounts += 1 unless num_deleted.zero?
budget -= num_deleted budget -= num_deleted
unless num_deleted.zero?
num_processed_accounts += 1
affected_policies << policy.id if full_iteration
end
full_iteration = false if !first_iteration && policy.id >= first_policy_id
if budget.zero? if budget.zero?
save_last_processed_id(policy.id) save_last_processed_id(policy.id)
break break
@ -61,37 +77,55 @@ class Scheduler::AccountsStatusesCleanupScheduler
# The idea here is to loop through all policies at least once until the budget is exhausted # The idea here is to loop through all policies at least once until the budget is exhausted
# and start back after the last processed account otherwise # and start back after the last processed account otherwise
break if budget.zero? || (num_processed_accounts.zero? && first_policy_id.nil?) break if budget.zero? || (num_processed_accounts.zero? && !full_iteration)
first_policy_id = nil
full_iteration = false unless first_iteration
first_iteration = false
end end
end end
def compute_budget def compute_budget
threads = Sidekiq::ProcessSet.new.select { |x| x['queues'].include?('push') }.map { |x| x['concurrency'] }.sum # Each post deletion is a `RemovalWorker` job (on `default` queue), each
# potentially spawning many `ActivityPub::DeliveryWorker` jobs (on the `push` queue).
threads = Sidekiq::ProcessSet.new.select { |x| x['queues'].include?('push') }.pluck('concurrency').sum
[PER_THREAD_BUDGET * threads, MAX_BUDGET].min [PER_THREAD_BUDGET * threads, MAX_BUDGET].min
end end
def under_load? def under_load?
return true if Sidekiq::Stats.new.retry_size > MAX_RETRY_SIZE LOAD_LATENCY_THRESHOLDS.any? { |queue, max_latency| queue_under_load?(queue, max_latency) }
queue_under_load?('default', MAX_DEFAULT_SIZE, MAX_DEFAULT_LATENCY) || queue_under_load?('push', MAX_PUSH_SIZE, MAX_PUSH_LATENCY) || queue_under_load?('pull', MAX_PULL_SIZE, MAX_PULL_LATENCY)
end end
private private
def queue_under_load?(name, max_size, max_latency) def cleanup_policies(first_policy_id, affected_policies, first_iteration, full_iteration)
queue = Sidekiq::Queue.new(name) scope = AccountStatusesCleanupPolicy.where(enabled: true)
queue.size > max_size || queue.latency > max_latency
if full_iteration
# If we are doing a full iteration, examine all policies we have not examined yet
if first_iteration
scope.where(id: first_policy_id...)
else
scope.where(id: ..first_policy_id).or(scope.where(id: affected_policies))
end
else
# Otherwise, examine only policies that previously yielded posts to delete
scope.where(id: affected_policies)
end
end
def queue_under_load?(name, max_latency)
Sidekiq::Queue.new(name).latency > max_latency
end end
def last_processed_id def last_processed_id
redis.get('account_statuses_cleanup_scheduler:last_account_id') redis.get('account_statuses_cleanup_scheduler:last_policy_id')&.to_i
end end
def save_last_processed_id(id) def save_last_processed_id(id)
if id.nil? if id.nil?
redis.del('account_statuses_cleanup_scheduler:last_account_id') redis.del('account_statuses_cleanup_scheduler:last_policy_id')
else else
redis.set('account_statuses_cleanup_scheduler:last_account_id', id, ex: 1.hour.seconds) redis.set('account_statuses_cleanup_scheduler:last_policy_id', id, ex: 1.hour.seconds)
end end
end end
end end

View File

@ -6,17 +6,19 @@ class Scheduler::IndexingScheduler
sidekiq_options retry: 0 sidekiq_options retry: 0
IMPORT_BATCH_SIZE = 1000
SCAN_BATCH_SIZE = 10 * IMPORT_BATCH_SIZE
def perform def perform
return unless Chewy.enabled? return unless Chewy.enabled?
indexes.each do |type| indexes.each do |type|
with_redis do |redis| with_redis do |redis|
ids = redis.smembers("chewy:queue:#{type.name}") redis.sscan_each("chewy:queue:#{type.name}", count: SCAN_BATCH_SIZE).each_slice(IMPORT_BATCH_SIZE) do |ids|
type.import!(ids) type.import!(ids)
redis.pipelined do |pipeline| redis.pipelined do |pipeline|
ids.each { |id| pipeline.srem("chewy:queue:#{type.name}", id) } pipeline.srem("chewy:queue:#{type.name}", ids)
end
end end
end end
end end

View File

@ -24,7 +24,7 @@ class Scheduler::UserCleanupScheduler
def clean_discarded_statuses! def clean_discarded_statuses!
Status.unscoped.discarded.where('deleted_at <= ?', 30.days.ago).find_in_batches do |statuses| Status.unscoped.discarded.where('deleted_at <= ?', 30.days.ago).find_in_batches do |statuses|
RemovalWorker.push_bulk(statuses) do |status| RemovalWorker.push_bulk(statuses) do |status|
[status.id, { 'immediate' => true }] [status.id, { 'immediate' => true, 'skip_streaming' => true }]
end end
end end
end end

View File

@ -10,13 +10,7 @@ class UnfollowFollowWorker
old_target_account = Account.find(old_target_account_id) old_target_account = Account.find(old_target_account_id)
new_target_account = Account.find(new_target_account_id) new_target_account = Account.find(new_target_account_id)
follow = follower_account.active_relationships.find_by(target_account: old_target_account) FollowMigrationService.new.call(follower_account, new_target_account, old_target_account, bypass_locked: bypass_locked)
reblogs = follow&.show_reblogs?
notify = follow&.notify?
languages = follow&.languages
FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true)
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
true true
end end

View File

@ -5,7 +5,9 @@ require_relative '../config/boot'
require_relative '../lib/cli' require_relative '../lib/cli'
begin begin
Chewy.strategy(:mastodon) do
Mastodon::CLI.start(ARGV) Mastodon::CLI.start(ARGV)
end
rescue Interrupt rescue Interrupt
exit(130) exit(130)
end end

View File

@ -28,6 +28,7 @@ require_relative '../lib/paperclip/url_generator_extensions'
require_relative '../lib/paperclip/attachment_extensions' require_relative '../lib/paperclip/attachment_extensions'
require_relative '../lib/paperclip/lazy_thumbnail' require_relative '../lib/paperclip/lazy_thumbnail'
require_relative '../lib/paperclip/gif_transcoder' require_relative '../lib/paperclip/gif_transcoder'
require_relative '../lib/paperclip/media_type_spoof_detector_extensions'
require_relative '../lib/paperclip/transcoder' require_relative '../lib/paperclip/transcoder'
require_relative '../lib/paperclip/type_corrector' require_relative '../lib/paperclip/type_corrector'
require_relative '../lib/paperclip/response_with_limit_adapter' require_relative '../lib/paperclip/response_with_limit_adapter'
@ -35,9 +36,11 @@ require_relative '../lib/terrapin/multi_pipe_extensions'
require_relative '../lib/mastodon/snowflake' require_relative '../lib/mastodon/snowflake'
require_relative '../lib/mastodon/version' require_relative '../lib/mastodon/version'
require_relative '../lib/mastodon/rack_middleware' require_relative '../lib/mastodon/rack_middleware'
require_relative '../lib/public_file_server_middleware'
require_relative '../lib/devise/two_factor_ldap_authenticatable' require_relative '../lib/devise/two_factor_ldap_authenticatable'
require_relative '../lib/devise/two_factor_pam_authenticatable' require_relative '../lib/devise/two_factor_pam_authenticatable'
require_relative '../lib/chewy/strategy/mastodon' require_relative '../lib/chewy/strategy/mastodon'
require_relative '../lib/chewy/strategy/bypass_with_warning'
require_relative '../lib/webpacker/manifest_extensions' require_relative '../lib/webpacker/manifest_extensions'
require_relative '../lib/webpacker/helper_extensions' require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/rails/engine_extensions' require_relative '../lib/rails/engine_extensions'
@ -181,6 +184,10 @@ module Mastodon
config.active_job.queue_adapter = :sidekiq config.active_job.queue_adapter = :sidekiq
config.action_mailer.deliver_later_queue_name = 'mailers' config.action_mailer.deliver_later_queue_name = 'mailers'
# We use our own middleware for this
config.public_file_server.enabled = false
config.middleware.use PublicFileServerMiddleware if Rails.env.development? || ENV['RAILS_SERVE_STATIC_FILES'] == 'true'
config.middleware.use Rack::Attack config.middleware.use Rack::Attack
config.middleware.use Mastodon::RackMiddleware config.middleware.use Mastodon::RackMiddleware

View File

@ -5,6 +5,7 @@ default: &default
connect_timeout: 15 connect_timeout: 15
encoding: unicode encoding: unicode
sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %> sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %>
application_name: ''
development: development:
<<: *default <<: *default

View File

@ -16,12 +16,7 @@ Rails.application.configure do
# Run rails dev:cache to toggle caching. # Run rails dev:cache to toggle caching.
if Rails.root.join('tmp/caching-dev.txt').exist? if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true config.action_controller.perform_caching = true
config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS config.cache_store = :redis_cache_store, REDIS_CACHE_PARAMS
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.to_i}",
}
else else
config.action_controller.perform_caching = false config.action_controller.perform_caching = false

View File

@ -19,27 +19,16 @@ Rails.application.configure do
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true # config.require_master_key = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
ActiveSupport::Logger.new(STDOUT).tap do |logger| ActiveSupport::Logger.new(STDOUT).tap do |logger|
logger.formatter = config.log_formatter logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger) config.logger = ActiveSupport::TaggedLogging.new(logger)
end end
# Compress JavaScripts and CSS.
# config.assets.js_compressor = Uglifier.new(mangle: false)
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed. # Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false config.assets.compile = false
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
# Specifies the header that your server uses for sending files. # Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache config.action_dispatch.x_sendfile_header = ENV['SENDFILE_HEADER'] if ENV['SENDFILE_HEADER'].present?
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Allow to specify public IP of reverse proxy if it's needed # Allow to specify public IP of reverse proxy if it's needed
config.action_dispatch.trusted_proxies = ENV['TRUSTED_PROXY_IP'].split(/(?:\s*,\s*|\s+)/).map { |item| IPAddr.new(item) } if ENV['TRUSTED_PROXY_IP'].present? config.action_dispatch.trusted_proxies = ENV['TRUSTED_PROXY_IP'].split(/(?:\s*,\s*|\s+)/).map { |item| IPAddr.new(item) } if ENV['TRUSTED_PROXY_IP'].present?
@ -67,7 +56,7 @@ Rails.application.configure do
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# English when a translation cannot be found). # English when a translation cannot be found).
config.i18n.fallbacks = [:en] config.i18n.fallbacks = true
# Send deprecation notices to registered listeners. # Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify config.active_support.deprecation = :notify
@ -128,6 +117,7 @@ Rails.application.configure do
enable_starttls_auto: enable_starttls_auto, enable_starttls_auto: enable_starttls_auto,
tls: ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true', tls: ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true',
ssl: ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true', ssl: ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true',
read_timeout: 20,
} }
config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym

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