Compare commits
79 commits
Author | SHA1 | Date | |
---|---|---|---|
|
637c7d464b | ||
|
aeccbb2a14 | ||
|
b99c58b6fe | ||
|
d7adbf5a63 | ||
|
5231ae7ae6 | ||
|
1cc5c35bb0 | ||
|
f22f6d970d | ||
|
5efc927261 | ||
|
5e9118c9bb | ||
|
b8a5b3a3db | ||
|
b84182b5ba | ||
|
0842e3b4fb | ||
|
e8a2d12338 | ||
|
df6a953f52 | ||
|
c2bd6e90b4 | ||
|
7619689fcb | ||
|
5ec943f85d | ||
|
ad06423e71 | ||
|
82a490ac7f | ||
|
dbe5e29e38 | ||
|
ff19501e50 | ||
|
e02a7cfeb2 | ||
|
5d72e6c4d0 | ||
|
b6b19419e2 | ||
|
a19aec0f48 | ||
|
1ddbefb787 | ||
|
85c845c001 | ||
|
80ca4fdb3c | ||
|
168272fe61 | ||
|
2c02cb59ef | ||
|
edc55002cf | ||
|
e10920eb20 | ||
|
d33b361000 | ||
|
5b07f4e90e | ||
|
0994c4b11a | ||
|
d2cdfe92ed | ||
|
6c344d90c7 | ||
|
c0b2c2c166 | ||
|
19edb7a3f4 | ||
|
53e4efd07d | ||
|
3f882c2c17 | ||
|
1cfa2bdb03 | ||
|
1b32c001bc | ||
|
31d9aa8ed0 | ||
|
4d41c91335 | ||
|
678e0ad419 | ||
|
c89809afc5 | ||
|
a319fd3cc4 | ||
|
24dee67d32 | ||
|
07042a0913 | ||
|
4978d387ee | ||
|
0951c691ff | ||
|
fc4b9856f8 | ||
|
add7b9f82e | ||
|
9437e9f0b9 | ||
|
0fe57a9140 | ||
|
3a4d9f1f2d | ||
|
78d5bda973 | ||
|
4b025cf7e6 | ||
|
4bd8dc09d8 | ||
|
3799fd17ba | ||
|
53814b2b31 | ||
|
2012c5ae50 | ||
|
9aa7286c8f | ||
|
4b9a0cfe5e | ||
|
b593a7da8c | ||
|
48b25e457d | ||
|
da14725a96 | ||
|
cc21670b3c | ||
|
685cde55cb | ||
|
a2dc4e583b | ||
|
13d1111a92 | ||
|
6386421d1a | ||
|
4063bbe04e | ||
|
4ea2c1da95 | ||
|
47cab05003 | ||
|
02d195809b | ||
|
64d9b84f1d | ||
|
a6e9c41ed4 |
131 changed files with 2445 additions and 655 deletions
|
@ -160,8 +160,45 @@ jobs:
|
||||||
name: Create database
|
name: Create database
|
||||||
command: ./bin/rails db:create
|
command: ./bin/rails db:create
|
||||||
- run:
|
- run:
|
||||||
name: Run migrations
|
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||||
|
name: Run migrations up to v2.0.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
command: ./bin/rails db:migrate
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all remaining migrations
|
||||||
|
|
||||||
|
test-two-step-migrations:
|
||||||
|
<<: *defaults
|
||||||
|
docker:
|
||||||
|
- image: circleci/ruby:2.7-buster-node
|
||||||
|
environment: *ruby_environment
|
||||||
|
- image: circleci/postgres:12.2
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: root
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
- image: circleci/redis:5-alpine
|
||||||
|
steps:
|
||||||
|
- *attach_workspace
|
||||||
|
- *install_system_dependencies
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:create
|
||||||
|
name: Create database
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||||
|
name: Run migrations up to v2.0.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all pre-deployment migrations
|
||||||
|
evironment:
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all post-deployment remaining migrations
|
||||||
|
|
||||||
test-ruby2.7:
|
test-ruby2.7:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
|
@ -233,6 +270,9 @@ workflows:
|
||||||
- test-migrations:
|
- test-migrations:
|
||||||
requires:
|
requires:
|
||||||
- install-ruby2.7
|
- install-ruby2.7
|
||||||
|
- test-two-step-migrations:
|
||||||
|
requires:
|
||||||
|
- install-ruby2.7
|
||||||
- test-ruby2.7:
|
- test-ruby2.7:
|
||||||
requires:
|
requires:
|
||||||
- install-ruby2.7
|
- install-ruby2.7
|
||||||
|
|
|
@ -4,6 +4,12 @@
|
||||||
# not demonstrate all available configuration options. Please look at
|
# not demonstrate all available configuration options. Please look at
|
||||||
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
|
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
|
||||||
|
|
||||||
|
# Note that this file accepts slightly different syntax depending on whether
|
||||||
|
# you are using `docker-compose` or not. In particular, if you use
|
||||||
|
# `docker-compose`, the value of each declared variable will be taken verbatim,
|
||||||
|
# including surrounding quotes.
|
||||||
|
# See: https://github.com/mastodon/mastodon/issues/16895
|
||||||
|
|
||||||
# Federation
|
# Federation
|
||||||
# ----------
|
# ----------
|
||||||
# This identifies your server and cannot be changed safely later
|
# This identifies your server and cannot be changed safely later
|
||||||
|
|
34
.github/workflows/build-image.yml
vendored
Normal file
34
.github/workflows/build-image.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
name: Build container image
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
jobs:
|
||||||
|
build-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: docker/setup-buildx-action@v1
|
||||||
|
- uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- uses: docker/metadata-action@v3
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: tootsuite/mastodon
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
|
tags: |
|
||||||
|
type=edge,branch=main
|
||||||
|
type=semver,pattern={{ raw }}
|
||||||
|
- uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=registry,ref=tootsuite/mastodon:latest
|
||||||
|
cache-to: type=inline
|
78
CHANGELOG.md
78
CHANGELOG.md
|
@ -3,6 +3,84 @@ 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.
|
||||||
|
|
||||||
|
## [3.3.2] - 2022-02-03
|
||||||
|
### Fixed
|
||||||
|
- Fix `mastodon:webpush:generate_vapid_key` task requiring a functional environment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17338))
|
||||||
|
- Fix spurious errors when receiving an Add activity for a private post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17425))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Fix error-prone SQL queries ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15828))
|
||||||
|
- Fix not compacting incoming signed JSON-LD activities ([puckipedia](https://github.com/mastodon/mastodon/pull/17426), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17428)) (CVE-2022-24307)
|
||||||
|
- Fix insufficient sanitization of report comments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17430))
|
||||||
|
- Fix stop condition of a Common Table Expression ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17427))
|
||||||
|
- Disable legacy XSS filtering ([Wonderfall](https://github.com/mastodon/mastodon/pull/17289))
|
||||||
|
|
||||||
|
## [3.3.1] - 2022-01-31
|
||||||
|
### Added
|
||||||
|
- Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393))
|
||||||
|
- Add github workflow to build Docker images ([unasuke](https://github.com/mastodon/mastodon/pull/16973), [Gargron](https://github.com/mastodon/mastodon/pull/16980), [Gargron](https://github.com/mastodon/mastodon/pull/17000))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Update some dependencies that were broken or unavailable
|
||||||
|
- Fix some old migrations failing when skipping releases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17394))
|
||||||
|
- Fix migrations script failing in certain edge cases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17398))
|
||||||
|
- Fix media redownload worker retrying on unexpected response codes ([Gargron](https://github.com/tootsuite/mastodon/pull/16111))
|
||||||
|
- Fix thread resolve worker retrying when status no longer exists ([Gargron](https://github.com/tootsuite/mastodon/pull/16109))
|
||||||
|
- Fix n+1 queries when rendering statuses in REST API ([abcang](https://github.com/tootsuite/mastodon/pull/15641))
|
||||||
|
- Fix remote reporters not receiving suspend/unsuspend activities ([Gargron](https://github.com/tootsuite/mastodon/pull/16050))
|
||||||
|
- Fix understanding (not fully qualified) `as:Public` and `Public` ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15948))
|
||||||
|
- Fix actor update not being distributed on profile picture deletion ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15461))
|
||||||
|
- Fix processing of incoming Delete activities ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16084))
|
||||||
|
- Fix processing of incoming Block activities ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15546))
|
||||||
|
- Fix processing of incoming Update activities of unknown accounts ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15514))
|
||||||
|
- Fix URIs of repeat follow requests not being recorded ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15662))
|
||||||
|
- Fix error on requests with no `Digest` header ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15782))
|
||||||
|
- Fix activity object not requiring signature in secure mode ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15592))
|
||||||
|
- Fix database serialization failure returning HTTP 500 ([Gargron](https://github.com/tootsuite/mastodon/pull/16101))
|
||||||
|
- Fix media processing getting stuck on too much stdin/stderr ([Gargron](https://github.com/tootsuite/mastodon/pull/16136))
|
||||||
|
- Fix `tootctl maintenance fix-duplicates` failures ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15923), [ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15515))
|
||||||
|
- Fix blocking someone not clearing up list feeds ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16205))
|
||||||
|
- Fix edge case where follow limit interferes with accepting a follow ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/16098))
|
||||||
|
- Fix reports of already suspended accounts being recorded ([Gargron](https://github.com/tootsuite/mastodon/pull/16047))
|
||||||
|
- Fix sign-up restrictions based on IP addresses not being enforced ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15607))
|
||||||
|
- Fix race conditions on account migration creation ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15597))
|
||||||
|
- Fix not being able to change world filter expiration back to “Never” ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15858))
|
||||||
|
- Fix error when muting users with `duration` in REST API ([Tak](https://github.com/tootsuite/mastodon/pull/15516))
|
||||||
|
- Fix wrong URL to custom CSS when `CDN_HOST` is used ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15927))
|
||||||
|
- Fix `tootctl accounts unfollow` ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15639))
|
||||||
|
- Fix `tootctl emoji import` wasting time on MacOS shadow files ([cortices](https://github.com/tootsuite/mastodon/pull/15430))
|
||||||
|
- Fix `tootctl emoji import` not treating shortcodes as case-insensitive ([angristan](https://github.com/tootsuite/mastodon/pull/15738))
|
||||||
|
- Fix some issues with SAML account creation ([Gargron](https://github.com/tootsuite/mastodon/pull/15222), [kaiyou](https://github.com/tootsuite/mastodon/pull/15511
|
||||||
|
- Fix instance actor not being automatically created if it wasn't seeded properly ([ClearlyClaire](https://github.com/tootsuite/mastodon/pull/15693))))
|
||||||
|
- Fix app name, website and redirect URIs not having a maximum length ([Gargron](https://github.com/tootsuite/mastodon/pull/16042))
|
||||||
|
- Fix some ActivityPub identifiers in server actor outbox ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16343))
|
||||||
|
- Fix custom CSS path setting cookies and being uncacheable due to it ([tribela](https://github.com/mastodon/mastodon/pull/16314))
|
||||||
|
- Fix some redis locks auto-releasing too fast ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16276), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16291))
|
||||||
|
- Fix migration script not being able to run if it fails midway ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16312))
|
||||||
|
- Fix PWA not being usable from alternate domains ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16714))
|
||||||
|
- Fix scheduling a status decreasing status count ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16791))
|
||||||
|
- Fix followers synchronization mechanism not working when URI has empty path ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16744))
|
||||||
|
- Fix serialization of counts in REST API when user hides their network ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16418))
|
||||||
|
- Fix `tootctl self-destruct` not sending delete activities for recently-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16688))
|
||||||
|
- Fix `mastodon:setup` generated env-file syntax ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16896), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16976))
|
||||||
|
- Fix link previews being incorrectly generated from earlier links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16885))
|
||||||
|
- Fix wrong `to`/`cc` values for remote groups in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16700))
|
||||||
|
- Fix mentions with non-ascii TLDs not being processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16689))
|
||||||
|
- Fix authentication failures halfway through a sign-in attempt ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16792))
|
||||||
|
- Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628))
|
||||||
|
- Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598))
|
||||||
|
- Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583))
|
||||||
|
- Fix newlines being added to accout notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
|
||||||
|
- Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
|
||||||
|
- Fix confusing error when WebFinger request returns empty document ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16986))
|
||||||
|
- Fix upload of remote media with OpenStack Swift sometimes failing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16998))
|
||||||
|
- Fix Docker build ([tribela](https://github.com/mastodon/mastodon/pull/17188))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Fix user notes not having a length limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16942))
|
||||||
|
- Fix revoking a specific session not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
|
||||||
|
- Fix filtering DMs from non-followed users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17042))
|
||||||
|
|
||||||
## [3.3.0] - 2020-12-27
|
## [3.3.0] - 2020-12-27
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
|
@ -71,8 +71,8 @@ RUN npm install -g yarn && \
|
||||||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||||
|
|
||||||
RUN cd /opt/mastodon && \
|
RUN cd /opt/mastodon && \
|
||||||
bundle config set deployment 'true' && \
|
bundle config set --local deployment 'true' && \
|
||||||
bundle config set without 'development test' && \
|
bundle config set --local without 'development test' && \
|
||||||
bundle install -j$(nproc) && \
|
bundle install -j$(nproc) && \
|
||||||
yarn install --pure-lockfile
|
yarn install --pure-lockfile
|
||||||
|
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -21,8 +21,6 @@ gem 'aws-sdk-s3', '~> 1.85', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'paperclip', '~> 6.0'
|
gem 'paperclip', '~> 6.0'
|
||||||
gem 'paperclip-av-transcoder', '~> 0.6'
|
|
||||||
gem 'streamio-ffmpeg', '~> 3.0'
|
|
||||||
gem 'blurhash', '~> 0.1'
|
gem 'blurhash', '~> 0.1'
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
|
|
25
Gemfile.lock
25
Gemfile.lock
|
@ -75,8 +75,6 @@ GEM
|
||||||
ast (2.4.1)
|
ast (2.4.1)
|
||||||
attr_encrypted (3.1.0)
|
attr_encrypted (3.1.0)
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
av (0.9.0)
|
|
||||||
cocaine (~> 0.5.3)
|
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.397.0)
|
aws-partitions (1.397.0)
|
||||||
|
@ -151,8 +149,6 @@ GEM
|
||||||
cld3 (3.3.0)
|
cld3 (3.3.0)
|
||||||
ffi (>= 1.1.0, < 1.12.0)
|
ffi (>= 1.1.0, < 1.12.0)
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
cocaine (0.5.8)
|
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
color_diff (0.1)
|
color_diff (0.1)
|
||||||
concurrent-ruby (1.1.7)
|
concurrent-ruby (1.1.7)
|
||||||
|
@ -346,9 +342,11 @@ GEM
|
||||||
mime-types (3.3.1)
|
mime-types (3.3.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2020.0512)
|
mime-types-data (3.2020.0512)
|
||||||
mimemagic (0.3.5)
|
mimemagic (0.3.10)
|
||||||
|
nokogiri (~> 1)
|
||||||
|
rake
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.7.1)
|
||||||
minitest (5.14.2)
|
minitest (5.14.2)
|
||||||
msgpack (1.3.3)
|
msgpack (1.3.3)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
|
@ -358,9 +356,10 @@ GEM
|
||||||
net-ssh (>= 2.6.5, < 7.0.0)
|
net-ssh (>= 2.6.5, < 7.0.0)
|
||||||
net-ssh (6.1.0)
|
net-ssh (6.1.0)
|
||||||
nio4r (2.5.4)
|
nio4r (2.5.4)
|
||||||
nokogiri (1.10.10)
|
nokogiri (1.13.1)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.7.0)
|
||||||
nokogumbo (2.0.2)
|
racc (~> 1.4)
|
||||||
|
nokogumbo (2.0.5)
|
||||||
nokogiri (~> 1.8, >= 1.8.4)
|
nokogiri (~> 1.8, >= 1.8.4)
|
||||||
nsa (0.2.7)
|
nsa (0.2.7)
|
||||||
activesupport (>= 4.2, < 6)
|
activesupport (>= 4.2, < 6)
|
||||||
|
@ -391,9 +390,6 @@ GEM
|
||||||
mime-types
|
mime-types
|
||||||
mimemagic (~> 0.3.0)
|
mimemagic (~> 0.3.0)
|
||||||
terrapin (~> 0.6.0)
|
terrapin (~> 0.6.0)
|
||||||
paperclip-av-transcoder (0.6.4)
|
|
||||||
av (~> 0.9.0)
|
|
||||||
paperclip (>= 2.5.2)
|
|
||||||
parallel (1.20.1)
|
parallel (1.20.1)
|
||||||
parallel_tests (3.4.0)
|
parallel_tests (3.4.0)
|
||||||
parallel
|
parallel
|
||||||
|
@ -432,6 +428,7 @@ GEM
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.3.3)
|
raabro (1.3.3)
|
||||||
|
racc (1.6.0)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-attack (6.3.1)
|
rack-attack (6.3.1)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
|
@ -605,8 +602,6 @@ GEM
|
||||||
stackprof (0.2.16)
|
stackprof (0.2.16)
|
||||||
statsd-ruby (1.4.0)
|
statsd-ruby (1.4.0)
|
||||||
stoplight (2.2.1)
|
stoplight (2.2.1)
|
||||||
streamio-ffmpeg (3.0.2)
|
|
||||||
multi_json (~> 1.8)
|
|
||||||
strong_migrations (0.7.2)
|
strong_migrations (0.7.2)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
temple (0.8.2)
|
temple (0.8.2)
|
||||||
|
@ -751,7 +746,6 @@ DEPENDENCIES
|
||||||
omniauth-saml (~> 1.10)
|
omniauth-saml (~> 1.10)
|
||||||
ox (~> 2.13)
|
ox (~> 2.13)
|
||||||
paperclip (~> 6.0)
|
paperclip (~> 6.0)
|
||||||
paperclip-av-transcoder (~> 0.6)
|
|
||||||
parallel (~> 1.20)
|
parallel (~> 1.20)
|
||||||
parallel_tests (~> 3.4)
|
parallel_tests (~> 3.4)
|
||||||
parslet
|
parslet
|
||||||
|
@ -797,7 +791,6 @@ DEPENDENCIES
|
||||||
sprockets-rails (~> 3.2)
|
sprockets-rails (~> 3.2)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 2.2.1)
|
stoplight (~> 2.2.1)
|
||||||
streamio-ffmpeg (~> 3.0)
|
|
||||||
strong_migrations (~> 0.7)
|
strong_migrations (~> 0.7)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
tty-prompt (~> 0.22)
|
tty-prompt (~> 0.22)
|
||||||
|
|
|
@ -19,11 +19,11 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
|
||||||
private
|
private
|
||||||
|
|
||||||
def uri_prefix
|
def uri_prefix
|
||||||
signed_request_account.uri[/http(s?):\/\/[^\/]+\//]
|
signed_request_account.uri[Account::URL_PREFIX_RE]
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_items
|
def set_items
|
||||||
@items = @account.followers.where(Account.arel_table[:uri].matches(uri_prefix + '%', false, true)).pluck(:uri)
|
@items = @account.followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(uri_prefix)}/%", false, true)).or(@account.followers.where(uri: uri_prefix)).pluck(:uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
def collection_presenter
|
def collection_presenter
|
||||||
|
|
|
@ -5,7 +5,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
include AccountOwnedConcern
|
include AccountOwnedConcern
|
||||||
|
|
||||||
before_action :skip_unknown_actor_delete
|
before_action :skip_unknown_actor_activity
|
||||||
before_action :require_signature!
|
before_action :require_signature!
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
|
|
||||||
|
@ -18,13 +18,13 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def skip_unknown_actor_delete
|
def skip_unknown_actor_activity
|
||||||
head 202 if unknown_deleted_account?
|
head 202 if unknown_affected_account?
|
||||||
end
|
end
|
||||||
|
|
||||||
def unknown_deleted_account?
|
def unknown_affected_account?
|
||||||
json = Oj.load(body, mode: :strict)
|
json = Oj.load(body, mode: :strict)
|
||||||
json.is_a?(Hash) && json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
|
json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
|
||||||
rescue Oj::ParseError
|
rescue Oj::ParseError
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ActivityPub::CollectionPresenter.new(
|
ActivityPub::CollectionPresenter.new(
|
||||||
id: account_outbox_url(@account),
|
id: outbox_url,
|
||||||
type: :ordered,
|
type: :ordered,
|
||||||
size: @account.statuses_count,
|
size: @account.statuses_count,
|
||||||
first: outbox_url(page: true),
|
first: outbox_url(page: true),
|
||||||
|
@ -47,11 +47,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def next_page
|
def next_page
|
||||||
account_outbox_url(@account, page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
|
outbox_url(page: true, max_id: @statuses.last.id) if @statuses.size == LIMIT
|
||||||
end
|
end
|
||||||
|
|
||||||
def prev_page
|
def prev_page
|
||||||
account_outbox_url(@account, page: true, min_id: @statuses.first.id) unless @statuses.empty?
|
outbox_url(page: true, min_id: @statuses.first.id) unless @statuses.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_statuses
|
def set_statuses
|
||||||
|
|
|
@ -42,7 +42,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def mute
|
def mute
|
||||||
MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration] || 0))
|
MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration]&.to_i || 0))
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -20,17 +20,16 @@ class ApplicationController < ActionController::Base
|
||||||
helper_method :use_seamless_external_login?
|
helper_method :use_seamless_external_login?
|
||||||
helper_method :whitelist_mode?
|
helper_method :whitelist_mode?
|
||||||
|
|
||||||
rescue_from ActionController::RoutingError, with: :not_found
|
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
|
||||||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
|
||||||
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
|
||||||
rescue_from ActionController::ParameterMissing, with: :bad_request
|
|
||||||
rescue_from Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
|
|
||||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
|
||||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||||
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found
|
||||||
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight, with: :service_unavailable
|
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
||||||
|
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
||||||
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
||||||
|
|
||||||
|
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||||
|
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable
|
||||||
|
|
||||||
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
||||||
before_action :require_functional!, if: :user_signed_in?
|
before_action :require_functional!, if: :user_signed_in?
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ class Auth::PasswordsController < Devise::PasswordsController
|
||||||
super do |resource|
|
super do |resource|
|
||||||
if resource.errors.empty?
|
if resource.errors.empty?
|
||||||
resource.session_activations.destroy_all
|
resource.session_activations.destroy_all
|
||||||
resource.forget_me!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::RegistrationsController < Devise::RegistrationsController
|
class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
include Devise::Controllers::Rememberable
|
|
||||||
include RegistrationSpamConcern
|
include RegistrationSpamConcern
|
||||||
|
|
||||||
layout :determine_layout
|
layout :determine_layout
|
||||||
|
@ -30,8 +29,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
super do |resource|
|
super do |resource|
|
||||||
if resource.saved_change_to_encrypted_password?
|
if resource.saved_change_to_encrypted_password?
|
||||||
resource.clear_other_sessions(current_session.session_id)
|
resource.clear_other_sessions(current_session.session_id)
|
||||||
resource.forget_me!
|
|
||||||
remember_me(resource)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::SessionsController < Devise::SessionsController
|
class Auth::SessionsController < Devise::SessionsController
|
||||||
include Devise::Controllers::Rememberable
|
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
skip_before_action :require_no_authentication, only: [:create]
|
skip_before_action :require_no_authentication, only: [:create]
|
||||||
|
@ -26,7 +24,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
def create
|
def create
|
||||||
super do |resource|
|
super do |resource|
|
||||||
resource.update_sign_in!(request, new_sign_in: true)
|
resource.update_sign_in!(request, new_sign_in: true)
|
||||||
remember_me(resource)
|
|
||||||
flash.delete(:notice)
|
flash.delete(:notice)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -40,7 +37,7 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def webauthn_options
|
def webauthn_options
|
||||||
user = find_user
|
user = User.find_by(id: session[:attempt_user_id])
|
||||||
|
|
||||||
if user.webauthn_enabled?
|
if user.webauthn_enabled?
|
||||||
options_for_get = WebAuthn::Credential.options_for_get(
|
options_for_get = WebAuthn::Credential.options_for_get(
|
||||||
|
@ -58,16 +55,20 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def find_user
|
def find_user
|
||||||
if session[:attempt_user_id]
|
if user_params[:email].present?
|
||||||
|
find_user_from_params
|
||||||
|
elsif session[:attempt_user_id]
|
||||||
User.find_by(id: session[:attempt_user_id])
|
User.find_by(id: session[:attempt_user_id])
|
||||||
else
|
|
||||||
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
|
|
||||||
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
|
|
||||||
user ||= User.find_for_authentication(email: user_params[:email])
|
|
||||||
user
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_user_from_params
|
||||||
|
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
|
||||||
|
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
|
||||||
|
user ||= User.find_for_authentication(email: user_params[:email])
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {})
|
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {})
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,21 +16,24 @@ module SignInTokenAuthenticationConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_with_sign_in_token
|
def authenticate_with_sign_in_token
|
||||||
user = self.resource = find_user
|
if user_params[:email].present?
|
||||||
|
user = self.resource = find_user_from_params
|
||||||
|
prompt_for_sign_in_token(user) if user&.external_or_valid_password?(user_params[:password])
|
||||||
|
elsif session[:attempt_user_id]
|
||||||
|
user = self.resource = User.find_by(id: session[:attempt_user_id])
|
||||||
|
return if user.nil?
|
||||||
|
|
||||||
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
|
if session[:attempt_user_updated_at] != user.updated_at.to_s
|
||||||
restart_session
|
restart_session
|
||||||
elsif user_params.key?(:sign_in_token_attempt) && session[:attempt_user_id]
|
elsif user_params.key?(:sign_in_token_attempt)
|
||||||
authenticate_with_sign_in_token_attempt(user)
|
authenticate_with_sign_in_token_attempt(user)
|
||||||
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
end
|
||||||
prompt_for_sign_in_token(user)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_with_sign_in_token_attempt(user)
|
def authenticate_with_sign_in_token_attempt(user)
|
||||||
if valid_sign_in_token_attempt?(user)
|
if valid_sign_in_token_attempt?(user)
|
||||||
clear_attempt_from_session
|
clear_attempt_from_session
|
||||||
remember_me(user)
|
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
else
|
else
|
||||||
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
|
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
|
||||||
|
|
|
@ -133,6 +133,7 @@ module SignatureVerification
|
||||||
|
|
||||||
def verify_body_digest!
|
def verify_body_digest!
|
||||||
return unless signed_headers.include?('digest')
|
return unless signed_headers.include?('digest')
|
||||||
|
raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest')
|
||||||
|
|
||||||
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
|
digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] }
|
||||||
sha256 = digests.assoc('sha-256')
|
sha256 = digests.assoc('sha-256')
|
||||||
|
|
|
@ -35,16 +35,20 @@ module TwoFactorAuthenticationConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_with_two_factor
|
def authenticate_with_two_factor
|
||||||
user = self.resource = find_user
|
if user_params[:email].present?
|
||||||
|
user = self.resource = find_user_from_params
|
||||||
|
prompt_for_two_factor(user) if user&.external_or_valid_password?(user_params[:password])
|
||||||
|
elsif session[:attempt_user_id]
|
||||||
|
user = self.resource = User.find_by(id: session[:attempt_user_id])
|
||||||
|
return if user.nil?
|
||||||
|
|
||||||
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
|
if session[:attempt_user_updated_at] != user.updated_at.to_s
|
||||||
restart_session
|
restart_session
|
||||||
elsif user.webauthn_enabled? && user_params.key?(:credential) && session[:attempt_user_id]
|
elsif user.webauthn_enabled? && user_params.key?(:credential)
|
||||||
authenticate_with_two_factor_via_webauthn(user)
|
authenticate_with_two_factor_via_webauthn(user)
|
||||||
elsif user_params.key?(:otp_attempt) && session[:attempt_user_id]
|
elsif user_params.key?(:otp_attempt)
|
||||||
authenticate_with_two_factor_via_otp(user)
|
authenticate_with_two_factor_via_otp(user)
|
||||||
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
end
|
||||||
prompt_for_two_factor(user)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -53,7 +57,6 @@ module TwoFactorAuthenticationConcern
|
||||||
|
|
||||||
if valid_webauthn_credential?(user, webauthn_credential)
|
if valid_webauthn_credential?(user, webauthn_credential)
|
||||||
clear_attempt_from_session
|
clear_attempt_from_session
|
||||||
remember_me(user)
|
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
render json: { redirect_path: root_path }, status: :ok
|
render json: { redirect_path: root_path }, status: :ok
|
||||||
else
|
else
|
||||||
|
@ -64,7 +67,6 @@ module TwoFactorAuthenticationConcern
|
||||||
def authenticate_with_two_factor_via_otp(user)
|
def authenticate_with_two_factor_via_otp(user)
|
||||||
if valid_otp_attempt?(user)
|
if valid_otp_attempt?(user)
|
||||||
clear_attempt_from_session
|
clear_attempt_from_session
|
||||||
remember_me(user)
|
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
else
|
else
|
||||||
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
||||||
|
|
|
@ -3,11 +3,16 @@
|
||||||
class CustomCssController < ApplicationController
|
class CustomCssController < ApplicationController
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
skip_before_action :update_user_sign_in
|
||||||
|
skip_before_action :set_session_activity
|
||||||
|
|
||||||
|
skip_around_action :set_locale
|
||||||
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
expires_in 3.minutes, public: true
|
expires_in 3.minutes, public: true
|
||||||
|
request.session_options[:skip] = true
|
||||||
render plain: Setting.custom_css || '', content_type: 'text/css'
|
render plain: Setting.custom_css || '', content_type: 'text/css'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,7 +85,7 @@ class FollowerAccountsController < ApplicationController
|
||||||
if page_requested? || !@account.user_hides_network?
|
if page_requested? || !@account.user_hides_network?
|
||||||
# Return all fields
|
# Return all fields
|
||||||
else
|
else
|
||||||
%i(id type totalItems)
|
%i(id type total_items)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,7 +85,7 @@ class FollowingAccountsController < ApplicationController
|
||||||
if page_requested? || !@account.user_hides_network?
|
if page_requested? || !@account.user_hides_network?
|
||||||
# Return all fields
|
# Return all fields
|
||||||
else
|
else
|
||||||
%i(id type totalItems)
|
%i(id type total_items)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ class InstanceActorsController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find(-99)
|
@account = Account.representative
|
||||||
end
|
end
|
||||||
|
|
||||||
def restrict_fields_to
|
def restrict_fields_to
|
||||||
|
|
|
@ -45,7 +45,7 @@ class MediaProxyController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "media_download:#{params[:id]}" }
|
{ redis: Redis.current, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds }
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject_media?
|
def reject_media?
|
||||||
|
|
|
@ -7,8 +7,12 @@ module Settings
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if valid_picture?
|
if valid_picture?
|
||||||
msg = I18n.t('generic.changes_saved_msg') if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
|
if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
|
||||||
redirect_to settings_profile_path, notice: msg, status: 303
|
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||||
|
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
|
||||||
|
else
|
||||||
|
redirect_to settings_profile_path
|
||||||
|
end
|
||||||
else
|
else
|
||||||
bad_request
|
bad_request
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ class StatusesController < ApplicationController
|
||||||
|
|
||||||
layout 'public'
|
layout 'public'
|
||||||
|
|
||||||
before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_status
|
before_action :set_status
|
||||||
before_action :set_instance_presenter
|
before_action :set_instance_presenter
|
||||||
before_action :set_link_headers
|
before_action :set_link_headers
|
||||||
|
|
55
app/helpers/context_helper.rb
Normal file
55
app/helpers/context_helper.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ContextHelper
|
||||||
|
NAMED_CONTEXT_MAP = {
|
||||||
|
activitystreams: 'https://www.w3.org/ns/activitystreams',
|
||||||
|
security: 'https://w3id.org/security/v1',
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
CONTEXT_EXTENSION_MAP = {
|
||||||
|
manually_approves_followers: { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' },
|
||||||
|
sensitive: { 'sensitive' => 'as:sensitive' },
|
||||||
|
hashtag: { 'Hashtag' => 'as:Hashtag' },
|
||||||
|
moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } },
|
||||||
|
also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } },
|
||||||
|
emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' },
|
||||||
|
featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' }, 'featuredTags' => { '@id' => 'toot:featuredTags', '@type' => '@id' } },
|
||||||
|
property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' },
|
||||||
|
atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' },
|
||||||
|
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
|
||||||
|
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
|
||||||
|
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
|
||||||
|
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||||
|
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
||||||
|
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||||
|
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
|
||||||
|
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def full_context
|
||||||
|
serialized_context(NAMED_CONTEXT_MAP, CONTEXT_EXTENSION_MAP)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialized_context(named_contexts_map, context_extensions_map)
|
||||||
|
context_array = []
|
||||||
|
|
||||||
|
named_contexts = named_contexts_map.keys
|
||||||
|
context_extensions = context_extensions_map.keys
|
||||||
|
|
||||||
|
named_contexts.each do |key|
|
||||||
|
context_array << NAMED_CONTEXT_MAP[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
extensions = context_extensions.each_with_object({}) do |key, h|
|
||||||
|
h.merge!(CONTEXT_EXTENSION_MAP[key])
|
||||||
|
end
|
||||||
|
|
||||||
|
context_array << extensions unless extensions.empty?
|
||||||
|
|
||||||
|
if context_array.size == 1
|
||||||
|
context_array.first
|
||||||
|
else
|
||||||
|
context_array
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module JsonLdHelper
|
module JsonLdHelper
|
||||||
|
include ContextHelper
|
||||||
|
|
||||||
def equals_or_includes?(haystack, needle)
|
def equals_or_includes?(haystack, needle)
|
||||||
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
|
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
|
||||||
end
|
end
|
||||||
|
@ -63,6 +65,84 @@ module JsonLdHelper
|
||||||
graph.dump(:normalize)
|
graph.dump(:normalize)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def compact(json)
|
||||||
|
compacted = JSON::LD::API.compact(json.without('signature'), full_context, documentLoader: method(:load_jsonld_context))
|
||||||
|
compacted['signature'] = json['signature']
|
||||||
|
compacted
|
||||||
|
end
|
||||||
|
|
||||||
|
# Patches a JSON-LD document to avoid compatibility issues on redistribution
|
||||||
|
#
|
||||||
|
# Since compacting a JSON-LD document against Mastodon's built-in vocabulary
|
||||||
|
# means other extension namespaces will be expanded, malformed JSON-LD
|
||||||
|
# attributes lost, and some values “unexpectedly” compacted this method
|
||||||
|
# patches the following likely sources of incompatibility:
|
||||||
|
# - 'https://www.w3.org/ns/activitystreams#Public' being compacted to
|
||||||
|
# 'as:Public' (for instance, pre-3.4.0 Mastodon does not understand
|
||||||
|
# 'as:Public')
|
||||||
|
# - single-item arrays being compacted to the item itself (`[foo]` being
|
||||||
|
# compacted to `foo`)
|
||||||
|
#
|
||||||
|
# It is not always possible for `patch_for_forwarding!` to produce a document
|
||||||
|
# deemed safe for forwarding. Use `safe_for_forwarding?` to check the status
|
||||||
|
# of the output document.
|
||||||
|
#
|
||||||
|
# @param original [Hash] The original JSON-LD document used as reference
|
||||||
|
# @param compacted [Hash] The compacted JSON-LD document to be patched
|
||||||
|
# @return [void]
|
||||||
|
def patch_for_forwarding!(original, compacted)
|
||||||
|
original.without('@context', 'signature').each do |key, value|
|
||||||
|
next if value.nil? || !compacted.key?(key)
|
||||||
|
|
||||||
|
compacted_value = compacted[key]
|
||||||
|
if value.is_a?(Hash) && compacted_value.is_a?(Hash)
|
||||||
|
patch_for_forwarding!(value, compacted_value)
|
||||||
|
elsif value.is_a?(Array)
|
||||||
|
compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
|
||||||
|
return if value.size != compacted_value.size
|
||||||
|
|
||||||
|
compacted[key] = value.zip(compacted_value).map do |v, vc|
|
||||||
|
if v.is_a?(Hash) && vc.is_a?(Hash)
|
||||||
|
patch_for_forwarding!(v, vc)
|
||||||
|
vc
|
||||||
|
elsif v == 'https://www.w3.org/ns/activitystreams#Public' && vc == 'as:Public'
|
||||||
|
v
|
||||||
|
else
|
||||||
|
vc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elsif value == 'https://www.w3.org/ns/activitystreams#Public' && compacted_value == 'as:Public'
|
||||||
|
compacted[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tests whether a JSON-LD compaction is deemed safe for redistribution,
|
||||||
|
# that is, if it doesn't change its meaning to consumers that do not actually
|
||||||
|
# handle JSON-LD, but rely on values being serialized in a certain way.
|
||||||
|
#
|
||||||
|
# See `patch_for_forwarding!` for details.
|
||||||
|
#
|
||||||
|
# @param original [Hash] The original JSON-LD document used as reference
|
||||||
|
# @param compacted [Hash] The compacted JSON-LD document to be patched
|
||||||
|
# @return [Boolean] Whether the patched document is deemed safe
|
||||||
|
def safe_for_forwarding?(original, compacted)
|
||||||
|
original.without('@context', 'signature').all? do |key, value|
|
||||||
|
compacted_value = compacted[key]
|
||||||
|
return false unless value.class == compacted_value.class
|
||||||
|
|
||||||
|
if value.is_a?(Hash)
|
||||||
|
safe_for_forwarding?(value, compacted_value)
|
||||||
|
elsif value.is_a?(Array)
|
||||||
|
value.zip(compacted_value).all? do |v, vc|
|
||||||
|
v.is_a?(Hash) ? (vc.is_a?(Hash) && safe_for_forwarding?(v, vc)) : v == vc
|
||||||
|
end
|
||||||
|
else
|
||||||
|
value == compacted_value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_resource(uri, id, on_behalf_of = nil)
|
def fetch_resource(uri, id, on_behalf_of = nil)
|
||||||
unless id
|
unless id
|
||||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||||
|
|
25
app/lib/account_reach_finder.rb
Normal file
25
app/lib/account_reach_finder.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountReachFinder
|
||||||
|
def initialize(account)
|
||||||
|
@account = account
|
||||||
|
end
|
||||||
|
|
||||||
|
def inboxes
|
||||||
|
(followers_inboxes + reporters_inboxes + relay_inboxes).uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def followers_inboxes
|
||||||
|
@account.followers.inboxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def reporters_inboxes
|
||||||
|
Account.where(id: @account.targeted_reports.select(:account_id)).inboxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def relay_inboxes
|
||||||
|
Relay.enabled.pluck(:inbox_url)
|
||||||
|
end
|
||||||
|
end
|
|
@ -144,7 +144,7 @@ class ActivityPub::Activity
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_later!(uri)
|
def delete_later!(uri)
|
||||||
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
|
redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_from_object
|
def status_from_object
|
||||||
|
@ -210,12 +210,22 @@ class ActivityPub::Activity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_or_return(key, expire_after = 7.days.seconds)
|
def lock_or_return(key, expire_after = 2.hours.seconds)
|
||||||
yield if redis.set(key, true, nx: true, ex: expire_after)
|
yield if redis.set(key, true, nx: true, ex: expire_after)
|
||||||
ensure
|
ensure
|
||||||
redis.del(key)
|
redis.del(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lock_or_fail(key, expire_after = 15.minutes.seconds)
|
||||||
|
RedisLock.acquire({ redis: Redis.current, key: key, autorelease: expire_after }) do |lock|
|
||||||
|
if lock.acquired?
|
||||||
|
yield
|
||||||
|
else
|
||||||
|
raise Mastodon::RaceConditionError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fetch?
|
def fetch?
|
||||||
!@options[:delivery]
|
!@options[:delivery]
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ class ActivityPub::Activity::Add < ActivityPub::Activity
|
||||||
status = status_from_uri(object_uri)
|
status = status_from_uri(object_uri)
|
||||||
status ||= fetch_remote_original_status
|
status ||= fetch_remote_original_status
|
||||||
|
|
||||||
return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status)
|
return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status) && status.distributable?
|
||||||
|
|
||||||
StatusPin.create!(account: @account, status: status)
|
StatusPin.create!(account: @account, status: status)
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,29 +4,25 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
|
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
lock_or_fail("announce:#{@object['id']}") do
|
||||||
if lock.acquired?
|
original_status = status_from_object
|
||||||
original_status = status_from_object
|
|
||||||
|
|
||||||
return reject_payload! if original_status.nil? || !announceable?(original_status)
|
return reject_payload! if original_status.nil? || !announceable?(original_status)
|
||||||
|
|
||||||
@status = Status.find_by(account: @account, reblog: original_status)
|
@status = Status.find_by(account: @account, reblog: original_status)
|
||||||
|
|
||||||
return @status unless @status.nil?
|
return @status unless @status.nil?
|
||||||
|
|
||||||
@status = Status.create!(
|
@status = Status.create!(
|
||||||
account: @account,
|
account: @account,
|
||||||
reblog: original_status,
|
reblog: original_status,
|
||||||
uri: @json['id'],
|
uri: @json['id'],
|
||||||
created_at: @json['published'],
|
created_at: @json['published'],
|
||||||
override_timestamps: @options[:override_timestamps],
|
override_timestamps: @options[:override_timestamps],
|
||||||
visibility: visibility_from_audience
|
visibility: visibility_from_audience
|
||||||
)
|
)
|
||||||
|
|
||||||
distribute(@status)
|
distribute(@status)
|
||||||
else
|
|
||||||
raise Mastodon::RaceConditionError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@status
|
@status
|
||||||
|
@ -43,9 +39,9 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
end
|
end
|
||||||
|
|
||||||
def visibility_from_audience
|
def visibility_from_audience
|
||||||
if audience_to.include?(ActivityPub::TagManager::COLLECTIONS[:public])
|
if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) }
|
||||||
:public
|
:public
|
||||||
elsif audience_cc.include?(ActivityPub::TagManager::COLLECTIONS[:public])
|
elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) }
|
||||||
:unlisted
|
:unlisted
|
||||||
elsif audience_to.include?(@account.followers_url)
|
elsif audience_to.include?(@account.followers_url)
|
||||||
:private
|
:private
|
||||||
|
@ -69,8 +65,4 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||||
def reblog_of_local_status?
|
def reblog_of_local_status?
|
||||||
status_from_uri(object_uri)&.account&.local?
|
status_from_uri(object_uri)&.account&.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
|
||||||
{ redis: Redis.current, key: "announce:#{@object['id']}" }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,8 +11,13 @@ class ActivityPub::Activity::Block < ActivityPub::Activity
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
UnfollowService.new.call(@account, target_account) if @account.following?(target_account)
|
||||||
UnfollowService.new.call(target_account, @account) if target_account.following?(@account)
|
UnfollowService.new.call(target_account, @account) if target_account.following?(@account)
|
||||||
|
RejectFollowService.new.call(target_account, @account) if target_account.requested?(@account)
|
||||||
|
|
||||||
@account.block!(target_account, uri: @json['id']) unless delete_arrived_first?(@json['id'])
|
unless delete_arrived_first?(@json['id'])
|
||||||
|
BlockWorker.perform_async(@account.id, target_account.id)
|
||||||
|
@account.block!(target_account, uri: @json['id'])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,19 +45,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
def create_status
|
def create_status
|
||||||
return reject_payload! if unsupported_object_type? || invalid_origin?(object_uri) || tombstone_exists? || !related_to_local_activity?
|
return reject_payload! if unsupported_object_type? || invalid_origin?(object_uri) || tombstone_exists? || !related_to_local_activity?
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
lock_or_fail("create:#{object_uri}") do
|
||||||
if lock.acquired?
|
return if delete_arrived_first?(object_uri) || poll_vote? # rubocop:disable Lint/NonLocalExitFromIterator
|
||||||
return if delete_arrived_first?(object_uri) || poll_vote? # rubocop:disable Lint/NonLocalExitFromIterator
|
|
||||||
|
|
||||||
@status = find_existing_status
|
@status = find_existing_status
|
||||||
|
|
||||||
if @status.nil?
|
if @status.nil?
|
||||||
process_status
|
process_status
|
||||||
elsif @options[:delivered_to_account_id].present?
|
elsif @options[:delivered_to_account_id].present?
|
||||||
postprocess_audience_and_deliver
|
postprocess_audience_and_deliver
|
||||||
end
|
|
||||||
else
|
|
||||||
raise Mastodon::RaceConditionError
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -123,7 +119,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
|
|
||||||
def process_audience
|
def process_audience
|
||||||
(audience_to + audience_cc).uniq.each do |audience|
|
(audience_to + audience_cc).uniq.each do |audience|
|
||||||
next if audience == ActivityPub::TagManager::COLLECTIONS[:public]
|
next if ActivityPub::TagManager.instance.public_collection?(audience)
|
||||||
|
|
||||||
# Unlike with tags, there is no point in resolving accounts we don't already
|
# Unlike with tags, there is no point in resolving accounts we don't already
|
||||||
# know here, because silent mentions would only be used for local access
|
# know here, because silent mentions would only be used for local access
|
||||||
|
@ -314,13 +310,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
poll = replied_to_status.preloadable_poll
|
poll = replied_to_status.preloadable_poll
|
||||||
already_voted = true
|
already_voted = true
|
||||||
|
|
||||||
RedisLock.acquire(poll_lock_options) do |lock|
|
lock_or_fail("vote:#{replied_to_status.poll_id}:#{@account.id}") do
|
||||||
if lock.acquired?
|
already_voted = poll.votes.where(account: @account).exists?
|
||||||
already_voted = poll.votes.where(account: @account).exists?
|
poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri)
|
||||||
poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri)
|
|
||||||
else
|
|
||||||
raise Mastodon::RaceConditionError
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
increment_voters_count! unless already_voted
|
increment_voters_count! unless already_voted
|
||||||
|
@ -356,9 +348,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
end
|
end
|
||||||
|
|
||||||
def visibility_from_audience
|
def visibility_from_audience
|
||||||
if audience_to.include?(ActivityPub::TagManager::COLLECTIONS[:public])
|
if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) }
|
||||||
:public
|
:public
|
||||||
elsif audience_cc.include?(ActivityPub::TagManager::COLLECTIONS[:public])
|
elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) }
|
||||||
:unlisted
|
:unlisted
|
||||||
elsif audience_to.include?(@account.followers_url)
|
elsif audience_to.include?(@account.followers_url)
|
||||||
:private
|
:private
|
||||||
|
@ -455,10 +447,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
end
|
end
|
||||||
|
|
||||||
def supported_blurhash?(blurhash)
|
def supported_blurhash?(blurhash)
|
||||||
components = blurhash.blank? ? nil : Blurhash.components(blurhash)
|
components = blurhash.blank? || !blurhash_valid_chars?(blurhash) ? nil : Blurhash.components(blurhash)
|
||||||
components.present? && components.none? { |comp| comp > 5 }
|
components.present? && components.none? { |comp| comp > 5 }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def blurhash_valid_chars?(blurhash)
|
||||||
|
/^[\w#$%*+-.:;=?@\[\]^{|}~]+$/.match?(blurhash)
|
||||||
|
end
|
||||||
|
|
||||||
def skip_download?
|
def skip_download?
|
||||||
return @skip_download if defined?(@skip_download)
|
return @skip_download if defined?(@skip_download)
|
||||||
|
|
||||||
|
@ -513,12 +509,4 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
poll.reload
|
poll.reload
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
|
||||||
{ redis: Redis.current, key: "create:#{object_uri}" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def poll_lock_options
|
|
||||||
{ redis: Redis.current, key: "vote:#{replied_to_status.poll_id}:#{@account.id}" }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,33 +20,35 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
def delete_note
|
def delete_note
|
||||||
return if object_uri.nil?
|
return if object_uri.nil?
|
||||||
|
|
||||||
unless invalid_origin?(object_uri)
|
lock_or_return("delete_status_in_progress:#{object_uri}", 5.minutes.seconds) do
|
||||||
RedisLock.acquire(lock_options) { |_lock| delete_later!(object_uri) }
|
unless invalid_origin?(object_uri)
|
||||||
Tombstone.find_or_create_by(uri: object_uri, account: @account)
|
# This lock ensures a concurrent `ActivityPub::Activity::Create` either
|
||||||
|
# does not create a status at all, or has finished saving it to the
|
||||||
|
# database before we try to load it.
|
||||||
|
# Without the lock, `delete_later!` could be called after `delete_arrived_first?`
|
||||||
|
# and `Status.find` before `Status.create!`
|
||||||
|
lock_or_fail("create:#{object_uri}") { delete_later!(object_uri) }
|
||||||
|
|
||||||
|
Tombstone.find_or_create_by(uri: object_uri, account: @account)
|
||||||
|
end
|
||||||
|
|
||||||
|
@status = Status.find_by(uri: object_uri, account: @account)
|
||||||
|
@status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||||
|
|
||||||
|
return if @status.nil?
|
||||||
|
|
||||||
|
forward! if @json['signature'].present? && @status.distributable?
|
||||||
|
delete_now!
|
||||||
end
|
end
|
||||||
|
|
||||||
@status = Status.find_by(uri: object_uri, account: @account)
|
|
||||||
@status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
|
||||||
|
|
||||||
return if @status.nil?
|
|
||||||
|
|
||||||
if @status.distributable?
|
|
||||||
forward_for_reply
|
|
||||||
forward_for_reblogs
|
|
||||||
end
|
|
||||||
|
|
||||||
delete_now!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def forward_for_reblogs
|
def rebloggers_ids
|
||||||
return if @json['signature'].blank?
|
return @rebloggers_ids if defined?(@rebloggers_ids)
|
||||||
|
@rebloggers_ids = @status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)
|
||||||
|
end
|
||||||
|
|
||||||
rebloggers_ids = @status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)
|
def inboxes_for_reblogs
|
||||||
inboxes = Account.where(id: ::Follow.where(target_account_id: rebloggers_ids).select(:account_id)).inboxes - [@account.preferred_inbox_url]
|
Account.where(id: ::Follow.where(target_account_id: rebloggers_ids).select(:account_id)).inboxes
|
||||||
|
|
||||||
ActivityPub::LowPriorityDeliveryWorker.push_bulk(inboxes) do |inbox_url|
|
|
||||||
[payload, rebloggers_ids.first, inbox_url]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def replied_to_status
|
def replied_to_status
|
||||||
|
@ -58,13 +60,19 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
!replied_to_status.nil? && replied_to_status.account.local?
|
!replied_to_status.nil? && replied_to_status.account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def forward_for_reply
|
def inboxes_for_reply
|
||||||
return unless @json['signature'].present? && reply_to_local?
|
replied_to_status.account.followers.inboxes
|
||||||
|
end
|
||||||
|
|
||||||
inboxes = replied_to_status.account.followers.inboxes - [@account.preferred_inbox_url]
|
def forward!
|
||||||
|
inboxes = inboxes_for_reblogs
|
||||||
|
inboxes += inboxes_for_reply if reply_to_local?
|
||||||
|
inboxes -= [@account.preferred_inbox_url]
|
||||||
|
|
||||||
ActivityPub::LowPriorityDeliveryWorker.push_bulk(inboxes) do |inbox_url|
|
sender_id = reply_to_local? ? replied_to_status.account_id : rebloggers_ids.first
|
||||||
[payload, replied_to_status.account_id, inbox_url]
|
|
||||||
|
ActivityPub::LowPriorityDeliveryWorker.push_bulk(inboxes.uniq) do |inbox_url|
|
||||||
|
[payload, sender_id, inbox_url]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -75,8 +83,4 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||||
def payload
|
def payload
|
||||||
@payload ||= Oj.dump(@json)
|
@payload ||= Oj.dump(@json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
|
||||||
{ redis: Redis.current, key: "create:#{object_uri}" }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,8 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
|
||||||
target_accounts.each do |target_account|
|
target_accounts.each do |target_account|
|
||||||
target_statuses = target_statuses_by_account[target_account.id]
|
target_statuses = target_statuses_by_account[target_account.id]
|
||||||
|
|
||||||
|
next if target_account.suspended?
|
||||||
|
|
||||||
ReportService.new.call(
|
ReportService.new.call(
|
||||||
@account,
|
@account,
|
||||||
target_account,
|
target_account,
|
||||||
|
|
|
@ -6,7 +6,14 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
target_account = account_from_uri(object_uri)
|
target_account = account_from_uri(object_uri)
|
||||||
|
|
||||||
return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account)
|
return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id'])
|
||||||
|
|
||||||
|
# Update id of already-existing follow requests
|
||||||
|
existing_follow_request = ::FollowRequest.find_by(account: @account, target_account: target_account)
|
||||||
|
unless existing_follow_request.nil?
|
||||||
|
existing_follow_request.update!(uri: @json['id'])
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor?
|
if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor?
|
||||||
reject_follow_request!(target_account)
|
reject_follow_request!(target_account)
|
||||||
|
@ -14,7 +21,9 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fast-forward repeat follow requests
|
# Fast-forward repeat follow requests
|
||||||
if @account.following?(target_account)
|
existing_follow = ::Follow.find_by(account: @account, target_account: target_account)
|
||||||
|
unless existing_follow.nil?
|
||||||
|
existing_follow.update!(uri: @json['id'])
|
||||||
AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true, follow_request_uri: @json['id'])
|
AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true, follow_request_uri: @json['id'])
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,9 +4,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity
|
||||||
PROCESSING_COOLDOWN = 7.days.seconds
|
PROCESSING_COOLDOWN = 7.days.seconds
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
return if origin_account.uri != object_uri || processed?
|
return if origin_account.uri != object_uri
|
||||||
|
return unless mark_as_processing!
|
||||||
mark_as_processing!
|
|
||||||
|
|
||||||
target_account = ActivityPub::FetchRemoteAccountService.new.call(target_uri)
|
target_account = ActivityPub::FetchRemoteAccountService.new.call(target_uri)
|
||||||
|
|
||||||
|
@ -35,12 +34,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity
|
||||||
value_or_id(@json['target'])
|
value_or_id(@json['target'])
|
||||||
end
|
end
|
||||||
|
|
||||||
def processed?
|
|
||||||
redis.exists?("move_in_progress:#{@account.id}")
|
|
||||||
end
|
|
||||||
|
|
||||||
def mark_as_processing!
|
def mark_as_processing!
|
||||||
redis.setex("move_in_progress:#{@account.id}", PROCESSING_COOLDOWN, true)
|
redis.set("move_in_progress:#{@account.id}", true, nx: true, ex: PROCESSING_COOLDOWN)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unmark_as_processing!
|
def unmark_as_processing!
|
||||||
|
|
|
@ -1,30 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
||||||
NAMED_CONTEXT_MAP = {
|
include ContextHelper
|
||||||
activitystreams: 'https://www.w3.org/ns/activitystreams',
|
|
||||||
security: 'https://w3id.org/security/v1',
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
CONTEXT_EXTENSION_MAP = {
|
|
||||||
manually_approves_followers: { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' },
|
|
||||||
sensitive: { 'sensitive' => 'as:sensitive' },
|
|
||||||
hashtag: { 'Hashtag' => 'as:Hashtag' },
|
|
||||||
moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } },
|
|
||||||
also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } },
|
|
||||||
emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' },
|
|
||||||
featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' }, 'featuredTags' => { '@id' => 'toot:featuredTags', '@type' => '@id' } },
|
|
||||||
property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' },
|
|
||||||
atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' },
|
|
||||||
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
|
|
||||||
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
|
|
||||||
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
|
|
||||||
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
|
||||||
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
|
||||||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
|
||||||
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
|
|
||||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
def self.default_key_transform
|
def self.default_key_transform
|
||||||
:camel_lower
|
:camel_lower
|
||||||
|
@ -35,7 +12,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def serializable_hash(options = nil)
|
def serializable_hash(options = nil)
|
||||||
named_contexts = {}
|
named_contexts = { activitystreams: NAMED_CONTEXT_MAP['activitystreams'] }
|
||||||
context_extensions = {}
|
context_extensions = {}
|
||||||
|
|
||||||
options = serialization_options(options)
|
options = serialization_options(options)
|
||||||
|
@ -45,29 +22,4 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
||||||
|
|
||||||
{ '@context' => serialized_context(named_contexts, context_extensions) }.merge(serialized_hash)
|
{ '@context' => serialized_context(named_contexts, context_extensions) }.merge(serialized_hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def serialized_context(named_contexts_map, context_extensions_map)
|
|
||||||
context_array = []
|
|
||||||
|
|
||||||
named_contexts = [:activitystreams] + named_contexts_map.keys
|
|
||||||
context_extensions = context_extensions_map.keys
|
|
||||||
|
|
||||||
named_contexts.each do |key|
|
|
||||||
context_array << NAMED_CONTEXT_MAP[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
extensions = context_extensions.each_with_object({}) do |key, h|
|
|
||||||
h.merge!(CONTEXT_EXTENSION_MAP[key])
|
|
||||||
end
|
|
||||||
|
|
||||||
context_array << extensions unless extensions.empty?
|
|
||||||
|
|
||||||
if context_array.size == 1
|
|
||||||
context_array.first
|
|
||||||
else
|
|
||||||
context_array
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,10 @@ class ActivityPub::TagManager
|
||||||
public: 'https://www.w3.org/ns/activitystreams#Public',
|
public: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
def public_collection?(uri)
|
||||||
|
uri == COLLECTIONS[:public] || uri == 'as:Public' || uri == 'Public'
|
||||||
|
end
|
||||||
|
|
||||||
def url_for(target)
|
def url_for(target)
|
||||||
return target.url if target.respond_to?(:local?) && !target.local?
|
return target.url if target.respond_to?(:local?) && !target.local?
|
||||||
|
|
||||||
|
@ -60,6 +64,10 @@ class ActivityPub::TagManager
|
||||||
account_status_replies_url(target.account, target, page_params)
|
account_status_replies_url(target.account, target, page_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def followers_uri_for(target)
|
||||||
|
target.local? ? account_followers_url(target) : target.followers_url.presence
|
||||||
|
end
|
||||||
|
|
||||||
# Primary audience of a status
|
# Primary audience of a status
|
||||||
# Public statuses go out to primarily the public collection
|
# Public statuses go out to primarily the public collection
|
||||||
# Unlisted and private statuses go out primarily to the followers collection
|
# Unlisted and private statuses go out primarily to the followers collection
|
||||||
|
@ -76,17 +84,17 @@ class ActivityPub::TagManager
|
||||||
account_ids = status.active_mentions.pluck(:account_id)
|
account_ids = status.active_mentions.pluck(:account_id)
|
||||||
to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
|
to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
|
||||||
result << uri_for(account)
|
result << uri_for(account)
|
||||||
result << account_followers_url(account) if account.group?
|
result << followers_uri_for(account) if account.group?
|
||||||
end
|
end
|
||||||
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
|
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
|
||||||
result << uri_for(request.account)
|
result << uri_for(request.account)
|
||||||
result << account_followers_url(request.account) if request.account.group?
|
result << followers_uri_for(request.account) if request.account.group?
|
||||||
end)
|
end).compact
|
||||||
else
|
else
|
||||||
status.active_mentions.each_with_object([]) do |mention, result|
|
status.active_mentions.each_with_object([]) do |mention, result|
|
||||||
result << uri_for(mention.account)
|
result << uri_for(mention.account)
|
||||||
result << account_followers_url(mention.account) if mention.account.group?
|
result << followers_uri_for(mention.account) if mention.account.group?
|
||||||
end
|
end.compact
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -114,17 +122,17 @@ class ActivityPub::TagManager
|
||||||
account_ids = status.active_mentions.pluck(:account_id)
|
account_ids = status.active_mentions.pluck(:account_id)
|
||||||
cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
|
cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
|
||||||
result << uri_for(account)
|
result << uri_for(account)
|
||||||
result << account_followers_url(account) if account.group?
|
result << followers_uri_for(account) if account.group?
|
||||||
end)
|
end.compact)
|
||||||
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
|
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
|
||||||
result << uri_for(request.account)
|
result << uri_for(request.account)
|
||||||
result << account_followers_url(request.account) if request.account.group?
|
result << followers_uri_for(request.account) if request.account.group?
|
||||||
end)
|
end.compact)
|
||||||
else
|
else
|
||||||
cc.concat(status.active_mentions.each_with_object([]) do |mention, result|
|
cc.concat(status.active_mentions.each_with_object([]) do |mention, result|
|
||||||
result << uri_for(mention.account)
|
result << uri_for(mention.account)
|
||||||
result << account_followers_url(mention.account) if mention.account.group?
|
result << followers_uri_for(mention.account) if mention.account.group?
|
||||||
end)
|
end.compact)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ module ApplicationExtension
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
validates :website, url: true, if: :website?
|
validates :name, length: { maximum: 60 }
|
||||||
|
validates :website, url: true, length: { maximum: 2_000 }, if: :website?
|
||||||
|
validates :redirect_uri, length: { maximum: 2_000 }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,11 @@ module Mastodon
|
||||||
class RateLimitExceededError < Error; end
|
class RateLimitExceededError < Error; end
|
||||||
|
|
||||||
class UnexpectedResponseError < Error
|
class UnexpectedResponseError < Error
|
||||||
|
attr_reader :response
|
||||||
|
|
||||||
def initialize(response = nil)
|
def initialize(response = nil)
|
||||||
|
@response = response
|
||||||
|
|
||||||
if response.respond_to? :uri
|
if response.respond_to? :uri
|
||||||
super("#{response.uri} returned code #{response.code}")
|
super("#{response.uri} returned code #{response.code}")
|
||||||
else
|
else
|
||||||
|
|
|
@ -194,6 +194,36 @@ class FeedManager
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Clear all statuses from or mentioning target_account from a list feed
|
||||||
|
# @param [List] list
|
||||||
|
# @param [Account] target_account
|
||||||
|
# @return [void]
|
||||||
|
def clear_from_list(list, target_account)
|
||||||
|
timeline_key = key(:list, list.id)
|
||||||
|
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
|
||||||
|
statuses = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a
|
||||||
|
reblogged_ids = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id)
|
||||||
|
with_mentions_ids = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id)
|
||||||
|
|
||||||
|
target_statuses = statuses.select do |status|
|
||||||
|
status.account_id == target_account.id || reblogged_ids.include?(status.reblog_of_id) || with_mentions_ids.include?(status.id) || with_mentions_ids.include?(status.reblog_of_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
target_statuses.each do |status|
|
||||||
|
unpush_from_list(list, status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clear all statuses from or mentioning target_account from an account's lists
|
||||||
|
# @param [Account] account
|
||||||
|
# @param [Account] target_account
|
||||||
|
# @return [void]
|
||||||
|
def clear_from_lists(account, target_account)
|
||||||
|
List.where(account: account).each do |list|
|
||||||
|
clear_from_list(list, target_account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Populate home feed of account from scratch
|
# Populate home feed of account from scratch
|
||||||
# @param [Account] account
|
# @param [Account] account
|
||||||
# @return [void]
|
# @return [void]
|
||||||
|
|
54
app/lib/video_metadata_extractor.rb
Normal file
54
app/lib/video_metadata_extractor.rb
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class VideoMetadataExtractor
|
||||||
|
attr_reader :duration, :bitrate, :video_codec, :audio_codec,
|
||||||
|
:colorspace, :width, :height, :frame_rate
|
||||||
|
|
||||||
|
def initialize(path)
|
||||||
|
@path = path
|
||||||
|
@metadata = Oj.load(ffmpeg_command_output, mode: :strict, symbol_keys: true)
|
||||||
|
|
||||||
|
parse_metadata
|
||||||
|
rescue Terrapin::ExitStatusError, Oj::ParseError
|
||||||
|
@invalid = true
|
||||||
|
rescue Terrapin::CommandNotFoundError
|
||||||
|
raise Paperclip::Errors::CommandNotFoundError, 'Could not run the `ffprobe` command. Please install ffmpeg.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
!@invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ffmpeg_command_output
|
||||||
|
command = Terrapin::CommandLine.new('ffprobe', '-i :path -print_format :format -show_format -show_streams -show_error -loglevel :loglevel')
|
||||||
|
command.run(path: @path, format: 'json', loglevel: 'fatal')
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_metadata
|
||||||
|
if @metadata.key?(:format)
|
||||||
|
@duration = @metadata[:format][:duration].to_f
|
||||||
|
@bitrate = @metadata[:format][:bit_rate].to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
if @metadata.key?(:streams)
|
||||||
|
video_streams = @metadata[:streams].select { |stream| stream[:codec_type] == 'video' }
|
||||||
|
audio_streams = @metadata[:streams].select { |stream| stream[:codec_type] == 'audio' }
|
||||||
|
|
||||||
|
if (video_stream = video_streams.first)
|
||||||
|
@video_codec = video_stream[:codec_name]
|
||||||
|
@colorspace = video_stream[:pix_fmt]
|
||||||
|
@width = video_stream[:width]
|
||||||
|
@height = video_stream[:height]
|
||||||
|
@frame_rate = video_stream[:avg_frame_rate] == '0/0' ? nil : Rational(video_stream[:avg_frame_rate])
|
||||||
|
end
|
||||||
|
|
||||||
|
if (audio_stream = audio_streams.first)
|
||||||
|
@audio_codec = audio_stream[:codec_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@invalid = true if @metadata.key?(:error)
|
||||||
|
end
|
||||||
|
end
|
|
@ -46,7 +46,9 @@ class Webfinger
|
||||||
def body_from_webfinger(url = standard_url, use_fallback = true)
|
def body_from_webfinger(url = standard_url, use_fallback = true)
|
||||||
webfinger_request(url).perform do |res|
|
webfinger_request(url).perform do |res|
|
||||||
if res.code == 200
|
if res.code == 200
|
||||||
res.body_with_limit
|
body = res.body_with_limit
|
||||||
|
raise Webfinger::Error, "Request for #{@uri} returned empty response" if body.empty?
|
||||||
|
body
|
||||||
elsif res.code == 404 && use_fallback
|
elsif res.code == 404 && use_fallback
|
||||||
body_from_host_meta
|
body_from_host_meta
|
||||||
elsif res.code == 410
|
elsif res.code == 410
|
||||||
|
|
|
@ -55,8 +55,9 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class Account < ApplicationRecord
|
class Account < ApplicationRecord
|
||||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
||||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
|
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
|
||||||
|
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
|
||||||
|
|
||||||
include AccountAssociations
|
include AccountAssociations
|
||||||
include AccountAvatar
|
include AccountAvatar
|
||||||
|
@ -301,7 +302,11 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def fields
|
def fields
|
||||||
(self[:fields] || []).map { |f| Field.new(self, f) }
|
(self[:fields] || []).map do |f|
|
||||||
|
Field.new(self, f)
|
||||||
|
rescue
|
||||||
|
nil
|
||||||
|
end.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def fields_attributes=(attributes)
|
def fields_attributes=(attributes)
|
||||||
|
@ -381,7 +386,7 @@ class Account < ApplicationRecord
|
||||||
def synchronization_uri_prefix
|
def synchronization_uri_prefix
|
||||||
return 'local' if local?
|
return 'local' if local?
|
||||||
|
|
||||||
@synchronization_uri_prefix ||= uri[/http(s?):\/\/[^\/]+\//]
|
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
|
||||||
end
|
end
|
||||||
|
|
||||||
class Field < ActiveModelSerializers::Model
|
class Field < ActiveModelSerializers::Model
|
||||||
|
@ -435,6 +440,9 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/.freeze
|
||||||
|
TEXTSEARCH = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
|
||||||
|
|
||||||
def readonly_attributes
|
def readonly_attributes
|
||||||
super - %w(statuses_count following_count followers_count)
|
super - %w(statuses_count following_count followers_count)
|
||||||
end
|
end
|
||||||
|
@ -445,70 +453,29 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_for(terms, limit = 10, offset = 0)
|
def search_for(terms, limit = 10, offset = 0)
|
||||||
textsearch, query = generate_query_for_search(terms)
|
tsquery = generate_query_for_search(terms)
|
||||||
|
|
||||||
sql = <<-SQL.squish
|
sql = <<-SQL.squish
|
||||||
SELECT
|
SELECT
|
||||||
accounts.*,
|
accounts.*,
|
||||||
ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
|
ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE #{query} @@ #{textsearch}
|
WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
|
||||||
AND accounts.suspended_at IS NULL
|
AND accounts.suspended_at IS NULL
|
||||||
AND accounts.moved_to_account_id IS NULL
|
AND accounts.moved_to_account_id IS NULL
|
||||||
ORDER BY rank DESC
|
ORDER BY rank DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT :limit OFFSET :offset
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
records = find_by_sql([sql, limit, offset])
|
records = find_by_sql([sql, limit: limit, offset: offset, tsquery: tsquery])
|
||||||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||||||
records
|
records
|
||||||
end
|
end
|
||||||
|
|
||||||
def advanced_search_for(terms, account, limit = 10, following = false, offset = 0)
|
def advanced_search_for(terms, account, limit = 10, following = false, offset = 0)
|
||||||
textsearch, query = generate_query_for_search(terms)
|
tsquery = generate_query_for_search(terms)
|
||||||
|
sql = advanced_search_for_sql_template(following)
|
||||||
if following
|
records = find_by_sql([sql, id: account.id, limit: limit, offset: offset, tsquery: tsquery])
|
||||||
sql = <<-SQL.squish
|
|
||||||
WITH first_degree AS (
|
|
||||||
SELECT target_account_id
|
|
||||||
FROM follows
|
|
||||||
WHERE account_id = ?
|
|
||||||
UNION ALL
|
|
||||||
SELECT ?
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
accounts.*,
|
|
||||||
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
|
|
||||||
FROM accounts
|
|
||||||
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)
|
|
||||||
WHERE accounts.id IN (SELECT * FROM first_degree)
|
|
||||||
AND #{query} @@ #{textsearch}
|
|
||||||
AND accounts.suspended_at IS NULL
|
|
||||||
AND accounts.moved_to_account_id IS NULL
|
|
||||||
GROUP BY accounts.id
|
|
||||||
ORDER BY rank DESC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
SQL
|
|
||||||
|
|
||||||
records = find_by_sql([sql, account.id, account.id, account.id, limit, offset])
|
|
||||||
else
|
|
||||||
sql = <<-SQL.squish
|
|
||||||
SELECT
|
|
||||||
accounts.*,
|
|
||||||
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
|
|
||||||
FROM accounts
|
|
||||||
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
|
|
||||||
WHERE #{query} @@ #{textsearch}
|
|
||||||
AND accounts.suspended_at IS NULL
|
|
||||||
AND accounts.moved_to_account_id IS NULL
|
|
||||||
GROUP BY accounts.id
|
|
||||||
ORDER BY rank DESC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
SQL
|
|
||||||
|
|
||||||
records = find_by_sql([sql, account.id, account.id, limit, offset])
|
|
||||||
end
|
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||||||
records
|
records
|
||||||
end
|
end
|
||||||
|
@ -530,12 +497,55 @@ class Account < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_query_for_search(terms)
|
def generate_query_for_search(unsanitized_terms)
|
||||||
terms = Arel.sql(connection.quote(terms.gsub(/['?\\:]/, ' ')))
|
terms = unsanitized_terms.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ')
|
||||||
textsearch = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
|
|
||||||
query = "to_tsquery('simple', ''' ' || #{terms} || ' ''' || ':*')"
|
|
||||||
|
|
||||||
[textsearch, query]
|
# The final ":*" is for prefix search.
|
||||||
|
# The trailing space does not seem to fit any purpose, but `to_tsquery`
|
||||||
|
# behaves differently with and without a leading space if the terms start
|
||||||
|
# with `./`, `../`, or `.. `. I don't understand why, so, in doubt, keep
|
||||||
|
# the same query.
|
||||||
|
"' #{terms} ':*"
|
||||||
|
end
|
||||||
|
|
||||||
|
def advanced_search_for_sql_template(following)
|
||||||
|
if following
|
||||||
|
<<-SQL.squish
|
||||||
|
WITH first_degree AS (
|
||||||
|
SELECT target_account_id
|
||||||
|
FROM follows
|
||||||
|
WHERE account_id = :id
|
||||||
|
UNION ALL
|
||||||
|
SELECT :id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
accounts.*,
|
||||||
|
(count(f.id) + 1) * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
|
||||||
|
FROM accounts
|
||||||
|
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id)
|
||||||
|
WHERE accounts.id IN (SELECT * FROM first_degree)
|
||||||
|
AND to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
|
||||||
|
AND accounts.suspended_at IS NULL
|
||||||
|
AND accounts.moved_to_account_id IS NULL
|
||||||
|
GROUP BY accounts.id
|
||||||
|
ORDER BY rank DESC
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
|
SQL
|
||||||
|
else
|
||||||
|
<<-SQL.squish
|
||||||
|
SELECT
|
||||||
|
accounts.*,
|
||||||
|
(count(f.id) + 1) * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
|
||||||
|
FROM accounts
|
||||||
|
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id) OR (accounts.id = f.target_account_id AND f.account_id = :id)
|
||||||
|
WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
|
||||||
|
AND accounts.suspended_at IS NULL
|
||||||
|
AND accounts.moved_to_account_id IS NULL
|
||||||
|
GROUP BY accounts.id
|
||||||
|
ORDER BY rank DESC
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
|
SQL
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class AccountMigration < ApplicationRecord
|
class AccountMigration < ApplicationRecord
|
||||||
|
include Redisable
|
||||||
|
|
||||||
COOLDOWN_PERIOD = 30.days.freeze
|
COOLDOWN_PERIOD = 30.days.freeze
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
@ -39,7 +41,13 @@ class AccountMigration < ApplicationRecord
|
||||||
|
|
||||||
return false unless errors.empty?
|
return false unless errors.empty?
|
||||||
|
|
||||||
save
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
|
if lock.acquired?
|
||||||
|
save
|
||||||
|
else
|
||||||
|
raise Mastodon::RaceConditionError
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def cooldown_at
|
def cooldown_at
|
||||||
|
@ -75,4 +83,8 @@ class AccountMigration < ApplicationRecord
|
||||||
def validate_migration_cooldown
|
def validate_migration_cooldown
|
||||||
errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists?
|
errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lock_options
|
||||||
|
{ redis: redis, key: "account_migration:#{account.id}" }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,4 +17,5 @@ class AccountNote < ApplicationRecord
|
||||||
belongs_to :target_account, class_name: 'Account'
|
belongs_to :target_account, class_name: 'Account'
|
||||||
|
|
||||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||||
|
validates :comment, length: { maximum: 2_000 }
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,8 @@ module AccountFinderConcern
|
||||||
|
|
||||||
def representative
|
def representative
|
||||||
Account.find(-99)
|
Account.find(-99)
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_local(username)
|
def find_local(username)
|
||||||
|
|
|
@ -243,10 +243,13 @@ module AccountInteractions
|
||||||
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
|
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_followers_hash(url_prefix)
|
def remote_followers_hash(url)
|
||||||
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}") do
|
url_prefix = url[Account::URL_PREFIX_RE]
|
||||||
|
return if url_prefix.blank?
|
||||||
|
|
||||||
|
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}/") do
|
||||||
digest = "\x00" * 32
|
digest = "\x00" * 32
|
||||||
followers.where(Account.arel_table[:uri].matches(url_prefix + '%', false, true)).pluck_each(:uri) do |uri|
|
followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(url_prefix)}/%", false, true)).or(followers.where(uri: url_prefix)).pluck_each(:uri) do |uri|
|
||||||
Xorcist.xor!(digest, Digest::SHA256.digest(uri))
|
Xorcist.xor!(digest, Digest::SHA256.digest(uri))
|
||||||
end
|
end
|
||||||
digest.unpack('H*')[0]
|
digest.unpack('H*')[0]
|
||||||
|
|
|
@ -17,7 +17,7 @@ module Expireable
|
||||||
end
|
end
|
||||||
|
|
||||||
def expires_in=(interval)
|
def expires_in=(interval)
|
||||||
self.expires_at = interval.to_i.seconds.from_now if interval.present?
|
self.expires_at = interval.present? ? interval.to_i.seconds.from_now : nil
|
||||||
@expires_in = interval
|
@expires_in = interval
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,6 @@ module Omniauthable
|
||||||
def user_params_from_auth(email, auth)
|
def user_params_from_auth(email, auth)
|
||||||
{
|
{
|
||||||
email: email || "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
|
email: email || "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
|
||||||
password: Devise.friendly_token[0, 20],
|
|
||||||
agreement: true,
|
agreement: true,
|
||||||
external: true,
|
external: true,
|
||||||
account_attributes: {
|
account_attributes: {
|
||||||
|
|
|
@ -29,7 +29,7 @@ class FollowRequest < ApplicationRecord
|
||||||
validates :account_id, uniqueness: { scope: :target_account_id }
|
validates :account_id, uniqueness: { scope: :target_account_id }
|
||||||
|
|
||||||
def authorize!
|
def authorize!
|
||||||
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri)
|
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri, bypass_limit: true)
|
||||||
MergeWorker.perform_async(target_account.id, account.id) if account.local?
|
MergeWorker.perform_async(target_account.id, account.id) if account.local?
|
||||||
destroy!
|
destroy!
|
||||||
end
|
end
|
||||||
|
|
|
@ -287,7 +287,7 @@ class MediaAttachment < ApplicationRecord
|
||||||
if instance.file_content_type == 'image/gif'
|
if instance.file_content_type == 'image/gif'
|
||||||
[:gif_transcoder, :blurhash_transcoder]
|
[:gif_transcoder, :blurhash_transcoder]
|
||||||
elsif VIDEO_MIME_TYPES.include?(instance.file_content_type)
|
elsif VIDEO_MIME_TYPES.include?(instance.file_content_type)
|
||||||
[:video_transcoder, :blurhash_transcoder, :type_corrector]
|
[:transcoder, :blurhash_transcoder, :type_corrector]
|
||||||
elsif AUDIO_MIME_TYPES.include?(instance.file_content_type)
|
elsif AUDIO_MIME_TYPES.include?(instance.file_content_type)
|
||||||
[:image_extractor, :transcoder, :type_corrector]
|
[:image_extractor, :transcoder, :type_corrector]
|
||||||
else
|
else
|
||||||
|
@ -388,7 +388,7 @@ class MediaAttachment < ApplicationRecord
|
||||||
# paths but ultimately the same file, so it makes sense to memoize the
|
# paths but ultimately the same file, so it makes sense to memoize the
|
||||||
# result while disregarding the path
|
# result while disregarding the path
|
||||||
def ffmpeg_data(path = nil)
|
def ffmpeg_data(path = nil)
|
||||||
@ffmpeg_data ||= FFMPEG::Movie.new(path)
|
@ffmpeg_data ||= VideoMetadataExtractor.new(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_processing
|
def enqueue_processing
|
||||||
|
|
|
@ -96,15 +96,12 @@ class Status < ApplicationRecord
|
||||||
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
||||||
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
||||||
scope :tagged_with_all, ->(tag_ids) {
|
scope :tagged_with_all, ->(tag_ids) {
|
||||||
Array(tag_ids).reduce(self) do |result, id|
|
Array(tag_ids).map(&:to_i).reduce(self) do |result, id|
|
||||||
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
scope :tagged_with_none, ->(tag_ids) {
|
scope :tagged_with_none, ->(tag_ids) {
|
||||||
Array(tag_ids).reduce(self) do |result, id|
|
where('NOT EXISTS (SELECT * FROM statuses_tags forbidden WHERE forbidden.status_id = statuses.id AND forbidden.tag_id IN (?))', tag_ids)
|
||||||
result.joins("LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
|
||||||
.where("t#{id}.tag_id IS NULL")
|
|
||||||
end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cache_associated :application,
|
cache_associated :application,
|
||||||
|
@ -114,7 +111,7 @@ class Status < ApplicationRecord
|
||||||
:tags,
|
:tags,
|
||||||
:preview_cards,
|
:preview_cards,
|
||||||
:preloadable_poll,
|
:preloadable_poll,
|
||||||
account: :account_stat,
|
account: [:account_stat, :user],
|
||||||
active_mentions: { account: :account_stat },
|
active_mentions: { account: :account_stat },
|
||||||
reblog: [
|
reblog: [
|
||||||
:application,
|
:application,
|
||||||
|
@ -124,7 +121,7 @@ class Status < ApplicationRecord
|
||||||
:conversation,
|
:conversation,
|
||||||
:status_stat,
|
:status_stat,
|
||||||
:preloadable_poll,
|
:preloadable_poll,
|
||||||
account: :account_stat,
|
account: [:account_stat, :user],
|
||||||
active_mentions: { account: :account_stat },
|
active_mentions: { account: :account_stat },
|
||||||
],
|
],
|
||||||
thread: { account: :account_stat }
|
thread: { account: :account_stat }
|
||||||
|
@ -301,7 +298,7 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
return if account_ids.empty?
|
return if account_ids.empty?
|
||||||
|
|
||||||
accounts = Account.where(id: account_ids).includes(:account_stat).each_with_object({}) { |a, h| h[a.id] = a }
|
accounts = Account.where(id: account_ids).includes(:account_stat, :user).index_by(&:id)
|
||||||
|
|
||||||
cached_items.each do |item|
|
cached_items.each do |item|
|
||||||
item.account = accounts[item.account_id]
|
item.account = accounts[item.account_id]
|
||||||
|
@ -422,7 +419,7 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def decrement_counter_caches
|
def decrement_counter_caches
|
||||||
return if direct_visibility?
|
return if direct_visibility? || new_record?
|
||||||
|
|
||||||
account&.decrement_count!(:statuses_count)
|
account&.decrement_count!(:statuses_count)
|
||||||
reblog&.decrement_count!(:reblogs_count) if reblog?
|
reblog&.decrement_count!(:reblogs_count) if reblog?
|
||||||
|
|
|
@ -63,7 +63,7 @@ class User < ApplicationRecord
|
||||||
devise :two_factor_backupable,
|
devise :two_factor_backupable,
|
||||||
otp_number_of_backup_codes: 10
|
otp_number_of_backup_codes: 10
|
||||||
|
|
||||||
devise :registerable, :recoverable, :rememberable, :validatable,
|
devise :registerable, :recoverable, :validatable,
|
||||||
:confirmable
|
:confirmable
|
||||||
|
|
||||||
include Omniauthable
|
include Omniauthable
|
||||||
|
@ -152,7 +152,7 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
def confirm
|
def confirm
|
||||||
new_user = !confirmed?
|
new_user = !confirmed?
|
||||||
self.approved = true if open_registrations?
|
self.approved = true if open_registrations? && !sign_up_from_ip_requires_approval?
|
||||||
|
|
||||||
super
|
super
|
||||||
|
|
||||||
|
@ -468,7 +468,7 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_email_dns?
|
def validate_email_dns?
|
||||||
email_changed? && !(Rails.env.test? || Rails.env.development?)
|
email_changed? && !external? && !(Rails.env.test? || Rails.env.development?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def invite_text_required?
|
def invite_text_required?
|
||||||
|
|
|
@ -48,7 +48,7 @@ class ManifestSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def scope
|
def scope
|
||||||
root_url
|
'/'
|
||||||
end
|
end
|
||||||
|
|
||||||
def share_target
|
def share_target
|
||||||
|
|
|
@ -281,7 +281,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "process_account:#{@uri}" }
|
{ redis: Redis.current, key: "process_account:#{@uri}", autorelease: 15.minutes.seconds }
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_tags
|
def process_tags
|
||||||
|
|
|
@ -5,11 +5,27 @@ class ActivityPub::ProcessCollectionService < BaseService
|
||||||
|
|
||||||
def call(body, account, **options)
|
def call(body, account, **options)
|
||||||
@account = account
|
@account = account
|
||||||
@json = Oj.load(body, mode: :strict)
|
@json = original_json = Oj.load(body, mode: :strict)
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
|
begin
|
||||||
|
@json = compact(@json) if @json['signature'].is_a?(Hash)
|
||||||
|
rescue JSON::LD::JsonLdError => e
|
||||||
|
Rails.logger.debug "Error when compacting JSON-LD document for #{value_or_id(@json['actor'])}: #{e.message}"
|
||||||
|
@json = original_json.without('signature')
|
||||||
|
end
|
||||||
|
|
||||||
return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
|
return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
|
||||||
|
|
||||||
|
if @json['signature'].present?
|
||||||
|
# We have verified the signature, but in the compaction step above, might
|
||||||
|
# have introduced incompatibilities with other servers that do not
|
||||||
|
# normalize the JSON-LD documents (for instance, previous Mastodon
|
||||||
|
# versions), so skip redistribution if we can't get a safe document.
|
||||||
|
patch_for_forwarding!(original_json, @json)
|
||||||
|
@json.delete('signature') unless safe_for_forwarding?(original_json, @json)
|
||||||
|
end
|
||||||
|
|
||||||
case @json['type']
|
case @json['type']
|
||||||
when 'Collection', 'CollectionPage'
|
when 'Collection', 'CollectionPage'
|
||||||
process_items @json['items']
|
process_items @json['items']
|
||||||
|
|
|
@ -6,6 +6,7 @@ class AfterBlockService < BaseService
|
||||||
@target_account = target_account
|
@target_account = target_account
|
||||||
|
|
||||||
clear_home_feed!
|
clear_home_feed!
|
||||||
|
clear_list_feeds!
|
||||||
clear_notifications!
|
clear_notifications!
|
||||||
clear_conversations!
|
clear_conversations!
|
||||||
end
|
end
|
||||||
|
@ -16,6 +17,10 @@ class AfterBlockService < BaseService
|
||||||
FeedManager.instance.clear_from_home(@account, @target_account)
|
FeedManager.instance.clear_from_home(@account, @target_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clear_list_feeds!
|
||||||
|
FeedManager.instance.clear_from_lists(@account, @target_account)
|
||||||
|
end
|
||||||
|
|
||||||
def clear_conversations!
|
def clear_conversations!
|
||||||
AccountConversation.where(account: @account).where('? = ANY(participant_account_ids)', @target_account.id).in_batches.destroy_all
|
AccountConversation.where(account: @account).where('? = ANY(participant_account_ids)', @target_account.id).in_batches.destroy_all
|
||||||
end
|
end
|
||||||
|
|
|
@ -174,6 +174,6 @@ class FetchLinkCardService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "fetch:#{@url}" }
|
{ redis: Redis.current, key: "fetch:#{@url}", autorelease: 15.minutes.seconds }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class FetchOEmbedService
|
class FetchOEmbedService
|
||||||
ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze
|
ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze
|
||||||
|
URL_REGEX = /(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i.freeze
|
||||||
|
|
||||||
attr_reader :url, :options, :format, :endpoint_url
|
attr_reader :url, :options, :format, :endpoint_url
|
||||||
|
|
||||||
|
@ -55,10 +56,12 @@ class FetchOEmbedService
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_endpoint!
|
def cache_endpoint!
|
||||||
|
return unless URL_REGEX.match?(@endpoint_url)
|
||||||
|
|
||||||
url_domain = Addressable::URI.parse(@url).normalized_host
|
url_domain = Addressable::URI.parse(@url).normalized_host
|
||||||
|
|
||||||
endpoint_hash = {
|
endpoint_hash = {
|
||||||
endpoint: @endpoint_url.gsub(/(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i, '={url}'),
|
endpoint: @endpoint_url.gsub(URL_REGEX, '={url}'),
|
||||||
format: @format,
|
format: @format,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,8 +67,53 @@ class NotifyService < BaseService
|
||||||
message? && @notification.target_status.direct_visibility?
|
message? && @notification.target_status.direct_visibility?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns true if the sender has been mentionned by the recipient up the thread
|
||||||
def response_to_recipient?
|
def response_to_recipient?
|
||||||
@notification.target_status.in_reply_to_account_id == @recipient.id && @notification.target_status.thread&.direct_visibility?
|
return false if @notification.target_status.in_reply_to_id.nil?
|
||||||
|
|
||||||
|
# Using an SQL CTE to avoid unneeded back-and-forth with SQL server in case of long threads
|
||||||
|
!Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id]).zero?
|
||||||
|
WITH RECURSIVE ancestors(id, in_reply_to_id, replying_to_sender, path) AS (
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.in_reply_to_id,
|
||||||
|
(CASE
|
||||||
|
WHEN s.account_id = :recipient_id THEN
|
||||||
|
EXISTS (
|
||||||
|
SELECT *
|
||||||
|
FROM mentions m
|
||||||
|
WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
||||||
|
)
|
||||||
|
ELSE
|
||||||
|
FALSE
|
||||||
|
END),
|
||||||
|
ARRAY[s.id]
|
||||||
|
FROM statuses s
|
||||||
|
WHERE s.id = :id
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.in_reply_to_id,
|
||||||
|
(CASE
|
||||||
|
WHEN s.account_id = :recipient_id THEN
|
||||||
|
EXISTS (
|
||||||
|
SELECT *
|
||||||
|
FROM mentions m
|
||||||
|
WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
||||||
|
)
|
||||||
|
ELSE
|
||||||
|
FALSE
|
||||||
|
END),
|
||||||
|
st.path || s.id
|
||||||
|
FROM ancestors st
|
||||||
|
JOIN statuses s ON s.id = st.in_reply_to_id
|
||||||
|
WHERE st.replying_to_sender IS FALSE AND NOT s.id = ANY(path)
|
||||||
|
)
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM ancestors st
|
||||||
|
JOIN statuses s ON s.id = st.id
|
||||||
|
WHERE st.replying_to_sender IS TRUE AND s.visibility = 3
|
||||||
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def from_staff?
|
def from_staff?
|
||||||
|
|
|
@ -74,6 +74,9 @@ class PostStatusService < BaseService
|
||||||
status_for_validation = @account.statuses.build(status_attributes)
|
status_for_validation = @account.statuses.build(status_attributes)
|
||||||
|
|
||||||
if status_for_validation.valid?
|
if status_for_validation.valid?
|
||||||
|
# Marking the status as destroyed is necessary to prevent the status from being
|
||||||
|
# persisted when the associated media attachments get updated when creating the
|
||||||
|
# scheduled status.
|
||||||
status_for_validation.destroy
|
status_for_validation.destroy
|
||||||
|
|
||||||
# The following transaction block is needed to wrap the UPDATEs to
|
# The following transaction block is needed to wrap the UPDATEs to
|
||||||
|
|
|
@ -16,6 +16,8 @@ class RemoveStatusService < BaseService
|
||||||
@account = status.account
|
@account = status.account
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
|
@status.discard
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
if lock.acquired?
|
if lock.acquired?
|
||||||
remove_from_self if @account.local?
|
remove_from_self if @account.local?
|
||||||
|
@ -168,6 +170,6 @@ class RemoveStatusService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "distribute:#{@status.id}" }
|
{ redis: Redis.current, key: "distribute:#{@status.id}", autorelease: 5.minutes.seconds }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,8 @@ class ReportService < BaseService
|
||||||
@comment = options.delete(:comment) || ''
|
@comment = options.delete(:comment) || ''
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
|
raise ActiveRecord::RecordNotFound if @target_account.suspended?
|
||||||
|
|
||||||
create_report!
|
create_report!
|
||||||
notify_staff!
|
notify_staff!
|
||||||
forward_to_origin! if !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
forward_to_origin! if !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||||
|
|
|
@ -141,10 +141,11 @@ class ResolveAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def queue_deletion!
|
def queue_deletion!
|
||||||
|
@account.suspend!(origin: :remote)
|
||||||
AccountDeletionWorker.perform_async(@account.id, reserve_username: false, skip_activitypub: true)
|
AccountDeletionWorker.perform_async(@account.id, reserve_username: false, skip_activitypub: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "resolve:#{@username}@#{@domain}" }
|
{ redis: Redis.current, key: "resolve:#{@username}@#{@domain}", autorelease: 15.minutes.seconds }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,7 +42,13 @@ class SuspendAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def distribute_update_actor!
|
def distribute_update_actor!
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id) if @account.local?
|
return unless @account.local?
|
||||||
|
|
||||||
|
account_reach_finder = AccountReachFinder.new(@account)
|
||||||
|
|
||||||
|
ActivityPub::DeliveryWorker.push_bulk(account_reach_finder.inboxes) do |inbox_url|
|
||||||
|
[signed_activity_json, @account.id, inbox_url]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unmerge_from_home_timelines!
|
def unmerge_from_home_timelines!
|
||||||
|
@ -90,4 +96,8 @@ class SuspendAccountService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def signed_activity_json
|
||||||
|
@signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,11 +7,12 @@ class UnsuspendAccountService < BaseService
|
||||||
unsuspend!
|
unsuspend!
|
||||||
refresh_remote_account!
|
refresh_remote_account!
|
||||||
|
|
||||||
return if @account.nil?
|
return if @account.nil? || @account.suspended?
|
||||||
|
|
||||||
merge_into_home_timelines!
|
merge_into_home_timelines!
|
||||||
merge_into_list_timelines!
|
merge_into_list_timelines!
|
||||||
publish_media_attachments!
|
publish_media_attachments!
|
||||||
|
distribute_update_actor!
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -36,6 +37,16 @@ class UnsuspendAccountService < BaseService
|
||||||
# @account would now be nil.
|
# @account would now be nil.
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def distribute_update_actor!
|
||||||
|
return unless @account.local?
|
||||||
|
|
||||||
|
account_reach_finder = AccountReachFinder.new(@account)
|
||||||
|
|
||||||
|
ActivityPub::DeliveryWorker.push_bulk(account_reach_finder.inboxes) do |inbox_url|
|
||||||
|
[signed_activity_json, @account.id, inbox_url]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def merge_into_home_timelines!
|
def merge_into_home_timelines!
|
||||||
@account.followers_for_local_distribution.find_each do |follower|
|
@account.followers_for_local_distribution.find_each do |follower|
|
||||||
FeedManager.instance.merge_into_home(@account, follower)
|
FeedManager.instance.merge_into_home(@account, follower)
|
||||||
|
@ -81,4 +92,8 @@ class UnsuspendAccountService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def signed_activity_json
|
||||||
|
@signed_activity_json ||= Oj.dump(serialize_payload(@account, ActivityPub::UpdateSerializer, signer: @account))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
%hr.spacer
|
%hr.spacer
|
||||||
|
|
||||||
.speech-bubble
|
.speech-bubble
|
||||||
.speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
|
.speech-bubble__bubble= simple_format(h(@report.comment.presence || t('admin.reports.comment.none')))
|
||||||
.speech-bubble__owner
|
.speech-bubble__owner
|
||||||
- if @report.account.local?
|
- if @report.account.local?
|
||||||
= admin_account_link_to @report.account
|
= admin_account_link_to @report.account
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :phrase, as: :string, wrapper: :with_label, hint: false
|
= f.input :phrase, as: :string, wrapper: :with_label, hint: false
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, include_blank: I18n.t('invites.expires_in_prompt')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :context, wrapper: :with_block_label, collection: CustomFilter::VALID_CONTEXTS, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: lambda { |context| I18n.t("filters.contexts.#{context}") }, include_blank: false
|
= f.input :context, wrapper: :with_block_label, collection: CustomFilter::VALID_CONTEXTS, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: lambda { |context| I18n.t("filters.contexts.#{context}") }, include_blank: false
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
= stylesheet_link_tag '/inert.css', skip_pipeline: true, media: 'all', id: 'inert-style'
|
= stylesheet_link_tag '/inert.css', skip_pipeline: true, media: 'all', id: 'inert-style'
|
||||||
|
|
||||||
- if Setting.custom_css.present?
|
- if Setting.custom_css.present?
|
||||||
= stylesheet_link_tag custom_css_path, media: 'all'
|
= stylesheet_link_tag custom_css_path, host: request.host, media: 'all'
|
||||||
|
|
||||||
= yield :header_tags
|
= yield :header_tags
|
||||||
|
|
||||||
|
|
|
@ -44,11 +44,7 @@ class ActivityPub::DeliveryWorker
|
||||||
end
|
end
|
||||||
|
|
||||||
def synchronization_header
|
def synchronization_header
|
||||||
"collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(inbox_url_prefix)}\", url=\"#{account_followers_synchronization_url(@source_account)}\""
|
"collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(@inbox_url)}\", url=\"#{account_followers_synchronization_url(@source_account)}\""
|
||||||
end
|
|
||||||
|
|
||||||
def inbox_url_prefix
|
|
||||||
@inbox_url[/http(s?):\/\/[^\/]+\//]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform_request
|
def perform_request
|
||||||
|
|
|
@ -4,7 +4,7 @@ class DistributionWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform(status_id)
|
def perform(status_id)
|
||||||
RedisLock.acquire(redis: Redis.current, key: "distribute:#{status_id}") do |lock|
|
RedisLock.acquire(redis: Redis.current, key: "distribute:#{status_id}", autorelease: 5.minutes.seconds) do |lock|
|
||||||
if lock.acquired?
|
if lock.acquired?
|
||||||
FanOutOnWriteService.new.call(Status.find(status_id))
|
FanOutOnWriteService.new.call(Status.find(status_id))
|
||||||
else
|
else
|
||||||
|
|
|
@ -13,9 +13,13 @@ class MoveWorker
|
||||||
queue_follow_unfollows!
|
queue_follow_unfollows!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@deferred_error = nil
|
||||||
|
|
||||||
copy_account_notes!
|
copy_account_notes!
|
||||||
carry_blocks_over!
|
carry_blocks_over!
|
||||||
carry_mutes_over!
|
carry_mutes_over!
|
||||||
|
|
||||||
|
raise @deferred_error unless @deferred_error.nil?
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -36,21 +40,31 @@ class MoveWorker
|
||||||
|
|
||||||
@source_account.followers.local.select(:id).find_in_batches do |accounts|
|
@source_account.followers.local.select(:id).find_in_batches do |accounts|
|
||||||
UnfollowFollowWorker.push_bulk(accounts.map(&:id)) { |follower_id| [follower_id, @source_account.id, @target_account.id, bypass_locked] }
|
UnfollowFollowWorker.push_bulk(accounts.map(&:id)) { |follower_id| [follower_id, @source_account.id, @target_account.id, bypass_locked] }
|
||||||
|
rescue => e
|
||||||
|
@deferred_error = e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def copy_account_notes!
|
def copy_account_notes!
|
||||||
AccountNote.where(target_account: @source_account).find_each do |note|
|
AccountNote.where(target_account: @source_account).find_each do |note|
|
||||||
text = I18n.with_locale(note.account.user.locale || I18n.default_locale) do
|
text = I18n.with_locale(note.account.user&.locale || I18n.default_locale) do
|
||||||
I18n.t('move_handler.copy_account_note_text', acct: @source_account.acct)
|
I18n.t('move_handler.copy_account_note_text', acct: @source_account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
new_note = AccountNote.find_by(account: note.account, target_account: @target_account)
|
new_note = AccountNote.find_by(account: note.account, target_account: @target_account)
|
||||||
if new_note.nil?
|
if new_note.nil?
|
||||||
AccountNote.create!(account: note.account, target_account: @target_account, comment: [text, note.comment].join('\n'))
|
begin
|
||||||
|
AccountNote.create!(account: note.account, target_account: @target_account, comment: [text, note.comment].join("\n"))
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
AccountNote.create!(account: note.account, target_account: @target_account, comment: note.comment)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
new_note.update!(comment: [text, note.comment, '\n', new_note.comment].join('\n'))
|
new_note.update!(comment: [text, note.comment, "\n", new_note.comment].join("\n"))
|
||||||
end
|
end
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
nil
|
||||||
|
rescue => e
|
||||||
|
@deferred_error = e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -60,6 +74,8 @@ class MoveWorker
|
||||||
BlockService.new.call(block.account, @target_account)
|
BlockService.new.call(block.account, @target_account)
|
||||||
add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text')
|
add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text')
|
||||||
end
|
end
|
||||||
|
rescue => e
|
||||||
|
@deferred_error = e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -67,12 +83,14 @@ class MoveWorker
|
||||||
@source_account.muted_by_relationships.where(account: Account.local).find_each do |mute|
|
@source_account.muted_by_relationships.where(account: Account.local).find_each do |mute|
|
||||||
MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless mute.account.muting?(@target_account) || mute.account.following?(@target_account)
|
MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless mute.account.muting?(@target_account) || mute.account.following?(@target_account)
|
||||||
add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text')
|
add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text')
|
||||||
|
rescue => e
|
||||||
|
@deferred_error = e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_account_note_if_needed!(account, id)
|
def add_account_note_if_needed!(account, id)
|
||||||
unless AccountNote.where(account: account, target_account: @target_account).exists?
|
unless AccountNote.where(account: account, target_account: @target_account).exists?
|
||||||
text = I18n.with_locale(account.user.locale || I18n.default_locale) do
|
text = I18n.with_locale(account.user&.locale || I18n.default_locale) do
|
||||||
I18n.t(id, acct: @source_account.acct)
|
I18n.t(id, acct: @source_account.acct)
|
||||||
end
|
end
|
||||||
AccountNote.create!(account: account, target_account: @target_account, comment: text)
|
AccountNote.create!(account: account, target_account: @target_account, comment: text)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
class RedownloadMediaWorker
|
class RedownloadMediaWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
include ExponentialBackoff
|
include ExponentialBackoff
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
sidekiq_options queue: 'pull', retry: 3
|
sidekiq_options queue: 'pull', retry: 3
|
||||||
|
|
||||||
|
@ -15,6 +16,14 @@ class RedownloadMediaWorker
|
||||||
media_attachment.download_thumbnail!
|
media_attachment.download_thumbnail!
|
||||||
media_attachment.save
|
media_attachment.save
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
# Do nothing
|
||||||
|
rescue Mastodon::UnexpectedResponseError => e
|
||||||
|
response = e.response
|
||||||
|
|
||||||
|
if response_error_unsalvageable?(response)
|
||||||
|
# Give up
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,5 +14,7 @@ class ThreadResolveWorker
|
||||||
|
|
||||||
child_status.thread = parent_status
|
child_status.thread = parent_status
|
||||||
child_status.save!
|
child_status.save!
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ image:
|
||||||
repository: tootsuite/mastodon
|
repository: tootsuite/mastodon
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
# https://hub.docker.com/r/tootsuite/mastodon/tags
|
# https://hub.docker.com/r/tootsuite/mastodon/tags
|
||||||
tag: v3.3.0
|
tag: v3.3.2
|
||||||
# alternatively, use `latest` for the latest release or `edge` for the image
|
# alternatively, use `latest` for the latest release or `edge` for the image
|
||||||
# built from the most recent commit
|
# built from the most recent commit
|
||||||
#
|
#
|
||||||
|
|
|
@ -11,12 +11,12 @@ require_relative '../lib/redis/namespace_extensions'
|
||||||
require_relative '../lib/paperclip/url_generator_extensions'
|
require_relative '../lib/paperclip/url_generator_extensions'
|
||||||
require_relative '../lib/paperclip/attachment_extensions'
|
require_relative '../lib/paperclip/attachment_extensions'
|
||||||
require_relative '../lib/paperclip/media_type_spoof_detector_extensions'
|
require_relative '../lib/paperclip/media_type_spoof_detector_extensions'
|
||||||
require_relative '../lib/paperclip/transcoder_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/video_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'
|
||||||
|
require_relative '../lib/terrapin/multi_pipe_extensions'
|
||||||
require_relative '../lib/mastodon/snowflake'
|
require_relative '../lib/mastodon/snowflake'
|
||||||
require_relative '../lib/mastodon/version'
|
require_relative '../lib/mastodon/version'
|
||||||
require_relative '../lib/devise/two_factor_ldap_authenticatable'
|
require_relative '../lib/devise/two_factor_ldap_authenticatable'
|
||||||
|
|
|
@ -153,46 +153,6 @@
|
||||||
"confidence": "High",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "6e4051854bb62e2ddbc671f82d6c2328892e1134b8b28105ecba9b0122540714",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/account.rb",
|
|
||||||
"line": 491,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "find_by_sql([\" WITH first_degree AS (\\n SELECT target_account_id\\n FROM follows\\n WHERE account_id = ?\\n UNION ALL\\n SELECT ?\\n )\\n SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)\\n WHERE accounts.id IN (SELECT * FROM first_degree)\\n AND #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, account.id, limit, offset])",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Account",
|
|
||||||
"method": "advanced_search_for"
|
|
||||||
},
|
|
||||||
"user_input": "textsearch",
|
|
||||||
"confidence": "Medium",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "6f075c1484908e3ec9bed21ab7cf3c7866be8da3881485d1c82e13093aefcbd7",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/status.rb",
|
|
||||||
"line": 105,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "result.joins(\"LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Status",
|
|
||||||
"method": null
|
|
||||||
},
|
|
||||||
"user_input": "id",
|
|
||||||
"confidence": "Weak",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "Mass Assignment",
|
"warning_type": "Mass Assignment",
|
||||||
"warning_code": 105,
|
"warning_code": 105,
|
||||||
|
@ -213,26 +173,6 @@
|
||||||
"confidence": "High",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "9251d682c4e2840e1b2fea91e7d758efe2097ecb7f6255c065e3750d25eb178c",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/account.rb",
|
|
||||||
"line": 460,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, limit, offset])",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Account",
|
|
||||||
"method": "search_for"
|
|
||||||
},
|
|
||||||
"user_input": "textsearch",
|
|
||||||
"confidence": "Medium",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
|
@ -324,26 +264,6 @@
|
||||||
"confidence": "High",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "e21d8fee7a5805761679877ca35ed1029c64c45ef3b4012a30262623e1ba8bb9",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/account.rb",
|
|
||||||
"line": 507,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, limit, offset])",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Account",
|
|
||||||
"method": "advanced_search_for"
|
|
||||||
},
|
|
||||||
"user_input": "textsearch",
|
|
||||||
"confidence": "Medium",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "Mass Assignment",
|
"warning_type": "Mass Assignment",
|
||||||
"warning_code": 105,
|
"warning_code": 105,
|
||||||
|
|
|
@ -108,7 +108,7 @@ Rails.application.configure do
|
||||||
'Server' => 'Mastodon',
|
'Server' => 'Mastodon',
|
||||||
'X-Frame-Options' => 'DENY',
|
'X-Frame-Options' => 'DENY',
|
||||||
'X-Content-Type-Options' => 'nosniff',
|
'X-Content-Type-Options' => 'nosniff',
|
||||||
'X-XSS-Protection' => '1; mode=block',
|
'X-XSS-Protection' => '0',
|
||||||
}
|
}
|
||||||
|
|
||||||
config.x.otp_secret = ENV.fetch('OTP_SECRET')
|
config.x.otp_secret = ENV.fetch('OTP_SECRET')
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require 'devise/strategies/authenticatable'
|
||||||
|
|
||||||
Warden::Manager.after_set_user except: :fetch do |user, warden|
|
Warden::Manager.after_set_user except: :fetch do |user, warden|
|
||||||
if user.session_active?(warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'])
|
if user.session_active?(warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'])
|
||||||
session_id = warden.cookies.signed['_session_id'] || warden.raw_session['auth_id']
|
session_id = warden.cookies.signed['_session_id'] || warden.raw_session['auth_id']
|
||||||
|
@ -72,17 +74,48 @@ module Devise
|
||||||
mattr_accessor :ldap_uid_conversion_replace
|
mattr_accessor :ldap_uid_conversion_replace
|
||||||
@@ldap_uid_conversion_replace = nil
|
@@ldap_uid_conversion_replace = nil
|
||||||
|
|
||||||
class Strategies::PamAuthenticatable
|
module Strategies
|
||||||
def valid?
|
class PamAuthenticatable
|
||||||
super && ::Devise.pam_authentication
|
def valid?
|
||||||
|
super && ::Devise.pam_authentication
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class SessionActivationRememberable < Authenticatable
|
||||||
|
def valid?
|
||||||
|
@session_cookie = nil
|
||||||
|
session_cookie.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate!
|
||||||
|
resource = SessionActivation.find_by(session_id: session_cookie)&.user
|
||||||
|
|
||||||
|
unless resource
|
||||||
|
cookies.delete('_session_id')
|
||||||
|
return pass
|
||||||
|
end
|
||||||
|
|
||||||
|
if validate(resource)
|
||||||
|
success!(resource)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def session_cookie
|
||||||
|
@session_cookie ||= cookies.signed['_session_id']
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Warden::Strategies.add(:session_activation_rememberable, Devise::Strategies::SessionActivationRememberable)
|
||||||
|
|
||||||
Devise.setup do |config|
|
Devise.setup do |config|
|
||||||
config.warden do |manager|
|
config.warden do |manager|
|
||||||
manager.default_strategies(scope: :user).unshift :two_factor_ldap_authenticatable if Devise.ldap_authentication
|
manager.default_strategies(scope: :user).unshift :two_factor_ldap_authenticatable if Devise.ldap_authentication
|
||||||
manager.default_strategies(scope: :user).unshift :two_factor_pam_authenticatable if Devise.pam_authentication
|
manager.default_strategies(scope: :user).unshift :two_factor_pam_authenticatable if Devise.pam_authentication
|
||||||
|
manager.default_strategies(scope: :user).unshift :session_activation_rememberable
|
||||||
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
|
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
|
||||||
manager.default_strategies(scope: :user).unshift :two_factor_backupable
|
manager.default_strategies(scope: :user).unshift :two_factor_backupable
|
||||||
end
|
end
|
||||||
|
|
|
@ -52,6 +52,11 @@ Doorkeeper.configure do
|
||||||
# Issue access tokens with refresh token (disabled by default)
|
# Issue access tokens with refresh token (disabled by default)
|
||||||
# use_refresh_token
|
# use_refresh_token
|
||||||
|
|
||||||
|
# Forbids creating/updating applications with arbitrary scopes that are
|
||||||
|
# not in configuration, i.e. `default_scopes` or `optional_scopes`.
|
||||||
|
# (Disabled by default)
|
||||||
|
enforce_configured_scopes
|
||||||
|
|
||||||
# Provide support for an owner to be assigned to each registered application (disabled by default)
|
# Provide support for an owner to be assigned to each registered application (disabled by default)
|
||||||
# Optional parameter :confirmation => true (default false) if you want to enforce ownership of
|
# Optional parameter :confirmation => true (default false) if you want to enforce ownership of
|
||||||
# a registered application
|
# a registered application
|
||||||
|
|
|
@ -60,6 +60,7 @@ Devise.setup do |config|
|
||||||
saml_options[:attribute_statements][:verified] = [ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED']] if ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED']
|
saml_options[:attribute_statements][:verified] = [ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED']] if ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED']
|
||||||
saml_options[:attribute_statements][:verified_email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL']
|
saml_options[:attribute_statements][:verified_email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL']
|
||||||
saml_options[:uid_attribute] = ENV['SAML_UID_ATTRIBUTE'] if ENV['SAML_UID_ATTRIBUTE']
|
saml_options[:uid_attribute] = ENV['SAML_UID_ATTRIBUTE'] if ENV['SAML_UID_ATTRIBUTE']
|
||||||
|
saml_options[:allowed_clock_drift] = ENV['SAML_ALLOWED_CLOCK_DRIFT'] if ENV['SAML_ALLOWED_CLOCK_DRIFT']
|
||||||
config.omniauth :saml, saml_options
|
config.omniauth :saml, saml_options
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,46 @@
|
||||||
class RemoveFauxRemoteAccountDuplicates < ActiveRecord::Migration[5.2]
|
class RemoveFauxRemoteAccountDuplicates < ActiveRecord::Migration[5.2]
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
class StreamEntry < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, inverse_of: :stream_entries
|
||||||
|
end
|
||||||
|
|
||||||
|
class Status < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, inverse_of: :statuses
|
||||||
|
has_many :favourites, inverse_of: :status, dependent: :destroy
|
||||||
|
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||||
|
end
|
||||||
|
|
||||||
|
class Favourite < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, inverse_of: :favourites
|
||||||
|
belongs_to :status, inverse_of: :favourites
|
||||||
|
end
|
||||||
|
|
||||||
|
class Mention < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, inverse_of: :mentions
|
||||||
|
belongs_to :status
|
||||||
|
end
|
||||||
|
|
||||||
|
class Notification < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, optional: true
|
||||||
|
belongs_to :from_account, class_name: 'Account', optional: true
|
||||||
|
belongs_to :activity, polymorphic: true, optional: true
|
||||||
|
end
|
||||||
|
|
||||||
|
class Account < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
has_many :stream_entries, inverse_of: :account, dependent: :destroy
|
||||||
|
has_many :statuses, inverse_of: :account, dependent: :destroy
|
||||||
|
has_many :favourites, inverse_of: :account, dependent: :destroy
|
||||||
|
has_many :mentions, inverse_of: :account, dependent: :destroy
|
||||||
|
has_many :notifications, inverse_of: :account, dependent: :destroy
|
||||||
|
end
|
||||||
|
|
||||||
def up
|
def up
|
||||||
local_domain = Rails.configuration.x.local_domain
|
local_domain = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
class AddInstanceActor < ActiveRecord::Migration[5.2]
|
class AddInstanceActor < ActiveRecord::Migration[5.2]
|
||||||
|
class Account < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
validates :username, uniqueness: { scope: :domain, case_sensitive: false }
|
||||||
|
end
|
||||||
|
|
||||||
def up
|
def up
|
||||||
Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
|
Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,13 @@ class AddCaseInsensitiveIndexToTags < ActiveRecord::Migration[5.2]
|
||||||
Tag.where(id: redundant_tag_ids).in_batches.delete_all
|
Tag.where(id: redundant_tag_ids).in_batches.delete_all
|
||||||
end
|
end
|
||||||
|
|
||||||
safety_assured { execute 'CREATE UNIQUE INDEX CONCURRENTLY index_tags_on_name_lower ON tags (lower(name))' }
|
begin
|
||||||
|
safety_assured { execute 'CREATE UNIQUE INDEX CONCURRENTLY index_tags_on_name_lower ON tags (lower(name))' }
|
||||||
|
rescue ActiveRecord::StatementInvalid
|
||||||
|
remove_index :tags, name: 'index_tags_on_name_lower'
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
remove_index :tags, name: 'index_tags_on_name'
|
remove_index :tags, name: 'index_tags_on_name'
|
||||||
remove_index :tags, name: 'hashtag_search_index'
|
remove_index :tags, name: 'hashtag_search_index'
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
class UpdatePtLocales < ActiveRecord::Migration[5.2]
|
class UpdatePtLocales < ActiveRecord::Migration[5.2]
|
||||||
|
class User < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
end
|
||||||
|
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
class AddFixedLowercaseIndexToAccounts < ActiveRecord::Migration[5.2]
|
class AddFixedLowercaseIndexToAccounts < ActiveRecord::Migration[5.2]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
class CorruptionError < StandardError
|
|
||||||
def cause
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def backtrace
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def up
|
def up
|
||||||
if index_name_exists?(:accounts, 'old_index_accounts_on_username_and_domain_lower') && index_name_exists?(:accounts, 'index_accounts_on_username_and_domain_lower')
|
if index_name_exists?(:accounts, 'old_index_accounts_on_username_and_domain_lower') && index_name_exists?(:accounts, 'index_accounts_on_username_and_domain_lower')
|
||||||
remove_index :accounts, name: 'index_accounts_on_username_and_domain_lower'
|
remove_index :accounts, name: 'index_accounts_on_username_and_domain_lower'
|
||||||
|
@ -21,7 +15,8 @@ class AddFixedLowercaseIndexToAccounts < ActiveRecord::Migration[5.2]
|
||||||
begin
|
begin
|
||||||
add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true, algorithm: :concurrently
|
add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true, algorithm: :concurrently
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
raise CorruptionError, 'Migration failed because of index corruption, see https://docs.joinmastodon.org/admin/troubleshooting/index-corruption/#fixing'
|
remove_index :accounts, name: 'index_accounts_on_username_and_domain_lower'
|
||||||
|
raise CorruptionError
|
||||||
end
|
end
|
||||||
|
|
||||||
remove_index :accounts, name: 'old_index_accounts_on_username_and_domain_lower' if index_name_exists?(:accounts, 'old_index_accounts_on_username_and_domain_lower')
|
remove_index :accounts, name: 'old_index_accounts_on_username_and_domain_lower' if index_name_exists?(:accounts, 'old_index_accounts_on_username_and_domain_lower')
|
||||||
|
|
|
@ -43,7 +43,7 @@ services:
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: tootsuite/mastodon
|
image: tootsuite/mastodon:v3.3.2
|
||||||
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"
|
||||||
|
@ -63,7 +63,7 @@ services:
|
||||||
|
|
||||||
streaming:
|
streaming:
|
||||||
build: .
|
build: .
|
||||||
image: tootsuite/mastodon
|
image: tootsuite/mastodon:v3.3.2
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming
|
command: node ./streaming
|
||||||
|
@ -80,7 +80,7 @@ services:
|
||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: tootsuite/mastodon
|
image: tootsuite/mastodon:v3.3.2
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|
22
lib/cli.rb
22
lib/cli.rb
|
@ -94,17 +94,22 @@ module Mastodon
|
||||||
|
|
||||||
exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
|
exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
|
||||||
|
|
||||||
prompt.warn('This operation WILL NOT be reversible. It can also take a long time.')
|
unless options[:dry_run]
|
||||||
prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
|
prompt.warn('This operation WILL NOT be reversible. It can also take a long time.')
|
||||||
prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.')
|
prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
|
||||||
|
prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.')
|
||||||
|
|
||||||
exit(1) if prompt.no?('Are you sure you want to proceed?')
|
exit(1) if prompt.no?('Are you sure you want to proceed?')
|
||||||
|
end
|
||||||
|
|
||||||
inboxes = Account.inboxes
|
inboxes = Account.inboxes
|
||||||
processed = 0
|
processed = 0
|
||||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||||
|
|
||||||
|
Setting.registrations_mode = 'none' unless options[:dry_run]
|
||||||
|
|
||||||
if inboxes.empty?
|
if inboxes.empty?
|
||||||
|
Account.local.without_suspended.in_batches.update_all(suspended_at: Time.now.utc, suspension_origin: :local) unless options[:dry_run]
|
||||||
prompt.ok('It seems like your server has not federated with anything')
|
prompt.ok('It seems like your server has not federated with anything')
|
||||||
prompt.ok('You can shut it down and delete it any time')
|
prompt.ok('You can shut it down and delete it any time')
|
||||||
return
|
return
|
||||||
|
@ -112,9 +117,7 @@ module Mastodon
|
||||||
|
|
||||||
prompt.warn('Do NOT interrupt this process...')
|
prompt.warn('Do NOT interrupt this process...')
|
||||||
|
|
||||||
Setting.registrations_mode = 'none'
|
delete_account = ->(account) do
|
||||||
|
|
||||||
Account.local.without_suspended.find_each do |account|
|
|
||||||
payload = ActiveModelSerializers::SerializableResource.new(
|
payload = ActiveModelSerializers::SerializableResource.new(
|
||||||
account,
|
account,
|
||||||
serializer: ActivityPub::DeleteActorSerializer,
|
serializer: ActivityPub::DeleteActorSerializer,
|
||||||
|
@ -128,12 +131,15 @@ module Mastodon
|
||||||
[json, account.id, inbox_url]
|
[json, account.id, inbox_url]
|
||||||
end
|
end
|
||||||
|
|
||||||
account.suspend!
|
account.suspend!(block_email: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
processed += 1
|
processed += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Account.local.without_suspended.find_each { |account| delete_account.call(account) }
|
||||||
|
Account.local.suspended.joins(:deletion_request).find_each { |account| delete_account.call(account) }
|
||||||
|
|
||||||
prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run}")
|
prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run}")
|
||||||
prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data')
|
prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data')
|
||||||
rescue TTY::Reader::InputInterrupt
|
rescue TTY::Reader::InputInterrupt
|
||||||
|
|
|
@ -402,7 +402,7 @@ module Mastodon
|
||||||
exit(1)
|
exit(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
parallelize_with_progress(target_account.followers.local) do |account|
|
processed, = parallelize_with_progress(target_account.followers.local) do |account|
|
||||||
UnfollowService.new.call(account, target_account)
|
UnfollowService.new.call(account, target_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,13 @@ module Mastodon
|
||||||
tar.each do |entry|
|
tar.each do |entry|
|
||||||
next unless entry.file? && entry.full_name.end_with?('.png')
|
next unless entry.file? && entry.full_name.end_with?('.png')
|
||||||
|
|
||||||
shortcode = [options[:prefix], File.basename(entry.full_name, '.*'), options[:suffix]].compact.join
|
filename = File.basename(entry.full_name, '.*')
|
||||||
custom_emoji = CustomEmoji.local.find_by(shortcode: shortcode)
|
|
||||||
|
# Skip macOS shadow files
|
||||||
|
next if filename.start_with?('._')
|
||||||
|
|
||||||
|
shortcode = [options[:prefix], filename, options[:suffix]].compact.join
|
||||||
|
custom_emoji = CustomEmoji.local.find_by("LOWER(shortcode) = ?", shortcode.downcase)
|
||||||
|
|
||||||
if custom_emoji && !options[:overwrite]
|
if custom_emoji && !options[:overwrite]
|
||||||
skipped += 1
|
skipped += 1
|
||||||
|
|
|
@ -14,7 +14,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
MIN_SUPPORTED_VERSION = 2019_10_01_213028
|
MIN_SUPPORTED_VERSION = 2019_10_01_213028
|
||||||
MAX_SUPPORTED_VERSION = 2020_12_18_054746
|
MAX_SUPPORTED_VERSION = 2021_03_08_133107
|
||||||
|
|
||||||
# Stubs to enjoy ActiveRecord queries while not depending on a particular
|
# Stubs to enjoy ActiveRecord queries while not depending on a particular
|
||||||
# version of the code/database
|
# version of the code/database
|
||||||
|
@ -142,7 +142,6 @@ module Mastodon
|
||||||
@prompt.warn 'Please make sure to stop Mastodon and have a backup.'
|
@prompt.warn 'Please make sure to stop Mastodon and have a backup.'
|
||||||
exit(1) unless @prompt.yes?('Continue?')
|
exit(1) unless @prompt.yes?('Continue?')
|
||||||
|
|
||||||
deduplicate_accounts!
|
|
||||||
deduplicate_users!
|
deduplicate_users!
|
||||||
deduplicate_account_domain_blocks!
|
deduplicate_account_domain_blocks!
|
||||||
deduplicate_account_identity_proofs!
|
deduplicate_account_identity_proofs!
|
||||||
|
@ -157,9 +156,11 @@ module Mastodon
|
||||||
deduplicate_media_attachments!
|
deduplicate_media_attachments!
|
||||||
deduplicate_preview_cards!
|
deduplicate_preview_cards!
|
||||||
deduplicate_statuses!
|
deduplicate_statuses!
|
||||||
|
deduplicate_accounts!
|
||||||
deduplicate_tags!
|
deduplicate_tags!
|
||||||
deduplicate_webauthn_credentials!
|
deduplicate_webauthn_credentials!
|
||||||
|
|
||||||
|
Scenic.database.refresh_materialized_view('instances', concurrently: true, cascade: false) if ActiveRecord::Migrator.current_version >= 2020_12_06_004238
|
||||||
Rails.cache.clear
|
Rails.cache.clear
|
||||||
|
|
||||||
@prompt.say 'Finished!'
|
@prompt.say 'Finished!'
|
||||||
|
@ -188,6 +189,11 @@ module Mastodon
|
||||||
else
|
else
|
||||||
ActiveRecord::Base.connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true
|
ActiveRecord::Base.connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@prompt.say 'Reindexing textual indexes on accounts…'
|
||||||
|
ActiveRecord::Base.connection.execute('REINDEX INDEX search_index;')
|
||||||
|
ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_uri;')
|
||||||
|
ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_url;')
|
||||||
end
|
end
|
||||||
|
|
||||||
def deduplicate_users!
|
def deduplicate_users!
|
||||||
|
|
|
@ -41,6 +41,20 @@
|
||||||
|
|
||||||
module Mastodon
|
module Mastodon
|
||||||
module MigrationHelpers
|
module MigrationHelpers
|
||||||
|
class CorruptionError < StandardError
|
||||||
|
def initialize(message = nil)
|
||||||
|
super(message.presence || 'Migration failed because of index corruption, see https://docs.joinmastodon.org/admin/troubleshooting/index-corruption/#fixing')
|
||||||
|
end
|
||||||
|
|
||||||
|
def cause
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def backtrace
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Stub for Database.postgresql? from GitLab
|
# Stub for Database.postgresql? from GitLab
|
||||||
def self.postgresql?
|
def self.postgresql?
|
||||||
ActiveRecord::Base.configurations[Rails.env]['adapter'].casecmp('postgresql').zero?
|
ActiveRecord::Base.configurations[Rails.env]['adapter'].casecmp('postgresql').zero?
|
||||||
|
@ -315,7 +329,7 @@ module Mastodon
|
||||||
table = Arel::Table.new(table_name)
|
table = Arel::Table.new(table_name)
|
||||||
|
|
||||||
total = estimate_rows_in_table(table_name).to_i
|
total = estimate_rows_in_table(table_name).to_i
|
||||||
if total == 0
|
if total < 1
|
||||||
count_arel = table.project(Arel.star.count.as('count'))
|
count_arel = table.project(Arel.star.count.as('count'))
|
||||||
count_arel = yield table, count_arel if block_given?
|
count_arel = yield table, count_arel if block_given?
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
0
|
2
|
||||||
end
|
end
|
||||||
|
|
||||||
def flags
|
def flags
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
module Paperclip
|
module Paperclip
|
||||||
module AttachmentExtensions
|
module AttachmentExtensions
|
||||||
|
def meta
|
||||||
|
instance_read(:meta)
|
||||||
|
end
|
||||||
|
|
||||||
# We overwrite this method to support delayed processing in
|
# We overwrite this method to support delayed processing in
|
||||||
# Sidekiq. Since we process the original file to reduce disk
|
# Sidekiq. Since we process the original file to reduce disk
|
||||||
# usage, and we still want to generate thumbnails straight
|
# usage, and we still want to generate thumbnails straight
|
||||||
|
|
|
@ -100,7 +100,8 @@ end
|
||||||
|
|
||||||
module Paperclip
|
module Paperclip
|
||||||
# This transcoder is only to be used for the MediaAttachment model
|
# This transcoder is only to be used for the MediaAttachment model
|
||||||
# to convert animated gifs to webm
|
# to convert animated GIFs to videos
|
||||||
|
|
||||||
class GifTranscoder < Paperclip::Processor
|
class GifTranscoder < Paperclip::Processor
|
||||||
def make
|
def make
|
||||||
return File.open(@file.path) unless needs_convert?
|
return File.open(@file.path) unless needs_convert?
|
||||||
|
|
|
@ -31,21 +31,17 @@ module Paperclip
|
||||||
private
|
private
|
||||||
|
|
||||||
def extract_image_from_file!
|
def extract_image_from_file!
|
||||||
::Av.logger = Paperclip.logger
|
|
||||||
|
|
||||||
cli = ::Av.cli
|
|
||||||
dst = Tempfile.new([File.basename(@file.path, '.*'), '.png'])
|
dst = Tempfile.new([File.basename(@file.path, '.*'), '.png'])
|
||||||
dst.binmode
|
dst.binmode
|
||||||
|
|
||||||
cli.add_source(@file.path)
|
|
||||||
cli.add_destination(dst.path)
|
|
||||||
cli.add_output_param loglevel: 'fatal'
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
cli.run
|
command = Terrapin::CommandLine.new('ffmpeg', '-i :source -loglevel :loglevel -y :destination', logger: Paperclip.logger)
|
||||||
rescue Cocaine::ExitStatusError, ::Av::CommandError
|
command.run(source: @file.path, destination: dst.path, loglevel: 'fatal')
|
||||||
|
rescue Terrapin::ExitStatusError
|
||||||
dst.close(true)
|
dst.close(true)
|
||||||
return nil
|
return nil
|
||||||
|
rescue Terrapin::CommandNotFoundError
|
||||||
|
raise Paperclip::Errors::CommandNotFoundError, 'Could not run the `ffmpeg` command. Please install ffmpeg.'
|
||||||
end
|
end
|
||||||
|
|
||||||
dst
|
dst
|
||||||
|
|
|
@ -17,9 +17,9 @@ module Paperclip
|
||||||
|
|
||||||
def cache_current_values
|
def cache_current_values
|
||||||
@original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
|
@original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
|
||||||
@size = @target.response.content_length
|
|
||||||
@tempfile = copy_to_tempfile(@target)
|
@tempfile = copy_to_tempfile(@target)
|
||||||
@content_type = ContentTypeDetector.new(@tempfile.path).detect
|
@content_type = ContentTypeDetector.new(@tempfile.path).detect
|
||||||
|
@size = File.size(@tempfile)
|
||||||
end
|
end
|
||||||
|
|
||||||
def copy_to_tempfile(source)
|
def copy_to_tempfile(source)
|
||||||
|
|
102
lib/paperclip/transcoder.rb
Normal file
102
lib/paperclip/transcoder.rb
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Paperclip
|
||||||
|
# This transcoder is only to be used for the MediaAttachment model
|
||||||
|
# to check when uploaded videos are actually gifv's
|
||||||
|
class Transcoder < Paperclip::Processor
|
||||||
|
def initialize(file, options = {}, attachment = nil)
|
||||||
|
super
|
||||||
|
|
||||||
|
@current_format = File.extname(@file.path)
|
||||||
|
@basename = File.basename(@file.path, @current_format)
|
||||||
|
@format = options[:format]
|
||||||
|
@time = options[:time] || 3
|
||||||
|
@passthrough_options = options[:passthrough_options]
|
||||||
|
@convert_options = options[:convert_options].dup
|
||||||
|
end
|
||||||
|
|
||||||
|
def make
|
||||||
|
metadata = VideoMetadataExtractor.new(@file.path)
|
||||||
|
|
||||||
|
unless metadata.valid?
|
||||||
|
log("Unsupported file #{@file.path}")
|
||||||
|
return File.open(@file.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
update_attachment_type(metadata)
|
||||||
|
update_options_from_metadata(metadata)
|
||||||
|
|
||||||
|
destination = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
|
||||||
|
destination.binmode
|
||||||
|
|
||||||
|
@output_options = @convert_options[:output]&.dup || {}
|
||||||
|
@input_options = @convert_options[:input]&.dup || {}
|
||||||
|
|
||||||
|
case @format.to_s
|
||||||
|
when /jpg$/, /jpeg$/, /png$/, /gif$/
|
||||||
|
@input_options['ss'] = @time
|
||||||
|
|
||||||
|
@output_options['f'] = 'image2'
|
||||||
|
@output_options['vframes'] = 1
|
||||||
|
when 'mp4'
|
||||||
|
@output_options['acodec'] = 'aac'
|
||||||
|
@output_options['strict'] = 'experimental'
|
||||||
|
end
|
||||||
|
|
||||||
|
command_arguments, interpolations = prepare_command(destination)
|
||||||
|
|
||||||
|
begin
|
||||||
|
command = Terrapin::CommandLine.new('ffmpeg', command_arguments.join(' '), logger: Paperclip.logger)
|
||||||
|
command.run(interpolations)
|
||||||
|
rescue Terrapin::ExitStatusError => e
|
||||||
|
raise Paperclip::Error, "Error while transcoding #{@basename}: #{e}"
|
||||||
|
rescue Terrapin::CommandNotFoundError
|
||||||
|
raise Paperclip::Errors::CommandNotFoundError, 'Could not run the `ffmpeg` command. Please install ffmpeg.'
|
||||||
|
end
|
||||||
|
|
||||||
|
destination
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def prepare_command(destination)
|
||||||
|
command_arguments = ['-nostdin']
|
||||||
|
interpolations = {}
|
||||||
|
interpolation_keys = 0
|
||||||
|
|
||||||
|
@input_options.each_pair do |key, value|
|
||||||
|
interpolation_key = interpolation_keys
|
||||||
|
command_arguments << "-#{key} :#{interpolation_key}"
|
||||||
|
interpolations[interpolation_key] = value
|
||||||
|
interpolation_keys += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
command_arguments << '-i :source'
|
||||||
|
interpolations[:source] = @file.path
|
||||||
|
|
||||||
|
@output_options.each_pair do |key, value|
|
||||||
|
interpolation_key = interpolation_keys
|
||||||
|
command_arguments << "-#{key} :#{interpolation_key}"
|
||||||
|
interpolations[interpolation_key] = value
|
||||||
|
interpolation_keys += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
command_arguments << '-y :destination'
|
||||||
|
interpolations[:destination] = destination.path
|
||||||
|
|
||||||
|
[command_arguments, interpolations]
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_options_from_metadata(metadata)
|
||||||
|
return unless @passthrough_options && @passthrough_options[:video_codecs].include?(metadata.video_codec) && @passthrough_options[:audio_codecs].include?(metadata.audio_codec) && @passthrough_options[:colorspaces].include?(metadata.colorspace)
|
||||||
|
|
||||||
|
@format = @passthrough_options[:options][:format] || @format
|
||||||
|
@time = @passthrough_options[:options][:time] || @time
|
||||||
|
@convert_options = @passthrough_options[:options][:convert_options].dup
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_attachment_type(metadata)
|
||||||
|
@attachment.instance.type = MediaAttachment.types[:gifv] unless metadata.audio_codec
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue