Compare commits

..

59 Commits

Author SHA1 Message Date
khr 1e31148c57 Merge tag 'v2.6.5' into cybrespace 2018-12-21 12:23:13 -08:00
Eugen Rochko 887f9de6dc Bump version to 2.6.5 2018-12-02 17:04:03 +01:00
ThibG e625425c8f Include replies to list owner and replies to list members in list statuses (#9324) 2018-12-02 17:02:44 +01:00
ThibG f13d08314e Preload common JSON-LD contexts (#9412)
Fixes #9411
2018-12-02 16:50:28 +01:00
Eugen Rochko 13979a84f9 Bump version to 2.6.4 2018-11-30 19:54:24 +01:00
Eugen Rochko 82570019ba Remove npm-run-all dependency (#9401)
Fix #9359
2018-11-30 19:51:59 +01:00
Eugen Rochko a1216e6315 Bump version to 2.6.3 2018-11-30 03:08:37 +01:00
Eugen Rochko 34de90c486 Fix TLS handshake timeout not being enforced (#9381)
Follow-up to #9329
2018-11-30 03:02:55 +01:00
Eugen Rochko 442f335504 Skip deliveries to inboxes that have already been marked as unavailable (#9358) 2018-11-30 03:02:55 +01:00
Eugen Rochko 58108b4481 Don't count suspended users in user count (#9380)
Fix #7637
2018-11-30 03:02:55 +01:00
Eugen Rochko cc0c1674f0 Fix nil error when no DNS addresses are found for host (#9379) 2018-11-30 03:02:55 +01:00
ThibG 49f49cf367 Allow hyphens in the middle of remote user names (#9345)
Fixes #9309

This only allows hyphens in the middle of a username, much like dots,
although I don't have a compelling reason to do so other than keeping
the changes minimal.
2018-11-30 03:02:55 +01:00
Hugo Gameiro ec20a5d53a add loglevel to ffmpeg in gif upload (#9368) 2018-11-30 03:02:55 +01:00
Eugen Rochko 404dc97fb0 Bump version to 2.6.2 2018-11-23 22:32:20 +01:00
Eugen Rochko a2cda74ba3 Fix connect timeout not being enforced (#9329)
* Fix connect timeout not being enforced

The loop was catching the timeout exception that should stop execution, so the next IP would no longer be within a timed block, which led to requests taking much longer than 10 seconds.

* Use timeout on each IP attempt, but limit to 2 attempts

* Fix code style issue

* Do not break Request#perform if no block given

* Update method stub in spec for Request

* Move timeout inside the begin/rescue block

* Use Resolv::DNS with timeout of 1 to get IP addresses

* Update Request spec to stub Resolv::DNS instead of Addrinfo

* Fix Resolve::DNS stubs in Request spec
2018-11-23 22:17:04 +01:00
valerauko 12bdd7dc5f Ignore JSON-LD profile in mime type comparison (#9179)
Ignore JSON-LD profile in mime type comparison
2018-11-23 22:16:13 +01:00
Renato "Lond" Cerqueira 15dcb414bf Touch account on successful response, change char shown when culled (#9293)
Just the color is not enough change since not everyone uses colored
terminals.
Touching the account makes it so that the account is not in the
threshold window in case of running again
2018-11-23 22:15:56 +01:00
Alexandre Alapetite 2c36d35784 WebSub: ATOM before RSS (#9302)
Hello,
The ATOM feed contains the hub declaration for WebSub, but the RSS
version does not.
RSS/ATOM readers will typically pick whichever version comes first, and
will thus not see the WebSub feature.
I therefore suggest putting the ATOM version first, as it is more
feature-rich than its RSS counterpart is.

Clients not compatible with ATOM would not pick it anyway due to the
different type attribute.

A more complicated alternative would be to declare the WebSub feature in
the RSS version as well, using something like the following code, and
ensuring that clients subscribed to the RSS version would receive PuSH
updates just like those subscribed to the ATOM version.

````xml
<rss version="2.0" xmlns:webfeeds="http://webfeeds.org/rss/1.0"
xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link rel="self" type="application/rss+xml"
href="https://diaspodon.fr/users/test.rss"/>
<atom:link rel="hub" href="https://diaspodon.fr/api/push"/>
</channel>
</rss>
```
2018-11-23 22:15:46 +01:00
Dan Hunsaker c0736c466c Update Nginx config for Nanobox apps (#9310)
The Nanobox files have gotten out of sync, a touch, with what Masto needs for Nginx settings. This PR updates them accordingly.
2018-11-23 22:15:41 +01:00
Eugen Rochko fa02f878fc Fix filter ID not being a string in REST API (#9303) 2018-11-23 22:15:22 +01:00
Eugen Rochko ecc58c0f23 Prevent multiple handlers for Delete of Actor from running (#9292) 2018-11-23 22:15:12 +01:00
Eugen Rochko 6d4438a6ae Remove intermediary arrays when creating hash maps from results (#9291) 2018-11-23 22:15:03 +01:00
mayaeh 01a8ab921e Fix "tootctl media remove" can't count the file size (#9288)
* Fixed an issue where "tootctl media remove" can not count the file size.

* Fixed the problem pointed out by codeclimate.
2018-11-23 22:14:51 +01:00
ThibG a3ef076160 Fix race condition causing shallow status with only a "favourited" attribute (#9272)
Fixes #9231
2018-11-23 22:14:42 +01:00
Eugen Rochko cd8575aef6 Fix null error introduced in #9270 (#9275) 2018-11-23 22:14:35 +01:00
ThibG 4ce6ed2021 Perform deep comparison for card data when receiving new props (#9270)
Fixes #9226
2018-11-23 22:14:25 +01:00
ThibG 886ef1cc38 Fix emoji update date processing (#9255) 2018-11-23 22:14:15 +01:00
ThibG d06a724b1c Check that twitter:player is valid before using it (#9254)
Fixes #9251
2018-11-23 22:14:08 +01:00
Eugen Rochko f73b7e77da Improve ActiveRecord connection in on_worker_boot (#9238)
This is how it looks in the example in the Puma README
2018-11-23 22:13:44 +01:00
Eugen Rochko 63f168c3bf Fix nil error regression from #9229 in tootctl media remove (#9239)
Fix #9237
2018-11-23 22:13:34 +01:00
Eugen Rochko 0f436de035 Add "Show thread" link to self-replies (#9228)
Fix #4716
2018-11-23 22:12:56 +01:00
Eugen Rochko 21fd335dd7 Display amount of freed disk space in tootctl media remove (#9229)
* Display amount of freed disk space in tootctl media remove

Fix #9213

* Fix code style issue
2018-11-23 22:12:50 +01:00
Eugen Rochko 4b2f254806 Fix form validation flash message color and input borders (#9235)
* Fix form validation flash message color and input borders

* Fix typo
2018-11-23 22:12:42 +01:00
Eugen Rochko b3c29ece47 Fix follow limit validator reporting lower number past threshold (#9230)
* Fix follow limit validator reporting lower number past threshold

* Avoid floating point follow limit
2018-11-23 22:12:33 +01:00
Eugen Rochko 330401bec0 Optimize the process of following someone (#9220)
* Eliminate extra accounts select query from FollowService

* Optimistically update follow state in web UI and hide loading bar

Fix #6205

* Asynchronize NotifyService in FollowService

And fix failing test

* Skip Webfinger resolve routine when called from FollowService if possible

If an account is ActivityPub, then webfinger re-resolving is not necessary
when called from FollowService. Improve options of ResolveAccountService
2018-11-23 22:12:26 +01:00
Eugen Rochko 5ee4fd4606 Increase default column width from 330px to 350px (#9227) 2018-11-23 22:12:15 +01:00
m.b 430499fbe1 Update resolve_url_service.rb (#9188) 2018-11-23 22:11:31 +01:00
Steven Tappert 449e6e451f Check for empty "last_status" before sorting DM column (#9207)
* Check for empty "last_status" before sorting

* Small touchups for codeclimate
2018-11-23 22:11:05 +01:00
LenPayne d510ef7375 Re-added add_user_automatic rake task (#3) 2018-11-20 07:29:00 +01:00
nightpool b9bab4e477 Fix embed width, light theme embeds (#2) 2018-11-20 07:27:41 +01:00
khr 9a49c503c8 Fix unnecessary scrollbar on compose drawer 2018-11-17 20:00:41 -08:00
khr 7c0624ccfa Merge branch 'theme_win95' into cybrespace 2018-11-17 17:27:22 -08:00
khr 6f6c4f2a50 Merge branch 'theme_light' into cybrespace 2018-11-17 17:26:59 -08:00
khr 42954f329a Merge branch 'theme_cybre' into cybrespace 2018-11-17 17:26:55 -08:00
khr a0ba41a78c Merge branch 'theme_base' into cybrespace 2018-11-17 17:26:47 -08:00
khr 6695882f19 Merge branches 'branding_cybre', 'dependency_whatinput', 'feature_512_char_toots', 'feature_disable_reply_counts', 'feature_hotlink_twitter_mentions', 'feature_cybrespace_locale', 'feature_doodlebox' and 'feature_longer_bios' into cybrespace 2018-11-17 17:26:31 -08:00
khr 35d7ae4974 Cybrespace branding changes 2018-11-17 17:26:07 -08:00
khr f40d8a3c37 Windows 95 theme 2018-11-17 17:21:37 -08:00
khr 9eab8a139f Cybrespace light theme 2018-11-17 16:30:57 -08:00
khr 7abb76fd6f Cybre dark theme 2018-11-17 16:22:42 -08:00
khr a0bce3a23f Fix media height weirdness, handle new status cards 2018-11-17 16:20:49 -08:00
chr 7dae98557c Base SCSS for cybrespace themes
Performs some baseline tweaks to the mastodon application scss that are
shared between all the cybrespace themes
2018-11-12 23:26:12 -08:00
Andrew 87eb1df45a Add whatinput dependency for more responsive focus styling 2018-11-12 23:00:14 -08:00
khr a01c7ce321 Cybrespace locale
An alternative locale that replaces "toot" with "ping", "favorite" with
"florp", and "boost" with "relay", as well as a few other miscellaneous
changes. Also includes alternate settings that make the en-CY locale
default instead of en-US, and fixes the timestamps.
2018-11-12 22:35:38 -08:00
khr ebed61cf88 Doodlebox
A modification to the Mastodon interface that allows you to select a
doodle icon in addition to uploading a media file from your computer.
The doodle interface allows you to draw using your mouse, finger, or
pointing device in several colors. It was originally developed by
Ondřej Hruška <ondra@ondrovo.com> for use on the GlitchSoc fork.
2018-11-12 21:53:37 -08:00
Andrew 99b3c18b28 Hotlink twitter mentions
Differentiate twitter mentions from normal mentions
2018-11-12 21:44:10 -08:00
khr df0855a91b Bio length -> 413 characters
Increase the cybre.space profile bio text length limit to 413
characters. Also modifies the account settings page to automatically
resize the textbox to the size of the contained text, so that it's
easier to type longer bios.
2018-11-12 21:28:25 -08:00
Andrew bf7b956a99 Remove reply counts on non-expanded view 2018-11-12 20:16:48 -08:00
Andrew 5834fc5aa1 Feature: 512-character limit in the toot box on client and serverside 2018-11-12 19:58:20 -08:00
1747 changed files with 35720 additions and 86111 deletions

View File

@ -3,7 +3,7 @@ version: 2
aliases: aliases:
- &defaults - &defaults
docker: docker:
- image: circleci/ruby:2.6-stretch-node - image: circleci/ruby:2.5.1-stretch-node
environment: &ruby_environment environment: &ruby_environment
BUNDLE_APP_CONFIG: ./.bundle/ BUNDLE_APP_CONFIG: ./.bundle/
DB_HOST: localhost DB_HOST: localhost
@ -98,21 +98,21 @@ jobs:
<<: *defaults <<: *defaults
<<: *install_steps <<: *install_steps
install-ruby2.6:
<<: *defaults
<<: *install_ruby_dependencies
install-ruby2.5: install-ruby2.5:
<<: *defaults <<: *defaults
docker:
- image: circleci/ruby:2.5-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies <<: *install_ruby_dependencies
install-ruby2.4: install-ruby2.4:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/ruby:2.4-stretch-node - image: circleci/ruby:2.4.4-stretch-node
environment: *ruby_environment
<<: *install_ruby_dependencies
install-ruby2.3:
<<: *defaults
docker:
- image: circleci/ruby:2.3.7-stretch-node
environment: *ruby_environment environment: *ruby_environment
<<: *install_ruby_dependencies <<: *install_ruby_dependencies
@ -128,43 +128,43 @@ jobs:
- ./mastodon/public/assets - ./mastodon/public/assets
- ./mastodon/public/packs-test/ - ./mastodon/public/packs-test/
test-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.6-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:5-alpine
<<: *test_steps
test-ruby2.5: test-ruby2.5:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/ruby:2.5-stretch-node - image: circleci/ruby:2.5.1-stretch-node
environment: *ruby_environment environment: *ruby_environment
- image: circleci/postgres:10.6-alpine - image: circleci/postgres:10.3-alpine
environment: environment:
POSTGRES_USER: root POSTGRES_USER: root
- image: circleci/redis:5-alpine - image: circleci/redis:4.0.9-alpine
<<: *test_steps <<: *test_steps
test-ruby2.4: test-ruby2.4:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/ruby:2.4-stretch-node - image: circleci/ruby:2.4.4-stretch-node
environment: *ruby_environment environment: *ruby_environment
- image: circleci/postgres:10.6-alpine - image: circleci/postgres:10.3-alpine
environment: environment:
POSTGRES_USER: root POSTGRES_USER: root
- image: circleci/redis:5-alpine - image: circleci/redis:4.0.9-alpine
<<: *test_steps
test-ruby2.3:
<<: *defaults
docker:
- image: circleci/ruby:2.3.7-stretch-node
environment: *ruby_environment
- image: circleci/postgres:10.3-alpine
environment:
POSTGRES_USER: root
- image: circleci/redis:4.0.9-alpine
<<: *test_steps <<: *test_steps
test-webui: test-webui:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/node:12.9-stretch - image: circleci/node:8.11.1-stretch
steps: steps:
- *attach_workspace - *attach_workspace
- run: ./bin/retry yarn test:jest - run: ./bin/retry yarn test:jest
@ -173,35 +173,30 @@ jobs:
<<: *defaults <<: *defaults
steps: steps:
- *attach_workspace - *attach_workspace
- *install_system_dependencies
- run: bundle exec i18n-tasks check-normalized - run: bundle exec i18n-tasks check-normalized
- run: bundle exec i18n-tasks unused -l en - run: bundle exec i18n-tasks unused
- run: bundle exec i18n-tasks missing -t plural
- run: bundle exec i18n-tasks check-consistent-interpolations - run: bundle exec i18n-tasks check-consistent-interpolations
- run: bundle exec rake repo:check_locales_files
workflows: workflows:
version: 2 version: 2
build-and-test: build-and-test:
jobs: jobs:
- install - install
- install-ruby2.6:
requires:
- install
- install-ruby2.5: - install-ruby2.5:
requires: requires:
- install - install
- install-ruby2.6
- install-ruby2.4: - install-ruby2.4:
requires: requires:
- install - install
- install-ruby2.6 - install-ruby2.5
- install-ruby2.3:
requires:
- install
- install-ruby2.5
- build: - build:
requires: requires:
- install-ruby2.6 - install-ruby2.5
- test-ruby2.6:
requires:
- install-ruby2.6
- build
- test-ruby2.5: - test-ruby2.5:
requires: requires:
- install-ruby2.5 - install-ruby2.5
@ -210,9 +205,13 @@ workflows:
requires: requires:
- install-ruby2.4 - install-ruby2.4
- build - build
- test-ruby2.3:
requires:
- install-ruby2.3
- build
- test-webui: - test-webui:
requires: requires:
- install - install
- check-i18n: - check-i18n:
requires: requires:
- install-ruby2.6 - install-ruby2.5

View File

@ -27,11 +27,11 @@ plugins:
enabled: true enabled: true
eslint: eslint:
enabled: true enabled: true
channel: eslint-5 channel: eslint-4
rubocop: rubocop:
enabled: true enabled: true
channel: rubocop-0-71 channel: rubocop-0-54
sass-lint: scss-lint:
enabled: true enabled: true
exclude_patterns: exclude_patterns:
- spec/ - spec/

View File

@ -1,10 +0,0 @@
version: 1
update_configs:
- package_manager: "ruby:bundler"
directory: "/"
update_schedule: "weekly"
- package_manager: "javascript"
directory: "/"
update_schedule: "weekly"

View File

@ -11,14 +11,24 @@ DB_NAME=gonano
DB_PASS=$DATA_DB_PASS DB_PASS=$DATA_DB_PASS
DB_PORT=5432 DB_PORT=5432
# DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
# Optional ElasticSearch configuration # Optional ElasticSearch configuration
ES_ENABLED=true ES_ENABLED=true
ES_HOST=$DATA_ELASTIC_HOST ES_HOST=$DATA_ELASTIC_HOST
ES_PORT=9200 ES_PORT=9200
BIND=0.0.0.0 # Optimizations
LD_PRELOAD=/data/lib/libjemalloc.so
# ImageMagick optimizations
MAGICK_TEMPORARY_PATH=/app/tmp
MAGICK_MEMORY_LIMIT=128MiB
MAGICK_MAP_LIMIT=64MiB
MAGICK_TIME_LIMIT=15
MAGICK_AREA_LIMIT=16MP
MAGICK_WIDTH_LIMIT=8KP
MAGICK_HEIGHT_LIMIT=8KP
# Federation # Federation
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation. # Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
@ -74,7 +84,6 @@ SMTP_PORT=587
SMTP_LOGIN=$SMTP_LOGIN SMTP_LOGIN=$SMTP_LOGIN
SMTP_PASSWORD=$SMTP_PASSWORD SMTP_PASSWORD=$SMTP_PASSWORD
SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN #SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail #SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain #SMTP_AUTH_METHOD=plain
@ -88,17 +97,9 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# PAPERCLIP_ROOT_URL=/system # PAPERCLIP_ROOT_URL=/system
# Optional asset host for multi-server setups # Optional asset host for multi-server setups
# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN
# if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://example.com/
# CDN_HOST=https://assets.example.com # CDN_HOST=https://assets.example.com
# S3 (optional) # S3 (optional)
# The attachment host must allow cross origin request from WEB_DOMAIN or
# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://192.168.1.123:9000/
# S3_ENABLED=true # S3_ENABLED=true
# S3_BUCKET= # S3_BUCKET=
# AWS_ACCESS_KEY_ID= # AWS_ACCESS_KEY_ID=
@ -108,8 +109,6 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# S3_HOSTNAME=192.168.1.123:9000 # S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details) # S3 (Minio Config (optional) Please check Minio instance for details)
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true # S3_ENABLED=true
# S3_BUCKET= # S3_BUCKET=
# AWS_ACCESS_KEY_ID= # AWS_ACCESS_KEY_ID=
@ -120,30 +119,12 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# S3_ENDPOINT= # S3_ENDPOINT=
# S3_SIGNATURE_VERSION= # S3_SIGNATURE_VERSION=
# Google Cloud Storage (optional)
# Use S3 compatible API. Since GCS does not support Multipart Upload,
# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload.
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=storage.googleapis.com
# S3_ENDPOINT=https://storage.googleapis.com
# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes
# Swift (optional) # Swift (optional)
# The attachment host must allow cross origin request - see the description
# above.
# SWIFT_ENABLED=true # SWIFT_ENABLED=true
# SWIFT_USERNAME= # SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name # For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT= # SWIFT_TENANT=
# SWIFT_PASSWORD= # SWIFT_PASSWORD=
# Some OpenStack V3 providers require PROJECT_ID (optional)
# SWIFT_PROJECT_ID=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid # Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load. # issues with token rate-limiting during high load.
# SWIFT_AUTH_URL= # SWIFT_AUTH_URL=
@ -190,8 +171,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# The pam environment variable "email" is provided by: # The pam environment variable "email" is provided by:
# https://github.com/devkral/pam_email_extractor # https://github.com/devkral/pam_email_extractor
# PAM_ENABLED=true # PAM_ENABLED=true
# Fallback email domain for email address generation (LOCAL_DOMAIN by default) # Fallback Suffix for email address generation (nil by default)
# PAM_EMAIL_DOMAIN=example.com # PAM_DEFAULT_SUFFIX=pam
# Name of the pam service (pam "auth" section is evaluated) # Name of the pam service (pam "auth" section is evaluated)
# PAM_DEFAULT_SERVICE=rpam # PAM_DEFAULT_SERVICE=rpam
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default) # Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
@ -239,14 +220,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true # SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1" # SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" # SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241" # SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42"
# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME="urn:oid:2.5.4.42"
# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME="urn:oid:2.5.4.4"
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1" # SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED= # SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL= # SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
# Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118
# Access control for hidden service.
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true

View File

@ -10,7 +10,6 @@ DB_NAME=postgres
DB_PASS= DB_PASS=
DB_PORT=5432 DB_PORT=5432
# Optional ElasticSearch configuration # Optional ElasticSearch configuration
# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set)
# ES_ENABLED=true # ES_ENABLED=true
# ES_HOST=es # ES_HOST=es
# ES_PORT=9200 # ES_PORT=9200
@ -69,7 +68,6 @@ SMTP_PORT=587
SMTP_LOGIN= SMTP_LOGIN=
SMTP_PASSWORD= SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN #SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail #SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain #SMTP_AUTH_METHOD=plain
@ -115,20 +113,6 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_ENDPOINT= # S3_ENDPOINT=
# S3_SIGNATURE_VERSION= # S3_SIGNATURE_VERSION=
# Google Cloud Storage (optional)
# Use S3 compatible API. Since GCS does not support Multipart Upload,
# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload.
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=storage.googleapis.com
# S3_ENDPOINT=https://storage.googleapis.com
# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes
# Swift (optional) # Swift (optional)
# The attachment host must allow cross origin request - see the description # The attachment host must allow cross origin request - see the description
# above. # above.
@ -178,7 +162,7 @@ STREAMING_CLUSTER_NUM=1
# LDAP_BIND_DN= # LDAP_BIND_DN=
# LDAP_PASSWORD= # LDAP_PASSWORD=
# LDAP_UID=cn # LDAP_UID=cn
# LDAP_SEARCH_FILTER=%{uid}=%{email} # LDAP_SEARCH_FILTER="%{uid}=%{email}"
# PAM authentication (optional) # PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable # PAM authentication uses for the email generation the "email" pam variable

View File

@ -1,13 +1,30 @@
/build/** # See https://help.github.com/articles/ignoring-files for more about ignoring files.
/coverage/** #
/db/** # If you find yourself ignoring temporary files generated by your text editor
/lib/** # or operating system, you probably want to add a global ignore instead:
/log/** # git config --global core.excludesfile '~/.gitignore_global'
/node_modules/**
/nonobox/** # Ignore bundler config.
/public/** /.bundle
!/public/embed.js
/spec/** # Ignore the default SQLite database.
/tmp/** /db/*.sqlite3
/vendor/** /db/*.sqlite3-journal
!.eslintrc.js
# Ignore all logfiles and tempfiles.
/log/*
!/log/.keep
/tmp
coverage
public/system
public/assets
.env
.env.production
node_modules/
neo4j/
# Ignore Vagrant files
.vagrant/
# Ignore Capistrano customizations
config/deploy/*

View File

@ -1,204 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true,
jest: true,
},
globals: {
ATTACHMENT_HOST: false,
},
parser: 'babel-eslint',
plugins: [
'react',
'jsx-a11y',
'import',
'promise',
],
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
ecmaVersion: 2018,
},
settings: {
react: {
version: 'detect',
},
'import/extensions': [
'.js',
],
'import/ignore': [
'node_modules',
'\\.(css|scss|json)$',
],
'import/resolver': {
node: {
paths: ['app/javascript'],
},
},
},
rules: {
'brace-style': 'warn',
'comma-dangle': ['error', 'always-multiline'],
'comma-spacing': [
'warn',
{
before: false,
after: true,
},
],
'comma-style': ['warn', 'last'],
'consistent-return': 'error',
'dot-notation': 'error',
eqeqeq: 'error',
indent: ['warn', 2],
'jsx-quotes': ['error', 'prefer-single'],
'no-catch-shadow': 'error',
'no-cond-assign': 'error',
'no-console': [
'warn',
{
allow: [
'error',
'warn',
],
},
],
'no-fallthrough': 'error',
'no-irregular-whitespace': 'error',
'no-mixed-spaces-and-tabs': 'warn',
'no-nested-ternary': 'warn',
'no-trailing-spaces': 'warn',
'no-undef': 'error',
'no-unreachable': 'error',
'no-unused-expressions': 'error',
'no-unused-vars': [
'error',
{
vars: 'all',
args: 'after-used',
ignoreRestSiblings: true,
},
],
'object-curly-spacing': ['error', 'always'],
'padded-blocks': [
'error',
{
classes: 'always',
},
],
quotes: ['error', 'single'],
semi: 'error',
strict: 'off',
'valid-typeof': 'error',
'react/jsx-boolean-value': 'error',
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
'react/jsx-curly-spacing': 'error',
'react/jsx-equals-spacing': 'error',
'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'],
'react/jsx-indent': ['error', 2],
'react/jsx-no-bind': 'error',
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-undef': 'error',
'react/jsx-tag-spacing': 'error',
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/jsx-wrap-multilines': 'error',
'react/no-multi-comp': 'off',
'react/no-string-refs': 'error',
'react/prop-types': 'error',
'react/self-closing-comp': 'error',
'jsx-a11y/accessible-emoji': 'warn',
'jsx-a11y/alt-text': 'warn',
'jsx-a11y/anchor-has-content': 'warn',
'jsx-a11y/anchor-is-valid': [
'warn',
{
components: [
'Link',
'NavLink',
],
specialLink: [
'to',
],
aspect: [
'noHref',
'invalidHref',
'preferButton',
],
},
],
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
'jsx-a11y/aria-props': 'warn',
'jsx-a11y/aria-proptypes': 'warn',
'jsx-a11y/aria-role': 'warn',
'jsx-a11y/aria-unsupported-elements': 'warn',
'jsx-a11y/heading-has-content': 'warn',
'jsx-a11y/html-has-lang': 'warn',
'jsx-a11y/iframe-has-title': 'warn',
'jsx-a11y/img-redundant-alt': 'warn',
'jsx-a11y/interactive-supports-focus': 'warn',
'jsx-a11y/label-has-for': 'off',
'jsx-a11y/mouse-events-have-key-events': 'warn',
'jsx-a11y/no-access-key': 'warn',
'jsx-a11y/no-distracting-elements': 'warn',
'jsx-a11y/no-noninteractive-element-interactions': [
'warn',
{
handlers: [
'onClick',
],
},
],
'jsx-a11y/no-onchange': 'warn',
'jsx-a11y/no-redundant-roles': 'warn',
'jsx-a11y/no-static-element-interactions': [
'warn',
{
handlers: [
'onClick',
],
},
],
'jsx-a11y/role-has-required-aria-props': 'warn',
'jsx-a11y/role-supports-aria-props': 'off',
'jsx-a11y/scope': 'warn',
'jsx-a11y/tabindex-no-positive': 'warn',
'import/extensions': [
'error',
'always',
{
js: 'never',
},
],
'import/newline-after-import': 'error',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'config/webpack/**',
'app/javascript/mastodon/test_setup.js',
'app/javascript/**/__tests__/**',
],
},
],
'import/no-unresolved': 'error',
'import/no-webpack-loader-syntax': 'error',
'promise/catch-or-return': 'error',
},
};

170
.eslintrc.yml Normal file
View File

@ -0,0 +1,170 @@
---
root: true
env:
browser: true
node: true
es6: true
jest: true
globals:
ATTACHMENT_HOST: false
parser: babel-eslint
plugins:
- react
- jsx-a11y
- import
- promise
parserOptions:
sourceType: module
ecmaFeatures:
experimentalObjectRestSpread: true
jsx: true
ecmaVersion: 2018
settings:
import/extensions:
- .js
import/ignore:
- node_modules
- \\.(css|scss|json)$
rules:
brace-style: warn
comma-dangle:
- error
- always-multiline
comma-spacing:
- warn
- before: false
after: true
comma-style:
- warn
- last
consistent-return: error
dot-notation: error
eqeqeq: error
indent:
- warn
- 2
jsx-quotes:
- error
- prefer-single
no-catch-shadow: error
no-cond-assign: error
no-console:
- warn
- allow:
- error
- warn
no-fallthrough: error
no-irregular-whitespace: error
no-mixed-spaces-and-tabs: warn
no-nested-ternary: warn
no-trailing-spaces: warn
no-undef: error
no-unreachable: error
no-unused-expressions: error
no-unused-vars:
- error
- vars: all
args: after-used
ignoreRestSiblings: true
object-curly-spacing:
- error
- always
padded-blocks:
- error
- classes: always
quotes:
- error
- single
semi: error
strict: off
valid-typeof: error
react/jsx-boolean-value: error
react/jsx-closing-bracket-location:
- error
- line-aligned
react/jsx-curly-spacing: error
react/jsx-equals-spacing: error
react/jsx-first-prop-new-line:
- error
- multiline-multiprop
react/jsx-indent:
- error
- 2
react/jsx-no-bind: error
react/jsx-no-duplicate-props: error
react/jsx-no-undef: error
react/jsx-tag-spacing: error
react/jsx-uses-react: error
react/jsx-uses-vars: error
react/jsx-wrap-multilines: error
react/no-multi-comp: off
react/no-string-refs: error
react/prop-types: error
react/self-closing-comp: error
jsx-a11y/accessible-emoji: warn
jsx-a11y/alt-text: warn
jsx-a11y/anchor-has-content: warn
jsx-a11y/anchor-is-valid:
- warn
- components:
- Link
- NavLink
specialLink:
- to
aspect:
- noHref
- invalidHref
- preferButton
jsx-a11y/aria-activedescendant-has-tabindex: warn
jsx-a11y/aria-props: warn
jsx-a11y/aria-proptypes: warn
jsx-a11y/aria-role: warn
jsx-a11y/aria-unsupported-elements: warn
jsx-a11y/heading-has-content: warn
jsx-a11y/html-has-lang: warn
jsx-a11y/iframe-has-title: warn
jsx-a11y/img-redundant-alt: warn
jsx-a11y/interactive-supports-focus: warn
jsx-a11y/label-has-for: off
jsx-a11y/mouse-events-have-key-events: warn
jsx-a11y/no-access-key: warn
jsx-a11y/no-distracting-elements: warn
jsx-a11y/no-noninteractive-element-interactions:
- warn
- handlers:
- onClick
jsx-a11y/no-onchange: warn
jsx-a11y/no-redundant-roles: warn
jsx-a11y/no-static-element-interactions:
- warn
- handlers:
- onClick
jsx-a11y/role-has-required-aria-props: warn
jsx-a11y/role-supports-aria-props: off
jsx-a11y/scope: warn
jsx-a11y/tabindex-no-positive: warn
import/extensions:
- error
- always
- js: never
import/newline-after-import: error
import/no-extraneous-dependencies:
- error
- devDependencies:
- "config/webpack/**"
- "app/javascript/mastodon/test_setup.js"
- "app/javascript/**/__tests__/**"
import/no-unresolved: error
import/no-webpack-loader-syntax: error
promise/catch-or-return: error

2
.github/FUNDING.yml vendored
View File

@ -1,2 +0,0 @@
patreon: mastodon
open_collective: mastodon

9
.postcssrc.yml Normal file
View File

@ -0,0 +1,9 @@
plugins:
postcss-smart-import: {}
precss: {}
autoprefixer:
browsers:
- last 2 versions
- IE >= 11
- iOS >= 9
postcss-object-fit-images: {}

View File

@ -1,6 +1,3 @@
require:
- rubocop-rails
AllCops: AllCops:
TargetRubyVersion: 2.3 TargetRubyVersion: 2.3
Exclude: Exclude:
@ -83,10 +80,7 @@ Rails/HttpStatus:
Rails/Exit: Rails/Exit:
Exclude: Exclude:
- 'lib/mastodon/*' - 'lib/mastodon/*'
- 'lib/cli.rb' - 'lib/cli'
Rails/HelperInstanceVariable:
Enabled: false
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false

View File

@ -1 +1 @@
2.6.5 2.5.3

View File

@ -1,37 +0,0 @@
# Linter Documentation:
# https://github.com/sasstools/sass-lint/tree/v1.13.1/docs/options
files:
include: app/javascript/styles/**/*.scss
ignore:
- app/javascript/styles/mastodon/reset.scss
rules:
# Disallows
no-color-literals: 0
no-css-comments: 0
no-duplicate-properties: 0
no-ids: 0
no-important: 0
no-mergeable-selectors: 0
no-misspelled-properties: 0
no-qualifying-elements: 0
no-transition-all: 0
no-vendor-prefixes: 0
# Nesting
force-element-nesting: 0
force-attribute-nesting: 0
force-pseudo-nesting: 0
# Name Formats
class-name-format: 0
leading-zero: 0
# Style Guide
attribute-quotes: 0
hex-length: 0
indentation: 0
nesting-depth: 0
property-sort-order: 0
quotes: 0

264
.scss-lint.yml Normal file
View File

@ -0,0 +1,264 @@
# Linter Documentation:
# https://github.com/brigade/scss-lint/blob/v0.42.2/lib/scss_lint/linter/README.md
scss_files: 'app/javascript/styles/**/*.scss'
exclude:
- app/javascript/styles/reset.scss
linters:
# Reports when you use improper spacing around ! (the "bang") in !default,
# !global, !important, and !optional flags.
BangFormat:
enabled: false
# Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
enabled: false
# Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes).
ChainedClasses:
enabled: false
# Prefer hexadecimal color codes over color keywords.
# (e.g. `color: green` is a color keyword)
ColorKeyword:
enabled: false
# Prefer color literals (keywords or hexadecimal codes) to be used only in
# variable declarations. They should be referred to via variables everywhere
# else.
ColorVariable:
enabled: true
# Which form of comments to prefer in CSS.
Comment:
enabled: false
# Reports @debug statements (which you probably left behind accidentally).
DebugStatement:
enabled: false
# Rule sets should be ordered as follows:
# - @extend declarations
# - @include declarations without inner @content
# - properties, @include declarations with inner @content
# - nested rule sets.
DeclarationOrder:
enabled: false
# `scss-lint:disable` control comments should be preceded by a comment
# explaining why these linters are being disabled for this file.
# See https://github.com/brigade/scss-lint#disabling-linters-via-source for
# more information.
DisableLinterReason:
enabled: true
# Reports when you define the same property twice in a single rule set.
DuplicateProperty:
enabled: false
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
enabled: true
# Reports when you have an empty rule set.
EmptyRule:
enabled: true
# Reports when you have an @extend directive.
ExtendDirective:
enabled: false
# Files should always have a final newline. This results in better diffs
# when adding lines to the file, since SCM systems such as git won't
# think that you touched the last line.
FinalNewline:
enabled: false
# HEX colors should use three-character values where possible.
HexLength:
enabled: false
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
HexNotation:
enabled: true
# Avoid using ID selectors.
IdSelector:
enabled: false
# The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension.
ImportPath:
enabled: false
# Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code.
ImportantRule:
enabled: false
# Indentation should always be done in increments of 2 spaces.
Indentation:
enabled: true
width: 2
# Don't write leading zeros for numeric values with a decimal point.
LeadingZero:
enabled: false
# Reports when you define the same selector twice in a single sheet.
MergeableSelector:
enabled: false
# Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores.
NameFormat:
enabled: false
# Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
# Always use placeholder selectors in @extend.
PlaceholderInExtend:
enabled: false
# Sort properties in a strict order.
PropertySortOrder:
enabled: false
# Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties).
PropertySpelling:
enabled: false
# Configure which units are allowed for property values.
PropertyUnits:
enabled: false
# Pseudo-elements, like ::before, and ::first-letter, should be declared
# with two colons. Pseudo-classes, like :hover and :first-child, should
# be declared with one colon.
PseudoElement:
enabled: true
# Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement:
enabled: false
# Don't write selectors with a depth of applicability greater than 3.
SelectorDepth:
enabled: false
# Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case.
SelectorFormat:
enabled: false
convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it.
Shorthand:
enabled: true
# Each property should have its own line, except in the special case of
# single line rulesets.
SingleLinePerProperty:
enabled: true
allow_single_line_rule_sets: true
# Split selectors onto separate lines after each comma, and have each
# individual selector occupy a single line.
SingleLinePerSelector:
enabled: true
# Commas in lists should be followed by a space.
SpaceAfterComma:
enabled: false
# Properties should be formatted with a single space separating the colon
# from the property's value.
SpaceAfterPropertyColon:
enabled: true
# Properties should be formatted with no space between the name and the
# colon.
SpaceAfterPropertyName:
enabled: true
# Variables should be formatted with a single space separating the colon
# from the variable's value.
SpaceAfterVariableColon:
enabled: true
# Variables should be formatted with no space between the name and the
# colon.
SpaceAfterVariableName:
enabled: false
# Operators should be formatted with a single space on both sides of an
# infix operator.
SpaceAroundOperator:
enabled: true
# Opening braces should be preceded by a single space.
SpaceBeforeBrace:
enabled: true
# Parentheses should not be padded with spaces.
SpaceBetweenParens:
enabled: false
# Enforces that string literals should be written with a consistent form
# of quotes (single or double).
StringQuotes:
enabled: false
# Property values, @extend, @include, and @import directives, and variable
# declarations should always end with a semicolon.
TrailingSemicolon:
enabled: true
# Reports lines containing trailing whitespace.
TrailingWhitespace:
enabled: true
# Don't write trailing zeros for numeric values with a decimal point.
TrailingZero:
enabled: false
# Don't use the `all` keyword to specify transition properties.
TransitionAll:
enabled: false
# Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa:
enabled: false
# Do not use parent selector references (&) when they would otherwise
# be unnecessary.
UnnecessaryParentReference:
enabled: false
# URLs should be valid and not contain protocols or domain names.
UrlFormat:
enabled: true
# URLs should always be enclosed within quotes.
UrlQuotes:
enabled: true
# Properties, like color and font, are easier to read and maintain
# when defined using variables rather than literals.
VariableForProperty:
enabled: false
# Avoid vendor prefixes. Or rather: don't write them yourself.
VendorPrefix:
enabled: false
# Omit length units on zero values, e.g. `0px` vs. `0`.
ZeroUnit:
enabled: true

View File

@ -43,4 +43,4 @@ Gruntfile.js
# for specific ignore # for specific ignore
!.svgo.yml !.svgo.yml
!sass-lint/**/*.yml

View File

@ -1,104 +1,86 @@
Authors
=======
Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon) Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon)
and provided thanks to the work of the following contributors: and provided thanks to the work of the following contributors:
* [Gargron](https://github.com/Gargron) * [Gargron](https://github.com/Gargron)
* [ThibG](https://github.com/ThibG)
* [ykzts](https://github.com/ykzts) * [ykzts](https://github.com/ykzts)
* [dependabot[bot]](https://github.com/apps/dependabot)
* [akihikodaki](https://github.com/akihikodaki) * [akihikodaki](https://github.com/akihikodaki)
* [dependabot-preview[bot]](https://github.com/apps/dependabot-preview)
* [mjankowski](https://github.com/mjankowski) * [mjankowski](https://github.com/mjankowski)
* [ThibG](https://github.com/ThibG)
* [unarist](https://github.com/unarist) * [unarist](https://github.com/unarist)
* [m4sk1n](https://github.com/m4sk1n)
* [yiskah](https://github.com/yiskah) * [yiskah](https://github.com/yiskah)
* [nolanlawson](https://github.com/nolanlawson) * [nolanlawson](https://github.com/nolanlawson)
* [ysksn](https://github.com/ysksn)
* [abcang](https://github.com/abcang)
* [sorin-davidoi](https://github.com/sorin-davidoi) * [sorin-davidoi](https://github.com/sorin-davidoi)
* [abcang](https://github.com/abcang)
* [lynlynlynx](https://github.com/lynlynlynx) * [lynlynlynx](https://github.com/lynlynlynx)
* [mayaeh](https://github.com/mayaeh) * [dependabot[bot]](https://github.com/apps/dependabot)
* [m4sk1n](mailto:me@m4sk.in)
* [Marcin Mikołajczak](mailto:me@m4sk.in)
* [Kjwon15](https://github.com/Kjwon15)
* [renatolond](https://github.com/renatolond)
* [alpaca-tc](https://github.com/alpaca-tc) * [alpaca-tc](https://github.com/alpaca-tc)
* [jeroenpraat](https://github.com/jeroenpraat)
* [nclm](https://github.com/nclm) * [nclm](https://github.com/nclm)
* [ineffyble](https://github.com/ineffyble) * [ineffyble](https://github.com/ineffyble)
* [mabkenar](https://github.com/mabkenar) * [renatolond](https://github.com/renatolond)
* [jeroenpraat](https://github.com/jeroenpraat)
* [mayaeh](https://github.com/mayaeh)
* [blackle](https://github.com/blackle) * [blackle](https://github.com/blackle)
* [Quent-in](https://github.com/Quent-in) * [Quent-in](https://github.com/Quent-in)
* [JantsoP](https://github.com/JantsoP) * [JantsoP](https://github.com/JantsoP)
* [zunda](https://github.com/zunda)
* [nullkal](https://github.com/nullkal) * [nullkal](https://github.com/nullkal)
* [yookoala](https://github.com/yookoala) * [yookoala](https://github.com/yookoala)
* [Aditoo17](https://github.com/Aditoo17) * [mabkenar](https://github.com/mabkenar)
* [Quenty31](https://github.com/Quenty31) * [ysksn](https://github.com/ysksn)
* [marek-lach](https://github.com/marek-lach)
* [shuheiktgw](https://github.com/shuheiktgw) * [shuheiktgw](https://github.com/shuheiktgw)
* [ashfurrow](https://github.com/ashfurrow) * [ashfurrow](https://github.com/ashfurrow)
* [Kjwon15](https://github.com/Kjwon15)
* [zunda](https://github.com/zunda)
* [eramdam](https://github.com/eramdam) * [eramdam](https://github.com/eramdam)
* [noellabo](https://github.com/noellabo)
* [takayamaki](https://github.com/takayamaki)
* [danhunsaker](https://github.com/danhunsaker)
* [masarakki](https://github.com/masarakki) * [masarakki](https://github.com/masarakki)
* [takayamaki](https://github.com/takayamaki)
* [ticky](https://github.com/ticky) * [ticky](https://github.com/ticky)
* [Quenty31](https://github.com/Quenty31)
* [danhunsaker](https://github.com/danhunsaker)
* [ThisIsMissEm](https://github.com/ThisIsMissEm) * [ThisIsMissEm](https://github.com/ThisIsMissEm)
* [hcmiya](https://github.com/hcmiya) * [hcmiya](https://github.com/hcmiya)
* [stephenburgess8](https://github.com/stephenburgess8) * [stephenburgess8](https://github.com/stephenburgess8)
* [Wonderfall](https://github.com/Wonderfall) * [Wonderfall](https://github.com/Wonderfall)
* [matteoaquila](https://github.com/matteoaquila) * [matteoaquila](https://github.com/matteoaquila)
* [yukimochi](https://github.com/yukimochi)
* [palindromordnilap](https://github.com/palindromordnilap)
* [rkarabut](https://github.com/rkarabut) * [rkarabut](https://github.com/rkarabut)
* [yukimochi](https://github.com/yukimochi)
* [Artoria2e5](https://github.com/Artoria2e5) * [Artoria2e5](https://github.com/Artoria2e5)
* [nightpool](https://github.com/nightpool)
* [marrus-sh](https://github.com/marrus-sh) * [marrus-sh](https://github.com/marrus-sh)
* [hinaloe](https://github.com/hinaloe)
* [krainboltgreene](https://github.com/krainboltgreene) * [krainboltgreene](https://github.com/krainboltgreene)
* [pfigel](https://github.com/pfigel) * [patf](https://github.com/patf)
* [Aldarone](https://github.com/Aldarone) * [Aldarone](https://github.com/Aldarone)
* [BoFFire](https://github.com/BoFFire) * [BoFFire](https://github.com/BoFFire)
* [clworld](https://github.com/clworld) * [clworld](https://github.com/clworld)
* [MasterGroosha](https://github.com/MasterGroosha)
* [dracos](https://github.com/dracos) * [dracos](https://github.com/dracos)
* [MaciekBaron](https://github.com/MaciekBaron)
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com) * [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
* [Sylvhem](https://github.com/Sylvhem) * [Sylvhem](https://github.com/Sylvhem)
* [MitarashiDango](https://github.com/MitarashiDango) * [nightpool](https://github.com/nightpool)
* [MasterGroosha](https://github.com/MasterGroosha)
* [JeanGauthier](https://github.com/JeanGauthier) * [JeanGauthier](https://github.com/JeanGauthier)
* [kschaper](https://github.com/kschaper) * [kschaper](https://github.com/kschaper)
* [MaciekBaron](https://github.com/MaciekBaron)
* [MitarashiDango](mailto:mitarashidango@users.noreply.github.com)
* [beatrix-bitrot](https://github.com/beatrix-bitrot) * [beatrix-bitrot](https://github.com/beatrix-bitrot)
* [angristan](https://github.com/angristan)
* [adbelle](https://github.com/adbelle) * [adbelle](https://github.com/adbelle)
* [evanminto](https://github.com/evanminto) * [evanminto](https://github.com/evanminto)
* [MightyPork](https://github.com/MightyPork) * [MightyPork](https://github.com/MightyPork)
* [ashleyhull-versent](mailto:ashley.hull@versent.com.au)
* [yhirano55](https://github.com/yhirano55) * [yhirano55](https://github.com/yhirano55)
* [rinsuki](https://github.com/rinsuki)
* [camponez](https://github.com/camponez) * [camponez](https://github.com/camponez)
* [SerCom_KC](mailto:szescxz@gmail.com) * [SerCom-KC](https://github.com/SerCom-KC)
* [aschmitz](https://github.com/aschmitz) * [aschmitz](https://github.com/aschmitz)
* [trwnh](https://github.com/trwnh)
* [devkral](https://github.com/devkral) * [devkral](https://github.com/devkral)
* [fpiesche](https://github.com/fpiesche) * [fpiesche](https://github.com/fpiesche)
* [hugogameiro](https://github.com/hugogameiro)
* [gandaro](https://github.com/gandaro) * [gandaro](https://github.com/gandaro)
* [johnsudaar](https://github.com/johnsudaar) * [johnsudaar](https://github.com/johnsudaar)
* [ariasuni](https://github.com/ariasuni)
* [trebmuh](https://github.com/trebmuh) * [trebmuh](https://github.com/trebmuh)
* [rmhasan](https://github.com/rmhasan) * [Rakib Hasan](mailto:rmhasan@gmail.com)
* [kedamaDQ](https://github.com/kedamaDQ)
* [lindwurm](https://github.com/lindwurm) * [lindwurm](https://github.com/lindwurm)
* [victorhck](mailto:victorhck@geeko.site) * [victorhck](mailto:victorhck@geeko.site)
* [voidsatisfaction](https://github.com/voidsatisfaction) * [voidsatisfaction](https://github.com/voidsatisfaction)
* [BenLubar](https://github.com/BenLubar)
* [hikari-no-yume](https://github.com/hikari-no-yume) * [hikari-no-yume](https://github.com/hikari-no-yume)
* [angristan](https://github.com/angristan)
* [seefood](https://github.com/seefood) * [seefood](https://github.com/seefood)
* [jackjennings](https://github.com/jackjennings) * [jackjennings](https://github.com/jackjennings)
* [koyuawsmbrtn](https://github.com/koyuawsmbrtn)
* [spla](mailto:spla@mastodont.cat) * [spla](mailto:spla@mastodont.cat)
* [expenses](https://github.com/expenses) * [expenses](https://github.com/expenses)
* [walf443](https://github.com/walf443) * [walf443](https://github.com/walf443)
@ -108,29 +90,26 @@ and provided thanks to the work of the following contributors:
* [xqus](https://github.com/xqus) * [xqus](https://github.com/xqus)
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp) * [pfm-eyesightjp](https://github.com/pfm-eyesightjp)
* [fakenine](https://github.com/fakenine) * [fakenine](https://github.com/fakenine)
* [Shleeble](https://github.com/Shleeble)
* [tsuwatch](https://github.com/tsuwatch) * [tsuwatch](https://github.com/tsuwatch)
* [victorhck](https://github.com/victorhck) * [victorhck](https://github.com/victorhck)
* [mkljczk](https://github.com/mkljczk)
* [manuelviens](https://github.com/manuelviens)
* [puckipedia](https://github.com/puckipedia) * [puckipedia](https://github.com/puckipedia)
* [fvh-P](https://github.com/fvh-P) * [fvh-P](https://github.com/fvh-P)
* [rtucker](https://github.com/rtucker) * [contraexemplo](https://github.com/contraexemplo)
* [Anna e só](mailto:contraexemplos@gmail.com) * [hugogameiro](https://github.com/hugogameiro)
* [kazu9su](https://github.com/kazu9su) * [kazu9su](https://github.com/kazu9su)
* [Komic](https://github.com/Komic) * [Komic](https://github.com/Komic)
* [lmorchard](https://github.com/lmorchard)
* [diomed](https://github.com/diomed) * [diomed](https://github.com/diomed)
* [ariasuni](https://github.com/ariasuni)
* [Neetshin](mailto:neetshin@neetsh.in) * [Neetshin](mailto:neetshin@neetsh.in)
* [rainyday](https://github.com/rainyday) * [rainyday](https://github.com/rainyday)
* [ProgVal](https://github.com/ProgVal) * [ProgVal](https://github.com/ProgVal)
* [valentin2105](https://github.com/valentin2105) * [valentin2105](https://github.com/valentin2105)
* [yuntan](https://github.com/yuntan) * [yuntan](https://github.com/yuntan)
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
* [goofy-bz](mailto:goofy@babelzilla.org) * [goofy-bz](mailto:goofy@babelzilla.org)
* [kadiix](https://github.com/kadiix) * [kadiix](https://github.com/kadiix)
* [kodacs](https://github.com/kodacs) * [kodacs](https://github.com/kodacs)
* [marcin mikołajczak](mailto:me@m4sk.in) * [rtucker](https://github.com/rtucker)
* [JMendyk](https://github.com/JMendyk)
* [KScl](https://github.com/KScl) * [KScl](https://github.com/KScl)
* [sterdev](https://github.com/sterdev) * [sterdev](https://github.com/sterdev)
* [TheKinrar](https://github.com/TheKinrar) * [TheKinrar](https://github.com/TheKinrar)
@ -140,139 +119,114 @@ and provided thanks to the work of the following contributors:
* [northerner](https://github.com/northerner) * [northerner](https://github.com/northerner)
* [fhemberger](https://github.com/fhemberger) * [fhemberger](https://github.com/fhemberger)
* [greysteil](https://github.com/greysteil) * [greysteil](https://github.com/greysteil)
* [hencatsmith](https://github.com/hencatsmith) * [hnrysmth](https://github.com/hnrysmth)
* [d6rkaiz](https://github.com/d6rkaiz) * [d6rkaiz](https://github.com/d6rkaiz)
* [Reverite](https://github.com/Reverite) * [JMendyk](https://github.com/JMendyk)
* [JohnD28](https://github.com/JohnD28) * [JohnD28](https://github.com/JohnD28)
* [znz](https://github.com/znz) * [znz](https://github.com/znz)
* [Naouak](https://github.com/Naouak) * [Naouak](https://github.com/Naouak)
* [pawelngei](https://github.com/pawelngei)
* [reneklacan](https://github.com/reneklacan) * [reneklacan](https://github.com/reneklacan)
* [ekiru](https://github.com/ekiru) * [ekiru](https://github.com/ekiru)
* [tcitworld](https://github.com/tcitworld) * [tcitworld](https://github.com/tcitworld)
* [geta6](https://github.com/geta6) * [geta6](https://github.com/geta6)
* [happycoloredbanana](https://github.com/happycoloredbanana) * [happycoloredbanana](https://github.com/happycoloredbanana)
* [kedamaDQ](https://github.com/kedamaDQ)
* [leopku](https://github.com/leopku) * [leopku](https://github.com/leopku)
* [SansPseudoFix](https://github.com/SansPseudoFix) * [SansPseudoFix](https://github.com/SansPseudoFix)
* [salvadorpla](https://github.com/salvadorpla)
* [tomfhowe](https://github.com/tomfhowe) * [tomfhowe](https://github.com/tomfhowe)
* [noraworld](https://github.com/noraworld) * [noraworld](https://github.com/noraworld)
* [theboss](https://github.com/theboss) * [theboss](https://github.com/theboss)
* [nzws](https://github.com/nzws)
* [178inaba](https://github.com/178inaba) * [178inaba](https://github.com/178inaba)
* [xgess](https://github.com/xgess)
* [alyssais](https://github.com/alyssais) * [alyssais](https://github.com/alyssais)
* [aablinov](https://github.com/aablinov) * [kodnaplakal](https://github.com/kodnaplakal)
* [stalker314314](https://github.com/stalker314314) * [stalker314314](https://github.com/stalker314314)
* [cutls](https://github.com/cutls)
* [huertanix](https://github.com/huertanix) * [huertanix](https://github.com/huertanix)
* [genesixx](https://github.com/genesixx) * [genesixx](https://github.com/genesixx)
* [halkeye](https://github.com/halkeye) * [halkeye](https://github.com/halkeye)
* [hinaloe](https://github.com/hinaloe)
* [treby](https://github.com/treby) * [treby](https://github.com/treby)
* [Reverite](https://github.com/Reverite)
* [jpdevries](https://github.com/jpdevries) * [jpdevries](https://github.com/jpdevries)
* [gdpelican](https://github.com/gdpelican) * [H-C-F](https://github.com/H-C-F)
* [kmichl](https://github.com/kmichl)
* [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name) * [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name)
* [saper](https://github.com/saper) * [saper](https://github.com/saper)
* [Dar13](https://github.com/Dar13)
* [nevillepark](https://github.com/nevillepark) * [nevillepark](https://github.com/nevillepark)
* [ornithocoder](https://github.com/ornithocoder) * [ornithocoder](https://github.com/ornithocoder)
* [pwoolcoc](https://github.com/pwoolcoc)
* [pierreozoux](https://github.com/pierreozoux) * [pierreozoux](https://github.com/pierreozoux)
* [qguv](https://github.com/qguv) * [qguv](https://github.com/qguv)
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com) * [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
* [aurelia-sl](https://github.com/aurelia-sl)
* [harukasan](https://github.com/harukasan) * [harukasan](https://github.com/harukasan)
* [stamak](https://github.com/stamak) * [stamak](https://github.com/stamak)
* [Technowix](https://github.com/Technowix) * [Technowix](mailto:technowix@users.noreply.github.com)
* [Zoeille](https://github.com/Zoeille) * [Eychics](https://github.com/Eychics)
* [Thor Harald Johansen](mailto:thj@thj.no) * [Thor Harald Johansen](mailto:thj@thj.no)
* [0x70b1a5](https://github.com/0x70b1a5) * [0x70b1a5](https://github.com/0x70b1a5)
* [gled-rs](https://github.com/gled-rs) * [gled-rs](https://github.com/gled-rs)
* [Valentin_NC](mailto:valentin.ouvrard@nautile.sarl) * [Valentin_NC](mailto:valentin.ouvrard@nautile.sarl)
* [R0ckweb](https://github.com/R0ckweb) * [R0ckweb](https://github.com/R0ckweb)
* [unasuke](https://github.com/unasuke)
* [caasi](https://github.com/caasi) * [caasi](https://github.com/caasi)
* [chr-1x](https://github.com/chr-1x)
* [esetomo](https://github.com/esetomo) * [esetomo](https://github.com/esetomo)
* [foxiehkins](https://github.com/foxiehkins) * [foxiehkins](https://github.com/foxiehkins)
* [hoodie](mailto:hoodiekitten@outlook.com) * [hoodie](mailto:hoodiekitten@outlook.com)
* [luzi82](https://github.com/luzi82) * [luzi82](https://github.com/luzi82)
* [duxovni](https://github.com/duxovni) * [duxovni](https://github.com/duxovni)
* [slice](https://github.com/slice) * [unsmell](https://github.com/unsmell)
* [tmm576](https://github.com/tmm576)
* [unsmell](mailto:unsmell@users.noreply.github.com)
* [valerauko](https://github.com/valerauko)
* [chriswmartin](https://github.com/chriswmartin) * [chriswmartin](https://github.com/chriswmartin)
* [vahnj](https://github.com/vahnj) * [vahnj](https://github.com/vahnj)
* [ikuradon](https://github.com/ikuradon) * [ikuradon](https://github.com/ikuradon)
* [AndreLewin](https://github.com/AndreLewin) * [AndreLewin](https://github.com/AndreLewin)
* [0xflotus](https://github.com/0xflotus) * [rinsuki](https://github.com/rinsuki)
* [redtachyons](https://github.com/redtachyons) * [redtachyons](https://github.com/redtachyons)
* [acid-chicken](https://github.com/acid-chicken)
* [thurloat](https://github.com/thurloat) * [thurloat](https://github.com/thurloat)
* [aaribaud](https://github.com/aaribaud) * [aaribaud](https://github.com/aaribaud)
* [pointlessone](https://github.com/pointlessone)
* [Andrew](mailto:andrewlchronister@gmail.com) * [Andrew](mailto:andrewlchronister@gmail.com)
* [aurelien-reeves](https://github.com/aurelien-reeves)
* [AnaGelez](https://github.com/AnaGelez)
* [estuans](https://github.com/estuans) * [estuans](https://github.com/estuans)
* [dissolve](https://github.com/dissolve) * [dissolve](https://github.com/dissolve)
* [PurpleBooth](https://github.com/PurpleBooth) * [PurpleBooth](https://github.com/PurpleBooth)
* [bradurani](https://github.com/bradurani) * [bradurani](https://github.com/bradurani)
* [wavebeem](https://github.com/wavebeem) * [wavebeem](https://github.com/wavebeem)
* [bruwalfas](https://github.com/bruwalfas) * [bruwalfas](https://github.com/bruwalfas)
* [LottieVixen](https://github.com/LottieVixen) * [foxsan48](https://github.com/foxsan48)
* [wchristian](https://github.com/wchristian) * [wchristian](https://github.com/wchristian)
* [muffinista](https://github.com/muffinista) * [muffinista](https://github.com/muffinista)
* [cdutson](https://github.com/cdutson) * [cdutson](https://github.com/cdutson)
* [farlistener](https://github.com/farlistener) * [farlistener](https://github.com/farlistener)
* [dariusk](https://github.com/dariusk)
* [DavidLibeau](https://github.com/DavidLibeau) * [DavidLibeau](https://github.com/DavidLibeau)
* [ddevault](https://github.com/ddevault) * [SirCmpwn](https://github.com/SirCmpwn)
* [Fjoerfoks](https://github.com/Fjoerfoks) * [Fjoerfoks](https://github.com/Fjoerfoks)
* [fmauNeko](https://github.com/fmauNeko) * [fmauNeko](https://github.com/fmauNeko)
* [gloaec](https://github.com/gloaec) * [gloaec](https://github.com/gloaec)
* [Gomasy](https://github.com/Gomasy) * [Gomasy](https://github.com/Gomasy)
* [unstabler](https://github.com/unstabler) * [unstabler](https://github.com/unstabler)
* [potato4d](https://github.com/potato4d) * [potato4d](https://github.com/potato4d)
* [Hanage999](https://github.com/Hanage999)
* [h-izumi](https://github.com/h-izumi) * [h-izumi](https://github.com/h-izumi)
* [ErikXXon](https://github.com/ErikXXon) * [ErikXXon](https://github.com/ErikXXon)
* [ian-kelling](https://github.com/ian-kelling) * [ian-kelling](https://github.com/ian-kelling)
* [immae](https://github.com/immae) * [immae](https://github.com/immae)
* [J0WI](https://github.com/J0WI)
* [foozmeat](https://github.com/foozmeat) * [foozmeat](https://github.com/foozmeat)
* [jasonrhodes](https://github.com/jasonrhodes) * [jasonrhodes](https://github.com/jasonrhodes)
* [Jason Snell](mailto:jason@newrelic.com) * [Jason Snell](mailto:jason@newrelic.com)
* [jviide](https://github.com/jviide) * [jviide](https://github.com/jviide)
* [YuleZ](https://github.com/YuleZ)
* [crakaC](https://github.com/crakaC) * [crakaC](https://github.com/crakaC)
* [tkbky](https://github.com/tkbky) * [tkbky](https://github.com/tkbky)
* [Kaylee](mailto:kaylee@codethat.sucks) * [Kaylee](mailto:kaylee@codethat.sucks)
* [Kazhnuz](https://github.com/Kazhnuz) * [Kazhnuz](https://github.com/Kazhnuz)
* [connyduck](https://github.com/connyduck) * [connyduck](https://github.com/connyduck)
* [LindseyB](https://github.com/LindseyB) * [Lindsey Bieda](mailto:lindseyb@users.noreply.github.com)
* [Lorenz Diener](mailto:halcyon@icosahedron.website) * [Lorenz Diener](mailto:halcyon@icosahedron.website)
* [alimony](https://github.com/alimony) * [alimony](https://github.com/alimony)
* [mig5](https://github.com/mig5) * [mig5](https://github.com/mig5)
* [moritzheiber](https://github.com/moritzheiber)
* [ndarville](https://github.com/ndarville) * [ndarville](https://github.com/ndarville)
* [Abzol](https://github.com/Abzol) * [Abzol](https://github.com/Abzol)
* [PatOnTheBack](https://github.com/PatOnTheBack) * [pwoolcoc](https://github.com/pwoolcoc)
* [xPaw](https://github.com/xPaw) * [xPaw](https://github.com/xPaw)
* [petzah](https://github.com/petzah) * [petzah](https://github.com/petzah)
* [ignisf](https://github.com/ignisf) * [ignisf](https://github.com/ignisf)
* [raymestalez](https://github.com/raymestalez) * [raymestalez](https://github.com/raymestalez)
* [remram44](https://github.com/remram44) * [sascha-sl](https://github.com/sascha-sl)
* [sts10](https://github.com/sts10)
* [SuperSandro2000](https://github.com/SuperSandro2000)
* [u1-liquid](https://github.com/u1-liquid) * [u1-liquid](https://github.com/u1-liquid)
* [rosylilly](https://github.com/rosylilly)
* [sim6](https://github.com/sim6) * [sim6](https://github.com/sim6)
* [Sir-Boops](https://github.com/Sir-Boops)
* [stemid](https://github.com/stemid) * [stemid](https://github.com/stemid)
* [sumdog](https://github.com/sumdog)
* [ThomasLeister](https://github.com/ThomasLeister) * [ThomasLeister](https://github.com/ThomasLeister)
* [mcat-ee](https://github.com/mcat-ee) * [mcat-ee](https://github.com/mcat-ee)
* [tototoshi](https://github.com/tototoshi) * [tototoshi](https://github.com/tototoshi)
@ -289,49 +243,49 @@ and provided thanks to the work of the following contributors:
* [aus-social](https://github.com/aus-social) * [aus-social](https://github.com/aus-social)
* [imbsky](https://github.com/imbsky) * [imbsky](https://github.com/imbsky)
* [bsky](mailto:me@imbsky.net) * [bsky](mailto:me@imbsky.net)
* [chr-1x](https://github.com/chr-1x)
* [codl](https://github.com/codl) * [codl](https://github.com/codl)
* [cpsdqs](https://github.com/cpsdqs) * [cpsdqs](https://github.com/cpsdqs)
* [barzamin](https://github.com/barzamin) * [barzamin](https://github.com/barzamin)
* [fhalna](https://github.com/fhalna) * [fhalna](https://github.com/fhalna)
* [highemerly](https://github.com/highemerly)
* [haoyayoi](https://github.com/haoyayoi) * [haoyayoi](https://github.com/haoyayoi)
* [ik11235](https://github.com/ik11235) * [ik11235](https://github.com/ik11235)
* [kawax](https://github.com/kawax) * [kawax](https://github.com/kawax)
* [007lva](https://github.com/007lva) * [007lva](https://github.com/007lva)
* [mbajur](https://github.com/mbajur)
* [matsurai25](https://github.com/matsurai25) * [matsurai25](https://github.com/matsurai25)
* [mecab](https://github.com/mecab) * [mecab](https://github.com/mecab)
* [nicobz25](https://github.com/nicobz25) * [nicobz25](https://github.com/nicobz25)
* [oliverkeeble](https://github.com/oliverkeeble) * [oliverkeeble](https://github.com/oliverkeeble)
* [partev](https://github.com/partev)
* [pinfort](https://github.com/pinfort) * [pinfort](https://github.com/pinfort)
* [rbaumert](https://github.com/rbaumert) * [rbaumert](https://github.com/rbaumert)
* [rhoio](https://github.com/rhoio) * [rhoio](https://github.com/rhoio)
* [trwnh](https://github.com/trwnh)
* [usagi-f](https://github.com/usagi-f) * [usagi-f](https://github.com/usagi-f)
* [vidarlee](https://github.com/vidarlee) * [vidarlee](https://github.com/vidarlee)
* [vjackson725](https://github.com/vjackson725) * [vjackson725](https://github.com/vjackson725)
* [wxcafe](https://github.com/wxcafe) * [wxcafe](https://github.com/wxcafe)
* [新都心(Neet Shin)](mailto:nucx@dio-vox.com) * [新都心(Neet Shin)](mailto:nucx@dio-vox.com)
* [clarfon](https://github.com/clarfon)
* [cygnan](https://github.com/cygnan) * [cygnan](https://github.com/cygnan)
* [Awea](https://github.com/Awea) * [Awea](https://github.com/Awea)
* [halcy](https://github.com/halcy) * [halcy](https://github.com/halcy)
* [naaaaaaaaaaaf](https://github.com/naaaaaaaaaaaf)
* [NecroTechno](https://github.com/NecroTechno)
* [8398a7](https://github.com/8398a7) * [8398a7](https://github.com/8398a7)
* [857b](https://github.com/857b) * [857b](https://github.com/857b)
* [insom](https://github.com/insom) * [insom](https://github.com/insom)
* [tachyons](https://github.com/tachyons) * [Aditoo17](https://github.com/Aditoo17)
* [Esteth](https://github.com/Esteth)
* [unascribed](https://github.com/unascribed) * [unascribed](https://github.com/unascribed)
* [Aguay-val](https://github.com/Aguay-val) * [Aguay-val](https://github.com/Aguay-val)
* [Akihiko Odaki](mailto:nekomanma@pixiv.co.jp)
* [knu](https://github.com/knu) * [knu](https://github.com/knu)
* [h3poteto](https://github.com/h3poteto) * [h3poteto](https://github.com/h3poteto)
* [unleashed](https://github.com/unleashed) * [unleashed](https://github.com/unleashed)
* [alxrcs](https://github.com/alxrcs) * [alxrcs](https://github.com/alxrcs)
* [console-cowboy](https://github.com/console-cowboy) * [console-cowboy](https://github.com/console-cowboy)
* [Alkarex](https://github.com/Alkarex) * [pointlessone](https://github.com/pointlessone)
* [a2](https://github.com/a2) * [a2](https://github.com/a2)
* [alfiedotwtf](https://github.com/alfiedotwtf)
* [0xa](https://github.com/0xa) * [0xa](https://github.com/0xa)
* [palindromordnilap](https://github.com/palindromordnilap)
* [virtualpain](https://github.com/virtualpain) * [virtualpain](https://github.com/virtualpain)
* [sapphirus](https://github.com/sapphirus) * [sapphirus](https://github.com/sapphirus)
* [amandavisconti](https://github.com/amandavisconti) * [amandavisconti](https://github.com/amandavisconti)
@ -340,13 +294,12 @@ and provided thanks to the work of the following contributors:
* [Andreas Drop](mailto:andy@remline.de) * [Andreas Drop](mailto:andy@remline.de)
* [andi1984](https://github.com/andi1984) * [andi1984](https://github.com/andi1984)
* [schas002](https://github.com/schas002) * [schas002](https://github.com/schas002)
* [contraexemplo](https://github.com/contraexemplo)
* [abackstrom](https://github.com/abackstrom) * [abackstrom](https://github.com/abackstrom)
* [armandfardeau](https://github.com/armandfardeau)
* [raboof](https://github.com/raboof)
* [jumbosushi](https://github.com/jumbosushi) * [jumbosushi](https://github.com/jumbosushi)
* [ayumin](https://github.com/ayumin) * [ayumin](https://github.com/ayumin)
* [BaptisteGelez](https://github.com/BaptisteGelez)
* [bzg](https://github.com/bzg) * [bzg](https://github.com/bzg)
* [BenLubar](https://github.com/BenLubar)
* [benediktg](https://github.com/benediktg) * [benediktg](https://github.com/benediktg)
* [blakebarnett](https://github.com/blakebarnett) * [blakebarnett](https://github.com/blakebarnett)
* [bradj](https://github.com/bradj) * [bradj](https://github.com/bradj)
@ -359,7 +312,7 @@ and provided thanks to the work of the following contributors:
* [DoubleMalt](https://github.com/DoubleMalt) * [DoubleMalt](https://github.com/DoubleMalt)
* [Moosh-be](https://github.com/Moosh-be) * [Moosh-be](https://github.com/Moosh-be)
* [Motoma](https://github.com/Motoma) * [Motoma](https://github.com/Motoma)
* [Christopher Kolstad](mailto:christopher.kolstad@finn.no) * [chriswk](https://github.com/chriswk)
* [csu](https://github.com/csu) * [csu](https://github.com/csu)
* [kklleemm](https://github.com/kklleemm) * [kklleemm](https://github.com/kklleemm)
* [colindean](https://github.com/colindean) * [colindean](https://github.com/colindean)
@ -367,7 +320,6 @@ and provided thanks to the work of the following contributors:
* [multiple-creatures](https://github.com/multiple-creatures) * [multiple-creatures](https://github.com/multiple-creatures)
* [watilde](https://github.com/watilde) * [watilde](https://github.com/watilde)
* [daprice](https://github.com/daprice) * [daprice](https://github.com/daprice)
* [da2x](https://github.com/da2x)
* [dar5hak](https://github.com/dar5hak) * [dar5hak](https://github.com/dar5hak)
* [kant](https://github.com/kant) * [kant](https://github.com/kant)
* [maxolasersquad](https://github.com/maxolasersquad) * [maxolasersquad](https://github.com/maxolasersquad)
@ -376,7 +328,7 @@ and provided thanks to the work of the following contributors:
* [davefp](https://github.com/davefp) * [davefp](https://github.com/davefp)
* [yipdw](https://github.com/yipdw) * [yipdw](https://github.com/yipdw)
* [debanshuk](https://github.com/debanshuk) * [debanshuk](https://github.com/debanshuk)
* [DerekNonGeneric](https://github.com/DerekNonGeneric) * [Derek Lewis](mailto:derekcecillewis@gmail.com)
* [dblandin](https://github.com/dblandin) * [dblandin](https://github.com/dblandin)
* [Drew Gates](mailto:aranaur@users.noreply.github.com) * [Drew Gates](mailto:aranaur@users.noreply.github.com)
* [dtschust](https://github.com/dtschust) * [dtschust](https://github.com/dtschust)
@ -384,17 +336,12 @@ and provided thanks to the work of the following contributors:
* [eai04191](https://github.com/eai04191) * [eai04191](https://github.com/eai04191)
* [d3vgru](https://github.com/d3vgru) * [d3vgru](https://github.com/d3vgru)
* [Elizafox](https://github.com/Elizafox) * [Elizafox](https://github.com/Elizafox)
* [enewhuis](https://github.com/enewhuis)
* [ericblade](https://github.com/ericblade) * [ericblade](https://github.com/ericblade)
* [mikoim](https://github.com/mikoim) * [mikoim](https://github.com/mikoim)
* [espenronnevik](https://github.com/espenronnevik) * [espenronnevik](https://github.com/espenronnevik)
* [fabianonline](https://github.com/fabianonline)
* [Finariel](https://github.com/Finariel) * [Finariel](https://github.com/Finariel)
* [siuying](https://github.com/siuying) * [siuying](https://github.com/siuying)
* [zoc](https://github.com/zoc)
* [fwenzel](https://github.com/fwenzel)
* [GenbuHase](https://github.com/GenbuHase) * [GenbuHase](https://github.com/GenbuHase)
* [nilsding](https://github.com/nilsding)
* [hattori6789](https://github.com/hattori6789) * [hattori6789](https://github.com/hattori6789)
* [algernon](https://github.com/algernon) * [algernon](https://github.com/algernon)
* [Fastbyte01](https://github.com/Fastbyte01) * [Fastbyte01](https://github.com/Fastbyte01)
@ -410,19 +357,17 @@ and provided thanks to the work of the following contributors:
* [suzukaze](https://github.com/suzukaze) * [suzukaze](https://github.com/suzukaze)
* [Hiromi-Kai](https://github.com/Hiromi-Kai) * [Hiromi-Kai](https://github.com/Hiromi-Kai)
* [hishamhm](https://github.com/hishamhm) * [hishamhm](https://github.com/hishamhm)
* [Slaynash](https://github.com/Slaynash)
* [musashino205](https://github.com/musashino205) * [musashino205](https://github.com/musashino205)
* [iwaim](https://github.com/iwaim) * [iwaim](https://github.com/iwaim)
* [valrus](https://github.com/valrus) * [valrus](https://github.com/valrus)
* [IMcD23](https://github.com/IMcD23) * [IMcD23](https://github.com/IMcD23)
* [yi0713](https://github.com/yi0713) * [yi0713](https://github.com/yi0713)
* [iblech](https://github.com/iblech) * [iblech](https://github.com/iblech)
* [J Yeary](mailto:usbsnowcrash@users.noreply.github.com) * [usbsnowcrash](https://github.com/usbsnowcrash)
* [jack-michaud](https://github.com/jack-michaud) * [jack-michaud](https://github.com/jack-michaud)
* [Floppy](https://github.com/Floppy) * [Floppy](https://github.com/Floppy)
* [loomchild](https://github.com/loomchild) * [loomchild](https://github.com/loomchild)
* [jenkr55](https://github.com/jenkr55) * [jenkr55](https://github.com/jenkr55)
* [hyenagirl64](https://github.com/hyenagirl64)
* [press5](https://github.com/press5) * [press5](https://github.com/press5)
* [TrollDecker](https://github.com/TrollDecker) * [TrollDecker](https://github.com/TrollDecker)
* [jmontane](https://github.com/jmontane) * [jmontane](https://github.com/jmontane)
@ -430,19 +375,21 @@ and provided thanks to the work of the following contributors:
* [jguerder](https://github.com/jguerder) * [jguerder](https://github.com/jguerder)
* [Jehops](https://github.com/Jehops) * [Jehops](https://github.com/Jehops)
* [joshuap](https://github.com/joshuap) * [joshuap](https://github.com/joshuap)
* [YuleZ](https://github.com/YuleZ)
* [Tiwy57](https://github.com/Tiwy57) * [Tiwy57](https://github.com/Tiwy57)
* [xuv](https://github.com/xuv) * [xuv](https://github.com/xuv)
* [Jnsll](https://github.com/Jnsll) * [Jnsll](https://github.com/Jnsll)
* [j0k3r](https://github.com/j0k3r) * [j0k3r](https://github.com/j0k3r)
* [KEINOS](https://github.com/KEINOS) * [KEINOS](https://github.com/KEINOS)
* [futoase](https://github.com/futoase) * [futoase](https://github.com/futoase)
* [pot8to](https://github.com/pot8to) * [Pneumaticat](https://github.com/Pneumaticat)
* [Kit Redgrave](mailto:qwertyitis@gmail.com) * [Kit Redgrave](mailto:qwertyitis@gmail.com)
* [Knut Erik](mailto:abjectio@users.noreply.github.com) * [Knut Erik](mailto:abjectio@users.noreply.github.com)
* [mkody](https://github.com/mkody) * [mkody](https://github.com/mkody)
* [k0ta0uchi](https://github.com/k0ta0uchi) * [k0ta0uchi](https://github.com/k0ta0uchi)
* [KrzysiekJ](https://github.com/KrzysiekJ) * [KrzysiekJ](https://github.com/KrzysiekJ)
* [Leo Wzukw](mailto:leowzukw@users.noreply.github.com) * [leowzukw](https://github.com/leowzukw)
* [lmorchard](https://github.com/lmorchard)
* [Tak](https://github.com/Tak) * [Tak](https://github.com/Tak)
* [cacheflow](https://github.com/cacheflow) * [cacheflow](https://github.com/cacheflow)
* [ldidry](https://github.com/ldidry) * [ldidry](https://github.com/ldidry)
@ -450,92 +397,83 @@ and provided thanks to the work of the following contributors:
* [lfuelling](https://github.com/lfuelling) * [lfuelling](https://github.com/lfuelling)
* [Grabacr07](https://github.com/Grabacr07) * [Grabacr07](https://github.com/Grabacr07)
* [mistermantas](https://github.com/mistermantas) * [mistermantas](https://github.com/mistermantas)
* [MareenaKunjachan](https://github.com/MareenaKunjachan)
* [mareklach](https://github.com/mareklach) * [mareklach](https://github.com/mareklach)
* [wirehack7](https://github.com/wirehack7) * [wirehack7](https://github.com/wirehack7)
* [martymcguire](https://github.com/martymcguire) * [martymcguire](https://github.com/martymcguire)
* [marvinkopf](https://github.com/marvinkopf) * [marvinkopf](https://github.com/marvinkopf)
* [otsune](https://github.com/otsune) * [otsune](https://github.com/otsune)
* [mbugowski](https://github.com/mbugowski)
* [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com) * [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com)
* [madmath03](https://github.com/madmath03)
* [matt-auckland](https://github.com/matt-auckland) * [matt-auckland](https://github.com/matt-auckland)
* [webroo](https://github.com/webroo) * [webroo](https://github.com/webroo)
* [Matthias Beyer](mailto:mail@beyermatthias.de) * [matthiasbeyer](https://github.com/matthiasbeyer)
* [Matthias Jouan](mailto:matthias.jouan@gmail.com) * [mattjmattj](https://github.com/mattjmattj)
* [Matthieu Paret](mailto:matthieuparet69@gmail.com) * [mtparet](https://github.com/mtparet)
* [Maxime BORGES](mailto:maxime.borges@gmail.com) * [maximeborges](https://github.com/maximeborges)
* [Mayu Laierlence](mailto:minacle@live.com) * [minacle](https://github.com/minacle)
* [Michael Deeb](mailto:michaeldeeb@me.com) * [michaeljdeeb](https://github.com/michaeljdeeb)
* [Michael Vieira](mailto:dtox94@gmail.com) * [Themimitoof](https://github.com/Themimitoof)
* [Michel](mailto:michel@cyweo.com) * [cyweo](https://github.com/cyweo)
* [Midgard](mailto:m1dgard@users.noreply.github.com) * [Midgard](mailto:m1dgard@users.noreply.github.com)
* [Mike Burns](mailto:mburns@thoughtbot.com) * [mike-burns](https://github.com/mike-burns)
* [Milan](mailto:me@petabyteboy.de) * [verymilan](https://github.com/verymilan)
* [Milan*](mailto:tchncs@vivaldi.net) * [milmazz](https://github.com/milmazz)
* [Milton Mazzarri](mailto:milmazz@gmail.com) * [premist](https://github.com/premist)
* [Minku Lee](mailto:premist@me.com) * [Mnkai](https://github.com/Mnkai)
* [Minori Hiraoka](mailto:mnkai@users.noreply.github.com) * [mitchhentges](https://github.com/mitchhentges)
* [Mitchell Hentges](mailto:mitch9654@gmail.com) * [moritzheiber](https://github.com/moritzheiber)
* [Mostafa Ahangarha](mailto:ahangarha@users.noreply.github.com) * [mouse-reeve](https://github.com/mouse-reeve)
* [Mouse Reeve](mailto:mousereeve@riseup.net) * [Mozinet-fr](https://github.com/Mozinet-fr)
* [Mozinet](mailto:mozinet-fr@users.noreply.github.com) * [lae](https://github.com/lae)
* [Musee U](mailto:lae@users.noreply.github.com) * [Nanamachi](https://github.com/Nanamachi)
* [NOGISAKA Sadata](mailto:ngsksdt@gmail.com) * [orinthe](https://github.com/orinthe)
* [Naf](mailto:uenok.htc@gmail.com) * [Dar13](https://github.com/Dar13)
* [Nanamachi](mailto:town7.haruki@gmail.com) * [ngerakines](https://github.com/ngerakines)
* [Nathaniel Ekoniak](mailto:nekoniak@ennate.tech) * [vonneudeck](https://github.com/vonneudeck)
* [NecroTechno](mailto:necrotechno@riseup.net) * [Ninetailed](https://github.com/Ninetailed)
* [Nick Gerakines](mailto:nick@gerakines.net) * [k24](https://github.com/k24)
* [Nicolai von Neudeck](mailto:nicolai@vonneudeck.com) * [noiob](https://github.com/noiob)
* [Ninetailed](mailto:ninetailed@gmail.com) * [kwaio](https://github.com/kwaio)
* [Nishi, Keisuke](mailto:k24@users.noreply.github.com) * [norayr](https://github.com/norayr)
* [Noiob](mailto:noiob@users.noreply.github.com) * [joyeusenoelle](https://github.com/joyeusenoelle)
* [Nope Nope](mailto:hireme@kwaio.ninja) * [OlivierNicole](https://github.com/OlivierNicole)
* [Norayr Chilingarian](mailto:norayr@arnet.am) * [noppa](https://github.com/noppa)
* [Noëlle Anthony](mailto:noelle.d.anthony@gmail.com) * [Otakan951](https://github.com/Otakan951)
* [N氏](mailto:uenok.htc@gmail.com) * [fahy](https://github.com/fahy)
* [Olivier Nicole](mailto:olivierthnicole@gmail.com) * [PatrickRWells](https://github.com/PatrickRWells)
* [Oskari Noppa](mailto:noppa@users.noreply.github.com) * [Pangoraw](https://github.com/Pangoraw)
* [Otakan](mailto:otakan951@gmail.com) * [peterkeen](https://github.com/peterkeen)
* [Padraig Fahy](mailto:tech@padraigfahy.com) * [pgate](https://github.com/pgate)
* [PatrickRWells](mailto:32802366+patrickrwells@users.noreply.github.com) * [remram44](https://github.com/remram44)
* [Paul](mailto:naydex.mc+github@gmail.com) * [retokromer](https://github.com/retokromer)
* [Pete Keen](mailto:pete@petekeen.net) * [rfwatson](https://github.com/rfwatson)
* [Pierre-Morgan Gate](mailto:pgate@users.noreply.github.com) * [rfreebern](https://github.com/rfreebern)
* [Ratmir Karabut](mailto:rkarabut@sfmodern.ru)
* [Reto Kromer](mailto:retokromer@users.noreply.github.com)
* [Rob Watson](mailto:rfwatson@users.noreply.github.com)
* [Ryan Freebern](mailto:ryan@freebern.org)
* [Ryan Wade](mailto:ryan.wade@protonmail.com) * [Ryan Wade](mailto:ryan.wade@protonmail.com)
* [Ryo Kajiwara](mailto:kfe-fecn6.prussian@s01.info) * [sylph01](https://github.com/sylph01)
* [S.H](mailto:gamelinks007@gmail.com) * [S-H-GAMELINKS](https://github.com/S-H-GAMELINKS)
* [Sadiq Saif](mailto:staticsafe@users.noreply.github.com) * [staticsafe](https://github.com/staticsafe)
* [Sam Hewitt](mailto:hewittsamuel@gmail.com) * [snwh](https://github.com/snwh)
* [Sasha Sorokin](mailto:dafri.nochiterov8@gmail.com) * [sts10](https://github.com/sts10)
* [Satoshi KOJIMA](mailto:skoji@mac.com) * [skoji](https://github.com/skoji)
* [ScienJus](mailto:i@scienjus.com) * [ScienJus](https://github.com/ScienJus)
* [Scott Larkin](mailto:scott@codeclimate.com) * [larkinscott](https://github.com/larkinscott)
* [Sebastian Hübner](mailto:imolein@users.noreply.github.com) * [imolein](https://github.com/imolein)
* [Sebastian Morr](mailto:sebastian@morr.cc) * [blinry](https://github.com/blinry)
* [Sergei Č](mailto:noiwex1911@gmail.com) * [Noiwex](https://github.com/Noiwex)
* [Setuu](mailto:yuki764setuu@gmail.com) * [yuki764](https://github.com/yuki764)
* [Shaun Gillies](mailto:me@shaungillies.net) * [shnjp](https://github.com/shnjp)
* [Shin Adachi](mailto:shn@glucose.jp) * [ernix](https://github.com/ernix)
* [Shin Kojima](mailto:shin@kojima.org) * [rosylilly](https://github.com/rosylilly)
* [Shouko Yu](mailto:imshouko@gmail.com) * [shouko](https://github.com/shouko)
* [Sina Mashek](mailto:sina@mashek.xyz) * [Sina Mashek](mailto:sina@mashek.xyz)
* [Soshi Kato](mailto:mail@sossii.com) * [sossii](https://github.com/sossii)
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com) * [Spanky](mailto:2788886+spankyworks@users.noreply.github.com)
* [StefOfficiel](mailto:pichard.stephane@free.fr) * [StefOfficiel](mailto:pichard.stephane@free.fr)
* [Steven Tappert](mailto:admin@dark-it.net)
* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com) * [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com)
* [Sébastien Santoro](mailto:dereckson@espace-win.org) * [Sébastien Santoro](mailto:dereckson@espace-win.org)
* [Tad Thorley](mailto:phaedryx@users.noreply.github.com) * [Tad Thorley](mailto:phaedryx@users.noreply.github.com)
* [Takayoshi Nishida](mailto:takayoshi.nishida@gmail.com) * [Takayoshi Nishida](mailto:takayoshi.nishida@gmail.com)
* [Takayuki KUSANO](mailto:github@tkusano.jp) * [Takayuki KUSANO](mailto:github@tkusano.jp)
* [TakesxiSximada](mailto:takesxi.sximada@gmail.com) * [TakesxiSximada](mailto:takesxi.sximada@gmail.com)
* [Tao Bror Bojlén](mailto:brortao@users.noreply.github.com)
* [TheInventrix](mailto:theinventrix@users.noreply.github.com) * [TheInventrix](mailto:theinventrix@users.noreply.github.com)
* [Thomas Alberola](mailto:thomas@needacoffee.fr) * [Thomas Alberola](mailto:thomas@needacoffee.fr)
* [Toby Deshane](mailto:fortyseven@users.noreply.github.com) * [Toby Deshane](mailto:fortyseven@users.noreply.github.com)
@ -545,12 +483,10 @@ and provided thanks to the work of the following contributors:
* [Treyssat-Vincent Nino](mailto:treyssatvincent@users.noreply.github.com) * [Treyssat-Vincent Nino](mailto:treyssatvincent@users.noreply.github.com)
* [Udo Kramer](mailto:optik@fluffel.io) * [Udo Kramer](mailto:optik@fluffel.io)
* [Una](mailto:una@unascribed.com) * [Una](mailto:una@unascribed.com)
* [Ushitora Anqou](mailto:ushitora@anqou.net)
* [Ushitora Anqou](mailto:ushitora_anqou@yahoo.co.jp) * [Ushitora Anqou](mailto:ushitora_anqou@yahoo.co.jp)
* [Valentin Lorentz](mailto:progval+git@progval.net) * [Valentin Lorentz](mailto:progval+git@progval.net)
* [Vladimir Mincev](mailto:vladimir@canicinteractive.com) * [Vladimir Mincev](mailto:vladimir@canicinteractive.com)
* [Waldir Pimenta](mailto:waldyrious@gmail.com) * [Waldir Pimenta](mailto:waldyrious@gmail.com)
* [Wenceslao Páez Chávez](mailto:wcpaez@gmail.com)
* [Wesley Ellis](mailto:tahnok@gmail.com) * [Wesley Ellis](mailto:tahnok@gmail.com)
* [Wiktor](mailto:wiktor@metacode.biz) * [Wiktor](mailto:wiktor@metacode.biz)
* [Wonderfall](mailto:wonderfall@schrodinger.io) * [Wonderfall](mailto:wonderfall@schrodinger.io)
@ -561,7 +497,6 @@ and provided thanks to the work of the following contributors:
* [YaQ](mailto:i_k_o_m_a_7@yahoo.co.jp) * [YaQ](mailto:i_k_o_m_a_7@yahoo.co.jp)
* [Yanaken](mailto:yanakend@gmail.com) * [Yanaken](mailto:yanakend@gmail.com)
* [Yann Klis](mailto:yann.klis@gmail.com) * [Yann Klis](mailto:yann.klis@gmail.com)
* [Yağızhan](mailto:35808275+yagizhan49@users.noreply.github.com)
* [Yeechan Lu](mailto:wz.bluesnow@gmail.com) * [Yeechan Lu](mailto:wz.bluesnow@gmail.com)
* [Yusuke Abe](mailto:moonset20@gmail.com) * [Yusuke Abe](mailto:moonset20@gmail.com)
* [Zachary Spector](mailto:logicaldash@gmail.com) * [Zachary Spector](mailto:logicaldash@gmail.com)
@ -575,39 +510,31 @@ and provided thanks to the work of the following contributors:
* [chrolis](mailto:chrolis@users.noreply.github.com) * [chrolis](mailto:chrolis@users.noreply.github.com)
* [cormo](mailto:cormorant2+github@gmail.com) * [cormo](mailto:cormorant2+github@gmail.com)
* [d0p1](mailto:dopi-sama@hush.com) * [d0p1](mailto:dopi-sama@hush.com)
* [dxwc](mailto:dxwc@users.noreply.github.com)
* [evilny0](mailto:evilny0@moomoocamp.net) * [evilny0](mailto:evilny0@moomoocamp.net)
* [febrezo](mailto:felixbrezo@gmail.com) * [febrezo](mailto:felixbrezo@gmail.com)
* [fsubal](mailto:fsubal@users.noreply.github.com) * [fsubal](mailto:fsubal@users.noreply.github.com)
* [fusshi-](mailto:dikky1218@users.noreply.github.com) * [fusshi-](mailto:dikky1218@users.noreply.github.com)
* [gentaro](mailto:gentaroooo@gmail.com) * [gentaro](mailto:gentaroooo@gmail.com)
* [gol-cha](mailto:info@mevo.xyz)
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp) * [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
* [haosbvnker](mailto:github@chaosbunker.com) * [haosbvnker](mailto:github@chaosbunker.com)
* [ichi_i](mailto:51489410+ichi-i@users.noreply.github.com)
* [isati](mailto:phil@juchnowi.cz) * [isati](mailto:phil@juchnowi.cz)
* [jacob](mailto:jacobherringtondeveloper@gmail.com) * [jacob](mailto:jacobherringtondeveloper@gmail.com)
* [jenn kaplan](mailto:me@jkap.io) * [jenn kaplan](mailto:me@jkap.io)
* [jirayudech](mailto:jirayudech@gmail.com) * [jirayudech](mailto:jirayudech@gmail.com)
* [jomo](mailto:github@jomo.tv)
* [jooops](mailto:joops@autistici.org) * [jooops](mailto:joops@autistici.org)
* [jukper](mailto:jukkaperanto@gmail.com) * [jukper](mailto:jukkaperanto@gmail.com)
* [jumoru](mailto:jumoru@mailbox.org) * [jumoru](mailto:jumoru@mailbox.org)
* [karlyeurl](mailto:karl.yeurl@gmail.com) * [karlyeurl](mailto:karl.yeurl@gmail.com)
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com) * [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
* [kodai](mailto:shirafuta.kodai@gmail.com)
* [kuro5hin](mailto:rusty@kuro5hin.org) * [kuro5hin](mailto:rusty@kuro5hin.org)
* [luzpaz](mailto:luzpaz@users.noreply.github.com) * [luzpaz](mailto:luzpaz@users.noreply.github.com)
* [maxypy](mailto:maxime@mpigou.fr) * [maxypy](mailto:maxime@mpigou.fr)
* [mhe](mailto:mail@marcus-herrmann.com) * [mhe](mailto:mail@marcus-herrmann.com)
* [mike castleman](mailto:m@mlcastle.net)
* [mimikun](mailto:dzdzble_effort_311@outlook.jp) * [mimikun](mailto:dzdzble_effort_311@outlook.jp)
* [mohemohe](mailto:mohemohe@users.noreply.github.com)
* [mshrtkch](mailto:mshrtkch@users.noreply.github.com) * [mshrtkch](mailto:mshrtkch@users.noreply.github.com)
* [muan](mailto:muan@github.com) * [muan](mailto:muan@github.com)
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com)
* [neetshin](mailto:neetshin@neetsh.in) * [neetshin](mailto:neetshin@neetsh.in)
* [nzws](mailto:git-yuzu@svk.jp) * [nightpool](mailto:nightpool@users.noreply.github.com)
* [rch850](mailto:rich850@gmail.com) * [rch850](mailto:rich850@gmail.com)
* [roikale](mailto:roikale@users.noreply.github.com) * [roikale](mailto:roikale@users.noreply.github.com)
* [rysiekpl](mailto:rysiek@hackerspace.pl) * [rysiekpl](mailto:rysiek@hackerspace.pl)
@ -620,8 +547,6 @@ and provided thanks to the work of the following contributors:
* [tateisu](mailto:tateisu@gmail.com) * [tateisu](mailto:tateisu@gmail.com)
* [tmyt](mailto:shigure@refy.net) * [tmyt](mailto:shigure@refy.net)
* [trevDev()](mailto:trev@trevdev.ca) * [trevDev()](mailto:trev@trevdev.ca)
* [tsia](mailto:github@tsia.de)
* [umonaca](mailto:53662960+umonaca@users.noreply.github.com)
* [utam0k](mailto:k0ma@utam0k.jp) * [utam0k](mailto:k0ma@utam0k.jp)
* [vpzomtrrfrt](mailto:vpzomtrrfrt@gmail.com) * [vpzomtrrfrt](mailto:vpzomtrrfrt@gmail.com)
* [walfie](mailto:walfington@gmail.com) * [walfie](mailto:walfington@gmail.com)
@ -630,137 +555,12 @@ and provided thanks to the work of the following contributors:
* [yoshipc](mailto:yoooo@yoshipc.net) * [yoshipc](mailto:yoooo@yoshipc.net)
* [Özcan Zafer AYAN](mailto:ozcanzaferayan@gmail.com) * [Özcan Zafer AYAN](mailto:ozcanzaferayan@gmail.com)
* [ばん](mailto:detteiu0321@gmail.com) * [ばん](mailto:detteiu0321@gmail.com)
* [ふるふる](mailto:frfs@users.noreply.github.com) * [みたらしだんご](mailto:mitarashidango@users.noreply.github.com)
* [りんすき](mailto:6533808+rinsuki@users.noreply.github.com) * [りんすき](mailto:6533808+rinsuki@users.noreply.github.com)
* [ヨイツの賢狼ホロ | 3rd style](mailto:horo@yoitsu.moe) * [ヨイツの賢狼ホロ | 3rd style](mailto:horo@yoitsu.moe)
* [唐宗勛](mailto:tangzongxun@hotmail.com)
* [猫吸血鬼ディフリス / 猫ロキP](mailto:deflis@gmail.com) * [猫吸血鬼ディフリス / 猫ロキP](mailto:deflis@gmail.com)
* [艮 鮟鱇](mailto:ushitora_anqou@yahoo.co.jp) * [艮 鮟鱇](mailto:ushitora_anqou@yahoo.co.jp)
* [西小倉宏信](mailto:nishiko@mindia.jp) * [西小倉宏信](mailto:nishiko@mindia.jp)
* [雨宮美羽](mailto:k737566@gmail.com) * [雨宮美羽](mailto:k737566@gmail.com)
This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead. This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead.
## Translators
Following people have contributed to translation of Mastodon:
- Zoltán Gera (*Hungarian*)
- Kristijan Tkalec (*Slovenian*)
- Evert Prants (*Estonian*)
- borys_sh (*Ukrainian*)
- ButterflyOfFire (*Arabic; French*)
- Osoitz (*Basque*)
- oɹʇuʞ (*Spanish, Argentina*)
- koyu (*German*)
- Jeroen (*Dutch*)
- Muha Aliss (*Turkish*)
- 唐宗勛 (*Chinese Simplified*)
- Jeong Arm (*Korean; Esperanto; Japanese*)
- Oguz Ersen (*Turkish*)
- spla (*Catalan*)
- Ramdziana F Y (*Indonesian*)
- Aditoo17 (*Czech*)
- Xosé M. (*Galician*)
- Roboron (*Spanish*)
- Alix Rossi (*Corsican; French*)
- Maya Minatsuki (*Japanese*)
- Masoud Abkenar (*Persian*)
- Thai Localization (*Thai*)
- Marek Ľach (*Slovak; Polish*)
- d5Ziif3K (*Ukrainian*)
- lamnatos (*Greek*)
- Emyn Nant Nefydd (*Welsh*)
- Diluns (*Occitan*)
- atarashiako (*Chinese Simplified*)
- 101010 (*Polish*)
- Yi-Jyun Pan (*Chinese Traditional*)
- silkevicious (*Italian*)
- FédiQuébec (*French*)
- Jaz-Michael King (*Welsh*)
- christalleras (*Norwegian Nynorsk*)
- tykayn (*French*)
- Alessandro Levati (*Italian*)
- carolinagiorno (*Portuguese, Brazilian*)
- taoxvx (*Danish*)
- sabri (*Spanish*)
- Sasha Sorokin (*Russian*)
- shioko (*Chinese Simplified*)
- Evgeny Petrov (*Russian*)
- ariasuni (*French; Esperanto*)
- Tiago Epifânio (*Portuguese*)
- dxwc (*Bengali*)
- liffon (*Swedish*)
- Vanege (*Esperanto*)
- Johan Schiff (*Swedish*)
- kat (*Ukrainian; Russian*)
- oti4500 (*Hungarian; Ukrainian*)
- Juan José Salvador Piedra (*Spanish*)
- diazepan (*Spanish*)
- SHeija (*Finnish*)
- Jack R (*Spanish*)
- Saederup92 (*Danish*)
- Stasiek Michalski (*Polish*)
- Dewi (*Breton; French*)
- cybergene (*Japanese*)
- AW Unad (*Indonesian*)
- Andrea Lo Iacono (*Italian*)
- Ray (*Spanish*)
- Unmual (*Spanish*)
- Ryo (*Korean*)
- juanda097 (*Spanish*)
- Anunnakey (*Macedonian*)
- Cutls (*Japanese*)
- erikstl (*Esperanto*)
- ruine (*Japanese*)
- MadeInSteak (*Finnish*)
- Sokratis Alichanidis (*Greek*)
- dragnucs2 (*Arabic*)
- frumble (*German*)
- Rikard Linde (*Swedish*)
- PPNplus (*Thai*)
- arethsu (*Swedish*)
- EPEMA YT (*German*)
- Rhys Harrison (*Esperanto*)
- KEINOS (*Japanese*)
- filippodb (*Italian*)
- JzshAC (*Chinese Simplified*)
- Rintan1 (*Japanese*)
- Antillion (*Spanish*)
- hiphipvargas (*Portuguese*)
- Ch. (*Korean*)
- tctovsli (*Norwegian Nynorsk*)
- vjasiegd (*Polish*)
- SamitiMed (*Thai*)
- umelard (*Hebrew*)
- 硫酸鶏 (*Japanese*)
- Adrián Lattes (*Spanish*)
- Hinaloe (*Japanese*)
- Renato "Lond" Cerqueira (*Portuguese, Brazilian*)
- parnikkapore (*Thai*)
- Marcin Mikołajczak (*Polish*)
- 森の子リスのミーコの大冒険 (*Japanese*)
- Marcepanek_ (*Polish*)
- Sahak Petrosyan (*Armenian*)
- Daniel Dimitrov (*Bulgarian*)
- Hugh Liu (*Chinese Simplified*)
- Rakino (*Chinese Simplified*)
- hussama (*Portuguese, Brazilian*)
- ThibG (*French*)
- SnDer (*Dutch*)
- PifyZ (*French*)
- eichkat3r (*German*)
- Karol Kosek (*Polish*)
- Akarshan Biswas (*Bengali*)
- Tradjincal (*French*)
- Steven Tappert (*German*)
- sergioaraujo1 (*Portuguese, Brazilian*)
- mmokhi (*Persian*)
- fedot (*Russian*)
- skaaarrr (*German*)
- JackXu (*Chinese Simplified*)
- Lukas Fülling (*German*)
- Zoé Bőle (*German*)
- Dremski (*Bulgarian*)
- tamaina (*Japanese*)
- OpenAlgeria (*Arabic*)

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
Contributing Contributing
============ ============
Thank you for considering contributing to Mastodon 🐘 Thank you for considering contributing to Mastodon 🐘
You can contribute in the following ways: You can contribute in the following ways:
@ -10,17 +10,15 @@ You can contribute in the following ways:
- Contributing code to Mastodon by fixing bugs or implementing features - Contributing code to Mastodon by fixing bugs or implementing features
- Improving the documentation - Improving the documentation
If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
## Bug reports ## Bug reports
Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected in the past using the search function. Please also use descriptive, concise titles. Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected in the past using the search function. Please also use descriptive, concise titles.
## Translations ## Translations
You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase. You can submit translations via [Weblate](https://weblate.joinmastodon.org/). They are periodically merged into the codebase.
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] [![Mastodon translation statistics by language](https://weblate.joinmastodon.org/widgets/mastodon/-/multi-auto.svg)](https://weblate.joinmastodon.org/)
## Pull requests ## Pull requests

View File

@ -1,125 +1,86 @@
FROM ubuntu:18.04 as build-dep FROM node:8.12.0-alpine as node
FROM ruby:2.4.5-alpine3.8
# Use bash for the shell LABEL maintainer="https://github.com/tootsuite/mastodon" \
SHELL ["bash", "-c"] description="Your self-hosted, globally interconnected microblogging community"
# Install Node
ENV NODE_VER="12.11.1"
RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \
apt -y install wget python && \
cd ~ && \
wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-x64.tar.gz && \
tar xf node-v$NODE_VER-linux-x64.tar.gz && \
rm node-v$NODE_VER-linux-x64.tar.gz && \
mv node-v$NODE_VER-linux-x64 /opt/node
# Install jemalloc
ENV JE_VER="5.2.1"
RUN apt update && \
apt -y install make autoconf gcc g++ && \
cd ~ && \
wget https://github.com/jemalloc/jemalloc/archive/$JE_VER.tar.gz && \
tar xf $JE_VER.tar.gz && \
cd jemalloc-$JE_VER && \
./autogen.sh && \
./configure --prefix=/opt/jemalloc && \
make -j$(nproc) > /dev/null && \
make install_bin install_include install_lib
# Install ruby
ENV RUBY_VER="2.6.5"
ENV CPPFLAGS="-I/opt/jemalloc/include"
ENV LDFLAGS="-L/opt/jemalloc/lib/"
RUN apt update && \
apt -y install build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev \
libncurses5-dev libffi-dev zlib1g-dev libssl-dev && \
cd ~ && \
wget https://cache.ruby-lang.org/pub/ruby/${RUBY_VER%.*}/ruby-$RUBY_VER.tar.gz && \
tar xf ruby-$RUBY_VER.tar.gz && \
cd ruby-$RUBY_VER && \
./configure --prefix=/opt/ruby \
--with-jemalloc \
--with-shared \
--disable-install-doc && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
make -j$(nproc) > /dev/null && \
make install
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
RUN npm install -g yarn && \
gem install bundler && \
apt update && \
apt -y install git libicu-dev libidn11-dev \
libpq-dev libprotobuf-dev protobuf-compiler
COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN cd /opt/mastodon && \
bundle install -j$(nproc) --deployment --without development test && \
yarn install --pure-lockfile
FROM ubuntu:18.04
# Copy over all the langs needed for runtime
COPY --from=build-dep /opt/node /opt/node
COPY --from=build-dep /opt/ruby /opt/ruby
COPY --from=build-dep /opt/jemalloc /opt/jemalloc
# Add more PATHs to the PATH
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin"
# Create the mastodon user
ARG UID=991 ARG UID=991
ARG GID=991 ARG GID=991
RUN apt update && \
echo "Etc/UTC" > /etc/localtime && \
ln -s /opt/jemalloc/lib/* /usr/lib/ && \
apt install -y whois wget && \
addgroup --gid $GID mastodon && \
useradd -m -u $UID -g $GID -d /opt/mastodon mastodon && \
echo "mastodon:`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 24 | mkpasswd -s -m sha-256`" | chpasswd
# Install mastodon runtime deps ENV PATH=/mastodon/bin:$PATH \
RUN apt -y --no-install-recommends install \ RAILS_SERVE_STATIC_FILES=true \
libssl1.1 libpq5 imagemagick ffmpeg \ RAILS_ENV=production \
libicu60 libprotobuf10 libidn11 libyaml-0-2 \ NODE_ENV=production
file ca-certificates tzdata libreadline7 && \
apt -y install gcc && \
ln -s /opt/mastodon /mastodon && \
gem install bundler && \
rm -rf /var/cache && \
rm -rf /var/lib/apt/lists/*
# Add tini ARG LIBICONV_VERSION=1.15
ENV TINI_VERSION="0.18.0" ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
ENV TINI_SUM="12d20136605531b09a2c2dac02ccee85e1b874eb322ef6baf7561cd93f93c855"
ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini /tini
RUN echo "$TINI_SUM tini" | sha256sum -c -
RUN chmod +x /tini
# Copy over mastodon source, and dependencies from building, and set permissions EXPOSE 3000 4000
COPY --chown=mastodon:mastodon . /opt/mastodon
COPY --from=build-dep --chown=mastodon:mastodon /opt/mastodon /opt/mastodon
# Run mastodon services in prod mode WORKDIR /mastodon
ENV RAILS_ENV="production"
ENV NODE_ENV="production"
# Tell rails to serve static files COPY --from=node /usr/local/bin/node /usr/local/bin/node
ENV RAILS_SERVE_STATIC_FILES="true" COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
ENV BIND="0.0.0.0" COPY --from=node /usr/local/bin/npm /usr/local/bin/npm
COPY --from=node /opt/yarn-* /opt/yarn
RUN apk -U upgrade \
&& apk add -t build-dependencies \
build-base \
icu-dev \
libidn-dev \
libressl \
libtool \
postgresql-dev \
protobuf-dev \
python \
&& apk add \
ca-certificates \
ffmpeg \
file \
git \
icu-libs \
imagemagick \
libidn \
libpq \
protobuf \
tini \
tzdata \
&& update-ca-certificates \
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
&& ln -s /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg \
&& mkdir -p /tmp/src /opt \
&& wget -O libiconv.tar.gz "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \
&& cd /tmp/src/libiconv-$LIBICONV_VERSION \
&& ./configure --prefix=/usr/local \
&& make -j$(getconf _NPROCESSORS_ONLN)\
&& make install \
&& libtool --finish /usr/local/lib \
&& cd /mastodon \
&& rm -rf /tmp/* /var/cache/apk/*
COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
&& yarn install --pure-lockfile --ignore-engines \
&& yarn cache clean
RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon \
&& mkdir -p /mastodon/public/system /mastodon/public/assets /mastodon/public/packs \
&& chown -R mastodon:mastodon /mastodon/public
COPY . /mastodon
RUN chown -R mastodon:mastodon /mastodon
VOLUME /mastodon/public/system
# Set the run user
USER mastodon USER mastodon
# Precompile assets RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile
RUN cd ~ && \
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile && \
yarn cache clean
# Set the work dir and the container entry point ENTRYPOINT ["/sbin/tini", "--"]
WORKDIR /opt/mastodon
ENTRYPOINT ["/tini", "--"]

110
Gemfile
View File

@ -1,38 +1,37 @@
# frozen_string_literal: true # frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '>= 2.4.0', '< 2.7.0' ruby '>= 2.3.0', '< 2.6.0'
gem 'pkg-config', '~> 1.3' gem 'pkg-config', '~> 1.3'
gem 'puma', '~> 4.2' gem 'puma', '~> 3.12'
gem 'rails', '~> 5.2.3' gem 'rails', '~> 5.2.1'
gem 'thor', '~> 0.20' gem 'thor', '~> 0.20'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.1' gem 'pg', '~> 1.1'
gem 'makara', '~> 0.4' gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.3' gem 'pghero', '~> 2.2'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.5'
gem 'aws-sdk-s3', '~> 1.48', require: false gem 'aws-sdk-s3', '~> 1.23', 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 'paperclip-av-transcoder', '~> 0.6'
gem 'streamio-ffmpeg', '~> 3.0' gem 'streamio-ffmpeg', '~> 3.0'
gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7' gem 'addressable', '~> 2.5'
gem 'bootsnap', '~> 1.4', require: false gem 'bootsnap', '~> 1.3', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.6' gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.1' gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.4' gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.7' gem 'devise', '~> 4.5'
gem 'devise-two-factor', '~> 3.1' gem 'devise-two-factor', '~> 3.0'
group :pam_authentication, optional: true do group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.2' gem 'devise_pam_authenticatable2', '~> 9.2'
@ -41,70 +40,64 @@ end
gem 'net-ldap', '~> 0.10' gem 'net-ldap', '~> 0.10'
gem 'omniauth-cas', '~> 1.1' gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.2'
gem 'discard', '~> 1.1' gem 'doorkeeper', '~> 5.0'
gem 'doorkeeper', '~> 5.2'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'goldfinger', '~> 2.1' gem 'goldfinger', '~> 2.1'
gem 'hiredis', '~> 0.6' gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.5' gem 'redis-namespace', '~> 1.5'
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
gem 'htmlentities', '~> 4.3' gem 'htmlentities', '~> 4.3'
gem 'http', '~> 3.3' gem 'http', '~> 3.3'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2'
gem 'httplog', '~> 1.3' gem 'httplog', '~> 1.1'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1' gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.3', require: 'mime/types/columnar' gem 'mime-types', '~> 3.2', require: 'mime/types/columnar'
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532' gem 'nokogiri', '~> 1.8'
gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.9' gem 'oj', '~> 3.7'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.11' gem 'ox', '~> 2.10'
gem 'parslet'
gem 'parallel', '~> 1.17'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.1' gem 'pundit', '~> 2.0'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 6.1' gem 'rack-attack', '~> 5.4'
gem 'rack-cors', '~> 1.0', require: 'rack/cors' gem 'rack-cors', '~> 1.0', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1' gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.0', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 0.10' gem 'rqrcode', '~> 0.10'
gem 'ruby-progressbar', '~> 1.10' gem 'sanitize', '~> 5.0'
gem 'sanitize', '~> 5.1'
gem 'sidekiq', '~> 5.2' gem 'sidekiq', '~> 5.2'
gem 'sidekiq-scheduler', '~> 3.0' gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 6.0' gem 'sidekiq-unique-jobs', '~> 5.0'
gem 'sidekiq-bulk', '~>0.2.0' gem 'sidekiq-bulk', '~>0.1.1'
gem 'simple-navigation', '~> 4.1' gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 4.1' gem 'simple_form', '~> 4.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3' gem 'stoplight', '~> 2.1.3'
gem 'strong_migrations', '~> 0.4' gem 'strong_migrations', '~> 0.3'
gem 'tty-command', '~> 0.9', require: false gem 'tty-command', '~> 0.8', require: false
gem 'tty-prompt', '~> 0.19', require: false gem 'tty-prompt', '~> 0.17', require: false
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2019' gem 'tzinfo-data', '~> 1.2018'
gem 'webpacker', '~> 4.0' gem 'webpacker', '~> 3.5'
gem 'webpush' gem 'webpush'
gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: 'e742697a0906e74e8bb777ef98137bc3955d981d' gem 'json-ld', '~> 2.2'
gem 'json-ld-preloaded', '~> 3.0' gem 'json-ld-preloaded', '~> 2.2'
gem 'rdf-normalize', '~> 0.3' gem 'rdf-normalize', '~> 0.3'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.20' gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.4' gem 'fuubar', '~> 2.3'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.7' gem 'pry-byebug', '~> 3.6'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.8' gem 'rspec-rails', '~> 3.8'
end end
@ -114,30 +107,30 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.29' gem 'capybara', '~> 3.10'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.5' gem 'faker', '~> 1.9'
gem 'microformats', '~> 4.1' gem 'microformats', '~> 4.0'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.17', require: false gem 'simplecov', '~> 0.16', require: false
gem 'webmock', '~> 3.7' gem 'webmock', '~> 3.4'
gem 'parallel_tests', '~> 2.29' gem 'parallel_tests', '~> 2.26'
end end
group :development do group :development do
gem 'active_record_query_trace', '~> 1.6' gem 'active_record_query_trace', '~> 1.5'
gem 'annotate', '~> 2.7' gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.5' gem 'better_errors', '~> 2.5'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 6.0' gem 'bullet', '~> 5.7'
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.4'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.74', require: false gem 'rubocop', '~> 0.60', require: false
gem 'rubocop-rails', '~> 2.3', require: false gem 'brakeman', '~> 4.3', require: false
gem 'brakeman', '~> 4.6', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.57', require: false
gem 'capistrano', '~> 3.11' gem 'capistrano', '~> 3.11'
gem 'capistrano-rails', '~> 1.4' gem 'capistrano-rails', '~> 1.4'
@ -149,9 +142,6 @@ group :development do
end end
group :production do group :production do
gem 'lograge', '~> 0.11' gem 'lograge', '~> 0.10'
gem 'redis-rails', '~> 5.0' gem 'redis-rails', '~> 5.0'
end end
gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,2 @@
web: if [ "$RUN_STREAMING" != "true" ]; then BIND=0.0.0.0 bundle exec puma -C config/puma.rb; else BIND=0.0.0.0 node ./streaming; fi web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq worker: bundle exec sidekiq
# For the streaming API, you need a separate app that shares Postgres and Redis:
#
# heroku create
# heroku buildpacks:add heroku/nodejs
# heroku config:set RUN_STREAMING=true
# heroku addons:attach <main-app>::DATABASE
# heroku addons:attach <main-app>::REDIS
#
# and let the main app use the separate app:
#
# heroku config:set STREAMING_API_BASE_URL=wss://<streaming-app>.herokuapp.com -a <main-app>

View File

@ -4,13 +4,13 @@
[![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases] [![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci] [![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate] [![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] [![Translation status](https://weblate.joinmastodon.org/widgets/mastodon/-/svg-badge.svg)][weblate]
[![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker] [![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker]
[releases]: https://github.com/tootsuite/mastodon/releases [releases]: https://github.com/tootsuite/mastodon/releases
[circleci]: https://circleci.com/gh/tootsuite/mastodon [circleci]: https://circleci.com/gh/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon [code_climate]: https://codeclimate.com/github/tootsuite/mastodon
[crowdin]: https://crowdin.com/project/mastodon [weblate]: https://weblate.joinmastodon.org/engage/mastodon/
[docker]: https://hub.docker.com/r/tootsuite/mastodon/ [docker]: https://hub.docker.com/r/tootsuite/mastodon/
Mastodon is a **free, open-source social network server** based on ActivityPub. Follow friends and discover new ones. Publish anything you want: links, pictures, text, video. All servers of Mastodon are interoperable as a federated network, i.e. users on one server can seamlessly communicate with users from another one. This includes non-Mastodon software that also implements ActivityPub! Mastodon is a **free, open-source social network server** based on ActivityPub. Follow friends and discover new ones. Publish anything you want: links, pictures, text, video. All servers of Mastodon are interoperable as a federated network, i.e. users on one server can seamlessly communicate with users from another one. This includes non-Mastodon software that also implements ActivityPub!
@ -21,7 +21,7 @@ Click below to **learn more** in a video:
[youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE [youtube_demo]: https://www.youtube.com/watch?v=IPSbNdBmWKE
## Navigation ## Navigation
- [Project homepage 🐘](https://joinmastodon.org) - [Project homepage 🐘](https://joinmastodon.org)
- [Support the development via Patreon][patreon] - [Support the development via Patreon][patreon]
@ -55,7 +55,7 @@ Private posts, locked accounts, phrase filtering, muting, blocking and all sorts
**OAuth2 and a straightforward REST API** **OAuth2 and a straightforward REST API**
Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Streaming APIs, resulting in a rich app ecosystem with a lot of choices! Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Streaming APIs, resulting in a rich app ecosystem with a lot of choice!
## Deployment ## Deployment
@ -80,13 +80,13 @@ A **Vagrant** configuration is included for development purposes.
Mastodon is **free, open source software** licensed under **AGPLv3**. Mastodon is **free, open source software** licensed under **AGPLv3**.
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md)
**IRC channel**: #mastodon on irc.freenode.net **IRC channel**: #mastodon on irc.freenode.net
## License ## License
Copyright (C) 2016-2019 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md)) Copyright (C) 2016-2018 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

13
Vagrantfile vendored
View File

@ -44,18 +44,7 @@ sudo apt-get install \
# Install rvm # Install rvm
read RUBY_VERSION < .ruby-version read RUBY_VERSION < .ruby-version
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
$($gpg_command)
if [ $? -ne 0 ];then
echo "GPG command failed, This prevented RVM from installing."
echo "Retrying once..." && $($gpg_command)
if [ $? -ne 0 ];then
echo "GPG failed for the second time, please ensure network connectivity."
echo "Exiting..." && exit 1
fi
fi
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm source /home/vagrant/.rvm/scripts/rvm

View File

@ -13,6 +13,15 @@
"description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)", "description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)",
"required": true "required": true
}, },
"LOCAL_HTTPS": {
"description": "Will your domain support HTTPS? (Automatic for herokuapp, requires manual configuration for custom domains)",
"value": "false",
"required": true
},
"PAPERCLIP_SECRET": {
"description": "The secret key for storing media files",
"generator": "secret"
},
"SECRET_KEY_BASE": { "SECRET_KEY_BASE": {
"description": "The secret key base", "description": "The secret key base",
"generator": "secret" "generator": "secret"

View File

@ -1,43 +0,0 @@
# frozen_string_literal: true
class AccountsIndex < Chewy::Index
settings index: { refresh_interval: '5m' }, analysis: {
analyzer: {
content: {
tokenizer: 'whitespace',
filter: %w(lowercase asciifolding cjk_width),
},
edge_ngram: {
tokenizer: 'edge_ngram',
filter: %w(lowercase asciifolding cjk_width),
},
},
tokenizer: {
edge_ngram: {
type: 'edge_ngram',
min_gram: 1,
max_gram: 15,
},
},
}
define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do
root date_detection: false do
field :id, type: 'long'
field :display_name, type: 'text', analyzer: 'content' do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
end
end
end

View File

@ -31,31 +31,31 @@ class StatusesIndex < Chewy::Index
}, },
} }
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments), delete_if: ->(status) { status.searchable_by.empty? } do define_type ::Status.unscoped.without_reblogs do
crutch :mentions do |collection| crutch :mentions do |collection|
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id) data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end end
crutch :favourites do |collection| crutch :favourites do |collection|
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id) data = ::Favourite.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end end
crutch :reblogs do |collection| crutch :reblogs do |collection|
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id) data = ::Status.where(reblog_of_id: collection.map(&:id)).pluck(:reblog_of_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end end
root date_detection: false do root date_detection: false do
field :id, type: 'long'
field :account_id, type: 'long' field :account_id, type: 'long'
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do
field :stemmed, type: 'text', analyzer: 'content' field :stemmed, type: 'text', analyzer: 'content'
end end
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
field :created_at, type: 'date'
end end
end end
end end

View File

@ -1,37 +0,0 @@
# frozen_string_literal: true
class TagsIndex < Chewy::Index
settings index: { refresh_interval: '15m' }, analysis: {
analyzer: {
content: {
tokenizer: 'keyword',
filter: %w(lowercase asciifolding cjk_width),
},
edge_ngram: {
tokenizer: 'edge_ngram',
filter: %w(lowercase asciifolding cjk_width),
},
},
tokenizer: {
edge_ngram: {
type: 'edge_ngram',
min_gram: 2,
max_gram: 15,
},
},
}
define_type ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } do
root date_detection: false do
field :name, type: 'text', analyzer: 'content' do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } }
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
end
end
end

View File

@ -1,64 +1,42 @@
# frozen_string_literal: true # frozen_string_literal: true
class AboutController < ApplicationController class AboutController < ApplicationController
layout 'public' before_action :set_body_classes
before_action :set_instance_presenter, only: [:show, :more, :terms]
before_action :require_open_federation!, only: [:show, :more] def show
before_action :set_body_classes, only: :show serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
before_action :set_instance_presenter @initial_state_json = serializable_resource.to_json
before_action :set_expires_in, only: [:show, :more, :terms]
skip_before_action :require_functional!, only: [:more, :terms]
def show; end
def more
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
@contents = toc_generator.html
@table_of_contents = toc_generator.toc
@blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
end end
def terms; end def more
render layout: 'public'
end
helper_method :display_blocks? def terms
helper_method :display_blocks_rationale? render layout: 'public'
helper_method :public_fetch_mode? end
helper_method :new_user
private private
def require_open_federation!
not_found if whitelist_mode?
end
def display_blocks?
Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
end
def display_blocks_rationale?
Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
end
def new_user def new_user
User.new.tap do |user| User.new.tap(&:build_account)
user.build_account
user.build_invite_request
end
end end
helper_method :new_user
def set_instance_presenter def set_instance_presenter
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end
def set_body_classes def set_body_classes
@hide_navbar = true @body_classes = 'with-modals'
end end
def set_expires_in def initial_state_params
expires_in 0, public: true {
settings: { known_fediverse: Setting.show_known_fediverse_at_about_page },
token: current_session&.token,
}
end end
end end

View File

@ -4,22 +4,15 @@ class AccountsController < ApplicationController
PAGE_SIZE = 20 PAGE_SIZE = 20
include AccountControllerConcern include AccountControllerConcern
include SignatureAuthentication
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_body_classes
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format) }
skip_before_action :require_functional!
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
expires_in 0, public: true unless user_signed_in? @body_classes = 'with-modals'
@pinned_statuses = [] @pinned_statuses = []
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
@featured_hashtags = @account.featured_tags.order(statuses_count: :desc)
if current_account && @account.blocking?(current_account) if current_account && @account.blocking?(current_account)
@statuses = [] @statuses = []
@ -29,7 +22,6 @@ class AccountsController < ApplicationController
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_status_page(params) @statuses = filtered_status_page(params)
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
@rss_url = rss_url
unless @statuses.empty? unless @statuses.empty?
@older_url = older_url if @statuses.last.id > filtered_statuses.last.id @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
@ -37,34 +29,34 @@ class AccountsController < ApplicationController
end end
end end
format.rss do format.atom do
expires_in 1.minute, public: true @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end
@statuses = filtered_statuses.without_reblogs.without_replies.limit(PAGE_SIZE) format.rss do
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag]) render xml: RSS::AccountSerializer.render(@account, @statuses)
end end
format.json do format.json do
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?) skip_session!
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
end
end end
end end
end end
private private
def set_body_classes
@body_classes = 'with-modals'
end
def show_pinned_statuses? def show_pinned_statuses?
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? [replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none?
end end
def filtered_statuses def filtered_statuses
default_statuses.tap do |statuses| default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested?
statuses.merge!(only_media_scope) if media_requested? statuses.merge!(only_media_scope) if media_requested?
statuses.merge!(no_replies_scope) unless replies_requested? statuses.merge!(no_replies_scope) unless replies_requested?
end end
@ -86,29 +78,12 @@ class AccountsController < ApplicationController
Status.without_replies Status.without_replies
end end
def hashtag_scope def set_account
tag = Tag.find_normalized(params[:tag]) @account = Account.find_local!(params[:username])
if tag
Status.tagged_with(tag.id)
else
Status.none
end
end
def username_param
params[:username]
end
def rss_url
if tag_requested?
short_account_tag_url(@account, params[:tag], format: 'rss')
else
short_account_url(@account, format: 'rss')
end
end end
def older_url def older_url
::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}")
pagination_url(max_id: @statuses.last.id) pagination_url(max_id: @statuses.last.id)
end end
@ -117,9 +92,7 @@ class AccountsController < ApplicationController
end end
def pagination_url(max_id: nil, min_id: nil) def pagination_url(max_id: nil, min_id: nil)
if tag_requested? if media_requested?
short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id)
elsif media_requested?
short_account_media_url(@account, max_id: max_id, min_id: min_id) short_account_media_url(@account, max_id: max_id, min_id: min_id)
elsif replies_requested? elsif replies_requested?
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
@ -129,15 +102,11 @@ class AccountsController < ApplicationController
end end
def media_requested? def media_requested?
request.path.ends_with?('/media') && !tag_requested? request.path.ends_with?('/media')
end end
def replies_requested? def replies_requested?
request.path.ends_with?('/with_replies') && !tag_requested? request.path.ends_with?('/with_replies')
end
def tag_requested?
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end end
def filtered_status_page(params) def filtered_status_page(params)
@ -147,12 +116,4 @@ class AccountsController < ApplicationController
filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
end end
end end
def restrict_fields_to
if signed_request_account.present? || public_fetch_mode?
# Return all fields
else
%i(id type preferred_username inbox public_key endpoints)
end
end
end end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
class ActivityPub::BaseController < Api::BaseController
skip_before_action :require_authenticated_user!
private
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode?
end
end

View File

@ -1,21 +1,26 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::CollectionsController < ActivityPub::BaseController class ActivityPub::CollectionsController < Api::BaseController
include SignatureVerification include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!, if: :authorized_fetch_mode? before_action :set_account
before_action :set_size before_action :set_size
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers
def show def show
expires_in 3.minutes, public: public_fetch_mode? render json: collection_presenter,
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json',
skip_activities: true
end end
private private
def set_account
@account = Account.find_local!(params[:account_username])
end
def set_statuses def set_statuses
@statuses = scope_for_collection @statuses = scope_for_collection
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
@ -26,18 +31,18 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
when 'featured' when 'featured'
@account.pinned_statuses.count @account.pinned_statuses.count
else else
raise ActiveRecord::RecordNotFound raise ActiveRecord::NotFound
end end
end end
def scope_for_collection def scope_for_collection
case params[:id] case params[:id]
when 'featured' when 'featured'
return Status.none if @account.blocking?(signed_request_account) @account.statuses.permitted_for(@account, signed_request_account).tap do |scope|
scope.merge!(@account.pinned_statuses)
@account.pinned_statuses end
else else
raise ActiveRecord::RecordNotFound raise ActiveRecord::NotFound
end end
end end

View File

@ -1,45 +1,28 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::InboxesController < ActivityPub::BaseController class ActivityPub::InboxesController < Api::BaseController
include SignatureVerification include SignatureVerification
include JsonLdHelper
include AccountOwnedConcern
before_action :skip_unknown_actor_delete before_action :set_account
before_action :require_signature!
def create def create
upgrade_account if signed_request_account
process_payload upgrade_account
head 202 process_payload
head 202
else
render plain: signature_verification_failure_reason, status: 401
end
end end
private private
def skip_unknown_actor_delete def set_account
head 202 if unknown_deleted_account? @account = Account.find_local!(params[:account_username]) if params[:account_username]
end
def unknown_deleted_account?
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?
rescue Oj::ParseError
false
end
def account_required?
params[:account_username].present?
end end
def body def body
return @body if defined?(@body) @body ||= request.body.read
@body = request.body.read
@body.force_encoding('UTF-8') if @body.present?
request.body.rewind if request.body.respond_to?(:rewind)
@body
end end
def upgrade_account def upgrade_account
@ -48,10 +31,11 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
ResolveAccountWorker.perform_async(signed_request_account.acct) ResolveAccountWorker.perform_async(signed_request_account.acct)
end end
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
DeliveryFailureTracker.track_inverse_success!(signed_request_account) DeliveryFailureTracker.track_inverse_success!(signed_request_account)
end end
def process_payload def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id) ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8'), @account&.id)
end end
end end

View File

@ -1,22 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::OutboxesController < ActivityPub::BaseController class ActivityPub::OutboxesController < Api::BaseController
LIMIT = 20 LIMIT = 20
include SignatureVerification include SignatureVerification
include AccountOwnedConcern
before_action :require_signature!, if: :authorized_fetch_mode? before_action :set_account
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers
def show def show
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
private private
def set_account
@account = Account.find_local!(params[:account_username])
end
def outbox_presenter def outbox_presenter
if page_requested? if page_requested?
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(

View File

@ -1,71 +0,0 @@
# frozen_string_literal: true
class ActivityPub::RepliesController < ActivityPub::BaseController
include SignatureAuthentication
include Authorization
include AccountOwnedConcern
DESCENDANTS_LIMIT = 60
before_action :require_signature!, if: :authorized_fetch_mode?
before_action :set_status
before_action :set_cache_headers
before_action :set_replies
def index
expires_in 0, public: public_fetch_mode?
render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true
end
private
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
end
def set_replies
@replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
def replies_collection_presenter
page = ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status, page_params),
type: :unordered,
part_of: account_status_replies_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.uri }
)
return page if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_status_replies_url(@account, @status),
type: :unordered,
first: page
)
end
def page_requested?
params[:page] == 'true'
end
def next_page
only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
account_status_replies_url(
@account,
@status,
page: true,
min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id,
only_other_accounts: only_other_accounts
)
end
def page_params
params_slice(:only_other_accounts, :min_id).merge(page: true)
end
end

View File

@ -1,36 +0,0 @@
# frozen_string_literal: true
module Admin
class AccountActionsController < BaseController
before_action :set_account
def new
@account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true, include_statuses: true)
@warning_presets = AccountWarningPreset.all
end
def create
account_action = Admin::AccountAction.new(resource_params)
account_action.target_account = @account
account_action.current_account = current_account
account_action.save!
if account_action.with_report?
redirect_to admin_reports_path
else
redirect_to admin_account_path(@account.id)
end
end
private
def set_account
@account = Account.find(params[:account_id])
end
def resource_params
params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses)
end
end
end

View File

@ -14,7 +14,6 @@ module Admin
else else
@account = @account_moderation_note.target_account @account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom
render template: 'admin/accounts/show' render template: 'admin/accounts/show'
end end

View File

@ -2,9 +2,9 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :enable, :disable, :memorialize]
before_action :require_remote_account!, only: [:redownload] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] before_action :require_local_account!, only: [:enable, :disable, :memorialize]
def index def index
authorize :account, :index? authorize :account, :index?
@ -13,10 +13,20 @@ module Admin
def show def show
authorize @account, :show? authorize @account, :show?
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom end
def subscribe
authorize @account, :subscribe?
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def unsubscribe
authorize @account, :unsubscribe?
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end end
def memorialize def memorialize
@ -33,37 +43,19 @@ module Admin
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def approve def disable
authorize @account.user, :approve? authorize @account.user, :disable?
@account.user.approve! @account.user.disable!
redirect_to admin_pending_accounts_path log_action :disable, @account.user
end
def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path
end
def unsilence
authorize @account, :unsilence?
@account.unsilence!
log_action :unsilence, @account
redirect_to admin_account_path(@account.id)
end
def unsuspend
authorize @account, :unsuspend?
@account.unsuspend!
log_action :unsuspend, @account
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def redownload def redownload
authorize @account, :redownload? authorize @account, :redownload?
@account.update!(last_webfingered_at: nil) @account.reset_avatar!
ResolveAccountService.new.call(@account) @account.reset_header!
@account.save!
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
@ -79,17 +71,6 @@ module Admin
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def remove_header
authorize @account, :remove_header?
@account.header = nil
@account.save!
log_action :remove_header, @account.user
redirect_to admin_account_path(@account.id)
end
private private
def set_account def set_account
@ -113,10 +94,8 @@ module Admin
:local, :local,
:remote, :remote,
:by_domain, :by_domain,
:active,
:pending,
:disabled,
:silenced, :silenced,
:alphabetic,
:suspended, :suspended,
:username, :username,
:display_name, :display_name,

View File

@ -15,9 +15,5 @@ module Admin
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end end
end end

View File

@ -25,6 +25,10 @@ module Admin
private private
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
def check_confirmation def check_confirmation
if @user.confirmed? if @user.confirmed?
flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed')

View File

@ -2,20 +2,16 @@
module Admin module Admin
class CustomEmojisController < BaseController class CustomEmojisController < BaseController
include ObfuscateFilename before_action :set_custom_emoji, except: [:index, :new, :create]
before_action :set_filter_params
obfuscate_filename [:custom_emoji, :image]
def index def index
authorize :custom_emoji, :index? authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
@form = Form::CustomEmojiBatch.new
end end
def new def new
authorize :custom_emoji, :create? authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new @custom_emoji = CustomEmoji.new
end end
@ -32,17 +28,69 @@ module Admin
end end
end end
def batch def update
@form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button)) authorize @custom_emoji, :update?
@form.save
rescue ActionController::ParameterMissing if @custom_emoji.update(resource_params)
flash[:alert] = I18n.t('admin.accounts.no_account_selected') log_action :update, @custom_emoji
ensure flash[:notice] = I18n.t('admin.custom_emojis.updated_msg')
redirect_to admin_custom_emojis_path(filter_params) else
flash[:alert] = I18n.t('admin.custom_emojis.update_failed_msg')
end
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def destroy
authorize @custom_emoji, :destroy?
@custom_emoji.destroy!
log_action :destroy, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.destroyed_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def copy
authorize @custom_emoji, :copy?
emoji = CustomEmoji.find_or_initialize_by(domain: nil,
shortcode: @custom_emoji.shortcode)
emoji.image = @custom_emoji.image
if emoji.save
log_action :create, emoji
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
else
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
end
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false)
log_action :enable, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def disable
authorize @custom_emoji, :disable?
@custom_emoji.update!(disabled: true)
log_action :disable, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end end
private private
def set_custom_emoji
@custom_emoji = CustomEmoji.find(params[:id])
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
end
def resource_params def resource_params
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker) params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end end
@ -52,29 +100,12 @@ module Admin
end end
def filter_params def filter_params
params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page) params.permit(
end :local,
:remote,
def action_from_button :by_domain,
if params[:update] :shortcode
'update' )
elsif params[:list]
'list'
elsif params[:unlist]
'unlist'
elsif params[:enable]
'enable'
elsif params[:disable]
'disable'
elsif params[:copy]
'copy'
elsif params[:delete]
'delete'
end
end
def form_custom_emoji_batch_params
params.require(:form_custom_emoji_batch).permit(:action, :category_id, :category_name, custom_emoji_ids: [])
end end
end end
end end

View File

@ -5,13 +5,12 @@ module Admin
class DashboardController < BaseController class DashboardController < BaseController
def index def index
@users_count = User.count @users_count = User.count
@pending_users_count = User.pending.count
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0 @registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}") @logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0 @interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
@relay_enabled = Relay.enabled.exists? @relay_enabled = Relay.enabled.exists?
@single_user_mode = Rails.configuration.x.single_user_mode @single_user_mode = Rails.configuration.x.single_user_mode
@registrations_enabled = Setting.registrations_mode != 'none' @registrations_enabled = Setting.open_registrations
@deletions_enabled = Setting.open_deletion @deletions_enabled = Setting.open_deletion
@invites_enabled = Setting.min_invite_role == 'user' @invites_enabled = Setting.min_invite_role == 'user'
@search_enabled = Chewy.enabled? @search_enabled = Chewy.enabled?
@ -20,7 +19,7 @@ module Admin
@redis_version = redis_info['redis_version'] @redis_version = redis_info['redis_version']
@reports_count = Report.unresolved.count @reports_count = Report.unresolved.count
@queue_backlog = Sidekiq::Stats.new.enqueued @queue_backlog = Sidekiq::Stats.new.enqueued
@recent_users = User.confirmed.recent.includes(:account).limit(8) @recent_users = User.confirmed.recent.includes(:account).limit(4)
@database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size'] @database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
@redis_size = redis_info['used_memory'] @redis_size = redis_info['used_memory']
@ldap_enabled = ENV['LDAP_ENABLED'] == 'true' @ldap_enabled = ENV['LDAP_ENABLED'] == 'true'
@ -28,14 +27,7 @@ module Admin
@saml_enabled = ENV['SAML_ENABLED'] == 'true' @saml_enabled = ENV['SAML_ENABLED'] == 'true'
@pam_enabled = ENV['PAM_ENABLED'] == 'true' @pam_enabled = ENV['PAM_ENABLED'] == 'true'
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' @hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
@trending_hashtags = TrendingTags.get(10, filtered: false) @trending_hashtags = TrendingTags.get(7)
@pending_tags_count = Tag.pending_review.count
@authorized_fetch = authorized_fetch_mode?
@whitelist_enabled = whitelist_mode?
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
@spam_check_enabled = Setting.spam_check_enabled
@trends_enabled = Setting.trends
end end
private private
@ -45,13 +37,7 @@ module Admin
end end
def redis_info def redis_info
@redis_info ||= begin @redis_info ||= Redis.current.info
if Redis.current.is_a?(Redis::Namespace)
Redis.current.redis.info
else
Redis.current.info
end
end
end end
end end
end end

View File

@ -1,40 +0,0 @@
# frozen_string_literal: true
class Admin::DomainAllowsController < Admin::BaseController
before_action :set_domain_allow, only: [:destroy]
def new
authorize :domain_allow, :create?
@domain_allow = DomainAllow.new(domain: params[:_domain])
end
def create
authorize :domain_allow, :create?
@domain_allow = DomainAllow.new(resource_params)
if @domain_allow.save
log_action :create, @domain_allow
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.created_msg')
else
render :new
end
end
def destroy
authorize @domain_allow, :destroy?
UnallowDomainService.new.call(@domain_allow)
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
end
private
def set_domain_allow
@domain_allow = DomainAllow.find(params[:id])
end
def resource_params
params.require(:domain_allow).permit(:domain)
end
end

View File

@ -2,56 +2,29 @@
module Admin module Admin
class DomainBlocksController < BaseController class DomainBlocksController < BaseController
before_action :set_domain_block, only: [:show, :destroy, :edit, :update] before_action :set_domain_block, only: [:show, :destroy]
def index
authorize :domain_block, :index?
@domain_blocks = DomainBlock.page(params[:page])
end
def new def new
authorize :domain_block, :create? authorize :domain_block, :create?
@domain_block = DomainBlock.new(domain: params[:_domain]) @domain_block = DomainBlock.new
end
def edit
authorize :domain_block, :create?
end end
def create def create
authorize :domain_block, :create? authorize :domain_block, :create?
@domain_block = DomainBlock.new(resource_params) @domain_block = DomainBlock.new(resource_params)
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save
flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
@domain_block.errors[:domain].clear
render :new
else
if existing_domain_block.present?
@domain_block = existing_domain_block
@domain_block.update(resource_params)
end
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else
render :new
end
end
end
def update
authorize :domain_block, :create?
@domain_block.update(update_params)
severity_changed = @domain_block.severity_changed?
if @domain_block.save if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id, severity_changed) DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block log_action :create, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
else else
render :edit render :new
end end
end end
@ -61,9 +34,9 @@ module Admin
def destroy def destroy
authorize @domain_block, :destroy? authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block) UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
log_action :destroy, @domain_block log_action :destroy, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg') redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
end end
private private
@ -72,12 +45,12 @@ module Admin
@domain_block = DomainBlock.find(params[:id]) @domain_block = DomainBlock.find(params[:id])
end end
def update_params def resource_params
params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment) params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :retroactive)
end end
def resource_params def retroactive_unblock?
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment) ActiveRecord::Type.lookup(:boolean).cast(resource_params[:retroactive])
end end
end end
end end

View File

@ -1,18 +0,0 @@
# frozen_string_literal: true
module Admin
class FollowersController < BaseController
before_action :set_account
PER_PAGE = 40
def index
authorize :account, :index?
@followers = @account.followers.local.recent.page(params[:page]).per(PER_PAGE)
end
def set_account
@account = Account.find(params[:account_id])
end
end
end

View File

@ -2,53 +2,22 @@
module Admin module Admin
class InstancesController < BaseController class InstancesController < BaseController
before_action :set_domain_block, only: :show
before_action :set_domain_allow, only: :show
before_action :set_instance, only: :show
def index def index
authorize :instance, :index? authorize :instance, :index?
@instances = ordered_instances @instances = ordered_instances
end end
def show def resubscribe
authorize :instance, :show? authorize :instance, :resubscribe?
params.require(:by_domain)
@following_count = Follow.where(account: Account.where(domain: params[:id])).count Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count redirect_to admin_instances_path
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
@private_comment = @domain_block&.private_comment
@public_comment = @domain_block&.public_comment
end end
private private
def set_domain_block
@domain_block = DomainBlock.rule_for(params[:id])
end
def set_domain_allow
@domain_allow = DomainAllow.rule_for(params[:id])
end
def set_instance
resource = Account.by_domain_accounts.find_by(domain: params[:id])
resource ||= @domain_block
resource ||= @domain_allow
if resource
@instance = Instance.new(resource)
else
not_found
end
end
def filtered_instances def filtered_instances
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results InstanceFilter.new(filter_params).results
end end
def paginated_instances def paginated_instances
@ -58,11 +27,17 @@ module Admin
helper_method :paginated_instances helper_method :paginated_instances
def ordered_instances def ordered_instances
paginated_instances.map { |resource| Instance.new(resource) } paginated_instances.map { |account| Instance.new(account) }
end
def subscribeable_accounts
Account.with_followers.remote.where(domain: params[:by_domain])
end end
def filter_params def filter_params
params.permit(:limited, :by_domain) params.permit(
:domain_name
)
end end
end end
end end

View File

@ -1,52 +0,0 @@
# frozen_string_literal: true
module Admin
class PendingAccountsController < BaseController
before_action :set_accounts, only: :index
def index
@form = Form::AccountBatch.new
end
def batch
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_pending_accounts_path(current_params)
end
def approve_all
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save
redirect_to admin_pending_accounts_path(current_params)
end
def reject_all
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save
redirect_to admin_pending_accounts_path(current_params)
end
private
def set_accounts
@accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page])
end
def form_account_batch_params
params.require(:form_account_batch).permit(:action, account_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end
def current_params
params.slice(:page).permit(:page)
end
end
end

View File

@ -3,7 +3,6 @@
module Admin module Admin
class RelaysController < BaseController class RelaysController < BaseController
before_action :set_relay, except: [:index, :new, :create] before_action :set_relay, except: [:index, :new, :create]
before_action :require_signatures_enabled!, only: [:new, :create, :enable]
def index def index
authorize :relay, :update? authorize :relay, :update?
@ -12,7 +11,7 @@ module Admin
def new def new
authorize :relay, :update? authorize :relay, :update?
@relay = Relay.new @relay = Relay.new(inbox_url: Relay::PRESET_RELAY)
end end
def create def create
@ -55,9 +54,5 @@ module Admin
def resource_params def resource_params
params.require(:relay).permit(:inbox_url) params.require(:relay).permit(:inbox_url)
end end
def require_signatures_enabled!
redirect_to admin_relays_path, alert: I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode?
end
end end
end end

View File

@ -5,10 +5,10 @@ module Admin
before_action :set_report_note, only: [:destroy] before_action :set_report_note, only: [:destroy]
def create def create
authorize :report_note, :create? authorize ReportNote, :create?
@report_note = current_account.report_notes.new(resource_params) @report_note = current_account.report_notes.new(resource_params)
@report = @report_note.report @report = @report_note.report
if @report_note.save if @report_note.save
if params[:create_and_resolve] if params[:create_and_resolve]
@ -26,8 +26,9 @@ module Admin
redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg') redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg')
else else
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at) @report_notes = @report.notes.latest
@form = Form::StatusBatch.new @report_history = @report.history
@form = Form::StatusBatch.new
render template: 'admin/reports/show' render template: 'admin/reports/show'
end end

View File

@ -10,10 +10,6 @@ module Admin
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end

View File

@ -13,42 +13,75 @@ module Admin
authorize @report, :show? authorize @report, :show?
@report_note = @report.notes.new @report_note = @report.notes.new
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at) @report_notes = (@report.notes.latest + @report.history).sort_by(&:created_at)
@form = Form::StatusBatch.new @form = Form::StatusBatch.new
end end
def assign_to_self def update
authorize @report, :update? authorize @report, :update?
@report.update!(assigned_account_id: current_account.id) process_report
log_action :assigned_to_self, @report
redirect_to admin_report_path(@report)
end
def unassign if @report.action_taken?
authorize @report, :update? redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
@report.update!(assigned_account_id: nil) else
log_action :unassigned, @report redirect_to admin_report_path(@report)
redirect_to admin_report_path(@report) end
end
def reopen
authorize @report, :update?
@report.unresolve!
log_action :reopen, @report
redirect_to admin_report_path(@report)
end
def resolve
authorize @report, :update?
@report.resolve!(current_account)
log_action :resolve, @report
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
end end
private private
def process_report
case params[:outcome].to_s
when 'assign_to_self'
@report.update!(assigned_account_id: current_account.id)
log_action :assigned_to_self, @report
when 'unassign'
@report.update!(assigned_account_id: nil)
log_action :unassigned, @report
when 'reopen'
@report.unresolve!
log_action :reopen, @report
when 'resolve'
@report.resolve!(current_account)
log_action :resolve, @report
when 'disable'
@report.resolve!(current_account)
@report.target_account.user.disable!
log_action :resolve, @report
log_action :disable, @report.target_account.user
resolve_all_target_account_reports
when 'silence'
@report.resolve!(current_account)
@report.target_account.update!(silenced: true)
log_action :resolve, @report
log_action :silence, @report.target_account
resolve_all_target_account_reports
else
raise ActiveRecord::RecordNotFound
end
@report.reload
end
def resolve_all_target_account_reports
unresolved_reports_for_target_account.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
end
def unresolved_reports_for_target_account
Report.where(
target_account: @report.target_account
).unresolved
end
def filtered_reports def filtered_reports
ReportFilter.new(filter_params).results.order(id: :desc).includes(:account, :target_account) ReportFilter.new(filter_params).results.order(id: :desc).includes(
:account,
:target_account
)
end end
def filter_params def filter_params

View File

@ -10,5 +10,11 @@ module Admin
log_action :reset_password, @user log_action :reset_password, @user
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
private
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end end
end end

View File

@ -17,5 +17,11 @@ module Admin
log_action :demote, @user log_action :demote, @user
redirect_to admin_account_path(@user.account_id) redirect_to admin_account_path(@user.account_id)
end end
private
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end end
end end

View File

@ -2,29 +2,83 @@
module Admin module Admin
class SettingsController < BaseController class SettingsController < BaseController
ADMIN_SETTINGS = %w(
site_contact_username
site_contact_email
site_title
site_short_description
site_description
site_extended_description
site_terms
open_registrations
closed_registrations_message
open_deletion
timeline_preview
show_staff_badge
bootstrap_timeline_accounts
theme
thumbnail
hero
mascot
min_invite_role
activity_api_enabled
peers_api_enabled
show_known_fediverse_at_about_page
preview_sensitive_media
custom_css
).freeze
BOOLEAN_SETTINGS = %w(
open_registrations
open_deletion
timeline_preview
show_staff_badge
activity_api_enabled
peers_api_enabled
show_known_fediverse_at_about_page
preview_sensitive_media
).freeze
UPLOAD_SETTINGS = %w(
thumbnail
hero
mascot
).freeze
def edit def edit
authorize :settings, :show? authorize :settings, :show?
@admin_settings = Form::AdminSettings.new @admin_settings = Form::AdminSettings.new
end end
def update def update
authorize :settings, :update? authorize :settings, :update?
@admin_settings = Form::AdminSettings.new(settings_params) settings_params.each do |key, value|
if UPLOAD_SETTINGS.include?(key)
if @admin_settings.save upload = SiteUpload.where(var: key).first_or_initialize(var: key)
flash[:notice] = I18n.t('generic.changes_saved_msg') upload.update(file: value)
redirect_to edit_admin_settings_path else
else setting = Setting.where(var: key).first_or_initialize(var: key)
render :edit setting.update(value: value_for_update(key, value))
end
end end
flash[:notice] = I18n.t('generic.changes_saved_msg')
redirect_to edit_admin_settings_path
end end
private private
def settings_params def settings_params
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) params.require(:form_admin_settings).permit(ADMIN_SETTINGS)
end
def value_for_update(key, value)
if BOOLEAN_SETTINGS.include?(key)
value == '1'
else
value
end
end end
end end
end end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Admin
class SilencesController < BaseController
before_action :set_account
def create
authorize @account, :silence?
@account.update!(silenced: true)
log_action :silence, @account
redirect_to admin_accounts_path
end
def destroy
authorize @account, :unsilence?
@account.update!(silenced: false)
log_action :unsilence, @account
redirect_to admin_accounts_path
end
private
def set_account
@account = Account.find(params[:account_id])
end
end
end

View File

@ -22,15 +22,6 @@ module Admin
@form = Form::StatusBatch.new @form = Form::StatusBatch.new
end end
def show
authorize :status, :index?
@statuses = @account.statuses.where(id: params[:id])
authorize @statuses.first, :show?
@form = Form::StatusBatch.new
end
def create def create
authorize :status, :update? authorize :status, :update?

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
module Admin
class SuspensionsController < BaseController
before_action :set_account
def new
@suspension = Form::AdminSuspensionConfirmation.new(report_id: params[:report_id])
end
def create
authorize @account, :suspend?
@suspension = Form::AdminSuspensionConfirmation.new(suspension_params)
if suspension_params[:acct] == @account.acct
resolve_report! if suspension_params[:report_id].present?
perform_suspend!
mark_reports_resolved!
redirect_to admin_accounts_path
else
flash.now[:alert] = I18n.t('admin.suspensions.bad_acct_msg')
render :new
end
end
def destroy
authorize @account, :unsuspend?
@account.unsuspend!
log_action :unsuspend, @account
redirect_to admin_accounts_path
end
private
def set_account
@account = Account.find(params[:account_id])
end
def suspension_params
params.require(:form_admin_suspension_confirmation).permit(:acct, :report_id)
end
def resolve_report!
report = Report.find(suspension_params[:report_id])
report.resolve!(current_account)
log_action :resolve, report
end
def perform_suspend!
@account.suspend!
Admin::SuspensionWorker.perform_async(@account.id)
log_action :suspend, @account
end
def mark_reports_resolved!
Report.where(target_account: @account).unresolved.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
end
end
end

View File

@ -1,103 +0,0 @@
# frozen_string_literal: true
module Admin
class TagsController < BaseController
before_action :set_tag, except: [:index, :batch, :approve_all, :reject_all]
before_action :set_usage_by_domain, except: [:index, :batch, :approve_all, :reject_all]
before_action :set_counters, except: [:index, :batch, :approve_all, :reject_all]
def index
authorize :tag, :index?
@tags = filtered_tags.page(params[:page])
@form = Form::TagBatch.new
end
def batch
@form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
ensure
redirect_to admin_tags_path(filter_params)
end
def approve_all
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'approve').save
redirect_to admin_tags_path(filter_params)
end
def reject_all
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'reject').save
redirect_to admin_tags_path(filter_params)
end
def show
authorize @tag, :show?
end
def update
authorize @tag, :update?
if @tag.update(tag_params.merge(reviewed_at: Time.now.utc))
redirect_to admin_tag_path(@tag.id), notice: I18n.t('admin.tags.updated_msg')
else
render :show
end
end
private
def set_tag
@tag = Tag.find(params[:id])
end
def set_usage_by_domain
@usage_by_domain = @tag.statuses
.with_public_visibility
.excluding_silenced_accounts
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
.joins(:account)
.group('accounts.domain')
.reorder('statuses_count desc')
.pluck('accounts.domain, count(*) AS statuses_count')
end
def set_counters
@accounts_today = @tag.history.first[:accounts]
@accounts_week = Redis.current.pfcount(*current_week_days.map { |day| "activity:tags:#{@tag.id}:#{day}:accounts" })
end
def filtered_tags
TagFilter.new(filter_params).results
end
def filter_params
params.slice(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name).permit(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name)
end
def tag_params
params.require(:tag).permit(:name, :trendable, :usable, :listable)
end
def current_week_days
now = Time.now.utc.beginning_of_day.to_date
(Date.commercial(now.cwyear, now.cweek)..now).map do |date|
date.to_time(:utc).beginning_of_day.to_i
end
end
def form_tag_batch_params
params.require(:form_tag_batch).permit(:action, tag_ids: [])
end
def action_from_button
if params[:approve]
'approve'
elsif params[:reject]
'reject'
end
end
end
end

View File

@ -2,19 +2,18 @@
module Admin module Admin
class TwoFactorAuthenticationsController < BaseController class TwoFactorAuthenticationsController < BaseController
before_action :set_target_user before_action :set_user
def destroy def destroy
authorize @user, :disable_2fa? authorize @user, :disable_2fa?
@user.disable_two_factor! @user.disable_two_factor!
log_action :disable_2fa, @user log_action :disable_2fa, @user
UserMailer.two_factor_disabled(@user).deliver_later!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
private private
def set_target_user def set_user
@user = User.find(params[:user_id]) @user = User.find(params[:user_id])
end end
end end

View File

@ -1,58 +0,0 @@
# frozen_string_literal: true
module Admin
class WarningPresetsController < BaseController
before_action :set_warning_preset, except: [:index, :create]
def index
authorize :account_warning_preset, :index?
@warning_presets = AccountWarningPreset.all
@warning_preset = AccountWarningPreset.new
end
def create
authorize :account_warning_preset, :create?
@warning_preset = AccountWarningPreset.new(warning_preset_params)
if @warning_preset.save
redirect_to admin_warning_presets_path
else
@warning_presets = AccountWarningPreset.all
render :index
end
end
def edit
authorize @warning_preset, :update?
end
def update
authorize @warning_preset, :update?
if @warning_preset.update(warning_preset_params)
redirect_to admin_warning_presets_path
else
render :edit
end
end
def destroy
authorize @warning_preset, :destroy?
@warning_preset.destroy!
redirect_to admin_warning_presets_path
end
private
def set_warning_preset
@warning_preset = AccountWarningPreset.find(params[:id])
end
def warning_preset_params
params.require(:account_warning_preset).permit(:text)
end
end
end

View File

@ -7,15 +7,10 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders include RateLimitHeaders
skip_before_action :store_current_location skip_before_action :store_current_location
skip_before_action :require_functional! skip_before_action :check_user_permissions
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :set_cache_headers
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
skip_around_action :set_locale
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422 render json: { error: e.to_s }, status: 422
end end
@ -36,14 +31,6 @@ class Api::BaseController < ApplicationController
render json: { error: 'This action is not allowed' }, status: 403 render json: { error: 'This action is not allowed' }, status: 403
end end
rescue_from Mastodon::RaceConditionError do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from ActionController::ParameterMissing do |e|
render json: { error: e.to_s }, status: 400
end
def doorkeeper_unauthorized_render_options(error: nil) def doorkeeper_unauthorized_render_options(error: nil)
{ json: { error: (error.try(:description) || 'Not authorized') } } { json: { error: (error.try(:description) || 'Not authorized') } }
end end
@ -80,21 +67,13 @@ class Api::BaseController < ApplicationController
nil nil
end end
def require_authenticated_user!
render json: { error: 'This API requires an authenticated user' }, status: 401 unless current_user
end
def require_user! def require_user!
if !current_user if current_user && !current_user.disabled?
render json: { error: 'This method requires an authenticated user' }, status: 422
elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403
elsif !current_user.confirmed?
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403
else
set_user_activity set_user_activity
elsif current_user
render json: { error: 'Your login is currently disabled' }, status: 403
else
render json: { error: 'This method requires an authenticated user' }, status: 422
end end
end end
@ -105,12 +84,4 @@ class Api::BaseController < ApplicationController
def authorize_if_got_token!(*scopes) def authorize_if_got_token!(*scopes)
doorkeeper_authorize!(*scopes) if doorkeeper_token doorkeeper_authorize!(*scopes) if doorkeeper_token
end end
def set_cache_headers
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
end
def disallow_unauthenticated_api_access?
authorized_fetch_mode?
end
end end

View File

@ -1,21 +0,0 @@
# frozen_string_literal: true
class Api::ProofsController < Api::BaseController
include AccountOwnedConcern
before_action :set_provider
def index
render json: @account, serializer: @provider.serializer_class
end
private
def set_provider
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
end
def username_param
params[:username]
end
end

View File

@ -0,0 +1,73 @@
# frozen_string_literal: true
class Api::PushController < Api::BaseController
include SignatureVerification
def update
response, status = process_push_request
render plain: response, status: status
end
private
def process_push_request
case hub_mode
when 'subscribe'
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
when 'unsubscribe'
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
else
["Unknown mode: #{hub_mode}", 422]
end
end
def hub_mode
params['hub.mode']
end
def hub_topic
params['hub.topic']
end
def hub_callback
params['hub.callback']
end
def hub_lease_seconds
params['hub.lease_seconds']
end
def hub_secret
params['hub.secret']
end
def account_from_topic
if hub_topic.present? && local_domain? && account_feed_path?
Account.find_local(hub_topic_params[:username])
end
end
def hub_topic_params
@_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path)
end
def hub_topic_uri
@_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize
end
def local_domain?
TagManager.instance.web_domain?(hub_topic_domain)
end
def verified_domain
return signed_request_account.domain if signed_request_account
end
def hub_topic_domain
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
end
def account_feed_path?
hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom'
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
class Api::SalmonController < Api::BaseController
include SignatureVerification
before_action :set_account
respond_to :txt
def update
if verify_payload?
process_salmon
head 202
elsif payload.present?
render plain: signature_verification_failure_reason, status: 401
else
head 400
end
end
private
def set_account
@account = Account.find(params[:id])
end
def payload
@_payload ||= request.body.read
end
def verify_payload?
payload.present? && VerifySalmonService.new.call(payload)
end
def process_salmon
SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8'))
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
class Api::SubscriptionsController < Api::BaseController
before_action :set_account
respond_to :txt
def show
if subscription.valid?(params['hub.topic'])
@account.update(subscription_expires_at: future_expires)
render plain: encoded_challenge, status: 200
else
head 404
end
end
def update
if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE'])
ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8'))
end
head 200
end
private
def subscription
@_subscription ||= @account.subscription(
api_subscription_url(@account.id)
)
end
def body
@_body ||= request.body.read
end
def encoded_challenge
HTMLEntities.new.encode(params['hub.challenge'])
end
def future_expires
Time.now.utc + lease_seconds_or_default
end
def lease_seconds_or_default
(params['hub.lease_seconds'] || 1.day).to_i.seconds
end
def set_account
@account = Account.find(params[:id])
end
end

View File

@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
private private
def account_params def account_params
params.permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value]) params.permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value])
end end
def user_settings_params def user_settings_params

View File

@ -19,17 +19,13 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
end end
def load_accounts def load_accounts
return [] if hide_results? return [] if @account.user_hides_network? && current_account.id != @account.id
default_accounts.merge(paginated_follows).to_a default_accounts.merge(paginated_follows).to_a
end end
def hide_results?
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
end
def default_accounts def default_accounts
Account.includes(:active_relationships, :account_stat).references(:active_relationships) Account.includes(:active_relationships).references(:active_relationships)
end end
def paginated_follows def paginated_follows

View File

@ -19,17 +19,13 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end end
def load_accounts def load_accounts
return [] if hide_results? return [] if @account.user_hides_network? && current_account.id != @account.id
default_accounts.merge(paginated_follows).to_a default_accounts.merge(paginated_follows).to_a
end end
def hide_results?
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
end
def default_accounts def default_accounts
Account.includes(:passive_relationships, :account_stat).references(:passive_relationships) Account.includes(:passive_relationships).references(:passive_relationships)
end end
def paginated_follows def paginated_follows

View File

@ -1,19 +0,0 @@
# frozen_string_literal: true
class Api::V1::Accounts::IdentityProofsController < Api::BaseController
before_action :require_user!
before_action :set_account
respond_to :json
def index
@proofs = @account.identity_proofs.active
render json: @proofs, each_serializer: REST::IdentityProofSerializer
end
private
def set_account
@account = Account.find(params[:account_id])
end
end

View File

@ -16,11 +16,10 @@ class Api::V1::Accounts::SearchController < Api::BaseController
def account_search def account_search
AccountSearchService.new.call( AccountSearchService.new.call(
params[:q], params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT),
current_account, current_account,
limit: limit_param(DEFAULT_ACCOUNTS_LIMIT),
resolve: truthy_param?(:resolve), resolve: truthy_param?(:resolve),
following: truthy_param?(:following), following: truthy_param?(:following)
offset: params[:offset]
) )
end end
end end

View File

@ -1,10 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Accounts::StatusesController < Api::BaseController class Api::V1::Accounts::StatusesController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :set_account before_action :set_account
after_action :insert_pagination_headers
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
respond_to :json respond_to :json
@ -29,13 +28,15 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses = statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?
statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id)) statuses
end end
def permitted_account_statuses def permitted_account_statuses
@ -51,14 +52,12 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
# Also, Avoid getting slow by not narrowing down by `statuses.account_id`. # Also, Avoid getting slow by not narrowing down by `statuses.account_id`.
# When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used # When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used
# and the table will be joined by `Merge Semi Join`, so the query will be slow. # and the table will be joined by `Merge Semi Join`, so the query will be slow.
@account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account) Status.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account)
.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) .paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
.reorder(id: :desc).distinct(:id).pluck(:id) .reorder(id: :desc).distinct(:id).pluck(:id)
end end
def pinned_scope def pinned_scope
return Status.none if @account.blocking?(current_account)
@account.pinned_statuses @account.pinned_statuses
end end
@ -66,20 +65,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
Status.without_replies Status.without_replies
end end
def no_reblogs_scope
Status.without_reblogs
end
def hashtag_scope
tag = Tag.find_normalized(params[:tagged])
if tag
Status.tagged_with(tag.id)
else
Status.none
end
end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end end

View File

@ -1,18 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::AccountsController < Api::BaseController class Api::V1::AccountsController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute] before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow] before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute] before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock] before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
before_action :require_user!, except: [:show, :create] before_action :require_user!, except: [:show]
before_action :set_account, except: [:create] before_action :set_account
before_action :check_account_suspension, only: [:show] before_action :check_account_suspension, only: [:show]
before_action :check_enabled_registrations, only: [:create]
skip_before_action :require_authenticated_user!, only: :create
respond_to :json respond_to :json
@ -20,20 +16,10 @@ class Api::V1::AccountsController < Api::BaseController
render json: @account, serializer: REST::AccountSerializer render json: @account, serializer: REST::AccountSerializer
end end
def create
token = AppSignUpService.new.call(doorkeeper_token.application, account_params)
response = Doorkeeper::OAuth::TokenResponse.new(token)
headers.merge!(response.headers)
self.response_body = Oj.dump(response.body)
self.status = response.status
end
def follow def follow
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs)) FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end end
@ -76,16 +62,4 @@ class Api::V1::AccountsController < Api::BaseController
def check_account_suspension def check_account_suspension
gone if @account.suspended? gone if @account.suspended?
end end
def account_params
params.permit(:username, :email, :password, :agreement, :locale, :reason)
end
def check_enabled_registrations
forbidden if single_user_mode? || !allowed_registrations?
end
def allowed_registrations?
Setting.registrations_mode != 'none'
end
end end

View File

@ -1,32 +0,0 @@
# frozen_string_literal: true
class Api::V1::Admin::AccountActionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }
before_action :require_staff!
before_action :set_account
def create
account_action = Admin::AccountAction.new(resource_params)
account_action.target_account = @account
account_action.current_account = current_account
account_action.save!
render_empty
end
private
def set_account
@account = Account.find(params[:account_id])
end
def resource_params
params.permit(
:type,
:report_id,
:warning_preset_id,
:text,
:send_email_notification
)
end
end

View File

@ -1,128 +0,0 @@
# frozen_string_literal: true
class Api::V1::Admin::AccountsController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
before_action :require_staff!
before_action :set_accounts, only: :index
before_action :set_account, except: :index
before_action :require_local_account!, only: [:enable, :approve, :reject]
after_action :insert_pagination_headers, only: :index
FILTER_PARAMS = %i(
local
remote
by_domain
active
pending
disabled
silenced
suspended
username
display_name
email
ip
staff
).freeze
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze
def index
authorize :account, :index?
render json: @accounts, each_serializer: REST::Admin::AccountSerializer
end
def show
authorize @account, :show?
render json: @account, serializer: REST::Admin::AccountSerializer
end
def enable
authorize @account.user, :enable?
@account.user.enable!
log_action :enable, @account.user
render json: @account, serializer: REST::Admin::AccountSerializer
end
def approve
authorize @account.user, :approve?
@account.user.approve!
render json: @account, serializer: REST::Admin::AccountSerializer
end
def reject
authorize @account.user, :reject?
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
render json: @account, serializer: REST::Admin::AccountSerializer
end
def unsilence
authorize @account, :unsilence?
@account.unsilence!
log_action :unsilence, @account
render json: @account, serializer: REST::Admin::AccountSerializer
end
def unsuspend
authorize @account, :unsuspend?
@account.unsuspend!
log_action :unsuspend, @account
render json: @account, serializer: REST::Admin::AccountSerializer
end
private
def set_accounts
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_account
@account = Account.find(params[:id])
end
def filtered_accounts
AccountFilter.new(filter_params).results
end
def filter_params
params.permit(*FILTER_PARAMS)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
end
def records_continue?
@accounts.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
def require_local_account!
forbidden unless @account.local? && @account.user.present?
end
end

View File

@ -1,108 +0,0 @@
# frozen_string_literal: true
class Api::V1::Admin::ReportsController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
before_action :require_staff!
before_action :set_reports, only: :index
before_action :set_report, except: :index
after_action :insert_pagination_headers, only: :index
FILTER_PARAMS = %i(
resolved
account_id
target_account_id
).freeze
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze
def index
authorize :report, :index?
render json: @reports, each_serializer: REST::Admin::ReportSerializer
end
def show
authorize @report, :show?
render json: @report, serializer: REST::Admin::ReportSerializer
end
def assign_to_self
authorize @report, :update?
@report.update!(assigned_account_id: current_account.id)
log_action :assigned_to_self, @report
render json: @report, serializer: REST::Admin::ReportSerializer
end
def unassign
authorize @report, :update?
@report.update!(assigned_account_id: nil)
log_action :unassigned, @report
render json: @report, serializer: REST::Admin::ReportSerializer
end
def reopen
authorize @report, :update?
@report.unresolve!
log_action :reopen, @report
render json: @report, serializer: REST::Admin::ReportSerializer
end
def resolve
authorize @report, :update?
@report.resolve!(current_account)
log_action :resolve, @report
render json: @report, serializer: REST::Admin::ReportSerializer
end
private
def set_reports
@reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_report
@report = Report.find(params[:id])
end
def filtered_reports
ReportFilter.new(filter_params).results
end
def filter_params
params.permit(*FILTER_PARAMS)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
end
def pagination_max_id
@reports.last.id
end
def pagination_since_id
@reports.first.id
end
def records_continue?
@reports.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
end

View File

@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController
respond_to :json respond_to :json
def show def show
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key) render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer
end end
end end

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::AppsController < Api::BaseController class Api::V1::AppsController < Api::BaseController
skip_before_action :require_authenticated_user!
def create def create
@app = Doorkeeper::Application.create!(application_options) @app = Doorkeeper::Application.create!(application_options)
render json: @app, serializer: REST::ApplicationSerializer render json: @app, serializer: REST::ApplicationSerializer

View File

@ -19,7 +19,7 @@ class Api::V1::BlocksController < Api::BaseController
end end
def paginated_blocks def paginated_blocks
@paginated_blocks ||= Block.eager_load(target_account: :account_stat) @paginated_blocks ||= Block.eager_load(:target_account)
.where(account: current_account) .where(account: current_account)
.paginate_by_max_id( .paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),

View File

@ -3,10 +3,7 @@
class Api::V1::CustomEmojisController < Api::BaseController class Api::V1::CustomEmojisController < Api::BaseController
respond_to :json respond_to :json
skip_before_action :set_cache_headers
def index def index
expires_in 3.minutes, public: true render json: CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer
render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) }
end end
end end

View File

@ -1,30 +0,0 @@
# frozen_string_literal: true
class Api::V1::DirectoriesController < Api::BaseController
before_action :require_enabled!
before_action :set_accounts
def show
render json: @accounts, each_serializer: REST::AccountSerializer
end
private
def require_enabled!
return not_found unless Setting.profile_directory
end
def set_accounts
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
end
def accounts_scope
Account.discoverable.tap do |scope|
scope.merge!(Account.local) if truthy_param?(:local)
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active'
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new'
scope.merge!(Account.not_excluded_by_account(current_account)) if current_account
scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local)
end
end
end

View File

@ -27,7 +27,7 @@ class Api::V1::EndorsementsController < Api::BaseController
end end
def endorsed_accounts def endorsed_accounts
current_account.endorsed_accounts.includes(:account_stat) current_account.endorsed_accounts
end end
def insert_pagination_headers def insert_pagination_headers

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action :require_user!
before_action :set_most_used_tags, only: :index
respond_to :json
def index
render json: @most_used_tags, each_serializer: REST::TagSerializer
end
private
def set_most_used_tags
@most_used_tags = Tag.most_used(current_account).where.not(id: current_account.featured_tags).limit(10)
end
end

View File

@ -1,40 +0,0 @@
# frozen_string_literal: true
class Api::V1::FeaturedTagsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
before_action :require_user!
before_action :set_featured_tags, only: :index
before_action :set_featured_tag, except: [:index, :create]
def index
render json: @featured_tags, each_serializer: REST::FeaturedTagSerializer
end
def create
@featured_tag = current_account.featured_tags.new(featured_tag_params)
@featured_tag.reset_data
@featured_tag.save!
render json: @featured_tag, serializer: REST::FeaturedTagSerializer
end
def destroy
@featured_tag.destroy!
render_empty
end
private
def set_featured_tag
@featured_tag = current_account.featured_tags.find(params[:id])
end
def set_featured_tags
@featured_tags = current_account.featured_tags.order(statuses_count: :desc)
end
def featured_tag_params
params.permit(:name)
end
end

View File

@ -14,12 +14,12 @@ class Api::V1::FollowRequestsController < Api::BaseController
def authorize def authorize
AuthorizeFollowService.new.call(account, current_account) AuthorizeFollowService.new.call(account, current_account)
NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account)) NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account))
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships render_empty
end end
def reject def reject
RejectFollowService.new.call(account, current_account) RejectFollowService.new.call(account, current_account)
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships render_empty
end end
private private
@ -28,16 +28,12 @@ class Api::V1::FollowRequestsController < Api::BaseController
Account.find(params[:id]) Account.find(params[:id])
end end
def relationships(**options)
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options)
end
def load_accounts def load_accounts
default_accounts.merge(paginated_follow_requests).to_a default_accounts.merge(paginated_follow_requests).to_a
end end
def default_accounts def default_accounts
Account.includes(:follow_requests, :account_stat).references(:follow_requests) Account.includes(:follow_requests).references(:follow_requests)
end end
def paginated_follow_requests def paginated_follow_requests

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
class Api::V1::FollowsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }
before_action :require_user!
respond_to :json
def create
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
if @account.nil?
username, domain = target_uri.split('@')
@account = Account.find_remote!(username, domain)
end
render json: @account, serializer: REST::AccountSerializer
end
private
def target_uri
follow_params[:uri].strip.gsub(/\A@/, '')
end
def follow_params
params.permit(:uri)
end
end

View File

@ -3,14 +3,10 @@
class Api::V1::Instances::ActivityController < Api::BaseController class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
respond_to :json respond_to :json
def show def show
expires_in 1.day, public: true render_cached_json('api:v1:instances:activity:show', expires_in: 1.day) { activity }
render_with_cache json: :activity, expires_in: 1.day
end end
private private
@ -35,6 +31,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
end end
def require_enabled_api! def require_enabled_api!
head 404 unless Setting.activity_api_enabled && !whitelist_mode? head 404 unless Setting.activity_api_enabled
end end
end end

View File

@ -3,19 +3,15 @@
class Api::V1::Instances::PeersController < Api::BaseController class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
respond_to :json respond_to :json
def index def index
expires_in 1.day, public: true render_cached_json('api:v1:instances:peers:index', expires_in: 1.day) { Account.remote.domains }
render_with_cache(expires_in: 1.day) { Account.remote.domains }
end end
private private
def require_enabled_api! def require_enabled_api!
head 404 unless Setting.peers_api_enabled && !whitelist_mode? head 404 unless Setting.peers_api_enabled
end end
end end

View File

@ -3,11 +3,9 @@
class Api::V1::InstancesController < Api::BaseController class Api::V1::InstancesController < Api::BaseController
respond_to :json respond_to :json
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
def show def show
expires_in 3.minutes, public: true render_cached_json('api:v1:instances', expires_in: 5.minutes) do
render_with_cache json: {}, serializer: REST::InstanceSerializer, root: 'instance' ActiveModelSerializers::SerializableResource.new({}, serializer: REST::InstanceSerializer)
end
end end
end end

View File

@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController
def load_accounts def load_accounts
if unlimited? if unlimited?
@list.accounts.includes(:account_stat).all @list.accounts.all
else else
@list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end end
end end

View File

@ -1,44 +0,0 @@
# frozen_string_literal: true
class Api::V1::MarkersController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:index]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, except: [:index]
before_action :require_user!
def index
@markers = current_user.markers.where(timeline: Array(params[:timeline])).each_with_object({}) { |marker, h| h[marker.timeline] = marker }
render json: serialize_map(@markers)
end
def create
Marker.transaction do
@markers = {}
resource_params.each_pair do |timeline, timeline_params|
@markers[timeline] = current_user.markers.find_or_initialize_by(timeline: timeline)
@markers[timeline].update!(timeline_params)
end
end
render json: serialize_map(@markers)
rescue ActiveRecord::StaleObjectError
render json: { error: 'Conflict during update, please try again' }, status: 409
end
private
def serialize_map(map)
serialized = {}
map.each_pair do |key, value|
serialized[key] = ActiveModelSerializers::SerializableResource.new(value, serializer: REST::MarkerSerializer).as_json
end
Oj.dump(serialized)
end
def resource_params
params.slice(*Marker::TIMELINES).permit(*Marker::TIMELINES.map { |timeline| { timeline.to_sym => [:last_read_id] } })
end
end

View File

@ -44,7 +44,7 @@ class Api::V1::NotificationsController < Api::BaseController
end end
def browserable_account_notifications def browserable_account_notifications
current_account.notifications.browserable(exclude_types, from_account) current_account.notifications.browserable(exclude_types)
end end
def target_statuses_from_notifications def target_statuses_from_notifications
@ -81,10 +81,6 @@ class Api::V1::NotificationsController < Api::BaseController
val val
end end
def from_account
params[:account_id]
end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params) params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params)
end end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
class Api::V1::Polls::VotesController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user!
before_action :set_poll
respond_to :json
def create
VoteService.new.call(current_account, @poll, vote_params[:choices])
render json: @poll, serializer: REST::PollSerializer
end
private
def set_poll
@poll = Poll.attached.find(params[:poll_id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
end
def vote_params
params.permit(choices: [])
end
end

View File

@ -1,28 +0,0 @@
# frozen_string_literal: true
class Api::V1::PollsController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, only: :show
before_action :set_poll
before_action :refresh_poll
respond_to :json
def show
render json: @poll, serializer: REST::PollSerializer, include_results: true
end
private
def set_poll
@poll = Poll.attached.find(params[:id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
end
def refresh_poll
ActivityPub::FetchRemotePollService.new.call(@poll, current_account) if user_signed_in? && @poll.possibly_stale?
end
end

View File

@ -1,12 +0,0 @@
# frozen_string_literal: true
class Api::V1::PreferencesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
before_action :require_user!
respond_to :json
def index
render json: current_account, serializer: REST::PreferencesSerializer
end
end

View File

@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def data_params def data_params
return {} if params[:data].blank? return {} if params[:data].blank?
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll]) params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
end end
end end

View File

@ -21,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController
private private
def reported_status_ids def reported_status_ids
reported_account.statuses.with_discarded.find(status_ids).pluck(:id) reported_account.statuses.find(status_ids).pluck(:id)
end end
def status_ids def status_ids

View File

@ -1,77 +0,0 @@
# frozen_string_literal: true
class Api::V1::ScheduledStatusesController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
before_action :set_statuses, only: :index
before_action :set_status, except: :index
after_action :insert_pagination_headers, only: :index
def index
render json: @statuses, each_serializer: REST::ScheduledStatusSerializer
end
def show
render json: @status, serializer: REST::ScheduledStatusSerializer
end
def update
@status.update!(scheduled_status_params)
render json: @status, serializer: REST::ScheduledStatusSerializer
end
def destroy
@status.destroy!
render_empty
end
private
def set_statuses
@statuses = current_account.scheduled_statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_status
@status = current_account.scheduled_statuses.find(params[:id])
end
def scheduled_status_params
params.permit(:scheduled_at)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @statuses.empty?
api_v1_scheduled_statuses_url pagination_params(min_id: pagination_since_id)
end
end
def records_continue?
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController
include Authorization
RESULTS_LIMIT = 5
before_action -> { doorkeeper_authorize! :read, :'read:search' }
before_action :require_user!
respond_to :json
def index
@search = Search.new(search)
render json: @search, serializer: REST::SearchSerializer
end
private
def search
search_results.tap do |search|
search[:statuses].keep_if do |status|
begin
authorize status, :show?
rescue Mastodon::NotPermittedError
false
end
end
end
end
def search_results
SearchService.new.call(
params[:q],
RESULTS_LIMIT,
truthy_param?(:resolve),
current_account
)
end
end

View File

@ -22,7 +22,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
def default_accounts def default_accounts
Account Account
.includes(:favourites, :account_stat) .includes(:favourites)
.references(:favourites) .references(:favourites)
.where(favourites: { status_id: @status.id }) .where(favourites: { status_id: @status.id })
end end

View File

@ -21,11 +21,11 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
end end
def default_accounts def default_accounts
Account.includes(:statuses, :account_stat).references(:statuses) Account.includes(:statuses).references(:statuses)
end end
def paginated_statuses def paginated_statuses
Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted]).paginate_by_max_id( Status.where(reblog_of_id: @status.id).paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id], params[:max_id],
params[:since_id] params[:since_id]

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