Merge tag 'v3.0.0' into hometown-dev
This commit is contained in:
		| @ -3,7 +3,7 @@ version: 2 | ||||
| aliases: | ||||
|   - &defaults | ||||
|     docker: | ||||
|       - image: circleci/ruby:2.6.0-stretch-node | ||||
|       - image: circleci/ruby:2.6-stretch-node | ||||
|         environment: &ruby_environment | ||||
|           BUNDLE_APP_CONFIG: ./.bundle/ | ||||
|           DB_HOST: localhost | ||||
| @ -105,14 +105,14 @@ jobs: | ||||
|   install-ruby2.5: | ||||
|     <<: *defaults | ||||
|     docker: | ||||
|       - image: circleci/ruby:2.5.3-stretch-node | ||||
|       - image: circleci/ruby:2.5-stretch-node | ||||
|         environment: *ruby_environment | ||||
|     <<: *install_ruby_dependencies | ||||
|  | ||||
|   install-ruby2.4: | ||||
|     <<: *defaults | ||||
|     docker: | ||||
|       - image: circleci/ruby:2.4.5-stretch-node | ||||
|       - image: circleci/ruby:2.4-stretch-node | ||||
|         environment: *ruby_environment | ||||
|     <<: *install_ruby_dependencies | ||||
|  | ||||
| @ -131,40 +131,40 @@ jobs: | ||||
|   test-ruby2.6: | ||||
|     <<: *defaults | ||||
|     docker: | ||||
|       - image: circleci/ruby:2.6.0-stretch-node | ||||
|       - image: circleci/ruby:2.6-stretch-node | ||||
|         environment: *ruby_environment | ||||
|       - image: circleci/postgres:10.6-alpine | ||||
|         environment: | ||||
|           POSTGRES_USER: root | ||||
|       - image: circleci/redis:5.0.3-alpine3.8 | ||||
|       - image: circleci/redis:5-alpine | ||||
|     <<: *test_steps | ||||
|  | ||||
|   test-ruby2.5: | ||||
|     <<: *defaults | ||||
|     docker: | ||||
|       - image: circleci/ruby:2.5.3-stretch-node | ||||
|       - image: circleci/ruby:2.5-stretch-node | ||||
|         environment: *ruby_environment | ||||
|       - image: circleci/postgres:10.6-alpine | ||||
|         environment: | ||||
|           POSTGRES_USER: root | ||||
|       - image: circleci/redis:4.0.12-alpine | ||||
|       - image: circleci/redis:5-alpine | ||||
|     <<: *test_steps | ||||
|  | ||||
|   test-ruby2.4: | ||||
|     <<: *defaults | ||||
|     docker: | ||||
|       - image: circleci/ruby:2.4.5-stretch-node | ||||
|       - image: circleci/ruby:2.4-stretch-node | ||||
|         environment: *ruby_environment | ||||
|       - image: circleci/postgres:10.6-alpine | ||||
|         environment: | ||||
|           POSTGRES_USER: root | ||||
|       - image: circleci/redis:4.0.12-alpine | ||||
|       - image: circleci/redis:5-alpine | ||||
|     <<: *test_steps | ||||
|  | ||||
|   test-webui: | ||||
|     <<: *defaults | ||||
|     docker: | ||||
|       - image: circleci/node:8.15.0-stretch | ||||
|       - image: circleci/node:12.9-stretch | ||||
|     steps: | ||||
|       - *attach_workspace | ||||
|       - run: ./bin/retry yarn test:jest | ||||
| @ -173,9 +173,11 @@ jobs: | ||||
|     <<: *defaults | ||||
|     steps: | ||||
|       - *attach_workspace | ||||
|       - *install_system_dependencies | ||||
|       - run: bundle exec i18n-tasks check-normalized | ||||
|       - run: bundle exec i18n-tasks unused -l en | ||||
|       - run: bundle exec i18n-tasks check-consistent-interpolations | ||||
|       - run: bundle exec rake repo:check_locales_files | ||||
|  | ||||
| workflows: | ||||
|   version: 2 | ||||
|  | ||||
							
								
								
									
										56
									
								
								.env.nanobox
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								.env.nanobox
									
									
									
									
									
								
							| @ -11,24 +11,14 @@ DB_NAME=gonano | ||||
| DB_PASS=$DATA_DB_PASS | ||||
| 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 | ||||
| ES_ENABLED=true | ||||
| ES_HOST=$DATA_ELASTIC_HOST | ||||
| ES_PORT=9200 | ||||
|  | ||||
| # 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 | ||||
| BIND=0.0.0.0 | ||||
|  | ||||
| # Federation | ||||
| # Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation. | ||||
| @ -84,6 +74,7 @@ SMTP_PORT=587 | ||||
| SMTP_LOGIN=$SMTP_LOGIN | ||||
| SMTP_PASSWORD=$SMTP_PASSWORD | ||||
| SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io | ||||
| #SMTP_REPLY_TO= | ||||
| #SMTP_DOMAIN= # defaults to LOCAL_DOMAIN | ||||
| #SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail | ||||
| #SMTP_AUTH_METHOD=plain | ||||
| @ -97,9 +88,17 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io | ||||
| # PAPERCLIP_ROOT_URL=/system | ||||
|  | ||||
| # 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 | ||||
|  | ||||
| # 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_BUCKET= | ||||
| # AWS_ACCESS_KEY_ID= | ||||
| @ -109,6 +108,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io | ||||
| # S3_HOSTNAME=192.168.1.123:9000 | ||||
|  | ||||
| # 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_BUCKET= | ||||
| # AWS_ACCESS_KEY_ID= | ||||
| @ -119,12 +120,30 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io | ||||
| # S3_ENDPOINT= | ||||
| # 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) | ||||
| # The attachment host must allow cross origin request - see the description | ||||
| # above. | ||||
| # SWIFT_ENABLED=true | ||||
| # SWIFT_USERNAME= | ||||
| # For Keystone V3, the value for SWIFT_TENANT should be the project name | ||||
| # SWIFT_TENANT= | ||||
| # 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 | ||||
| # issues with token rate-limiting during high load. | ||||
| # SWIFT_AUTH_URL= | ||||
| @ -171,8 +190,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io | ||||
| # The pam environment variable "email" is provided by: | ||||
| # https://github.com/devkral/pam_email_extractor | ||||
| # PAM_ENABLED=true | ||||
| # Fallback Suffix for email address generation (nil by default) | ||||
| # PAM_DEFAULT_SUFFIX=pam | ||||
| # Fallback email domain for email address generation (LOCAL_DOMAIN by default) | ||||
| # PAM_EMAIL_DOMAIN=example.com | ||||
| # Name of the pam service (pam "auth" section is evaluated) | ||||
| # 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) | ||||
| @ -220,7 +239,14 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io | ||||
| # SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true | ||||
| # 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_FULL_NAME="urn:oid:2.5.4.42" | ||||
| # SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241" | ||||
| # 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_ATTRIBUTES_STATEMENTS_VERIFIED= | ||||
| # 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 | ||||
|  | ||||
| @ -69,6 +69,7 @@ SMTP_PORT=587 | ||||
| SMTP_LOGIN= | ||||
| SMTP_PASSWORD= | ||||
| SMTP_FROM_ADDRESS=notifications@example.com | ||||
| #SMTP_REPLY_TO= | ||||
| #SMTP_DOMAIN= # defaults to LOCAL_DOMAIN | ||||
| #SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail | ||||
| #SMTP_AUTH_METHOD=plain | ||||
| @ -114,6 +115,20 @@ SMTP_FROM_ADDRESS=notifications@example.com | ||||
| # S3_ENDPOINT= | ||||
| # 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) | ||||
| # The attachment host must allow cross origin request - see the description | ||||
| # above. | ||||
| @ -163,7 +178,7 @@ STREAMING_CLUSTER_NUM=1 | ||||
| # LDAP_BIND_DN= | ||||
| # LDAP_PASSWORD= | ||||
| # LDAP_UID=cn | ||||
| # LDAP_SEARCH_FILTER="%{uid}=%{email}" | ||||
| # LDAP_SEARCH_FILTER=%{uid}=%{email} | ||||
|  | ||||
| # PAM authentication (optional) | ||||
| # PAM authentication uses for the email generation the "email" pam variable | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| 2.6.1 | ||||
| 2.6.5 | ||||
|  | ||||
							
								
								
									
										196
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										196
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @ -3,6 +3,202 @@ Changelog | ||||
|  | ||||
| All notable changes to this project will be documented in this file. | ||||
|  | ||||
| ## [3.0.0] - 2019-10-03 | ||||
| ### Added | ||||
|  | ||||
| - Add "not available" label to unloaded media attachments in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11715), [Gargron](https://github.com/tootsuite/mastodon/pull/11745)) | ||||
| - **Add profile directory to web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11688), [mayaeh](https://github.com/tootsuite/mastodon/pull/11872)) | ||||
|   - Add profile directory opt-in federation | ||||
|   - Add profile directory REST API | ||||
| - Add special alert for throttled requests in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11677)) | ||||
| - Add confirmation modal when logging out from the web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11671)) | ||||
| - **Add audio player in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11644), [Gargron](https://github.com/tootsuite/mastodon/pull/11652), [Gargron](https://github.com/tootsuite/mastodon/pull/11654), [ThibG](https://github.com/tootsuite/mastodon/pull/11629), [Gargron](https://github.com/tootsuite/mastodon/pull/12056)) | ||||
| - **Add autosuggestions for hashtags in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11422), [ThibG](https://github.com/tootsuite/mastodon/pull/11632), [Gargron](https://github.com/tootsuite/mastodon/pull/11764), [Gargron](https://github.com/tootsuite/mastodon/pull/11588), [Gargron](https://github.com/tootsuite/mastodon/pull/11442)) | ||||
| - **Add media editing modal with OCR tool in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11563), [Gargron](https://github.com/tootsuite/mastodon/pull/11566), [ThibG](https://github.com/tootsuite/mastodon/pull/11575), [ThibG](https://github.com/tootsuite/mastodon/pull/11576), [Gargron](https://github.com/tootsuite/mastodon/pull/11577), [Gargron](https://github.com/tootsuite/mastodon/pull/11573), [Gargron](https://github.com/tootsuite/mastodon/pull/11571)) | ||||
| - Add indicator of unread notifications to window title when web UI is out of focus ([Gargron](https://github.com/tootsuite/mastodon/pull/11560), [Gargron](https://github.com/tootsuite/mastodon/pull/11572)) | ||||
| - Add indicator for which options you voted for in a poll in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11195)) | ||||
| - **Add search results pagination to web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11409), [ThibG](https://github.com/tootsuite/mastodon/pull/11447)) | ||||
| - **Add option to disable real-time updates in web UI ("slow mode")** ([Gargron](https://github.com/tootsuite/mastodon/pull/9984), [ykzts](https://github.com/tootsuite/mastodon/pull/11880), [ThibG](https://github.com/tootsuite/mastodon/pull/11883), [Gargron](https://github.com/tootsuite/mastodon/pull/11898), [ThibG](https://github.com/tootsuite/mastodon/pull/11859)) | ||||
| - Add option to disable blurhash previews in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11188)) | ||||
| - Add native smooth scrolling when supported in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11207)) | ||||
| - Add scrolling to the search bar on focus in web UI ([Kjwon15](https://github.com/tootsuite/mastodon/pull/12032)) | ||||
| - Add refresh button to list of rebloggers/favouriters in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12031)) | ||||
| - Add error description and button to copy stack trace to web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12033)) | ||||
| - Add search and sort functions to hashtag admin UI ([mayaeh](https://github.com/tootsuite/mastodon/pull/11829), [Gargron](https://github.com/tootsuite/mastodon/pull/11897), [mayaeh](https://github.com/tootsuite/mastodon/pull/11875)) | ||||
| - Add setting for default search engine indexing in admin UI ([brortao](https://github.com/tootsuite/mastodon/pull/11804)) | ||||
| - Add account bio to account view in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11473)) | ||||
| - **Add option to include reported statuses in warning e-mail from admin UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11639), [Gargron](https://github.com/tootsuite/mastodon/pull/11812), [Gargron](https://github.com/tootsuite/mastodon/pull/11741), [Gargron](https://github.com/tootsuite/mastodon/pull/11698), [mayaeh](https://github.com/tootsuite/mastodon/pull/11765)) | ||||
| - Add number of pending accounts and pending hashtags to dashboard in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11514)) | ||||
| - **Add account migration UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11846), [noellabo](https://github.com/tootsuite/mastodon/pull/11905), [noellabo](https://github.com/tootsuite/mastodon/pull/11907), [noellabo](https://github.com/tootsuite/mastodon/pull/11906), [noellabo](https://github.com/tootsuite/mastodon/pull/11902)) | ||||
| - **Add table of contents to about page** ([Gargron](https://github.com/tootsuite/mastodon/pull/11885), [ykzts](https://github.com/tootsuite/mastodon/pull/11941), [ykzts](https://github.com/tootsuite/mastodon/pull/11895), [Kjwon15](https://github.com/tootsuite/mastodon/pull/11916)) | ||||
| - **Add password challenge to 2FA settings, e-mail notifications** ([Gargron](https://github.com/tootsuite/mastodon/pull/11878)) | ||||
| - **Add optional public list of domain blocks with comments** ([ThibG](https://github.com/tootsuite/mastodon/pull/11298), [ThibG](https://github.com/tootsuite/mastodon/pull/11515), [Gargron](https://github.com/tootsuite/mastodon/pull/11908)) | ||||
| - Add an RSS feed for featured hashtags ([noellabo](https://github.com/tootsuite/mastodon/pull/10502)) | ||||
| - Add explanations to featured hashtags UI and profile ([Gargron](https://github.com/tootsuite/mastodon/pull/11586)) | ||||
| - **Add hashtag trends with admin and user settings** ([Gargron](https://github.com/tootsuite/mastodon/pull/11490), [Gargron](https://github.com/tootsuite/mastodon/pull/11502), [Gargron](https://github.com/tootsuite/mastodon/pull/11641), [Gargron](https://github.com/tootsuite/mastodon/pull/11594), [Gargron](https://github.com/tootsuite/mastodon/pull/11517), [mayaeh](https://github.com/tootsuite/mastodon/pull/11845), [Gargron](https://github.com/tootsuite/mastodon/pull/11774), [Gargron](https://github.com/tootsuite/mastodon/pull/11712), [Gargron](https://github.com/tootsuite/mastodon/pull/11791), [Gargron](https://github.com/tootsuite/mastodon/pull/11743), [Gargron](https://github.com/tootsuite/mastodon/pull/11740), [Gargron](https://github.com/tootsuite/mastodon/pull/11714), [ThibG](https://github.com/tootsuite/mastodon/pull/11631), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/11569), [Gargron](https://github.com/tootsuite/mastodon/pull/11524), [Gargron](https://github.com/tootsuite/mastodon/pull/11513)) | ||||
|   - Add hashtag usage breakdown to admin UI | ||||
|   - Add batch actions for hashtags to admin UI | ||||
|   - Add trends to web UI | ||||
|   - Add trends to public pages | ||||
|   - Add user preference to hide trends | ||||
|   - Add admin setting to disable trends | ||||
| - **Add categories for custom emojis** ([Gargron](https://github.com/tootsuite/mastodon/pull/11196), [Gargron](https://github.com/tootsuite/mastodon/pull/11793), [Gargron](https://github.com/tootsuite/mastodon/pull/11920), [highemerly](https://github.com/tootsuite/mastodon/pull/11876)) | ||||
|   - Add custom emoji categories to emoji picker in web UI | ||||
|   - Add `category` to custom emojis in REST API | ||||
|   - Add batch actions for custom emojis in admin UI | ||||
| - Add max image dimensions to error message ([raboof](https://github.com/tootsuite/mastodon/pull/11552)) | ||||
| - Add aac, m4a, 3gp, amr, wma to allowed audio formats ([Gargron](https://github.com/tootsuite/mastodon/pull/11342), [umonaca](https://github.com/tootsuite/mastodon/pull/11687)) | ||||
| - **Add search syntax for operators and phrases** ([Gargron](https://github.com/tootsuite/mastodon/pull/11411)) | ||||
| - **Add REST API for managing featured hashtags** ([noellabo](https://github.com/tootsuite/mastodon/pull/11778)) | ||||
| - **Add REST API for managing timeline read markers** ([Gargron](https://github.com/tootsuite/mastodon/pull/11762)) | ||||
| - Add `exclude_unreviewed` param to `GET /api/v2/search` REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/11977)) | ||||
| - Add `reason` param to `POST /api/v1/accounts` REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/12064)) | ||||
| - **Add ActivityPub secure mode** ([Gargron](https://github.com/tootsuite/mastodon/pull/11269), [ThibG](https://github.com/tootsuite/mastodon/pull/11332), [ThibG](https://github.com/tootsuite/mastodon/pull/11295)) | ||||
| - Add HTTP signatures to all outgoing ActivityPub GET requests ([Gargron](https://github.com/tootsuite/mastodon/pull/11284), [ThibG](https://github.com/tootsuite/mastodon/pull/11300)) | ||||
| - Add support for ActivityPub Audio activities ([ThibG](https://github.com/tootsuite/mastodon/pull/11189)) | ||||
| - Add ActivityPub actor representing the entire server ([ThibG](https://github.com/tootsuite/mastodon/pull/11321), [rtucker](https://github.com/tootsuite/mastodon/pull/11400), [ThibG](https://github.com/tootsuite/mastodon/pull/11561), [Gargron](https://github.com/tootsuite/mastodon/pull/11798)) | ||||
| - **Add whitelist mode** ([Gargron](https://github.com/tootsuite/mastodon/pull/11291), [mayaeh](https://github.com/tootsuite/mastodon/pull/11634)) | ||||
| - Add config of multipart threshold for S3 ([ykzts](https://github.com/tootsuite/mastodon/pull/11924), [ykzts](https://github.com/tootsuite/mastodon/pull/11944)) | ||||
| - Add health check endpoint for web ([ykzts](https://github.com/tootsuite/mastodon/pull/11770), [ykzts](https://github.com/tootsuite/mastodon/pull/11947)) | ||||
| - Add HTTP signature keyId to request log ([Gargron](https://github.com/tootsuite/mastodon/pull/11591)) | ||||
| - Add `SMTP_REPLY_TO` environment variable ([hugogameiro](https://github.com/tootsuite/mastodon/pull/11718)) | ||||
| - Add `tootctl preview_cards remove` command ([mayaeh](https://github.com/tootsuite/mastodon/pull/11320)) | ||||
| - Add `tootctl media refresh` command ([Gargron](https://github.com/tootsuite/mastodon/pull/11775)) | ||||
| - Add `tootctl cache recount` command ([Gargron](https://github.com/tootsuite/mastodon/pull/11597)) | ||||
| - Add option to exclude suspended domains from `tootctl domains crawl` ([dariusk](https://github.com/tootsuite/mastodon/pull/11454)) | ||||
| - Add parallelization to `tootctl search deploy` ([noellabo](https://github.com/tootsuite/mastodon/pull/12051)) | ||||
| - Add soft delete for statuses for instant deletes through API ([Gargron](https://github.com/tootsuite/mastodon/pull/11623), [Gargron](https://github.com/tootsuite/mastodon/pull/11648)) | ||||
| - Add rails-level JSON caching ([Gargron](https://github.com/tootsuite/mastodon/pull/11333), [Gargron](https://github.com/tootsuite/mastodon/pull/11271)) | ||||
| - **Add request pool to improve delivery performance** ([Gargron](https://github.com/tootsuite/mastodon/pull/10353), [ykzts](https://github.com/tootsuite/mastodon/pull/11756)) | ||||
| - Add concurrent connection attempts to resolved IP addresses ([ThibG](https://github.com/tootsuite/mastodon/pull/11757)) | ||||
| - Add index for remember_token to improve login performance ([abcang](https://github.com/tootsuite/mastodon/pull/11881)) | ||||
| - **Add more accurate hashtag search** ([Gargron](https://github.com/tootsuite/mastodon/pull/11579), [Gargron](https://github.com/tootsuite/mastodon/pull/11427), [Gargron](https://github.com/tootsuite/mastodon/pull/11448)) | ||||
| - **Add more accurate account search** ([Gargron](https://github.com/tootsuite/mastodon/pull/11537), [Gargron](https://github.com/tootsuite/mastodon/pull/11580)) | ||||
| - **Add a spam check** ([Gargron](https://github.com/tootsuite/mastodon/pull/11217), [Gargron](https://github.com/tootsuite/mastodon/pull/11806), [ThibG](https://github.com/tootsuite/mastodon/pull/11296)) | ||||
| - Add new languages ([Gargron](https://github.com/tootsuite/mastodon/pull/12062)) | ||||
|   - Breton | ||||
|   - Spanish (Argentina) | ||||
|   - Estonian | ||||
|   - Macedonian | ||||
|   - New Norwegian | ||||
| - Add NodeInfo endpoint ([Gargron](https://github.com/tootsuite/mastodon/pull/12002), [Gargron](https://github.com/tootsuite/mastodon/pull/12058)) | ||||
|  | ||||
| ### Changed | ||||
|  | ||||
| - **Change conversations UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/11896)) | ||||
| - Change dashboard to short number notation ([noellabo](https://github.com/tootsuite/mastodon/pull/11847), [noellabo](https://github.com/tootsuite/mastodon/pull/11911)) | ||||
| - Change REST API `GET /api/v1/timelines/public` to require authentication when public preview is off ([ThibG](https://github.com/tootsuite/mastodon/pull/11802)) | ||||
| - Change REST API `POST /api/v1/follow_requests/:id/(approve|reject)` to return relationship ([ThibG](https://github.com/tootsuite/mastodon/pull/11800)) | ||||
| - Change rate limit for media proxy ([ykzts](https://github.com/tootsuite/mastodon/pull/11814)) | ||||
| - Change unlisted custom emoji to not appear in autosuggestions ([Gargron](https://github.com/tootsuite/mastodon/pull/11818)) | ||||
| - Change max length of media descriptions from 420 to 1500 characters ([Gargron](https://github.com/tootsuite/mastodon/pull/11819), [ThibG](https://github.com/tootsuite/mastodon/pull/11836)) | ||||
| - **Change deletes to preserve soft-deleted statuses in unresolved reports** ([Gargron](https://github.com/tootsuite/mastodon/pull/11805)) | ||||
| - **Change tootctl to use inline parallelization instead of Sidekiq** ([Gargron](https://github.com/tootsuite/mastodon/pull/11776)) | ||||
| - **Change account deletion page to have better explanations** ([Gargron](https://github.com/tootsuite/mastodon/pull/11753), [Gargron](https://github.com/tootsuite/mastodon/pull/11763)) | ||||
| - Change hashtag component in web UI to show numbers for 2 last days ([Gargron](https://github.com/tootsuite/mastodon/pull/11742), [Gargron](https://github.com/tootsuite/mastodon/pull/11755), [Gargron](https://github.com/tootsuite/mastodon/pull/11754)) | ||||
| - Change OpenGraph description on sign-up page to reflect invite ([Gargron](https://github.com/tootsuite/mastodon/pull/11744)) | ||||
| - Change layout of public profile directory to be the same as in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11705)) | ||||
| - Change detailed status child ordering to sort self-replies on top ([ThibG](https://github.com/tootsuite/mastodon/pull/11686)) | ||||
| - Change window resize handler to switch to/from mobile layout as soon as needed ([ThibG](https://github.com/tootsuite/mastodon/pull/11656)) | ||||
| - Change icon button styles to make hover/focus states more obvious ([ThibG](https://github.com/tootsuite/mastodon/pull/11474)) | ||||
| - Change contrast of status links that are not mentions or hashtags ([ThibG](https://github.com/tootsuite/mastodon/pull/11406)) | ||||
| - **Change hashtags to preserve first-used casing** ([Gargron](https://github.com/tootsuite/mastodon/pull/11416), [Gargron](https://github.com/tootsuite/mastodon/pull/11508), [Gargron](https://github.com/tootsuite/mastodon/pull/11504), [Gargron](https://github.com/tootsuite/mastodon/pull/11507), [Gargron](https://github.com/tootsuite/mastodon/pull/11441)) | ||||
| - **Change unconfirmed user login behaviour** ([Gargron](https://github.com/tootsuite/mastodon/pull/11375), [ThibG](https://github.com/tootsuite/mastodon/pull/11394), [Gargron](https://github.com/tootsuite/mastodon/pull/11860)) | ||||
| - **Change single-column mode to scroll the whole page** ([Gargron](https://github.com/tootsuite/mastodon/pull/11359), [Gargron](https://github.com/tootsuite/mastodon/pull/11894), [Gargron](https://github.com/tootsuite/mastodon/pull/11891), [ThibG](https://github.com/tootsuite/mastodon/pull/11655), [Gargron](https://github.com/tootsuite/mastodon/pull/11463), [Gargron](https://github.com/tootsuite/mastodon/pull/11458), [ThibG](https://github.com/tootsuite/mastodon/pull/11395), [Gargron](https://github.com/tootsuite/mastodon/pull/11418)) | ||||
| - Change `tootctl accounts follow` to only work with local accounts ([angristan](https://github.com/tootsuite/mastodon/pull/11592)) | ||||
| - Change Dockerfile ([Shleeble](https://github.com/tootsuite/mastodon/pull/11710), [ykzts](https://github.com/tootsuite/mastodon/pull/11768), [Shleeble](https://github.com/tootsuite/mastodon/pull/11707)) | ||||
| - Change supported Node versions to include v12 ([abcang](https://github.com/tootsuite/mastodon/pull/11706)) | ||||
| - Change Portuguese language from `pt` to `pt-PT` ([Gargron](https://github.com/tootsuite/mastodon/pull/11820)) | ||||
| - Change domain block silence to always require approval on follow ([ThibG](https://github.com/tootsuite/mastodon/pull/11975)) | ||||
| - Change link preview fetcher to not perform a HEAD request first ([Gargron](https://github.com/tootsuite/mastodon/pull/12028)) | ||||
| - Change `tootctl domains purge` to accept multiple domains at once ([Gargron](https://github.com/tootsuite/mastodon/pull/12046)) | ||||
|  | ||||
| ### Removed | ||||
|  | ||||
| - **Remove OStatus support** ([Gargron](https://github.com/tootsuite/mastodon/pull/11205), [Gargron](https://github.com/tootsuite/mastodon/pull/11303), [Gargron](https://github.com/tootsuite/mastodon/pull/11460), [ThibG](https://github.com/tootsuite/mastodon/pull/11280), [ThibG](https://github.com/tootsuite/mastodon/pull/11278)) | ||||
| - Remove Atom feeds and old URLs in the form of `GET /:username/updates/:id` ([Gargron](https://github.com/tootsuite/mastodon/pull/11247)) | ||||
| - Remove WebP support ([angristan](https://github.com/tootsuite/mastodon/pull/11589)) | ||||
| - Remove deprecated config options from Heroku and Scalingo ([ykzts](https://github.com/tootsuite/mastodon/pull/11925)) | ||||
| - Remove deprecated REST API `GET /api/v1/search` API ([Gargron](https://github.com/tootsuite/mastodon/pull/11823)) | ||||
| - Remove deprecated REST API `GET /api/v1/statuses/:id/card` ([Gargron](https://github.com/tootsuite/mastodon/pull/11213)) | ||||
| - Remove deprecated REST API `POST /api/v1/notifications/dismiss?id=:id` ([Gargron](https://github.com/tootsuite/mastodon/pull/11214)) | ||||
| - Remove deprecated REST API `GET /api/v1/timelines/direct` ([Gargron](https://github.com/tootsuite/mastodon/pull/11212)) | ||||
|  | ||||
| ### Fixed | ||||
|  | ||||
| - Fix manifest warning ([ykzts](https://github.com/tootsuite/mastodon/pull/11767)) | ||||
| - Fix admin UI for custom emoji not respecting GIF autoplay preference ([ThibG](https://github.com/tootsuite/mastodon/pull/11801)) | ||||
| - Fix page body not being scrollable in admin/settings layout ([Gargron](https://github.com/tootsuite/mastodon/pull/11893)) | ||||
| - Fix placeholder colors for inputs not being explicitly defined ([Gargron](https://github.com/tootsuite/mastodon/pull/11890)) | ||||
| - Fix incorrect enclosure length in RSS ([tsia](https://github.com/tootsuite/mastodon/pull/11889)) | ||||
| - Fix TOTP codes not being filtered from logs during enabling/disabling ([Gargron](https://github.com/tootsuite/mastodon/pull/11877)) | ||||
| - Fix webfinger response not returning 410 when account is suspended ([Gargron](https://github.com/tootsuite/mastodon/pull/11869)) | ||||
| - Fix ActivityPub Move handler queuing jobs that will fail if account is suspended ([Gargron](https://github.com/tootsuite/mastodon/pull/11864)) | ||||
| - Fix SSO login not using existing account when e-mail is verified ([Gargron](https://github.com/tootsuite/mastodon/pull/11862)) | ||||
| - Fix web UI allowing uploads past status limit via drag & drop ([Gargron](https://github.com/tootsuite/mastodon/pull/11863)) | ||||
| - Fix expiring polls not being displayed as such in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11835)) | ||||
| - Fix 2FA challenge and password challenge for non-database users ([Gargron](https://github.com/tootsuite/mastodon/pull/11831), [Gargron](https://github.com/tootsuite/mastodon/pull/11943)) | ||||
| - Fix profile fields overflowing page width in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11828)) | ||||
| - Fix web push subscriptions being deleted on rate limit or timeout ([Gargron](https://github.com/tootsuite/mastodon/pull/11826)) | ||||
| - Fix display of long poll options in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11717), [ThibG](https://github.com/tootsuite/mastodon/pull/11833)) | ||||
| - Fix search API not resolving URL when `type` is given ([Gargron](https://github.com/tootsuite/mastodon/pull/11822)) | ||||
| - Fix hashtags being split by ZWNJ character ([Gargron](https://github.com/tootsuite/mastodon/pull/11821)) | ||||
| - Fix scroll position resetting when opening media modals in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11815)) | ||||
| - Fix duplicate HTML IDs on about page ([ThibG](https://github.com/tootsuite/mastodon/pull/11803)) | ||||
| - Fix admin UI showing superfluous reject media/reports on suspended domain blocks ([ThibG](https://github.com/tootsuite/mastodon/pull/11749)) | ||||
| - Fix ActivityPub context not being dynamically computed ([ThibG](https://github.com/tootsuite/mastodon/pull/11746)) | ||||
| - Fix Mastodon logo style on hover on public pages' footer ([ThibG](https://github.com/tootsuite/mastodon/pull/11735)) | ||||
| - Fix height of dashboard counters ([ThibG](https://github.com/tootsuite/mastodon/pull/11736)) | ||||
| - Fix custom emoji animation on hover in web UI directory bios ([ThibG](https://github.com/tootsuite/mastodon/pull/11716)) | ||||
| - Fix non-numbers being passed to Redis and causing an error ([Gargron](https://github.com/tootsuite/mastodon/pull/11697)) | ||||
| - Fix error in REST API for an account's statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/11700)) | ||||
| - Fix uncaught error when resource param is missing in Webfinger request ([Gargron](https://github.com/tootsuite/mastodon/pull/11701)) | ||||
| - Fix uncaught domain normalization error in remote follow ([Gargron](https://github.com/tootsuite/mastodon/pull/11703)) | ||||
| - Fix uncaught 422 and 500 errors ([Gargron](https://github.com/tootsuite/mastodon/pull/11590), [Gargron](https://github.com/tootsuite/mastodon/pull/11811)) | ||||
| - Fix uncaught parameter missing exceptions and missing error templates ([Gargron](https://github.com/tootsuite/mastodon/pull/11702)) | ||||
| - Fix encoding error when checking e-mail MX records ([Gargron](https://github.com/tootsuite/mastodon/pull/11696)) | ||||
| - Fix items in StatusContent render list not all having a key ([ThibG](https://github.com/tootsuite/mastodon/pull/11645)) | ||||
| - Fix remote and staff-removed statuses leaving media behind for a day ([Gargron](https://github.com/tootsuite/mastodon/pull/11638)) | ||||
| - Fix CSP needlessly allowing blob URLs in script-src ([ThibG](https://github.com/tootsuite/mastodon/pull/11620)) | ||||
| - Fix ignoring whole status because of one invalid hashtag ([Gargron](https://github.com/tootsuite/mastodon/pull/11621)) | ||||
| - Fix hidden statuses losing focus ([ThibG](https://github.com/tootsuite/mastodon/pull/11208)) | ||||
| - Fix loading bar being obscured by other elements in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11598)) | ||||
| - Fix multiple issues with replies collection for pages further than self-replies ([ThibG](https://github.com/tootsuite/mastodon/pull/11582)) | ||||
| - Fix blurhash and autoplay not working on public pages ([Gargron](https://github.com/tootsuite/mastodon/pull/11585)) | ||||
| - Fix 422 being returned instead of 404 when POSTing to unmatched routes ([Gargron](https://github.com/tootsuite/mastodon/pull/11574), [Gargron](https://github.com/tootsuite/mastodon/pull/11704)) | ||||
| - Fix client-side resizing of image uploads ([ThibG](https://github.com/tootsuite/mastodon/pull/11570)) | ||||
| - Fix short number formatting for numbers above million in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11559)) | ||||
| - Fix ActivityPub and REST API queries setting cookies and preventing caching ([ThibG](https://github.com/tootsuite/mastodon/pull/11539), [ThibG](https://github.com/tootsuite/mastodon/pull/11557), [ThibG](https://github.com/tootsuite/mastodon/pull/11336), [ThibG](https://github.com/tootsuite/mastodon/pull/11331)) | ||||
| - Fix some emojis in profile metadata labels are not emojified. ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/11534)) | ||||
| - Fix account search always returning exact match on paginated results ([Gargron](https://github.com/tootsuite/mastodon/pull/11525)) | ||||
| - Fix acct URIs with IDN domains not being resolved ([Gargron](https://github.com/tootsuite/mastodon/pull/11520)) | ||||
| - Fix admin dashboard missing latest features ([Gargron](https://github.com/tootsuite/mastodon/pull/11505)) | ||||
| - Fix jumping of toot date when clicking spoiler button ([ariasuni](https://github.com/tootsuite/mastodon/pull/11449)) | ||||
| - Fix boost to original audience not working on mobile in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11371)) | ||||
| - Fix handling of webfinger redirects in ResolveAccountService ([ThibG](https://github.com/tootsuite/mastodon/pull/11279)) | ||||
| - Fix URLs appearing twice in errors of ActivityPub::DeliveryWorker ([Gargron](https://github.com/tootsuite/mastodon/pull/11231)) | ||||
| - Fix support for HTTP proxies ([ThibG](https://github.com/tootsuite/mastodon/pull/11245)) | ||||
| - Fix HTTP requests to IPv6 hosts ([ThibG](https://github.com/tootsuite/mastodon/pull/11240)) | ||||
| - Fix error in ElasticSearch index import ([mayaeh](https://github.com/tootsuite/mastodon/pull/11192)) | ||||
| - Fix duplicate account error when seeding development database ([ysksn](https://github.com/tootsuite/mastodon/pull/11366)) | ||||
| - Fix performance of session clean-up scheduler ([abcang](https://github.com/tootsuite/mastodon/pull/11871)) | ||||
| - Fix older migrations not running ([zunda](https://github.com/tootsuite/mastodon/pull/11377)) | ||||
| - Fix URLs counting towards RTL detection ([ahangarha](https://github.com/tootsuite/mastodon/pull/11759)) | ||||
| - Fix unnecessary status re-rendering in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/11211)) | ||||
| - Fix http_parser.rb gem not being compiled when no network available ([petabyteboy](https://github.com/tootsuite/mastodon/pull/11444)) | ||||
| - Fix muted text color not applying to all text ([trwnh](https://github.com/tootsuite/mastodon/pull/11996)) | ||||
| - Fix follower/following lists resetting on back-navigation in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11986)) | ||||
| - Fix n+1 query when approving multiple follow requests ([abcang](https://github.com/tootsuite/mastodon/pull/12004)) | ||||
| - Fix records not being indexed into ElasticSearch sometimes ([Gargron](https://github.com/tootsuite/mastodon/pull/12024)) | ||||
| - Fix needlessly indexing unsearchable statuses into ElasticSearch ([Gargron](https://github.com/tootsuite/mastodon/pull/12041)) | ||||
| - Fix new user bootstrapping crashing when to-be-followed accounts are invalid ([ThibG](https://github.com/tootsuite/mastodon/pull/12037)) | ||||
| - Fix featured hashtag URL being interpreted as media or replies tab ([Gargron](https://github.com/tootsuite/mastodon/pull/12048)) | ||||
| - Fix account counters being overwritten by parallel writes ([Gargron](https://github.com/tootsuite/mastodon/pull/12045)) | ||||
|  | ||||
| ### Security | ||||
|  | ||||
| - Fix performance of GIF re-encoding and always strip EXIF data from videos ([Gargron](https://github.com/tootsuite/mastodon/pull/12057)) | ||||
|  | ||||
| ## [2.9.3] - 2019-08-10 | ||||
| ### Added | ||||
|  | ||||
|  | ||||
							
								
								
									
										20
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -4,22 +4,20 @@ FROM ubuntu:18.04 as build-dep | ||||
| SHELL ["bash", "-c"] | ||||
|  | ||||
| # Install Node | ||||
| ENV NODE_VER="8.15.0" | ||||
| ENV NODE_VER="12.11.1" | ||||
| RUN	echo "Etc/UTC" > /etc/localtime && \ | ||||
| 	apt update && \ | ||||
| 	apt -y install wget make gcc g++ python && \ | ||||
| 	apt -y install wget python && \ | ||||
| 	cd ~ && \ | ||||
| 	wget https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER.tar.gz && \ | ||||
| 	tar xf node-v$NODE_VER.tar.gz && \ | ||||
| 	cd node-v$NODE_VER && \ | ||||
| 	./configure --prefix=/opt/node && \ | ||||
| 	make -j$(nproc) > /dev/null && \ | ||||
| 	make install | ||||
| 	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.1.0" | ||||
| ENV JE_VER="5.2.1" | ||||
| RUN apt update && \ | ||||
| 	apt -y install autoconf && \ | ||||
| 	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 && \ | ||||
| @ -30,7 +28,7 @@ RUN apt update && \ | ||||
| 	make install_bin install_include install_lib | ||||
|  | ||||
| # Install ruby | ||||
| ENV RUBY_VER="2.6.1" | ||||
| ENV RUBY_VER="2.6.5" | ||||
| ENV CPPFLAGS="-I/opt/jemalloc/include" | ||||
| ENV LDFLAGS="-L/opt/jemalloc/lib/" | ||||
| RUN apt update && \ | ||||
|  | ||||
							
								
								
									
										53
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								Gemfile
									
									
									
									
									
								
							| @ -5,17 +5,17 @@ ruby '>= 2.4.0', '< 2.7.0' | ||||
|  | ||||
| gem 'pkg-config', '~> 1.3' | ||||
|  | ||||
| gem 'puma', '~> 3.12' | ||||
| gem 'puma', '~> 4.2' | ||||
| gem 'rails', '~> 5.2.3' | ||||
| gem 'thor', '~> 0.20' | ||||
|  | ||||
| gem 'hamlit-rails', '~> 0.2' | ||||
| gem 'pg', '~> 1.1' | ||||
| gem 'makara', '~> 0.4' | ||||
| gem 'pghero', '~> 2.2' | ||||
| gem 'pghero', '~> 2.3' | ||||
| gem 'dotenv-rails', '~> 2.7' | ||||
|  | ||||
| gem 'aws-sdk-s3', '~> 1.42', require: false | ||||
| gem 'aws-sdk-s3', '~> 1.48', require: false | ||||
| gem 'fog-core', '<= 2.1.0' | ||||
| gem 'fog-openstack', '~> 0.3', require: false | ||||
| gem 'paperclip', '~> 6.0' | ||||
| @ -24,15 +24,15 @@ gem 'streamio-ffmpeg', '~> 3.0' | ||||
| gem 'blurhash', '~> 0.1' | ||||
|  | ||||
| gem 'active_model_serializers', '~> 0.10' | ||||
| gem 'addressable', '~> 2.6' | ||||
| gem 'addressable', '~> 2.7' | ||||
| gem 'bootsnap', '~> 1.4', require: false | ||||
| gem 'browser' | ||||
| gem 'charlock_holmes', '~> 0.7.6' | ||||
| gem 'iso-639' | ||||
| gem 'chewy', '~> 5.0' | ||||
| gem 'chewy', '~> 5.1' | ||||
| gem 'cld3', '~> 3.2.4' | ||||
| gem 'devise', '~> 4.6' | ||||
| gem 'devise-two-factor', '~> 3.0' | ||||
| gem 'devise', '~> 4.7' | ||||
| gem 'devise-two-factor', '~> 3.1' | ||||
|  | ||||
| group :pam_authentication, optional: true do | ||||
|   gem 'devise_pam_authenticatable2', '~> 9.2' | ||||
| @ -43,42 +43,48 @@ gem 'omniauth-cas', '~> 1.1' | ||||
| gem 'omniauth-saml', '~> 1.10' | ||||
| gem 'omniauth', '~> 1.9' | ||||
|  | ||||
| gem 'doorkeeper', '~> 5.1' | ||||
| gem 'discard', '~> 1.1' | ||||
| gem 'doorkeeper', '~> 5.2' | ||||
| gem 'fast_blank', '~> 1.0' | ||||
| gem 'fastimage' | ||||
| gem 'goldfinger', '~> 2.1' | ||||
| gem 'hiredis', '~> 0.6' | ||||
| gem 'redis-namespace', '~> 1.5' | ||||
| gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b' | ||||
| gem 'htmlentities', '~> 4.3' | ||||
| gem 'http', '~> 3.3' | ||||
| gem 'http_accept_language', '~> 2.1' | ||||
| gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2' | ||||
| gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true | ||||
| gem 'httplog', '~> 1.3' | ||||
| gem 'idn-ruby', require: 'idn' | ||||
| gem 'kaminari', '~> 1.1' | ||||
| gem 'link_header', '~> 0.0' | ||||
| gem 'mime-types', '~> 3.2', require: 'mime/types/columnar' | ||||
| gem 'mime-types', '~> 3.3', require: 'mime/types/columnar' | ||||
| gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532' | ||||
| gem 'nokogiri', '~> 1.10' | ||||
| gem 'nsa', '~> 0.2' | ||||
| gem 'oj', '~> 3.7' | ||||
| gem 'oj', '~> 3.9' | ||||
| gem 'ostatus2', '~> 2.0' | ||||
| gem 'ox', '~> 2.11' | ||||
| gem 'parslet' | ||||
| gem 'parallel', '~> 1.17' | ||||
| gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' | ||||
| gem 'pundit', '~> 2.0' | ||||
| gem 'pundit', '~> 2.1' | ||||
| gem 'premailer-rails' | ||||
| gem 'rack-attack', '~> 6.0' | ||||
| gem 'rack-attack', '~> 6.1' | ||||
| gem 'rack-cors', '~> 1.0', require: 'rack/cors' | ||||
| gem 'rails-i18n', '~> 5.1' | ||||
| gem 'rails-settings-cached', '~> 0.6' | ||||
| gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis'] | ||||
| gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' | ||||
| gem 'rqrcode', '~> 0.10' | ||||
| gem 'sanitize', '~> 5.0' | ||||
| gem 'ruby-progressbar', '~> 1.10' | ||||
| gem 'sanitize', '~> 5.1' | ||||
| gem 'sidekiq', '~> 5.2' | ||||
| gem 'sidekiq-scheduler', '~> 3.0' | ||||
| gem 'sidekiq-unique-jobs', '~> 6.0' | ||||
| gem 'sidekiq-bulk', '~>0.2.0' | ||||
| gem 'simple-navigation', '~> 4.0' | ||||
| gem 'simple-navigation', '~> 4.1' | ||||
| gem 'simple_form', '~> 4.1' | ||||
| gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' | ||||
| gem 'stoplight', '~> 2.1.3' | ||||
| @ -90,7 +96,7 @@ gem 'tzinfo-data', '~> 1.2019' | ||||
| gem 'webpacker', '~> 4.0' | ||||
| gem 'webpush' | ||||
|  | ||||
| gem 'json-ld', '~> 3.0' | ||||
| gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: 'e742697a0906e74e8bb777ef98137bc3955d981d' | ||||
| gem 'json-ld-preloaded', '~> 3.0' | ||||
| gem 'rdf-normalize', '~> 0.3' | ||||
|  | ||||
| @ -108,14 +114,14 @@ group :production, :test do | ||||
| end | ||||
|  | ||||
| group :test do | ||||
|   gem 'capybara', '~> 3.24' | ||||
|   gem 'capybara', '~> 3.29' | ||||
|   gem 'climate_control', '~> 0.2' | ||||
|   gem 'faker', '~> 1.9' | ||||
|   gem 'faker', '~> 2.4' | ||||
|   gem 'microformats', '~> 4.1' | ||||
|   gem 'rails-controller-testing', '~> 1.0' | ||||
|   gem 'rspec-sidekiq', '~> 3.0' | ||||
|   gem 'simplecov', '~> 0.16', require: false | ||||
|   gem 'webmock', '~> 3.6' | ||||
|   gem 'simplecov', '~> 0.17', require: false | ||||
|   gem 'webmock', '~> 3.7' | ||||
|   gem 'parallel_tests', '~> 2.29' | ||||
| end | ||||
|  | ||||
| @ -128,9 +134,9 @@ group :development do | ||||
|   gem 'letter_opener', '~> 1.7' | ||||
|   gem 'letter_opener_web', '~> 1.3' | ||||
|   gem 'memory_profiler' | ||||
|   gem 'rubocop', '~> 0.71', require: false | ||||
|   gem 'rubocop-rails', '~> 2.0', require: false | ||||
|   gem 'brakeman', '~> 4.5', require: false | ||||
|   gem 'rubocop', '~> 0.74', require: false | ||||
|   gem 'rubocop-rails', '~> 2.3', require: false | ||||
|   gem 'brakeman', '~> 4.6', require: false | ||||
|   gem 'bundler-audit', '~> 0.6', require: false | ||||
|  | ||||
|   gem 'capistrano', '~> 3.11' | ||||
| @ -148,3 +154,4 @@ group :production do | ||||
| end | ||||
|  | ||||
| gem 'concurrent-ruby', require: false | ||||
| gem 'connection_pool', require: false | ||||
|  | ||||
							
								
								
									
										270
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										270
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @ -1,3 +1,11 @@ | ||||
| GIT | ||||
|   remote: https://github.com/ianheggie/health_check | ||||
|   revision: 0b799ead604f900ed50685e9b2d469cd2befba5b | ||||
|   ref: 0b799ead604f900ed50685e9b2d469cd2befba5b | ||||
|   specs: | ||||
|     health_check (4.0.0.pre) | ||||
|       rails (>= 4.0) | ||||
|  | ||||
| GIT | ||||
|   remote: https://github.com/rtomayko/posix-spawn | ||||
|   revision: 58465d2e213991f8afb13b984854a49fcdcc980c | ||||
| @ -5,13 +13,34 @@ GIT | ||||
|   specs: | ||||
|     posix-spawn (0.3.13) | ||||
|  | ||||
| GIT | ||||
|   remote: https://github.com/ruby-rdf/json-ld.git | ||||
|   revision: e742697a0906e74e8bb777ef98137bc3955d981d | ||||
|   ref: e742697a0906e74e8bb777ef98137bc3955d981d | ||||
|   specs: | ||||
|     json-ld (3.0.2) | ||||
|       htmlentities (~> 4.3) | ||||
|       json-canonicalization (~> 0.1) | ||||
|       link_header (~> 0.0, >= 0.0.8) | ||||
|       multi_json (~> 1.13) | ||||
|       rack (>= 1.6, < 3.0) | ||||
|       rdf (~> 3.0, >= 3.0.8) | ||||
|  | ||||
| GIT | ||||
|   remote: https://github.com/tmm1/http_parser.rb | ||||
|   revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2 | ||||
|   ref: 54b17ba8c7d8d20a16dfc65d1775241833219cf2 | ||||
|   submodules: true | ||||
|   specs: | ||||
|     http_parser.rb (0.6.1) | ||||
|  | ||||
| GIT | ||||
|   remote: https://github.com/witgo/nilsimsa | ||||
|   revision: fd184883048b922b176939f851338d0a4971a532 | ||||
|   ref: fd184883048b922b176939f851338d0a4971a532 | ||||
|   specs: | ||||
|     nilsimsa (1.1.2) | ||||
|  | ||||
| GEM | ||||
|   remote: https://rubygems.org/ | ||||
|   specs: | ||||
| @ -38,9 +67,9 @@ GEM | ||||
|       erubi (~> 1.4) | ||||
|       rails-dom-testing (~> 2.0) | ||||
|       rails-html-sanitizer (~> 1.0, >= 1.0.3) | ||||
|     active_model_serializers (0.10.9) | ||||
|       actionpack (>= 4.1, < 6) | ||||
|       activemodel (>= 4.1, < 6) | ||||
|     active_model_serializers (0.10.10) | ||||
|       actionpack (>= 4.1, < 6.1) | ||||
|       activemodel (>= 4.1, < 6.1) | ||||
|       case_transform (>= 0.2) | ||||
|       jsonapi-renderer (>= 0.1.1.beta1, < 0.3) | ||||
|     active_record_query_trace (1.6.2) | ||||
| @ -62,9 +91,9 @@ GEM | ||||
|       i18n (>= 0.7, < 2) | ||||
|       minitest (~> 5.1) | ||||
|       tzinfo (~> 1.1) | ||||
|     addressable (2.6.0) | ||||
|       public_suffix (>= 2.0.2, < 4.0) | ||||
|     airbrussh (1.3.0) | ||||
|     addressable (2.7.0) | ||||
|       public_suffix (>= 2.0.2, < 5.0) | ||||
|     airbrussh (1.3.3) | ||||
|       sshkit (>= 1.6.1, != 1.7.0) | ||||
|     annotate (2.7.5) | ||||
|       activerecord (>= 3.2, < 7.0) | ||||
| @ -76,17 +105,17 @@ GEM | ||||
|     av (0.9.0) | ||||
|       cocaine (~> 0.5.3) | ||||
|     aws-eventstream (1.0.3) | ||||
|     aws-partitions (1.175.0) | ||||
|     aws-sdk-core (3.55.0) | ||||
|     aws-partitions (1.207.0) | ||||
|     aws-sdk-core (3.65.1) | ||||
|       aws-eventstream (~> 1.0, >= 1.0.2) | ||||
|       aws-partitions (~> 1.0) | ||||
|       aws-sigv4 (~> 1.1) | ||||
|       jmespath (~> 1.0) | ||||
|     aws-sdk-kms (1.21.0) | ||||
|       aws-sdk-core (~> 3, >= 3.53.0) | ||||
|     aws-sdk-kms (1.24.0) | ||||
|       aws-sdk-core (~> 3, >= 3.61.1) | ||||
|       aws-sigv4 (~> 1.1) | ||||
|     aws-sdk-s3 (1.42.0) | ||||
|       aws-sdk-core (~> 3, >= 3.53.0) | ||||
|     aws-sdk-s3 (1.48.0) | ||||
|       aws-sdk-core (~> 3, >= 3.61.1) | ||||
|       aws-sdk-kms (~> 1) | ||||
|       aws-sigv4 (~> 1.1) | ||||
|     aws-sigv4 (1.1.0) | ||||
| @ -101,19 +130,19 @@ GEM | ||||
|       debug_inspector (>= 0.0.1) | ||||
|     blurhash (0.1.3) | ||||
|       ffi (~> 1.10.0) | ||||
|     bootsnap (1.4.4) | ||||
|     bootsnap (1.4.5) | ||||
|       msgpack (~> 1.0) | ||||
|     brakeman (4.5.1) | ||||
|     browser (2.5.3) | ||||
|     brakeman (4.6.1) | ||||
|     browser (2.6.1) | ||||
|     builder (3.2.3) | ||||
|     bullet (6.0.0) | ||||
|     bullet (6.0.2) | ||||
|       activesupport (>= 3.0.0) | ||||
|       uniform_notifier (~> 1.11) | ||||
|     bundler-audit (0.6.1) | ||||
|       bundler (>= 1.2.0, < 3) | ||||
|       thor (~> 0.18) | ||||
|     byebug (11.0.0) | ||||
|     capistrano (3.11.0) | ||||
|     capistrano (3.11.1) | ||||
|       airbrussh (>= 1.0.0) | ||||
|       i18n | ||||
|       rake (>= 10.0.0) | ||||
| @ -129,7 +158,7 @@ GEM | ||||
|       sshkit (~> 1.3) | ||||
|     capistrano-yarn (2.0.2) | ||||
|       capistrano (~> 3.0) | ||||
|     capybara (3.24.0) | ||||
|     capybara (3.29.0) | ||||
|       addressable | ||||
|       mini_mime (>= 0.1.3) | ||||
|       nokogiri (~> 1.8) | ||||
| @ -140,7 +169,7 @@ GEM | ||||
|     case_transform (0.2) | ||||
|       activesupport | ||||
|     charlock_holmes (0.7.6) | ||||
|     chewy (5.0.0) | ||||
|     chewy (5.1.0) | ||||
|       activesupport (>= 4.0) | ||||
|       elasticsearch (>= 2.0.0) | ||||
|       elasticsearch-dsl | ||||
| @ -156,10 +185,10 @@ GEM | ||||
|     crack (0.4.3) | ||||
|       safe_yaml (~> 1.0.0) | ||||
|     crass (1.0.4) | ||||
|     css_parser (1.6.0) | ||||
|     css_parser (1.7.0) | ||||
|       addressable | ||||
|     debug_inspector (0.0.3) | ||||
|     derailed_benchmarks (1.3.5) | ||||
|     derailed_benchmarks (1.3.6) | ||||
|       benchmark-ips (~> 2) | ||||
|       get_process_mem (~> 0) | ||||
|       heapy (~> 0) | ||||
| @ -167,38 +196,40 @@ GEM | ||||
|       rack (>= 1) | ||||
|       rake (> 10, < 13) | ||||
|       thor (~> 0.19) | ||||
|     devise (4.6.2) | ||||
|     devise (4.7.1) | ||||
|       bcrypt (~> 3.0) | ||||
|       orm_adapter (~> 0.1) | ||||
|       railties (>= 4.1.0, < 6.0) | ||||
|       railties (>= 4.1.0) | ||||
|       responders | ||||
|       warden (~> 1.2.3) | ||||
|     devise-two-factor (3.0.3) | ||||
|       activesupport (< 5.3) | ||||
|     devise-two-factor (3.1.0) | ||||
|       activesupport (< 6.1) | ||||
|       attr_encrypted (>= 1.3, < 4, != 2) | ||||
|       devise (~> 4.0) | ||||
|       railties (< 5.3) | ||||
|       railties (< 6.1) | ||||
|       rotp (~> 2.0) | ||||
|     devise_pam_authenticatable2 (9.2.0) | ||||
|       devise (>= 4.0.0) | ||||
|       rpam2 (~> 4.0) | ||||
|     diff-lcs (1.3) | ||||
|     docile (1.3.0) | ||||
|     discard (1.1.0) | ||||
|       activerecord (>= 4.2, < 7) | ||||
|     docile (1.3.2) | ||||
|     domain_name (0.5.20180417) | ||||
|       unf (>= 0.0.5, < 1.0.0) | ||||
|     doorkeeper (5.1.0) | ||||
|     doorkeeper (5.2.1) | ||||
|       railties (>= 5) | ||||
|     dotenv (2.7.2) | ||||
|     dotenv-rails (2.7.2) | ||||
|       dotenv (= 2.7.2) | ||||
|     dotenv (2.7.5) | ||||
|     dotenv-rails (2.7.5) | ||||
|       dotenv (= 2.7.5) | ||||
|       railties (>= 3.2, < 6.1) | ||||
|     elasticsearch (6.0.2) | ||||
|       elasticsearch-api (= 6.0.2) | ||||
|       elasticsearch-transport (= 6.0.2) | ||||
|     elasticsearch-api (6.0.2) | ||||
|     elasticsearch (7.3.0) | ||||
|       elasticsearch-api (= 7.3.0) | ||||
|       elasticsearch-transport (= 7.3.0) | ||||
|     elasticsearch-api (7.3.0) | ||||
|       multi_json | ||||
|     elasticsearch-dsl (0.1.5) | ||||
|     elasticsearch-transport (6.0.2) | ||||
|     elasticsearch-dsl (0.1.8) | ||||
|     elasticsearch-transport (7.3.0) | ||||
|       faraday | ||||
|       multi_json | ||||
|     encryptor (3.0.0) | ||||
| @ -208,12 +239,12 @@ GEM | ||||
|       tzinfo | ||||
|     excon (0.62.0) | ||||
|     fabrication (2.20.2) | ||||
|     faker (1.9.3) | ||||
|       i18n (>= 0.7) | ||||
|     faraday (0.15.0) | ||||
|     faker (2.4.0) | ||||
|       i18n (~> 1.6.0) | ||||
|     faraday (0.15.4) | ||||
|       multipart-post (>= 1.2, < 3) | ||||
|     fast_blank (1.0.0) | ||||
|     fastimage (2.1.5) | ||||
|     fastimage (2.1.7) | ||||
|     ffi (1.10.0) | ||||
|     fog-core (2.1.0) | ||||
|       builder | ||||
| @ -253,7 +284,7 @@ GEM | ||||
|       railties (>= 4.0.1) | ||||
|     hamster (3.0.0) | ||||
|       concurrent-ruby (~> 1.0) | ||||
|     hashdiff (0.4.0) | ||||
|     hashdiff (1.0.0) | ||||
|     hashie (3.6.0) | ||||
|     heapy (0.1.4) | ||||
|     highline (2.0.1) | ||||
| @ -269,7 +300,7 @@ GEM | ||||
|       domain_name (~> 0.5) | ||||
|     http-form_data (2.1.1) | ||||
|     http_accept_language (2.1.1) | ||||
|     httplog (1.3.1) | ||||
|     httplog (1.3.2) | ||||
|       rack (>= 1.0) | ||||
|       rainbow (>= 2.0.0) | ||||
|     i18n (1.6.0) | ||||
| @ -287,17 +318,15 @@ GEM | ||||
|     idn-ruby (0.1.0) | ||||
|     ipaddress (0.8.3) | ||||
|     iso-639 (0.2.8) | ||||
|     jaro_winkler (1.5.2) | ||||
|     jaro_winkler (1.5.3) | ||||
|     jmespath (1.4.0) | ||||
|     json (2.1.0) | ||||
|     json-ld (3.0.2) | ||||
|       multi_json (~> 1.12) | ||||
|       rdf (>= 2.2.8, < 4.0) | ||||
|     json-ld-preloaded (3.0.2) | ||||
|     json (2.2.0) | ||||
|     json-canonicalization (0.1.0) | ||||
|     json-ld-preloaded (3.0.4) | ||||
|       json-ld (~> 3.0) | ||||
|       multi_json (~> 1.12) | ||||
|       rdf (~> 3.0) | ||||
|     jsonapi-renderer (0.2.0) | ||||
|     jsonapi-renderer (0.2.2) | ||||
|     jwt (2.1.0) | ||||
|     kaminari (1.1.1) | ||||
|       activesupport (>= 4.1.0) | ||||
| @ -336,37 +365,37 @@ GEM | ||||
|       mimemagic (~> 0.3.2) | ||||
|     mario-redis-lock (1.2.1) | ||||
|       redis (>= 3.0.5) | ||||
|     memory_profiler (0.9.13) | ||||
|     memory_profiler (0.9.14) | ||||
|     method_source (0.9.2) | ||||
|     microformats (4.1.0) | ||||
|       json (~> 2.1) | ||||
|       nokogiri (~> 1.8, >= 1.8.3) | ||||
|     mime-types (3.2.2) | ||||
|     mime-types (3.3) | ||||
|       mime-types-data (~> 3.2015) | ||||
|     mime-types-data (3.2018.0812) | ||||
|     mime-types-data (3.2019.0904) | ||||
|     mimemagic (0.3.3) | ||||
|     mini_mime (1.0.1) | ||||
|     mini_mime (1.0.2) | ||||
|     mini_portile2 (2.4.0) | ||||
|     minitest (5.11.3) | ||||
|     msgpack (1.2.10) | ||||
|     minitest (5.12.0) | ||||
|     msgpack (1.3.1) | ||||
|     multi_json (1.13.1) | ||||
|     multipart-post (2.0.0) | ||||
|     multipart-post (2.1.1) | ||||
|     necromancer (0.5.0) | ||||
|     net-ldap (0.16.1) | ||||
|     net-scp (1.2.1) | ||||
|       net-ssh (>= 2.6.5) | ||||
|     net-ssh (5.0.2) | ||||
|     nio4r (2.3.1) | ||||
|     nokogiri (1.10.3) | ||||
|     net-scp (2.0.0) | ||||
|       net-ssh (>= 2.6.5, < 6.0.0) | ||||
|     net-ssh (5.2.0) | ||||
|     nio4r (2.5.1) | ||||
|     nokogiri (1.10.4) | ||||
|       mini_portile2 (~> 2.4.0) | ||||
|     nokogumbo (2.0.0) | ||||
|     nokogumbo (2.0.1) | ||||
|       nokogiri (~> 1.8, >= 1.8.4) | ||||
|     nsa (0.2.7) | ||||
|       activesupport (>= 4.2, < 6) | ||||
|       concurrent-ruby (~> 1.0, >= 1.0.2) | ||||
|       sidekiq (>= 3.5) | ||||
|       statsd-ruby (~> 1.4, >= 1.4.0) | ||||
|     oj (3.7.12) | ||||
|     oj (3.9.1) | ||||
|     omniauth (1.9.0) | ||||
|       hashie (>= 3.4.6, < 3.7.0) | ||||
|       rack (>= 1.6.2, < 3) | ||||
| @ -393,23 +422,24 @@ GEM | ||||
|       av (~> 0.9.0) | ||||
|       paperclip (>= 2.5.2) | ||||
|     parallel (1.17.0) | ||||
|     parallel_tests (2.29.0) | ||||
|     parallel_tests (2.29.2) | ||||
|       parallel | ||||
|     parser (2.6.3.0) | ||||
|     parser (2.6.4.0) | ||||
|       ast (~> 2.4.0) | ||||
|     parslet (1.8.2) | ||||
|     pastel (0.7.2) | ||||
|       equatable (~> 0.5.0) | ||||
|       tty-color (~> 0.4.0) | ||||
|     pg (1.1.4) | ||||
|     pghero (2.2.1) | ||||
|       activerecord | ||||
|     pkg-config (1.3.7) | ||||
|     pghero (2.3.0) | ||||
|       activerecord (>= 5) | ||||
|     pkg-config (1.3.8) | ||||
|     premailer (1.11.1) | ||||
|       addressable | ||||
|       css_parser (>= 1.6.0) | ||||
|       htmlentities (>= 4.0.0) | ||||
|     premailer-rails (1.10.2) | ||||
|       actionmailer (>= 3, < 6) | ||||
|     premailer-rails (1.10.3) | ||||
|       actionmailer (>= 3) | ||||
|       premailer (~> 1.7, >= 1.7.9) | ||||
|     private_address_check (0.5.0) | ||||
|     pry (0.12.2) | ||||
| @ -420,13 +450,14 @@ GEM | ||||
|       pry (~> 0.10) | ||||
|     pry-rails (0.3.9) | ||||
|       pry (>= 0.10.4) | ||||
|     public_suffix (3.1.0) | ||||
|     puma (3.12.1) | ||||
|     pundit (2.0.1) | ||||
|     public_suffix (4.0.1) | ||||
|     puma (4.2.0) | ||||
|       nio4r (~> 2.0) | ||||
|     pundit (2.1.0) | ||||
|       activesupport (>= 3.0.0) | ||||
|     raabro (1.1.6) | ||||
|     rack (2.0.7) | ||||
|     rack-attack (6.0.0) | ||||
|     rack-attack (6.1.0) | ||||
|       rack (>= 1.0, < 3) | ||||
|     rack-cors (1.0.3) | ||||
|     rack-protection (2.0.5) | ||||
| @ -455,7 +486,7 @@ GEM | ||||
|     rails-dom-testing (2.0.3) | ||||
|       activesupport (>= 4.2.0) | ||||
|       nokogiri (>= 1.6) | ||||
|     rails-html-sanitizer (1.0.4) | ||||
|     rails-html-sanitizer (1.2.0) | ||||
|       loofah (~> 2.2, >= 2.2.2) | ||||
|     rails-i18n (5.1.3) | ||||
|       i18n (>= 0.7, < 2) | ||||
| @ -469,13 +500,13 @@ GEM | ||||
|       rake (>= 0.8.7) | ||||
|       thor (>= 0.19.0, < 2.0) | ||||
|     rainbow (3.0.0) | ||||
|     rake (12.3.2) | ||||
|     rdf (3.0.9) | ||||
|     rake (12.3.3) | ||||
|     rdf (3.0.12) | ||||
|       hamster (~> 3.0) | ||||
|       link_header (~> 0.0, >= 0.0.8) | ||||
|     rdf-normalize (0.3.3) | ||||
|       rdf (>= 2.2, < 4.0) | ||||
|     redis (4.1.2) | ||||
|     redis (4.1.3) | ||||
|     redis-actionpack (5.0.2) | ||||
|       actionpack (>= 4.0, < 6) | ||||
|       redis-rack (>= 1, < 3) | ||||
| @ -494,12 +525,12 @@ GEM | ||||
|       redis-store (>= 1.2, < 2) | ||||
|     redis-store (1.5.0) | ||||
|       redis (>= 2.2, < 5) | ||||
|     regexp_parser (1.5.1) | ||||
|     regexp_parser (1.6.0) | ||||
|     request_store (1.4.1) | ||||
|       rack (>= 1.4) | ||||
|     responders (2.4.1) | ||||
|       actionpack (>= 4.2.0, < 6.0) | ||||
|       railties (>= 4.2.0, < 6.0) | ||||
|     responders (3.0.0) | ||||
|       actionpack (>= 5.0) | ||||
|       railties (>= 5.0) | ||||
|     rotp (2.1.2) | ||||
|     rpam2 (4.0.2) | ||||
|     rqrcode (0.10.1) | ||||
| @ -524,23 +555,23 @@ GEM | ||||
|       rspec-core (~> 3.0, >= 3.0.0) | ||||
|       sidekiq (>= 2.4.0) | ||||
|     rspec-support (3.8.0) | ||||
|     rubocop (0.71.0) | ||||
|     rubocop (0.74.0) | ||||
|       jaro_winkler (~> 1.5.1) | ||||
|       parallel (~> 1.10) | ||||
|       parser (>= 2.6) | ||||
|       rainbow (>= 2.2.2, < 4.0) | ||||
|       ruby-progressbar (~> 1.7) | ||||
|       unicode-display_width (>= 1.4.0, < 1.7) | ||||
|     rubocop-rails (2.0.1) | ||||
|     rubocop-rails (2.3.2) | ||||
|       rack (>= 1.1) | ||||
|       rubocop (>= 0.70.0) | ||||
|       rubocop (>= 0.72.0) | ||||
|     ruby-progressbar (1.10.1) | ||||
|     ruby-saml (1.9.0) | ||||
|       nokogiri (>= 1.5.10) | ||||
|     rufus-scheduler (3.5.2) | ||||
|       fugit (~> 1.1, >= 1.1.5) | ||||
|     safe_yaml (1.0.5) | ||||
|     sanitize (5.0.0) | ||||
|     sanitize (5.1.0) | ||||
|       crass (~> 1.0.2) | ||||
|       nokogiri (>= 1.8.0) | ||||
|       nokogumbo (~> 2.0) | ||||
| @ -560,12 +591,12 @@ GEM | ||||
|       concurrent-ruby (~> 1.0, >= 1.0.5) | ||||
|       sidekiq (>= 4.0, < 7.0) | ||||
|       thor (~> 0) | ||||
|     simple-navigation (4.0.5) | ||||
|     simple-navigation (4.1.0) | ||||
|       activesupport (>= 2.3.2) | ||||
|     simple_form (4.1.0) | ||||
|       actionpack (>= 5.0) | ||||
|       activemodel (>= 5.0) | ||||
|     simplecov (0.16.1) | ||||
|     simplecov (0.17.1) | ||||
|       docile (~> 1.1) | ||||
|       json (>= 1.8, < 3) | ||||
|       simplecov-html (~> 0.10.0) | ||||
| @ -577,7 +608,7 @@ GEM | ||||
|       actionpack (>= 4.0) | ||||
|       activesupport (>= 4.0) | ||||
|       sprockets (>= 3.0.0) | ||||
|     sshkit (1.17.0) | ||||
|     sshkit (1.20.0) | ||||
|       net-scp (>= 1.1.2) | ||||
|       net-ssh (>= 2.8.0) | ||||
|     stackprof (0.2.12) | ||||
| @ -585,7 +616,7 @@ GEM | ||||
|     stoplight (2.1.3) | ||||
|     streamio-ffmpeg (3.0.2) | ||||
|       multi_json (~> 1.8) | ||||
|     strong_migrations (0.4.0) | ||||
|     strong_migrations (0.4.1) | ||||
|       activerecord (>= 5) | ||||
|     temple (0.8.1) | ||||
|     terminal-table (1.8.0) | ||||
| @ -612,7 +643,7 @@ GEM | ||||
|       unf (~> 0.1.0) | ||||
|     tzinfo (1.2.5) | ||||
|       thread_safe (~> 0.1) | ||||
|     tzinfo-data (1.2019.1) | ||||
|     tzinfo-data (1.2019.3) | ||||
|       tzinfo (>= 1.0.0) | ||||
|     unf (0.1.4) | ||||
|       unf_ext | ||||
| @ -621,7 +652,7 @@ GEM | ||||
|     uniform_notifier (1.12.1) | ||||
|     warden (1.2.8) | ||||
|       rack (>= 2.0.6) | ||||
|     webmock (3.6.0) | ||||
|     webmock (3.7.5) | ||||
|       addressable (>= 2.3.6) | ||||
|       crack (>= 0.3.2) | ||||
|       hashdiff (>= 0.4.0, < 2.0.0) | ||||
| @ -645,14 +676,14 @@ PLATFORMS | ||||
| DEPENDENCIES | ||||
|   active_model_serializers (~> 0.10) | ||||
|   active_record_query_trace (~> 1.6) | ||||
|   addressable (~> 2.6) | ||||
|   addressable (~> 2.7) | ||||
|   annotate (~> 2.7) | ||||
|   aws-sdk-s3 (~> 1.42) | ||||
|   aws-sdk-s3 (~> 1.48) | ||||
|   better_errors (~> 2.5) | ||||
|   binding_of_caller (~> 0.7) | ||||
|   blurhash (~> 0.1) | ||||
|   bootsnap (~> 1.4) | ||||
|   brakeman (~> 4.5) | ||||
|   brakeman (~> 4.6) | ||||
|   browser | ||||
|   bullet (~> 6.0) | ||||
|   bundler-audit (~> 0.6) | ||||
| @ -660,20 +691,22 @@ DEPENDENCIES | ||||
|   capistrano-rails (~> 1.4) | ||||
|   capistrano-rbenv (~> 2.1) | ||||
|   capistrano-yarn (~> 2.0) | ||||
|   capybara (~> 3.24) | ||||
|   capybara (~> 3.29) | ||||
|   charlock_holmes (~> 0.7.6) | ||||
|   chewy (~> 5.0) | ||||
|   chewy (~> 5.1) | ||||
|   cld3 (~> 3.2.4) | ||||
|   climate_control (~> 0.2) | ||||
|   concurrent-ruby | ||||
|   connection_pool | ||||
|   derailed_benchmarks | ||||
|   devise (~> 4.6) | ||||
|   devise-two-factor (~> 3.0) | ||||
|   devise (~> 4.7) | ||||
|   devise-two-factor (~> 3.1) | ||||
|   devise_pam_authenticatable2 (~> 9.2) | ||||
|   doorkeeper (~> 5.1) | ||||
|   discard (~> 1.1) | ||||
|   doorkeeper (~> 5.2) | ||||
|   dotenv-rails (~> 2.7) | ||||
|   fabrication (~> 2.20) | ||||
|   faker (~> 1.9) | ||||
|   faker (~> 2.4) | ||||
|   fast_blank (~> 1.0) | ||||
|   fastimage | ||||
|   fog-core (<= 2.1.0) | ||||
| @ -681,6 +714,7 @@ DEPENDENCIES | ||||
|   fuubar (~> 2.4) | ||||
|   goldfinger (~> 2.1) | ||||
|   hamlit-rails (~> 0.2) | ||||
|   health_check! | ||||
|   hiredis (~> 0.6) | ||||
|   htmlentities (~> 4.3) | ||||
|   http (~> 3.3) | ||||
| @ -690,7 +724,7 @@ DEPENDENCIES | ||||
|   i18n-tasks (~> 0.9) | ||||
|   idn-ruby | ||||
|   iso-639 | ||||
|   json-ld (~> 3.0) | ||||
|   json-ld! | ||||
|   json-ld-preloaded (~> 3.0) | ||||
|   kaminari (~> 1.1) | ||||
|   letter_opener (~> 1.7) | ||||
| @ -701,11 +735,12 @@ DEPENDENCIES | ||||
|   mario-redis-lock (~> 1.2) | ||||
|   memory_profiler | ||||
|   microformats (~> 4.1) | ||||
|   mime-types (~> 3.2) | ||||
|   mime-types (~> 3.3) | ||||
|   net-ldap (~> 0.10) | ||||
|   nilsimsa! | ||||
|   nokogiri (~> 1.10) | ||||
|   nsa (~> 0.2) | ||||
|   oj (~> 3.7) | ||||
|   oj (~> 3.9) | ||||
|   omniauth (~> 1.9) | ||||
|   omniauth-cas (~> 1.1) | ||||
|   omniauth-saml (~> 1.10) | ||||
| @ -713,18 +748,20 @@ DEPENDENCIES | ||||
|   ox (~> 2.11) | ||||
|   paperclip (~> 6.0) | ||||
|   paperclip-av-transcoder (~> 0.6) | ||||
|   parallel (~> 1.17) | ||||
|   parallel_tests (~> 2.29) | ||||
|   parslet | ||||
|   pg (~> 1.1) | ||||
|   pghero (~> 2.2) | ||||
|   pghero (~> 2.3) | ||||
|   pkg-config (~> 1.3) | ||||
|   posix-spawn! | ||||
|   premailer-rails | ||||
|   private_address_check (~> 0.5) | ||||
|   pry-byebug (~> 3.7) | ||||
|   pry-rails (~> 0.3) | ||||
|   puma (~> 3.12) | ||||
|   pundit (~> 2.0) | ||||
|   rack-attack (~> 6.0) | ||||
|   puma (~> 4.2) | ||||
|   pundit (~> 2.1) | ||||
|   rack-attack (~> 6.1) | ||||
|   rack-cors (~> 1.0) | ||||
|   rails (~> 5.2.3) | ||||
|   rails-controller-testing (~> 1.0) | ||||
| @ -737,16 +774,17 @@ DEPENDENCIES | ||||
|   rqrcode (~> 0.10) | ||||
|   rspec-rails (~> 3.8) | ||||
|   rspec-sidekiq (~> 3.0) | ||||
|   rubocop (~> 0.71) | ||||
|   rubocop-rails (~> 2.0) | ||||
|   sanitize (~> 5.0) | ||||
|   rubocop (~> 0.74) | ||||
|   rubocop-rails (~> 2.3) | ||||
|   ruby-progressbar (~> 1.10) | ||||
|   sanitize (~> 5.1) | ||||
|   sidekiq (~> 5.2) | ||||
|   sidekiq-bulk (~> 0.2.0) | ||||
|   sidekiq-scheduler (~> 3.0) | ||||
|   sidekiq-unique-jobs (~> 6.0) | ||||
|   simple-navigation (~> 4.0) | ||||
|   simple-navigation (~> 4.1) | ||||
|   simple_form (~> 4.1) | ||||
|   simplecov (~> 0.16) | ||||
|   simplecov (~> 0.17) | ||||
|   sprockets-rails (~> 3.2) | ||||
|   stackprof | ||||
|   stoplight (~> 2.1.3) | ||||
| @ -757,12 +795,12 @@ DEPENDENCIES | ||||
|   tty-prompt (~> 0.19) | ||||
|   twitter-text (~> 1.14) | ||||
|   tzinfo-data (~> 1.2019) | ||||
|   webmock (~> 3.6) | ||||
|   webmock (~> 3.7) | ||||
|   webpacker (~> 4.0) | ||||
|   webpush | ||||
|  | ||||
| RUBY VERSION | ||||
|    ruby 2.6.1p33 | ||||
|    ruby 2.6.5p114 | ||||
|  | ||||
| BUNDLED WITH | ||||
|    1.17.3 | ||||
|  | ||||
							
								
								
									
										9
									
								
								app.json
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								app.json
									
									
									
									
									
								
							| @ -13,15 +13,6 @@ | ||||
|       "description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)", | ||||
|       "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": { | ||||
|       "description": "The secret key base", | ||||
|       "generator": "secret" | ||||
|  | ||||
							
								
								
									
										43
									
								
								app/chewy/accounts_index.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/chewy/accounts_index.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| # 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 | ||||
| @ -31,19 +31,19 @@ class StatusesIndex < Chewy::Index | ||||
|     }, | ||||
|   } | ||||
|  | ||||
|   define_type ::Status.unscoped.without_reblogs.includes(:media_attachments) do | ||||
|   define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments), delete_if: ->(status) { status.searchable_by.empty? } do | ||||
|     crutch :mentions do |collection| | ||||
|       data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) | ||||
|       data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id) | ||||
|       data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } | ||||
|     end | ||||
|  | ||||
|     crutch :favourites do |collection| | ||||
|       data = ::Favourite.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) | ||||
|       data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id) | ||||
|       data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } | ||||
|     end | ||||
|  | ||||
|     crutch :reblogs do |collection| | ||||
|       data = ::Status.where(reblog_of_id: collection.map(&:id)).pluck(:reblog_of_id, :account_id) | ||||
|       data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id) | ||||
|       data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } | ||||
|     end | ||||
|  | ||||
| @ -51,7 +51,7 @@ class StatusesIndex < Chewy::Index | ||||
|       field :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)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do | ||||
|         field :stemmed, type: 'text', analyzer: 'content' | ||||
|       end | ||||
|  | ||||
|  | ||||
							
								
								
									
										37
									
								
								app/chewy/tags_index.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/chewy/tags_index.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| # 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 | ||||
| @ -3,20 +3,46 @@ | ||||
| class AboutController < ApplicationController | ||||
|   layout 'public' | ||||
|  | ||||
|   before_action :set_instance_presenter, only: [:show, :more, :terms] | ||||
|   before_action :require_open_federation!, only: [:show, :more] | ||||
|   before_action :set_body_classes, only: :show | ||||
|   before_action :set_instance_presenter | ||||
|   before_action :set_expires_in, only: [:show, :more, :terms] | ||||
|  | ||||
|   skip_before_action :check_user_permissions, only: [:more, :terms] | ||||
|   skip_before_action :require_functional!, only: [:more, :terms] | ||||
|  | ||||
|   def show | ||||
|     @hide_navbar = true | ||||
|   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 | ||||
|  | ||||
|   def more; end | ||||
|  | ||||
|   def terms; end | ||||
|  | ||||
|   helper_method :display_blocks? | ||||
|   helper_method :display_blocks_rationale? | ||||
|   helper_method :public_fetch_mode? | ||||
|   helper_method :new_user | ||||
|  | ||||
|   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 | ||||
|     User.new.tap do |user| | ||||
|       user.build_account | ||||
| @ -24,9 +50,15 @@ class AboutController < ApplicationController | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   helper_method :new_user | ||||
|  | ||||
|   def set_instance_presenter | ||||
|     @instance_presenter = InstancePresenter.new | ||||
|   end | ||||
|  | ||||
|   def set_body_classes | ||||
|     @hide_navbar = true | ||||
|   end | ||||
|  | ||||
|   def set_expires_in | ||||
|     expires_in 0, public: true | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -4,17 +4,22 @@ class AccountsController < ApplicationController | ||||
|   PAGE_SIZE = 20 | ||||
|  | ||||
|   include AccountControllerConcern | ||||
|   include SignatureAuthentication | ||||
|  | ||||
|   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 | ||||
|     respond_to do |format| | ||||
|       format.html do | ||||
|         mark_cacheable! unless user_signed_in? | ||||
|         expires_in 0, public: true unless user_signed_in? | ||||
|  | ||||
|         @body_classes      = 'with-modals' | ||||
|         @pinned_statuses   = [] | ||||
|         @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) | ||||
|           @statuses = [] | ||||
| @ -24,6 +29,7 @@ class AccountsController < ApplicationController | ||||
|         @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? | ||||
|         @statuses        = filtered_status_page(params) | ||||
|         @statuses        = cache_collection(@statuses, Status) | ||||
|         @rss_url         = rss_url | ||||
|  | ||||
|         unless @statuses.empty? | ||||
|           @older_url = older_url if @statuses.last.id > filtered_statuses.last.id | ||||
| @ -31,30 +37,27 @@ class AccountsController < ApplicationController | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       format.atom do | ||||
|         mark_cacheable! | ||||
|  | ||||
|         @entries = @account.stream_entries.where(hidden: false).with_includes.without_local_only.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 | ||||
|  | ||||
|       format.rss do | ||||
|         mark_cacheable! | ||||
|         expires_in 1.minute, public: true | ||||
|  | ||||
|         @statuses = cache_collection(default_statuses.without_local_only.without_reblogs.without_replies.limit(PAGE_SIZE), Status) | ||||
|         render xml: RSS::AccountSerializer.render(@account, @statuses) | ||||
|         @statuses = filtered_statuses.without_local_only.without_reblogs.without_replies.limit(PAGE_SIZE) | ||||
|         @statuses = cache_collection(@statuses, Status) | ||||
|         render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag]) | ||||
|       end | ||||
|  | ||||
|       format.json do | ||||
|         render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do | ||||
|           ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) | ||||
|         end | ||||
|         expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?) | ||||
|         render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def set_body_classes | ||||
|     @body_classes = 'with-modals' | ||||
|   end | ||||
|  | ||||
|   def show_pinned_statuses? | ||||
|     [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? | ||||
|   end | ||||
| @ -101,6 +104,14 @@ class AccountsController < ApplicationController | ||||
|     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 | ||||
|  | ||||
|   def older_url | ||||
|     pagination_url(max_id: @statuses.last.id) | ||||
|   end | ||||
| @ -122,15 +133,15 @@ class AccountsController < ApplicationController | ||||
|   end | ||||
|  | ||||
|   def media_requested? | ||||
|     request.path.ends_with?('/media') | ||||
|     request.path.ends_with?('/media') && !tag_requested? | ||||
|   end | ||||
|  | ||||
|   def replies_requested? | ||||
|     request.path.ends_with?('/with_replies') | ||||
|     request.path.ends_with?('/with_replies') && !tag_requested? | ||||
|   end | ||||
|  | ||||
|   def tag_requested? | ||||
|     request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) | ||||
|     request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) | ||||
|   end | ||||
|  | ||||
|   def filtered_status_page(params) | ||||
| @ -140,4 +151,12 @@ class AccountsController < ApplicationController | ||||
|       filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a | ||||
|     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 | ||||
|  | ||||
							
								
								
									
										11
									
								
								app/controllers/activitypub/base_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/controllers/activitypub/base_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| # 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 | ||||
| @ -1,30 +1,21 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class ActivityPub::CollectionsController < Api::BaseController | ||||
| class ActivityPub::CollectionsController < ActivityPub::BaseController | ||||
|   include SignatureVerification | ||||
|   include AccountOwnedConcern | ||||
|  | ||||
|   before_action :set_account | ||||
|   before_action :require_signature!, if: :authorized_fetch_mode? | ||||
|   before_action :set_size | ||||
|   before_action :set_statuses | ||||
|   before_action :set_cache_headers | ||||
|  | ||||
|   def show | ||||
|     render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do | ||||
|       ActiveModelSerializers::SerializableResource.new( | ||||
|         collection_presenter, | ||||
|         serializer: ActivityPub::CollectionSerializer, | ||||
|         adapter: ActivityPub::Adapter, | ||||
|         skip_activities: true | ||||
|       ) | ||||
|     end | ||||
|     expires_in 3.minutes, public: public_fetch_mode? | ||||
|     render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def set_account | ||||
|     @account = Account.find_local!(params[:account_username]) | ||||
|   end | ||||
|  | ||||
|   def set_statuses | ||||
|     @statuses = scope_for_collection | ||||
|     @statuses = cache_collection(@statuses, Status) | ||||
| @ -42,9 +33,9 @@ class ActivityPub::CollectionsController < Api::BaseController | ||||
|   def scope_for_collection | ||||
|     case params[:id] | ||||
|     when 'featured' | ||||
|       @account.statuses.permitted_for(@account, signed_request_account).tap do |scope| | ||||
|         scope.merge!(@account.pinned_statuses) | ||||
|       end | ||||
|       return Status.none if @account.blocking?(signed_request_account) | ||||
|  | ||||
|       @account.pinned_statuses | ||||
|     else | ||||
|       raise ActiveRecord::RecordNotFound | ||||
|     end | ||||
|  | ||||
| @ -1,40 +1,44 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class ActivityPub::InboxesController < Api::BaseController | ||||
| class ActivityPub::InboxesController < ActivityPub::BaseController | ||||
|   include SignatureVerification | ||||
|   include JsonLdHelper | ||||
|   include AccountOwnedConcern | ||||
|  | ||||
|   before_action :set_account | ||||
|   before_action :skip_unknown_actor_delete | ||||
|   before_action :require_signature! | ||||
|  | ||||
|   def create | ||||
|     if unknown_deleted_account? | ||||
|       head 202 | ||||
|     elsif signed_request_account | ||||
|       upgrade_account | ||||
|       process_payload | ||||
|       head 202 | ||||
|     else | ||||
|       render plain: signature_verification_failure_reason, status: 401 | ||||
|     end | ||||
|     upgrade_account | ||||
|     process_payload | ||||
|     head 202 | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def skip_unknown_actor_delete | ||||
|     head 202 if unknown_deleted_account? | ||||
|   end | ||||
|  | ||||
|   def unknown_deleted_account? | ||||
|     json = Oj.load(body, mode: :strict) | ||||
|     json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? | ||||
|     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 set_account | ||||
|     @account = Account.find_local!(params[:account_username]) if params[:account_username] | ||||
|   def account_required? | ||||
|     params[:account_username].present? | ||||
|   end | ||||
|  | ||||
|   def body | ||||
|     return @body if defined?(@body) | ||||
|     @body = request.body.read.force_encoding('UTF-8') | ||||
|  | ||||
|     @body = request.body.read | ||||
|     @body.force_encoding('UTF-8') if @body.present? | ||||
|  | ||||
|     request.body.rewind if request.body.respond_to?(:rewind) | ||||
|  | ||||
|     @body | ||||
|   end | ||||
|  | ||||
| @ -44,7 +48,6 @@ class ActivityPub::InboxesController < Api::BaseController | ||||
|       ResolveAccountWorker.perform_async(signed_request_account.acct) | ||||
|     end | ||||
|  | ||||
|     Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed? | ||||
|     DeliveryFailureTracker.track_inverse_success!(signed_request_account) | ||||
|   end | ||||
|  | ||||
|  | ||||
| @ -1,26 +1,22 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class ActivityPub::OutboxesController < Api::BaseController | ||||
| class ActivityPub::OutboxesController < ActivityPub::BaseController | ||||
|   LIMIT = 20 | ||||
|  | ||||
|   include SignatureVerification | ||||
|   include AccountOwnedConcern | ||||
|  | ||||
|   before_action :set_account | ||||
|   before_action :require_signature!, if: :authorized_fetch_mode? | ||||
|   before_action :set_statuses | ||||
|   before_action :set_cache_headers | ||||
|  | ||||
|   def show | ||||
|     expires_in 1.minute, public: true unless page_requested? | ||||
|  | ||||
|     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' | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def set_account | ||||
|     @account = Account.find_local!(params[:account_username]) | ||||
|   end | ||||
|  | ||||
|   def outbox_presenter | ||||
|     if page_requested? | ||||
|       ActivityPub::CollectionPresenter.new( | ||||
|  | ||||
							
								
								
									
										71
									
								
								app/controllers/activitypub/replies_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								app/controllers/activitypub/replies_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| # 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 | ||||
| @ -5,7 +5,7 @@ module Admin | ||||
|     before_action :set_account | ||||
|  | ||||
|     def new | ||||
|       @account_action  = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true) | ||||
|       @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 | ||||
|  | ||||
| @ -30,7 +30,7 @@ module Admin | ||||
|     end | ||||
|  | ||||
|     def resource_params | ||||
|       params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification) | ||||
|       params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -2,8 +2,8 @@ | ||||
|  | ||||
| module Admin | ||||
|   class AccountsController < BaseController | ||||
|     before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] | ||||
|     before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] | ||||
|     before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] | ||||
|     before_action :require_remote_account!, only: [:redownload] | ||||
|     before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] | ||||
|  | ||||
|     def index | ||||
| @ -19,18 +19,6 @@ module Admin | ||||
|       @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 | ||||
|  | ||||
|     def memorialize | ||||
|       authorize @account, :memorialize? | ||||
|       @account.memorialize! | ||||
| @ -53,7 +41,7 @@ module Admin | ||||
|  | ||||
|     def reject | ||||
|       authorize @account.user, :reject? | ||||
|       SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) | ||||
|       SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) | ||||
|       redirect_to admin_pending_accounts_path | ||||
|     end | ||||
|  | ||||
|  | ||||
| @ -2,19 +2,20 @@ | ||||
|  | ||||
| module Admin | ||||
|   class CustomEmojisController < BaseController | ||||
|     before_action :set_custom_emoji, except: [:index, :new, :create] | ||||
|     before_action :set_filter_params | ||||
|  | ||||
|     include ObfuscateFilename | ||||
|  | ||||
|     obfuscate_filename [:custom_emoji, :image] | ||||
|  | ||||
|     def index | ||||
|       authorize :custom_emoji, :index? | ||||
|  | ||||
|       @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) | ||||
|       @form          = Form::CustomEmojiBatch.new | ||||
|     end | ||||
|  | ||||
|     def new | ||||
|       authorize :custom_emoji, :create? | ||||
|  | ||||
|       @custom_emoji = CustomEmoji.new | ||||
|     end | ||||
|  | ||||
| @ -31,69 +32,17 @@ module Admin | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     def update | ||||
|       authorize @custom_emoji, :update? | ||||
|  | ||||
|       if @custom_emoji.update(resource_params) | ||||
|         log_action :update, @custom_emoji | ||||
|         flash[:notice] = I18n.t('admin.custom_emojis.updated_msg') | ||||
|       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) | ||||
|     def batch | ||||
|       @form = Form::CustomEmojiBatch.new(form_custom_emoji_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_custom_emojis_path(filter_params) | ||||
|     end | ||||
|  | ||||
|     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 | ||||
|       params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker) | ||||
|     end | ||||
| @ -103,12 +52,29 @@ module Admin | ||||
|     end | ||||
|  | ||||
|     def filter_params | ||||
|       params.permit( | ||||
|         :local, | ||||
|         :remote, | ||||
|         :by_domain, | ||||
|         :shortcode | ||||
|       ) | ||||
|       params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page) | ||||
|     end | ||||
|  | ||||
|     def action_from_button | ||||
|       if params[:update] | ||||
|         '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 | ||||
|  | ||||
| @ -5,6 +5,7 @@ module Admin | ||||
|   class DashboardController < BaseController | ||||
|     def index | ||||
|       @users_count           = User.count | ||||
|       @pending_users_count   = User.pending.count | ||||
|       @registrations_week    = Redis.current.get("activity:accounts:local:#{current_week}") || 0 | ||||
|       @logins_week           = Redis.current.pfcount("activity:logins:#{current_week}") | ||||
|       @interactions_week     = Redis.current.get("activity:interactions:#{current_week}") || 0 | ||||
| @ -19,7 +20,7 @@ module Admin | ||||
|       @redis_version         = redis_info['redis_version'] | ||||
|       @reports_count         = Report.unresolved.count | ||||
|       @queue_backlog         = Sidekiq::Stats.new.enqueued | ||||
|       @recent_users          = User.confirmed.recent.includes(:account).limit(4) | ||||
|       @recent_users          = User.confirmed.recent.includes(:account).limit(8) | ||||
|       @database_size         = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size'] | ||||
|       @redis_size            = redis_info['used_memory'] | ||||
|       @ldap_enabled          = ENV['LDAP_ENABLED'] == 'true' | ||||
| @ -27,9 +28,14 @@ module Admin | ||||
|       @saml_enabled          = ENV['SAML_ENABLED'] == 'true' | ||||
|       @pam_enabled           = ENV['PAM_ENABLED'] == 'true' | ||||
|       @hidden_service        = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' | ||||
|       @trending_hashtags     = TrendingTags.get(7) | ||||
|       @trending_hashtags     = TrendingTags.get(10, filtered: false) | ||||
|       @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 | ||||
|  | ||||
|     private | ||||
| @ -39,7 +45,13 @@ module Admin | ||||
|     end | ||||
|  | ||||
|     def redis_info | ||||
|       @redis_info ||= Redis.current.info | ||||
|       @redis_info ||= begin | ||||
|         if Redis.current.is_a?(Redis::Namespace) | ||||
|           Redis.current.redis.info | ||||
|         else | ||||
|           Redis.current.info | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
							
								
								
									
										40
									
								
								app/controllers/admin/domain_allows_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/controllers/admin/domain_allows_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| # 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 | ||||
| @ -2,13 +2,17 @@ | ||||
|  | ||||
| module Admin | ||||
|   class DomainBlocksController < BaseController | ||||
|     before_action :set_domain_block, only: [:show, :destroy] | ||||
|     before_action :set_domain_block, only: [:show, :destroy, :edit, :update] | ||||
|  | ||||
|     def new | ||||
|       authorize :domain_block, :create? | ||||
|       @domain_block = DomainBlock.new(domain: params[:_domain]) | ||||
|     end | ||||
|  | ||||
|     def edit | ||||
|       authorize :domain_block, :create? | ||||
|     end | ||||
|  | ||||
|     def create | ||||
|       authorize :domain_block, :create? | ||||
|  | ||||
| @ -35,6 +39,22 @@ module Admin | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     def update | ||||
|       authorize :domain_block, :create? | ||||
|  | ||||
|       @domain_block.update(update_params) | ||||
|  | ||||
|       severity_changed = @domain_block.severity_changed? | ||||
|  | ||||
|       if @domain_block.save | ||||
|         DomainBlockWorker.perform_async(@domain_block.id, severity_changed) | ||||
|         log_action :create, @domain_block | ||||
|         redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') | ||||
|       else | ||||
|         render :edit | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     def show | ||||
|       authorize @domain_block, :show? | ||||
|     end | ||||
| @ -52,8 +72,12 @@ module Admin | ||||
|       @domain_block = DomainBlock.find(params[:id]) | ||||
|     end | ||||
|  | ||||
|     def update_params | ||||
|       params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment) | ||||
|     end | ||||
|  | ||||
|     def resource_params | ||||
|       params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports) | ||||
|       params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -2,6 +2,10 @@ | ||||
|  | ||||
| module Admin | ||||
|   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 | ||||
|       authorize :instance, :index? | ||||
|  | ||||
| @ -11,20 +15,40 @@ module Admin | ||||
|     def show | ||||
|       authorize :instance, :show? | ||||
|  | ||||
|       @instance        = Instance.new(Account.by_domain_accounts.find_by(domain: params[:id]) || DomainBlock.find_by!(domain: params[:id])) | ||||
|       @following_count = Follow.where(account: Account.where(domain: params[:id])).count | ||||
|       @followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count | ||||
|       @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) | ||||
|       @domain_block    = DomainBlock.rule_for(params[:id]) | ||||
|       @private_comment = @domain_block&.private_comment | ||||
|       @public_comment  = @domain_block&.public_comment | ||||
|     end | ||||
|  | ||||
|     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 | ||||
|       InstanceFilter.new(filter_params).results | ||||
|       InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results | ||||
|     end | ||||
|  | ||||
|     def paginated_instances | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| module Admin | ||||
|   class RelaysController < BaseController | ||||
|     before_action :set_relay, except: [:index, :new, :create] | ||||
|     before_action :require_signatures_enabled!, only: [:new, :create, :enable] | ||||
|  | ||||
|     def index | ||||
|       authorize :relay, :update? | ||||
| @ -11,7 +12,7 @@ module Admin | ||||
|  | ||||
|     def new | ||||
|       authorize :relay, :update? | ||||
|       @relay = Relay.new(inbox_url: Relay::PRESET_RELAY) | ||||
|       @relay = Relay.new | ||||
|     end | ||||
|  | ||||
|     def create | ||||
| @ -54,5 +55,9 @@ module Admin | ||||
|     def resource_params | ||||
|       params.require(:relay).permit(:inbox_url) | ||||
|     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 | ||||
|  | ||||
| @ -5,10 +5,10 @@ module Admin | ||||
|     before_action :set_report_note, only: [:destroy] | ||||
|  | ||||
|     def create | ||||
|       authorize ReportNote, :create? | ||||
|       authorize :report_note, :create? | ||||
|  | ||||
|       @report_note = current_account.report_notes.new(resource_params) | ||||
|       @report = @report_note.report | ||||
|       @report      = @report_note.report | ||||
|  | ||||
|       if @report_note.save | ||||
|         if params[:create_and_resolve] | ||||
| @ -26,9 +26,8 @@ module Admin | ||||
|  | ||||
|         redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg') | ||||
|       else | ||||
|         @report_notes = @report.notes.latest | ||||
|         @report_history = @report.history | ||||
|         @form = Form::StatusBatch.new | ||||
|         @report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at) | ||||
|         @form         = Form::StatusBatch.new | ||||
|  | ||||
|         render template: 'admin/reports/show' | ||||
|       end | ||||
|  | ||||
| @ -2,43 +2,102 @@ | ||||
|  | ||||
| module Admin | ||||
|   class TagsController < BaseController | ||||
|     before_action :set_tags, only: :index | ||||
|     before_action :set_tag, except: :index | ||||
|     before_action :set_filter_params | ||||
|     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 hide | ||||
|       authorize @tag, :hide? | ||||
|       @tag.account_tag_stat.update!(hidden: true) | ||||
|       redirect_to admin_tags_path(@filter_params) | ||||
|     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 unhide | ||||
|       authorize @tag, :unhide? | ||||
|       @tag.account_tag_stat.update!(hidden: false) | ||||
|       redirect_to admin_tags_path(@filter_params) | ||||
|     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_tags | ||||
|       @tags = Tag.discoverable | ||||
|       @tags.merge!(Tag.hidden) if filter_params[:hidden] | ||||
|     end | ||||
|  | ||||
|     def set_tag | ||||
|       @tag = Tag.find(params[:id]) | ||||
|     end | ||||
|  | ||||
|     def set_filter_params | ||||
|       @filter_params = filter_params.to_hash.symbolize_keys | ||||
|     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.permit(:hidden) | ||||
|       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 | ||||
|  | ||||
| @ -8,6 +8,7 @@ module Admin | ||||
|       authorize @user, :disable_2fa? | ||||
|       @user.disable_two_factor! | ||||
|       log_action :disable_2fa, @user | ||||
|       UserMailer.two_factor_disabled(@user).deliver_later! | ||||
|       redirect_to admin_accounts_path | ||||
|     end | ||||
|  | ||||
|  | ||||
| @ -7,12 +7,15 @@ class Api::BaseController < ApplicationController | ||||
|   include RateLimitHeaders | ||||
|  | ||||
|   skip_before_action :store_current_location | ||||
|   skip_before_action :check_user_permissions | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? | ||||
|   before_action :set_cache_headers | ||||
|  | ||||
|   protect_from_forgery with: :null_session | ||||
|  | ||||
|   skip_around_action :set_locale | ||||
|  | ||||
|   rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| | ||||
|     render json: { error: e.to_s }, status: 422 | ||||
|   end | ||||
| @ -33,6 +36,14 @@ class Api::BaseController < ApplicationController | ||||
|     render json: { error: 'This action is not allowed' }, status: 403 | ||||
|   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) | ||||
|     { json: { error: (error.try(:description) || 'Not authorized') } } | ||||
|   end | ||||
| @ -69,6 +80,10 @@ class Api::BaseController < ApplicationController | ||||
|     nil | ||||
|   end | ||||
|  | ||||
|   def require_authenticated_user! | ||||
|     render json: { error: 'This API requires an authenticated user' }, status: 401 unless current_user | ||||
|   end | ||||
|  | ||||
|   def require_user! | ||||
|     if !current_user | ||||
|       render json: { error: 'This method requires an authenticated user' }, status: 422 | ||||
| @ -94,4 +109,8 @@ class Api::BaseController < ApplicationController | ||||
|   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 | ||||
|  | ||||
| @ -1,10 +1,9 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Api::ProofsController < Api::BaseController | ||||
|   before_action :set_account | ||||
|   include AccountOwnedConcern | ||||
|  | ||||
|   before_action :set_provider | ||||
|   before_action :check_account_approval | ||||
|   before_action :check_account_suspension | ||||
|  | ||||
|   def index | ||||
|     render json: @account, serializer: @provider.serializer_class | ||||
| @ -16,15 +15,7 @@ class Api::ProofsController < Api::BaseController | ||||
|     @provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound) | ||||
|   end | ||||
|  | ||||
|   def set_account | ||||
|     @account = Account.find_local!(params[:username]) | ||||
|   end | ||||
|  | ||||
|   def check_account_approval | ||||
|     not_found if @account.user_pending? | ||||
|   end | ||||
|  | ||||
|   def check_account_suspension | ||||
|     gone if @account.suspended? | ||||
|   def username_param | ||||
|     params[:username] | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -1,73 +0,0 @@ | ||||
| # 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 | ||||
| @ -1,37 +0,0 @@ | ||||
| # 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 | ||||
| @ -1,51 +0,0 @@ | ||||
| # 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 | ||||
| @ -29,14 +29,13 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | ||||
|  | ||||
|   def 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!(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 | ||||
|     statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id)) | ||||
|   end | ||||
|  | ||||
|   def permitted_account_statuses | ||||
| @ -58,6 +57,8 @@ class Api::V1::Accounts::StatusesController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def pinned_scope | ||||
|     return Status.none if @account.blocking?(current_account) | ||||
|  | ||||
|     @account.pinned_statuses | ||||
|   end | ||||
|  | ||||
|  | ||||
| @ -12,6 +12,8 @@ class Api::V1::AccountsController < Api::BaseController | ||||
|   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 | ||||
|  | ||||
|   def show | ||||
| @ -31,7 +33,7 @@ class Api::V1::AccountsController < Api::BaseController | ||||
|   def follow | ||||
|     FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs)) | ||||
|  | ||||
|     options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } | ||||
|     options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } | ||||
|  | ||||
|     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) | ||||
|   end | ||||
| @ -76,7 +78,7 @@ class Api::V1::AccountsController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def account_params | ||||
|     params.permit(:username, :email, :password, :agreement, :locale) | ||||
|     params.permit(:username, :email, :password, :agreement, :locale, :reason) | ||||
|   end | ||||
|  | ||||
|   def check_enabled_registrations | ||||
|  | ||||
| @ -58,7 +58,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController | ||||
|  | ||||
|   def reject | ||||
|     authorize @account.user, :reject? | ||||
|     SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) | ||||
|     SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) | ||||
|     render json: @account, serializer: REST::Admin::AccountSerializer | ||||
|   end | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Api::V1::AppsController < Api::BaseController | ||||
|   skip_before_action :require_authenticated_user! | ||||
|  | ||||
|   def create | ||||
|     @app = Doorkeeper::Application.create!(application_options) | ||||
|     render json: @app, serializer: REST::ApplicationSerializer | ||||
|  | ||||
| @ -6,8 +6,7 @@ class Api::V1::CustomEmojisController < Api::BaseController | ||||
|   skip_before_action :set_cache_headers | ||||
|  | ||||
|   def index | ||||
|     render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do | ||||
|       ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer) | ||||
|     end | ||||
|     expires_in 3.minutes, public: true | ||||
|     render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) } | ||||
|   end | ||||
| end | ||||
|  | ||||
							
								
								
									
										30
									
								
								app/controllers/api/v1/directories_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/controllers/api/v1/directories_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| # 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 | ||||
| @ -0,0 +1,20 @@ | ||||
| # 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 | ||||
							
								
								
									
										40
									
								
								app/controllers/api/v1/featured_tags_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/controllers/api/v1/featured_tags_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| # 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 | ||||
| @ -14,12 +14,12 @@ class Api::V1::FollowRequestsController < Api::BaseController | ||||
|   def authorize | ||||
|     AuthorizeFollowService.new.call(account, current_account) | ||||
|     NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account)) | ||||
|     render_empty | ||||
|     render json: account, serializer: REST::RelationshipSerializer, relationships: relationships | ||||
|   end | ||||
|  | ||||
|   def reject | ||||
|     RejectFollowService.new.call(account, current_account) | ||||
|     render_empty | ||||
|     render json: account, serializer: REST::RelationshipSerializer, relationships: relationships | ||||
|   end | ||||
|  | ||||
|   private | ||||
| @ -28,6 +28,10 @@ class Api::V1::FollowRequestsController < Api::BaseController | ||||
|     Account.find(params[:id]) | ||||
|   end | ||||
|  | ||||
|   def relationships(**options) | ||||
|     AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, options) | ||||
|   end | ||||
|  | ||||
|   def load_accounts | ||||
|     default_accounts.merge(paginated_follow_requests).to_a | ||||
|   end | ||||
|  | ||||
| @ -1,31 +0,0 @@ | ||||
| # 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 | ||||
| @ -2,12 +2,14 @@ | ||||
|  | ||||
| class Api::V1::Instances::ActivityController < Api::BaseController | ||||
|   before_action :require_enabled_api! | ||||
|  | ||||
|   skip_before_action :set_cache_headers | ||||
|  | ||||
|   respond_to :json | ||||
|  | ||||
|   def show | ||||
|     render_cached_json('api:v1:instances:activity:show', expires_in: 1.day) { activity } | ||||
|     expires_in 1.day, public: true | ||||
|     render_with_cache json: :activity, expires_in: 1.day | ||||
|   end | ||||
|  | ||||
|   private | ||||
| @ -32,6 +34,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def require_enabled_api! | ||||
|     head 404 unless Setting.activity_api_enabled | ||||
|     head 404 unless Setting.activity_api_enabled && !whitelist_mode? | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -2,17 +2,19 @@ | ||||
|  | ||||
| class Api::V1::Instances::PeersController < Api::BaseController | ||||
|   before_action :require_enabled_api! | ||||
|  | ||||
|   skip_before_action :set_cache_headers | ||||
|  | ||||
|   respond_to :json | ||||
|  | ||||
|   def index | ||||
|     render_cached_json('api:v1:instances:peers:index', expires_in: 1.day) { Account.remote.domains } | ||||
|     expires_in 1.day, public: true | ||||
|     render_with_cache(expires_in: 1.day) { Account.remote.domains } | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def require_enabled_api! | ||||
|     head 404 unless Setting.peers_api_enabled | ||||
|     head 404 unless Setting.peers_api_enabled && !whitelist_mode? | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -2,11 +2,11 @@ | ||||
|  | ||||
| class Api::V1::InstancesController < Api::BaseController | ||||
|   respond_to :json | ||||
|  | ||||
|   skip_before_action :set_cache_headers | ||||
|  | ||||
|   def show | ||||
|     render_cached_json('api:v1:instances', expires_in: 5.minutes) do | ||||
|       ActiveModelSerializers::SerializableResource.new({}, serializer: REST::InstanceSerializer) | ||||
|     end | ||||
|     expires_in 3.minutes, public: true | ||||
|     render_with_cache json: {}, serializer: REST::InstanceSerializer, root: 'instance' | ||||
|   end | ||||
| end | ||||
|  | ||||
							
								
								
									
										44
									
								
								app/controllers/api/v1/markers_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/controllers/api/v1/markers_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| # 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 | ||||
| @ -21,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController | ||||
|   private | ||||
|  | ||||
|   def reported_status_ids | ||||
|     reported_account.statuses.find(status_ids).pluck(:id) | ||||
|     reported_account.statuses.with_discarded.find(status_ids).pluck(:id) | ||||
|   end | ||||
|  | ||||
|   def status_ids | ||||
|  | ||||
| @ -1,32 +0,0 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Api::V1::SearchController < Api::BaseController | ||||
|   include Authorization | ||||
|  | ||||
|   RESULTS_LIMIT = 20 | ||||
|  | ||||
|   before_action -> { doorkeeper_authorize! :read, :'read:search' } | ||||
|   before_action :require_user! | ||||
|  | ||||
|   respond_to :json | ||||
|  | ||||
|   def index | ||||
|     @search = Search.new(search_results) | ||||
|     render json: @search, serializer: REST::SearchSerializer | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def search_results | ||||
|     SearchService.new.call( | ||||
|       params[:q], | ||||
|       current_account, | ||||
|       limit_param(RESULTS_LIMIT), | ||||
|       search_params.merge(resolve: truthy_param?(:resolve)) | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def search_params | ||||
|     params.permit(:type, :offset, :min_id, :max_id, :account_id) | ||||
|   end | ||||
| end | ||||
| @ -18,6 +18,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController | ||||
|     @reblogs_map = { @status.id => false } | ||||
|  | ||||
|     authorize status_for_destroy, :unreblog? | ||||
|     status_for_destroy.discard | ||||
|     RemovalWorker.perform_async(status_for_destroy.id) | ||||
|  | ||||
|     render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map) | ||||
| @ -30,7 +31,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController | ||||
|   end | ||||
|  | ||||
|   def status_for_destroy | ||||
|     current_user.account.statuses.where(reblog_of_id: params[:status_id]).first! | ||||
|     @status_for_destroy ||= current_user.account.statuses.where(reblog_of_id: params[:status_id]).first! | ||||
|   end | ||||
|  | ||||
|   def reblog_params | ||||
|  | ||||
| @ -5,8 +5,8 @@ class Api::V1::StatusesController < Api::BaseController | ||||
|  | ||||
|   before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy] | ||||
|   before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only:   [:create, :destroy] | ||||
|   before_action :require_user!, except:  [:show, :context, :card] | ||||
|   before_action :set_status, only:       [:show, :context, :card] | ||||
|   before_action :require_user!, except:  [:show, :context] | ||||
|   before_action :set_status, only:       [:show, :context] | ||||
|  | ||||
|   respond_to :json | ||||
|  | ||||
| @ -33,16 +33,6 @@ class Api::V1::StatusesController < Api::BaseController | ||||
|     render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) | ||||
|   end | ||||
|  | ||||
|   def card | ||||
|     @card = @status.preview_cards.first | ||||
|  | ||||
|     if @card.nil? | ||||
|       render_empty | ||||
|     else | ||||
|       render json: @card, serializer: REST::PreviewCardSerializer | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|     @status = PostStatusService.new.call(current_user.account, | ||||
|                                          text: status_params[:status], | ||||
| @ -64,7 +54,8 @@ class Api::V1::StatusesController < Api::BaseController | ||||
|     @status = Status.where(account_id: current_user.account).find(params[:id]) | ||||
|     authorize @status, :destroy? | ||||
|  | ||||
|     RemovalWorker.perform_async(@status.id) | ||||
|     @status.discard | ||||
|     RemovalWorker.perform_async(@status.id, redraft: true) | ||||
|  | ||||
|     render json: @status, serializer: REST::StatusSerializer, source_requested: true | ||||
|   end | ||||
|  | ||||
| @ -1,63 +0,0 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Api::V1::Timelines::DirectController < Api::BaseController | ||||
|   before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show] | ||||
|   before_action :require_user!, only: [:show] | ||||
|   after_action :insert_pagination_headers, unless: -> { @statuses.empty? } | ||||
|  | ||||
|   respond_to :json | ||||
|  | ||||
|   def show | ||||
|     @statuses = load_statuses | ||||
|     render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def load_statuses | ||||
|     cached_direct_statuses | ||||
|   end | ||||
|  | ||||
|   def cached_direct_statuses | ||||
|     cache_collection direct_statuses, Status | ||||
|   end | ||||
|  | ||||
|   def direct_statuses | ||||
|     direct_timeline_statuses | ||||
|   end | ||||
|  | ||||
|   def direct_timeline_statuses | ||||
|     # this query requires built in pagination. | ||||
|     Status.as_direct_timeline( | ||||
|       current_account, | ||||
|       limit_param(DEFAULT_STATUSES_LIMIT), | ||||
|       params[:max_id], | ||||
|       params[:since_id], | ||||
|       true # returns array of cache_ids object | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def insert_pagination_headers | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|   end | ||||
|  | ||||
|   def pagination_params(core_params) | ||||
|     params.permit(:local, :limit).merge(core_params) | ||||
|   end | ||||
|  | ||||
|   def next_path | ||||
|     api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id) | ||||
|   end | ||||
|  | ||||
|   def prev_path | ||||
|     api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id) | ||||
|   end | ||||
|  | ||||
|   def pagination_max_id | ||||
|     @statuses.last.id | ||||
|   end | ||||
|  | ||||
|   def pagination_since_id | ||||
|     @statuses.first.id | ||||
|   end | ||||
| end | ||||
| @ -1,6 +1,7 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Api::V1::Timelines::PublicController < Api::BaseController | ||||
|   before_action :require_user!, only: [:show], if: :require_auth? | ||||
|   after_action :insert_pagination_headers, unless: -> { @statuses.empty? } | ||||
|  | ||||
|   respond_to :json | ||||
| @ -12,6 +13,10 @@ class Api::V1::Timelines::PublicController < Api::BaseController | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def require_auth? | ||||
|     !Setting.timeline_preview | ||||
|   end | ||||
|  | ||||
|   def load_statuses | ||||
|     cached_public_statuses | ||||
|   end | ||||
|  | ||||
							
								
								
									
										17
									
								
								app/controllers/api/v1/trends_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/controllers/api/v1/trends_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Api::V1::TrendsController < Api::BaseController | ||||
|   before_action :set_tags | ||||
|  | ||||
|   respond_to :json | ||||
|  | ||||
|   def index | ||||
|     render json: @tags, each_serializer: REST::TagSerializer | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def set_tags | ||||
|     @tags = TrendingTags.get(limit_param(10)) | ||||
|   end | ||||
| end | ||||
| @ -1,8 +1,32 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Api::V2::SearchController < Api::V1::SearchController | ||||
| class Api::V2::SearchController < Api::BaseController | ||||
|   include Authorization | ||||
|  | ||||
|   RESULTS_LIMIT = 20 | ||||
|  | ||||
|   before_action -> { doorkeeper_authorize! :read, :'read:search' } | ||||
|   before_action :require_user! | ||||
|  | ||||
|   respond_to :json | ||||
|  | ||||
|   def index | ||||
|     @search = Search.new(search_results) | ||||
|     render json: @search, serializer: REST::V2::SearchSerializer | ||||
|     render json: @search, serializer: REST::SearchSerializer | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def search_results | ||||
|     SearchService.new.call( | ||||
|       params[:q], | ||||
|       current_account, | ||||
|       limit_param(RESULTS_LIMIT), | ||||
|       search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed)) | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def search_params | ||||
|     params.permit(:type, :offset, :min_id, :max_id, :account_id) | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -10,21 +10,29 @@ class ApplicationController < ActionController::Base | ||||
|   include Localized | ||||
|   include UserTrackingConcern | ||||
|   include SessionTrackingConcern | ||||
|   include CacheConcern | ||||
|   include DomainControlHelper | ||||
|  | ||||
|   helper_method :current_account | ||||
|   helper_method :current_session | ||||
|   helper_method :current_theme | ||||
|   helper_method :single_user_mode? | ||||
|   helper_method :use_seamless_external_login? | ||||
|   helper_method :whitelist_mode? | ||||
|  | ||||
|   rescue_from ActionController::RoutingError, with: :not_found | ||||
|   rescue_from ActiveRecord::RecordNotFound, with: :not_found | ||||
|   rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity | ||||
|   rescue_from ActionController::UnknownFormat, with: :not_acceptable | ||||
|   rescue_from ActionController::ParameterMissing, with: :bad_request | ||||
|   rescue_from ActiveRecord::RecordNotFound, with: :not_found | ||||
|   rescue_from Mastodon::NotPermittedError, with: :forbidden | ||||
|   rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error | ||||
|   rescue_from Mastodon::RaceConditionError, with: :service_unavailable | ||||
|  | ||||
|   before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? | ||||
|   before_action :check_user_permissions, if: :user_signed_in? | ||||
|   before_action :require_functional!, if: :user_signed_in? | ||||
|  | ||||
|   skip_before_action :verify_authenticity_token, only: :raise_not_found | ||||
|  | ||||
|   def raise_not_found | ||||
|     raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}" | ||||
| @ -33,7 +41,15 @@ class ApplicationController < ActionController::Base | ||||
|   private | ||||
|  | ||||
|   def https_enabled? | ||||
|     Rails.env.production? | ||||
|     Rails.env.production? && !request.path.start_with?('/health') | ||||
|   end | ||||
|  | ||||
|   def authorized_fetch_mode? | ||||
|     ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode | ||||
|   end | ||||
|  | ||||
|   def public_fetch_mode? | ||||
|     !authorized_fetch_mode? | ||||
|   end | ||||
|  | ||||
|   def store_current_location | ||||
| @ -48,8 +64,8 @@ class ApplicationController < ActionController::Base | ||||
|     forbidden unless current_user&.staff? | ||||
|   end | ||||
|  | ||||
|   def check_user_permissions | ||||
|     forbidden if current_user.disabled? || current_user.account.suspended? | ||||
|   def require_functional! | ||||
|     redirect_to edit_user_registration_path unless current_user.functional? | ||||
|   end | ||||
|  | ||||
|   def after_sign_out_path_for(_resource_or_scope) | ||||
| @ -82,8 +98,20 @@ class ApplicationController < ActionController::Base | ||||
|     respond_with_error(406) | ||||
|   end | ||||
|  | ||||
|   def bad_request | ||||
|     respond_with_error(400) | ||||
|   end | ||||
|  | ||||
|   def internal_server_error | ||||
|     respond_with_error(500) | ||||
|   end | ||||
|  | ||||
|   def service_unavailable | ||||
|     respond_with_error(503) | ||||
|   end | ||||
|  | ||||
|   def single_user_mode? | ||||
|     @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists? | ||||
|     @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? | ||||
|   end | ||||
|  | ||||
|   def use_seamless_external_login? | ||||
| @ -107,51 +135,10 @@ class ApplicationController < ActionController::Base | ||||
|     current_user.setting_theme | ||||
|   end | ||||
|  | ||||
|   def cache_collection(raw, klass) | ||||
|     return raw unless klass.respond_to?(:with_includes) | ||||
|  | ||||
|     raw                    = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) | ||||
|     cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) | ||||
|     uncached_ids           = raw.map(&:id) - cached_keys_with_value.keys | ||||
|  | ||||
|     klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) | ||||
|  | ||||
|     unless uncached_ids.empty? | ||||
|       uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item } | ||||
|  | ||||
|       uncached.each_value do |item| | ||||
|         Rails.cache.write(item, item) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact | ||||
|   end | ||||
|  | ||||
|   def respond_with_error(code) | ||||
|     respond_to do |format| | ||||
|       format.any  { head code } | ||||
|       format.html { render "errors/#{code}", layout: 'error', status: code } | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def render_cached_json(cache_key, **options) | ||||
|     options[:expires_in] ||= 3.minutes | ||||
|     cache_public           = options.key?(:public) ? options.delete(:public) : true | ||||
|     content_type           = options.delete(:content_type) || 'application/json' | ||||
|  | ||||
|     data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do | ||||
|       yield.to_json | ||||
|     end | ||||
|  | ||||
|     expires_in options[:expires_in], public: cache_public | ||||
|     render json: data, content_type: content_type | ||||
|   end | ||||
|  | ||||
|   def set_cache_headers | ||||
|     response.headers['Vary'] = 'Accept' | ||||
|   end | ||||
|  | ||||
|   def mark_cacheable! | ||||
|     expires_in 0, public: true | ||||
|   end | ||||
| end | ||||
|  | ||||
							
								
								
									
										22
									
								
								app/controllers/auth/challenges_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/controllers/auth/challenges_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Auth::ChallengesController < ApplicationController | ||||
|   include ChallengableConcern | ||||
|  | ||||
|   layout 'auth' | ||||
|  | ||||
|   before_action :authenticate_user! | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def create | ||||
|     if challenge_passed? | ||||
|       session[:challenge_passed_at] = Time.now.utc | ||||
|       redirect_to challenge_params[:return_to] | ||||
|     else | ||||
|       @challenge = Form::Challenge.new(return_to: challenge_params[:return_to]) | ||||
|       flash.now[:alert] = I18n.t('challenge.invalid_password') | ||||
|       render_challenge | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -4,32 +4,36 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController | ||||
|   layout 'auth' | ||||
|  | ||||
|   before_action :set_body_classes | ||||
|   before_action :set_user, only: [:finish_signup] | ||||
|   before_action :require_unconfirmed! | ||||
|  | ||||
|   def finish_signup | ||||
|     return unless request.patch? && params[:user] | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|     if @user.update(user_params) | ||||
|       @user.skip_reconfirmation! | ||||
|       bypass_sign_in(@user) | ||||
|       redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions') | ||||
|     else | ||||
|       @show_errors = true | ||||
|     end | ||||
|   def new | ||||
|     super | ||||
|  | ||||
|     resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def set_user | ||||
|     @user = current_user | ||||
|   def require_unconfirmed! | ||||
|     redirect_to edit_user_registration_path if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? | ||||
|   end | ||||
|  | ||||
|   def set_body_classes | ||||
|     @body_classes = 'lighter' | ||||
|   end | ||||
|  | ||||
|   def user_params | ||||
|     params.require(:user).permit(:email) | ||||
|   def after_resending_confirmation_instructions_path_for(_resource_name) | ||||
|     if user_signed_in? | ||||
|       if current_user.confirmed? && current_user.approved? | ||||
|         edit_user_registration_path | ||||
|       else | ||||
|         auth_setup_path | ||||
|       end | ||||
|     else | ||||
|       new_user_session_path | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def after_confirmation_path_for(_resource_name, user) | ||||
|  | ||||
| @ -27,7 +27,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController | ||||
|     if resource.email_verified? | ||||
|       root_path | ||||
|     else | ||||
|       finish_signup_path | ||||
|       auth_setup_path(missing_email: '1') | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -9,6 +9,9 @@ class Auth::RegistrationsController < Devise::RegistrationsController | ||||
|   before_action :set_sessions, only: [:edit, :update] | ||||
|   before_action :set_instance_presenter, only: [:new, :create, :update] | ||||
|   before_action :set_body_classes, only: [:new, :create, :edit, :update] | ||||
|   before_action :require_not_suspended!, only: [:update] | ||||
|  | ||||
|   skip_before_action :require_functional!, only: [:edit, :update] | ||||
|  | ||||
|   def new | ||||
|     super(&:build_invite_request) | ||||
| @ -43,7 +46,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController | ||||
|   end | ||||
|  | ||||
|   def after_sign_up_path_for(_resource) | ||||
|     new_user_session_path | ||||
|     auth_setup_path | ||||
|   end | ||||
|  | ||||
|   def after_sign_in_path_for(_resource) | ||||
| @ -102,4 +105,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController | ||||
|   def set_sessions | ||||
|     @sessions = current_user.session_activations | ||||
|   end | ||||
|  | ||||
|   def require_not_suspended! | ||||
|     forbidden if current_account.suspended? | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -6,8 +6,10 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|   layout 'auth' | ||||
|  | ||||
|   skip_before_action :require_no_authentication, only: [:create] | ||||
|   skip_before_action :check_user_permissions, only: [:destroy] | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] | ||||
|  | ||||
|   before_action :set_instance_presenter, only: [:new] | ||||
|   before_action :set_body_classes | ||||
|  | ||||
| @ -29,6 +31,7 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|   def destroy | ||||
|     tmp_stored_location = stored_location_for(:user) | ||||
|     super | ||||
|     session.delete(:challenge_passed_at) | ||||
|     flash.delete(:notice) | ||||
|     store_location_for(:user, tmp_stored_location) if continue_after? | ||||
|   end | ||||
| @ -38,12 +41,10 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|   def find_user | ||||
|     if session[:otp_user_id] | ||||
|       User.find(session[:otp_user_id]) | ||||
|     elsif user_params[:email] | ||||
|       if use_seamless_external_login? && Devise.check_at_sign && user_params[:email].index('@').nil? | ||||
|         User.joins(:account).find_by(accounts: { username: user_params[:email] }) | ||||
|       else | ||||
|         User.find_for_authentication(email: user_params[:email]) | ||||
|       end | ||||
|     else | ||||
|       user   = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication | ||||
|       user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication | ||||
|       user ||= User.find_for_authentication(email: user_params[:email]) | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @ -70,13 +71,13 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|   end | ||||
|  | ||||
|   def two_factor_enabled? | ||||
|     find_user.try(:otp_required_for_login?) | ||||
|     find_user&.otp_required_for_login? | ||||
|   end | ||||
|  | ||||
|   def valid_otp_attempt?(user) | ||||
|     user.validate_and_consume_otp!(user_params[:otp_attempt]) || | ||||
|       user.invalidate_otp_backup_code!(user_params[:otp_attempt]) | ||||
|   rescue OpenSSL::Cipher::CipherError => _error | ||||
|   rescue OpenSSL::Cipher::CipherError | ||||
|     false | ||||
|   end | ||||
|  | ||||
| @ -85,7 +86,10 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|  | ||||
|     if user_params[:otp_attempt].present? && session[:otp_user_id] | ||||
|       authenticate_with_two_factor_via_otp(user) | ||||
|     elsif user&.valid_password?(user_params[:password]) | ||||
|     elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password])) | ||||
|       # If encrypted_password is blank, we got the user from LDAP or PAM, | ||||
|       # so credentials are already valid | ||||
|  | ||||
|       prompt_for_two_factor(user) | ||||
|     end | ||||
|   end | ||||
| @ -103,6 +107,7 @@ class Auth::SessionsController < Devise::SessionsController | ||||
|  | ||||
|   def prompt_for_two_factor(user) | ||||
|     session[:otp_user_id] = user.id | ||||
|     @body_classes = 'lighter' | ||||
|     render :two_factor | ||||
|   end | ||||
|  | ||||
|  | ||||
							
								
								
									
										58
									
								
								app/controllers/auth/setup_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								app/controllers/auth/setup_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Auth::SetupController < ApplicationController | ||||
|   layout 'auth' | ||||
|  | ||||
|   before_action :authenticate_user! | ||||
|   before_action :require_unconfirmed_or_pending! | ||||
|   before_action :set_body_classes | ||||
|   before_action :set_user | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def show | ||||
|     flash.now[:notice] = begin | ||||
|       if @user.pending? | ||||
|         I18n.t('devise.registrations.signed_up_but_pending') | ||||
|       else | ||||
|         I18n.t('devise.registrations.signed_up_but_unconfirmed') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def update | ||||
|     # This allows updating the e-mail without entering a password as is required | ||||
|     # on the account settings page; however, we only allow this for accounts | ||||
|     # that were not confirmed yet | ||||
|  | ||||
|     if @user.update(user_params) | ||||
|       redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions') | ||||
|     else | ||||
|       render :show | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   helper_method :missing_email? | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def require_unconfirmed_or_pending! | ||||
|     redirect_to root_path if current_user.confirmed? && current_user.approved? | ||||
|   end | ||||
|  | ||||
|   def set_user | ||||
|     @user = current_user | ||||
|   end | ||||
|  | ||||
|   def set_body_classes | ||||
|     @body_classes = 'lighter' | ||||
|   end | ||||
|  | ||||
|   def user_params | ||||
|     params.require(:user).permit(:email) | ||||
|   end | ||||
|  | ||||
|   def missing_email? | ||||
|     truthy_param?(:missing_email) | ||||
|   end | ||||
| end | ||||
| @ -3,24 +3,19 @@ | ||||
| module AccountControllerConcern | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   include AccountOwnedConcern | ||||
|  | ||||
|   FOLLOW_PER_PAGE = 12 | ||||
|  | ||||
|   included do | ||||
|     layout 'public' | ||||
|  | ||||
|     before_action :set_account | ||||
|     before_action :check_account_approval | ||||
|     before_action :check_account_suspension | ||||
|     before_action :set_instance_presenter | ||||
|     before_action :set_link_headers | ||||
|     before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def set_account | ||||
|     @account = Account.find_local!(username_param) | ||||
|   end | ||||
|  | ||||
|   def set_instance_presenter | ||||
|     @instance_presenter = InstancePresenter.new | ||||
|   end | ||||
| @ -29,27 +24,15 @@ module AccountControllerConcern | ||||
|     response.headers['Link'] = LinkHeader.new( | ||||
|       [ | ||||
|         webfinger_account_link, | ||||
|         atom_account_url_link, | ||||
|         actor_url_link, | ||||
|       ] | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def username_param | ||||
|     params[:account_username] | ||||
|   end | ||||
|  | ||||
|   def webfinger_account_link | ||||
|     [ | ||||
|       webfinger_account_url, | ||||
|       [%w(rel lrdd), %w(type application/xrd+xml)], | ||||
|     ] | ||||
|   end | ||||
|  | ||||
|   def atom_account_url_link | ||||
|     [ | ||||
|       account_url(@account, format: 'atom'), | ||||
|       [%w(rel alternate), %w(type application/atom+xml)], | ||||
|       [%w(rel lrdd), %w(type application/jrd+json)], | ||||
|     ] | ||||
|   end | ||||
|  | ||||
| @ -63,15 +46,4 @@ module AccountControllerConcern | ||||
|   def webfinger_account_url | ||||
|     webfinger_url(resource: @account.to_webfinger_s) | ||||
|   end | ||||
|  | ||||
|   def check_account_approval | ||||
|     not_found if @account.user_pending? | ||||
|   end | ||||
|  | ||||
|   def check_account_suspension | ||||
|     if @account.suspended? | ||||
|       expires_in(3.minutes, public: true) | ||||
|       gone | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
							
								
								
									
										34
									
								
								app/controllers/concerns/account_owned_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/controllers/concerns/account_owned_concern.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| module AccountOwnedConcern | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   included do | ||||
|     before_action :authenticate_user!, if: -> { whitelist_mode? && request.format != :json } | ||||
|     before_action :set_account, if: :account_required? | ||||
|     before_action :check_account_approval, if: :account_required? | ||||
|     before_action :check_account_suspension, if: :account_required? | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def account_required? | ||||
|     true | ||||
|   end | ||||
|  | ||||
|   def set_account | ||||
|     @account = Account.find_local!(username_param) | ||||
|   end | ||||
|  | ||||
|   def username_param | ||||
|     params[:account_username] | ||||
|   end | ||||
|  | ||||
|   def check_account_approval | ||||
|     not_found if @account.local? && @account.user_pending? | ||||
|   end | ||||
|  | ||||
|   def check_account_suspension | ||||
|     expires_in(3.minutes, public: true) && gone if @account.suspended? | ||||
|   end | ||||
| end | ||||
							
								
								
									
										50
									
								
								app/controllers/concerns/cache_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/controllers/concerns/cache_concern.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| module CacheConcern | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   def render_with_cache(**options) | ||||
|     raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given? | ||||
|  | ||||
|     key        = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':') | ||||
|     expires_in = options.delete(:expires_in) || 3.minutes | ||||
|     body       = Rails.cache.read(key, raw: true) | ||||
|  | ||||
|     if body | ||||
|       render(options.except(:json, :serializer, :each_serializer, :adapter, :fields).merge(json: body)) | ||||
|     else | ||||
|       if block_given? | ||||
|         options[:json] = yield | ||||
|       elsif options[:json].is_a?(Symbol) | ||||
|         options[:json] = send(options[:json]) | ||||
|       end | ||||
|  | ||||
|       render(options) | ||||
|       Rails.cache.write(key, response.body, expires_in: expires_in, raw: true) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def set_cache_headers | ||||
|     response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature' | ||||
|   end | ||||
|  | ||||
|   def cache_collection(raw, klass) | ||||
|     return raw unless klass.respond_to?(:with_includes) | ||||
|  | ||||
|     raw                    = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) | ||||
|     cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) | ||||
|     uncached_ids           = raw.map(&:id) - cached_keys_with_value.keys | ||||
|  | ||||
|     klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) | ||||
|  | ||||
|     unless uncached_ids.empty? | ||||
|       uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item } | ||||
|  | ||||
|       uncached.each_value do |item| | ||||
|         Rails.cache.write(item, item) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact | ||||
|   end | ||||
| end | ||||
							
								
								
									
										65
									
								
								app/controllers/concerns/challengable_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								app/controllers/concerns/challengable_concern.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| # This concern is inspired by "sudo mode" on GitHub. It | ||||
| # is a way to re-authenticate a user before allowing them | ||||
| # to see or perform an action. | ||||
| # | ||||
| # Add `before_action :require_challenge!` to actions you | ||||
| # want to protect. | ||||
| # | ||||
| # The user will be shown a page to enter the challenge (which | ||||
| # is either the password, or just the username when no | ||||
| # password exists). Upon passing, there is a grace period | ||||
| # during which no challenge will be asked from the user. | ||||
| # | ||||
| # Accessing challenge-protected resources during the grace | ||||
| # period will refresh the grace period. | ||||
| module ChallengableConcern | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   CHALLENGE_TIMEOUT = 1.hour.freeze | ||||
|  | ||||
|   def require_challenge! | ||||
|     return if skip_challenge? | ||||
|  | ||||
|     if challenge_passed_recently? | ||||
|       session[:challenge_passed_at] = Time.now.utc | ||||
|       return | ||||
|     end | ||||
|  | ||||
|     @challenge = Form::Challenge.new(return_to: request.url) | ||||
|  | ||||
|     if params.key?(:form_challenge) | ||||
|       if challenge_passed? | ||||
|         session[:challenge_passed_at] = Time.now.utc | ||||
|         return | ||||
|       else | ||||
|         flash.now[:alert] = I18n.t('challenge.invalid_password') | ||||
|         render_challenge | ||||
|       end | ||||
|     else | ||||
|       render_challenge | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def render_challenge | ||||
|     @body_classes = 'lighter' | ||||
|     render template: 'auth/challenges/new', layout: 'auth' | ||||
|   end | ||||
|  | ||||
|   def challenge_passed? | ||||
|     current_user.valid_password?(challenge_params[:current_password]) | ||||
|   end | ||||
|  | ||||
|   def skip_challenge? | ||||
|     current_user.encrypted_password.blank? | ||||
|   end | ||||
|  | ||||
|   def challenge_passed_recently? | ||||
|     session[:challenge_passed_at].present? && session[:challenge_passed_at] >= CHALLENGE_TIMEOUT.ago | ||||
|   end | ||||
|  | ||||
|   def challenge_params | ||||
|     params.require(:form_challenge).permit(:current_password, :return_to) | ||||
|   end | ||||
| end | ||||
| @ -5,7 +5,10 @@ module ExportControllerConcern | ||||
|  | ||||
|   included do | ||||
|     before_action :authenticate_user! | ||||
|     before_action :require_not_suspended! | ||||
|     before_action :load_export | ||||
|  | ||||
|     skip_before_action :require_functional! | ||||
|   end | ||||
|  | ||||
|   private | ||||
| @ -27,4 +30,8 @@ module ExportControllerConcern | ||||
|   def export_filename | ||||
|     "#{controller_name}.csv" | ||||
|   end | ||||
|  | ||||
|   def require_not_suspended! | ||||
|     forbidden if current_account.suspended? | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -5,12 +5,35 @@ | ||||
| module SignatureVerification | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   include DomainControlHelper | ||||
|  | ||||
|   def require_signature! | ||||
|     render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account | ||||
|   end | ||||
|  | ||||
|   def signed_request? | ||||
|     request.headers['Signature'].present? | ||||
|   end | ||||
|  | ||||
|   def signature_verification_failure_reason | ||||
|     return @signature_verification_failure_reason if defined?(@signature_verification_failure_reason) | ||||
|     @signature_verification_failure_reason | ||||
|   end | ||||
|  | ||||
|   def signature_verification_failure_code | ||||
|     @signature_verification_failure_code || 401 | ||||
|   end | ||||
|  | ||||
|   def signature_key_id | ||||
|     raw_signature    = request.headers['Signature'] | ||||
|     signature_params = {} | ||||
|  | ||||
|     raw_signature.split(',').each do |part| | ||||
|       parsed_parts = part.match(/([a-z]+)="([^"]+)"/i) | ||||
|       next if parsed_parts.nil? || parsed_parts.size != 3 | ||||
|       signature_params[parsed_parts[1]] = parsed_parts[2] | ||||
|     end | ||||
|  | ||||
|     signature_params['keyId'] | ||||
|   end | ||||
|  | ||||
|   def signed_request_account | ||||
| @ -123,6 +146,13 @@ module SignatureVerification | ||||
|   end | ||||
|  | ||||
|   def account_from_key_id(key_id) | ||||
|     domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id | ||||
|  | ||||
|     if domain_not_allowed?(domain) | ||||
|       @signature_verification_failure_code = 403 | ||||
|       return | ||||
|     end | ||||
|  | ||||
|     if key_id.start_with?('acct:') | ||||
|       stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } | ||||
|     elsif !ActivityPub::TagManager.instance.local_uri?(key_id) | ||||
| @ -137,7 +167,7 @@ module SignatureVerification | ||||
|       .with_fallback { nil } | ||||
|       .with_threshold(1) | ||||
|       .with_cool_off_time(5.minutes.seconds) | ||||
|       .with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) } | ||||
|       .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } | ||||
|       .run | ||||
|   end | ||||
|  | ||||
|  | ||||
							
								
								
									
										87
									
								
								app/controllers/concerns/status_controller_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								app/controllers/concerns/status_controller_concern.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| module StatusControllerConcern | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   ANCESTORS_LIMIT         = 40 | ||||
|   DESCENDANTS_LIMIT       = 60 | ||||
|   DESCENDANTS_DEPTH_LIMIT = 20 | ||||
|  | ||||
|   def create_descendant_thread(starting_depth, statuses) | ||||
|     depth = starting_depth + statuses.size | ||||
|  | ||||
|     if depth < DESCENDANTS_DEPTH_LIMIT | ||||
|       { | ||||
|         statuses: statuses, | ||||
|         starting_depth: starting_depth, | ||||
|       } | ||||
|     else | ||||
|       next_status = statuses.pop | ||||
|  | ||||
|       { | ||||
|         statuses: statuses, | ||||
|         starting_depth: starting_depth, | ||||
|         next_status: next_status, | ||||
|       } | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def set_ancestors | ||||
|     @ancestors     = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : [] | ||||
|     @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift | ||||
|   end | ||||
|  | ||||
|   def set_descendants | ||||
|     @max_descendant_thread_id   = params[:max_descendant_thread_id]&.to_i | ||||
|     @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i | ||||
|  | ||||
|     descendants = cache_collection( | ||||
|       @status.descendants( | ||||
|         DESCENDANTS_LIMIT, | ||||
|         current_account, | ||||
|         @max_descendant_thread_id, | ||||
|         @since_descendant_thread_id, | ||||
|         DESCENDANTS_DEPTH_LIMIT | ||||
|       ), | ||||
|       Status | ||||
|     ) | ||||
|  | ||||
|     @descendant_threads = [] | ||||
|  | ||||
|     if descendants.present? | ||||
|       statuses       = [descendants.first] | ||||
|       starting_depth = 0 | ||||
|  | ||||
|       descendants.drop(1).each_with_index do |descendant, index| | ||||
|         if descendants[index].id == descendant.in_reply_to_id | ||||
|           statuses << descendant | ||||
|         else | ||||
|           @descendant_threads << create_descendant_thread(starting_depth, statuses) | ||||
|  | ||||
|           # The thread is broken, assume it's a reply to the root status | ||||
|           starting_depth = 0 | ||||
|  | ||||
|           # ... unless we can find its ancestor in one of the already-processed threads | ||||
|           @descendant_threads.reverse_each do |descendant_thread| | ||||
|             statuses = descendant_thread[:statuses] | ||||
|  | ||||
|             index = statuses.find_index do |thread_status| | ||||
|               thread_status.id == descendant.in_reply_to_id | ||||
|             end | ||||
|  | ||||
|             if index.present? | ||||
|               starting_depth = descendant_thread[:starting_depth] + index + 1 | ||||
|               break | ||||
|             end | ||||
|           end | ||||
|  | ||||
|           statuses = [descendant] | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       @descendant_threads << create_descendant_thread(starting_depth, statuses) | ||||
|     end | ||||
|  | ||||
|     @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT | ||||
|   end | ||||
| end | ||||
| @ -2,10 +2,12 @@ | ||||
|  | ||||
| class CustomCssController < ApplicationController | ||||
|   skip_before_action :store_current_location | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   before_action :set_cache_headers | ||||
|  | ||||
|   def show | ||||
|     expires_in 3.minutes, public: true | ||||
|     render plain: Setting.custom_css || '', content_type: 'text/css' | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -3,12 +3,14 @@ | ||||
| class DirectoriesController < ApplicationController | ||||
|   layout 'public' | ||||
|  | ||||
|   before_action :check_enabled | ||||
|   before_action :authenticate_user!, if: :whitelist_mode? | ||||
|   before_action :require_enabled! | ||||
|   before_action :set_instance_presenter | ||||
|   before_action :set_tag, only: :show | ||||
|   before_action :set_tags | ||||
|   before_action :set_accounts | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def index | ||||
|     render :index | ||||
|   end | ||||
| @ -19,21 +21,18 @@ class DirectoriesController < ApplicationController | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def check_enabled | ||||
|   def require_enabled! | ||||
|     return not_found unless Setting.profile_directory | ||||
|   end | ||||
|  | ||||
|   def set_tag | ||||
|     @tag = Tag.discoverable.find_by!(name: params[:id].downcase) | ||||
|   end | ||||
|  | ||||
|   def set_tags | ||||
|     @tags = Tag.discoverable.limit(30).reject { |tag| tag.cached_sample_accounts.empty? } | ||||
|     @tag = Tag.discoverable.find_normalized!(params[:id]) | ||||
|   end | ||||
|  | ||||
|   def set_accounts | ||||
|     @accounts = Account.discoverable.by_recent_status.page(params[:page]).per(40).tap do |query| | ||||
|     @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query| | ||||
|       query.merge!(Account.tagged_with(@tag.id)) if @tag | ||||
|       query.merge!(Account.not_excluded_by_account(current_account)) if current_account | ||||
|     end | ||||
|   end | ||||
|  | ||||
|  | ||||
| @ -7,9 +7,8 @@ class EmojisController < ApplicationController | ||||
|   def show | ||||
|     respond_to do |format| | ||||
|       format.json do | ||||
|         render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do | ||||
|           ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) | ||||
|         end | ||||
|         expires_in 3.minutes, public: true | ||||
|         render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @ -2,13 +2,18 @@ | ||||
|  | ||||
| class FollowerAccountsController < ApplicationController | ||||
|   include AccountControllerConcern | ||||
|   include SignatureVerification | ||||
|  | ||||
|   before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } | ||||
|   before_action :set_cache_headers | ||||
|  | ||||
|   skip_around_action :set_locale, if: -> { request.format == :json } | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def index | ||||
|     respond_to do |format| | ||||
|       format.html do | ||||
|         mark_cacheable! unless user_signed_in? | ||||
|         expires_in 0, public: true unless user_signed_in? | ||||
|  | ||||
|         next if @account.user_hides_network? | ||||
|  | ||||
| @ -17,9 +22,9 @@ class FollowerAccountsController < ApplicationController | ||||
|       end | ||||
|  | ||||
|       format.json do | ||||
|         raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? | ||||
|         raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network? | ||||
|  | ||||
|         expires_in 3.minutes, public: true if params[:page].blank? | ||||
|         expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) | ||||
|  | ||||
|         render json: collection_presenter, | ||||
|                serializer: ActivityPub::CollectionSerializer, | ||||
| @ -35,12 +40,16 @@ class FollowerAccountsController < ApplicationController | ||||
|     @follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account) | ||||
|   end | ||||
|  | ||||
|   def page_requested? | ||||
|     params[:page].present? | ||||
|   end | ||||
|  | ||||
|   def page_url(page) | ||||
|     account_followers_url(@account, page: page) unless page.nil? | ||||
|   end | ||||
|  | ||||
|   def collection_presenter | ||||
|     if params[:page].present? | ||||
|     if page_requested? | ||||
|       ActivityPub::CollectionPresenter.new( | ||||
|         id: account_followers_url(@account, page: params.fetch(:page, 1)), | ||||
|         type: :ordered, | ||||
|  | ||||
| @ -2,13 +2,18 @@ | ||||
|  | ||||
| class FollowingAccountsController < ApplicationController | ||||
|   include AccountControllerConcern | ||||
|   include SignatureVerification | ||||
|  | ||||
|   before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } | ||||
|   before_action :set_cache_headers | ||||
|  | ||||
|   skip_around_action :set_locale, if: -> { request.format == :json } | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def index | ||||
|     respond_to do |format| | ||||
|       format.html do | ||||
|         mark_cacheable! unless user_signed_in? | ||||
|         expires_in 0, public: true unless user_signed_in? | ||||
|  | ||||
|         next if @account.user_hides_network? | ||||
|  | ||||
| @ -17,9 +22,9 @@ class FollowingAccountsController < ApplicationController | ||||
|       end | ||||
|  | ||||
|       format.json do | ||||
|         raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? | ||||
|         raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network? | ||||
|  | ||||
|         expires_in 3.minutes, public: true if params[:page].blank? | ||||
|         expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) | ||||
|  | ||||
|         render json: collection_presenter, | ||||
|                serializer: ActivityPub::CollectionSerializer, | ||||
| @ -35,12 +40,16 @@ class FollowingAccountsController < ApplicationController | ||||
|     @follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account) | ||||
|   end | ||||
|  | ||||
|   def page_requested? | ||||
|     params[:page].present? | ||||
|   end | ||||
|  | ||||
|   def page_url(page) | ||||
|     account_following_index_url(@account, page: page) unless page.nil? | ||||
|   end | ||||
|  | ||||
|   def collection_presenter | ||||
|     if params[:page].present? | ||||
|     if page_requested? | ||||
|       ActivityPub::CollectionPresenter.new( | ||||
|         id: account_following_index_url(@account, page: params.fetch(:page, 1)), | ||||
|         type: :ordered, | ||||
|  | ||||
| @ -3,7 +3,6 @@ | ||||
| class HomeController < ApplicationController | ||||
|   before_action :authenticate_user! | ||||
|   before_action :set_referrer_policy_header | ||||
|   before_action :set_initial_state_json | ||||
|  | ||||
|   def index | ||||
|     @body_classes = 'app-body' | ||||
| @ -21,7 +20,7 @@ class HomeController < ApplicationController | ||||
|       when 'statuses' | ||||
|         status = Status.find_by(id: matches[2]) | ||||
|  | ||||
|         if status && (status.public_visibility? || status.unlisted_visibility?) | ||||
|         if status&.distributable? | ||||
|           redirect_to(ActivityPub::TagManager.instance.url_for(status)) | ||||
|           return | ||||
|         end | ||||
| @ -39,26 +38,11 @@ class HomeController < ApplicationController | ||||
|     redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path) | ||||
|   end | ||||
|  | ||||
|   def set_initial_state_json | ||||
|     serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) | ||||
|     @initial_state_json   = serializable_resource.to_json | ||||
|   end | ||||
|  | ||||
|   def initial_state_params | ||||
|     { | ||||
|       settings: Web::Setting.find_by(user: current_user)&.data || {}, | ||||
|       push_subscription: current_account.user.web_push_subscription(current_session), | ||||
|       current_account: current_account, | ||||
|       token: current_session.token, | ||||
|       admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def default_redirect_path | ||||
|     if request.path.start_with?('/web') | ||||
|     if request.path.start_with?('/web') || whitelist_mode? | ||||
|       new_user_session_path | ||||
|     elsif single_user_mode? | ||||
|       short_account_path(Account.local.without_suspended.first) | ||||
|       short_account_path(Account.local.without_suspended.where('id > 0').first) | ||||
|     else | ||||
|       about_path | ||||
|     end | ||||
|  | ||||
							
								
								
									
										22
									
								
								app/controllers/instance_actors_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/controllers/instance_actors_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class InstanceActorsController < ApplicationController | ||||
|   include AccountControllerConcern | ||||
|  | ||||
|   skip_around_action :set_locale | ||||
|  | ||||
|   def show | ||||
|     expires_in 10.minutes, public: true | ||||
|     render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def set_account | ||||
|     @account = Account.find(-99) | ||||
|   end | ||||
|  | ||||
|   def restrict_fields_to | ||||
|     %i(id type preferred_username inbox public_key endpoints url manually_approves_followers) | ||||
|   end | ||||
| end | ||||
| @ -2,6 +2,7 @@ | ||||
|  | ||||
| class IntentsController < ApplicationController | ||||
|   before_action :check_uri | ||||
|  | ||||
|   rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri | ||||
|  | ||||
|   def show | ||||
|  | ||||
| @ -43,7 +43,7 @@ class InvitesController < ApplicationController | ||||
|   end | ||||
|  | ||||
|   def resource_params | ||||
|     params.require(:invite).permit(:max_uses, :expires_in, :autofollow) | ||||
|     params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment) | ||||
|   end | ||||
|  | ||||
|   def set_body_classes | ||||
|  | ||||
| @ -2,8 +2,10 @@ | ||||
|  | ||||
| class ManifestsController < ApplicationController | ||||
|   skip_before_action :store_current_location | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def show | ||||
|     render json: InstancePresenter.new, serializer: ManifestSerializer | ||||
|     expires_in 3.minutes, public: true | ||||
|     render json: InstancePresenter.new, serializer: ManifestSerializer, root: 'instance' | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -4,7 +4,9 @@ class MediaController < ApplicationController | ||||
|   include Authorization | ||||
|  | ||||
|   skip_before_action :store_current_location | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   before_action :authenticate_user!, if: :whitelist_mode? | ||||
|   before_action :set_media_attachment | ||||
|   before_action :verify_permitted_status! | ||||
|   before_action :check_playable, only: :player | ||||
| @ -31,7 +33,6 @@ class MediaController < ApplicationController | ||||
|   def verify_permitted_status! | ||||
|     authorize @media_attachment.status, :show? | ||||
|   rescue Mastodon::NotPermittedError | ||||
|     # Reraise in order to get a 404 instead of a 403 error code | ||||
|     raise ActiveRecord::RecordNotFound | ||||
|   end | ||||
|  | ||||
|  | ||||
| @ -4,6 +4,13 @@ class MediaProxyController < ApplicationController | ||||
|   include RoutingHelper | ||||
|  | ||||
|   skip_before_action :store_current_location | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   before_action :authenticate_user!, if: :whitelist_mode? | ||||
|  | ||||
|   rescue_from ActiveRecord::RecordInvalid, with: :not_found | ||||
|   rescue_from Mastodon::UnexpectedResponseError, with: :not_found | ||||
|   rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error | ||||
|  | ||||
|   def show | ||||
|     RedisLock.acquire(lock_options) do |lock| | ||||
|  | ||||
| @ -7,6 +7,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio | ||||
|   before_action :authenticate_resource_owner! | ||||
|   before_action :set_body_classes | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   include Localized | ||||
|  | ||||
|   def destroy | ||||
|  | ||||
| @ -3,25 +3,17 @@ | ||||
| class PublicTimelinesController < ApplicationController | ||||
|   layout 'public' | ||||
|  | ||||
|   before_action :check_enabled | ||||
|   before_action :authenticate_user!, if: :whitelist_mode? | ||||
|   before_action :require_enabled! | ||||
|   before_action :set_body_classes | ||||
|   before_action :set_instance_presenter | ||||
|  | ||||
|   def show | ||||
|     respond_to do |format| | ||||
|       format.html do | ||||
|         @initial_state_json = ActiveModelSerializers::SerializableResource.new( | ||||
|           InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), | ||||
|           serializer: InitialStateSerializer | ||||
|         ).to_json | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   def show; end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def check_enabled | ||||
|     raise ActiveRecord::RecordNotFound unless Setting.timeline_preview | ||||
|   def require_enabled! | ||||
|     not_found unless Setting.timeline_preview | ||||
|   end | ||||
|  | ||||
|   def set_body_classes | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class RemoteFollowController < ApplicationController | ||||
|   include AccountOwnedConcern | ||||
|  | ||||
|   layout 'modal' | ||||
|  | ||||
|   before_action :set_account | ||||
|   before_action :gone, if: :suspended_account? | ||||
|   before_action :set_body_classes | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def new | ||||
|     @remote_follow = RemoteFollow.new(session_params) | ||||
|   end | ||||
| @ -29,15 +31,7 @@ class RemoteFollowController < ApplicationController | ||||
|   end | ||||
|  | ||||
|   def session_params | ||||
|     { acct: session[:remote_follow] } | ||||
|   end | ||||
|  | ||||
|   def set_account | ||||
|     @account = Account.find_local!(params[:account_username]) | ||||
|   end | ||||
|  | ||||
|   def suspended_account? | ||||
|     @account.suspended? | ||||
|     { acct: session[:remote_follow] || current_account&.username } | ||||
|   end | ||||
|  | ||||
|   def set_body_classes | ||||
|  | ||||
| @ -5,10 +5,13 @@ class RemoteInteractionController < ApplicationController | ||||
|  | ||||
|   layout 'modal' | ||||
|  | ||||
|   before_action :authenticate_user!, if: :whitelist_mode? | ||||
|   before_action :set_interaction_type | ||||
|   before_action :set_status | ||||
|   before_action :set_body_classes | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def new | ||||
|     @remote_follow = RemoteFollow.new(session_params) | ||||
|   end | ||||
| @ -31,14 +34,13 @@ class RemoteInteractionController < ApplicationController | ||||
|   end | ||||
|  | ||||
|   def session_params | ||||
|     { acct: session[:remote_follow] } | ||||
|     { acct: session[:remote_follow] || current_account&.username } | ||||
|   end | ||||
|  | ||||
|   def set_status | ||||
|     @status = Status.find(params[:id]) | ||||
|     authorize @status, :show? | ||||
|   rescue Mastodon::NotPermittedError | ||||
|     # Reraise in order to get a 404 | ||||
|     raise ActiveRecord::RecordNotFound | ||||
|   end | ||||
|  | ||||
|  | ||||
| @ -1,39 +0,0 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class RemoteUnfollowsController < ApplicationController | ||||
|   layout 'modal' | ||||
|  | ||||
|   before_action :authenticate_user! | ||||
|   before_action :set_body_classes | ||||
|  | ||||
|   def create | ||||
|     @account = unfollow_attempt.try(:target_account) | ||||
|  | ||||
|     if @account.nil? | ||||
|       render :error | ||||
|     else | ||||
|       render :success | ||||
|     end | ||||
|   rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError | ||||
|     render :error | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def unfollow_attempt | ||||
|     username, domain = acct_without_prefix.split('@') | ||||
|     UnfollowService.new.call(current_account, Account.find_remote!(username, domain)) | ||||
|   end | ||||
|  | ||||
|   def acct_without_prefix | ||||
|     acct_params.gsub(/\Aacct:/, '') | ||||
|   end | ||||
|  | ||||
|   def acct_params | ||||
|     params.fetch(:acct, '') | ||||
|   end | ||||
|  | ||||
|   def set_body_classes | ||||
|     @body_classes = 'modal-layout' | ||||
|   end | ||||
| end | ||||
							
								
								
									
										43
									
								
								app/controllers/settings/aliases_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/controllers/settings/aliases_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Settings::AliasesController < Settings::BaseController | ||||
|   layout 'admin' | ||||
|  | ||||
|   before_action :authenticate_user! | ||||
|   before_action :set_aliases, except: :destroy | ||||
|   before_action :set_alias, only: :destroy | ||||
|  | ||||
|   def index | ||||
|     @alias = current_account.aliases.build | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|     @alias = current_account.aliases.build(resource_params) | ||||
|  | ||||
|     if @alias.save | ||||
|       ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) | ||||
|       redirect_to settings_aliases_path, notice: I18n.t('aliases.created_msg') | ||||
|     else | ||||
|       render :index | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def destroy | ||||
|     @alias.destroy! | ||||
|     redirect_to settings_aliases_path, notice: I18n.t('aliases.deleted_msg') | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def resource_params | ||||
|     params.require(:account_alias).permit(:acct) | ||||
|   end | ||||
|  | ||||
|   def set_alias | ||||
|     @alias = current_account.aliases.find(params[:id]) | ||||
|   end | ||||
|  | ||||
|   def set_aliases | ||||
|     @aliases = current_account.aliases.order(id: :desc).reject(&:new_record?) | ||||
|   end | ||||
| end | ||||
| @ -5,18 +5,20 @@ class Settings::DeletesController < Settings::BaseController | ||||
|  | ||||
|   before_action :check_enabled_deletion | ||||
|   before_action :authenticate_user! | ||||
|   before_action :require_not_suspended! | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def show | ||||
|     @confirmation = Form::DeleteConfirmation.new | ||||
|   end | ||||
|  | ||||
|   def destroy | ||||
|     if current_user.valid_password?(delete_params[:password]) | ||||
|       Admin::SuspensionWorker.perform_async(current_user.account_id, true) | ||||
|       sign_out | ||||
|     if challenge_passed? | ||||
|       destroy_account! | ||||
|       redirect_to new_user_session_path, notice: I18n.t('deletes.success_msg') | ||||
|     else | ||||
|       redirect_to settings_delete_path, alert: I18n.t('deletes.bad_password_msg') | ||||
|       redirect_to settings_delete_path, alert: I18n.t('deletes.challenge_not_passed') | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @ -26,7 +28,25 @@ class Settings::DeletesController < Settings::BaseController | ||||
|     redirect_to root_path unless Setting.open_deletion | ||||
|   end | ||||
|  | ||||
|   def delete_params | ||||
|     params.require(:form_delete_confirmation).permit(:password) | ||||
|   def resource_params | ||||
|     params.require(:form_delete_confirmation).permit(:password, :username) | ||||
|   end | ||||
|  | ||||
|   def require_not_suspended! | ||||
|     forbidden if current_account.suspended? | ||||
|   end | ||||
|  | ||||
|   def challenge_passed? | ||||
|     if current_user.encrypted_password.blank? | ||||
|       current_account.username == resource_params[:username] | ||||
|     else | ||||
|       current_user.valid_password?(resource_params[:password]) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def destroy_account! | ||||
|     current_account.suspend! | ||||
|     Admin::SuspensionWorker.perform_async(current_user.account_id, true) | ||||
|     sign_out | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -6,6 +6,9 @@ class Settings::ExportsController < Settings::BaseController | ||||
|   layout 'admin' | ||||
|  | ||||
|   before_action :authenticate_user! | ||||
|   before_action :require_not_suspended! | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def show | ||||
|     @export  = Export.new(current_account) | ||||
| @ -34,4 +37,8 @@ class Settings::ExportsController < Settings::BaseController | ||||
|   def lock_options | ||||
|     { redis: Redis.current, key: "backup:#{current_user.id}" } | ||||
|   end | ||||
|  | ||||
|   def require_not_suspended! | ||||
|     forbidden if current_account.suspended? | ||||
|   end | ||||
| end | ||||
|  | ||||
							
								
								
									
										45
									
								
								app/controllers/settings/migration/redirects_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/controllers/settings/migration/redirects_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Settings::Migration::RedirectsController < Settings::BaseController | ||||
|   layout 'admin' | ||||
|  | ||||
|   before_action :authenticate_user! | ||||
|   before_action :require_not_suspended! | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def new | ||||
|     @redirect = Form::Redirect.new | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|     @redirect = Form::Redirect.new(resource_params.merge(account: current_account)) | ||||
|  | ||||
|     if @redirect.valid_with_challenge?(current_user) | ||||
|       current_account.update!(moved_to_account: @redirect.target_account) | ||||
|       ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) | ||||
|       redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct) | ||||
|     else | ||||
|       render :new | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def destroy | ||||
|     if current_account.moved_to_account_id.present? | ||||
|       current_account.update!(moved_to_account: nil) | ||||
|       ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) | ||||
|     end | ||||
|  | ||||
|     redirect_to settings_migration_path, notice: I18n.t('migrations.cancelled_msg') | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def resource_params | ||||
|     params.require(:form_redirect).permit(:acct, :current_password, :current_username) | ||||
|   end | ||||
|  | ||||
|   def require_not_suspended! | ||||
|     forbidden if current_account.suspended? | ||||
|   end | ||||
| end | ||||
| @ -4,31 +4,48 @@ class Settings::MigrationsController < Settings::BaseController | ||||
|   layout 'admin' | ||||
|  | ||||
|   before_action :authenticate_user! | ||||
|   before_action :require_not_suspended! | ||||
|   before_action :set_migrations | ||||
|   before_action :set_cooldown | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def show | ||||
|     @migration = Form::Migration.new(account: current_account.moved_to_account) | ||||
|     @migration = current_account.migrations.build | ||||
|   end | ||||
|  | ||||
|   def update | ||||
|     @migration = Form::Migration.new(resource_params) | ||||
|   def create | ||||
|     @migration = current_account.migrations.build(resource_params) | ||||
|  | ||||
|     if @migration.valid? && migration_account_changed? | ||||
|       current_account.update!(moved_to_account: @migration.account) | ||||
|       ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) | ||||
|       redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg') | ||||
|     if @migration.save_with_challenge(current_user) | ||||
|       MoveService.new.call(@migration) | ||||
|       redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct) | ||||
|     else | ||||
|       render :show | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   helper_method :on_cooldown? | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def resource_params | ||||
|     params.require(:migration).permit(:acct) | ||||
|     params.require(:account_migration).permit(:acct, :current_password, :current_username) | ||||
|   end | ||||
|  | ||||
|   def migration_account_changed? | ||||
|     current_account.moved_to_account_id != @migration.account&.id && | ||||
|       current_account.id != @migration.account&.id | ||||
|   def set_migrations | ||||
|     @migrations = current_account.migrations.includes(:target_account).order(id: :desc).reject(&:new_record?) | ||||
|   end | ||||
|  | ||||
|   def set_cooldown | ||||
|     @cooldown = current_account.migrations.within_cooldown.first | ||||
|   end | ||||
|  | ||||
|   def on_cooldown? | ||||
|     @cooldown.present? | ||||
|   end | ||||
|  | ||||
|   def require_not_suspended! | ||||
|     forbidden if current_account.suspended? | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -55,7 +55,10 @@ class Settings::PreferencesController < Settings::BaseController | ||||
|       :setting_aggregate_reblogs, | ||||
|       :setting_show_application, | ||||
|       :setting_advanced_layout, | ||||
|       notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), | ||||
|       :setting_use_blurhash, | ||||
|       :setting_use_pending_items, | ||||
|       :setting_trends, | ||||
|       notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), | ||||
|       interactions: %i(must_be_follower must_be_following must_be_following_dm) | ||||
|     ) | ||||
|   end | ||||
|  | ||||
| @ -4,6 +4,8 @@ class Settings::SessionsController < Settings::BaseController | ||||
|   before_action :authenticate_user! | ||||
|   before_action :set_session, only: :destroy | ||||
|  | ||||
|   skip_before_action :require_functional! | ||||
|  | ||||
|   def destroy | ||||
|     @session.destroy! | ||||
|     flash[:notice] = I18n.t('sessions.revoke_success') | ||||
|  | ||||
| @ -3,23 +3,30 @@ | ||||
| module Settings | ||||
|   module TwoFactorAuthentication | ||||
|     class ConfirmationsController < BaseController | ||||
|       include ChallengableConcern | ||||
|  | ||||
|       layout 'admin' | ||||
|  | ||||
|       before_action :authenticate_user! | ||||
|       before_action :require_challenge! | ||||
|       before_action :ensure_otp_secret | ||||
|  | ||||
|       skip_before_action :require_functional! | ||||
|  | ||||
|       def new | ||||
|         prepare_two_factor_form | ||||
|       end | ||||
|  | ||||
|       def create | ||||
|         if current_user.validate_and_consume_otp!(confirmation_params[:code]) | ||||
|         if current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) | ||||
|           flash.now[:notice] = I18n.t('two_factor_authentication.enabled_success') | ||||
|  | ||||
|           current_user.otp_required_for_login = true | ||||
|           @recovery_codes = current_user.generate_otp_backup_codes! | ||||
|           current_user.save! | ||||
|  | ||||
|           UserMailer.two_factor_enabled(current_user).deliver_later! | ||||
|  | ||||
|           render 'settings/two_factor_authentication/recovery_codes/index' | ||||
|         else | ||||
|           flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code') | ||||
| @ -31,7 +38,7 @@ module Settings | ||||
|       private | ||||
|  | ||||
|       def confirmation_params | ||||
|         params.require(:form_two_factor_confirmation).permit(:code) | ||||
|         params.require(:form_two_factor_confirmation).permit(:otp_attempt) | ||||
|       end | ||||
|  | ||||
|       def prepare_two_factor_form | ||||
|  | ||||
| @ -3,14 +3,22 @@ | ||||
| module Settings | ||||
|   module TwoFactorAuthentication | ||||
|     class RecoveryCodesController < BaseController | ||||
|       include ChallengableConcern | ||||
|  | ||||
|       layout 'admin' | ||||
|  | ||||
|       before_action :authenticate_user! | ||||
|       before_action :require_challenge!, on: :create | ||||
|  | ||||
|       skip_before_action :require_functional! | ||||
|  | ||||
|       def create | ||||
|         @recovery_codes = current_user.generate_otp_backup_codes! | ||||
|         current_user.save! | ||||
|  | ||||
|         UserMailer.two_factor_recovery_codes_changed(current_user).deliver_later! | ||||
|         flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated') | ||||
|  | ||||
|         render :index | ||||
|       end | ||||
|     end | ||||
|  | ||||
| @ -2,10 +2,15 @@ | ||||
|  | ||||
| module Settings | ||||
|   class TwoFactorAuthenticationsController < BaseController | ||||
|     include ChallengableConcern | ||||
|  | ||||
|     layout 'admin' | ||||
|  | ||||
|     before_action :authenticate_user! | ||||
|     before_action :verify_otp_required, only: [:create] | ||||
|     before_action :require_challenge!, only: [:create] | ||||
|  | ||||
|     skip_before_action :require_functional! | ||||
|  | ||||
|     def show | ||||
|       @confirmation = Form::TwoFactorConfirmation.new | ||||
| @ -21,6 +26,7 @@ module Settings | ||||
|       if acceptable_code? | ||||
|         current_user.otp_required_for_login = false | ||||
|         current_user.save! | ||||
|         UserMailer.two_factor_disabled(current_user).deliver_later! | ||||
|         redirect_to settings_two_factor_authentication_path | ||||
|       else | ||||
|         flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code') | ||||
| @ -32,7 +38,7 @@ module Settings | ||||
|     private | ||||
|  | ||||
|     def confirmation_params | ||||
|       params.require(:form_two_factor_confirmation).permit(:code) | ||||
|       params.require(:form_two_factor_confirmation).permit(:otp_attempt) | ||||
|     end | ||||
|  | ||||
|     def verify_otp_required | ||||
| @ -40,8 +46,8 @@ module Settings | ||||
|     end | ||||
|  | ||||
|     def acceptable_code? | ||||
|       current_user.validate_and_consume_otp!(confirmation_params[:code]) || | ||||
|         current_user.invalidate_otp_backup_code!(confirmation_params[:code]) | ||||
|       current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) || | ||||
|         current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt]) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -6,26 +6,10 @@ class SharesController < ApplicationController | ||||
|   before_action :authenticate_user! | ||||
|   before_action :set_body_classes | ||||
|  | ||||
|   def show | ||||
|     serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) | ||||
|     @initial_state_json   = serializable_resource.to_json | ||||
|   end | ||||
|   def show; end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def initial_state_params | ||||
|     text = [params[:title], params[:text], params[:url]].compact.join(' ') | ||||
|  | ||||
|     { | ||||
|       settings: Web::Setting.find_by(user: current_user)&.data || {}, | ||||
|       push_subscription: current_account.user.web_push_subscription(current_session), | ||||
|       current_account: current_account, | ||||
|       token: current_session.token, | ||||
|       admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), | ||||
|       text: text, | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def set_body_classes | ||||
|     @body_classes = 'modal-layout compose-standalone' | ||||
|   end | ||||
|  | ||||
| @ -1,24 +1,25 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class StatusesController < ApplicationController | ||||
|   include StatusControllerConcern | ||||
|   include SignatureAuthentication | ||||
|   include Authorization | ||||
|  | ||||
|   ANCESTORS_LIMIT         = 40 | ||||
|   DESCENDANTS_LIMIT       = 60 | ||||
|   DESCENDANTS_DEPTH_LIMIT = 20 | ||||
|   include AccountOwnedConcern | ||||
|  | ||||
|   layout 'public' | ||||
|  | ||||
|   before_action :set_account | ||||
|   before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? } | ||||
|   before_action :set_status | ||||
|   before_action :set_instance_presenter | ||||
|   before_action :set_link_headers | ||||
|   before_action :check_account_suspension | ||||
|   before_action :redirect_to_original, only: [:show] | ||||
|   before_action :set_referrer_policy_header, only: [:show] | ||||
|   before_action :redirect_to_original, only: :show | ||||
|   before_action :set_referrer_policy_header, only: :show | ||||
|   before_action :set_cache_headers | ||||
|   before_action :set_replies, only: [:replies] | ||||
|   before_action :set_body_classes | ||||
|   before_action :set_autoplay, only: :embed | ||||
|  | ||||
|   skip_around_action :set_locale, if: -> { request.format == :json } | ||||
|   skip_before_action :require_functional!, only: [:show, :embed] | ||||
|  | ||||
|   content_security_policy only: :embed do |p| | ||||
|     p.frame_ancestors(false) | ||||
| @ -28,27 +29,20 @@ class StatusesController < ApplicationController | ||||
|     respond_to do |format| | ||||
|       format.html do | ||||
|         expires_in 10.seconds, public: true if current_account.nil? | ||||
|  | ||||
|         @body_classes = 'with-modals' | ||||
|  | ||||
|         set_ancestors | ||||
|         set_descendants | ||||
|  | ||||
|         render 'stream_entries/show' | ||||
|       end | ||||
|  | ||||
|       format.json do | ||||
|         render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do | ||||
|           ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) | ||||
|         end | ||||
|         expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? | ||||
|         render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def activity | ||||
|     render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do | ||||
|       ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) | ||||
|     end | ||||
|     expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? | ||||
|     render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter | ||||
|   end | ||||
|  | ||||
|   def embed | ||||
| @ -56,130 +50,24 @@ class StatusesController < ApplicationController | ||||
|  | ||||
|     expires_in 180, public: true | ||||
|     response.headers['X-Frame-Options'] = 'ALLOWALL' | ||||
|     @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) | ||||
|  | ||||
|     render 'stream_entries/embed', layout: 'embedded' | ||||
|   end | ||||
|  | ||||
|   def replies | ||||
|     render json: replies_collection_presenter, | ||||
|            serializer: ActivityPub::CollectionSerializer, | ||||
|            adapter: ActivityPub::Adapter, | ||||
|            content_type: 'application/activity+json', | ||||
|            skip_activities: true | ||||
|     render layout: 'embedded' | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def replies_collection_presenter | ||||
|     page = ActivityPub::CollectionPresenter.new( | ||||
|       id: replies_account_status_url(@account, @status, page_params), | ||||
|       type: :unordered, | ||||
|       part_of: replies_account_status_url(@account, @status), | ||||
|       next: next_page, | ||||
|       items: @replies.map { |status| status.local ? status : status.id } | ||||
|     ) | ||||
|     if page_requested? | ||||
|       page | ||||
|     else | ||||
|       ActivityPub::CollectionPresenter.new( | ||||
|         id: replies_account_status_url(@account, @status), | ||||
|         type: :unordered, | ||||
|         first: page | ||||
|       ) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def create_descendant_thread(starting_depth, statuses) | ||||
|     depth = starting_depth + statuses.size | ||||
|     if depth < DESCENDANTS_DEPTH_LIMIT | ||||
|       { statuses: statuses, starting_depth: starting_depth } | ||||
|     else | ||||
|       next_status = statuses.pop | ||||
|       { statuses: statuses, starting_depth: starting_depth, next_status: next_status } | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def set_account | ||||
|     @account = Account.find_local!(params[:account_username]) | ||||
|   end | ||||
|  | ||||
|   def set_ancestors | ||||
|     @ancestors     = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : [] | ||||
|     @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift | ||||
|   end | ||||
|  | ||||
|   def set_descendants | ||||
|     @max_descendant_thread_id   = params[:max_descendant_thread_id]&.to_i | ||||
|     @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i | ||||
|  | ||||
|     descendants = cache_collection( | ||||
|       @status.descendants( | ||||
|         DESCENDANTS_LIMIT, | ||||
|         current_account, | ||||
|         @max_descendant_thread_id, | ||||
|         @since_descendant_thread_id, | ||||
|         DESCENDANTS_DEPTH_LIMIT | ||||
|       ), | ||||
|       Status | ||||
|     ) | ||||
|  | ||||
|     @descendant_threads = [] | ||||
|  | ||||
|     if descendants.present? | ||||
|       statuses       = [descendants.first] | ||||
|       starting_depth = 0 | ||||
|  | ||||
|       descendants.drop(1).each_with_index do |descendant, index| | ||||
|         if descendants[index].id == descendant.in_reply_to_id | ||||
|           statuses << descendant | ||||
|         else | ||||
|           @descendant_threads << create_descendant_thread(starting_depth, statuses) | ||||
|  | ||||
|           # The thread is broken, assume it's a reply to the root status | ||||
|           starting_depth = 0 | ||||
|  | ||||
|           # ... unless we can find its ancestor in one of the already-processed threads | ||||
|           @descendant_threads.reverse_each do |descendant_thread| | ||||
|             statuses = descendant_thread[:statuses] | ||||
|  | ||||
|             index = statuses.find_index do |thread_status| | ||||
|               thread_status.id == descendant.in_reply_to_id | ||||
|             end | ||||
|  | ||||
|             if index.present? | ||||
|               starting_depth = descendant_thread[:starting_depth] + index + 1 | ||||
|               break | ||||
|             end | ||||
|           end | ||||
|  | ||||
|           statuses = [descendant] | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       @descendant_threads << create_descendant_thread(starting_depth, statuses) | ||||
|     end | ||||
|  | ||||
|     @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT | ||||
|   def set_body_classes | ||||
|     @body_classes = 'with-modals' | ||||
|   end | ||||
|  | ||||
|   def set_link_headers | ||||
|     response.headers['Link'] = LinkHeader.new( | ||||
|       [ | ||||
|         [account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]], | ||||
|         [ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]], | ||||
|       ] | ||||
|     ) | ||||
|     response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]) | ||||
|   end | ||||
|  | ||||
|   def set_status | ||||
|     @status       = @account.statuses.find(params[:id]) | ||||
|     @stream_entry = @status.stream_entry | ||||
|     @type         = @stream_entry.activity_type.downcase | ||||
|  | ||||
|     @status = @account.statuses.find(params[:id]) | ||||
|     authorize @status, :show? | ||||
|   rescue Mastodon::NotPermittedError | ||||
|     # Reraise in order to get a 404 | ||||
|     raise ActiveRecord::RecordNotFound | ||||
|   end | ||||
|  | ||||
| @ -187,39 +75,15 @@ class StatusesController < ApplicationController | ||||
|     @instance_presenter = InstancePresenter.new | ||||
|   end | ||||
|  | ||||
|   def check_account_suspension | ||||
|     gone if @account.suspended? | ||||
|   end | ||||
|  | ||||
|   def redirect_to_original | ||||
|     redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog? | ||||
|     redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? | ||||
|   end | ||||
|  | ||||
|   def set_referrer_policy_header | ||||
|     return if @status.public_visibility? || @status.unlisted_visibility? | ||||
|     response.headers['Referrer-Policy'] = 'origin' | ||||
|     response.headers['Referrer-Policy'] = 'origin' unless @status.distributable? | ||||
|   end | ||||
|  | ||||
|   def page_requested? | ||||
|     params[:page] == 'true' | ||||
|   end | ||||
|  | ||||
|   def set_replies | ||||
|     @replies = page_params[: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 next_page | ||||
|     last_reply = @replies.last | ||||
|     return if last_reply.nil? | ||||
|     same_account = last_reply.account_id == @account.id | ||||
|     return unless same_account || @replies.size == DESCENDANTS_LIMIT | ||||
|     same_account = false unless @replies.size == DESCENDANTS_LIMIT | ||||
|     replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account) | ||||
|   end | ||||
|  | ||||
|   def page_params | ||||
|     { page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact | ||||
|   def set_autoplay | ||||
|     @autoplay = truthy_param?(:autoplay) | ||||
|   end | ||||
| end | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user