Compare commits
75 commits
paravielfa
...
stable-4.0
Author | SHA1 | Date | |
---|---|---|---|
|
60b70755be | ||
|
0716346194 | ||
|
93a87b96c7 | ||
|
614aaeff41 | ||
|
237f2adfa6 | ||
|
8d7f6550f9 | ||
|
2d42175ef0 | ||
|
3af396e561 | ||
|
2119aadf0a | ||
|
102ed6e8ca | ||
|
f626e0d228 | ||
|
35830cd8cc | ||
|
94c67e8bfd | ||
|
798d26dd04 | ||
|
9ad33eb160 | ||
|
5e55ca25d6 | ||
|
0bcb4f73f1 | ||
|
04f76675d1 | ||
|
53acab6d2b | ||
|
78358b84b9 | ||
|
c285f9d1a1 | ||
|
42bffbc337 | ||
|
f94aee0ed5 | ||
|
41a0a3c87f | ||
|
995ad9602b | ||
|
660845f781 | ||
|
0b627dcf9e | ||
|
a3f58ceea4 | ||
|
bb87736bf0 | ||
|
37972fe3c7 | ||
|
64416e4000 | ||
|
eceb960744 | ||
|
ebe009ff09 | ||
|
2617c33fc3 | ||
|
d81b891fa8 | ||
|
a705bb84e6 | ||
|
214c367095 | ||
|
05c45e9eeb | ||
|
448986438e | ||
|
274bb193b2 | ||
|
46b91cd817 | ||
|
acc277a152 | ||
|
971e8b8f5f | ||
|
aa37eeadf3 | ||
|
f75fba0531 | ||
|
2125dbf610 | ||
|
9715a211c7 | ||
|
a6217bd035 | ||
|
3e9978071b | ||
|
8236c3affc | ||
|
43a16e43ba | ||
|
520377a609 | ||
|
0941230e22 | ||
|
98c59c1d58 | ||
|
2c3cb903ad | ||
|
86924c344d | ||
|
f834fdaf6a | ||
|
1da72b41c7 | ||
|
97e19e8802 | ||
|
bd43f7d4cc | ||
|
c44ddbdb3e | ||
|
4ea4c3f49c | ||
|
419bd9281d | ||
|
d6f1bd2e08 | ||
|
c2d38ef0f1 | ||
|
ad77e8a2fb | ||
|
0f2e8476e0 | ||
|
290d02e936 | ||
|
11f04e3b97 | ||
|
76c96cdd72 | ||
|
c22c4247d9 | ||
|
348599a543 | ||
|
0e3f06da99 | ||
|
cc80f4ed9b | ||
|
e2103c9175 |
103 changed files with 1106 additions and 255 deletions
|
@ -68,7 +68,9 @@ jobs:
|
||||||
cache-version: v1
|
cache-version: v1
|
||||||
pkg-manager: yarn
|
pkg-manager: yarn
|
||||||
- run:
|
- run:
|
||||||
command: ./bin/rails assets:precompile
|
command: |
|
||||||
|
export NODE_OPTIONS=--openssl-legacy-provider
|
||||||
|
./bin/rails assets:precompile
|
||||||
name: Precompile assets
|
name: Precompile assets
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
paths:
|
paths:
|
||||||
|
|
32
.github/workflows/build-image.yml
vendored
32
.github/workflows/build-image.yml
vendored
|
@ -12,6 +12,7 @@ on:
|
||||||
- Dockerfile
|
- Dockerfile
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-image:
|
build-image:
|
||||||
|
@ -20,15 +21,28 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- 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: |
|
||||||
|
@ -36,11 +50,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@v3
|
|
||||||
|
- uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
provenance: false
|
||||||
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
cache-from: type=registry,ref=tootsuite/mastodon:edge
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-to: type=inline
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
3.0.4
|
3.0.6
|
||||||
|
|
94
CHANGELOG.md
94
CHANGELOG.md
|
@ -3,6 +3,100 @@ 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.0.6] - 2023-07-07
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix branding:generate_app_icons failing because of disallowed ICO coder ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25794))
|
||||||
|
- Fix crash in admin interface when viewing a remote user with verified links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25796))
|
||||||
|
- Fix processing of media files with unusual names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25788))
|
||||||
|
|
||||||
|
## [4.0.5] - 2023-07-06
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058))
|
||||||
|
- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852))
|
||||||
|
- 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 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 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 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.0.4] - 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/24333))
|
||||||
|
- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379))
|
||||||
|
|
||||||
|
# [4.0.3] - 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))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix “Remove all followers from the selected domains” being more destructive than it claims ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805))
|
||||||
|
- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526))
|
||||||
|
- 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 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 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 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))
|
||||||
|
|
||||||
|
### 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.0.2] - 2022-11-15
|
## [4.0.2] - 2022-11-15
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ RUN ARCH= && \
|
||||||
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
||||||
|
|
||||||
# Install Ruby 3.0
|
# Install Ruby 3.0
|
||||||
ENV RUBY_VER="3.0.4"
|
ENV RUBY_VER="3.0.6"
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends build-essential \
|
apt-get install -y --no-install-recommends build-essential \
|
||||||
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
||||||
|
|
136
Gemfile.lock
136
Gemfile.lock
|
@ -10,40 +10,40 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.7)
|
actioncable (6.1.7.4)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
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)
|
actionmailbox (6.1.7.4)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 6.1.7)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.7)
|
actionmailer (6.1.7.4)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.4)
|
||||||
actionview (= 6.1.7)
|
actionview (= 6.1.7.4)
|
||||||
activejob (= 6.1.7)
|
activejob (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
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)
|
actionpack (6.1.7.4)
|
||||||
actionview (= 6.1.7)
|
actionview (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
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)
|
actiontext (6.1.7.4)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.4)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.7)
|
actionview (6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
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)
|
activejob (6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.7)
|
activemodel (6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.4)
|
||||||
activerecord (6.1.7)
|
activerecord (6.1.7.4)
|
||||||
activemodel (= 6.1.7)
|
activemodel (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.4)
|
||||||
activestorage (6.1.7)
|
activestorage (6.1.7.4)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.4)
|
||||||
activejob (= 6.1.7)
|
activejob (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7)
|
activerecord (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
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)
|
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)
|
||||||
|
@ -206,7 +206,7 @@ GEM
|
||||||
docile (1.3.4)
|
docile (1.3.4)
|
||||||
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.0)
|
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)
|
||||||
|
@ -282,7 +282,7 @@ GEM
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
omniauth (>= 1.9, < 3)
|
omniauth (>= 1.9, < 3)
|
||||||
openid_connect (~> 1.2)
|
openid_connect (~> 1.2)
|
||||||
globalid (1.0.0)
|
globalid (1.0.1)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 5.0)
|
||||||
hamlit (2.13.0)
|
hamlit (2.13.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
|
@ -382,7 +382,7 @@ GEM
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.19.0)
|
loofah (2.19.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -402,7 +402,7 @@ 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.0)
|
mini_portile2 (2.8.2)
|
||||||
minitest (5.16.3)
|
minitest (5.16.3)
|
||||||
msgpack (1.5.4)
|
msgpack (1.5.4)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
|
@ -411,9 +411,9 @@ GEM
|
||||||
net-scp (4.0.0.rc1)
|
net-scp (4.0.0.rc1)
|
||||||
net-ssh (>= 2.6.5, < 8.0.0)
|
net-ssh (>= 2.6.5, < 8.0.0)
|
||||||
net-ssh (7.0.1)
|
net-ssh (7.0.1)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.9)
|
||||||
nokogiri (1.13.9)
|
nokogiri (1.15.3)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.2.8)
|
nsa (0.2.8)
|
||||||
activesupport (>= 4.2, < 7)
|
activesupport (>= 4.2, < 7)
|
||||||
|
@ -482,8 +482,8 @@ GEM
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.7.1)
|
||||||
rack (2.2.4)
|
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)
|
||||||
|
@ -498,20 +498,20 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-test (2.0.2)
|
rack-test (2.0.2)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (6.1.7)
|
rails (6.1.7.4)
|
||||||
actioncable (= 6.1.7)
|
actioncable (= 6.1.7.4)
|
||||||
actionmailbox (= 6.1.7)
|
actionmailbox (= 6.1.7.4)
|
||||||
actionmailer (= 6.1.7)
|
actionmailer (= 6.1.7.4)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.4)
|
||||||
actiontext (= 6.1.7)
|
actiontext (= 6.1.7.4)
|
||||||
actionview (= 6.1.7)
|
actionview (= 6.1.7.4)
|
||||||
activejob (= 6.1.7)
|
activejob (= 6.1.7.4)
|
||||||
activemodel (= 6.1.7)
|
activemodel (= 6.1.7.4)
|
||||||
activerecord (= 6.1.7)
|
activerecord (= 6.1.7.4)
|
||||||
activestorage (= 6.1.7)
|
activestorage (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.4)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.7)
|
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)
|
||||||
|
@ -520,16 +520,16 @@ GEM
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.4.3)
|
rails-html-sanitizer (1.4.4)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.19, >= 2.19.1)
|
||||||
rails-i18n (6.0.0)
|
rails-i18n (6.0.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
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)
|
railties (6.1.7.4)
|
||||||
actionpack (= 6.1.7)
|
actionpack (= 6.1.7.4)
|
||||||
activesupport (= 6.1.7)
|
activesupport (= 6.1.7.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
|
@ -602,7 +602,7 @@ GEM
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
sanitize (6.0.0)
|
sanitize (6.0.2)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.6.0)
|
scenic (1.6.0)
|
||||||
|
@ -661,7 +661,7 @@ 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)
|
||||||
tpm-key_attestation (0.11.0)
|
tpm-key_attestation (0.11.0)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
|
@ -680,7 +680,7 @@ GEM
|
||||||
twitter-text (3.1.0)
|
twitter-text (3.1.0)
|
||||||
idn-ruby
|
idn-ruby
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (2.0.5)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2022.4)
|
tzinfo-data (1.2022.4)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
|
@ -725,7 +725,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.0)
|
zeitwerk (2.6.8)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
|
with_lock("reblog:#{current_account.id}:#{@reblog.id}") do
|
||||||
|
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
|
||||||
|
end
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
31
app/controllers/backups_controller.rb
Normal file
31
app/controllers/backups_controller.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -49,6 +49,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)
|
||||||
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
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)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -216,7 +216,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -386,7 +386,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;
|
||||||
|
|
|
@ -4407,6 +4407,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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -24,7 +24,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
|
||||||
|
|
105
app/lib/admin/system_check/media_privacy_check.rb
Normal file
105
app/lib/admin/system_check/media_privacy_check.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
<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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)) }
|
||||||
|
|
|
@ -16,34 +16,44 @@
|
||||||
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 = {})
|
||||||
if options[:min_id]
|
array = begin
|
||||||
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
|
if options[:min_id]
|
||||||
else
|
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
|
||||||
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
|
else
|
||||||
|
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
|
||||||
|
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
|
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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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", prepend: true) 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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -480,10 +480,13 @@ 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
|
||||||
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
|
||||||
|
|
|
@ -19,6 +19,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
|
||||||
|
@ -26,6 +28,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
|
||||||
|
@ -42,12 +45,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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
40
app/services/follow_migration_service.rb
Normal file
40
app/services/follow_migration_service.rb
Normal 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
|
23
app/services/remove_domains_from_followers_service.rb
Normal file
23
app/services/remove_domains_from_followers_service.rb
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
return unless recognized_params[:action] == 'show'
|
case recognized_params[:controller]
|
||||||
|
when 'statuses'
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -50,15 +50,15 @@
|
||||||
.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
|
||||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
= 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)
|
||||||
- unless status.application.nil?
|
- unless status.application.nil?
|
||||||
·
|
·
|
||||||
= status.application.name
|
= status.application.name
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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('admin.reports.are_you_sure') } 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('admin.reports.are_you_sure') } unless following_relationship?
|
||||||
|
|
||||||
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
|
= 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'
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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 }/
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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) %>
|
||||||
|
|
17
app/workers/activitypub/migrated_follow_delivery_worker.rb
Normal file
17
app/workers/activitypub/migrated_follow_delivery_worker.rb
Normal 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
|
|
@ -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|
|
||||||
|
pipeline.srem("chewy:queue:#{type.name}", ids)
|
||||||
redis.pipelined do |pipeline|
|
end
|
||||||
ids.each { |id| pipeline.srem("chewy:queue:#{type.name}", id) }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -5,7 +5,9 @@ require_relative '../config/boot'
|
||||||
require_relative '../lib/cli'
|
require_relative '../lib/cli'
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Mastodon::CLI.start(ARGV)
|
Chewy.strategy(:mastodon) do
|
||||||
|
Mastodon::CLI.start(ARGV)
|
||||||
|
end
|
||||||
rescue Interrupt
|
rescue Interrupt
|
||||||
exit(130)
|
exit(130)
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,6 +29,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'
|
||||||
|
@ -39,6 +40,7 @@ require_relative '../lib/mastodon/rack_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'
|
||||||
|
@ -159,6 +161,10 @@ module Mastodon
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config.public_file_server.headers = {
|
||||||
|
'X-Content-Type-Options' => 'nosniff',
|
||||||
|
}
|
||||||
|
|
||||||
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
||||||
# config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
|
# config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ default: &default
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
encoding: unicode
|
encoding: unicode
|
||||||
sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %>
|
sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %>
|
||||||
|
application_name: ''
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *default
|
<<: *default
|
||||||
|
|
27
config/imagemagick/policy.xml
Normal file
27
config/imagemagick/policy.xml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<policymap>
|
||||||
|
<!-- Set some basic system resource limits -->
|
||||||
|
<policy domain="resource" name="time" value="60" />
|
||||||
|
|
||||||
|
<policy domain="module" rights="none" pattern="URL" />
|
||||||
|
|
||||||
|
<policy domain="filter" rights="none" pattern="*" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Ideally, we would restrict ImageMagick to only accessing its own
|
||||||
|
disk-backed pixel cache as well as Mastodon-created Tempfiles.
|
||||||
|
|
||||||
|
However, those paths depend on the operating system and environment
|
||||||
|
variables, so they can only be known at runtime.
|
||||||
|
|
||||||
|
Furthermore, those paths are not necessarily shared across Mastodon
|
||||||
|
processes, so even creating a policy.xml at runtime is impractical.
|
||||||
|
|
||||||
|
For the time being, only disable indirect reads.
|
||||||
|
-->
|
||||||
|
<policy domain="path" rights="none" pattern="@*" />
|
||||||
|
|
||||||
|
<!-- Disallow any coder by default, and only enable ones required by Mastodon -->
|
||||||
|
<policy domain="coder" rights="none" pattern="*" />
|
||||||
|
<policy domain="coder" rights="read | write" pattern="{PNG,JPEG,GIF,HEIC,WEBP}" />
|
||||||
|
<policy domain="coder" rights="write" pattern="{HISTOGRAM,RGB,INFO}" />
|
||||||
|
</policymap>
|
|
@ -19,7 +19,7 @@ Chewy.settings = {
|
||||||
# cycle, which takes care of checking if Elasticsearch is enabled
|
# cycle, which takes care of checking if Elasticsearch is enabled
|
||||||
# or not. However, mind that for the Rails console, the :urgent
|
# or not. However, mind that for the Rails console, the :urgent
|
||||||
# strategy is set automatically with no way to override it.
|
# strategy is set automatically with no way to override it.
|
||||||
Chewy.root_strategy = :mastodon
|
Chewy.root_strategy = :bypass_with_warning if Rails.env.production?
|
||||||
Chewy.request_strategy = :mastodon
|
Chewy.request_strategy = :mastodon
|
||||||
Chewy.use_after_commit_callbacks = false
|
Chewy.use_after_commit_callbacks = false
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
|
||||||
|
|
||||||
def host_to_url(str)
|
def host_to_url(str)
|
||||||
"http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}" unless str.blank?
|
"http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}".split('/').first if str.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
base_host = Rails.configuration.x.web_domain
|
base_host = Rails.configuration.x.web_domain
|
||||||
|
|
|
@ -124,6 +124,7 @@ elsif ENV['SWIFT_ENABLED'] == 'true'
|
||||||
openstack_domain_name: ENV.fetch('SWIFT_DOMAIN_NAME') { 'default' },
|
openstack_domain_name: ENV.fetch('SWIFT_DOMAIN_NAME') { 'default' },
|
||||||
openstack_region: ENV['SWIFT_REGION'],
|
openstack_region: ENV['SWIFT_REGION'],
|
||||||
openstack_cache_ttl: ENV.fetch('SWIFT_CACHE_TTL') { 60 },
|
openstack_cache_ttl: ENV.fetch('SWIFT_CACHE_TTL') { 60 },
|
||||||
|
openstack_temp_url_key: ENV['SWIFT_TEMP_URL_KEY'],
|
||||||
},
|
},
|
||||||
|
|
||||||
fog_file: { 'Cache-Control' => 'public, max-age=315576000, immutable' },
|
fog_file: { 'Cache-Control' => 'public, max-age=315576000, immutable' },
|
||||||
|
@ -154,3 +155,10 @@ unless defined?(Seahorse)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Set our ImageMagick security policy, but allow admins to override it
|
||||||
|
ENV['MAGICK_CONFIGURE_PATH'] = begin
|
||||||
|
imagemagick_config_paths = ENV.fetch('MAGICK_CONFIGURE_PATH', '').split(File::PATH_SEPARATOR)
|
||||||
|
imagemagick_config_paths << Rails.root.join('config', 'imagemagick').expand_path.to_s
|
||||||
|
imagemagick_config_paths.join(File::PATH_SEPARATOR)
|
||||||
|
end
|
||||||
|
|
|
@ -25,7 +25,7 @@ module Twitter::TwitterText
|
||||||
\)
|
\)
|
||||||
/iox
|
/iox
|
||||||
UCHARS = '\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}'
|
UCHARS = '\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}'
|
||||||
REGEXEN[:valid_url_query_chars] = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@#{UCHARS}]/iou
|
REGEXEN[:valid_url_query_chars] = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@\^#{UCHARS}]/iou
|
||||||
REGEXEN[:valid_url_query_ending_chars] = /[a-z0-9_&=#\/\-#{UCHARS}]/iou
|
REGEXEN[:valid_url_query_ending_chars] = /[a-z0-9_&=#\/\-#{UCHARS}]/iou
|
||||||
REGEXEN[:valid_url_path] = /(?:
|
REGEXEN[:valid_url_path] = /(?:
|
||||||
(?:
|
(?:
|
||||||
|
|
|
@ -53,3 +53,7 @@ en:
|
||||||
position:
|
position:
|
||||||
elevated: cannot be higher than your current role
|
elevated: cannot be higher than your current role
|
||||||
own_role: cannot be changed with your current role
|
own_role: cannot be changed with your current role
|
||||||
|
webhook:
|
||||||
|
attributes:
|
||||||
|
events:
|
||||||
|
invalid_permissions: cannot include events you don't have the rights to
|
||||||
|
|
|
@ -756,6 +756,12 @@ en:
|
||||||
message_html: You haven't defined any server rules.
|
message_html: You haven't defined any server rules.
|
||||||
sidekiq_process_check:
|
sidekiq_process_check:
|
||||||
message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration
|
message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration
|
||||||
|
upload_check_privacy_error:
|
||||||
|
action: Check here for more information
|
||||||
|
message_html: "<strong>Your web server is misconfigured. The privacy of your users is at risk.</strong>"
|
||||||
|
upload_check_privacy_error_object_storage:
|
||||||
|
action: Check here for more information
|
||||||
|
message_html: "<strong>Your object storage is misconfigured. The privacy of your users is at risk.</strong>"
|
||||||
tags:
|
tags:
|
||||||
review: Review status
|
review: Review status
|
||||||
updated_msg: Hashtag settings updated successfully
|
updated_msg: Hashtag settings updated successfully
|
||||||
|
@ -1326,6 +1332,7 @@ en:
|
||||||
expired: The poll has already ended
|
expired: The poll has already ended
|
||||||
invalid_choice: The chosen vote option does not exist
|
invalid_choice: The chosen vote option does not exist
|
||||||
over_character_limit: cannot be longer than %{max} characters each
|
over_character_limit: cannot be longer than %{max} characters each
|
||||||
|
self_vote: You cannot vote in your own polls
|
||||||
too_few_options: must have more than one item
|
too_few_options: must have more than one item
|
||||||
too_many_options: can't contain more than %{max} items
|
too_many_options: can't contain more than %{max} items
|
||||||
preferences:
|
preferences:
|
||||||
|
@ -1341,6 +1348,7 @@ en:
|
||||||
relationships:
|
relationships:
|
||||||
activity: Account activity
|
activity: Account activity
|
||||||
dormant: Dormant
|
dormant: Dormant
|
||||||
|
follow_failure: Could not follow some of the selected accounts.
|
||||||
follow_selected_followers: Follow selected followers
|
follow_selected_followers: Follow selected followers
|
||||||
followers: Followers
|
followers: Followers
|
||||||
following: Following
|
following: Following
|
||||||
|
|
|
@ -109,6 +109,8 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resource :inbox, only: [:create], module: :activitypub
|
resource :inbox, only: [:create], module: :activitypub
|
||||||
|
|
||||||
|
get '/:encoded_at(*path)', to: redirect("/@%{path}"), constraints: { encoded_at: /%40/ }
|
||||||
|
|
||||||
constraints(username: /[^@\/.]+/) do
|
constraints(username: /[^@\/.]+/) do
|
||||||
get '/@:username', to: 'accounts#show', as: :short_account
|
get '/@:username', to: 'accounts#show', as: :short_account
|
||||||
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
|
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
|
||||||
|
@ -217,6 +219,7 @@ Rails.application.routes.draw do
|
||||||
resource :statuses_cleanup, controller: :statuses_cleanup, only: [:show, :update]
|
resource :statuses_cleanup, controller: :statuses_cleanup, only: [:show, :update]
|
||||||
|
|
||||||
get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy, format: false
|
get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy, format: false
|
||||||
|
get '/backups/:id/download', to: 'backups#download', as: :download_backup, format: false
|
||||||
|
|
||||||
resource :authorize_interaction, only: [:show, :create]
|
resource :authorize_interaction, only: [:show, :create]
|
||||||
resource :share, only: [:show, :create]
|
resource :share, only: [:show, :create]
|
||||||
|
@ -447,7 +450,9 @@ Rails.application.routes.draw do
|
||||||
resources :list, only: :show
|
resources :list, only: :show
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :streaming, only: [:index]
|
get '/streaming', to: 'streaming#index'
|
||||||
|
get '/streaming/(*any)', to: 'streaming#index'
|
||||||
|
|
||||||
resources :custom_emojis, only: [:index]
|
resources :custom_emojis, only: [:index]
|
||||||
resources :suggestions, only: [:index, :destroy]
|
resources :suggestions, only: [:index, :destroy]
|
||||||
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Dir[Rails.root.join('db', 'seeds', '*.rb')].sort.each do |seed|
|
Chewy.strategy(:mastodon) do
|
||||||
load seed
|
Dir[Rails.root.join('db', 'seeds', '*.rb')].sort.each do |seed|
|
||||||
|
load seed
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
2
dist/nginx.conf
vendored
2
dist/nginx.conf
vendored
|
@ -109,6 +109,8 @@ server {
|
||||||
location ~ ^/system/ {
|
location ~ ^/system/ {
|
||||||
add_header Cache-Control "public, max-age=2419200, immutable";
|
add_header Cache-Control "public, max-age=2419200, immutable";
|
||||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header Content-Security-Policy "default-src 'none'; form-action 'none'";
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ services:
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: tootsuite/mastodon
|
image: ghcr.io/mastodon/mastodon
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||||
|
@ -77,7 +77,7 @@ services:
|
||||||
|
|
||||||
streaming:
|
streaming:
|
||||||
build: .
|
build: .
|
||||||
image: tootsuite/mastodon
|
image: ghcr.io/mastodon/mastodon
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming
|
command: node ./streaming
|
||||||
|
@ -95,7 +95,7 @@ services:
|
||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: tootsuite/mastodon
|
image: ghcr.io/mastodon/mastodon
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|
12
lib/chewy/strategy/bypass_with_warning.rb
Normal file
12
lib/chewy/strategy/bypass_with_warning.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Chewy
|
||||||
|
class Strategy
|
||||||
|
class BypassWithWarning < Base
|
||||||
|
def update(...)
|
||||||
|
Rails.logger.warn 'Chewy update without a root strategy' unless @warning_issued
|
||||||
|
@warning_issued = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -513,7 +513,7 @@ module Mastodon
|
||||||
User.pending.find_each(&:approve!)
|
User.pending.find_each(&:approve!)
|
||||||
say('OK', :green)
|
say('OK', :green)
|
||||||
elsif options[:number]
|
elsif options[:number]
|
||||||
User.pending.limit(options[:number]).each(&:approve!)
|
User.pending.order(created_at: :asc).limit(options[:number]).each(&:approve!)
|
||||||
say('OK', :green)
|
say('OK', :green)
|
||||||
elsif username.present?
|
elsif username.present?
|
||||||
account = Account.find_local(username)
|
account = Account.find_local(username)
|
||||||
|
|
|
@ -53,14 +53,16 @@ module Mastodon
|
||||||
|
|
||||||
progress.log("Processing #{item.id}") if options[:verbose]
|
progress.log("Processing #{item.id}") if options[:verbose]
|
||||||
|
|
||||||
result = ActiveRecord::Base.connection_pool.with_connection do
|
Chewy.strategy(:mastodon) do
|
||||||
yield(item)
|
result = ActiveRecord::Base.connection_pool.with_connection do
|
||||||
ensure
|
yield(item)
|
||||||
RedisConfiguration.pool.checkin if Thread.current[:redis]
|
ensure
|
||||||
Thread.current[:redis] = nil
|
RedisConfiguration.pool.checkin if Thread.current[:redis]
|
||||||
end
|
Thread.current[:redis] = nil
|
||||||
|
end
|
||||||
|
|
||||||
aggregate.increment(result) if result.is_a?(Integer)
|
aggregate.increment(result) if result.is_a?(Integer)
|
||||||
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
progress.log pastel.red("Error processing #{item.id}: #{e}")
|
progress.log pastel.red("Error processing #{item.id}: #{e}")
|
||||||
ensure
|
ensure
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
class Mastodon::SidekiqMiddleware
|
class Mastodon::SidekiqMiddleware
|
||||||
BACKTRACE_LIMIT = 3
|
BACKTRACE_LIMIT = 3
|
||||||
|
|
||||||
def call(*)
|
def call(*, &block)
|
||||||
yield
|
Chewy.strategy(:mastodon, &block)
|
||||||
rescue Mastodon::HostValidationError
|
rescue Mastodon::HostValidationError
|
||||||
# Do not retry
|
# Do not retry
|
||||||
rescue => e
|
rescue => e
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
2
|
6
|
||||||
end
|
end
|
||||||
|
|
||||||
def flags
|
def flags
|
||||||
|
|
22
lib/paperclip/media_type_spoof_detector_extensions.rb
Normal file
22
lib/paperclip/media_type_spoof_detector_extensions.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Paperclip
|
||||||
|
module MediaTypeSpoofDetectorExtensions
|
||||||
|
def calculated_content_type
|
||||||
|
return @calculated_content_type if defined?(@calculated_content_type)
|
||||||
|
|
||||||
|
@calculated_content_type = type_from_file_command.chomp
|
||||||
|
|
||||||
|
# The `file` command fails to recognize some MP3 files as such
|
||||||
|
@calculated_content_type = type_from_marcel if @calculated_content_type == 'application/octet-stream' && type_from_marcel == 'audio/mpeg'
|
||||||
|
@calculated_content_type
|
||||||
|
end
|
||||||
|
|
||||||
|
def type_from_marcel
|
||||||
|
@type_from_marcel ||= Marcel::MimeType.for Pathname.new(@file.path),
|
||||||
|
name: @file.path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Paperclip::MediaTypeSpoofDetector.prepend(Paperclip::MediaTypeSpoofDetectorExtensions)
|
|
@ -19,10 +19,7 @@ module Paperclip
|
||||||
def make
|
def make
|
||||||
metadata = VideoMetadataExtractor.new(@file.path)
|
metadata = VideoMetadataExtractor.new(@file.path)
|
||||||
|
|
||||||
unless metadata.valid?
|
raise Paperclip::Error, "Error while transcoding #{@file.path}: unsupported file" unless metadata.valid?
|
||||||
Paperclip.log("Unsupported file #{@file.path}")
|
|
||||||
return File.open(@file.path)
|
|
||||||
end
|
|
||||||
|
|
||||||
update_attachment_type(metadata)
|
update_attachment_type(metadata)
|
||||||
update_options_from_metadata(metadata)
|
update_options_from_metadata(metadata)
|
||||||
|
|
|
@ -94,26 +94,26 @@ class Sanitize
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
MASTODON_OEMBED ||= freeze_config merge(
|
MASTODON_OEMBED ||= freeze_config(
|
||||||
RELAXED,
|
elements: %w(audio embed iframe source video),
|
||||||
elements: RELAXED[:elements] + %w(audio embed iframe source video),
|
|
||||||
|
|
||||||
attributes: merge(
|
attributes: {
|
||||||
RELAXED[:attributes],
|
|
||||||
'audio' => %w(controls),
|
'audio' => %w(controls),
|
||||||
'embed' => %w(height src type width),
|
'embed' => %w(height src type width),
|
||||||
'iframe' => %w(allowfullscreen frameborder height scrolling src width),
|
'iframe' => %w(allowfullscreen frameborder height scrolling src width),
|
||||||
'source' => %w(src type),
|
'source' => %w(src type),
|
||||||
'video' => %w(controls height loop width),
|
'video' => %w(controls height loop width),
|
||||||
'div' => [:data]
|
},
|
||||||
),
|
|
||||||
|
|
||||||
protocols: merge(
|
protocols: {
|
||||||
RELAXED[:protocols],
|
|
||||||
'embed' => { 'src' => HTTP_PROTOCOLS },
|
'embed' => { 'src' => HTTP_PROTOCOLS },
|
||||||
'iframe' => { 'src' => HTTP_PROTOCOLS },
|
'iframe' => { 'src' => HTTP_PROTOCOLS },
|
||||||
'source' => { 'src' => HTTP_PROTOCOLS }
|
'source' => { 'src' => HTTP_PROTOCOLS },
|
||||||
)
|
},
|
||||||
|
|
||||||
|
add_attributes: {
|
||||||
|
'iframe' => { 'sandbox' => 'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms' },
|
||||||
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,7 @@ namespace :branding do
|
||||||
output_dest = Rails.root.join('app', 'javascript', 'icons')
|
output_dest = Rails.root.join('app', 'javascript', 'icons')
|
||||||
|
|
||||||
rsvg_convert = Terrapin::CommandLine.new('rsvg-convert', '-w :size -h :size --keep-aspect-ratio :input -o :output')
|
rsvg_convert = Terrapin::CommandLine.new('rsvg-convert', '-w :size -h :size --keep-aspect-ratio :input -o :output')
|
||||||
convert = Terrapin::CommandLine.new('convert', ':input :output')
|
convert = Terrapin::CommandLine.new('convert', ':input :output', environment: { 'MAGICK_CONFIGURE_PATH' => nil })
|
||||||
|
|
||||||
favicon_sizes = [16, 32, 48]
|
favicon_sizes = [16, 32, 48]
|
||||||
apple_icon_sizes = [57, 60, 72, 76, 114, 120, 144, 152, 167, 180, 1024]
|
apple_icon_sizes = [57, 60, 72, 76, 114, 120, 144, 152, 167, 180, 1024]
|
||||||
|
|
|
@ -16,6 +16,7 @@ RSpec.describe Api::V1::ConversationsController, type: :controller do
|
||||||
|
|
||||||
before do
|
before do
|
||||||
PostStatusService.new.call(other.account, text: 'Hey @alice', visibility: 'direct')
|
PostStatusService.new.call(other.account, text: 'Hey @alice', visibility: 'direct')
|
||||||
|
PostStatusService.new.call(user.account, text: 'Hey, nobody here', visibility: 'direct')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
|
@ -31,7 +32,26 @@ RSpec.describe Api::V1::ConversationsController, type: :controller do
|
||||||
it 'returns conversations' do
|
it 'returns conversations' do
|
||||||
get :index
|
get :index
|
||||||
json = body_as_json
|
json = body_as_json
|
||||||
expect(json.size).to eq 1
|
expect(json.size).to eq 2
|
||||||
|
expect(json[0][:accounts].size).to eq 1
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with since_id' do
|
||||||
|
context 'when requesting old posts' do
|
||||||
|
it 'returns conversations' do
|
||||||
|
get :index, params: { since_id: Mastodon::Snowflake.id_at(1.hour.ago, with_random: false) }
|
||||||
|
json = body_as_json
|
||||||
|
expect(json.size).to eq 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when requesting posts in the future' do
|
||||||
|
it 'returns no conversation' do
|
||||||
|
get :index, params: { since_id: Mastodon::Snowflake.id_at(1.hour.from_now, with_random: false) }
|
||||||
|
json = body_as_json
|
||||||
|
expect(json.size).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +23,7 @@ describe Api::V1::Statuses::HistoriesController do
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json.size).to_not be 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -69,5 +69,13 @@ RSpec.describe Api::V2::Admin::AccountsController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with limit param' do
|
||||||
|
let(:params) { { limit: 1 } }
|
||||||
|
|
||||||
|
it 'sets the correct pagination headers' do
|
||||||
|
expect(response.headers['Link'].find_link(%w(rel next)).href).to eq api_v2_admin_accounts_url(limit: 1, max_id: admin_account.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -55,7 +55,7 @@ describe RelationshipsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when select parameter is provided' do
|
context 'when select parameter is provided' do
|
||||||
subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, block_domains: '' } }
|
subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, remove_domains_from_followers: '' } }
|
||||||
|
|
||||||
it 'soft-blocks followers from selected domains' do
|
it 'soft-blocks followers from selected domains' do
|
||||||
poopfeast.follow!(user.account)
|
poopfeast.follow!(user.account)
|
||||||
|
@ -66,6 +66,15 @@ describe RelationshipsController do
|
||||||
expect(poopfeast.following?(user.account)).to be false
|
expect(poopfeast.following?(user.account)).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not unfollow users from selected domains' do
|
||||||
|
user.account.follow!(poopfeast)
|
||||||
|
|
||||||
|
sign_in user, scope: :user
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(user.account.following?(poopfeast)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
include_examples 'authenticate user'
|
include_examples 'authenticate user'
|
||||||
include_examples 'redirects back to followers page'
|
include_examples 'redirects back to followers page'
|
||||||
end
|
end
|
||||||
|
|
|
@ -248,7 +248,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
||||||
|
|
||||||
post :create, params: { credential: new_webauthn_credential, nickname: 'USB Key' }
|
post :create, params: { credential: new_webauthn_credential, nickname: 'USB Key' }
|
||||||
|
|
||||||
expect(response).to have_http_status(500)
|
expect(response).to have_http_status(422)
|
||||||
expect(flash[:error]).to be_present
|
expect(flash[:error]).to be_present
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -268,7 +268,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
||||||
|
|
||||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||||
|
|
||||||
expect(response).to have_http_status(500)
|
expect(response).to have_http_status(422)
|
||||||
expect(flash[:error]).to be_present
|
expect(flash[:error]).to be_present
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
BIN
spec/fixtures/files/attachment-jpg.123456_abcd
vendored
Normal file
BIN
spec/fixtures/files/attachment-jpg.123456_abcd
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
BIN
spec/fixtures/files/boop.mp3
vendored
Normal file
BIN
spec/fixtures/files/boop.mp3
vendored
Normal file
Binary file not shown.
53
spec/lib/account_reach_finder_spec.rb
Normal file
53
spec/lib/account_reach_finder_spec.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe AccountReachFinder do
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
|
let(:follower1) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-1') }
|
||||||
|
let(:follower2) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-2') }
|
||||||
|
let(:follower3) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://foo.bar/users/a/inbox', shared_inbox_url: 'https://foo.bar/inbox') }
|
||||||
|
|
||||||
|
let(:mentioned1) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://foo.bar/users/b/inbox', shared_inbox_url: 'https://foo.bar/inbox') }
|
||||||
|
let(:mentioned2) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-3') }
|
||||||
|
let(:mentioned3) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-4') }
|
||||||
|
|
||||||
|
let(:unrelated_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/unrelated-inbox') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
follower1.follow!(account)
|
||||||
|
follower2.follow!(account)
|
||||||
|
follower3.follow!(account)
|
||||||
|
|
||||||
|
Fabricate(:status, account: account).tap do |status|
|
||||||
|
status.mentions << Mention.new(account: follower1)
|
||||||
|
status.mentions << Mention.new(account: mentioned1)
|
||||||
|
end
|
||||||
|
|
||||||
|
Fabricate(:status, account: account)
|
||||||
|
|
||||||
|
Fabricate(:status, account: account).tap do |status|
|
||||||
|
status.mentions << Mention.new(account: mentioned2)
|
||||||
|
status.mentions << Mention.new(account: mentioned3)
|
||||||
|
end
|
||||||
|
|
||||||
|
Fabricate(:status).tap do |status|
|
||||||
|
status.mentions << Mention.new(account: unrelated_account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#inboxes' do
|
||||||
|
it 'includes the preferred inbox URL of followers' do
|
||||||
|
expect(described_class.new(account).inboxes).to include(*[follower1, follower2, follower3].map(&:preferred_inbox_url))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes the preferred inbox URL of recently-mentioned accounts' do
|
||||||
|
expect(described_class.new(account).inboxes).to include(*[mentioned1, mentioned2, mentioned3].map(&:preferred_inbox_url))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not include the inbox of unrelated users' do
|
||||||
|
expect(described_class.new(account).inboxes).to_not include(unrelated_account.preferred_inbox_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,7 +4,7 @@ RSpec.describe PlainTextFormatter do
|
||||||
describe '#to_s' do
|
describe '#to_s' do
|
||||||
subject { described_class.new(status.text, status.local?).to_s }
|
subject { described_class.new(status.text, status.local?).to_s }
|
||||||
|
|
||||||
context 'given a post with local status' do
|
context 'when status is local' do
|
||||||
let(:status) { Fabricate(:status, text: '<p>a text by a nerd who uses an HTML tag in text</p>', uri: nil) }
|
let(:status) { Fabricate(:status, text: '<p>a text by a nerd who uses an HTML tag in text</p>', uri: nil) }
|
||||||
|
|
||||||
it 'returns the raw text' do
|
it 'returns the raw text' do
|
||||||
|
@ -12,12 +12,63 @@ RSpec.describe PlainTextFormatter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'given a post with remote status' do
|
context 'when status is remote' do
|
||||||
let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') }
|
let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') }
|
||||||
let(:status) { Fabricate(:status, account: remote_account, text: '<p>Hello</p><script>alert("Hello")</script>') }
|
|
||||||
|
|
||||||
it 'returns tag-stripped text' do
|
context 'when text contains inline HTML tags' do
|
||||||
is_expected.to eq 'Hello'
|
let(:status) { Fabricate(:status, account: remote_account, text: '<b>Lorem</b> <em>ipsum</em>') }
|
||||||
|
|
||||||
|
it 'strips the tags' do
|
||||||
|
expect(subject).to eq 'Lorem ipsum'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when text contains <p> tags' do
|
||||||
|
let(:status) { Fabricate(:status, account: remote_account, text: '<p>Lorem</p><p>ipsum</p>') }
|
||||||
|
|
||||||
|
it 'inserts a newline' do
|
||||||
|
expect(subject).to eq "Lorem\nipsum"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when text contains a single <br> tag' do
|
||||||
|
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem<br>ipsum') }
|
||||||
|
|
||||||
|
it 'inserts a newline' do
|
||||||
|
expect(subject).to eq "Lorem\nipsum"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when text contains consecutive <br> tag' do
|
||||||
|
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem<br><br><br>ipsum') }
|
||||||
|
|
||||||
|
it 'inserts a single newline' do
|
||||||
|
expect(subject).to eq "Lorem\nipsum"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when text contains HTML entity' do
|
||||||
|
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem & ipsum ❤') }
|
||||||
|
|
||||||
|
it 'unescapes the entity' do
|
||||||
|
expect(subject).to eq 'Lorem & ipsum ❤'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when text contains <script> tag' do
|
||||||
|
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem <script> alert("Booh!") </script>ipsum') }
|
||||||
|
|
||||||
|
it 'strips the tag and its contents' do
|
||||||
|
expect(subject).to eq 'Lorem ipsum'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when text contains an HTML comment tags' do
|
||||||
|
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem <!-- Booh! -->ipsum') }
|
||||||
|
|
||||||
|
it 'strips the comment' do
|
||||||
|
expect(subject).to eq 'Lorem ipsum'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -150,6 +150,26 @@ RSpec.describe MediaAttachment, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'mp3 with large cover art' do
|
||||||
|
let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('boop.mp3')) }
|
||||||
|
|
||||||
|
it 'detects it as an audio file' do
|
||||||
|
expect(media.type).to eq 'audio'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets meta for the duration' do
|
||||||
|
expect(media.file.meta['original']['duration']).to be_within(0.05).of(0.235102)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'extracts thumbnail' do
|
||||||
|
expect(media.thumbnail.present?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gives the file a random name' do
|
||||||
|
expect(media.file_file_name).to_not eq 'boop.mp3'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'jpeg' do
|
describe 'jpeg' do
|
||||||
let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) }
|
let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) }
|
||||||
|
|
||||||
|
|
18
spec/requests/api/v2/media_spec.rb
Normal file
18
spec/requests/api/v2/media_spec.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Media API', paperclip_processing: true do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:scopes) { 'write' }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
describe 'POST /api/v2/media' do
|
||||||
|
it 'returns http success' do
|
||||||
|
post '/api/v2/media', headers: headers, params: { file: fixture_file_upload('attachment-jpg.123456_abcd', 'image/jpeg') }
|
||||||
|
expect(File.exist?(user.account.media_attachments.first.file.path(:small))).to be true
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue